<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>キャッシュ on 思いつきそうで思いつかなくていたときに</title><link>https://blog.fuga.jp/tags/%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5/</link><description>Recent content in キャッシュ on 思いつきそうで思いつかなくていたときに</description><generator>Hugo -- gohugo.io</generator><language>ja</language><copyright>Copyright(c) 2022-2025 SATO Daisuke. All rights reserved.</copyright><lastBuildDate>Wed, 24 Jun 2026 06:00:00 +0900</lastBuildDate><atom:link href="https://blog.fuga.jp/tags/%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5/index.xml" rel="self" type="application/rss+xml"/><item><title>podcast-tool 開発日記 #3 複数エンジンとキャッシュ — 声の出どころを増やし、二度目を速くする</title><link>https://blog.fuga.jp/posts/2026-06-24-podcast-tool-devdiary-03-multi-engine-and-cache/</link><pubDate>Wed, 24 Jun 2026 06:00:00 +0900</pubDate><guid>https://blog.fuga.jp/posts/2026-06-24-podcast-tool-devdiary-03-multi-engine-and-cache/</guid><description>&lt;h2 id="前回までのあらすじ">&lt;a href="#%e5%89%8d%e5%9b%9e%e3%81%be%e3%81%a7%e3%81%ae%e3%81%82%e3%82%89%e3%81%99%e3%81%98" class="header-anchor">&lt;/a>前回までのあらすじ
&lt;/h2>&lt;p>第2回では、誕生当日の夜から翌々日にかけての「&lt;strong>音を整える&lt;/strong>」数日間を追った。話者ごとの速度調整、BGMのクロスフェードとループ、セクション間の無音、動的な音量調整——「とりあえず音が出る」から「聴けるものにする」へ、地味だが効く改良が密に積まれた。&lt;/p>
&lt;p>その締めで、第2回の予告にこう書いた。「次は増えすぎた &lt;code>main.py&lt;/code> にメスを入れ、&lt;code>pytest&lt;/code> と GitHub Actions を入れるところまで追う」と。実はその工事は、誕生からわずか3日後——&lt;strong>2025年7月14日に、ほぼ一日で済んでしまっていた&lt;/strong>。一枚岩だった &lt;code>main.py&lt;/code> は、設定読み込みが &lt;code>config_utils.py&lt;/code> へ（&lt;code>3f68288&lt;/code>）、VOICEVOX 通信が &lt;code>voice.py&lt;/code> へ（&lt;code>b4062da&lt;/code>）、BGM・オーディオ処理が &lt;code>mixer.py&lt;/code> へ（&lt;code>f5eb224&lt;/code>）と切り出され、その日のうちに &lt;code>pytest&lt;/code> 環境（&lt;code>pytest.ini&lt;/code>）とテスト実行用の GitHub Actions ワークフロー（&lt;code>test_and_coverage.yml&lt;/code>）まで載った。一枚岩は、思っていたよりあっさり崩れた。&lt;/p>
&lt;p>なので第3回は、その&lt;strong>分割され、テストの土台が入ったコード&lt;/strong>の上で初めて成立する話に進む。テーマは二つ。&lt;strong>声の出どころを増やす&lt;/strong>（複数エンジン対応）ことと、&lt;strong>二度目の生成を速くする&lt;/strong>（キャッシュ）ことだ。時期は2025年8月の前半。誕生から1ヶ月弱が経っている。&lt;/p>
&lt;h2 id="声の出どころをひとつから複数へ">&lt;a href="#%e5%a3%b0%e3%81%ae%e5%87%ba%e3%81%a9%e3%81%93%e3%82%8d%e3%82%92%e3%81%b2%e3%81%a8%e3%81%a4%e3%81%8b%e3%82%89%e8%a4%87%e6%95%b0%e3%81%b8" class="header-anchor">&lt;/a>声の出どころを、ひとつから複数へ
&lt;/h2>&lt;p>8月4日の午後、エンジンまわりに手が入る。&lt;/p>
&lt;div class="highlight">&lt;div style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;">&lt;tr>&lt;td style="vertical-align:top;padding:0;margin:0;border:0;">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>6b91408 2025-08-04 14:52 [feat] 複数のVoicevox互換エンジンに対応
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>それまで &lt;code>config.json&lt;/code> は、エンジンの宛先をたった一行で持っていた。&lt;/p>
&lt;div class="highlight">&lt;div style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;">&lt;tr>&lt;td style="vertical-align:top;padding:0;margin:0;border:0;">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#79a8ff">&amp;#34;voicevox_engine_url&amp;#34;&lt;/span>: &lt;span style="color:#79a8ff">&amp;#34;http://localhost:50021&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>VOICEVOX エンジン1台に話しかける、という前提だ。これがこのコミットで、こう変わる。&lt;/p>
&lt;div class="highlight">&lt;div style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;">&lt;tr>&lt;td style="vertical-align:top;padding:0;margin:0;border:0;">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#79a8ff">&amp;#34;engines&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> { &amp;#34;name&amp;#34;: &lt;span style="color:#79a8ff">&amp;#34;voicevox&amp;#34;&lt;/span>, &amp;#34;url&amp;#34;: &lt;span style="color:#79a8ff">&amp;#34;http://localhost:50021&amp;#34;&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> { &amp;#34;name&amp;#34;: &lt;span style="color:#79a8ff">&amp;#34;aivis&amp;#34;&lt;/span>, &amp;#34;url&amp;#34;: &lt;span style="color:#79a8ff">&amp;#34;http://localhost:10101&amp;#34;&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>単数の URL から、&lt;strong>エンジンの配列&lt;/strong>へ。新しく加わった &lt;code>aivis&lt;/code> は &lt;a class="link" href="https://aivis-project.com/" target="_blank" rel="noopener"
>AivisSpeech&lt;/a>——VOICEVOX 互換の API を持つ、別系統の音声合成エンジンだ。互換 API という一点を足がかりに、声の選択肢をエンジンをまたいで広げにいった格好になる。&lt;/p>
&lt;p>実装側、&lt;code>voice.py&lt;/code> は &lt;strong>+115 / −69&lt;/strong> とこの回でいちばん動いている。象徴的なのが新設された &lt;code>get_all_engine_speakers()&lt;/code> で、設定に並んだ全エンジンから話者一覧をかき集めて束ねる。話者は「VOICEVOX の中の誰か」ではなく「&lt;strong>登録したエンジン群のどこかにいる誰か&lt;/strong>」になった。台本に書いた話者名から ID を引く &lt;code>resolve_speaker_ids()&lt;/code> も、この少し前（7月15日の &lt;code>27fc83a&lt;/code>）にフォールバック機構が入っていて、名前が見つからないときの逃げ道が用意されている。データ駆動の発想は相変わらずで、&lt;strong>増えたのは設定ファイルの数行&lt;/strong>、というのが気持ちいい。&lt;/p>
&lt;p>翌コミットではテストカバレッジの底上げ（&lt;code>40413ed&lt;/code>）と、ruff を使った整形・型ヒント追加（&lt;code>4d8f7f9&lt;/code>）が続く。7月14日に入れたテストとリンタの土台が、さっそく機能追加の安全網として効いている。&lt;/p>
&lt;h2 id="作る前に仕様書を書く">&lt;a href="#%e4%bd%9c%e3%82%8b%e5%89%8d%e3%81%ab%e4%bb%95%e6%a7%98%e6%9b%b8%e3%82%92%e6%9b%b8%e3%81%8f" class="header-anchor">&lt;/a>作る前に、仕様書を書く
&lt;/h2>&lt;p>複数エンジンから5日空いて、8月10日の未明。次の大物——キャッシュに取りかかる前に、まず手が動いたのは&lt;strong>コードではなく文書&lt;/strong>だった。&lt;/p>
&lt;div class="highlight">&lt;div style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;">&lt;tr>&lt;td style="vertical-align:top;padding:0;margin:0;border:0;">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>7007461 2025-08-10 04:46 [docs] 包括的なアプリケーション仕様書を追加
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>SPECIFICATION.md&lt;/code> を &lt;strong>302行&lt;/strong>まとめて新設している。いきなり実装に突っ込むのではなく、ツール全体の仕様をいったん文章で固めてから次に進む。立ち上げ期の勢い一辺倒だった第1回・第2回と比べると、開発の呼吸が少し変わってきたのが分かる箇所だ。この仕様書は、このあとキャッシュを足すたびに追記されていく。&lt;/p>
&lt;h2 id="二度目の生成を速くする">&lt;a href="#%e4%ba%8c%e5%ba%a6%e7%9b%ae%e3%81%ae%e7%94%9f%e6%88%90%e3%82%92%e9%80%9f%e3%81%8f%e3%81%99%e3%82%8b" class="header-anchor">&lt;/a>二度目の生成を、速くする
&lt;/h2>&lt;p>そして同じ未明、キャッシュシステムの構築が一気に進む。ここは枝（ブランチ）を切って組み上げ、最後に本流へ合流させる、という作り方をしている。&lt;/p>
&lt;div class="highlight">&lt;div style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;">&lt;tr>&lt;td style="vertical-align:top;padding:0;margin:0;border:0;">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>298f8cf 2025-08-10 04:50 [feat] キャッシュシステムの基盤機能を追加
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>a3fc86f 2025-08-10 04:53 [feat] --init時のキャッシュディレクトリ自動作成機能を追加
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3c46dd1 2025-08-10 05:03 [feat] キャッシュシステムの完全統合とドキュメント整備
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>f1134b2 2025-08-10 05:09 [feat] キャッシュクリーンアップ機能の実装完了
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>780a899 2025-08-10 12:28 [feat] キャッシュシステムの完全実装（マージ）
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>そもそも、なぜキャッシュが要るのか。音声合成は遅い。台本を少し直して作り直すたびに、&lt;strong>変えていないセリフまで全部もう一度エンジンに投げ直す&lt;/strong>のは、明らかに無駄だ。同じ入力からは同じ音が出るのだから、一度作った音は取っておけばいい——それがこのキャッシュの動機だ。&lt;/p>
&lt;p>基盤になる &lt;code>cache_utils.py&lt;/code>（約170行）は、責務ごとに小さな関数を並べている。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>関数&lt;/th>
&lt;th>役割&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>generate_cache_key(speaker_id, text, speed_scale)&lt;/code>&lt;/td>
&lt;td>生成パラメータからキャッシュキーを作る&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>get_cache_path(cache_dir, cache_key, cache_type)&lt;/code>&lt;/td>
&lt;td>キーから保存先パスを決める&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>save_to_cache(...)&lt;/code> / &lt;code>load_from_cache(...)&lt;/code>&lt;/td>
&lt;td>キャッシュへの保存・読み出し&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>cleanup_old_cache(cache_dir, max_age_days=30)&lt;/code>&lt;/td>
&lt;td>古いキャッシュを掃除する&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>get_cache_stats(cache_dir)&lt;/code>&lt;/td>
&lt;td>キャッシュの統計を取る&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>肝は &lt;code>generate_cache_key()&lt;/code> だ。&lt;strong>話者ID・発話テキスト・話速倍率&lt;/strong>の3つを &lt;code>|&lt;/code> で繋ぎ、SHA256 で64文字のハッシュにする。テキストはハッシュ前に Unicode の NFC 正規化をかけていて、見た目が同じで内部表現だけ違う文字列が別物扱いされる事故を防いでいる。つまり「&lt;strong>同じ話者が・同じ速さで・同じ文を喋る&lt;/strong>」なら、必ず同じキーになる。キーが一致すれば、エンジンを叩かずに前回の音を返せる、という寸法だ。&lt;/p>
&lt;p>呼び出し側の &lt;code>voice.py&lt;/code> には &lt;code>generate_voice_with_cache()&lt;/code> が加わった。素の &lt;code>generate_voice()&lt;/code> の手前にキャッシュ確認をかぶせる薄いラッパで、ヒットすれば即返し、外れたときだけエンジンに合成を頼んで結果を保存する。設定は &lt;code>config.json&lt;/code> の &lt;code>cache&lt;/code> ブロックに集約された。&lt;/p>
&lt;div class="highlight">&lt;div style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;">&lt;tr>&lt;td style="vertical-align:top;padding:0;margin:0;border:0;">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
&lt;/span>&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
&lt;pre tabindex="0" style="color:#fff;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#79a8ff">&amp;#34;cache&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;enabled&amp;#34;: &lt;span style="color:#00bcff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;directory&amp;#34;: &lt;span style="color:#79a8ff">&amp;#34;.cache&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;max_age_days&amp;#34;: &lt;span style="color:#00bcff">30&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;auto_cleanup&amp;#34;: &lt;span style="color:#00bcff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>既定で有効、保存先は &lt;code>.cache&lt;/code>、30日より古いものは自動で掃除する。&lt;code>--init&lt;/code>（プロジェクト初期化）時にはキャッシュ用ディレクトリも自動で掘られる（&lt;code>a3fc86f&lt;/code>）ので、使う側は存在をほとんど意識しなくていい。掃除まで含めて「&lt;strong>置いておいても太りすぎない&lt;/strong>」よう手当てされているのが、いかにも実運用を見据えた作りだ。&lt;/p>
&lt;p>この一連の作業は、最後に &lt;code>780a899&lt;/code> として本流へマージされ、コミットメッセージのとおり「キャッシュシステムの完全実装」が main に乗った。マージコミットの差分には &lt;code>cache_utils.py&lt;/code> と4本のテストファイル（&lt;code>test_cache_utils.py&lt;/code> / &lt;code>test_init_cache.py&lt;/code> / &lt;code>test_voice_cache.py&lt;/code> / &lt;code>test_cache_cleanup.py&lt;/code>）がまとめて姿を現す。機能とテストが必ずセットで入ってくるあたり、7月14日に整えた土台がちゃんと習慣として根づいているのが見てとれる。&lt;/p>
&lt;h2 id="この期間でできたこと">&lt;a href="#%e3%81%93%e3%81%ae%e6%9c%9f%e9%96%93%e3%81%a7%e3%81%a7%e3%81%8d%e3%81%9f%e3%81%93%e3%81%a8" class="header-anchor">&lt;/a>この期間でできたこと
&lt;/h2>&lt;p>8月前半に積み上がったのは、このあたりだ。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>できたこと&lt;/th>
&lt;th>コミット&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>複数の VOICEVOX 互換エンジン対応（voicevox + aivis）&lt;/td>
&lt;td>&lt;code>6b91408&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>全エンジン横断の話者収集 &lt;code>get_all_engine_speakers()&lt;/code>&lt;/td>
&lt;td>&lt;code>6b91408&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>包括的な仕様書 &lt;code>SPECIFICATION.md&lt;/code>&lt;/td>
&lt;td>&lt;code>7007461&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>キャッシュ基盤 &lt;code>cache_utils.py&lt;/code>（SHA256キー・NFC正規化）&lt;/td>
&lt;td>&lt;code>298f8cf&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--init&lt;/code> でのキャッシュディレクトリ自動作成&lt;/td>
&lt;td>&lt;code>a3fc86f&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>voice.py&lt;/code> へのキャッシュ統合（&lt;code>generate_voice_with_cache&lt;/code>）&lt;/td>
&lt;td>&lt;code>3c46dd1&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>古いキャッシュの自動クリーンアップ&lt;/td>
&lt;td>&lt;code>f1134b2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>キャッシュシステムの完成（main へマージ）&lt;/td>
&lt;td>&lt;code>780a899&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>声の出どころを増やし、二度目の生成を速くした——どちらも「&lt;strong>作り続けるための土台&lt;/strong>」を厚くする回だった。エンジンが増えれば表現の幅が広がり、キャッシュが効けば試行錯誤のサイクルが速くなる。派手な見栄えはまだ無いが、このあと動画へ突っ込んでいくための足回りが、ここで静かに固められている。&lt;/p>
&lt;h2 id="次回予告">&lt;a href="#%e6%ac%a1%e5%9b%9e%e4%ba%88%e5%91%8a" class="header-anchor">&lt;/a>次回予告
&lt;/h2>&lt;p>第4回は、いよいよ &lt;strong>動画対応の幕開け&lt;/strong>を追う。8月後半、&lt;code>podcast-tool&lt;/code> に動画生成機能が初めて登場し（&lt;code>43fe3a1&lt;/code>）、台本の &lt;code>media&lt;/code> タグのトランジション属性で映像を切り替える仕掛けが入る。「音声ツール」が「音と映像のツール」へと舵を切る、シリーズの転回点だ。&lt;/p>
&lt;hr>
&lt;p>&lt;em>この記事は &lt;code>podcast-tool&lt;/code> のコミット履歴を一次資料として書いています。引用したコミットハッシュ・時刻・コード構成は当時のリポジトリ状態に基づきます（時刻は JST 表記）。&lt;/em>&lt;/p></description></item></channel></rss>