diff --git a/README.ja.md b/README.ja.md index cd2367f..5903661 100644 --- a/README.ja.md +++ b/README.ja.md @@ -2,7 +2,7 @@ [English](README.md) | 日本語 -テーマを伝えるだけで、それを説明する**アニメーション動画を作り、ナレーションと BGM を付けて MP4 にする** Agent Skill です。 +テーマを伝えるだけで、それを説明する**アニメーション動画を作り、ナレーション・BGM・効果音を付けて MP4 にする** Agent Skill です。 ``` /motion-render:motion-render Workload Identity Federation による GitHub Actions から Google Cloud への認証の仕組みを説明する動画つくって @@ -19,19 +19,23 @@ motion-render 自身が作った紹介動画です(59 秒、音声あり)。 https://github.com/user-attachments/assets/eb1bf7e6-a880-4bbe-b575-7f4ec5c21672 +効果音付きの動画など、ほかの作例は Wiki の[作例](https://github.com/umiyosh/motion-render/wiki/%E4%BD%9C%E4%BE%8B)にあります。 + ![motion-render の仕組み](docs/how-it-works.svg) - 見た目・長さ・声・BGM の雰囲気はテーマから自動で決まります(指定すればそれに従います) +- ElevenLabs の API キーがあれば、カードが現れる・流れ図が光るといった動きに合わせて効果音も付きます - 既存の Motion の動画の URL を渡して、ナレーションと BGM 付きの MP4 にすることもできます - 描画用のブラウザ(Chromium)と ffmpeg は自動で用意されるので、環境ごとの差が出ません ## 必要なもの -| もの | 備考 | -| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Claude Code | [Claude Motion](https://support.claude.com/ja/articles/17454997-claude-motion%E3%82%92%E5%A7%8B%E3%82%81%E3%82%8B) を使えるアカウント(ベータ版。Team・Enterprise プランのみ)。Claude Code 専用です(Codex・Cursor などには Motion がありません) | -| Node.js 22.18 以上 | `node -v` で確認。コマンドの TypeScript を Node がそのまま実行します | -| Gemini API キー | ナレーションと BGM に使います。Google AI Studio で発行します([発行の手順](docs/api-key.ja.md)) | +| もの | 備考 | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Claude Code | [Claude Motion](https://support.claude.com/ja/articles/17454997-claude-motion%E3%82%92%E5%A7%8B%E3%82%81%E3%82%8B) を使えるアカウント(ベータ版。Team・Enterprise プランのみ)。Claude Code 専用です(Codex・Cursor などには Motion がありません) | +| Node.js 22.18 以上 | `node -v` で確認。コマンドの TypeScript を Node がそのまま実行します | +| Gemini API キー | ナレーションと BGM に使います。Google AI Studio で発行します([発行の手順](docs/api-key.ja.md)) | +| ElevenLabs API キー | 任意。効果音に使います。月額プランなしの従量課金で作れます([作成の手順](docs/elevenlabs-key.ja.md)) | macOS・Linux・Windows で動きます。 @@ -72,6 +76,14 @@ Google AI Studio で Gemini API キーを発行します([発行の手順](doc export GEMINI_API_KEY="発行したキー" ``` +効果音も付けたい場合は、ElevenLabs のキーも足します([作成の手順](docs/elevenlabs-key.ja.md))。無ければ効果音なしで作ります。 + +```sh +export ELEVENLABS_API_KEY="作成したキー" +``` + +月額プランなしで作った効果音は商用に使えず、公開するときは題名に `11.ai` などの表記が要ります。motion-render は効果音を付けた動画のタイトルに `🎧:11.ai` を入れます。 + ### 3. Claude Code の sandbox を使っている場合だけ: 設定を足す sandbox の中では描画用のブラウザが起動できません。`~/.claude/settings.json` に次の 2 つを足し、`motion-render` だけを sandbox の外で動かします。 @@ -118,14 +130,16 @@ auto mode を使っていて、それでも実行が拒否される場合は、` | ------------------------------------------------ | -------------------------------------------------------------------------------- | | `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-render stills 0,5,12` | 確認用の静止画を書き出す | -| `motion-render build [out.mp4]` | ナレーションと BGM を合成し、映像と合わせて MP4 にする(どちらかが無いと止まる) | +| `motion-render build [out.mp4]` | ナレーション・BGM・効果音を合成し、映像と合わせて MP4 にする(足りないと止まる) | | `motion-render --version` | 版を表示する | | 環境変数 | 効果 | | ----------------------- | ------------------------------------------------------------ | | `GEMINI_API_KEY` | ナレーションと BGM の生成に必要 | +| `ELEVENLABS_API_KEY` | 効果音の生成に必要(無ければ効果音を付けない) | | `MOTION_RENDER_OUT_DIR` | `build` の出力先を省略したときの保存先 | | `CHROMIUM_PATH` | 同梱の Chromium の代わりに、手元の Chrome や Chromium を使う | | `FFMPEG_PATH` | 同梱の ffmpeg の代わりに、手元の ffmpeg を使う | @@ -136,6 +150,9 @@ auto mode を使っていて、それでも実行が拒否される場合は、` | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `bootstrap_check_in ... Permission denied` でブラウザが起動しない | Claude Code の sandbox の中で動いています。インストールの手順 3 を確認してください。`motion-render` は単体で実行する必要があります(`\| grep` や `&&` でつなぐと sandbox の外に出ません) | | `GEMINI_API_KEY is not set` | インストールの手順 2 を確認し、Claude Code を起動し直してください | +| `ELEVENLABS_API_KEY is not set` | 効果音のキーがありません。[作成の手順](docs/elevenlabs-key.ja.md)で作るか、効果音なしで作るよう Claude に伝えてください | +| 効果音の生成が `HTTP 401` などで失敗する | キーで Sound Effects が許可されていないか、残高が足りません。エラーに続く理由を見て、ElevenLabs の Developers で確認してください | +| `came back silent` で効果音の生成が止まる | 3 回とも無音が返りました。`prompt` を具体的な音の説明に書き換えてやり直してください | | ナレーションや BGM の生成が何度も失敗する | 1 回 90 秒で打ち切り、3 回まで再試行します。それでも失敗するなら、API キーの権限と利用上限を確認してください | | `motion-render` のインストールで ffmpeg のダウンロードが失敗する | 社内プロキシなどで GitHub からの取得が止められています。`npm install -g --ignore-scripts <スキルのフォルダ>` で入れ、手元の ffmpeg を `FFMPEG_PATH` で指定してください(PATH 上の `ffmpeg` も自動で使います) | | 初回の Chromium のダウンロードが失敗する | `CHROMIUM_PATH` に手元の Chrome か Chromium を指定してください | @@ -201,6 +218,7 @@ skills/motion-render/ # スキル本体(skills CLI とプラグインの package.json # motion-render コマンドと、依存の版(playwright-core, ffmpeg-static) scripts/cli.ts # motion-render コマンド scripts/audio.ts # ナレーションと BGM の生成、音声の合成 + scripts/sfx.ts # 効果音の生成(ElevenLabs)、音量のそろえ方、試聴用ファイル scripts/work.ts # script.json の検証と場面の秒数の計算 scripts/render.ts # ブラウザで描画する部分 scripts/json.ts # ファイル・API・プレーヤーから来る JSON を、形を確かめながら読む diff --git a/README.md b/README.md index 7765859..e1fe8e7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ English | [日本語](README.ja.md) -An Agent Skill that turns a theme into an **animated explainer video with narration and background music, rendered to MP4**. +An Agent Skill that turns a theme into an **animated explainer video with narration, background music and sound effects, rendered to MP4**. ``` /motion-render:motion-render Make a video that explains how GitHub Actions authenticates to Google Cloud with Workload Identity Federation @@ -19,9 +19,12 @@ An introduction video made by motion-render itself (57 seconds, with sound). https://github.com/user-attachments/assets/b62fe703-223d-478e-9e9b-3239a48ba1ea +More videos, including one with sound effects, are in [Examples](https://github.com/umiyosh/motion-render/wiki/Examples) on the wiki. + ![How motion-render works](docs/how-it-works.en.svg) - Look, length, voice and the mood of the music are chosen from the theme (anything you specify wins) +- With an ElevenLabs API key, sound effects follow the animation too: a card appearing, a step of a flow lighting up - You can also pass the URL of an existing Motion film to get an MP4 with narration and BGM - The rendering browser (Chromium) and ffmpeg are set up automatically, so results don't depend on what is installed on your machine @@ -32,6 +35,7 @@ https://github.com/user-attachments/assets/b62fe703-223d-478e-9e9b-3239a48ba1ea | Claude Code | An account that can use [Claude Motion](https://support.claude.com/en/articles/17454997-get-started-with-claude-motion) (beta, Team and Enterprise plans only). Claude Code only: other agents such as Codex or Cursor have no Motion | | Node.js 22.18 or later | Check with `node -v`. Node runs the command's TypeScript directly | | Gemini API key | Used for narration and BGM. Create one in Google AI Studio ([step by step](docs/api-key.md)) | +| ElevenLabs API key | Optional, for sound effects. Pay-as-you-go works without a subscription ([step by step](docs/elevenlabs-key.md)) | Works on macOS, Linux and Windows. @@ -72,6 +76,14 @@ Create a Gemini API key in Google AI Studio ([step by step](docs/api-key.md)). T export GEMINI_API_KEY="your key" ``` +For sound effects, add an ElevenLabs key as well ([step by step](docs/elevenlabs-key.md)). Without it, videos are made without sound effects. + +```sh +export ELEVENLABS_API_KEY="your key" +``` + +Sound effects made without a subscription are for non-commercial use only and must be credited in the title (for example with `11.ai`) when published. motion-render puts `🎧:11.ai` on the title of a video with sound effects. + ### 3. Only if you use the Claude Code sandbox: add settings Chromium cannot start inside the sandbox. Add these two entries to `~/.claude/settings.json` so that only `motion-render` runs outside it. @@ -114,18 +126,20 @@ 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 prepare /film` | Expands a Motion film's data into a directory it can render | -| `motion-render stills 0,5,12` | Writes still frames for checking | -| `motion-render build [out.mp4]` | Mixes narration and BGM and renders the MP4 (stops if either is missing) | -| `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` | Expands a Motion film's data into a directory it can render | +| `motion-render stills 0,5,12` | Writes still frames for checking | +| `motion-render build [out.mp4]` | Mixes narration, BGM and sound effects and renders the MP4 (stops if any is missing) | +| `motion-render --version` | Prints the version | | Environment variable | Effect | | ----------------------- | ---------------------------------------------------------- | | `GEMINI_API_KEY` | Required for narration and BGM | +| `ELEVENLABS_API_KEY` | Required for sound effects (none are added without it) | | `MOTION_RENDER_OUT_DIR` | Default output directory when `build` gets no output path | | `CHROMIUM_PATH` | Use your own Chrome or Chromium instead of the bundled one | | `FFMPEG_PATH` | Use your own ffmpeg instead of the bundled one | @@ -136,6 +150,9 @@ Each video uses one work directory (``). Its layout and the `script.json` | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | The browser fails with `bootstrap_check_in ... Permission denied` | It is running inside the Claude Code sandbox. Check step 3 of the install. `motion-render` must run on its own (chaining it with `\| grep` or `&&` keeps it in the sandbox) | | `GEMINI_API_KEY is not set` | Check step 2 of the install and restart Claude Code | +| `ELEVENLABS_API_KEY is not set` | There is no key for sound effects. Create one ([step by step](docs/elevenlabs-key.md)) or ask Claude to make the video without sound effects | +| Sound effect generation fails with `HTTP 401` or similar | The key does not allow Sound Effects, or the balance has run out. Read the reason after the status and check Developers in ElevenLabs | +| Sound effect generation stops with `came back silent` | Silence came back three times. Rewrite the `prompt` as a more concrete sound and try again | | Narration or BGM generation keeps failing | Each request times out after 90 seconds and is retried up to 3 times. If it still fails, check your API key's permissions and quota | | ffmpeg fails to download | A proxy is blocking downloads from GitHub. Install ffmpeg yourself (it is used from `PATH`) or point `FFMPEG_PATH` at it | | Chromium fails to download on first run | Point `CHROMIUM_PATH` at your own Chrome or Chromium | @@ -201,6 +218,7 @@ skills/motion-render/ # The skill (read by both the skills CLI and the plugi package.json # The motion-render command and pinned dependencies (playwright-core, ffmpeg-static) 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 scripts/work.ts # script.json validation and scene timing scripts/render.ts # Rendering in the browser scripts/json.ts # Reading JSON from files, APIs and the player without trusting its shape diff --git a/docs/elevenlabs-key.ja.md b/docs/elevenlabs-key.ja.md new file mode 100644 index 0000000..0e7a119 --- /dev/null +++ b/docs/elevenlabs-key.ja.md @@ -0,0 +1,50 @@ +# ElevenLabs で効果音用の API キーを作る + +[English](elevenlabs-key.md) + +motion-render は、アニメーションに合わせた効果音を ElevenLabs の Sound Effects API で作る。効果音は任意で、`ELEVENLABS_API_KEY` が無ければ付かない(ナレーションと BGM だけの動画になる)。このページでは、月額プランに入らずに従量課金(Pay As You Go)でキーを作り、motion-render に渡すまでを説明する。 + +## 1. 残高を入れる + +Sound Effects API は無料プランでは使えない。従量課金の残高を入れると使えるようになる。月額プランへの加入は要らない。 + +1. [elevenlabs.io](https://elevenlabs.io) でアカウントを作り、ログインする。 +2. [Developers](https://elevenlabs.io/app/developers) を開く。 +3. **Usage**(使用状況)の **Add credits**(クレジットを追加)を押す。隣の月額プランへの加入ボタンとは別物なので注意する。 +4. 金額を入れる。最低 $5。月額プランをすすめる確認が出たら、そのまま進める(日本語の画面では「チャージ」)。 +5. 支払い方法を選んで払う。カードを登録していなくても、Stripe の画面で払える。 +6. 自動チャージ(Auto Top Up)は使わなくてよい。 + +画面に「Pay As You Go」(従量課金プラン)と出れば完了。残高が 0 になると API はその場で止まり、後から請求されることはない。入れた残高は払い戻されず、購入から 12 か月で失効する。 + +## 2. キーを作る + +1. Developers の **API Keys** タブで、新しいキーを作る。名前は `motion-render` など。 +2. **Restrict key**(キーを制限)をオンのままにし、**Sound Effects**(サウンドエフェクト)だけを **Access** にする。ほかはすべて **No Access** のままにする。漏れたときの被害を効果音の生成に限るため。 +3. 使用制限(クレジット)は空欄でよい。残高を超えて使われることはない。 +4. 作成すると、キーが 1 回だけ表示される。後からは末尾 4 文字しか見られないので、すぐにコピーする。 + +## 3. キーを設定する + +motion-render は環境変数 `ELEVENLABS_API_KEY` からキーを読む。キーをリポジトリや dotfiles にコミットしないこと。 + +```sh +# ~/.zshrc など +export ELEVENLABS_API_KEY="..." +``` + +macOS のキーチェーンに入れる方法は、[Gemini のキーの手順](api-key.ja.md#2-キーを設定する)と同じ(サービス名を変える)。設定したら Claude Code を起動し直す。 + +## 料金 + +出力の秒数に比例する。2026 年 10 月に確かめた値で、1 秒あたり約 5 クレジット、約 $0.002。1 秒の効果音を 10 個付けた動画で約 $0.02。同じ `prompt` と `duration` の効果音は 1 回しか作らないので、作り直しても変えた分しか課金されない。 + +## 公開と商用利用 + +ElevenLabs のヘルプ([Can I publish the content I generate on the platform?](https://elevenlabs.io/docs/help-center/legal/can-i-publish-the-content-i-generate-on-the-platform))では、商用ライセンスが付くのは有料の月額プランで作ったものだけで、それ以外は非商用に限られ、公開するときは題名に `elevenlabs.io` か `11.ai` を入れる。従量課金についての記述は無い。 + +motion-render は、効果音を付けた動画のタイトルの場面に `🎧:11.ai` を入れる。商用に使う動画には、月額プランに入ってから効果音を作ること(プラン外で作った音は、後から加入しても商用に使えない)。 + +## キーが漏れたとき + +API Keys タブでそのキーを削除し、新しいキーを作る。 diff --git a/docs/elevenlabs-key.md b/docs/elevenlabs-key.md new file mode 100644 index 0000000..ed42575 --- /dev/null +++ b/docs/elevenlabs-key.md @@ -0,0 +1,50 @@ +# Creating an ElevenLabs API key for sound effects + +[日本語](elevenlabs-key.ja.md) + +motion-render makes sound effects that follow the animation with the ElevenLabs Sound Effects API. Sound effects are optional: without `ELEVENLABS_API_KEY`, the video gets narration and BGM only. This page shows how to create a key on pay-as-you-go, without a subscription, and hand it to motion-render. + +## 1. Add credits + +The Sound Effects API is not available on the free plan. A pay-as-you-go balance unlocks it; no subscription is needed. + +1. Sign up at [elevenlabs.io](https://elevenlabs.io) and log in. +2. Open [Developers](https://elevenlabs.io/app/developers). +3. Under **Usage**, choose **Add credits**. It is not the subscription button next to it. +4. Enter an amount (at least $5). If you are offered a subscription first, continue with the top-up. +5. Pay. A saved card is not required: the Stripe checkout takes the payment. +6. Auto Top Up can stay off. + +The account now shows "Pay As You Go". When the balance reaches zero, the API stops at once; you are never billed afterwards. Top-ups are not refundable and expire 12 months after purchase. + +## 2. Create the key + +1. In the **API Keys** tab of Developers, create a key, for example `motion-render`. +2. Keep **Restrict key** on and set only **Sound Effects** to **Access**; leave everything else at **No Access**. A leaked key can then only make sound effects. +3. Leave the credit limit empty: usage can never exceed the balance. +4. The key is shown once. Copy it now: afterwards only its last four characters are visible. + +## 3. Set the key + +motion-render reads the key from `ELEVENLABS_API_KEY`. Do not commit the key to a repository or dotfiles. + +```sh +# ~/.zshrc etc. +export ELEVENLABS_API_KEY="..." +``` + +To keep it in the macOS keychain instead, follow the [Gemini key steps](api-key.md#2-set-the-key) with a different service name. Restart Claude Code after setting it. + +## Cost + +Billed by the second of output. Measured in October 2026: about 5 credits, roughly $0.002, per second. Ten one-second effects cost about $0.02. An effect with the same `prompt` and `duration` is made only once, so a rebuild only pays for what changed. + +## Publishing and commercial use + +According to the ElevenLabs help center ([Can I publish the content I generate on the platform?](https://elevenlabs.io/docs/help-center/legal/can-i-publish-the-content-i-generate-on-the-platform)), only content made on a paid subscription carries a commercial license. Anything else is for non-commercial use only and must be credited with `elevenlabs.io` or `11.ai` in its title when published. The page does not mention pay-as-you-go. + +motion-render puts `🎧:11.ai` on the title scene of a video with sound effects. For a commercial video, subscribe before making its effects: audio made outside a subscription does not become licensed when you subscribe later. + +## If the key leaks + +Delete the key in the API Keys tab and create a new one. diff --git a/quality-baseline.json b/quality-baseline.json index 5c5ddae..87a5cfb 100644 --- a/quality-baseline.json +++ b/quality-baseline.json @@ -4,16 +4,16 @@ "thresholds": {}, "issues": [ { - "id": "eslint|complexity|skills/motion-render/scripts/cli.ts|210|1", + "id": "eslint|complexity|skills/motion-render/scripts/cli.ts|157|1", "tool": "eslint", "category": "complexity", "path": "skills/motion-render/scripts/cli.ts", - "line": 210, + "line": 157, "column": 1, - "detail": "Async function 'main' has a complexity of 25. Maximum allowed is 20.", + "detail": "Async function 'main' has a complexity of 22. Maximum allowed is 20.", "metric": { "name": "complexity", - "value": 25, + "value": 22, "limit": 20, "direction": "lowerIsBetter" }, diff --git a/skills/motion-render/SKILL.md b/skills/motion-render/SKILL.md index b07e366..920e844 100644 --- a/skills/motion-render/SKILL.md +++ b/skills/motion-render/SKILL.md @@ -1,6 +1,6 @@ --- name: motion-render -description: テーマを渡されたら、それを説明するアニメーション動画を Claude Motion で作り、ナレーション(Gemini TTS)とテーマに合う BGM(Lyria)を付けて MP4 にする。「〇〇を説明する動画を作って」「〇〇の紹介動画」「/motion-render 〇〇」、既存の Motion の動画を MP4 にしたいときに使う。 +description: テーマを渡されたら、それを説明するアニメーション動画を Claude Motion で作り、ナレーション(Gemini TTS)とテーマに合う BGM(Lyria)、アニメーションに合わせた効果音(ElevenLabs)を付けて MP4 にする。「〇〇を説明する動画を作って」「〇〇の紹介動画」「/motion-render 〇〇」、既存の Motion の動画を MP4 にしたいときに使う。 argument-hint: "<動画のテーマ。長さ・出力先などの指定があれば一緒に>" --- @@ -9,7 +9,7 @@ argument-hint: "<動画のテーマ。長さ・出力先などの指定があれ `/motion-render <テーマ>` の一言で、テーマを説明するアニメーション動画を作り、ナレーションと BGM を付けて MP4 にする。 **ナレーションと BGM は必ず付ける。** 音のない動画は作らない。既存の Motion の動画を MP4 にするときも、 -場面に合わせてナレーションを書き、BGM を作る。 +場面に合わせてナレーションを書き、BGM を作る。効果音は `ELEVENLABS_API_KEY` があるときだけ付ける(下の「効果音」)。 ## 決め方: 聞かずに決める @@ -37,7 +37,8 @@ Motion の Artifact 型は作成時に Style・Colors・Length を質問する Claude Code のプラグインとして入れ直すよう案内する(README の「インストール」。それで追加の作業は要らなくなる)。 `Unknown file extension ".ts"` で止まるのは Node が 22.18 より古いため。Node の更新を案内する 3. `GEMINI_API_KEY` が無いと `narrate`・`bgm` が止まる。止まったら、キーを Google AI Studio で発行して - シェルの設定に `export GEMINI_API_KEY=...` を足し、Claude Code を起動し直すよう案内する + シェルの設定に `export GEMINI_API_KEY=...` を足し、Claude Code を起動し直すよう案内する。 + `test -n "$ELEVENLABS_API_KEY" && echo set` で効果音のキーの有無も見ておく(無ければ効果音を付けずに進める) 4. `motion-render stills` などで Chromium が `bootstrap_check_in ... Permission denied` で起動しないのは、 Claude Code の sandbox の中で動いているため。利用者に `~/.claude/settings.json` へ次を足すよう案内する (Claude は自分の権限設定を変えない): @@ -57,7 +58,7 @@ Motion の Artifact 型は作成時に Style・Colors・Length を質問する / script.json # 原稿(Claude が書く) timing.json # 場面の秒数(narrate が書く) - audio/ # <場面id>.wav、bgm.mp3、mix.m4a + audio/ # <場面id>.wav、bgm.mp3、mix.m4a、sfx/(効果音と試聴用の preview.m4a) film/ # Motion から取得した映像データ(prepare が展開する) ``` @@ -69,7 +70,12 @@ Motion の Artifact 型は作成時に Style・Colors・Length を質問する "bgm": "Calm, focused electronic music for a cloud security explainer: soft pulsing synth, light percussion, 100 BPM", "scenes": [ { "id": "title", "name": "タイトル", "narration": "ワークロードアイデンティティフェデレーションで、…" }, - { "id": "token", "name": "トークン発行", "narration": "…" } + { + "id": "token", + "name": "トークン発行", + "narration": "…", + "sfx": [{ "at": 1.2, "prompt": "soft UI pop", "duration": 0.6 }] + } ] } ``` @@ -77,7 +83,8 @@ Motion の Artifact 型は作成時に Style・Colors・Length を質問する - `id` は英小文字・数字・`-`。Motion の場面の id と同じにする - `narration` は TTS に読ませる文。英語の固有名詞・略語はカタカナで書く(`GitHub Actions` → `ギットハブアクションズ`)。 画面の文字は元の表記のまま -- 任意: `voice`・`speed`・`bgmVolume`(既定 0.15)・`lead`・`tail`・`endHold` +- 任意: `voice`・`speed`・`bgmVolume`(既定 0.15)・`sfxVolume`(既定 1)・`lead`・`tail`・`endHold` +- 任意: 場面ごとの `sfx`。`at` は場面の頭からの秒、`duration` は 0.5〜10 秒(既定 1.5)、`volume` は 0〜1(既定 `sfxVolume`) ## 手順 @@ -107,7 +114,10 @@ motion-render narrate - 質問はしない(上の「決め方」) - `scenes.json` の場面の `id`・`dur` は `timing.json` と同じにする - Motion 内蔵の BGM(`score.js`)は作らない。音はこのスキルが付ける + - 効果音を付けるときは、タイトルの場面に `🎧:11.ai` を小さく入れる(下の「効果音」) 3. 並行して BGM を作ってよい: `motion-render bgm ` +4. 効果音を付けるときは、場面ごとに「何秒目に何が出るか」を決めた値で `script.json` の `sfx` を書き、 + `motion-render sfx ` で作る(下の「効果音」) ### 4. 映像データを取得して確認する @@ -116,6 +126,8 @@ motion-render narrate 3. `motion-render prepare /store /film` 4. `motion-render stills /film /stills <各場面の中ほどの秒をカンマ区切り>` で全場面を見る。 見出しの不自然な改行、はみ出し、重なりがあれば Motion 側を直し、手順 4 をやり直す +5. 効果音があれば、各効果音の秒(場面の開始 + `at`)の静止画も書き出し、鳴らす対象がその時点で画面に出ているかを見る。 + ずれていれば `at` を直して `motion-render sfx` をやり直す(作り直されるのは変えたものだけ) ### 5. MP4 にする @@ -123,12 +135,15 @@ motion-render narrate motion-render build [<出力.mp4>] ``` -ナレーションを各場面の頭に置き、BGM を一定の音量で重ねて MP4 にする。ナレーションか BGM が無いと止まる。 +ナレーションを各場面の頭に置き、BGM を一定の音量で重ね、効果音をそれぞれの秒に置いて MP4 にする。 +ナレーションか BGM、または `script.json` にある効果音が無いと止まる。 ### 6. 確認して報告する - `ffprobe` で、長さと映像・音声の両トラックがあることを見る - 報告: 出力のパス、長さ、場面の流れ、BGM の雰囲気、Motion の URL。耳で聞いていないことは聞いていないと書く +- 効果音を付けたときは、試聴用の `audio/sfx/preview.m4a` のパスと、その中の順番(`motion-render sfx` の出力)も報告し、 + 合わない音があれば教えてほしいと頼む。Claude には音の良し悪しが判定できないため、ここは人が聞いて決める ## BGM @@ -144,6 +159,34 @@ motion-render build [<出力.mp4>] 利用者が BGM を「ださい」などと言ったら、`bgm` を書き直して `motion-render bgm` → `motion-render build` をやり直す。 映像の描き直しは要らない(`build` は毎回描画するが、Motion 側の変更は不要)。 +## 効果音 + +`ELEVENLABS_API_KEY` があるときだけ付ける。無ければ付けずに進め、報告で「効果音はキーが無いので付けていない」と書く。 + +置き方: + +- 画面で目に見える出来事にだけ置く: カードが現れる、チップが着く、流れ図の段が光る、場面が切り替わる。 + `at` は Motion の場面で、その動きが始まる秒にする(自分で書いたアニメーションの秒をそのまま使う) +- 少なく: 1 場面 0〜2 個。毎回鳴る音は耳障りになる。同じ種類の出来事には同じ `prompt` と `duration` を使う + (音がそろい、同じものは 1 回しか作られない) +- 短く: 0.5〜1.5 秒。長い上昇音・環境音・音楽・声は頼まない +- `prompt` は英語で、柔らかさを指定する(`soft`・`subtle`・`gentle`)。ナレーションより前に出ないこと + +| 出来事 | `prompt` の例 | `duration` | +| ------------------ | -------------------------------- | ---------- | +| カード・部品が出る | soft UI pop | 0.6 | +| チップが移動する | quick airy whoosh | 0.8 | +| 段が光る・強調 | gentle glassy chime | 1.0 | +| 完了・成功 | soft positive notification chime | 1.2 | + +`build` は、効果音ごとに音量をナレーションの平均より 4 dB 下にそろえ(生成される音は 30 dB 以上ばらつく)、 +`volume` を掛け、`duration` で切り、両端をフェードしてから混ぜる。`volume` を下げすぎると話し声に埋もれて聞こえない +(10 dB 下で聞こえなくなった)。無音で返ってきた効果音は `sfx` が作り直し、3 回とも無音なら止まる。 + +**表記**: 月額プランなしで作った効果音は、商用に使えず、公開するときは題名に `elevenlabs.io` か `11.ai` を入れる +(ElevenLabs のヘルプ「Can I publish the content I generate on the platform?」)。効果音を付けた動画では、 +タイトルの場面に `🎧:11.ai` を小さく入れる。利用者が月額プランに入っていると言った場合は省いてよい。 + ## 既存の Motion の動画を MP4 にするとき URL を渡された場合も音を付ける。映像データを取得し(手順 4)、`scenes.json` の場面ごとに、その秒数に収まる @@ -155,6 +198,7 @@ URL を渡された場合も音を付ける。映像データを取得し(手 - `scripts/cli.ts`: `motion-render` コマンド(TypeScript。Node 22.18 以上がそのまま実行する) - `scripts/audio.ts`: Gemini TTS・Lyria の呼び出し(90 秒の上限と再試行付き)と音声の合成 +- `scripts/sfx.ts`: ElevenLabs による効果音の生成(`sfx`)、音量の測定とそろえ方、試聴用ファイル - `scripts/work.ts`: `script.json` の検証、`timing.json` の計算、ナレーションを動画の場面の開始時刻に置く処理 - `scripts/render.ts`: プレーヤーを開いてコマを返す。描画中は外部への通信を遮断し、Chromium の sandbox を有効にする - `scripts/json.ts`: ファイル・API・プレーヤーから来る JSON を、形を確かめながら読む diff --git a/skills/motion-render/scripts/audio.ts b/skills/motion-render/scripts/audio.ts index 200488e..22198f8 100644 --- a/skills/motion-render/scripts/audio.ts +++ b/skills/motion-render/scripts/audio.ts @@ -7,7 +7,7 @@ import { errorMessage, field, readJson } from "./json.ts"; import type { Script } from "./work.ts"; const API = "https://generativelanguage.googleapis.com/v1beta/models"; -const TIMEOUT_MS = 90_000; // a TTS request once hung for 15 minutes without this +export const TIMEOUT_MS = 90_000; // a TTS request once hung for 15 minutes without this const RETRIES = 3; interface Audio { @@ -15,10 +15,19 @@ interface Audio { data: Buffer; } -// Where each scene's narration goes on the film timeline. +// One sound effect on the film timeline. `gain` multiplies its volume (it already holds the leveling). +export interface SfxTrack { + file: string; + start: number; + duration: number; + gain: number; +} + +// Where each scene's narration and each sound effect go on the film timeline. export interface MixTiming { scenes: { id: string; start: number }[]; total: number; + sfx?: SfxTrack[]; } // ffmpeg-static downloads its binary in a postinstall script, which Claude Code's plugin dependency @@ -51,7 +60,7 @@ export function findFfmpeg(): string { throw new Error("ffmpeg not found: install ffmpeg or set FFMPEG_PATH"); } -function runFfmpeg(args: string[]): Promise { +export function runFfmpeg(args: string[]): Promise { return new Promise((ok, fail) => { const ff = spawn(findFfmpeg(), ["-y", "-loglevel", "error", ...args], { stdio: ["ignore", "inherit", "inherit"], @@ -63,6 +72,44 @@ function runFfmpeg(args: string[]): Promise { }); } +// Average and peak level of an audio file in dBFS (ffmpeg's volumedetect). Silence reads as -91 dB or lower. +export function volumeStats(file: string): Promise<{ mean: number; max: number }> { + return new Promise((ok, fail) => { + const args = ["-hide_banner", "-nostats", "-i", file, "-af", "volumedetect", "-f", "null", "-"]; + const ff = spawn(findFfmpeg(), args, { stdio: ["ignore", "ignore", "pipe"] }); + let log = ""; + ff.stderr?.on("data", (d: Buffer) => (log += d.toString())); + ff.on("error", fail); + ff.on("close", (code) => { + const level = (name: string): number => { + const v = new RegExp(`${name}: (-?[\\d.]+|-inf) dB`).exec(log)?.[1]; + return v === "-inf" ? -120 : Number(v ?? NaN); + }; + const mean = level("mean_volume"), + max = level("max_volume"); + if (code !== 0 || !Number.isFinite(mean) || !Number.isFinite(max)) + return fail( + new Error(`could not measure the level of ${file} (ffmpeg exited with ${code})`) + ); + ok({ mean, max }); + }); + }); +} + +// 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++) { + try { + return await attempt(); + } catch (e) { + last = e; + console.error(`motion-render: ${label} attempt ${i}/${RETRIES} failed: ${errorMessage(e)}`); + } + } + throw last; +} + // Picks the first inline audio part out of a generateContent response. function audioPart(json: unknown): { mimeType: string; data: string } | null { const candidates = field(json, "candidates"); @@ -82,33 +129,24 @@ function audioPart(json: unknown): { mimeType: string; data: string } | null { async function generateAudio(model: string, body: object): Promise