ローポリPS1ショート動画
全てはコードで実現:Claudeがショート動画ごとに1つのBlenderスクリプトを作成し、ユーザーのマシン上でレンダリング、フレームを確認して修正し、音声とキャプションを構築します。動画生成AIは使用しません。
examples/lift-rocket/(「もしエレベーターがロケットのように速かったら?」34秒)で実績あり。新たなショート動画を作成する前に、この例をテンプレートとして読んでください。
要件(まず確認し、不足しているものをインストール)
- Blender 4.2以上(5.3でテスト済み)。場所:
PATH上のblender、またはC:\Program Files\Blender Foundation\<ver>\blender.exe//Applications/Blender.app/Contents/MacOS/Blender。 - Python 3.10以上、
numpy pillow soundfile faster-whisper、PATH上のffmpeg。 - キャラクター:Quaternius「Ultimate Modular Men」(CC0、無料、quaternius.com)。
assets/quaternius-modular-men/に解凍するか、LOWPOLY_PACKをそのIndividual Characters/Blendsフォルダに設定します。 - TTS音声(ElevenLabsまたはユーザーが持っている任意のツール)。オプション:サウンドチェック用の
earshotMCP。
パイプライン(ショート動画ごとに1つのフォルダ、examples/lift-rocket/をコピーして開始)
| # | ステップ | ファイル/コマンド | 完了条件 |
|---|---|---|---|
| 1 | 脚本 | script.md | フックとなる質問 + 結果の連鎖(それぞれが前より悪く、より面白い) + 実際の数字 + ドライなオチ。30~40秒で60~110ワード。すべての数字は情報源で確認し、Factsリストに記載。 |
| 2 | 音声 | vo/takeA.mp3 | 2テイク録音し、ワードタイミングがきれいな方を選択。落ち着いた声ではなく、エネルギッシュなクリエイターの声(落ち着いた声は「退屈」とフィードバックされたため)。 |
| 3 | ワードタイミング | python ../../scripts/words.py vo/takeA.mp3 | takeA_words.jsonに長さ0のワードがない。 |
| 4 | ショットリスト | script.md内のテーブル | ナレーションのフレーズごとに1ショット、1~3秒ごとに新しい絵、各ショットがワードタイムにキー設定されている。 |
| 5 | シーン | build.py(Blender) | blender -b -P build.py -- frames --still 30,120,... でスチルをレンダリング。フルレンダリング前にすべてのスチルを確認。 |
| 6 | プレビュー+ノート | python ../../scripts/preview.py . render 次に serve | ユーザーに未処理のノートがない(プレビューループを参照)。 |
| 7 | レンダリング | blender -b -P build.py -- frames | 360x640 EEVEE PNG、f_0001.png... |
| 8 | サウンド | python build_mix.py | 効果音をピクチャーフレームに配置、上にナレーション、下にローファイBGM、ダッキング。mix_raw.wav + cues.jsonを出力。 |
| 9 | ポスト | python post.py | PS1ディザ、エフェクト、ワードハイライトキャプション、統計数値の表示 -> 1080x1920でout/に出力。 |
| 10 | 合成 | 以下を参照 | short.mp4、その後測定と同期確認。 |
フレームルール(全般):24 fps、フレーム1 = t 0、F = lambda t: int(round(t * 24)) + 1。
合成:
ffmpeg -y -framerate 24 -i out/f_%04d.png -i mix_raw.wav -c:v libx264 -crf 16 -pix_fmt yuv420p \
-af "loudnorm=I=-15:TP=-1.5:LRA=11,aresample=48000" -c:a aac -b:a 192k -shortest short.mp4
映像を再編集せずに音声を後で差し替える場合:
python ../../scripts/words.py vo/takeB.mp3 を実行し、次に python ../../scripts/align.py vo/takeA_words.json vo/takeB.mp3 --end 34(--end = ナレーションBGMが止まる場所、出力はend + 2秒の長さ。指定しない場合:最後のワード + 1.5秒となり、映像よりも短いファイルになる)
-> takeB_aligned.wav + takeB_aligned_words.json が出力される。build_mix.py(VO環境変数)とpost.pyをこれらに向ける。
プレビューループ(フルレンダリング前)
ブラウザで高速低解像度プレビューを行い、ユーザーがタイムラインにノートを残し、Claudeが修正して内容の変わったショットのみを再レンダリングします。ショット = build.py内のカメラマーカー。
| 操作 | コマンド(ショート動画のフォルダから実行) |
|---|---|
| プレビューをレンダリング | python ../../scripts/preview.py . render(初回:全ショット、以降:変更されたショットのみ) |
| ノートページを開く | python ../../scripts/preview.py . serve -> http://127.0.0.1:8765 |
| ノートを読む | python ../../scripts/preview.py . notes(ショットごとにグループ化、フレーム番号付き) |
| 修正後 | build.pyを編集し、renderを再実行、次にpython ../../scripts/preview.py . done 3 5 --reply "変更した内容" |
| 特定のショットを強制レンダリング | render --shots c_press,c_slam または render --all |
build.pyは--blend-only(シーンを構築して保存、レンダリングしない)を受け入れなければなりません。例を参照。- 変更検出:プレビューフレームごとにフィンガープリント(アクティブカメラ、表示されている全オブジェクトのトランスフォーム、アーマチュアポーズ、フェイススワップ、マテリアル、ライト、メッシュデータ)を取得します。ショットは、そのフレームのいずれかが変更された場合のみ再レンダリングされます。変更されていないショットは古いフレームを保持します。
preview.jsonの設定(オプション):scale50(%)、step2(2フレームごと)、samples1、audio(デフォルトはmix_raw.wav、それ以外はVO)、final= フルパイプラインのコマンド。ページの「承認して最終レンダリング」ボタンがそれらを実行します。notesはユーザーがクリックしたときにAPPROVEDと表示します。- ノートを一つずつ修正し、それぞれに返答し、再レンダリングし、ユーザーに「何もリフレッシュする必要はありません」と伝えてください。ページは自動的にビデオをリロードします。同じ種類のノートが繰り返されると、このファイル内のルールになります。
- lift-rocketでの測定結果(RTX 3070 Tiラップトップ、820フレーム):初回プレビュー76秒、1ショット変更13秒、変更なし12秒。フレームのレンダリング時間のほとんどは固定オーバーヘッドであるため、解像度を下げるよりもレンダリングするフレーム数を減らす方が効果的です。
キットAPI(scripts/lowpoly_kit.py、Blender内部でインポート)
load_character(scn, "Suit")->(armature, meshes, actions)。キャラクター:Adventurer, Beach, Casual_2, Casual_Hoodie, Farmer, King, Punk, Spacesuit, Suit, Swat, Worker。- アクション(その場で、24):Death, HitRecieve, HitRecieve_2, Idle, Idle_Neutral, Interact, Kick_Left/Right, Punch_Left/Right, Roll, Run, Run_Back/Left/Right, Walk, Wave, Gun_Shoot, Idle_Gun*, Idle_Sword, Sword_Slash, Run_Shoot。
add_faces(scn, arm, head, tag, kinds, spec, drop):アーマチュアを移動させる前に実行。kinds:neutral, blink, side, shock, grimace, squint, dazed, xeyes。dropはパックの3D目/眉毛を削除します。show_faces(faces, [(frame, kind), ...]):ハードカット。各フェイスは次のフェイスまで保持されます。nla(arm, [(action, start, end, blend[, influence]), ...]):後のエントリはより高いトラックに配置されます。drive_root(arm, [(frame, x, y, yaw, ease), ...], last):yaw 0は-Y(デフォルトカメラ)を向きます。new_cam,aim,yaw_to,ps1_render_settings(scn, outdir)。- セット:
scripts/set_lift_lobby.py(BUILD_ONLY=Trueで実行すると、box,uv_world,matを返します)。新しいセットは同じパターン(interpolation = "Closest"のピクセルテクスチャを持つボックス)に従います。
ルックルール
- レンダリング解像度360x640、
filter_size 0、AgX Punchy、フラットシェーディング。ポスト処理:15ビットカラー+ベイヤーディザ、x3ニアレスト補間。 - フェイスは32x28ピクセルのフラットなデカールで、ハードカットで切り替えます。フェイスをスムーズにアニメーションさせないでください。
- 力が加わったときの押しつぶし:アーマチュアのスケールをキー設定(zを縮小、xyを拡大)。コメディのビート:高速イン(2〜3フレーム)、ホールド。
- キャプション:1〜3ワード、太めのサンセリフ、白色に黒のストローク、現在のワードは黄色、下3分の1(1920のy 1300)。
- 統計数値の表示:赤い角丸ボックス、上3分の1、4フレームかけてポップイン、実在の数字ごとに1つ。
サウンドルール
- すべての効果音は、そのピークがピクチャーフレームに重なるように配置します(
at(name, frame, db, peak=...))。 - 音声が最も重要:VOは0 dBFSにノーマライズ、効果音バスは-11 dB、音楽は-7 dB、さらに音声が話している間のサイドチェインダッキング(効果音-5 dB、音楽-9 dB)。リフトショートのv1では効果音が-3、音楽が0だったため「ナレーションが聞こえない」というフィードバックがありました。
- 音楽は大きなストップビートで完全にカットし、その後戻します。無音がギャグを引き立てます。
- マスター:loudnorm -15 LUFS、トゥルーピーク -1.5 dBTP。
- 確認:earshotの
timelineをmp4とcues.jsonで(目標:すべてのキューが100ミリ秒以内)、measureでLUFSを確認。
すでに発生したバグ(繰り返さないこと)
- パックメッシュはレンダリング非表示で出荷される:
hide_render = False(キットが行う)。 - ドアメッシュはすでに閉じた位置にある:オフセットのみをアニメーションさせる。そうしないと、0.7mの隙間ができて閉じる。
- 天井が約2.75mより低いと、エレベーターサイズの車内のカメラから壁のディスプレイが見えなくなる。
- まだ見せてはいけないキャラクター:彼のショットまで
location.z = -50をキー設定する。 - NLAのワンショットからホールドへの移行:クリップの
blend_out = 0を設定する。そうしないと、1フレームだけポーズが飛ぶ。 - 逆ショットは真っ黒になる(照明のない天井):フロントライトと天井に少しのエミッションを追加する。逆アングルは早めにレンダリングする。
- mp3でのfaster-whisperはPyAVの不一致で壊れる可能性がある:
words.pyは最初にffmpegでデコードする。 - -0.04秒から始まるフレーズはアラインメントで空のスライスを生成する:
max(0.0, ...)でクランプする(対応済み)。
コスト(リフトショート、RTX 3070 Tiラップトップ)
レンダリング820フレーム92秒、ポスト17秒、0ドル。キャラクター、効果音、音楽:0ドル(CC0+コードで合成)。音声:ElevenLabsで1テイクあたり約0.10ドル、ショート動画あたり2テイク。
引き渡す前に
- スチルだけでなく、最終フレームのコンタクトシート(約2秒ごと)を確認する。
- ラウドネスを測定し、同期を確認し、キャプションがナレーションテキストと一致しているか確認する(スペル修正は
post.pyのFIXで行う)。 - 何を測定したか、何を人間が聴いていないかをユーザーに伝える。