|
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(()) |
|
} |