必要なもの
- xTranslatorの翻訳辞書
- Style-Bert-VITS2
- PHP
- Yakitori Audio Converter
- SSE CreationKit Fixes
xTranslatorの翻訳辞書からDBVO辞書を作る
xTranslatorの翻訳辞書を校正して誤字や脱字をなおしておきます。
1箇所だけ制御コードが紛れ込んでいましたので、これも除去しておきます。
変換スクリプトを使って DBVO.json
を生成します。
>php xTranslator_dic_to_DBVO.php YurianaWench_english_japanese.xml
ですます調からである調への変換も力技で行っています。
xTranslatorの翻訳辞書からDBVO辞書を作る (php)
<?php
try {
if ( $argc != 2 ) {
throw new Exception('Usage: find_from_xTranslator_dic.php <input filename>');
}
$in_filename = $argv[1];
parse_xml($in_filename);
} catch (Exception $e) {
echo $e->GetMessage() . PHP_EOL;
}
function parse_xml($in_filename)
{
$fp = fopen($in_filename, 'r');
if ( !$fp ) {
throw new Exception('could not open input file - ' . $in_filename);
}
$buf = '';
while ( !feof($fp) ) {
$buf .= fgets($fp);
}
fclose($fp);
if ( !preg_match_all('!<String.+?</String>!ms', $buf, $matches, PREG_SET_ORDER) ) {
throw new Exception('no match');
}
$fp1 = null;
$fp2 = null;
$fp3 = null;
try {
$out_filename = 'voicevox.csv';
$fp1 = fopen($out_filename, 'w');
if ( !$fp1 ) {
throw new Exception('could not open output file - ' . $out_filename);
}
$out_filename = 'LazyVoiceFinder_export.csv';
$fp2 = fopen($out_filename, 'w');
if ( !$fp2 ) {
throw new Exception('could not open output file - ' . $out_filename);
}
fputs($fp2, '"State","Plugin","FormId","Topic Edid","File Name","Voice Type","Dialogue 1 - en","Dialogue 2 - en"' . PHP_EOL);
$out_filename = 'DBVO.json';
$fp3 = fopen($out_filename, 'w');
if ( !$fp3 ) {
throw new Exception('could not open output file - ' . $out_filename);
}
fputs($fp3, '{' . PHP_EOL);
} catch (Exception $e) {
if ($fp1) {
fclose($fp1);
}
if ($fp2) {
fclose($fp2);
}
if ($fp3) {
fclose($fp3);
}
throw $e;
}
$history = [];
$dbvo_dic_buf = '';
foreach ($matches as $regs) {
if ( strpos($regs[0], '<REC>DIAL:FULL</REC>') === false ) {
continue;
}
if ( !preg_match('!<Source>(.+?)</Source>!ms', $regs[0], $regs2) ) {
continue;
}
$source = $regs2[1];
if ( in_array($source, $history) ) {
continue;
}
$history[] = $source;
if ( !preg_match('!<Dest>(.+?)</Dest>!ms', $regs[0], $regs2) ) {
continue;
}
$dest = $regs2[1];
// OK_Followerの翻訳辞書は日英なので入れ替える
//list($source, $dest) = [$dest, $source];
$dest = str_replace("\r", '', $dest);
$dest = str_replace("\n", '', $dest);
$source = str_replace("\r", '', $source);
$source = str_replace("\n", '', $source);
$dest = str_replace(''', "'", $dest);
$dest = str_replace('"', '', $dest);
$dest = str_replace('<', '<', $dest);
$dest = str_replace('>', '>', $dest);
$dest = preg_replace('/<Alias=Player> */', 'ドバコ', $dest);
$dest = preg_replace('/<[^>]+>/', '', $dest);
$dest = str_replace('(', '(', $dest);
$dest = str_replace(')', ')', $dest);
$dest = preg_replace('/\.\.\.+/', '…', $dest);
$dest = trim($dest);
$dest = preg_replace('/[\.……]+$/u', '', $dest);
$dest = trim($dest);
if ( strlen($dest) == 0 ) {
continue;
}
$dest = preg_replace('/^\((.+)\) *\((.+)\)$/', '$1', $dest);
$dest = preg_replace('/^\((.+)\)$/', '$1', $dest);
$dest = preg_replace('/^\[(.+)\]$/', '$1', $dest);
$dest = preg_replace('/^\*(.+)\*$/', '$1', $dest);
$dest = preg_replace('/^(.+)\(.+?\)/', '$1', $dest);
$dest = preg_replace('/^(.+)\[.+?\]/', '$1', $dest);
$dest = preg_replace('/^(.+)\*.+?\*/', '$1', $dest);
$dest = preg_replace('/^\(.+?\)(.+)$/', '$1', $dest);
$dest = preg_replace('/^\[.+?\](.+)$/', '$1', $dest);
$dest = preg_replace('/^\*.+?\*(.+)$/', '$1', $dest);
$dest = trim($dest);
if ( strlen($dest) == 0 ) {
continue;
}
$dest = preg_replace('/?/u', '?', $dest);
$dest = preg_replace('/!/u', '!', $dest);
$dest_old = $dest;
// ほぼ確実
$dest = str_replace('マスタードバコ', 'ご主人様', $dest);
$dest = str_replace('ウェンチ', '娼婦', $dest);
$dest = str_replace('主が', 'あるじが', $dest);
$dest = str_replace('主に', 'あるじに', $dest);
$dest = preg_replace('/Deadly Wenches */u', '娼婦', $dest);
$dest = str_replace('見えますが、', '見えるが、', $dest);
$dest = str_replace('しれませんが、', 'しれないが、', $dest);
$dest = str_replace('ありませんが、', 'ないが、', $dest);
$dest = str_replace('はい、', 'そうだ、', $dest);
$dest = str_replace('なのですから、', 'なのだから、', $dest);
$dest = str_replace('続けていますが、', '続けているが、', $dest);
$dest = str_replace('そうですね…', 'そうだな…', $dest);
$dest = str_replace('わかりました…', 'わかった…', $dest);
$dest = str_replace('やめています…', 'やめている…', $dest);
$dest = str_replace('ありませんでした…', 'なかった…', $dest);
$dest = str_replace('はどのようにして', 'はどうやって', $dest);
$dest = str_replace('どのようにして', 'どうして', $dest);
$dest = str_replace('いたのですが', 'いたのだが', $dest);
$dest = str_replace('ごめんなさい', 'すまない', $dest);
$dest = str_replace('わかりません', 'わからない', $dest);
$dest = str_replace('気がします', '気がする', $dest);
$dest = str_replace('していただければ', 'してもらえれば', $dest);
$dest = fixString('何ですか', '何だ', $dest);
$dest = fixString('何をしましたか', '何をしたんだ', $dest);
$dest = fixString('信じています', '信じている', $dest);
$dest = fixString('元気ですか', '元気か', $dest);
$dest = fixString('助けます', '助けよう', $dest);
$dest = fixString('奴隷です', '奴隷だ', $dest);
$dest = fixString('好きです', '好きだ', $dest);
$dest = fixString('好きですか', '好きか', $dest);
$dest = fixString('必要ですか', '必要だ', $dest);
$dest = fixString('思いました', '思った', $dest);
$dest = fixString('思います', '思う', $dest);
$dest = fixString('思いますか', '思う', $dest);
$dest = fixString('思えません', '思えない', $dest);
$dest = fixString('思っています', '思っている', $dest);
$dest = fixString('持っています', '持っている', $dest);
$dest = fixString('楽しんでいますか', '楽しんでいるのか', $dest);
$dest = fixString('構いません', '構わない', $dest);
$dest = fixString('決めます', '決める', $dest);
$dest = fixString('着ています', '着ている', $dest);
$dest = fixString('知っていますか', '知っている', $dest);
$dest = fixString('知りたいのです', '知りたいんだ', $dest);
$dest = fixString('知れませんが', '知れないが', $dest);
$dest = fixString('言いません', '言わない', $dest);
$dest = fixString('言うんですか', '言うのか', $dest);
$dest = fixString('誰ですか', '誰なんだ', $dest);
$dest = fixString('従ったのです', '従ったんだ', $dest);
// まず大丈夫
$dest = fixString('いくらですか', 'いくらだ', $dest);
$dest = fixString('いるの', 'いる', $dest);
$dest = fixString('いるのです', 'いるんだ', $dest);
$dest = fixString('おかしいですか', 'おかしいか', $dest);
$dest = fixString('おきます', 'おこう', $dest);
$dest = fixString('ことですか', 'ことか', $dest);
$dest = fixString('したんですか', 'したのか', $dest);
$dest = fixString('していません', 'していない', $dest);
$dest = fixString('しましたか', 'したのか', $dest);
$dest = fixString('するつもりですか', 'するつもりなんだ', $dest);
$dest = fixString('たいですか', 'たいか', $dest);
$dest = fixString('たかったのです', 'たかったんだ', $dest);
$dest = fixString('できます', 'できる', $dest);
$dest = fixString('どうですか', 'どうだ', $dest);
$dest = fixString('なのでしょうか', 'なのか', $dest);
$dest = fixString('なんですか', 'なのか', $dest);
$dest = fixString('るのでしょうか', 'るんだ', $dest);
$dest = fixString('わかるよ', 'わかる', $dest);
// 誤変換の可能性がある
$dest = fixString('つもりですか', 'つもりか', $dest);
$dest = fixString('いますか', 'いるんだ', $dest);
$dest = fixString('ください', 'ほしい', $dest);
$dest = fixString('ありません', 'ない', $dest);
$dest = fixString('あります', 'ある', $dest);
$dest = fixString('でした', 'だった', $dest);
$dest = fixString('でしょう', 'だろう', $dest);
$dest = fixString('のでしょうか', 'んだ', $dest);
$dest = fixString('しました', 'した', $dest);
$dest = fixString('しれません', 'しれない', $dest);
$dest = fixString('いいよ', 'いいだろう', $dest);
$dest = fixString('だけです', 'だけだ', $dest);
// 誤変換の可能性が非常に高い
$dest = fixString('します', 'しよう', $dest);
$dest = fixString('のですか', 'んだ', $dest);
$dest = fixString('ですか', 'なのか', $dest);
$dest = fixString('です', 'だ', $dest);
if ($dest != $dest_old) {
echo $dest_old . PHP_EOL;
echo $dest . PHP_EOL;
}
fputs($fp1, 'ずんだもん,' . $dest . PHP_EOL);
$s = sprintf('"オーバーライド","fallout4.esm","00043A15","[000E0BFF]","%s.fuz","synthgen1male01","%s",""', $source, $dest);
fputs($fp2, $s . PHP_EOL);
if ( $dbvo_dic_buf ) {
fputs($fp3, $dbvo_dic_buf . ',' . PHP_EOL);
}
$dbvo_dic_buf = "\t" . '"' . $dest . '": "' . $source . '"';
}
if ( $dbvo_dic_buf ) {
fputs($fp3, $dbvo_dic_buf . PHP_EOL);
}
fputs($fp3, '}' . PHP_EOL);
fclose($fp1);
fclose($fp2);
fclose($fp3);
}
function fixString($replace_from, $replace_to, $haystack)
{
$haystack = preg_replace('/' . $replace_from . '$/u', $replace_to, $haystack);
$haystack = preg_replace('/' . $replace_from . '([、。…!\?])/u', $replace_to . '$1', $haystack);
return $haystack;
}
Style-Bert-VITS2で音声合成する
Style-Bert-VITS2についてはこちらで解説しています。
サーバーを立ち上げておきます。
生成スクリプトを使ってwavファイルを生成します。
>php create_DBVO_wav.php DBVO.json
wavフォルダの中に作られます。
Style-Bert-VITS2で音声合成する (php)
<?php
define('WAV_DIR', 'wav');
$context_opts = [];
$http_opts = [];
try {
if ($argc < 2) {
throw new Exception('Usage: create_DBVO_wav.php [-cn] <DBVO local pack json>');
}
$options = getopt('cfn');
$clean_up_mode = array_key_exists('c', $options);
$force_create = array_key_exists('f', $options);
$test_mode = array_key_exists('n', $options);
$json_filename = $argv[$argc - 1];
if ( !is_readable($json_filename) ) {
throw new Exception($json_filename . ' is not readable.');
}
$json_data = json_decode( file_get_contents($json_filename) );
if ( !$json_data ) {
throw new Exception('json decode failed.');
}
if ( !is_dir(WAV_DIR) ) {
mkdir(WAV_DIR);
}
$context_opts = [
'http' => [
'method' => 'GET',
]
];
$http_opts = [
'model_name' => 'Nier-2B',
'model_id' => 0,
'speaker_id' => 0,
'sdp_ratio' => 0.2,
'noise' => 0.6,
'noisew' => 0.8,
'length' => 1,
'language' => 'JP',
'auto_split' => 'true',
'split_interval' => 0.5,
'assist_text_weight' => 1,
'style' => 'Neutral',
'style_weight' => 1,
];
$basename_list = [];
foreach ($json_data as $japanese => $english) {
$japanese = preg_replace('/\(.+?\)/', '', $japanese);
$japanese = preg_replace('/__[0-9a-zA-Z=\.]+?__/', '', $japanese);
$japanese = preg_replace('/?_/', '?', $japanese);
$japanese = preg_replace('/!_/', '!', $japanese);
$japanese = str_replace('_', '、', $japanese);
$japanese = trim($japanese);
$japanese = preg_replace('/。$/', '', $japanese);
$japanese = trim($japanese);
if ( strlen($japanese) == 0 ) {
continue;
}
$english = str_replace(''', "'", $english);
$english = str_replace('"', '_', $english);
$english = str_replace('<', '<', $english);
$english = str_replace('>', '>', $english);
$english = preg_replace('/<Alias=Player> */', 'Dovaco', $english);
$english = preg_replace('/<[^>]+>/', '', $english);
$english = preg_replace('/(^.+) +\(.+?\)$/', '$1', $english);
$english = str_replace(' ', '_', $english);
$english = str_replace('?', '_', $english);
$english = str_replace('*', '_', $english);
$english = str_replace('[', '_', $english);
$english = str_replace(']', '_', $english);
$english = str_replace('%', '_', $english);
$english = str_replace(':', '_', $english);
$filename = WAV_DIR . '/' . $english . '.wav';
if ( $clean_up_mode ) {
$basename_list[] = $english;
} else {
if ( $force_create || !file_exists($filename) ) {
echo $filename . PHP_EOL;
echo $japanese . PHP_EOL;
if ( !$test_mode ) {
create_wav($japanese, $filename);
}
}
}
}
if ( $clean_up_mode ) {
$dh = opendir(WAV_DIR);
if ( !$dh ) {
throw new Exception('could not open dir - ' . WAV_DIR);
}
$pattern = '/^(.+)\.(wav|lip|fuz)$/';
while ( $file = readdir($dh) ) {
if ( $file == '.' ) {
continue;
}
if ( $file == '..' ) {
continue;
}
if ( preg_match($pattern, $file, $matches) ) {
$basename = $matches[1];
if ( !in_array($basename, $basename_list) ) {
echo $file . PHP_EOL;
$fullpath = WAV_DIR . '/' . $file;
unlink($fullpath);
}
}
}
closedir($dh);
}
} catch (Exception $e) {
echo $e->GetMessage() . PHP_EOL;
}
function create_wav($text, $filename)
{
global $context_opts, $http_opts;
$http_opts['text'] = $text;
$url = 'http://127.0.0.1:5000/voice?' . http_build_query($http_opts);
$context = stream_context_create($context_opts);
$data = file_get_contents($url, false, $context);
file_put_contents($filename, $data);
}
SSE CreationKit FixesのFaceFXWrapperでlipファイルを作る
>php create_lip.php DBVO.json
SSE CreationKit FixesのFaceFXWrapperでlipファイルを作る (php)
<?php
define('FACEFXWRAPPER', 'E:\Steam\steamapps\common\Skyrim Special Edition\Tools\Audio\FaceFXWrapper.exe');
define('ARG_TYPE', 'Skyrim');
define('ARG_LANG', 'USEnglish');
define('FONIXDATAPATH', 'E:\Steam\steamapps\common\Skyrim Special Edition\Data\Sound\Voice\Processing\FonixData.cdf');
define('WAV_DIR', 'wav');
try {
parse_dir(WAV_DIR);
} catch (Exception $e) {
echo $e->GetMessage() . PHP_EOL;
}
function parse_dir($dir)
{
$dh = opendir($dir);
if ( !$dh ) {
throw new Exception('could not open dir - ' . $dir);
}
$pattern = '/\.wav$/';
while ( $file = readdir($dh) ) {
if ( $file == '.' ) {
continue;
}
if ( $file == '..' ) {
continue;
}
if ( preg_match($pattern, $file) ) {
$fullpath = $dir . '/' . $file;
create_lip($fullpath);
}
}
closedir($dh);
}
function create_lip($wav_filename)
{
$lip_filename = str_replace('.wav', '.lip', $wav_filename);
if ( file_exists($lip_filename) ) {
return;
}
$wav_filename_new = $wav_filename;
$lip_filename_new = $lip_filename;
if ( strpos($wav_filename, '!') ) {
$wav_filename_new = str_replace('!', '_', $wav_filename_new);
$lip_filename_new = str_replace('.wav', '.lip', $wav_filename_new);
copy($wav_filename, $wav_filename_new);
}
echo $wav_filename_new . PHP_EOL;
echo $lip_filename_new . PHP_EOL;
// FaceFXWrapper [Type] [Lang] [FonixDataPath] [ResampledWavPath] [LipPath] [Text]
$cmd = sprintf(
'"%s" %s %s %s %s %s ""',
FACEFXWRAPPER,
escapeshellarg(ARG_TYPE),
escapeshellarg(ARG_LANG),
escapeshellarg(FONIXDATAPATH),
escapeshellarg($wav_filename_new),
escapeshellarg($lip_filename_new)
);
exec($cmd);
if ( !file_exists($lip_filename_new) ) {
return;
}
if ( $lip_filename_new != $lip_filename ) {
rename($lip_filename_new, $lip_filename);
unlink($wav_filename_new);
}
}
Yakitori Audio Converterでfuzファイルを作る
wavファイルからfuzファイルに変換します。
wavフォルダをYakitori Audio Converterにドラッグ&ドロップすればいいです。
Modとしてインストールする
fuzファイルを所定の位置に配置します。