朗読は聞こえているのに、画面の文字は先に進んでしまう。あるいは、声が終わってもまだ出続けている。
Paper Morning は、詩を入れると縦書きの筆文字で、動く背景つきの縦型ショート動画(9:16)にしてくれるツールです。毎日詩を投稿している方のために、2026-07-05 に作りました。朗読の機能は最初の日に入っていて、動画にも声は載せられました。ところが2か月ほどたった 2026-09-09、この「ずれ」を直す作業に入ります。
1文字は、声と関係なく出ていた
文字の出し方は、1文字あたり 0.07 秒の固定でした。声の長さとは、何の関係もありません。
声は、句読点のところで間を取ります。固定の速さで出る文字と、読む声が合うはずがありません。しかも、書き出し時のナレーションは初期設定でオフでした。切り替えに気づかなければ、動画は無音のまま出てしまう。まず、これをオンに変えた。
2音声合成は「何秒に何を読んだか」を返さない
文字を声に合わせるには、各行を何秒から何秒まで読んでいるかが要ります。ところが、使っている Gemini の音声合成(Aoede という声)は、音声のファイルを返すだけです。文字ごとの時刻は付いてきません。
そこで、返ってきた音声そのものを調べることにしました。やっていることは3段です。
1つめ。20ミリ秒ごとに音量(RMS)を測って、波形の起伏を作る。2つめ。音量が小さい区間、つまり無音のところで音声を区切って、「しゃべっている塊」に分ける。3つめ。その塊を、詩の行に当てはめる。
3しきい値を決めるのが、いちばん迷った
「無音」とみなす音量の線をどこに引くか。コードのコメントに、迷いがそのまま残っています。小さすぎると息継ぎを拾ってしまい、大きすぎると言葉の途中で切れる。
今の値は、いちばん大きい音(ピーク)の12%を下回ったら無音。無音が 0.22 秒以上続いたら区切る。0.18 秒に満たない塊は捨てる。短い無音で切らないのは、促音や破裂音の前に一瞬の間が入るからです。
ただ、この3つの数字をどうやって選んだのか、比べた記録は残っていません。実際の音声で試して合わせた、としか言えないのが正直なところです。
4行の数と、塊の数が合わないとき
塊の数と行の数がちょうど同じなら、順番に当てるだけで済みます。そう都合よくは揃いません。
塊の数と行の数が合わないときの扱い(lib/speechsync.ts)
最後に「行の開始時刻が前の行より早くならない」ことだけは必ず保証します。推定がぶれても、文字が逆戻りしないように。
5当たっていたのか
作った日のコミットには、確かめた結果が書かれています。実際の Gemini の音声で、検出した塊を1つずつ文字起こしすると、対応する行とぴったり同じ文字になった。書き出した MP4 でも、各行の開始は声から約 0.1 秒以内。
これは当時の記録です。今日あらためて測り直してはいません。そして、何本の詩で試したのかは、記録に書かれていません。短い詩ばかりだったのか、長い詩でも通るのかは、まだ分からないんです。
6同じ日に、メニューを7つから4つに減らした
同じ日の 18:22、別のコミットで、使い方のほうも直しました。詩・背景・書体・動き・保存・共有・詩集という7つのタブが並んでいて、初めて使う人にはどこから始めるか分からない。保存ボタンも、新しく書くボタンも見当たらなかったからです。
「書く/かざる/できあがり/詩集」の4つに。背景・文字・動きは中の小さなタブに入れて、保存と共有は「できあがり」の1画面にまとめました。詩は、入力が止まって 1.8 秒たつと自動で保存されます。
もう一つ、ムダも見つけています。ナレーションの音声を覚えておく場所が1件分しかなく、ページを開き直すたびに取り直していた。測ると、本当は2回で済む呼び出しが4回走っていました。直近12件を端末に覚えておく仕組みを足して、同じ詩を二度取りに行かないようにし、測り直したら2回になっています。
7正直に言うと
波形から探す方法は、きれいに間をあけて読む声には強いです。逆に、行と行の間をほとんどあけずに読み続ける声だと、塊が少なくなって、按分に頼ることになります。その場合のずれは、測っていません。
実際に毎日の投稿に使われているのか、動画が何本できたのかも、僕は数字を持っていません。分からないことは、分からないと書いておきます。
制作の経緯は 制作実績 に、ほかの開発の記録は ブログ にあります。長い詩での精度を測ったら、続きを追記します。