DBVOのセリフを作った時のおぼえがき

雑記

Buxom Wench YurianaをDBVOに対応させました。

必要なもの

  • 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ファイルを所定の位置に配置します。

タイトルとURLをコピーしました