VOICEVOXで動画にナレーションを自動で入れるには、台本をカットごとに分けてVOICEVOXエンジンのAPI(audio_query → synthesis)で音声を作り、カットの上限秒数を超えたら話す速さ(speedScale)を少しずつ上げて作り直します。できた音声をRemotionで映像のカットに重ね、最後にffmpegで音量を揃えれば、ナレーション入りの動画を書き出せます。
VOICEVOXのエディタで1本ずつ読み上げを作り、動画編集ソフトに並べる方法でもナレーションは入ります。困るのは本数が増えたときです。台本を1行直すたびに音声を書き出し直し、カットからはみ出した音声の速さを手で調整し、クレジットを書き忘れていないかを毎回見直すことになります。
この記事では、筆者がCodeQuest.workのツール紹介ショート(YouTubeチャンネル@codequest_work)で使っている手順を、汎用のサンプルに書き直して紹介します。手元の環境はVOICEVOX 0.25.2、Node.js 23、Remotion 4.0.537、ffmpeg 8.0.1で、サンプルコードはこの環境で実際に動かしました。キャラクターの利用規約とAPIの仕様は、2026年10月21日に公式ページで確認しています。
全体の流れ|台本からナレーション入り動画まで
ナレーション入りの動画は、台本をカットに分ける、音声を作る、尺に収める、映像に重ねる、音量を揃える、の5つの工程で作ります。どの工程もコマンドで実行できるので、台本を直したら最初から流し直すだけで動画が作り直されます。
| 工程 | 使うもの | できるもの |
|---|---|---|
| 1. 台本をカットに分ける | JSONファイル | カットごとのID・文・上限秒数 |
| 2. 音声を作る | VOICEVOXエンジンのAPI | カットごとのWAV |
| 3. 尺に収める | speedScaleの自動調整 | 上限秒数に収まったWAV |
| 4. 映像に重ねる | Remotion | ナレーション入りのMP4 |
| 5. 音量を揃える | ffmpegのloudnorm | -14 LUFS前後のMP4 |
2と3は1本のNode.jsスクリプトにまとめます。どんなカットに分けるかの考え方はAI動画のカット割りの作り方で、できた動画をYouTubeに上げる工程はYouTube Data APIで動画をアップロードする手順で解説しています。
映像とナレーションを動画生成AIに一度に作らせる方法もあり、その作例はAIでMV・CMを作る手順にまとめています。この記事は、映像を先に用意して、ナレーションを後から付ける方法です。台本どおりの言い回しで読ませたいときや、同じ形式の動画を何本も作るときに向いています。
VOICEVOXのAPIを呼ぶ|audio_query → synthesisの2段階
VOICEVOXの音声は、POST /audio_query で読み上げ用のクエリを作り、そのクエリを POST /synthesis に渡すとWAVで返ってきます。VOICEVOXのアプリを起動すると、裏でエンジンが http://127.0.0.1:50021 で待ち受けます。エンジンだけを単体で動かすこともできますが、手元で使うならアプリを起動しておくのが手軽です。
VOICEVOXエンジンのREADMEも、この2段階をcurlのサンプルとして載せています。アプリを起動した状態で http://127.0.0.1:50021/docs を開くと、起動中のエンジンのAPIドキュメントを確認できます。
curlで動作を確認する
まず、エンジンが起動しているかとAPIが通るかを確かめます。1行目でバージョン(筆者の環境では “0.25.2”)が返れば起動しています。
curl -s 127.0.0.1:50021/version
# 読み上げ用のクエリを作り、そのまま合成に渡す
curl -s -X POST "127.0.0.1:50021/audio_query?speaker=14" --get --data-urlencode "text=こんにちは" > query.json
curl -s -H "Content-Type: application/json" -X POST -d @query.json "127.0.0.1:50021/synthesis?speaker=14" > hello.wavhello.wav が再生できればAPIは動いています。初期値のまま合成したWAVは、24000Hzのモノラルでした。
speakerに渡すIDを調べる
speaker には、声とスタイルの組み合わせごとのIDを渡します。READMEによると、この値は /speakers で得られるスタイルのIDで、互換性のために speaker という名前になっています。手元の環境では、冥鳴ひまりのノーマルが14、四国めたんのノーマルが2、ずんだもんのノーマルが3でした。
curl -s 127.0.0.1:50021/speakers | jq -c '.[] | {name, id: .styles[0].id}'
# {"name":"四国めたん","id":2}
# {"name":"ずんだもん","id":3}
# {"name":"春日部つむぎ","id":8}audio_queryで変えられる主な値
audio_query の返り値は、そのまま合成に使える初期値です。変えたい値だけ書き換えて synthesis に渡します。値の意味はAPIドキュメントのAudioQueryで確認できます。
| 項目 | 意味 | 初期値(実測) |
|---|---|---|
| speedScale | 全体の話速 | 1.0 |
| prePhonemeLength | 音声の前の無音(秒) | 0.1 |
| postPhonemeLength | 音声の後の無音(秒) | 0.1 |
| volumeScale | 音量 | 1.0 |
| outputSamplingRate | 出力のサンプリングレート(Hz) | 24000 |
| outputStereo | ステレオで出力するか | false |
動画に使うなら、outputSamplingRate を48000にしておくと扱いやすくなります。YouTubeの推奨アップロード設定でも、音声のサンプルレートは48kHzです。
カットごとに音声を作る|台本の書き方とファイルの分け方
ナレーションは、映像のカットと同じ単位で1ファイルずつ作ります。1本の長い音声にすると、1カットの言い回しを直すだけで全体を作り直すことになり、映像との位置合わせも手作業になります。カットごとに分けておけば、映像側はカットの頭に音声を置くだけで済みます。
台本はJSONに書く
台本には、カットごとにID・読み上げる文・上限秒数を持たせます。IDは音声のファイル名にも使います。
[
{ "id": "01", "text": "CSSアニメーションを、コピペで使えます。", "maxSeconds": 2.8 },
{ "id": "02", "text": "再生速度やイージング、繰り返し回数も変えられます。", "maxSeconds": 2.8 },
{ "id": "03", "text": "無料で使えます。CodeQuest.workで検索。", "maxSeconds": 2.8 }
]上限秒数(maxSeconds)は、映像のカットの長さより少し短くします。筆者のショートでは、カットの長さから0.2秒引いた値にしています(3.4秒のカットなら3.2秒)。言い終わってから画面が切り替わるまでに少し間ができ、次のカットの声とも重なりません。
1カットに入る文の長さ
3秒前後のカットなら、20字前後の一文が目安です。冥鳴ひまりのノーマルで実測すると、25字の「再生速度やイージング、繰り返し回数も変えられます。」は等速で4.12秒かかり、3秒に収めるには話す速さを1.4まで上げる必要がありました。読点の位置でも長さが変わるので、迷ったら一度合成して長さを確かめます。
尺に収める|上限秒数を超えたら話す速さを上げる
カットの尺に収めるには、少し速めのspeedScaleで合成し、長さが上限秒数を超えていたら0.05ずつ上げて作り直します。上げすぎると聞き取りにくくなるので、上げる上限も決めておき、そこまで上げても収まらないときは台本を短くします。
サンプルコード
台本のJSONを読み、カットごとにWAVを書き出すNode.jsのスクリプトです。Node.js 18以降なら、標準のfetchだけで動きます。
// VOICEVOXでカットごとのナレーションを作り、上限秒数に収まるまで話す速さを上げる
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
const API = 'http://127.0.0.1:50021';
const SPEAKER = 14; // 冥鳴ひまり ノーマル(IDは /speakers で確認する)
const READING = [['CodeQuest.work', 'コードクエストドットワーク'], ['GSAP', 'ジーサップ']];
const toReading = (text) => READING.reduce((t, [from, to]) => t.replaceAll(from, to), text);
// WAVのヘッダーから長さ(秒)を出す
function wavSeconds(buf) {
const byteRate = buf.readUInt32LE(28);
const dataSize = buf.readUInt32LE(buf.indexOf('data') + 4);
return dataSize / byteRate;
}
async function synth(text, speedScale) {
const q = await fetch(`${API}/audio_query?speaker=${SPEAKER}&text=${encodeURIComponent(text)}`, { method: 'POST' });
if (!q.ok) throw new Error(`audio_queryに失敗しました: ${q.status}`);
const query = { ...(await q.json()), speedScale, outputSamplingRate: 48000 };
const res = await fetch(`${API}/synthesis?speaker=${SPEAKER}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(query),
});
if (!res.ok) throw new Error(`synthesisに失敗しました: ${res.status}`);
return Buffer.from(await res.arrayBuffer());
}
const cuts = JSON.parse(readFileSync('cuts.json', 'utf8'));
mkdirSync('voice', { recursive: true });
const results = [];
for (const { id, text, maxSeconds } of cuts) {
let speed = 1.15;
let wav = await synth(toReading(text), speed);
while (wavSeconds(wav) > maxSeconds && speed < 1.6) {
speed = +(speed + 0.05).toFixed(2);
wav = await synth(toReading(text), speed);
}
writeFileSync(`voice/${id}.wav`, wav);
results.push({ id, speed, seconds: +wavSeconds(wav).toFixed(2) });
}
console.table(results);実行すると、カットごとに決まった速さと長さが表で出ます。
node narrate.mjs| id | speed | seconds |
|---|---|---|
| 01 | 1.2 | 2.74 |
| 02 | 1.5 | 2.77 |
| 03 | 1.4 | 2.76 |
3カットとも上限の2.8秒以内に収まりました。25字ある2つ目は1.5まで、英語の読み替えが入る3つ目は1.4まで上がっています。
長さはWAVのヘッダーから読む
長さはffprobeでも測れますが、WAVのヘッダーには1秒あたりのバイト数とデータの大きさが入っているので、割り算で求められます。サンプルではこの方法で、外部コマンドを使わずに判定しています。同じファイルをffprobeで測った値との差は0.01秒未満でした。
速さと長さの関係(実測)
同じ文をspeedScaleだけ変えて合成すると、長さは次のようになりました(冥鳴ひまり ノーマル、25字の文)。
| speedScale | 長さ |
|---|---|
| 1.0 | 4.12秒 |
| 1.15 | 3.56秒 |
| 1.4 | 2.95秒 |
| 1.6 | 2.58秒 |
長さはほぼspeedScaleに反比例します(4.12 ÷ 1.15 ≒ 3.58)。エンジンの実装では、前後の無音を付けたあとで全体をspeedScaleで割っているため、前後の無音(初期値0.1秒ずつ)も一緒に短くなります。無音を固定の秒数として差し引く計算はしないでください。
始める速さと上限の決め方
筆者は1.15から始めて、1.6を上限にしています。公開・予約した5本のショートの30カットでは、18カットが1.15のまま収まり、残りの12カットは1.2〜1.45まで上げて収まりました。1.6まで上げたカットはありません。VOICEVOXのエディタのスライダーは0.5〜2.0の範囲ですが、APIでは0より大きい値なら受け付けます。
英語の読み替え表|固有名詞を正しく読ませる
英語の固有名詞は、カタカナの読みに置き換えてからVOICEVOXに渡します。辞書にない英語は、アルファベットを1字ずつ読んだり、記号を読み飛ばしたりします。読みは audio_query の返り値の kana で確かめられます。
curl -s -X POST "127.0.0.1:50021/audio_query?speaker=14" --get --data-urlencode "text=GSAPで動かす" | jq -r .kana
# ジ'イエスエエピイデ/ウゴカ'ス| 台本の表記 | そのままの読み(kana) | 読み替え後 |
|---|---|---|
| CodeQuest.work | コードクエスト、ワーク(ドットを読まずに間が空く) | コードクエストドットワーク |
| GSAP | ジーエスエーピー | ジーサップ |
置き換えは台本ではなくスクリプトの中で行います。サンプルの READING がその表です。台本の文をそのまま画面のテロップにも使えば、テロップは英語のまま、読み上げだけカタカナにできます。
キャラクター別の利用規約|商用で使える声とクレジット
VOICEVOXのソフトは商用・非商用を問わず使えますが、作った音声を商用で使えるかどうかは、キャラクター(音声ライブラリ)ごとの規約で決まります。VOICEVOXのソフトウェア利用規約は「作成された音声を利用する際は、各音声ライブラリの規約に従ってください」と定め、VOICEVOXを使ったことがわかるクレジットも求めています。
以下は、個人で運営するサイトの宣伝用ショートに使う前提で、各キャラクターの規約を2026年10月21日に読んだ結果です。規約は変わるので、使う前に必ず規約のページを読み直してください。
申請なしで商用利用できる声
| キャラクター | 規約の発行元 | クレジットの書き方 | 補足 |
|---|---|---|---|
| 冥鳴ひまり | 冥鳴ひまり公式 | VOICEVOX:冥鳴ひまり | 個人・法人とも申請や報告は不要 |
| 四国めたん・ずんだもん・九州そら・東北ずん子 ほか | SSS合同会社(zunko.jp) | VOICEVOX:四国めたん など | クレジットなしの商用利用は有償の契約 |
| 玄野武宏・白上虎太郎・雀松朱司・麒ヶ島宗麟・黒沢冴白 | VirVox Project | VOICEVOX:玄野武宏(CV:ガロ) の形 | 規約の表記例に声優名(CV)が入っている |
| 春日部つむぎ | 春日部つむぎ公式 | VOICEVOX:春日部つむぎ | 動画内か概要欄に表記 |
| 剣崎雌雄 | 銃口會 | VOICEVOX:剣崎雌雄 | 剣崎雌雄の名称も表記 |
| 雨晴はう | Amehare Project | VOICEVOX:雨晴はう | 必須なのはVOICEVOXの表記。キャラクター名は任意 |
| 満別花丸 | 満別花丸公式 | VOICEVOX:満別花丸 | 個人・法人とも連絡不要 |
| 小夜/SAYO | SAYO Project | VOICEVOX:小夜/SAYO | 表記例は「VOICEVOX:小夜」も可 |
VirVox Projectの声は、規約の表記例が「VOICEVOX:玄野武宏(CV:ガロ)」のように声優名まで入っています。声優名が必須かどうかは規約に書かれていないので、迷ったら表記例どおりに入れておくのが確実です。
クレジットを書かない商用利用は1キャラクター40万円の契約
四国めたんやずんだもんの規約(zunko.jp)では、「クレジット表記をしない商用利用については、利用料を1キャラクター毎に40万円(+消費税)で契約させていただきます」と定めています。対象はずんだもん・東北ずん子・東北きりたん・東北イタコ・四国めたん・九州そら・中国うさぎ・中部つるぎ・あんこもんの9キャラクターです。
裏を返すと、クレジットを1行書けば無料で使えます。書く場所について同じ規約は、「動画サイトの場合は説明画面や動画内のクレジットなど、ユーザーが気になって見にいった際にはわかる程度のところに記載をお願いします」としています。
商用で使う前に確認が要る声
| キャラクター | 規約で確認が要る点 |
|---|---|
| 青山龍星 | 企業・個人事業主は、収益の有無にかかわらず事前申請が必要(VirVox Project) |
| No.7 | 企業アカウントの広告収入などは商用扱いで、有償のライセンス(目安15,000円〜)(No.7公式) |
| ユーレイちゃん | 商用・営利目的での利用と法人の利用は許諾が必要(ユーステラ) |
| ぞん子 | 商用利用は問い合わせが必要(ZONe ENERGY) |
| 夜語トバリ・暁記ミタマ | 法人利用と商用・営利目的は問い合わせが必要(YOGATARI TOBARI Project) |
| 里石ユカ | 生成AIを使った作品での利用は禁止で、営利利用は事前相談(里石ユカ公式) |
夜語トバリ・暁記ミタマは、VOICEVOX公式サイトのキャラクターページに「クレジットを記載すれば商用・非商用で利用可能」という共通の案内文が出ていますが、提供元の規約では商用目的は問い合わせが必要です。食い違うときは、提供元の規約に従ってください。
クレジットは台本と同じファイルで管理する
筆者は、声のIDとクレジットの文字列を台本のJSONに一緒に持たせ、動画の最後の画面と説明欄の両方にこのクレジットを入れています。声を替えたのにクレジットだけ古いまま、という書き間違いを防げます。
Remotionでナレーションを映像に重ねる
Remotionでは、カットごとに<Sequence>で開始フレームと長さを決め、その中に@remotion/mediaの<Audio>で音声を置きます。Remotionの公式ドキュメントは、音声ファイルを public/ に置いて staticFile() で参照する方法を案内しています。
音声の部品は、@remotion/mediaの<Audio>が現在の推奨です。remotion パッケージの古い<Audio>は<Html5Audio>に名前が変わっているので、古い記事のimportをそのまま写さないでください。@remotion/media は別のパッケージなので、remotion と同じバージョンを指定して追加します(例は remotion 4.0.537 の場合)。
npm i @remotion/media@4.0.537カットごとに音声を置くコンポーネント
前の章で作った voice/01.wav などを public/voice/ に置いた前提です。1カット3秒(30fpsで90フレーム)に固定した最小の例です。
import { AbsoluteFill, Sequence, staticFile } from 'remotion';
import { Audio } from '@remotion/media';
const CUTS = [
{ id: '01', caption: 'コピペで使える' },
{ id: '02', caption: '速さも変えられる' },
{ id: '03', caption: '無料で使えます' },
];
const CUT_FRAMES = 90; // 1カット3秒(30fps)
export const Narration = () => (
<AbsoluteFill style={{ backgroundColor: '#0f172a' }}>
{CUTS.map((cut, i) => (
<Sequence key={cut.id} from={i * CUT_FRAMES} durationInFrames={CUT_FRAMES}>
<AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center', color: '#fff', fontSize: 80 }}>
{cut.caption}
</AbsoluteFill>
<Audio src={staticFile(`voice/${cut.id}.wav`)} />
</Sequence>
))}
</AbsoluteFill>
);import { Composition } from 'remotion';
import { Narration } from './Narration';
import { OldMono, NewMono, OldSt, NewSt } from './T';
const c = { durationInFrames: 270, fps: 30, width: 320, height: 320 };
export const Root = () => (
<>
<Composition id="Narration" component={Narration} {...c} width={1080} height={1920} />
<Composition id="OldMono" component={OldMono} {...c} />
<Composition id="NewMono" component={NewMono} {...c} />
<Composition id="OldSt" component={OldSt} {...c} />
<Composition id="NewSt" component={NewSt} {...c} />
</>
);書き出すコマンド
npx remotion render src/index.ts Narration out/narration.mp4 --codec=h264 --crf=18renderコマンドのドキュメントによると、–codec を省くとH.264で書き出されます。エンコードのガイドでは、H.264のCRFは1〜51で既定は18、小さいほど高画質です。
Remotionのライセンス
RemotionのLICENSEでは、個人と従業員3人までの営利団体は、商用でも無料で使えます。それより大きい営利団体はCompany Licenseが必要です。LICENSEには「Remotion 5.0でライセンスが少し変わる」という告知もあるので、5.0に上げるときは読み直してください。
音量を-14 LUFSに揃える
書き出した動画は、ffmpegのloudnormフィルタで音量を-14 LUFS前後に揃えます。合成した音声をそのまま重ねると、動画全体の音量は小さめになります。先ほどのサンプルを書き出したままの状態で測ると-21.7 LUFSで、揃えたあとは-14.2 LUFSになりました。
ffmpeg -y -i out/narration.mp4 -af loudnorm=I=-14:TP=-1.5 -ar 48000 -c:v copy -c:a aac -b:a 192k out/narration-14.mp4ffmpegの公式ドキュメントによると、loudnormはEBU R128にもとづくラウドネスの正規化フィルタで、I は統合ラウドネスの目標(既定-24.0)、TP は最大トゥルーピーク(既定-2.0)を指定します。音量を動的に揃えるモードでは内部で192kHzにアップサンプルされるので、-ar 48000 で出力のサンプリングレートを指定しておきます。映像は -c:v copy で再エンコードせずにそのまま使います。
-14 LUFSはYouTubeの公式値ではない
YouTubeのヘルプ(動画の音量を調整する)には、再生時の「Stable volume」が既定でオンになり、小さい音と大きい音の差を均すと書かれていますが、目標のLUFSは公表されていません。-14 LUFSは、配信サービスの多くが使う値として音響エンジニアの間で目安にされている数字です。目安として揃えておくと、ほかの動画と並んだときに極端に小さく聞こえることを防げます。
揃ったかを測る
ffmpeg -i out/narration-14.mp4 -af ebur128=peak=true -f null - 2>&1 | grep -A 1 "Integrated loudness"
# Integrated loudness:
# I: -14.2 LUFS作例|@codequest_workのショート動画
この手順で作ったショートを、YouTubeチャンネル@codequest_workで公開しています。下の動画は1本目で、Liquid Glass CSSジェネレーターを紹介しています。ナレーションは冥鳴ひまりで、6カット・約20秒です。
2本目以降は、規約で申請なしの商用利用を確認できた声の中から、1本ごとに声を替えています。クレジットは動画の最後の画面と説明欄の両方に入れています。
つまずいた点と確認方法
つまずきやすいのは、エンジンへの接続、英語の読み、尺からのはみ出し、音量の4つです。症状から原因を引けるように表にしました。
| 症状 | 原因 | 確認・対処 |
|---|---|---|
| スクリプトが fetch failed で止まる | VOICEVOXのアプリが起動していない | curl -s 127.0.0.1:50021/version でバージョンが返るか確かめる |
| 英語が1字ずつ読まれる・記号が飛ぶ | 辞書にない表記 | audio_query の kana で読みを確かめ、読み替え表に足す |
| 上限まで速くしても収まらない | 台本が長すぎる | 文を短くするか、映像のカットを延ばす |
| 声が次のカットに食い込む | 上限秒数をカットの長さと同じにしている | 上限秒数をカットより0.2秒ほど短くする |
| 書き出した動画の音が小さい | 合成音声をそのまま重ねている | loudnormで-14 LUFS前後に揃える |
| loudnorm後のサンプリングレートが高すぎる | 動的なモードで192kHzにアップサンプルされる | -ar 48000 を付ける |
よくある質問
Q. VOICEVOXのナレーションはYouTubeで商用利用できますか?
キャラクターごとの規約によります。冥鳴ひまりや四国めたん、春日部つむぎなどは「VOICEVOX:キャラ名」のクレジットを書けば申請なしで商用利用でき、青山龍星やNo.7のように事前申請や有償の契約が必要な声もあります。
Q. VOICEVOXのクレジットはどこに書けばいいですか?
動画内か説明欄の、見ればわかる場所に書きます。zunko.jpの規約は「説明画面や動画内のクレジットなど、ユーザーが気になって見にいった際にはわかる程度のところ」としており、筆者は動画の最後の画面と説明欄の両方に入れています。
Q. VOICEVOXのAPIはどこで動いていますか?
VOICEVOXのアプリを起動すると、手元のパソコンの http://127.0.0.1:50021 でエンジンが動きます。起動中は http://127.0.0.1:50021/docs でAPIドキュメントを確認できます。
Q. VOICEVOXのAPIで話す速さはどこで変えますか?
audio_query の返り値にある speedScale を書き換えて synthesis に渡します。1.0が等速で、値を大きくすると速くなり、前後の無音も同じ割合で短くなります。
Q. ナレーションをカットの長さに合わせるにはどうすればいいですか?
カットごとに上限秒数を決め、合成した音声が上限を超えたら speedScale を0.05ずつ上げて作り直します。筆者は1.15から始めて1.6を上限にし、それでも収まらないときは台本を短くしています。
Q. YouTubeの音量は何LUFSに合わせればいいですか?
YouTubeは目標のLUFSを公表していないため、配信向けの目安としてよく使われる-14 LUFS前後に揃えるのが無難です。ffmpegなら loudnorm=I=-14:TP=-1.5 で揃えられます。
