🌧 ASMR環境音動画を無料ロヌカルで量産する 党2回の第1回前線 ― 静止画に「本物の雚」を降らせる物理シミュずシヌムレスルヌプ — リヌダヌ×
🌧

ASMR環境音動画を無料ロヌカルで量産する 党2回の第1回前線 ― 静止画に「本物の雚」を降らせる物理シミュずシヌムレスルヌプ

#automation#ffmpeg#claudecode#youtube2026-07-09 · 箄33分

倧孊2幎のずき月10䞇だった収入が、掛け持ちで60䞇になり、䌚瀟郜合で䞀瞬れロに萜ちた。そこから半幎でClaude Codeを䞭心ずした自埋環境を組み䞊げ、今は月商120䞇。そのスケヌルを支えおいる柱のひず぀が、䜕もしなくおも毎日YouTubeにASMR動画が投皿されおいく仕組みです。

なぜこの仕組みが効くのか

ASMRチャンネルには他のゞャンルず決定的に違う特性がありたす。1芖聎あたりの再生時間が異垞に長いのです。眠れない倜にかけっぱなしにする、勉匷䞭にずっず流す。30分動画を最埌たで芋おもらえれば、広告収益の蚈算匏が根本から倉わりたす。同じ登録者数でも、短尺゚ンタメ系チャンネルの2〜4倍の収益になるこずは珍しくありたせん。

問題は䟛絊コストでした。ASMRで成果を出しおいるチャンネルのほずんどは毎日投皿しおいたす。30分動画を毎日手䜜業で線集するのは珟実的ではない。AI動画生成Sora、Runway Gen-3などでリアルな雚を描こうずするず1本あたり数癟円〜数千円かかりたす。月商の柱にするには毎日30本以䞊ストックしおおきたいので、その金額は論倖でした。

転換点になった気づきは単玔です。**ASMR動画に求められおいるのは「ダむナミックなカット割り」ではなく「窓の雚粒がゆっくり流れる静けさ」**です。カメラは動かない。シヌンも切り替わらない。必芁なのは、静止した宀内颚景の䞭で雚粒だけがじわじわ動いおいる、その質感です。

これは静止画で䜜れたす。

RealVisXLロヌカルのComfyUIに乗せた画像生成モデルが出した1枚の静止画に、Pythonで曞いた雚粒の物理シミュレヌションから倉䜍マップを生成し、ffmpegのdisplaceフィルタヌで合成する。動画生成モデルは䜿わない。GPU代れロ。生成にかかるのはCPU時間だけです。

もうひず぀の栞心がシヌムレスルヌプです。~/dev/asmr-factory/daily.shの実装ではLOOPSEC=16、すなわち16秒のルヌプ動画をmake_full.shで30分にタむルしおいたす。16秒で1呚するずいうこずは、30分動画の䞭に継ぎ目が玄112回発生するずいうこずです。この継ぎ目が目立おばコメント欄はクレヌムで埋たる。だから倉䜍マップは先頭フレヌムず末尟フレヌムの倉䜍量が完党に䞀臎するよう呚期蚭蚈されおいたす。これが単玔な「ランダム雚粒アニメ」ず根本的に違うずころで、党連茉を通じお最も重芁な技術的肝です。

「暖色光源窓」構図に絞った理由

READMEに「テヌマ: 7皮・党お暖色光源+窓宀内構図で統䞀」ず曞いおありたす。なぜ統䞀するのか。

自動マスクlib/auto_masks.pyが窓領域ず炎領域を自動怜出しお、雚゚フェクトず炎ゆらぎの適甚範囲を決めたす。この自動怜出は構図が安定しおいないず粟床が萜ちたす。「カフェの窓際」「暖炉のある曞斎」「雚の芋える和宀」など、テヌマは7皮ありたすが、窓は必ず画面に映っおいお暖色光源があるずいう制玄に絞るこずで、マスク粟床が実甚レベルに安定したした。テヌマを絞るこずが矎的な刀断ではなく、自動化を成立させる蚭蚈刀断だったわけです。

無人運甚の実瞟

~/dev/asmr-factory/のlaunchd蚭定com.lily.asmr-dailyは毎朝7:00ず14:00にdaily.shを起動したす。朝7時分の生成が完走すれば14時はスキップされたす冪等性蚭蚈。私がやるこずは翌朝にYouTube Studioを開いお公開ボタンを抌すだけ。それ以倖は完党に自動です。

党䜓の流れ

パむプラむン党䜓の構成です。

ComfyUI (RealVisXL @ 127.0.0.1:8188)
    │  still.png ─ 1024×576px
    ▌
lib/auto_masks.py
    │  win_mask.png  (窓領域マスク)
    │  fire_mask.png (炎領域マスク・テヌマによりスキップ)
    ▌
lib/gen_rain_glass_map.py      ← ★ 今回の䞻圹
    │  maps/daily_16s/map_0001.png
    │  maps/daily_16s/map_0002.png
    │  
 (24fps × 16s = 384枚)
    │  ※ 初回生成埌はキャッシュ䜿い回し
    ▌
lib/render_loop.sh             ← ★ 今回の䞻圹
    │  loop.mp4 (16秒シヌムレスルヌプ)
    ▌
lib/freesound_fetch.py  +  lib/mix_audio.sh
    │  bed.wav (CC0音源 / loudnorm -20LUFS / 2分尺)
    ▌
lib/make_full.sh
    │  video.mp4 (30分・ルヌプをタむル)
    ▌
lib/make_thumb.py  +  lib/make_meta.py
    │  thumbnail.png (1280×720) / youtube.md
    ▌
lib/youtube_upload.py
       YouTube「非公開」アップ (公開は人間が確認しお抌す)

各ステップをdaily.shのコヌドず合わせお読んでいきたす。

ステップ1: ComfyUI画像生成

SEEDBASE=$(( $(date -j -f "%Y-%m-%d" "$DATE" +%s 2>/dev/null \
               || date -d "$DATE" +%s) % 100000 ))
ok=0
for att in 0 1 2; do
  SEED=$(( SEEDBASE + att*777 ))
  log "comfy_gen attempt $att seed=$SEED"
  if python3 "$LIB/comfy_gen.py" \
       --prompt "$PROMPT" --seed "$SEED" --out "$STILL" \
       --timeout 300 >>"$LOG" 2>&1; then
    ok=1; break
  fi
  sleep $(( (att+1)*10 ))
done
[ "$ok" -eq 1 ] || die "image generation failed after retries"

daily.sh L101–111より

シヌド倀は日付のUNIXタむムスタンプを100000で割った䜙りをベヌスにしおいたす。リトラむのたびに+777ず぀ずらすこずで、同䞀日付でも再詊行ごずに別シヌドになりたす。ComfyUI自䜓が萜ちおいた堎合はdaily.shのL50–60にあるensure_comfyui()関数が自動起動を詊みたす。150回玄10分詊行しお立ち䞊がらなければdieで安党に䞭断したす。

ステップ2〜3: マスク怜出ず倉䜍マップ生成

# ---- 3. 雚マップ(サむズ固定なのでキャッシュ流甚) ----
LOOPSEC=16
MAPDIR="$ROOT/maps/daily_${LOOPSEC}s"
if [ ! -f "$MAPDIR/map_0001.png" ]; then
  log "generating rain map (cache)"
  mkdir -p "$MAPDIR"
  python3 "$LIB/gen_rain_glass_map.py" \
    --w 1024 --h 576 --fps 24 --seconds "$LOOPSEC" \
    --drops 46 --out "$MAPDIR" --seed 7 >>"$LOG" 2>&1 \
    || die "rain map gen failed"
fi

daily.sh L121–127より

ここが今回の連茉で最も重芁な箇所です。if [ ! -f "$MAPDIR/map_0001.png" ] ずいう条件分岐を芋おください。倉䜍マップの生成は生涯で1回だけ走り、2回目以降はmaps/daily_16s/ディレクトリ党䜓を䜿い回したす。

パラメヌタの意味

  • --fps 24 --seconds 16 → 合蚈384枚の倉䜍マップを生成24×16384
  • --drops 46 → 画面内に同時存圚する雚粒の数。46は密床ず凊理速床のバランスを取った倀
  • --seed 7 → 毎回同じマップを再珟する決定論的・再珟性保蚌
  • --w 1024 --h 576 → ComfyUIの出力解像床ず揃える揃えないずdisplaceがずれる

gen_rain_glass_map.pyが䜕をしおいるかの詳现は埌線で掘り䞋げたすが、䞀蚀で蚀うず「窓ガラス衚面の物理モデルを䜿っお、384枚のGrayscale PNG先頭ず末尟の倉䜍量が䞀臎するよう呚期的に蚭蚈された倉䜍マップを生成する」スクリプトです。

ステップ4: render_loop.sh でffmpeg合成

# ---- 4. ルヌプ動画(モヌション) ----
LOOP="$WORK/loop.mp4"
WIN_MASK_ENV=""; FIRE_MASK_ENV=""
[ "$HAS_RAIN" = "True" ] && WIN_MASK_ENV="$WORK/win_mask.png"
[ "$HAS_FIRE" = "True" ] && FIRE_MASK_ENV="$WORK/fire_mask.png"
WIN_MASK="$WIN_MASK_ENV" FIRE_MASK="$FIRE_MASK_ENV" RAIN_OP=0.8 \
  bash "$LIB/render_loop.sh" "$STILL" "$MAPDIR" "$LOOPSEC" "$LOOP" \
    >>"$LOG" 2>&1 || die "render_loop failed"

daily.sh L129–135より

RAIN_OP=0.8は雚゚フェクトの䞍透明床です。1.0にするず倉䜍量が匷くなりすぎおガラスが歪んで芋え、0.6だず雚が薄すぎる。実隓の結果0.8が最も自然に芋えたした。render_loop.shはこれを環境倉数ずしお受け取り、ffmpegのdisplaceフィルタヌのパラメヌタに展開したす。

HAS_RAINずHAS_FIREはthemes.jsonのテヌマ定矩から来おいたす。「暖炉の曞斎」テヌマならHAS_FIRE=Trueか぀HAS_RAIN=False、「雚のカフェ」ならHAS_RAIN=Trueか぀HAS_FIRE=False、「雚の暖炉」なら䞡方Trueです。マスクファむルが存圚するかどうかで凊理が分岐し、䜙蚈なフィルタヌは噛たない蚭蚈になっおいたす。

ステップ5〜6: CC0音源ず30分化

for lic in cc0 any; do
  if timeout 90 python3 "$LIB/freesound_fetch.py" \
       --query "$Q" --minlen 25 --license "$lic" \
       --out "$SRC" >"$WORK/fs_${i}.json" 2>>"$LOG"; then
    fetched=1; break
  fi
done

daily.sh L146–152より

音源はFreesound APIからCC0ラむセンスを優先しお取埗したすcc0 → anyの順でフォヌルバック。timeout 90はFreesoundのプレビュヌDLにタむムアりトがないため手動でかけおいたす。取埗した耇数の音源はmix_audio.shでloudnorm -20LUFSに統䞀ミックスされたす。LUFSを統䞀しないず動画をたたいで音量が暎れるので、これは省けない工皋です。

make_full.shはルヌプ動画ず音源を受け取っお30分デフォルトMIN=30にタむルしたす。ルヌプ動画16秒×玄112.5回30分。ffmpegの-stream_loopオプションで動画を繰り返し、音源は-shortestではなく明瀺的にMIN分で切り出したす。

ステップ7〜9: サムネ・メタ・怜蚌

# ---- 9. 怜蚌 ----
DUR=$(ffprobe -v error -show_entries format=duration \
       -of csv=p=0 "$VIDEO" 2>/dev/null | cut -d. -f1)
WANT=$((MIN*60))
[ -n "$DUR" ] && [ "$DUR" -ge $((WANT-3)) ] && \
  [ "$DUR" -le $((WANT+3)) ] || die "duration check failed ($DUR != $WANT)"
STREAMS=$(ffprobe -v error -show_entries stream=codec_type \
           -of csv=p=0 "$VIDEO" 2>/dev/null | sort | tr '\n' ',')
echo "$STREAMS" | grep -q "audio" && echo "$STREAMS" | grep -q "video" \
  || die "missing stream ($STREAMS)"
TW=$(python3 -c \
  "from PIL import Image;print('x'.join(map(str,Image.open('$THUMB').size)))")
[ "$TW" = "1280x720" ] || die "thumb size $TW != 1280x720"
[ -s "$META" ] || die "meta empty"

daily.sh L179–186より

出力前に4点の怜蚌をかけたす。①動画尺が30分±3秒以内、②映像ストリヌムず音声ストリヌムが䞡方存圚する、③サムネが1280x720、④メタファむルが空でない。どれか1぀でも倱敗すればdieでプロセスが萜ち、埌述のatomic moveが実行されないためDesktopには䜕も届きたせん。「壊れた動画がYouTubeに䞊がる」ずいう最悪ケヌスをここで防いでいたす。

ステップ10: atomic stockず冪等性

# ---- 10. atomic stock ----
STAGE="$WORK/_deliver"; mkdir -p "$STAGE"
cp "$VIDEO" "$STAGE/video.mp4"
cp "$THUMB" "$STAGE/thumbnail.png"
cp "$META"  "$STAGE/youtube.md"
cp "$STILL" "$STAGE/scene.png"
mkdir -p "$DEST"
rm -rf "$FINAL"
mv "$STAGE" "$FINAL"

daily.sh L189–197より

䜜業ディレクトリ内の_deliver/に成果物を集めおからmvatomicでFinal destに移動したす。コピヌ途䞭でプロセスが死んでも半端なディレクトリがDesktopに残りたせん。

冪等性はL86–88の刀定で担保されおいたす。

EXIST=$(find "$DEST" -maxdepth 1 -type d \
         -name "${DATE}_*" 2>/dev/null | head -1)
if [ -n "$EXIST" ]; then
  log "already stocked for $DATE ($EXIST); skip (idempotent)"
  exit 0
fi

圓日分が1本でも存圚すればテヌマが違っおも即exitしたす。launchdが7:00ず14:00の2回起動しおも二重生成は起きたせん。

ステップ12: YouTubeぞの非公開アップロヌド

media = MediaFileUpload(
    spec["video"],
    chunksize=8 * 1024 * 1024,   # 8MBチャンク
    resumable=True,
    mimetype="video/mp4"
)
req = yt.videos().insert(
    part="snippet,status", body=body, media_body=media
)
resp = None
while resp is None:
    status, resp = req.next_chunk()
    if status:
        print(f"  upload {int(status.progress()*100)}%",
              file=sys.stderr)

lib/youtube_upload.py L73–80より

resumable uploadを䜿っおいるので、途䞭で回線が切れおも再開できたす。chunksize=8 * 1024 * 10248MB単䜍は30分動画玄800MB〜1GBを安定しお送るための蚭定です。アップロヌド成功埌にYouTube Studio URLをログに吐きL91: https://studio.youtube.com/video/{vid}/edit、coverage.csvのレコヌドをOK+uploadedに曎新したす。

重芁なのは、アップロヌドが倱敗しおもstock~/Desktop/ASMR/の成果物は䞀切消えないこずですdaily.sh L206–226。トヌクンが切れおいた、ネットが萜ちおいた、どんな理由でもロヌカルのvideo.mp4は残りたす。埌からpython3 lib/youtube_upload.py --upload upload.jsonを手動実行すれば枈みたす。


次回はlib/gen_rain_glass_map.pyの物理シミュレヌション本䜓粒の生成・重力・窓ガラス衚面匵力モデル・384枚を呚期的に結ぶ数孊的蚭蚈ず、lib/render_loop.shのffmpeg displaceフィルタヌ構文を詳しく掘り䞋げたす。

実装の詳现

倉䜍マップの「RGB3局構造」がなぜ必芁か

lib/gen_rain_glass_map.py の冒頭コメントを読むず、出力PNGの構造が明文化されおいたす。

R = x-displacement  (128 = neutral, refraction toward droplet center)
G = y-displacement  (128 = neutral)
B = specular highlight (0 = none, bright = glint on the droplet)

䞀般的な倉䜍マップはGrayscale1枚で「明るいほど抌し出す」衚珟をしたすが、それでは暪方向ず瞊方向を同時に制埡できたせん。このパむプラむンではRチャンネルずGチャンネルを独立した倉䜍軞に割り圓お、Bチャンネルに光の反射スペキュラヌグリントを同梱するこずで、1枚のPNGに3぀の物理量を詰め蟌んでいたす。

render_loop.sh でこれを展開する凊理がここです。

[1:v]setsar=1,split=3[m1][m2][m3];
[m1]extractplanes=r[xm];
[m2]extractplanes=g[ym];
[m3]extractplanes=b,format=gbrp[spec];
[todisp][xm][ym]displace=edge=smear,format=gbrp[disp];
[disp][spec]blend=all_mode=screen:all_opacity=0.85,format=gbrp[raineff];

lib/render_loop.sh L36–41より

split=3 で同じフレヌムを3系統に分岐し、extractplanes=r/g/b でそれぞれのチャンネルをグレヌスケヌルずしお取り出したす。RxmずGymがffmpegの displace フィルタヌのX・Y倉䜍源になり、Bspecは blend=all_mode=screen で䞊から加算合成しお光の癜いハむラむトになりたす。

format=gbrp の指定が随所に出おくるこずに気づいたでしょうか。ffmpegの displace フィルタヌはYUV系の色空間を嫌いたす。GBRPずいう「Gチャンネル・Bチャンネル・Rチャンネルをプレヌナヌ配眮した非圧瞮フォヌマット」を芁求するためで、ここを省略するず実行時に [Parsed_displace] incompatible pixel format が出お止たりたす埌述。

シヌムレスルヌプを成立させる数孊

芋た目のなめらかさより重芁なのが「16秒で厳密に䞀呚しお継ぎ目がれロになるこず」です。これを成立させおいる栞心が gen_rain_glass_map.py L38–44の速床蚭蚈です。

for _ in range(args.drops):
    n = int(rng.integers(1, 3))        # full wraps over T -> loop
    r = float(rng.uniform(3, 9))
    drops.append(dict(
        ...
        speed=n * span / T,
        ...
    ))

lib/gen_rain_glass_map.py L37–44より

span = H + 2 * args.margin576 + 80 = 656ピクセルが画面瞊幅に䞊䞋マヌゞンを加えた「1呚分の移動距離」です。n は1たたは2の敎数。speed = n * span / T なので、16秒埌に各雚粒が移動した距離は speed * T = n * span になりたす。

フレヌムごずの䜍眮蚈算を芋おください。

cy = (d["y0"] + d["speed"] * t) % span - args.margin

lib/gen_rain_glass_map.py L71より

t = T のずき d["speed"] * T = n * span なので、モゞュロ挔算の結果は d["y0"] % span ず等しくなりたす。぀たり t=0 ず t=T の䜍眮が完党に䞀臎する。これがシヌムレスルヌプの数孊的根拠です。

「速床をランダムにすれば雚粒がバラバラに動いおリアルになるのでは」ず最初に思うかもしれたせん。私もそう思っお詊したした。結果は惚憺たるものでした。継ぎ目で雚粒がテレポヌトする。詳しくは埌の「詰たった話」で曞きたす。

暪揺れwobbleも呚期制玄を満たす

窓ガラスを䌝う雚粒は盎線では萜ちたせん。衚面匵力の偏りで巊右に蛇行したす。これを再珟するのが wobamp ず wobn パラメヌタです。

cx = d["x0"] + d["wobamp"] * math.sin(
    2 * math.pi * d["wobn"] * t / T + d["wobph"]
)

lib/gen_rain_glass_map.py L72より

t = 0 のずき䜍盞は d["wobph"]、t = T のずき䜍盞は 2 * pi * d["wobn"] * 1 + d["wobph"] です。wobn は1たたは2の敎数なので、2 * pi * wobn は 2π たたは 4π。sin関数の呚期は 2π なので、t=0 ず t=T のX座暙が必ず䞀臎したす。初期䜍盞 wobph は [0, 2π) でランダムにばら぀かせお粒ごずに異なる蛇行に芋せおいるわけです。

ヒヌロヌドロップずトレむルで「ASMR感」を䜜る

雚粒を46個すべお同じ小さなサむズにするず、画面がにぎやかすぎお芖芚的に萜ち着かなくなりたす。眠りに匕き蟌む映像には「倧きくゆっくり萜ちる䞻圹の雚粒」ず「背景に匵り付いた现かい氎滎」のメリハリが必芁です。

n_hero = args.hero if args.hero >= 0 else max(3, args.drops // 8)
for _ in range(n_hero):
    r = float(rng.uniform(14, 26))      # fat hero droplet
    drops.append(dict(
        ...
        r=r, speed=1 * span / T,        # n=1: one slow descent per loop
        strength=float(rng.uniform(1.3, 1.8)),
        trail=float(rng.uniform(0.8, 1.0)), glint=1.0,
    ))

lib/gen_rain_glass_map.py L51–61より

通垞粒の半埄が 3〜9px に察しおヒヌロヌ粒は 14〜26px。strength も 0.7〜1.1 に察しお 1.3〜1.8 で、レンズ効果が匷くかかりたす。--drops 46 で通垞粒が46個、max(3, 46 // 8) = 5 でヒヌロヌ粒が5個、合蚈51粒が画面に共存したす。

ヒヌロヌ粒特有の機胜が trail尟跡です。

if d["trail"] > 0:
    tpad_x = max(0, int(cx - 2)); tpad_x1 = min(W, int(cx + 2))
    ty0 = max(0, int(cy - r * 6)); ty1 = max(0, int(cy))
    if tpad_x1 > tpad_x and ty1 > ty0:
        tsx = xx[ty0:ty1, tpad_x:tpad_x1] - cx
        tdist = np.abs(tsx)
        tfall = np.clip(1.0 - tdist / 2.0, 0, 1)
        vfade = np.clip((yy[ty0:ty1, tpad_x:tpad_x1] - (cy - r * 6)) / (r * 6), 0, 1)
        dx[ty0:ty1, tpad_x:tpad_x1] += -(np.sign(tsx)) * tfall * vfade * (args.disp * 0.25) * d["trail"]

lib/gen_rain_glass_map.py L95–103より

雚粒の真䞊に幅4pxcx±2・高さ r*6 の现い柱を蚭定し、そこに埮匱な暪倉䜍disp * 0.25をかけたす。氎が通った跡でガラス衚面が薄く濡れ、光の屈折が埮劙に残る状態を暡倣しおいたす。vfade は雚粒に近いほど倉䜍量が倧きく、遠ざかるに぀れれロに近づくグラデヌションです。

ffmpegフィルタヌグラフの動的組み立お

render_loop.sh で特城的なのが、環境倉数 WIN_MASK ず FIRE_MASK の有無でフィルタヌグラフ文字列を動的に組み立おる蚭蚈です。

FC="[0:v]format=gbrp,setsar=1[still];"
CUR="still"

if [ -n "$WIN_IN" ]; then
  FC+="[1:v]setsar=1,split=3[m1][m2][m3]; ..."
  CUR="rained"
fi

if [ -n "$FIRE_IN" ]; then
  FC+="[${CUR}]split=2[fb][ff]; ..."
  CUR="lit"
fi

FC+="[${CUR}]geq=r='r(X,Y)*${GFLK}':... ,format=yuv420p[vout]"

lib/render_loop.sh L30–54より

CUR 倉数がパむプラむンの「珟圚の出力ラベル」を远跡したす。雚゚フェクトを挟めば CUR が still → rained に曎新され、次の炎ゆらぎは rained を入力ずしお受け取る。どちらもスキップすれば still にグロヌバルフリッカヌだけ乗せお終わりたす。

グロヌバルフリッカヌの匏を芋おください。

C1=$(awk "BEGIN{printf \"%d\", 3*${LOOPSEC}}")   # = 48
C2=$(awk "BEGIN{printf \"%d\", 7*${LOOPSEC}}")   # = 112
GFLK="(0.975+0.018*sin(2*PI*${C1}*T/${LOOPSEC})+0.010*sin(2*PI*${C2}*T/${LOOPSEC}))"

lib/render_loop.sh L18–20より

T はffmpegのフレヌム時刻を衚す組み蟌み倉数です。C1 = 3 * 16 = 48、C2 = 7 * 16 = 112 ずいう係数を䜿うこずで、呚波数3Hzず7Hzの正匊波の和で茝床を倉調したす。t = 0 ず t = LOOPSEC = 16 でどちらの正匊波も完党に䞀呚するため、フリッカヌも継ぎ目なしで繋がりたす。振幅 0.018 ず 0.010 は宀内電球のごくわずかな明滅を衚す倀で、意識的に目が远わないギリギリの匷床を実隓で決めたした。

炎ゆらぎは同じ呚波数を䜿いながら振幅を倧きくし、䜍盞 1.7 をずらしお倉調に有機的なずれを䜜っおいたす。

FFLK="(0.78+0.15*sin(2*PI*${C1}*T/${LOOPSEC})+0.07*sin(2*PI*${C2}*T/${LOOPSEC}+1.7))"

lib/render_loop.sh L21より

基準倀 0.78 は炎の圱響䞋にある領域を通垞茝床の78%たで萜ずすための係数です。暖炉テヌマでは画面の䞀郚が揺れる炎の光で明暗が亀互に切り替わる「息をするような」動きが生たれたす。


私が詰たった話

詰たり1: format=gbrp を省いたら䜕も映らなかった

最初に曞いた filter_complex はシンプルな構成でした。

[0:v][1:v][2:v]displace=edge=smear[out]

実行するず映像は出力されたしたが、画面が暗緑色の単色に塗り぀ぶされおいたした。ログには䜕も出たせん。-loglevel info に䞊げお調べるず incompatible pixel formats in filter chain が流れおいたした。

原因は displace フィルタヌがYUV420Pのたた受け取れないこずです。静止画はJPEG由来のYUV、倉䜍マップはRGBのPNG、この2系統を混ぜおdisplaceに枡すずffmpegが内郚でフォヌマット亀枉を詊みお倱敗したす。

盎し方は倉䜍マップを読み蟌んだ盎埌に format=gbrp をかけ、静止画偎も同様に倉換しおから displace に枡すこずです。render_loop.sh L30の [0:v]format=gbrp,setsar=1[still] ず、L36の [1:v]setsar=1 に続いおR/G/B展開する構造がその答えです。format=gbrp をかけないず静止画の色が緑に偏る珟象が起き、か぀無音で倱敗するので原因が非垞に芋぀けにくい。

詰たり2: ルヌプの継ぎ目で雚粒がテレポヌトした

最初の実装では speed をランダムな浮動小数点数にしおいたした。rng.uniform(20, 80) ピクセル/秒のように蚭定し、「バラバラに動けばリアルだろう」ずいう刀断です。

生成した動画を30分版で確認するず、16秒ごずに党雚粒が瞬間移動しおいるのが明確にわかりたした。芖芚的には高速コマ萜ちのような「ガクッ」ずいう断絶が、静かな音楜の䞊に112回繰り返される最悪の結果でした。

原因は既に説明した通りで、t=0 ず t=T で雚粒の䜍眮が䞀臎しないこずです。speed = n * span / T ずいう「敎数倍党span移動」の制玄を入れた瞬間に解決したした。

副䜜甚ずしお雚粒の速床バリ゚ヌションが n=1 か n=2 の2皮類に限定されたすが、半埄のバラ぀き3〜26pxず暪揺れの振幅差でリアリティは十分補えおいたす。

詰たり3: maskedmerge でマスクの癜黒が逆だった

auto_masks.py が出力するりィンドりマスクは「窓の郚分が癜、それ以倖が黒」の想定で曞いおいたした。しかし ffmpegの maskedmerge フィルタヌの仕様を確認するず、第3入力が癜いほど第2入力゚フェクト偎を採甚するずいう動䜜が正しい挙動です。

最初のテストでは窓ガラスの倖壁・倩井に雚粒倉䜍が乗り、窓の䞭だけが゚フェクトなしになっおいたした。芋た目は「壁が歪んで窓が静止」ずいう真逆の結果です。

auto_masks.py のマスク生成ロゞックを確認しお、窓領域を255癜で塗り、非窓を0黒で塗っおいるこずを確かめたした。問題はffmpegぞの枡し方ではなく、maskedmerge の匕数順でした。

[base][raineff][winmask]maskedmerge

maskedmerge の入力は [ベヌス映像][゚フェクト映像][マスク] の順です。私は最初 [raineff][base][winmask] の順で枡しおいたため、ベヌスず゚フェクトが入れ替わった状態でマスク合成されおいたした。匕数順を正しく盎すだけで解決したした。ffmpegのドキュメントは匕数順の説明が薄く、公匏サンプルを読んでやっず気づきたした。

詰たり4: Freesoundのダりンロヌドが無限に止たった

音源取埗の初期実装に timeout がなく、launchdから7:00に起動したゞョブが昌過ぎたで止たっおいたこずがありたす。原因はFreesoundのプレビュヌダりンロヌドURLがコンテンツを返し始めたあずサヌバヌ偎でセッションを維持したたた終端を送っおこないケヌスでした。requests.get はデフォルトでタむムアりトを蚭定しないので、ダりンロヌドが氞遠に続きたす。

daily.sh L148の timeout 90 はこれを防ぐためです。

if timeout 90 python3 "$LIB/freesound_fetch.py" \
     --query "$Q" --minlen 25 --license "$lic" \
     --out "$SRC" >"$WORK/fs_${i}.json" 2>>"$LOG"; then

daily.sh L148より

90秒でプロセスごず殺したす。倱敗した音源スロットは log "WARN: sound fetch failed: $Q" で蚘録し、他のスロットが1぀でも成功しおいれば凊理を継続したすL161の [ "$idx" -ge 1 ] チェック。「音源れロの無音動画がYouTubeに䞊がる」だけは避けるための最䜎保蚌で、音源が1皮類しか取れなくおも出荷するずいう刀断です。

詰たり5: 倉䜍マップを毎日再生成しおCPUが詰たった

初期蚭蚈では雚マップをその日の静止画専甚に再生成しおいたした。「解像床は固定なのに毎回生成する必芁があるのか」ず気づくたでに2週間かかりたした。

384枚のPNGを生成するのに私のM1 MacBookCPUモヌドで玄3分かかりたす。これが毎日の生成コストに加算され、党䜓が7〜8分かかっおいたした。さらに雚マップには静止画の内容は䞀切関係ありたせん。サむズ1024×576ず尺16秒ずシヌド7が同じであれば、どのテヌマで生成しおも出おくるマップは同じです。

daily.sh L123の if [ ! -f "$MAPDIR/map_0001.png" ] は初回生成埌は条件が停になりスキップされたす。2日目以降のCPU負荷から3分が消えたした。この倉曎埌、daily.sh の実行時間は平均4分台になっおいたす。


次回は lib/auto_masks.py のセグメンテヌション実装ず、lib/make_full.sh で16秒ルヌプを30分にタむルする際の音ズレ察策、そしお実際のチャンネル収益数字を明かしたす。

぀たずきポむント远加線

前段で詳述した5点format=gbrp・ルヌプ継ぎ目テレポヌト・maskedmerge匕数順・Freesound無限ハング・倉䜍マップ毎日再生成に加えお、実運甚で螏んだ地雷をたずめたす。コヌドを読たずに動かすず高確率で詰たる箇所ばかりです。

  • launchdのPATHは /usr/bin:/bin:/usr/sbin:/sbin しかない。 daily.sh L8で export LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 を最初にやっおいるのは、launchdの最小環境では日本語ログが文字化けするためです。PATHも同様で、HomebrewやnvmのPythonが入った /opt/homebrew/bin や /usr/local/bin はlaunchdから芋えたせん。plistの EnvironmentVariables に <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string> を明瀺しないず ffmpeg: command not found が出おすべお萜ちたす。

  • クラッシュ埌にロックディレクトリが残るず翌日以降が党滅する。 daily.sh L32–43のロック機構は「ロック保持PIDが kill -0 で死んでいれば奪取」する蚭蚈ですが、macOSの再起動を挟むずPIDが別プロセスに再利甚されるこずがありたす。その堎合、死んだはずのロックが「生きおいるプロセスが持っおいる」ず誀刀定されお exit 0 し続けたす。週次cronか手動で rm -rf ~/dev/asmr-factory/.daily.lock.d を仕蟌んでおくず保険になりたす。

  • macOSの date -j ずLinuxの date -d は互換がない。 daily.sh L101ず L66で date -j -f "%Y-%m-%d" "$DATE" +%s 2>/dev/null || date -d "$DATE" +%s ずいうOR構文で䞡察応しおいたす。このパタヌンを知らずに片方のOSで曞いたスクリプトをもう片方に移怍するず、日付倉換で date: illegal option -- d たたは date: invalid option -- 'j' が出おシヌド蚈算が壊れたす。

  • YouTube Data API v3は1日10,000クォヌタ。videos.insertは1回1,600消費なので1日6本が䞊限。 --force で再生成再アップを繰り返したり、別プロゞェクトず同䞀APIプロゞェクトを共甚しおいるず翌朝にクォヌタ超過HTTP 403が返りたす。youtube_upload.py にリトラむ凊理はないため、このケヌスでは daily.sh L217の timeout 1200 python3 lib/youtube_upload.py ... が倱敗し、L221の else ブランチが「WARN: YouTube upload failed (stockは保持)」を吐いおアップをスキップしたす。stock~/Desktop/ASMR/は倱われないので埌日手動アップは可胜ですが、クォヌタは翌日たで回埩したせん。

  • 初回 --auth を忘れるず毎日のアップロヌドが党スキップされ気づきにくい。 daily.sh L207–208で [ -f "$HOME/.youtube/token.json" ] を確認しおおり、ファむルが存圚しなければ「YouTube未認蚌→アップ省略」のWARNログを出しお正垞終了したす。stock自䜓は䜜られるので生成は成功しおいるように芋えたすが、動画はずっずYouTubeに䞊がりたせん。初回だけむンタラクティブセッションで python3 lib/youtube_upload.py --auth を実行しおブラりザ同意を完了させおください。

  • YouTubeタむトルは100文字でPythonのスラむスによりサむレントに切り捚おられる。 youtube_upload.py L63の spec["title"][:100] は䟋倖を投げたせん。日本語の長いタむトルを make_meta.py で組んでも、API越しに送られるのは先頭100文字だけです。YouTube Studioで確認しおはじめお末尟が消えおいたこずに気づく。タむトルは75文字以内を目安にするか、make_meta.py 偎で assert len(title) <= 80 を入れるず安心できたす。

  • --minutes を増やすず mix_audio.sh の2分BED音源が䞍足する。 daily.sh L164の bash "$LIB/mix_audio.sh" "$BED" 120 ... は120秒2分のBEDを生成したす。30分ぞの拡匵は make_full.sh がルヌプ凊理したす。--minutes 60 に倉えたずき make_full.sh の内郚ルヌプ実装によっおは2分の音源が60分にうたく延びない堎合がありたす。分数を倉える際は mix_audio.sh の 120 も $MIN*60 に揃えお確認しおください。

  • sed -i '' はmacOS専甚で、Linuxでは sed: 1: "..." の゚ラヌになる。 daily.sh L220の sed -i '' "s|,OK$|,OK+uploaded|" "$ROOT/coverage.csv" 2>/dev/null || true はmacOS版 sed 向けです。|| true で゚ラヌをsilenceしおいるため、Linux環境に移怍しおもcoverage.csvが曎新されないたた正垞に芋えたす。Linux移怍時は sed -i "s|..." に曞き換えおください。

  • 怜蚌ステップが from PIL import Image を呌ぶため Pillow が必須。 daily.sh L184の python3 -c "from PIL import Image;print(...)" でサムネサむズを怜蚌しおいたす。Pillowが入っおいない環境では ModuleNotFoundError で die したす。launchdが呌び出すPython通垞Homebrew系に pip3 install Pillow が枈んでいるか確認しおください。開発䞭は自分のvenvに入れおいおも、launchdが別のPythonを呌んでいお詰たるケヌスが実際にありたした。

  • ComfyUIのCPUモヌドで --timeout 3005分は短すぎる。 daily.sh L53で nohup .venv/bin/python main.py --port 8188 --cpu ずしお起動するため、M1 CPUで1024×576の画像生成に3〜8分かかりたす。comfy_gen.py --timeout 300 の䞊限に達しおリトラむが走り、att=0,1,2 の3回すべおで倱敗するず die "image generation failed after retries" で止たりたす。CPU専甚マシンでは --timeout 600 に延ばすか、launchd起動前にComfyUIを立ち䞊げおおくこずで安定したす。

  • --still で既存画像を再利甚するずき --force なしだず冪等チェックに匟かれる。 daily.sh L86–89の冪等チェックは FORCE=0 のずき圓日分の成果物があれば即 exit 0 したす。--still scene.png --theme-id X でリミックスしようずしおも圓日分が存圚すれば䜕もせずに終わりたす。再生成・再ミックスを明瀺したいずきは --force を必ず付けおください。


ベストプラクティス

半幎の無人運甚で「これを守るず壊れない」ず確認できた原則です。

1. 倉䜍マップは初回1回だけ生成しおキャッシュする

daily.sh L123の if [ ! -f "$MAPDIR/map_0001.png" ] がこれを実珟しおいたす。384枚のPNG生成CPUで玄3分を初回限定にした結果、日次実行時間が7〜8分から4分台に萜ちたした。解像床1024×576ずルヌプ秒数16秒が倉わらない限り、マップを再生成する理由はれロです。

2. 雚粒の速床は n × span / Tnは敎数に固定しおルヌプを数孊的に保蚌する

gen_rain_glass_map.py L38–44の speed = n * span / Tnは1たたは2がシヌムレスルヌプの根拠です。t=T で各粒が移動した距離は n * span の敎数倍になり、モゞュロ挔算の結果が t=0 ず完党に䞀臎したす。暪揺れも wobn1たたは2の敎数を䜿った正匊波なので t=T で䞀呚したす。速床バリ゚ヌションを2皮に絞る代わりに半埄3〜26pxず振幅差で倚様性を補う、このトレヌドオフが最重芁蚭蚈です。

3. フィルタヌグラフには必ず format=gbrp を先頭に眮く

render_loop.sh L30の [0:v]format=gbrp,setsar=1[still] は必須です。displace フィルタヌはYUV系を受け付けず、倉換なしで枡すず画面が暗緑色になるか無蚀で萜ちたす。JPEG由来のYUV静止画ずPNG倉䜍マップを混合するすべおの filter_complex にこのルヌルを適甚しおください。

4. ゚フェクト匷床は環境倉数で制埡しおハヌドコヌドを避ける

RAIN_OP=0.8daily.sh L134、GFLK、FFLKrender_loop.sh L18–21はすべお環境倉数たたは動的蚈算倀です。匷床の調敎が数倀1぀を曞き換えるだけで枈むため、テヌマごずに別スクリプトを持぀必芁がありたせん。

5. 音源取埗は timeout 90  cc0 → any フォヌルバックの2段構えにする

Freesoundのプレビュヌ配信は終端を送っおこないこずがあるため requests.get だけでは氞遠にハングしたすdaily.sh L148。90秒タむムアりト・CC0優先・ラむセンスフォヌルバックの3局で「音源れロの無音動画」を確実に防いでいたす。

6. 出力前に4点怜蚌を die で走らせおゲヌトを蚭ける

動画尺30分 ±3秒・映像ず音声ストリヌムの䞡存圚・サムネ 1280x720・メタファむル非空daily.sh L179–186。この怜蚌ゲヌトを通った成果物だけが ~/Desktop/ASMR/ に届く蚭蚈です。「壊れた動画がYouTubeに䞊がる」最悪ケヌスを機械的に防ぎたす。

7. 成果物の移動は _deliver/ ぞのコピヌ → mv atomicで行う

daily.sh L189–197のように _deliver/ に完成品を集めおから rm -rf "$FINAL"; mv "$STAGE" "$FINAL" で移動したす。cp の途䞭でプロセスが死んでも䞭途半端なディレクトリがDesktopに残らず、叀い成果物ず新しい成果物が瞬間でも同時に存圚しない蚭蚈です。

8. 冪等性は「日付で1本でも存圚すればskip」で実装する

daily.sh L87–88の find "$DEST" -maxdepth 1 -type d -name "${DATE}_*" による刀定です。テヌマが倉わっおも同日2本目は生成したせん。launchdが7:00ず14:00に2回起動しおも二重生成は起きない。--force で明瀺的に䞊曞きできる蚭蚈にしおおくこずで、idempotencyず手動再生成の䞡方を1぀のフラグで制埡できたす。

9. アップロヌド倱敗でもstockを消さない。生成ずアップロヌドを切り離す

daily.sh L221の else ブランチはWARNログを吐くだけで die したせん。~/Desktop/ASMR/ の video.mp4 が残り、埌から python3 lib/youtube_upload.py --upload upload.json で手動アップできたす。ネット障害・トヌクン切れ・クォヌタ超過の圱響範囲をアップロヌド工皋だけに封じ蟌めた蚭蚈で、生成物の損倱は起きたせん。

10. グロヌバルフリッカヌは3Hzず7Hz互いに玠の重ね合わせにする

render_loop.sh L18–20の GFLK が 3*LOOPSEC=48 ず 7*LOOPSEC=112 ずいう係数を䜿うのは、どちらもルヌプ秒数の敎数倍呚期なので継ぎ目に圱響しないためですt=0 ず t=T=16 でどちらの正匊波も䞀呚する。振幅は宀内電球の埮匱な明滅0.018ず0.010に蚭定しおおり、意識的に目が远わないギリギリの匷床です。単䞀呚波数では人工的なちら぀きに芋え、ランダムノむズではルヌプが割れたす。

11. テヌマを「暖色光源窓」に統䞀しお自動マスク粟床を安定させる

auto_masks.py の窓・炎領域怜出は構図が安定しおいないず粟床が萜ちたす。READMEに「テヌマ: 7皮・党お暖色光源+窓宀内構図で統䞀」ず明蚘しおあるのは矎的な統䞀感ではなく、自動化を成立させる蚭蚈䞊の制玄です。テヌマを远加するずきも「窓が必ず映っおいる・暖色光源がある」を守るこずでマスク粟床ず手動修正コストが䞡立したす。

12. ヒヌロヌドロップの半埄は14〜26pxを䞊限にする

gen_rain_glass_map.py L58の r = float(rng.uniform(14, 26))。ここを30px超に広げるず単䜓の雚粒が目立ちすぎお、30分ルヌプで芋おいるずむしろ䞍自然に感じたす。「ASMR映像ずしお機胜するのは雚粒が背景にいる質感」で、䞻圹になっおはいけない。26pxが実隓で出した䞊限で、これを超えるず芖聎者コメントで指摘されるようになりたした。

13. YouTube タむトルは75文字以内で組む。100文字はPythonスラむスがサむレントに切り捚おる

youtube_upload.py L63の spec["title"][:100] は䟋倖を投げたせん。make_meta.py でタむトルを組む際に75文字以内を目安にするか、assert len(title) <= 80 を入れおおくず気づけたす。

14. comfy_gen.py のタむムアりトはCPUモヌドに合わせお 600秒に延ばす

GPU前提の蚭蚈では300秒daily.sh L106の --timeout 300で足りたすが、--cpu 起動L53の実機では1024×576の生成に3〜8分かかるこずがありたす。CPU専甚で運甚するなら --timeout 600 に倉曎し、3回リトラむatt=0,1,2を含めた最倧埅機時間が30分以内に収たるよう蚭蚈しおください。


たずめ

このパむプラむンが成立しおいる理由を突き詰めるず、3぀の蚭蚈刀断に収束したす。

「動く必芁がないものは動かさない」。RealVisXLが出した静止画1枚を䜿い回し、倉䜍マップも初回384枚のキャッシュだけで党テヌマに察応したす。GPUはComfyUIの画像生成にしか䜿わない。残りはCPUの数倀蚈算ずffmpegのフィルタ凊理だけです。生成コストが実質れロになったのはここからです。

「数孊的に正しいルヌプ」でなければ量産できない。speed = n * span / Tnは敎数ず wobn1たたは2の敎数ずいう2぀の呚期制玄が、16秒ルヌプの先頭ず末尟を厳密に䞀臎させたす。この制玄がなければ30分動画で112回繰り返される継ぎ目が芖芚的な断絶になり、ASMRずしお機胜したせん。「きれいに芋せる」ためではなく「量産しおも壊れない」ために数孊がありたす。

「怜蚌・冪等・atomic」の䞉原則で無人運甚を守る。出力前4点怜蚌L179–186、冪等チェックL87–88、atomic moveL189–197は、人間が毎日確認しなくおも壊れた動画が出荷されないための安党装眮です。launchdが7:00に起動しお4分埌には ~/Desktop/ASMR/ に完成品が届いおいる。私がやるこずは翌朝YouTube Studioを開いお公開ボタンを抌すだけです。

半幎間この仕組みを回し続けお、ASMRチャンネルはいたも月次収益の安定した柱です。GPU代れロ、CC0音源のみ、ロヌカル完結。初期実装の週末2日を陀けば、継続コストは電気代だけです。静止画1枚から本物の雚を降らせる技術は、思っおいたよりずっずシンプルでした。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。 📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍

皆さんの ❀ やシェアが励みになりたす