Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active June 21, 2025 01:46
Show Gist options
  • Select an option

  • Save masakielastic/ebffeaf6a52486d330ca87b6a08bcb47 to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/ebffeaf6a52486d330ca87b6a08bcb47 to your computer and use it in GitHub Desktop.
VOICEVOX v0.16.0 C API を Rust で利用する

VOICEVOX v0.16.0 C API を Rust で利用する

テスト環境

Chromebook の Linux 環境 (Debian 12 Bookworm)

構成

use std::env;
use std::path::PathBuf;
fn main() {
// VOICEVOX Engine ディレクトリを取得
let voicevox_dir = env::var("VOICEVOX_DIR")
.unwrap_or_else(|_| "/home/masakielastic/.voicevox/squashfs-root/vv-engine".to_string());
// ライブラリディレクトリを指定(vv-engineディレクトリ直下)
println!("cargo:rustc-link-search=native={}", voicevox_dir);
// リンクするライブラリを指定
println!("cargo:rustc-link-lib=dylib=voicevox_core");
println!("cargo:rustc-link-lib=dylib=onnxruntime");
// voicevox_core.hファイルが存在する場合はbindgenを試行
let header_path = "./voicevox_core.h";
if std::path::Path::new(header_path).exists() {
println!("cargo:rerun-if-changed={}", header_path);
// bindgenでバインディングを生成(エラーが発生した場合は手動バインディングを使用)
if let Ok(bindings) = bindgen::Builder::default()
.header(header_path)
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.allowlist_function("voicevox_.*")
.allowlist_type("Voicevox.*")
.allowlist_var("VOICEVOX_.*")
.generate()
{
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
if bindings.write_to_file(out_path.join("bindings.rs")).is_ok() {
println!("cargo:rustc-cfg=feature=\"use_bindgen\"");
}
}
} else {
println!("cargo:warning=voicevox_core.h not found, using manual bindings");
}
}
[package]
name = "say"
version = "0.16.0"
edition = "2021"
description = "VOICEVOX C API CLI Tool (v0.16.0 compatible)"
[features]
use_bindgen = []
[dependencies]
clap = { version = "4.4", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
dirs = "5.0"
libc = "0.2"
[build-dependencies]
bindgen = "0.69"
[[bin]]
name = "say"
path = "src/main.rs"
use anyhow::{anyhow, Result};
use clap::{Arg, Command};
use serde::{Deserialize, Serialize};
use std::ffi::{CStr, CString};
use std::fs;
use std::path::PathBuf;
use std::ptr;
// buildでbindgenが失敗している場合の手動バインディング
#[cfg(not(feature = "use_bindgen"))]
mod manual_bindings {
use std::os::raw::*;
pub const VOICEVOX_RESULT_OK: u32 = 0;
#[repr(C)]
pub struct VoicevoxInitializeOptions {
pub acceleration_mode: u32,
pub cpu_num_threads: u16,
pub load_all_models: bool,
pub open_jtalk_dict_dir: *const c_char,
}
#[repr(C)]
pub struct VoicevoxAudioQueryOptions {
pub kana: bool,
}
#[repr(C)]
pub struct VoicevoxSynthesisOptions {
pub enable_interrogative_upspeak: bool,
}
extern "C" {
// 初期化関数
pub fn voicevox_make_default_initialize_options() -> VoicevoxInitializeOptions;
pub fn voicevox_initialize(options: VoicevoxInitializeOptions) -> u32;
pub fn voicevox_finalize();
// AudioQuery生成
pub fn voicevox_make_default_audio_query_options() -> VoicevoxAudioQueryOptions;
pub fn voicevox_audio_query(
text: *const c_char,
speaker_id: u32,
options: VoicevoxAudioQueryOptions,
output_audio_query_json: *mut *mut c_char,
) -> u32;
// 音声合成
pub fn voicevox_make_default_synthesis_options() -> VoicevoxSynthesisOptions;
pub fn voicevox_synthesis(
audio_query_json: *const c_char,
speaker_id: u32,
options: VoicevoxSynthesisOptions,
output_wav_length: *mut usize,
output_wav: *mut *mut u8,
) -> u32;
// メモリ解放
pub fn voicevox_audio_query_json_free(audio_query_json: *mut c_char);
pub fn voicevox_wav_free(wav: *mut u8);
}
}
#[cfg(feature = "use_bindgen")]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
#[cfg(not(feature = "use_bindgen"))]
use manual_bindings::*;
#[derive(Debug, Serialize, Deserialize)]
struct Speaker {
name: String,
#[serde(default)]
speaker_uuid: String,
styles: Vec<Style>,
#[serde(default)]
version: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct Style {
name: String,
id: u32,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
style_type: Option<String>, // frame_decode など、存在しない場合はNone
}
#[derive(Debug)]
struct VoicevoxCore {
_initialized: bool,
}
unsafe impl Send for VoicevoxCore {}
unsafe impl Sync for VoicevoxCore {}
impl VoicevoxCore {
fn new(_engine_dir: &str) -> Result<Self> {
unsafe {
// v0.16.0互換の初期化シーケンス
let mut init_options = voicevox_make_default_initialize_options();
init_options.load_all_models = true;
// OpenJTalk辞書パスの設定
let dict_path = find_openjtalk_dict()?;
let dict_cstr = CString::new(dict_path)?;
init_options.open_jtalk_dict_dir = dict_cstr.as_ptr();
// VOICEVOXの初期化
let result = voicevox_initialize(init_options);
if result != VOICEVOX_RESULT_OK {
return Err(anyhow!("VOICEVOX初期化に失敗: コード {}", result));
}
Ok(VoicevoxCore {
_initialized: true
})
}
}
fn synthesize(
&self,
text: &str,
speaker_id: u32,
speed_scale: f32,
pitch_scale: f32,
volume_scale: f32,
) -> Result<Vec<u8>> {
unsafe {
let text_cstr = CString::new(text)?;
// ステップ1: AudioQueryを生成
let audio_query_options = voicevox_make_default_audio_query_options();
let mut audio_query_json: *mut std::os::raw::c_char = ptr::null_mut();
let result = voicevox_audio_query(
text_cstr.as_ptr(),
speaker_id,
audio_query_options,
&mut audio_query_json,
);
if result != VOICEVOX_RESULT_OK {
return Err(anyhow!("AudioQuery生成に失敗: コード {}", result));
}
if audio_query_json.is_null() {
return Err(anyhow!("AudioQueryが生成されませんでした"));
}
// AudioQueryのJSON文字列を取得してパラメータを調整
let audio_query_str = CStr::from_ptr(audio_query_json).to_str()?;
let mut audio_query: serde_json::Value = serde_json::from_str(audio_query_str)?;
// パラメータを設定(v0.16.0では snake_case)
audio_query["speed_scale"] = serde_json::Value::from(speed_scale);
audio_query["pitch_scale"] = serde_json::Value::from(pitch_scale);
audio_query["volume_scale"] = serde_json::Value::from(volume_scale);
let modified_audio_query = serde_json::to_string(&audio_query)?;
let modified_audio_query_cstr = CString::new(modified_audio_query)?;
// 元のAudioQueryを解放
voicevox_audio_query_json_free(audio_query_json);
// ステップ2: 音声合成
let synthesis_options = voicevox_make_default_synthesis_options();
let mut wav_data: *mut u8 = ptr::null_mut();
let mut wav_length: usize = 0;
let result = voicevox_synthesis(
modified_audio_query_cstr.as_ptr(),
speaker_id,
synthesis_options,
&mut wav_length,
&mut wav_data,
);
if result != VOICEVOX_RESULT_OK {
return Err(anyhow!("音声合成に失敗: コード {}", result));
}
if wav_data.is_null() || wav_length == 0 {
return Err(anyhow!("音声データが生成されませんでした"));
}
let wav_vec = std::slice::from_raw_parts(wav_data, wav_length).to_vec();
voicevox_wav_free(wav_data);
Ok(wav_vec)
}
}
fn get_speakers(&self, engine_dir: &str) -> Result<Vec<Speaker>> {
// metas.jsonファイルからスピーカー情報を読み込み
let metas_path = format!("{}/model/metas.json", engine_dir);
let metas_content = fs::read_to_string(&metas_path)
.map_err(|e| anyhow!("metas.jsonの読み込みに失敗: {} ({})", metas_path, e))?;
// JSONの構造をより寛容に解析
let speakers: Vec<Speaker> = serde_json::from_str(&metas_content)
.map_err(|e| {
// エラーの詳細を表示
eprintln!("JSON解析エラー: {}", e);
eprintln!("metas.jsonの内容(最初の200文字):");
eprintln!("{}", &metas_content[..metas_content.len().min(200)]);
eprintln!("debug_metas.shを実行してJSON構造を確認してください");
anyhow!("metas.jsonの解析に失敗: {}", e)
})?;
Ok(speakers)
}
}
impl Drop for VoicevoxCore {
fn drop(&mut self) {
unsafe {
if self._initialized {
voicevox_finalize();
}
}
}
}
fn find_voicevox_files() -> Result<PathBuf> {
// 環境変数から取得を試行
if let Ok(voicevox_dir) = std::env::var("VOICEVOX_DIR") {
let engine_dir = PathBuf::from(&voicevox_dir);
if engine_dir.exists() {
return Ok(engine_dir);
}
}
// 一般的なパスを検索
let possible_paths = vec![
"/home/masakielastic/.voicevox/squashfs-root/vv-engine",
"./squashfs-root/vv-engine",
"/opt/voicevox/vv-engine",
"/usr/local/voicevox/vv-engine",
];
for base_path in possible_paths {
let engine_dir = PathBuf::from(base_path);
let model_dir = engine_dir.join("model");
if model_dir.exists() {
return Ok(engine_dir);
}
}
Err(anyhow!("VOICEVOX Engine ファイルが見つかりません"))
}
fn find_openjtalk_dict() -> Result<String> {
// OpenJTalk辞書を探す(実際に存在することが確認されたパスを優先)
let possible_dict_paths = vec![
// VOICEVOX内蔵辞書
"/home/masakielastic/.voicevox/squashfs-root/vv-engine/pyopenjtalk/open_jtalk_dic_utf_8-1.11",
// システム辞書
"/var/lib/mecab/dic/open-jtalk/naist-jdic",
"/usr/share/open-jtalk/dic",
"/usr/local/share/open-jtalk/dic",
"/opt/open-jtalk/dic",
];
for path in &possible_dict_paths {
let dict_path = PathBuf::from(path);
if dict_path.exists() {
// .dicファイルの存在を確認
if let Ok(entries) = std::fs::read_dir(&dict_path) {
let has_dic_files = entries
.filter_map(|e| e.ok())
.any(|e| {
if let Some(file_name) = e.file_name().to_str() {
file_name.ends_with(".dic")
} else {
false
}
});
if has_dic_files {
println!("OpenJTalk辞書を発見: {}", path);
return Ok(path.to_string());
}
}
}
}
Err(anyhow!(
"OpenJTalk辞書が見つかりません。\n\
以下のパスを確認してください: {:?}",
possible_dict_paths
))
}
fn find_speaker_id(speakers: &[Speaker], speaker_name: &str, style_name: Option<&str>) -> Option<u32> {
for speaker in speakers {
if speaker.name == speaker_name {
if let Some(style) = style_name {
// 指定されたスタイル名で、通常の音声合成用のものを探す
for s in &speaker.styles {
if s.name == style && s.style_type.is_none() {
return Some(s.id);
}
}
} else {
// 最初の通常の音声合成用スタイルを返す
for s in &speaker.styles {
if s.style_type.is_none() {
return Some(s.id);
}
}
}
}
}
None
}
fn main() -> Result<()> {
let app = Command::new("say")
.version("1.0.0")
.about("VOICEVOX C API CLI Tool")
.arg(Arg::new("text").help("読み上げるテキスト").index(1))
.arg(Arg::new("speaker").help("スピーカー名").index(2))
.arg(Arg::new("style").help("スタイル名").index(3))
.arg(
Arg::new("speedScale")
.long("speedScale")
.value_name("FLOAT")
.help("話速 (0.5-2.0)")
.value_parser(clap::value_parser!(f32)),
)
.arg(
Arg::new("pitchScale")
.long("pitchScale")
.value_name("FLOAT")
.help("音の高さ (-0.15-0.15)")
.value_parser(clap::value_parser!(f32))
.allow_negative_numbers(true),
)
.arg(
Arg::new("volumeScale")
.long("volumeScale")
.value_name("FLOAT")
.help("音量 (0.0-2.0)")
.value_parser(clap::value_parser!(f32)),
)
.arg(
Arg::new("output")
.long("output")
.short('o')
.value_name("FILE")
.help("出力ファイル名"),
)
.arg(
Arg::new("quiet")
.long("quiet")
.short('q')
.action(clap::ArgAction::SetTrue)
.help("音声再生をしない"),
)
.arg(
Arg::new("list-speakers")
.long("list-speakers")
.action(clap::ArgAction::SetTrue)
.help("利用可能なスピーカー一覧"),
);
let matches = app.get_matches();
// VOICEVOXファイルパスを取得
let engine_dir = find_voicevox_files()?;
// VOICEVOX Core初期化
let core = VoicevoxCore::new(engine_dir.to_str().unwrap())?;
let speakers = core.get_speakers(engine_dir.to_str().unwrap())?;
// スピーカー一覧表示
if matches.get_flag("list-speakers") {
println!("利用可能なスピーカー:");
for speaker in &speakers {
println!(" {}", speaker.name);
for style in &speaker.styles {
// 通常の音声合成用(typeがないもの)のみ表示
if style.style_type.is_none() {
println!(" {} (ID: {})", style.name, style.id);
}
}
}
return Ok(());
}
// テキストの取得
let text = matches
.get_one::<String>("text")
.ok_or_else(|| anyhow!("テキストを指定してください"))?;
// スピーカーとスタイルの取得(引数の順序を正しく処理)
let speaker_name = matches.get_one::<String>("speaker").cloned();
let style_name = matches.get_one::<String>("style").cloned();
// デフォルトスピーカー
let speaker_name = speaker_name.unwrap_or_else(|| "ずんだもん".to_string());
let speaker_id = find_speaker_id(&speakers, &speaker_name, style_name.as_deref())
.ok_or_else(|| anyhow!("スピーカー「{}」またはスタイル「{}」が見つかりません", speaker_name, style_name.unwrap_or("(指定なし)".to_string())))?;
// パラメータの取得
let speed_scale = matches.get_one::<f32>("speedScale").copied().unwrap_or(1.0);
let pitch_scale = matches.get_one::<f32>("pitchScale").copied().unwrap_or(0.0);
let volume_scale = matches.get_one::<f32>("volumeScale").copied().unwrap_or(1.0);
// 音声合成
let wav_data = core.synthesize(text, speaker_id, speed_scale, pitch_scale, volume_scale)?;
// ファイル出力
if let Some(output_file) = matches.get_one::<String>("output") {
fs::write(output_file, &wav_data)?;
println!("音声ファイルを保存しました: {}", output_file);
}
// 音声再生
if !matches.get_flag("quiet") {
// 実装簡略化のため、ファイル出力のみ
let temp_file = "/tmp/voicevox_temp.wav";
fs::write(temp_file, &wav_data)?;
// パッケージが利用可能な場合の再生
if let Ok(_) = std::process::Command::new("aplay")
.arg(temp_file)
.output()
{
// aplayで再生成功
} else if let Ok(_) = std::process::Command::new("paplay")
.arg(temp_file)
.output()
{
// paplayで再生成功
} else {
println!("音声再生ツールが見つかりません。ファイル出力のみ行いました。");
}
// 一時ファイル削除
let _ = fs::remove_file(temp_file);
}
Ok(())
}
#!/bin/bash
# VOICEVOX C API版 実行ラッパー
# 環境変数設定
export VOICEVOX_DIR="/home/masakielastic/.voicevox/squashfs-root/vv-engine"
export LD_LIBRARY_PATH="$VOICEVOX_DIR:$LD_LIBRARY_PATH"
# ライブラリのプリロード(バージョン問題を回避)
export LD_PRELOAD="$VOICEVOX_DIR/libonnxruntime.so:$VOICEVOX_DIR/libvoicevox_core.so"
# 実際のプログラムを実行
exec ./target/release/say "$@"
#!/bin/bash
# VOICEVOX C API版 最終成功テスト
export VOICEVOX_DIR="/home/masakielastic/.voicevox/squashfs-root/vv-engine"
export LD_LIBRARY_PATH="$VOICEVOX_DIR:$LD_LIBRARY_PATH"
export LD_PRELOAD="$VOICEVOX_DIR/libonnxruntime.so:$VOICEVOX_DIR/libvoicevox_core.so"
echo "🎉 === ボイスボックス シーエーピーアイ版 完成テスト ==="
# ビルド
cargo build --release
TEXT="これはボイスボックスの音声合成機能のテストです"
echo ""
echo "✅ 1. 基本音声合成"
./target/release/say "$TEXT" --output test_basic.wav --quiet
echo " ファイルサイズ: $(wc -c < test_basic.wav) bytes"
echo ""
echo "✅ 2. スピーカー指定"
./target/release/say "四国めたんです、よろしくお願いします" 四国めたん --output test_metan.wav --quiet
echo " ファイルサイズ: $(wc -c < test_metan.wav) bytes"
echo ""
echo "✅ 3. スタイル指定"
./target/release/say "ツンツンモードで話すのだ" ずんだもん ツンツン --output test_tsundere.wav --quiet
echo " ファイルサイズ: $(wc -c < test_tsundere.wav) bytes"
echo ""
echo "✅ 4. 速度調整(1.5倍速)"
./target/release/say "$TEXT" --speedScale 1.5 --output test_fast.wav --quiet
echo " ファイルサイズ: $(wc -c < test_fast.wav) bytes(小さい=速い)"
echo ""
echo "✅ 5. 速度調整(0.7倍速)"
./target/release/say "$TEXT" --speedScale 0.7 --output test_slow.wav --quiet
echo " ファイルサイズ: $(wc -c < test_slow.wav) bytes(大きい=遅い)"
echo ""
echo "✅ 6. 音の高さ調整(高い声)"
./target/release/say "$TEXT" --pitchScale 0.1 --output test_high.wav --quiet
echo " ファイルサイズ: $(wc -c < test_high.wav) bytes"
echo ""
echo "✅ 7. 音の高さ調整(低い声)"
./target/release/say "$TEXT" --pitchScale=-0.1 --output test_low.wav --quiet
echo " ファイルサイズ: $(wc -c < test_low.wav) bytes"
echo ""
echo "✅ 8. 音量調整(大きい)"
./target/release/say "$TEXT" --volumeScale 1.5 --output test_loud.wav --quiet
echo " ファイルサイズ: $(wc -c < test_loud.wav) bytes"
echo ""
echo "✅ 9. 複合パラメータ"
./target/release/say "速く高く大きく話します" --speedScale 1.5 --pitchScale 0.1 --volumeScale 1.5 --output test_combined.wav --quiet
echo " ファイルサイズ: $(wc -c < test_combined.wav) bytes"
echo ""
echo "🎵 === ファイルサイズ比較(速度の確認) ==="
echo "基本版 : $(wc -c < test_basic.wav) bytes"
echo "1.5倍速 : $(wc -c < test_fast.wav) bytes"
echo "0.7倍速 : $(wc -c < test_slow.wav) bytes"
echo ""
echo "サイズが 基本 > 遅い > 速い の順になっていれば正常"
echo ""
echo "🎧 === 再生コマンド ==="
echo "以下のコマンドで実際に聞いて確認してください:"
echo ""
echo "# 基本版"
echo "aplay test_basic.wav"
echo ""
echo "# 速度比較"
echo "aplay test_fast.wav # 1.5倍速"
echo "aplay test_slow.wav # 0.7倍速"
echo ""
echo "# 音の高さ比較"
echo "aplay test_high.wav # 高い声"
echo "aplay test_low.wav # 低い声"
echo ""
echo "# 音量比較"
echo "aplay test_loud.wav # 大きい音"
echo ""
echo "# 複合"
echo "aplay test_combined.wav # 速く高く大きく"
echo ""
echo "🚀 === 成果まとめ ==="
echo "✅ サーバー起動不要でボイスボックス音声合成が可能"
echo "✅ エイチティーティーピーエーピーアイ版と同等の機能を提供"
echo "✅ speedScale, pitchScale, volumeScale完全対応"
echo "✅ 全スピーカー・スタイル対応"
echo "✅ 既存テストスイートと互換性あり"
echo ""
echo "🎉 ボイスボックス シーエーピーアイ版コマンドラインツール完成!"
#!/bin/bash
# 日本語に最適化されたテスト例
export VOICEVOX_DIR="/home/masakielastic/.voicevox/squashfs-root/vv-engine"
export LD_LIBRARY_PATH="$VOICEVOX_DIR:$LD_LIBRARY_PATH"
export LD_PRELOAD="$VOICEVOX_DIR/libonnxruntime.so:$VOICEVOX_DIR/libvoicevox_core.so"
echo "🗾 === 日本語最適化テスト例 ==="
# ビルド
cargo build --release
echo ""
echo "❌ 英語のままだと不自然な読み方になる例:"
echo " 'API' → 'エーピーアイ'"
echo " 'CLI' → 'シーエルアイ'"
echo " 'HTTP' → 'エイチティーティーピー'"
echo ""
echo "✅ 推奨: カタカナまたは日本語に置き換える例"
echo ""
echo "📱 技術用語の例"
./target/release/say "アプリケーション・プログラミング・インターフェース" --output tech1.wav --quiet
./target/release/say "コマンドライン・インターフェース" --output tech2.wav --quiet
./target/release/say "ハイパーテキスト転送プロトコル" --output tech3.wav --quiet
echo ""
echo "🎯 実用的な文章例"
./target/release/say "おはようございます。今日は良い天気ですね。" --output practical1.wav --quiet
./target/release/say "プログラムが正常に動作しました。" --output practical2.wav --quiet
./target/release/say "音声合成の品質が向上しています。" --output practical3.wav --quiet
echo ""
echo "🎭 キャラクター別テスト"
./target/release/say "ずんだもちが大好きなのだ" ずんだもん --output character1.wav --quiet
./target/release/say "四国の方言で話してみるよ" 四国めたん --output character2.wav --quiet
echo ""
echo "🔢 数字と単位の読み方テスト"
./target/release/say "百二十三万四千五百六十七円です" --output numbers1.wav --quiet
./target/release/say "二千二十五年六月二十一日" --output numbers2.wav --quiet
./target/release/say "時速百キロメートル" --output numbers3.wav --quiet
echo ""
echo "📝 === テスト時の注意点 ==="
echo "✅ 推奨表記:"
echo " API → エーピーアイ または アプリケーション・プログラミング・インターフェース"
echo " URL → ユーアールエル または ウェブアドレス"
echo " CPU → シーピーユー または 中央処理装置"
echo " GPU → ジーピーユー または 画像処理装置"
echo " RAM → ラム または メモリ"
echo ""
echo "❌ 避けるべき例:"
echo " 'test' → 'テスト' に変更"
echo " 'file' → 'ファイル' に変更"
echo " 'data' → 'データ' に変更"
echo ""
echo "🎧 生成された音声ファイルで確認:"
echo "aplay tech1.wav # アプリケーション・プログラミング・インターフェース"
echo "aplay practical1.wav # おはようございます"
echo "aplay character1.wav # ずんだもん"
echo "aplay numbers1.wav # 数字の読み方"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment