社内Webアプリでもずんだもんに喋ってほしいのだ
podman run --rm -p 50021:50021 voicevox/voicevox_engine:cpu-latest
% 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のドキュメントは以下より参照できます。
declare | |
l_end integer; | |
l_length integer; | |
l_story clob; | |
begin | |
if :P2_STORY is not null then | |
select story into l_story from ebaj_stories where id = :P2_STORY; | |
if :P2_EXCLUDE_THINK = 'Y' then | |
-- DeepSeek-R1の出力はかならず<think>で始まることを想定。 | |
l_length := dbms_lob.getlength(l_story); | |
l_end := instr(l_story, '</think>') + length('</think>'); | |
-- 出力の先頭はつねに<think>から始まるから、つねに</think>以降を返す。 | |
l_story := dbms_lob.substr(l_story, (l_length - l_end + 1), l_end); | |
end if; | |
l_story := '<div id="STORY">' || apex_markdown.to_html(l_story) || '</div>'; | |
return l_story; | |
end if; | |
return ''; | |
end; |
/* | |
* 音声合成を呼び出す。 | |
*/ | |
const G_VOICEVOX_BASE_URL = apex.item("P2_BASE_URL").getValue(); | |
const G_SPEAKER_ID = apex.item("P2_SPEAKER").getValue(); | |
const G_VOICEVOX_PROXY_URL = apex.item("P2_PROXY_URL").getValue(); | |
const a_READOUT = { | |
name: "READOUT", | |
action: (event, element, args) => { | |
const request = { | |
base_url: G_VOICEVOX_BASE_URL, | |
speaker: G_SPEAKER_ID, | |
text: document.getElementById("STORY").textContent | |
// text: "音声で回答して" | |
}; | |
apex.debug.info(request); | |
fetch(G_VOICEVOX_PROXY_URL, | |
{ | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify(request) | |
} | |
) | |
.then(response => response.blob()) | |
.then(blob => { | |
const audioURL = URL.createObjectURL(blob); | |
const elem = document.getElementById("story-audio"); | |
elem.src = audioURL; | |
}); | |
} | |
} | |
/* | |
* APEXアクションの定義。 | |
*/ | |
const readoutContext = apex.actions.createContext('readout-novel', document.getElementById("READOUT")); | |
readoutContext.add([ a_READOUT ]); |
create or replace package ebaj_voicevox_proxy | |
as | |
/** | |
* 2つのWAVファイルをマージするプロシージャ。 | |
* OpenAI o3-mini-highに書いてもらった。 | |
*/ | |
PROCEDURE merge_wav_files ( | |
p_file1 IN BLOB, -- First WAV file (with standard 44-byte header) | |
p_file2 IN BLOB, -- Second WAV file (with standard 44-byte header) | |
p_merged OUT BLOB -- Output: merged WAV file as a BLOB | |
); | |
/** | |
* VOICEVOXの/audio_queryを呼び出す。 | |
*/ | |
function audio_query( | |
p_text in clob | |
,p_base_url in varchar2 | |
,p_speaker in number | |
) | |
return clob; | |
/** | |
* VOICEVOXのsynthesisを呼び出す。 | |
*/ | |
function synthesis( | |
p_query in clob | |
,p_base_url in varchar2 | |
,p_speaker in number | |
) | |
return blob; | |
/** | |
* VOICEVOXのaudio_queryとsynthesisを呼び出す。 | |
*/ | |
function audio_query_and_synthesis( | |
p_text in clob | |
,p_base_url in varchar2 | |
,p_speaker in number | |
) | |
return blob; | |
end ebaj_voicevox_proxy; | |
/ |
create or replace package body ebaj_voicevox_proxy | |
as | |
/** | |
* 2つのWAVファイルをマージするプロシージャ。 | |
* | |
* OpenAI o3-mini-highに書いてもらった。 | |
* | |
* function int_to_le_rawのコメントが日本語なのは、WAVをマージするプロシージャを | |
* 書いてもらったときのプロンプトは英語で、CHR型の代わりにRAWを使ってと修正を依頼した | |
* ときのプロンプトが日本語だったから。 | |
*/ | |
PROCEDURE merge_wav_files ( | |
p_file1 IN BLOB, -- First WAV file (with standard 44-byte header) | |
p_file2 IN BLOB, -- Second WAV file (with standard 44-byte header) | |
p_merged OUT BLOB -- Output: merged WAV file as a BLOB | |
) | |
IS | |
------------------------------------------------------------------------- | |
-- Local variables. | |
------------------------------------------------------------------------- | |
v_header RAW(44); -- Original header from the first file. | |
v_new_header RAW(44); -- Modified header for the merged file. | |
v_data_len1 INTEGER; -- Audio data length in file1 (in bytes). | |
v_data_len2 INTEGER; -- Audio data length in file2 (in bytes). | |
v_new_data_len INTEGER; -- Combined data length. | |
v_total_file_sz INTEGER; -- New overall file size field value. | |
------------------------------------------------------------------------- | |
-- Helper function: Convert an integer into a 4-byte little-endian RAW. | |
-- | |
-- WAV file header fields (e.g. overall file size and data length) | |
-- are stored as 4-byte little-endian integers. | |
------------------------------------------------------------------------- | |
FUNCTION int_to_le_raw(p_int IN INTEGER) RETURN RAW IS | |
l_byte1 RAW(1); | |
l_byte2 RAW(1); | |
l_byte3 RAW(1); | |
l_byte4 RAW(1); | |
l_result RAW(4); | |
BEGIN | |
-- 下位バイトから順に、16進数2桁の文字列に変換してからRAWに変換 | |
l_byte1 := hextoraw(LPAD(TO_CHAR(MOD(p_int, 256), 'FMXX'), 2, '0')); | |
l_byte2 := hextoraw(LPAD(TO_CHAR(MOD(TRUNC(p_int/256), 256), 'FMXX'), 2, '0')); | |
l_byte3 := hextoraw(LPAD(TO_CHAR(MOD(TRUNC(p_int/256/256), 256), 'FMXX'), 2, '0')); | |
l_byte4 := hextoraw(LPAD(TO_CHAR(MOD(TRUNC(p_int/256/256/256), 256), 'FMXX'), 2, '0')); | |
-- 4つのRAWを連結(リトルエンディアン:最下位バイトが先頭) | |
l_result := utl_raw.concat(utl_raw.concat(l_byte1, l_byte2), | |
utl_raw.concat(l_byte3, l_byte4)); | |
RETURN l_result; | |
END; | |
BEGIN | |
-- Check that each WAV file is at least 44 bytes long. | |
IF DBMS_LOB.getlength(p_file1) < 44 OR DBMS_LOB.getlength(p_file2) < 44 THEN | |
raise_application_error(-20001, 'One of the WAV files is too short to be valid.'); | |
END IF; | |
------------------------------------------------------------------------- | |
-- Extract the header from the first WAV file. | |
-- In a standard PCM WAV file the header is 44 bytes: | |
-- | |
-- Bytes 1-4: "RIFF" | |
-- Bytes 5-8: Overall file size (file size - 8) [little-endian] | |
-- Bytes 9-12: "WAVE" | |
-- Bytes 13-16: "fmt " | |
-- Bytes 17-40: Format information (subchunk size, audio format, | |
-- number of channels, sample rate, byte rate, etc.) | |
-- Bytes 37-40: "data" (the literal string) | |
-- Bytes 41-44: Data chunk size [little-endian] | |
------------------------------------------------------------------------- | |
v_header := DBMS_LOB.SUBSTR(p_file1, 44, 1); | |
-- Compute the length of the audio (data) portion in each file. | |
v_data_len1 := DBMS_LOB.getlength(p_file1) - 44; | |
v_data_len2 := DBMS_LOB.getlength(p_file2) - 44; | |
v_new_data_len := v_data_len1 + v_data_len2; | |
-- The WAV header “overall file size” (bytes 5-8) equals the final file size minus 8. | |
-- For a standard 44-byte header, this value becomes 36 + (data size). | |
v_total_file_sz := 36 + v_new_data_len; | |
------------------------------------------------------------------------- | |
-- Build the new header: | |
-- | |
-- • Keep the first 4 bytes (i.e. "RIFF") unchanged. | |
-- • Replace bytes 5-8 with the new overall file size (as a little-endian RAW). | |
-- • Keep bytes 9-40 unchanged. | |
-- • Replace bytes 41-44 with the new data length (as a little-endian RAW). | |
------------------------------------------------------------------------- | |
v_new_header := | |
UTL_RAW.SUBSTR(v_header, 1, 4) || -- "RIFF" | |
int_to_le_raw(v_total_file_sz) || -- new overall file size (bytes 5-8) | |
UTL_RAW.SUBSTR(v_header, 9, 32) || -- bytes 9-40 (unchanged header parts) | |
int_to_le_raw(v_new_data_len); -- new data chunk size (bytes 41-44) | |
------------------------------------------------------------------------- | |
-- Create a temporary LOB to hold the merged file. | |
------------------------------------------------------------------------- | |
DBMS_LOB.CREATETEMPORARY(p_merged, TRUE, DBMS_LOB.CALL); | |
------------------------------------------------------------------------- | |
-- Write the new header to the merged file. | |
------------------------------------------------------------------------- | |
DBMS_LOB.WRITEAPPEND(p_merged, UTL_RAW.LENGTH(v_new_header), v_new_header); | |
------------------------------------------------------------------------- | |
-- Append the audio data from the first file. | |
-- We copy from position 45 (i.e. skipping the 44-byte header) for v_data_len1 bytes. | |
------------------------------------------------------------------------- | |
DBMS_LOB.COPY( | |
dest_lob => p_merged, | |
src_lob => p_file1, | |
amount => v_data_len1, | |
dest_offset => DBMS_LOB.getlength(p_merged) + 1, -- should be 45 after header write | |
src_offset => 45 | |
); | |
------------------------------------------------------------------------- | |
-- Append the audio data from the second file (again, skipping its header). | |
------------------------------------------------------------------------- | |
DBMS_LOB.COPY( | |
dest_lob => p_merged, | |
src_lob => p_file2, | |
amount => v_data_len2, | |
dest_offset => DBMS_LOB.getlength(p_merged) + 1, | |
src_offset => 45 | |
); | |
EXCEPTION | |
WHEN OTHERS THEN | |
-- In case of error, free the temporary LOB if needed. | |
IF DBMS_LOB.ISTEMPORARY(p_merged) = 1 THEN | |
DBMS_LOB.FREETEMPORARY(p_merged); | |
END IF; | |
RAISE; | |
END merge_wav_files; | |
/** | |
* VOICEVOXの/audio_queryを呼び出す。 | |
*/ | |
function audio_query( | |
p_text in clob | |
,p_base_url in varchar2 | |
,p_speaker in number | |
) | |
return clob | |
as | |
l_response clob; | |
l_url clob; | |
e_api_call_failed exception; | |
begin | |
l_url := p_base_url || '/audio_query?text=' || utl_url.escape(p_text, false, 'AL32UTF8') || '&speaker=' || p_speaker; | |
apex_web_service.clear_request_headers(); | |
apex_web_service.set_request_headers('accept', 'application/json', p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => l_url | |
,p_http_method => 'POST' | |
,p_body => '' | |
); | |
if apex_web_service.g_status_code <> 200 then | |
raise e_api_call_failed; | |
end if; | |
return l_response; | |
end audio_query; | |
/** | |
* VOICEVOXのsynthesisを呼び出す。 | |
*/ | |
function synthesis( | |
p_query in clob | |
,p_base_url in varchar2 | |
,p_speaker in number | |
) | |
return blob | |
as | |
l_query clob; | |
l_blob blob; | |
l_url varchar2(400); | |
e_api_call_failed exception; | |
begin | |
l_url := p_base_url || '/synthesis?speaker=' || p_speaker; | |
apex_web_service.clear_request_headers(); | |
apex_web_service.set_request_headers('Content-Type', 'application/json', p_reset => false); | |
apex_web_service.set_request_headers('accept', 'audio/wav', p_reset => false); | |
l_blob := apex_web_service.make_rest_request_b( | |
p_url => l_url | |
,p_http_method => 'POST' | |
,p_body => p_query | |
); | |
if apex_web_service.g_status_code <> 200 then | |
raise e_api_call_failed; | |
end if; | |
return l_blob; | |
end synthesis; | |
/** | |
* 生成されたWAVを確認するためのファンクション。呼び出さない。 | |
* | |
動作確認用の表 | |
----- | |
create table checkwav ( | |
id number generated by default on null as identity | |
constraint checkwav_id_pk primary key, | |
idx number, | |
text clob, | |
query clob, | |
wav blob | |
); | |
*/ | |
procedure log_wav( | |
p_idx in number | |
,p_text in clob | |
,p_query in clob | |
,p_wav in blob | |
) | |
as | |
pragma autonomous_transaction; | |
begin | |
-- insert into checkwav(idx, text, query, wav) values(p_idx, p_text, p_query, p_wav); | |
commit; | |
end log_wav; | |
/** | |
* VOICEVOXのaudio_queryとsynthesisを呼び出す。 | |
* | |
* 長文をVOICEVOXに送ると、synthesisででエラーが発生する。 | |
* なので、改行ごとにテキストを送信し、WAVファイルに変換している。 | |
* もし、改行で区切られていず、長い文字列が渡されるとやはりエラーとなるが、 | |
* これといった対応はしていない。 | |
* | |
* 句読点で区切っても良いが、元々の文章に改行を入れた方が良いと思う。 | |
* | |
* WAVファイルに変換した後、今度はそれをマージして、ひとつのWAVファイルにしている。 | |
* それを呼び出し元に戻している。 | |
*/ | |
function audio_query_and_synthesis( | |
p_text in clob | |
,p_base_url in varchar2 | |
,p_speaker in number | |
) | |
return blob | |
as | |
l_query clob; -- チャンクごとのaudio_queryの結果 | |
l_wav0 blob; -- すべてのWAV | |
l_wav blob; -- チャンクごとのWAV | |
l_text clob; -- 音声にするテキスト - すべて | |
l_text0 varchar2(4000); -- 音声にするテキスト、改行で分割 | |
l_text_array apex_t_varchar2; | |
begin | |
/* 改行で分割 */ | |
l_text_array := apex_string.split(p_text); | |
l_wav0 := null; | |
for i in 1..l_text_array.count | |
loop | |
l_text0 := l_text_array(i); | |
if length(l_text0) > 0 then | |
l_query := audio_query(l_text0, p_base_url, p_speaker); | |
l_wav := synthesis(l_query, p_base_url, p_speaker); | |
log_wav(i, l_text0, l_query, l_wav); | |
if l_wav0 is null then | |
l_wav0 := l_wav; | |
else | |
merge_wav_files(l_wav0, l_wav, l_wav0); | |
-- l_wav0 := l_wav; | |
end if; | |
end if; | |
end loop; | |
return l_wav0; | |
end audio_query_and_synthesis; | |
end ebaj_voicevox_proxy; | |
/ |
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;
declare | |
l_wav blob; | |
l_json json_object_t; | |
l_text clob; | |
l_base_url varchar2(400); | |
l_speaker number; | |
e_api_call_failed exception; | |
begin | |
l_json := json_object_t(:body_json); | |
l_text := l_json.get_clob('text'); | |
l_base_url := l_json.get_string('base_url'); | |
l_speaker := l_json.get_number('speaker'); | |
l_wav := ebaj_voicevox_proxy.audio_query_and_synthesis(l_text, l_base_url, l_speaker); | |
/* wavとしてダウンロード */ | |
sys.htp.init; | |
sys.htp.p('Content-Length: ' || dbms_lob.getlength(l_wav)); | |
sys.htp.p('Content-Type: audio/wav'); | |
sys.htp.p('Content-Disposition: attachment'); | |
sys.owa_util.http_header_close; | |
sys.wpg_docload.download_file(l_wav); | |
end; |