VOICEVOXでナレーションを自動で入れる|動画の尺に合わせて速さを調整


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.wav

hello.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ずつ上げて作り直します。上げすぎると聞き取りにくくなるので、上げる上限も決めておき、そこまで上げても収まらないときは台本を短くします。

差分チェックツールで効率UPお手本コードと自分のコードを比較して、違いを一目で確認できます。練習前にブックマークしておくと便利です。
Diff Checkerを開く →

サンプルコード

台本の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
idspeedseconds
011.22.74
021.52.77
031.42.76

3カットとも上限の2.8秒以内に収まりました。25字ある2つ目は1.5まで、英語の読み替えが入る3つ目は1.4まで上がっています。

長さはWAVのヘッダーから読む

長さはffprobeでも測れますが、WAVのヘッダーには1秒あたりのバイト数とデータの大きさが入っているので、割り算で求められます。サンプルではこの方法で、外部コマンドを使わずに判定しています。同じファイルをffprobeで測った値との差は0.01秒未満でした。

速さと長さの関係(実測)

同じ文をspeedScaleだけ変えて合成すると、長さは次のようになりました(冥鳴ひまり ノーマル、25字の文)。

speedScale長さ
1.04.12秒
1.153.56秒
1.42.95秒
1.62.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 ProjectVOICEVOX:玄野武宏(CV:ガロ) の形規約の表記例に声優名(CV)が入っている
春日部つむぎ春日部つむぎ公式VOICEVOX:春日部つむぎ動画内か概要欄に表記
剣崎雌雄銃口會VOICEVOX:剣崎雌雄剣崎雌雄の名称も表記
雨晴はうAmehare ProjectVOICEVOX:雨晴はう必須なのはVOICEVOXの表記。キャラクター名は任意
満別花丸満別花丸公式VOICEVOX:満別花丸個人・法人とも連絡不要
小夜/SAYOSAYO ProjectVOICEVOX:小夜/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=18

renderコマンドのドキュメントによると、–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.mp4

ffmpegの公式ドキュメントによると、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 で揃えられます。


関連記事