# 【自分の声TTS×Claude Code×FFmpeg】画面録画を放り込むだけで"自分の声"で解説するショート動画を作ってみた

- 公開日: 2026-06-27
- カテゴリ: AI・Claude Code
- 著者: tan（@kama_bizdev）
- URL: https://flow-with-tech.com/voice-clone-short-video-pipeline/
- サイト: 改善紀 — 好奇心の赴くままに。

---

## はじめに

このブログはClaude Code が自分のレポジトリを参照して書いています。私自身、全てを理解できているわけでもなく、精査できているわけでもありません。あくまでも、自分の備忘録として記しています。

## 何を作ったか

作ったアプリの紹介動画を、**画面録画を1本放り込むだけで、自分の声で解説するショート動画**として吐き出す仕組み。今回は研修運営アプリの紹介動画を、X 用の横長（16:9）と、リール／TikTok／YouTube ショート用の縦長（9:16）の2種類、同じ素材・同じ音声から出し分けた。

![](https://flow-with-tech.com/media/wp/2026/06/CleanShot-2026-06-27-at-14.09.45@2x.png)

中身は大きく3つのパートに分かれている。（らしい）

1. **自分の声を学習させた TTS（音声合成）** — テキストを渡すと本人っぽい声で読み上げる
2. **Claude Code による台本づくり** — 録画の中身を実際に見てナレーション原稿を組む
3. **FFmpeg による動画合成** — 背景・音声・字幕・口パクアバターを1本の mp4 に焼き込む

順番に書いていく。

---

## 1. 自分の声を学習させる（TTS）

音声合成エンジンは **GPT-SoVITS** を使っている。MIT ライセンスのオープンソースで、数分の音声からでも  
声をクローンできる few-shot 型が売り。これを自分の録音 **12.8分ぶん**でファインチューニングした。

学習も推論も**完全ローカル**で回している。Mac の GPU（MPS バックエンド）で動かしていて、合成時はネットワークに一切出ない。

クローンの精度は **SECS（話者埋め込みのコサイン類似度）** で客観評価した。

ゼロショット（学習なし）だと 0.58 くらいだったのが、ファインチューニングで 0.70、さらに**参照音声を学習スライスそのものに差し替えたら 0.83** まで上がった（本人帯 0.78〜0.87 に入る）。

ここで一番効いたのが、地味だけど **参照音声の選び方**だった。学習済みモデルに加えて、合成時に「この声色で喋って」と渡す参照クリップが声の質を大きく左右する。クリーンな別録りより、**学習に使ったスライスをそのまま参照にする**のが一番似た。0.65 → 0.83 はここで動いた。

パラメータの確定値はこのあたり：

- text split = `cut1`（句点で分割）
- temperature = `1.1`
- top_k = `20`
- 参照 = 学習スライス（5.9秒）＋その文字起こし

ハマったのは **temperature を下げると即 EOS で音声が崩壊する**こと。  
普通は temperature を下げると安定しそうなものだが、この声は **1.0〜1.1 が安全帯**で、下げると最初の一音で勝手に終了してしまう。ここは何度か焼き直してようやく腹落ちした。

---

## 2. 録画を見て台本を組む（Claude Code）

ここが今回いちばん「らしい」ところ。

ナレーション原稿は、**Claude Code に画面録画を渡して、フレームを切り出させて中身を判断させてから**書いている。

具体的には、録画から数秒おきにフレームを抜き出し（FFmpeg で `-ss <時刻> -i in.mp4 -frames:v 1 out.png`）、その画像を実際に見て「今どの場面で何をしているのか」を把握する。会場マップを作っている／席に着いている／ランダムで席替えしている／休憩タイマーを出している……といった場面の切り替わりを、画像ベースで地図にしてから台本に落とす。

これをサボると痛い目に遭う（らしい）。以前、ファイル名（例「交差点」）と実際の中身（実はサーキュレーターの配置）を取り違えて、ナレーションと映像が完全に食い違う動画を作ったことがある。  
なので **「映像に映っていないものは喋らない」「必ず自分（or Claude）の目でフレームを確認する」**を鉄則にした。（私は確認していない）

原稿は6〜7セグメントくらいに割って、各セグメントを `{セリフ, 背景動画+開始秒(seek), 字幕}` の組にする。細かいテクニックとして、**英略語・数字はカナ表記にする**（Power Apps → パワーアップス、3D → スリーディー）。TTS が読み崩すのを防ぐため。冒頭3秒はフック、最後は CTA、という定石も台本側で効かせている。

---

## 3. 動画に焼き込む（FFmpeg + PIL）

合成は `build_short.py` という1本のスクリプトに集約していて、**プロジェクトごとの設定 `short.json` を1枚書くだけ**で動く。中でやっていることはこんな流れ。

### 3-1. 音声の連結と尺の確定

各セグメントのナレーション wav を、間に無音（既定 0.22秒、最後だけ 0.5秒）を挟んで連結する。これで動画全体の尺とフレーム数（30fps）が決まる。**音声の尺が映像の尺を決める**設計にしてあるので、ナレーションがはみ出ることがない。

### 3-2. 口パク（ML なし）

アバターの口の開閉は、**機械学習を使わず音声の音量から作っている**。連結した音声を1フレーム幅ごとに区切って RMS（実効値）を計算し、エンベロープを取って 0〜1 に正規化。これを口の開き具合に直結させる。音が大きいフレームほど口が開く。あわせてアバター本体も軽く bob（上下）と squash（縦横の伸縮）をかけて、喋っている感を足している。アイコンは円形にクロップして、金色のリングを付けて表示する。

![](https://flow-with-tech.com/media/wp/2026/06/CleanShot-2026-06-27-at-14.05.26@2x.png)

*イラストと口がずれてるけど放置しています*

### 3-3. 背景映像の整形（縦横の出し分け）

ここが縦横2種類を出し分けている肝。各セグメントの背景を FFmpeg で作る。

- `vert`（fill）: 録画を `scale=...:force_original_aspect_ratio=increase,crop` で**画面いっぱいに切り詰める**。モーダルや全画面（休憩タイマーの 09:58 とか）が主役のシーンに使う。
- `horz`（fit）: 録画を1回 `split` して、片方を**ガウシアンぼかし（`gblur=sigma=28`）＋暗め**にして背景に敷き、もう片方を等倍フィットして中央に重ねる。16:9 のソースを 9:16 に収めるとき、上下の余白をぼかし背景で埋める定番の見せ方。アプリ全景を見せたいシーン（フック・会場づくり・CTA）に使う。

横長版（16:9）は `short.json` に `width`/`height` を渡せば同じスクリプトで出る。  
字幕の Y 位置（`sub_cy_frac`）やアバター位置（右下 `br` など）も設定で寄せられるようにした。

### 3-4. 字幕

字幕は libass などを使わず、**PIL で直接 PNG に描いている**（フォントはヒラギノ角ゴ W6）。  
縁取り付きで、2行レイアウトを中央寄せ。フレームごとに「今どのセグメントか」を尺から逆算して、そのセグメントの字幕を焼く。

### 3-5. 最終 mux

背景セグメントを `concat` で1本に繋ぎ、その上にアバター＋字幕の PNG 連番を `overlay` で重ね、最後にナレーション音声を `-map` で乗せて mux。出力は `libx264 / yuv420p / +faststart` 相当の素直な mp4。

---

## ハマったところ（次にまた踏むやつ）

技術的に効いた教訓を2つだけ残しておく。

**(1) FFmpeg の `-ss` は `-i` の「後ろ」に置く（正確シーク）**  
最初、開始秒を `-ss` で `-i` の**前**に置いていた。これは高速シークで普段は問題ないが、`-stream_loop` と `fps` フィルタと長尺エンコードが絡むと、**指定した秒から数秒ずれた場所から再生される**。`-i` の後ろに置く正確シークに直して解消した。

**(2) オフセットより「尺ドリフト」が本質だった**  
背景と字幕が合わない症状を最初は「開始位置がずれている」と思ったが、本当の原因は別だった。各セグメントは指定した開始秒から**そのセグメントの尺ぶん再生され続ける**。今回の録画はモーダルが多く、例えば「着席済みマップ」がきれいに映っているのはソースの 4.9〜7.2秒のわずか 2.3秒だけ。セグメントの尺がそれより長いと、再生中にモーダルが閉じて別画面に流れてしまう。**「字幕に合う絵がセグメントの尺ぶん持続するか」を場面ごとに検証**して、足りなければ原稿を詰めるか seek 窓を選び直す、というのが結論だった。

検証は完成動画から各セグメントの開始・中盤・終盤でフレームを抜いて、字幕と背景が一致しているか目視している。

---

## できあがってみて

きっかけはTTS（Text To Speech ）を「ながらAIラジオ」で知ったことでした。Claude Code にこんなのあるらしいけどどうやればいいの？と相談から始めました。なんでもそうですが、自分で実際にやってみると、わかることがたくさんあると感じました。まだ、**仕組みは完全に理解できていません**が、とりあえず動いたから良しということで！

> 🗣 簡単に試せるのは、生成AIのおかげです。ありがとうございます😊
