diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edaeb04..c32e510 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,8 @@ permissions: jobs: node-suite: - runs-on: ubuntu-latest + # ubuntu-latest moved to 26.04, which playwright-core 1.56.1 has no Chromium for (#46). + runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - name: Checkout diff --git a/README.ja.md b/README.ja.md index f5f6683..2b356dc 100644 --- a/README.ja.md +++ b/README.ja.md @@ -137,17 +137,18 @@ auto mode を使っていて、それでも実行が拒否される場合は、` 動画 1 本ごとに作業フォルダ(``)を 1 つ使います。中身と `script.json` の書き方は [SKILL.md](skills/motion-render/SKILL.md) にあります。 -| コマンド | やること | -| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | -| `motion-render narrate ` | `script.json` の原稿からナレーションを作り、場面の秒数を `timing.json` に書く | -| `motion-render bgm ` | `script.json` の `bgm` の指示から BGM を作る | -| `motion-render sfx ` | `script.json` の `sfx` から効果音を作り、全部を順に聞ける `preview.m4a` を書く | -| `motion-render prepare /film` | Motion: Motion から取得した映像データを、描画できる形に展開する | -| `motion-render kit ` | HTML: `/film` にプレーヤー・スタイル・GSAP・日本語フォント・場面の秒数を置く。Claude はそのあと `film/index.html` を書く | -| `motion-render check /film` | HTML: 0.1 秒ごとに配置を調べ、重なり、箱や画面からのはみ出し、ほかのものの下に隠れたもの、小さすぎる文字や薄すぎる文字を報告する | -| `motion-render stills 0,5,12` | 確認用の静止画を書き出す(HTML の映像は先に配置を調べる) | -| `motion-render build [out.mp4]` | ナレーション・BGM・効果音を合成し、映像と合わせて MP4 にする。どれかが足りないとき、HTML の映像が配置のチェックに通らないときは止まる | -| `motion-render --version` | 版を表示する | +| コマンド | やること | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `motion-render narrate ` | `script.json` の原稿からナレーションを作り、場面の秒数を `timing.json` に書く | +| `motion-render bgm ` | `script.json` の `bgm` の指示から BGM を作る | +| `motion-render sfx ` | `script.json` の `sfx` から効果音を作り、全部を順に聞ける `preview.m4a` を書く | +| `motion-render prepare /film` | Motion: Motion から取得した映像データを、描画できる形に展開する | +| `motion-render kit ` | HTML: `/film` にプレーヤー・スタイル・GSAP・日本語フォント・場面の秒数を置く。Claude はそのあと `film/index.html` を書く | +| `motion-render check /film [<場面のファイル>…]` | HTML: 0.1 秒ごとに配置を調べ、重なり、箱や画面からのはみ出し、ほかのものの下に隠れたもの、小さすぎる文字や薄すぎる文字を報告する。場面のファイル(`film/scenes/` の名前)を渡すと、そのファイルの場面だけを調べる | +| `motion-render stills 0,5,12` | 確認用の静止画を書き出す(HTML の映像は先に配置を調べる) | +| `motion-render build [out.mp4]` | ナレーション・BGM・効果音を合成し、映像と合わせて MP4 にする。どれかが足りないとき、HTML の映像が配置のチェックに通らないときは止まる | +| `motion-render times ` | 工程ごとにかかった時間を表にする(各コマンドの時間と、その間の Claude の作業の時間) | +| `motion-render --version` | 版を表示する | | 環境変数 | 効果 | | ----------------------- | ------------------------------------------------------------------------------------------ | @@ -233,7 +234,8 @@ skills/motion-render/ # スキル本体(skills CLI とプラグインの SKILL.md # Claude が読む手順(準備、絵コンテ・原稿・BGM の決め方を含む) package.json # motion-render コマンドと、依存の版(playwright-core, ffmpeg-static, gsap, @fontsource/noto-sans-jp, @fontsource/klee-one) references/html-film.md # Claude が HTML の映像を書くときの決まり(配置、動き、配置のチェックの読み方) - kit/ # kit が HTML の映像に写すプレーヤーと基本のスタイル、作風のスタイル + references/pipeline.md # 工程のつながり(何を待ち、何を裏で動かすか)と、工程を足すときの決まり + kit/ # kit が HTML の映像に写すプレーヤーと基本のスタイル、作風のスタイル、部品(parts.css) scripts/cli.ts # motion-render コマンド scripts/audio.ts # ナレーションと BGM の生成、音声の合成 scripts/sfx.ts # 効果音の生成(ElevenLabs)、音量のそろえ方、試聴用ファイル @@ -241,6 +243,8 @@ skills/motion-render/ # スキル本体(skills CLI とプラグインの scripts/render.ts # Motion と HTML の映像をブラウザで描画する部分 scripts/kit.ts # HTML の映像のフォルダを用意する scripts/layout.ts # HTML の映像の配置のチェック + scripts/scenes.ts # 場面のファイル(film/scenes/*.html)を index.html に差し込む + scripts/steps.ts # 各コマンドの時間を残し、工程ごとの時間の表にする(times) scripts/snapshot.js # 配置のチェックのため、映像のページの中で箱と文字を測る scripts/json.ts # ファイル・API・プレーヤーから来る JSON を、形を確かめながら読む scripts/store.ts # Motion の store を映像データのフォルダに展開する diff --git a/README.md b/README.md index a7aabbd..864275f 100644 --- a/README.md +++ b/README.md @@ -137,17 +137,18 @@ On the first run, Chromium (about 210 MB) and ffmpeg are downloaded automaticall Each video uses one work directory (``). Its layout and the `script.json` format are in [SKILL.md](skills/motion-render/SKILL.md). -| Command | What it does | -| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `motion-render narrate ` | Generates narration from `script.json` and writes scene lengths to `timing.json` | -| `motion-render bgm ` | Generates the BGM from the `bgm` prompt in `script.json` | -| `motion-render sfx ` | Generates the sound effects in `script.json` and a `preview.m4a` of them in order | -| `motion-render prepare /film` | Motion: expands a Motion film's data into a directory it can render | -| `motion-render kit ` | HTML: sets up `/film` with the player, styles, GSAP, the Japanese fonts and the scene times. Claude then writes `film/index.html` | -| `motion-render check /film` | HTML: checks the layout every 0.1 s for overlaps, things sticking out of their box or the frame, things hidden under others, and text that is too small or too faint | -| `motion-render stills 0,5,12` | Writes still frames for checking (an HTML film is checked first) | -| `motion-render build [out.mp4]` | Mixes narration, BGM and sound effects and renders the MP4. Stops if any is missing, or if an HTML film fails the layout check | -| `motion-render --version` | Prints the version | +| Command | What it does | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `motion-render narrate ` | Generates narration from `script.json` and writes scene lengths to `timing.json` | +| `motion-render bgm ` | Generates the BGM from the `bgm` prompt in `script.json` | +| `motion-render sfx ` | Generates the sound effects in `script.json` and a `preview.m4a` of them in order | +| `motion-render prepare /film` | Motion: expands a Motion film's data into a directory it can render | +| `motion-render kit ` | HTML: sets up `/film` with the player, styles, GSAP, the Japanese fonts and the scene times. Claude then writes `film/index.html` | +| `motion-render check /film […]` | HTML: checks the layout every 0.1 s for overlaps, things sticking out of their box or the frame, things hidden under others, and text that is too small or too faint. Given scene files (names in `film/scenes/`), checks only their scenes | +| `motion-render stills 0,5,12` | Writes still frames for checking (an HTML film is checked first) | +| `motion-render build [out.mp4]` | Mixes narration, BGM and sound effects and renders the MP4. Stops if any is missing, or if an HTML film fails the layout check | +| `motion-render times ` | Shows how long each step took, as a table (each command, and Claude's work between them) | +| `motion-render --version` | Prints the version | | Environment variable | Effect | | ----------------------- | ------------------------------------------------------------------------------------------------------- | @@ -233,7 +234,8 @@ skills/motion-render/ # The skill (read by both the skills CLI and the plugi SKILL.md # Instructions for Claude (setup, storyboard, script and BGM choices) package.json # The motion-render command and pinned dependencies (playwright-core, ffmpeg-static, gsap, @fontsource/noto-sans-jp, @fontsource/klee-one) references/html-film.md # How Claude writes an HTML film: layout, motion, and reading the layout check - kit/ # The player, base styles and drawing styles that `kit` copies into an HTML film + references/pipeline.md # How the steps depend on each other (what waits, what runs in the background), and how to add a step + kit/ # The player, base styles and drawing styles that `kit` copies into an HTML film, and the parts (parts.css) scripts/cli.ts # The motion-render command scripts/audio.ts # Narration and BGM generation, audio mixing scripts/sfx.ts # Sound effect generation (ElevenLabs), leveling and the listening file @@ -241,6 +243,8 @@ skills/motion-render/ # The skill (read by both the skills CLI and the plugi scripts/render.ts # Rendering Motion and HTML films in the browser scripts/kit.ts # Setting up an HTML film directory scripts/layout.ts # The layout check of HTML films + scripts/scenes.ts # Putting the scene files (film/scenes/*.html) into index.html + scripts/steps.ts # Logging each command's time and making the per-step table (times) scripts/snapshot.js # Measures boxes and text inside the film page for the layout check scripts/json.ts # Reading JSON from files, APIs and the player without trusting its shape scripts/store.ts # Expanding a Motion store into a film directory diff --git a/skills/motion-render/SKILL.md b/skills/motion-render/SKILL.md index 86e4908..67a5949 100644 --- a/skills/motion-render/SKILL.md +++ b/skills/motion-render/SKILL.md @@ -62,8 +62,11 @@ Motion の Artifact 型は作成時に Style・Colors・Length を質問する / script.json # 原稿(Claude が書く) timing.json # 場面の秒数(narrate が書く) + plan.md # html の映像の絵コンテと秒の計画(Claude が書く。場面を手分けして書くときの共通の指示書) + steps.jsonl # 各コマンドの開始と終了(コマンドが書く。motion-render times で表にする) audio/ # <場面id>.wav、bgm.mp3、mix.m4a、sfx/(効果音と試聴用の preview.m4a) - film/ # 映像。Motion から取得したもの(prepare が展開する)か、html の映像(kit が用意し、Claude が index.html を書く) + film/ # 映像。Motion から取得したもの(prepare が展開する)か、html の映像(kit が用意し、 + # Claude が index.html と scenes/*.html を書く) ``` `script.json` の形: @@ -130,26 +133,40 @@ motion-render narrate #### 3B. html の映像を作る -1. `motion-render kit ` で `/film/` にプレーヤー・フォント・GSAP・場面の秒数を置く -2. [references/html-film.md](references/html-film.md) を読み、その決まりに従って `/film/index.html` を書く。 - 先に「作る前に決めること」を依頼文とテーマから決め、ページの先頭に書く。 - 場面は `
`、動きは `film.build` の中で `film.at(場面id, 秒)` に置く -3. 並行して BGM を作ってよい: `motion-render bgm ` -4. 効果音を付けるときは、`script.json` の `sfx` の `at` に、その動きに渡した `film.at` の秒を書き、 - `motion-render sfx ` で作る。タイトルの場面に `🎧:11.ai` を小さく入れる +待ち時間を作らない順に進める。API を待つ工程(BGM・効果音)は Bash の `run_in_background` で裏に回し、 +その間に Claude は書き続ける。場面は手分けして同時に書く(工程のつながりは [references/pipeline.md](references/pipeline.md))。 + +1. `motion-render kit ` で `/film/` にプレーヤー・部品・フォント・GSAP・場面の秒数を置き、 + `motion-render bgm ` を裏で始める +2. [references/html-film.md](references/html-film.md) を読み、`/plan.md`(絵コンテと秒の計画)を書く。 + 場面ごとに、見せるもの、使う部品、動きとその秒(`film.at` に渡す秒)、効果音、場面をまたいで残る物の + 入るときと出るときの状態を書く(書き方は html-film.md の「絵コンテと秒の計画」) +3. 効果音を付けるときは、plan.md の効果音を `script.json` の `sfx` に写し、`motion-render sfx ` を裏で始める。 + 効果音は `prompt` と `duration` だけで作られるので、あとで `at` を直しても作り直しにはならない +4. `/film/index.html` に、全場面で共通の部分を書く: 「作る前に決めること」、配色、共通のスタイル、 + 場面をまたいで残る物(`#stage`)とその動き、場面を差し込む `` の行 +5. 場面を書く。場面が 5 つ以上で Agent ツールがあれば、続いた場面を 2 つずつ(最大 7)のまとまりに分け、 + まとまりごとにサブエージェントを 1 つ、1 つのメッセージでまとめて起動する(頼み方は html-film.md の + 「場面を分けて書く」)。各サブエージェントは `film/scenes/<番号>-<名前>.html` を書き、 + `motion-render check /film <そのファイル名>` を `layout ok` まで直して返す。 + 場面が 4 つ以下か Agent ツールが無いときは、Claude が同じ形で場面のファイルを書く +6. 全員が返ったら `motion-render check /film` で全体を調べ、まとまりの境目の問題を直す。 + 書いた動きの秒が plan.md と違えば、`script.json` の `sfx` の `at` をその秒に直す。 + 効果音を付けるときは、タイトルの場面に `🎧:11.ai` を小さく入れる ### 4. 映像を確認する -html の映像(3B)は手順 4 の 1〜3 を飛ばし、先に `motion-render check /film` を `layout: ok` になるまで直す -(出力の読み方は references/html-film.md の「確認」)。そのあと 4・5 で静止画を見る。 +html の映像(3B)は手順 4 の 1〜3 を飛ばし、3B の 6 で `layout: ok` になってから 4・5 で静止画を見る +(`check` の出力の読み方は references/html-film.md の「確認」)。直すのは、その場面がある `film/scenes/` のファイル。 1. `Artifact` の `read` で `paths: ["index.html", "artifact-type/app.js", "artifact-type/app.css"]`、`out_dir: /film` 2. `ArtifactData` の `list`(`collection: "modules"`)と `get`(`film`/`script`)を `out_dir: /store` で保存 3. `motion-render prepare /store /film` 4. `motion-render stills /film /stills <各場面の中ほどの秒をカンマ区切り>` で全場面を見る。 見出しの不自然な改行、はみ出し、重なりがあれば Motion 側(html の映像なら index.html)を直し、手順 4 をやり直す -5. 効果音があれば、各効果音の秒(場面の開始 + `at`)の静止画も書き出し、鳴らす対象がその時点で画面に出ているかを見る。 - ずれていれば `at` を直して `motion-render sfx` をやり直す(作り直されるのは変えたものだけ) +5. 効果音があれば、4 の静止画と同じ `stills` の 1 回で、各効果音の秒(場面の開始 + `at`)も書き出し、 + 鳴らす対象がその時点で画面に出ているかを見る。ずれていれば `at` を直す(`at` だけなら作り直しは要らない)。 + `prompt` か `duration` を変えたときだけ `motion-render sfx` をやり直す(作り直されるのは変えたものだけ) ### 5. MP4 にする @@ -164,7 +181,8 @@ motion-render build [<出力.mp4>] ### 6. 確認して報告する - `ffprobe` で、長さと映像・音声の両トラックがあることを見る -- 報告: 出力のパス、長さ、場面の流れ、BGM の雰囲気、Motion の URL(html の映像なら `/film/index.html` のパス)。耳で聞いていないことは聞いていないと書く +- 報告: 出力のパス、長さ、場面の流れ、BGM の雰囲気、Motion の URL(html の映像なら `/film/index.html` のパス)、 + かかった時間(`motion-render times ` の最後の行)。耳で聞いていないことは聞いていないと書く - 効果音を付けたときは、試聴用の `audio/sfx/preview.m4a` のパスと、その中の順番(`motion-render sfx` の出力)も報告し、 合わない音があれば教えてほしいと頼む。Claude には音の良し悪しが判定できないため、ここは人が聞いて決める @@ -230,8 +248,11 @@ URL を渡された場合も音を付ける。映像データを取得し(手 - `scripts/sfx.ts`: ElevenLabs による効果音の生成(`sfx`)、音量の測定とそろえ方、試聴用ファイル - `scripts/work.ts`: `script.json` の検証、`timing.json` の計算、ナレーションを動画の場面の開始時刻に置く処理 - `scripts/render.ts`: プレーヤーを開いてコマを返す。描画中は外部への通信を遮断し、Chromium の sandbox を有効にする -- `scripts/kit.ts`・`kit/`: html の映像のプレーヤー(`player.js`)と土台の CSS(`base.css`)、作風の CSS(`styles.css`)を、フォントと GSAP と一緒に置く(`kit`) +- `scripts/kit.ts`・`kit/`: html の映像のプレーヤー(`player.js`)と土台の CSS(`base.css`)、作風の CSS(`styles.css`)、部品の CSS(`parts.css`)を、フォントと GSAP と一緒に置く(`kit`) +- `scripts/scenes.ts`: `film/scenes/*.html` を index.html の `` に差し込んで、描画するページにする - `scripts/layout.ts`・`scripts/snapshot.js`: html の映像の配置の確認(`check`。`stills`・`build` も実行する) -- `references/html-film.md`: html の映像の書き方と配置の決まり +- `scripts/steps.ts`: 各コマンドの開始と終了を `/steps.jsonl` に残し、工程ごとの時間の表にする(`times`) +- `references/html-film.md`: html の映像の書き方と配置の決まり、部品、場面を分けて書くときの決まり +- `references/pipeline.md`: 工程のつながり(何を待ち、何を裏で動かすか)と、工程を足すときの決まり - `scripts/json.ts`: ファイル・API・プレーヤーから来る JSON を、形を確かめながら読む - `scripts/store.ts`: Motion の store(ArtifactData の保存先)を映像データのフォルダに展開する(`prepare`) diff --git a/skills/motion-render/kit/parts.css b/skills/motion-render/kit/parts.css new file mode 100644 index 0000000..7f71e46 --- /dev/null +++ b/skills/motion-render/kit/parts.css @@ -0,0 +1,165 @@ +/* Parts that explainer films keep drawing, so a scene gives only its content and seconds: a row of + step cards, a card, lanes on a time axis, a scale with ticks, and the caption. Colors come from the + page's variables (--panel --line --ink --muted --accent), so the parts follow every style. + The player animates them: film.light (a step or card), film.ticks (a scale), film.caption. */ + +/* A row of steps. --on is 0 (off) to 1 (lit); film.light tweens it. */ +.steps { + position: absolute; + display: flex; + gap: var(--steps-gap, 20px); +} +.step, +.card { + --on: 0; + border: 3px solid color-mix(in srgb, var(--accent) calc(var(--on) * 100%), var(--line)); + background: color-mix(in srgb, var(--accent) calc(var(--on) * 16%), var(--panel)); + color: var(--ink); + border-radius: 14px; +} +/* In the hand-drawn styles add drawn: the style draws the line, and the part only lights it. */ +[data-style] :is(.step, .card).drawn { + border-color: transparent; +} +[data-style] :is(.step, .card).drawn::before { + border-color: color-mix(in srgb, var(--accent) calc(var(--on) * 100%), var(--line)); +} +.step { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + min-width: var(--step-w, 150px); + padding: 18px 16px; + font-size: 28px; +} +.step > svg { + width: 44px; + height: 44px; + color: color-mix(in srgb, var(--accent) calc(var(--on) * 100%), var(--muted)); +} +/* A card with a title and a body. Place it with position: absolute and a layer class. */ +.card { + display: flex; + flex-direction: column; + gap: 12px; + padding: 24px 28px; +} +.card-title { + font-size: 34px; + font-weight: 700; +} +/* The body darkens to --ink as the card lights, so it keeps its contrast on the lit fill. */ +.card-body { + font-size: 28px; + line-height: 1.5; + color: color-mix(in srgb, var(--ink) calc(var(--on) * 100%), var(--muted)); +} + +/* Lanes on one time axis: names on the left, tracks on the right. --from and --to (seconds or any + unit) set the axis; a .seg starts at --at and lasts --len, a .pin sits at --at. */ +.lanes { + --from: 0; + --to: 10; + position: absolute; + display: grid; + grid-template-columns: auto 1fr; + gap: 14px 20px; + align-items: center; +} +.lane-name { + font-size: 26px; + color: var(--muted); + text-align: right; +} +.track { + position: relative; + height: var(--lane-h, 52px); + border: 2px solid var(--line); + border-radius: 10px; + background: var(--panel); +} +.seg, +.pin { + position: absolute; + left: calc((var(--at) - var(--from)) / (var(--to) - var(--from)) * 100%); +} +.seg { + top: 6px; + bottom: 6px; + width: calc(var(--len) / (var(--to) - var(--from)) * 100%); + display: flex; + align-items: center; + padding: 0 12px; + border: 2px solid var(--accent); + border-radius: 8px; + background: color-mix(in srgb, var(--accent) 22%, var(--panel)); + font-size: 24px; + white-space: nowrap; +} +.pin { + top: 50%; + width: 18px; + height: 18px; + margin: -9px 0 0 -9px; + border-radius: 50%; + background: var(--accent); +} + +/* A scale: a line with ticks and labels, made by film.ticks. --from and --to set its ends. The line + runs --ruler-pad in from each side, so the labels at its ends stay inside the box. */ +.ruler { + --from: 0; + --to: 10; + --ruler-pad: 60px; + position: absolute; + height: 72px; +} +.ruler::before { + content: ""; + position: absolute; + left: var(--ruler-pad); + right: var(--ruler-pad); + top: 10px; + border-top: 3px solid var(--line); +} +.tick { + position: absolute; + top: 0; + /* Centered on its value by a negative margin, not a transform: film.spot measures layout boxes, + so a transform would put a chip sent to a tick half a tick off. */ + width: var(--tick-w, 120px); + margin-left: calc(var(--tick-w, 120px) / -2); + left: calc( + var(--ruler-pad) + (var(--at) - var(--from)) / (var(--to) - var(--from)) * + (100% - 2 * var(--ruler-pad)) + ); + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + font-size: 24px; + font-style: normal; + color: var(--ink); + white-space: nowrap; + font-variant-numeric: tabular-nums; +} +.tick::before { + content: ""; + width: 3px; + height: 23px; + background: var(--line); +} + +/* The caption: one line across the bottom, inside the safe area. Add layer-caption. */ +.caption { + position: absolute; + left: var(--safe-x); + right: var(--safe-x); + bottom: calc(var(--safe-y) + 8px); + text-align: center; + font-size: 54px; + font-weight: 700; + color: var(--ink); +} diff --git a/skills/motion-render/kit/player.js b/skills/motion-render/kit/player.js index f3d0c4d..6f8d02f 100644 --- a/skills/motion-render/kit/player.js +++ b/skills/motion-render/kit/player.js @@ -181,6 +181,50 @@ at ); }, + // Lights a .step or .card of kit/parts.css at `at` (its --on goes to 1), and puts it out again + // at o.off when given. o: { dur = 0.35, off }. + light: function (tl, what, at, o) { + o = o || {}; + var dur = o.dur || 0.35; + tl.to(what, { "--on": 1, duration: dur, ease: "power2.out" }, at); + if (o.off !== undefined) tl.to(what, { "--on": 0, duration: dur, ease: "power2.in" }, o.off); + return tl; + }, + // Fills a .ruler of kit/parts.css with a tick every o.step from its --from to its --to, each + // labeled with o.label ("{v} 秒"; "" for no labels). Call it inside film.build. + ticks: function (what, o) { + var e = el(what), + css = getComputedStyle(e), + from = Number(css.getPropertyValue("--from")), + to = Number(css.getPropertyValue("--to")), + step = o && o.step, + label = o && o.label !== undefined ? o.label : "{v}"; + if (!(step > 0) || !(to > from)) throw new Error("film.ticks needs --from < --to and a step"); + for (var i = 0; from + i * step <= to + 1e-9; i++) { + var v = Math.round((from + i * step) * 1000) / 1000, + t = document.createElement("i"); + t.className = "tick"; + t.style.setProperty("--at", String(v)); + if (label) t.textContent = label.replace("{v}", String(v)); + e.appendChild(t); + } + return e; + }, + // Shows a caption from `at` and takes it away so it is gone 0.25 s before scene `id` ends, as + // references/html-film.md asks. + caption: function (tl, what, id, at) { + tl.fromTo( + what, + { autoAlpha: 0, y: 16 }, + { autoAlpha: 1, y: 0, duration: 0.4, ease: "expo.out" }, + at + ); + return tl.to( + what, + { autoAlpha: 0, duration: 0.2, ease: "power3.in" }, + window.film.end(id, 0.45) + ); + }, // Raises the text of `what` one character at a time from below a mask line, starting `at`. // The text is split into spans the first time, so call it on an element that holds only text. // o: { dur = 0.5, stagger = 0.04 }. diff --git a/skills/motion-render/references/html-film.md b/skills/motion-render/references/html-film.md index bde6901..cdaaa63 100644 --- a/skills/motion-render/references/html-film.md +++ b/skills/motion-render/references/html-film.md @@ -1,8 +1,8 @@ # html の映像の作り方 Claude Motion が使えないときに、映像を 1 枚の HTML として書く。`motion-render kit ` が -`/film/kit/` にプレーヤー・スタイル・フォント(Noto Sans JP と、手描きの作風用の Klee One)・GSAP・場面の秒数を置くので、 -Claude は `/film/index.html` だけを書く。描画中はネットワークが遮断されるので、CDN や Google Fonts は使えない。 +`/film/kit/` にプレーヤー・スタイル・部品・フォント(Noto Sans JP と、手描きの作風用の Klee One)・GSAP・場面の秒数を置くので、 +Claude は `/film/index.html` と、場面を書いた `/film/scenes/*.html` だけを書く。描画中はネットワークが遮断されるので、CDN や Google Fonts は使えない。 ## ページの骨組み @@ -14,6 +14,7 @@ Claude は `/film/index.html` だけを書く。描画中はネットワ + @@ -39,17 +40,13 @@ Claude は `/film/index.html` だけを書く。描画中はネットワ
…
- -
…
-
…
+ +
@@ -67,6 +64,9 @@ Claude は `/film/index.html` だけを書く。描画中はネットワ | `film.arc(tl, 要素, 相手, 秒, o)` | `要素` を `相手` へ山なりに動かす。o: `dur`(既定 0.9)・`lift`(既定 120px)・`dx`・`dy` | | `film.breathe(tl, 要素, id, 秒, 量)` | `秒` から場面 `id` の終わりまで、`要素` を `量`(既定 0.01)だけゆっくり拡大縮小する | | `film.typeIn(tl, 要素, 秒, o)` | `要素` の文字を 1 文字ずつ下から立ち上げる。o: `dur`(既定 0.5)・`stagger`(既定 0.04) | +| `film.light(tl, 要素, 秒, o)` | 部品の `step`・`card` を `秒` に光らせる。o: `off`(消す秒)・`dur`(既定 0.35) | +| `film.ticks(要素, o)` | 部品の `ruler` に目盛りを付ける。o: `step`(間隔)・`label`(既定 `"{v}"`、`""` で無し) | +| `film.caption(tl, 要素, id, 秒)` | 字幕を `秒` に出し、場面 `id` の終わりの 0.25 秒前までに消す | | `section.scene` | その場面の間だけ見える。`#stage` のように section の外に置いたものはずっと見える | ## 作る前に決めること @@ -83,6 +83,27 @@ Claude は `/film/index.html` だけを書く。描画中はネットワ - **動きの決まり**: 出る・移る・消えるの緩急と、止めて読ませる長さ。同じ役割の物には同じ動きを使う - **この 1 本で使わないもの**: 下の「抑えるもの」から、テーマに合わせて足す +## 絵コンテと秒の計画 + +場面を書き始める前に `/plan.md` を書く。場面を手分けして書くときの共通の指示書であり、効果音を先に作る元でもある。 +秒は `timing.json` の場面の長さに収める。場面ごとに次の 5 つを書く。 + +```markdown +## flow(12.4 秒) +- 見せるもの: 6 つの工程カードが左から並び、今の工程が光る +- 部品: steps(6 枚)、caption +- 動き: 0.3 カードが順に出る(0.08 ずつ)/ 1.6 「原稿」が光る / 4.2 「ナレーション」が光る / 8.0 字幕 +- 効果音: 0.3 soft UI pop 0.6 / 1.6 gentle glassy chime 1.0 / 4.2 gentle glassy chime 1.0 +- 残る物: #steps は入るとき無い → 出るとき上端に縮んで残る(次の場面 lanes が使う) +``` + +- 動きの秒は、その動きに渡す `film.at(場面, 秒)` の秒にする。書き手はこの秒を変えない +- 場所は id で指す(「`#st-mix` の真下」)。座標を書くのは、画面の割り当て(どの帯に置くか)だけにする。 + 中身の大きさで位置が変わる物(並んだカードの 2 枚目以降)を座標で書くと、書き手の計算とずれる +- 残る物(`#stage` の物)は、場面の頭と終わりの状態(位置・大きさ・見えるか)を書く。 + まとまりの境目の場面ほど詳しく書く。境目で物が飛ばないようにするため +- 効果音は `prompt` と `duration` まで書く。そのまま `script.json` の `sfx` に写す + ## 作風 依頼文に見た目の指定(「手描きで」「ホワイトボードで」「設計図っぽく」)があればそれに合う作風にする。 @@ -120,6 +141,24 @@ Claude は `/film/index.html` だけを書く。描画中はネットワ - 作風が紙の目や揺れを足すので、ページでは模様や影を足さない - `Klee One` の太字は 600 まで。`font-weight: 700` は 600 で描かれる +## 部品 + +`kit/parts.css` に、解説の映像で毎回描く形がある。場面では中身と秒だけを書き、形を一から書かない。 +色はページの変数(`--panel` `--line` `--ink` `--muted` `--accent`)から取るので、どの作風でも使える。 +手描きの作風では `step`・`card` に `drawn` も付ける(線は作風が描き、部品は光らせるだけにする)。 + +| 部品 | 書き方 | 動かし方 | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| 工程の列 | `
` の中に `
…原稿
` を並べる | `film.light(tl, "#s-script", 秒, { off: 秒 })` | +| カード | `
` に `` と `` | `film.light` で光らせる。出し方は自分で書く | +| 時間のレーン | `
` に `語り
…
` を組で並べる。`track` の中に `
場面 1
` と `` | `seg`・`pin` を GSAP で出す(`scaleX` で伸ばす、`scale` で跳ねる) | +| 目盛り | `
` | `film.build` の中で `film.ticks("#ruler", { step: 2, label: "{v} 秒" })` | +| 字幕 | `

…

` | `film.caption(tl, "#cap-flow", "flow", 秒)` | + +- `seg` と `pin` の位置は、`lanes` の `--from`〜`--to` を `track` の幅に当てて決まる。秒でも、ほかの単位でもよい +- 1 本の `track` の中で `seg` と `pin` を重ねない(`check` が重なりとして出す)。印は別の `track` に置く +- 大きさは変数で変えられる: `--step-w`(工程の幅)、`--steps-gap`、`--lane-h`(レーンの高さ)、`--ruler-pad`、`--tick-w`(目盛りの文字の幅) + ## 配置の決まり `motion-render check` はこれを破った所を見つけて止める。最初から守って書く。 @@ -156,6 +195,47 @@ Claude は `/film/index.html` だけを書く。描画中はネットワ 意図した重なり(グラフの基準線が棒を横切る、軸のラベルが目盛りの外に出る、わざと斜めに押す判子)は、 その要素に `data-layout-ok` を付けると `check` が数えない。間違いを黙らせるためには使わない。 +## 場面を分けて書く + +場面は `film/scenes/<番号>-<名前>.html` に、続いた場面のまとまりごとに書く。プレーヤーが開くとき、 +`index.html` の `` の行に、ファイル名の順で差し込まれる。1 つのファイルには、 +その場面の ` +
…
+ +``` + +- id はページ全体で 1 度だけ。場面の id を頭に付ける(`#flow-s1`) +- スタイルは `#場面id` で始める。ほかの場面に効かせない +- `index.html` は書き換えない。`#stage` の物は自分の場面の秒の間だけ動かし、場面の終わりに plan.md の状態にする +- 自分のファイルだけを `motion-render check /film <ファイル名>` で調べる。ほかのファイルを入れずに、 + 自分の場面の秒だけを調べる。`layout ok` になり、`runner` の行が出なくなるまで直す +- 静止画は 1 回だけ、自分の場面の中ほどの秒で見る。全体の見た目は、全員が返ったあとに Claude がまとめて見る + +まとまりは続いた 2 場面ずつにする(最大 7)。書き手の時間は、手引きを読む決まった時間と、場面の数に比例する時間の和なので、 +1 人あたりの場面を減らすほど全員がそろって早く返る。ベンチマーク(14 場面)では、3〜4 場面ずつの 4 人だと最も遅い 1 人が +8 分 40 秒かかり、2 場面ずつの 7 人だと 5 分 15 秒だった(ほかの 6 人は 2 分 24 秒〜3 分 45 秒)。 +書き手 1 人あたり 12〜15 万トークンを使うので、7 人で 1 本あたり約 90 万トークンになる。 + +サブエージェントに頼むときは、1 つのまとまりに 1 つ、次を渡す。 + +``` +/film/scenes/<番号>-<名前>.html に、場面 , , … を書いてください。 +- 先に読むもの: /references/html-film.md、/plan.md、/film/index.html、/timing.json +- plan.md の各場面の「動き」の秒と「残る物」の状態を守る。index.html は変えない +- 書いたら `motion-render check /film <番号>-<名前>.html` を `layout ok` になるまで直す(単体で実行する) +- 静止画は 1 回だけ見る。plan.md に無い細部は自分で決め、問い返さない +- 返すもの: ファイル名、plan.md から変えた秒(無ければ「なし」)、直しきれなかった問題 +``` + ## 確認 ```bash @@ -175,6 +255,8 @@ motion-render check /film | `A has contrast 2.1:1, needs 4.5:1` | 文字が背景に対して薄い。40px 未満は 4.5:1、40px 以上は 3:1 を超えるよう、文字か背景の色を変える。明るい色の丸やチップに乗せる数字は、地の色(暗い色)で書く | `layout: ok` になるまで直してから `stills` で見る。`build` は問題が残っていると止まる。 +`runner` で始まる行は、ページのスクリプトの誤り(`NaN` から組んだ図形、見つからないセレクタ)。描かれない物があっても +配置の決まりは破らないので、`layout: ok` と一緒に出る。出たら直す。 ## 動き diff --git a/skills/motion-render/references/pipeline.md b/skills/motion-render/references/pipeline.md new file mode 100644 index 0000000..504bfb1 --- /dev/null +++ b/skills/motion-render/references/pipeline.md @@ -0,0 +1,34 @@ +# 工程のつながり + +1 本の動画は、Claude が書く工程と、API や描画を待つ工程でできている。待つ工程は、後の工程がその結果を +使うまで裏で動かし、Claude はその間も書き続ける。かかった時間は `motion-render times ` で見る。 + +## html の映像の工程 + +| 工程 | する人 | 読むもの | 書くもの | 待つ人 | +| ------------------------- | -------------------------------------- | ---------------------------------------------- | --------------------------------- | ------------------------ | +| 原稿 | Claude | 依頼 | `script.json`(場面と語り) | ナレーション | +| ナレーション(`narrate`) | API(4 本ずつ) | `script.json` | `audio/<場面>.wav`、`timing.json` | 計画(場面の秒数が要る) | +| BGM(`bgm`) | API・裏 | `script.json` の `bgm`、全体の秒 | `audio/bgm.*` | 書き出し | +| 絵コンテと秒の計画 | Claude | `timing.json` | `plan.md` | 効果音、場面 | +| 効果音(`sfx`) | API(3 本ずつ)・裏 | `script.json` の `sfx` の `prompt`・`duration` | `audio/sfx/` | 書き出し | +| 共通部分 | Claude | `plan.md` | `film/index.html` | 場面 | +| 場面 | サブエージェント(まとまりごとに同時) | `plan.md`、`index.html` | `film/scenes/*.html` | 確認 | +| 確認(`check`・`stills`) | Claude | 映像 | 直し、`sfx` の `at` | 書き出し | +| 書き出し(`build`) | 描画 | すべて | MP4 | — | + +裏で動かすのは、結果を使う工程が先にあるものだけ。BGM は書き出しまで、効果音は書き出しまで誰も待たない。 +ナレーションは場面の秒数を決めるので、終わるまで計画を書かない。 + +## 工程を足すとき + +キューシートと BGM の合わせ込み(#42)のように工程を足すときは、次を決めてから入れる。 + +1. **読むものと書くもの**: 上の表に行を足す。読むものが揃う一番早い所に置く。秒は `plan.md` か、 + 書いた映像の `film.at` の秒から取る(計画の秒ではなく、実際に書いた秒を正とする) +2. **誰が待つか**: 結果を使う工程が後ろにしかなければ、Bash の `run_in_background` で裏に回す。 + コマンドにするなら `scripts/steps.ts` の `BACKGROUND` に入れ、`times` が Claude の時間として数えるようにする +3. **Claude が書く量**: 場面ごとに分けられるもの(場面ごとの指示、場面ごとのキュー)は、場面のファイルと + 同じまとまりで分け、サブエージェントに同時に書かせられる形にする。全体を 1 人で通して書く工程を増やさない +4. **測る**: 足す前と後で同じ動画を作り、`motion-render times ` の表を PR に載せる。 + API を何度もやり直す工程(BGM を作り直すなど)は、回数も書く diff --git a/skills/motion-render/scripts/audio.ts b/skills/motion-render/scripts/audio.ts index 038d91e..cd532b9 100644 --- a/skills/motion-render/scripts/audio.ts +++ b/skills/motion-render/scripts/audio.ts @@ -5,6 +5,7 @@ import { spawn, spawnSync, type ChildProcess } from "node:child_process"; import { createRequire } from "node:module"; import { errorMessage, field, readJson } from "./json.ts"; import { limit } from "./limit.ts"; +import { inParallel } from "./inorder.ts"; import type { Script } from "./work.ts"; const API = "https://generativelanguage.googleapis.com/v1beta/models"; @@ -120,15 +121,49 @@ export async function volumeStats( return { mean, max }; } +// A provider's "too many requests" (HTTP 429). Calls run several at once, so a rate limit is a normal +// answer: the call waits `wait` ms and tries again without using up one of its attempts. +export class Busy extends Error { + wait: number; + constructor(message: string, wait: number) { + super(message); + this.wait = wait; + } +} +// Most a call waits on rate limits, in time and in tries, before a 429 counts as a failed attempt. +const BUSY_TOTAL_MS = 180_000; +const BUSY_TRIES = 10; + +// The wait an HTTP 429 asks for: Gemini's RetryInfo ("retryDelay": "23s") or a Retry-After header +// in seconds, else `fallback` ms. Never more than a minute at a time. +export function busyWait(body: unknown, retryAfter: string | null, fallback: number): number { + const details = field(field(body, "error"), "details"); + const delay = Array.isArray(details) + ? details.map((d: unknown) => field(d, "retryDelay")).find((d) => typeof d === "string") + : undefined; + const s = typeof delay === "string" ? parseFloat(delay) : Number(retryAfter ?? NaN); + return Math.min(60_000, Number.isFinite(s) && s >= 0 ? s * 1000 : fallback); +} + // Runs `attempt` up to RETRIES times, logging each failure under `label`, and rethrows the last error. export async function withRetries(label: string, attempt: () => Promise): Promise { let last: unknown; - for (let i = 1; i <= RETRIES; i++) { + let waited = 0, + waits = 0; + for (let i = 1; i <= RETRIES;) { try { return await attempt(); } catch (e) { last = e; + if (e instanceof Busy && waits < BUSY_TRIES && waited + e.wait <= BUSY_TOTAL_MS) { + console.error(`motion-render: ${label} is rate-limited, trying again in ${e.wait / 1000}s`); + waited += e.wait; + waits++; + await new Promise((ok) => setTimeout(ok, e.wait)); + continue; + } console.error(`motion-render: ${label} attempt ${i}/${RETRIES} failed: ${errorMessage(e)}`); + i++; } } throw last; @@ -163,9 +198,10 @@ async function generateAudio(model: string, body: object): Promise