先日作成したDeepSeek-R1を呼び出して小説を生成するAPEXアプリケーションに、小説の読み上げ機能を追加してみました。
VOICEVOXエンジンのコンテナで動作するHTTPサーバーに、Oracle APEXが動作しているデータベース・サーバーからリクエストを発行し音声合成をします。
以下の記事を参考にしています。
日経クロステック 2025年1月31日
NIFTY engineering blog
社内Webアプリでもずんだもんに喋ってほしいのだ
本記事ではフロントエンドはJavaScript、バックエンドはPL/SQLでの実装になるため、これらの記事のコードを流用しているということはありませんが、VOICEVOXと「ずんだもん」の使い方について参考にさせていただきました。
Oracle APEXのアプリケーションから音声合成を呼び出す手順としては、以前に以下の2本の記事を書いています。概ね似たような実装になっています。
「
OpenAIのText to speech APIを呼び出して写真の説明を読み上げる」
読み上げ対象の小説から推論過程にあたる<think>...</think>を削除するスイッチ、小説に書かれている文章からWAVファイルを生成する読み上げるボタン、生成されたWAVファイルを再生するオーディオ・コントールをページに配置します。
MシリーズのMacbook Proの
podmanを使って、VOICEVOXエンジンのコンテナを実行します。
podman run --rm -p 50021:50021 voicevox/voicevox_engine:cpu-latest
VOICEVOXエンジンのコンテナを実行すると、利用規約が表示されます。コマンドを一行実行するだけで、音声合成を行うREST APIを呼び出すことができるようになります。
% podman run --rm -p 50021:50021 voicevox/voicevox_engine:cpu-latest
+ cat /opt/voicevox_engine/README.md
# VOICEVOX エンジン利用規約
## 許諾内容
1. 商用・非商用問わず利用することができます
2. アプリケーションに組み込んで再配布することができます
3. 作成された音声を利用する際は、各音声ライブラリの規約に従ってください
4. 作成された音声の利用を他者に許諾する際は、当該他者に対し本許諾内容の 3 及び 4 の遵守を義務付けてください
## 禁止事項
- 逆コンパイル・リバースエンジニアリング及びこれらの方法の公開すること
- 製作者または第三者に不利益をもたらす行為
- 公序良俗に反する行為
## 免責事項
本ソフトウェアにより生じた損害・不利益について、製作者は一切の責任を負いません。
## その他
ご利用の際は VOICEVOX を利用したことがわかるクレジット表記が必要です。
---
[中略 - 詳細は実際の出力を確認してください。]
利用規約の詳細は以下をご確認ください。
https://zonko.zone-energy.jp/guideline
+ exec gosu user /opt/python/bin/python3 ./run.py --voicelib_dir /opt/voicevox_core/ --runtime_dir /opt/onnxruntime/lib --host 0.0.0.0
Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores.
reading /home/user/.local/share/voicevox-engine-dev/user.dict_csv-7bb7720c-7a5c-4341-84a6-9c9dd9a72dba.tmp ... 79
emitting double-array: 100% |###########################################|
INFO: Started server process [1]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:50021 (Press CTRL+C to quit)
Info: Loading core 0.15.7.
INFO: 10.88.0.3:37824 - "GET /docs HTTP/1.1" 200 OK
INFO: 10.88.0.3:37824 - "GET /openapi.json HTTP/1.1" 200 OK
上記のコマンドではコンテナのポート50021をホスト・ポート50021にマッピングしているため、REST APIのエンドポイントは
http://localhost:50021がベースURLになります。OpenAPIによるREST APIのドキュメントは以下より参照できます。
VOICEVOXエンジンの準備は以上で完了です。
以下よりAPEXアプリケーション側での作業を紹介します。
アプリケーション定義の置換に、いくつかAPEXアプリケーションが参照する値を置換文字列として設定します。
置換文字列G_VOICEVOX_BASE_URLとして、VOICEVOXエンジンのREST APIのベースURLを設定します。今回の構成はVOICEVOXエンジンを呼び出すOracle Databaseもローカルのコンテナで実行しているため、G_VOICEVOX_BASE_URLとしてhttp://host.containers.internal:50021を設定しています。
置換文字列
G_SPEAKERには、REST APIのaudio_queryやsynthesisに引数
speakerとして渡す値を設定します。
http://localhost:50021/speakersより、番号と音声の対応を参照できます。選択する音声ごとに音声ライブラリ利用規約が設定されているので、確認が必要です。
置換文字列G_VOICEVOX_PROXY_URLに、Oracle REST Data Servicesで実装したREST APIのエンドポイントを設定します。APEXアプリケーションはVOICEVOXエンジンを直接呼び出す代わりに、一旦Oracle REST Data ServicesのREST APIを呼び出し、そこからVOICEVOXエンジンのREST APIを呼び出します。このようにした理由のひとつは、CORS対策です。APEXが生成したページからJavaScriptで直接VOICEVOXエンジンを呼ぼうとすると、VOICEVOXにリクエストが拒否されます。VOICEVOXのコンテナに手を入れたくなかったので、ORDSでリクエストを中継することによりCORS有効化を不要にしています。
置換文字列G_VOICEVOX_PROXY_URLに/ords/apexdev/voicevox/audio_query_and_synthesisを設定しています。この値は、ORDS別名(通常はAPEXワークスペース名に同じ)に依存して、環境ごとに変わります。
ページ・デザイナで、読み上げ機能を追加するページ番号2のページを開きます。
非表示のページ・アイテムP2_BASE_URLを作成し、置換文字列G_VOICEVOX_BASE_URLの値を、このページで実行するJavaScriptから参照できるようにします。
ソースのタイプにアイテム、アイテムとしてG_VOICEVOX_BASE_URLを設定します。セッション・ステートのストレージはリクエストごと(メモリーのみ)を選択します。
同様にページ・アイテムP2_SPEAKERから、置換文字列G_SPEAKERの値を参照できるようにします。
ページ・アイテムP2_PROXY_URLから、置換文字列G_VOICEVOX_PROXY_URLの値を参照できるようにします。
読み上げに関するアイテムやボタンを含めるリージョンを作成します。識別の名前はReadout Container、タイプは静的コンテンツです。外観のテンプレートに装飾の無いBlank with Attributesを選択します。
このリージョンにAPEXアクションを保持するコンテキストを作成するため、静的IDとしてREADOUTを設定します。
DeepSeek-R1の出力には、<think>...</think>で囲まれた推論過程が含まれます。小説の読み上げ時には不要な文章なので、小説から除くための切替えとして、ページ・アイテムP2_EXCLUDE_THINKを作成します。ラベルはThinkを除くとします。
設定のデフォルトの使用はオンとします。この場合、切替えがオンでページ・アイテムの値はY、オフでNになります。レイアウトの行CSS クラスにu-flex u-align-items-centerを設定し、切替えやボタンの縦方向の位置を中央揃えにします。列スパンに2を設定し、この切替えの横幅に(横方向に12分割された列の内の)2列を割り当てます。
検証の必須の値はオン、デフォルトの静的値はNとしています。この値は動的コンテンツのリージョン小説のPL/SQLコードから参照するため、セッション・ステートのストレージにセッションごと(永続)を選択し、セッション・ステートに保存します。
リージョン小説のソースのCLOBを返すPL/SQLファンクション本体を、以下のコードに置き換えます。ページ・アイテムP2_EXCLUDE_THINKがYであれば、表示する文章から<think>...</think>で囲まれた文字列を取り除きます。
ボタンREADOUTを作成します。ラベルは読み上げるとします。このボタンをクリックして、リージョン小説に表示されている文章からWAVファイルを生成します。生成したWAVファイルは隣に表示されているオーディオ・コントールのソースに設定します。
Thinkの切替えの右隣りに配置するため、レイアウトの新規行の開始はオフ、列スパンは2とします。
このボタンを押したときに実行される処理は、APEXアクションREADOUTとして実装します。動作のアクションとして動的アクションで定義、詳細のカスタム属性にdata-action="READOUT"を設定します。
ボタンREADOUTの右隣りにオーディオ・コントロールを配置します。
静的コンテンツのリージョンを作成し、ソースのHTMLコードとして以下を記述します。
<audio controls id="story-audio"></audio>
以上で画面上のコンポーネントの配置は完了です。
静的アプリケーション・ファイルとして、このページで実行するJavaScriptのファイルを作成します。フォルダ名js以下に、ファイルreadout.jsとして以下の内容を記述します。
リージョン小説に表示されている文字列を取り出して、それをORDSのREST APIに渡しています。ORDSのREST APIはWAVファイルを返すので、受信したWAVファイルをオーディオ・コントールのsrcに設定しています。
ページ・プロパティのJavaScriptのファイルURLに以下を記述し、readout.jsをロードするようにします。
[module,defer]#APP_FILES#js/readout#MIN#.js
VOICEVOXエンジンは、Oracle REST Data Servicesのサービスから呼び出します。長いコードをORDSのRESTハンドラに記述すると開発が面倒になるため、音声合成の呼び出しはパッケージEBAJ_VOICEVOX_PROXYにまとめることにしました。
パッケージ定義部です。
RESTハンドラから呼び出すのはaudio_query_and_synthesisのみです。
パッケージ本体です。APEXのページから送信された小説本文を改行で区切り、一行ごとに音声合成を呼び出します。変換されたWAVファイルをすべて連結して、呼び出し元のAPEXページに戻します。
WAVファイルを連結するプロシージャは、ChatGPTで最近使えるようになったo3-mini-highで生成しました。以下のプロンプトでほとんど動くコードが生成されたのは、結構な驚きです。
Please write a procedure to merge 2 wav files into one by PL/SQL of Oracle?
最初に生成されたコードはリトルエンディアンの4バイト表現にCHAR型を使っている点で問題があったのですが、それも以下のプロンプトで修正されたコードが生成されました。
以下をCHARではなくRAWで実装してください。
-----
FUNCTION int_to_le_raw(p_int IN INTEGER) RETURN RAW IS
v_byte1 CHAR(1);
v_byte2 CHAR(1);
v_byte3 CHAR(1);
v_byte4 CHAR(1);
BEGIN
v_byte1 := CHR(MOD(p_int, 256));
v_byte2 := CHR(MOD(TRUNC(p_int/256), 256));
v_byte3 := CHR(MOD(TRUNC(p_int/256/256), 256));
v_byte4 := CHR(MOD(TRUNC(p_int/256/256/256), 256));
RETURN UTL_RAW.CAST_TO_RAW(v_byte1 || v_byte2 || v_byte3 || v_byte4);
END;
ChatGPTがPL/SQLではかなり難しいバイナリ・データを操作するコードを生成してくれたので、とても助かりました。
後は、ebaj_voicevox_proxy.audio_query_and_synthesisを呼びだすORDSのハンドラを作成します。
モジュールとしてvoicevox、テンプレートとしてaudio_query_and_synthesisを作成し、POSTハンドラのソースに以下を記述します。
作成したRESTサービスはAPEXのセッションで保護できます。今回は実装しませんが、以前のOpenAIやAzureでの記事では、API呼び出しが有料なためAPEXセッションによる保護を実装しています。
以上でアプリケーションは完成です。
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。APEX 24.2で作成しているため、それ以前のバージョンではインポートできません。
https://github.com/ujnak/apexapps/blob/master/exports/novel-generator-synthesis.zip
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完