视觉小说中给男主和旁白添加日语配音(以千恋万花为例)

最近无意发现,许多基于 kirikiri 的视觉小说(包括 Galgame)有一个给无语音的文字(如男主所说的话、旁白)添加配音的接口。使用语音合成软件后,游戏中男主将可以顺利发出语音(医 学 奇 迹):

注意右下角的 VOICE 条,表示正在播放语音
因为马上要考日语 N2 的缘故,我希望通过视觉小说练习日语听力,从而希望男主也有语音。这里以《千恋万花》为例,简述一下如何使用这个接口给男主和旁白添加配音。

在菜单栏中,先勾上【帮助—显示高级设置】,然后在【高级设置—语音合成引擎…—目标语言:Japanese】,就可以打开这个语音合成功能的配置窗口,如下图所示。

千恋万花的语音合成设置界面
我这里的语音合成引擎使用了 VOICEVOX,这是一个免费、开源的日语语音合成引擎。我使用 Python 写了一个简单的脚本,调用本地的 VOICEVOX 合成语音。下面将具体阐述实现方法。

实现原理

这里需要指定一个外部的 exe 文件将文字转为语音。这个接口要求 exe 文件支持以命令行输入文本,并输出 wav 音频文件。更详细地说,我们需要配置一个 exe 文件,并且配置命令行中调用这个文件的方法,使用 %t 传入需要语音合成的文字,使用 %f 指定输出的 wav 文件的位置。随后,游戏会自动将男主、旁白等无语音的文字传给这个程序,然后读取输出的 wav 文件,并在游戏内自动播放合成的语音。

因此,我们需要手动构造一个 exe 文件 my.exe,并通过如下命令合成语音:

.\my.exe -t "%t" -o "%f"

这个程序可以用 Python 或 C++ 实现。

实现方法

通过 HTTP 请求实现文字转语音

首先下载前文提到的 VOICEVOX,安装并启动。看不懂日语?没关系,这个大大的下载按钮你总是认得出来的吧?

根据 voicevox engine Github 仓库 的描述,当 voicevox 启动时,本地实际上运行了一个小服务端,可以往 50021 端口发送 http 请求进行语音合成。根据官方仓库的 readme,在 Linux 上,语音合成的终端命令为:

echo -n "こんにちは、音声合成の世界へようこそ" >text.txt

curl -s \
    -X POST \
    "127.0.0.1:50021/audio_query?speaker=1"\
    --get --data-urlencode text@text.txt \
    > query.json

curl -s \
    -H "Content-Type: application/json" \
    -X POST \
    -d @query.json \
    "127.0.0.1:50021/synthesis?speaker=1" \
    > audio.wav

但我们一般使用的是 Windows,为了便于通过 exe 文件调用,我们将其写为功能相同的 Python 代码或 C++ 代码。

Python 脚本实现

以下是用 Python 实现的 get_audio.py

import argparse
import re
import requests


def get_parser():
    parser = argparse.ArgumentParser(usage="get_audio.py -t TEXT -o OUTPUT")
    parser.add_argument("-t", "--text", type=str, required=True)
    parser.add_argument("-o", "--output", type=str, default="audio.wav")
    parser.add_argument("-s", "--speaker", type=int, default=2)
    parser.add_argument("-S", "--speed", type=float, default=1.0)
    return parser


def main(text: str, output_path: str, speaker: int, speed: float=1.0) -> None:
    url_audio_query = "http://127.0.0.1:50021/audio_query"
    params = {
        "speaker": speaker,
        "text": text
    }
    response = requests.post(url_audio_query, params=params)
    response.raise_for_status()
    query_data = response.text

    query_data = re.sub(r'"speedScale":[0-9.]+', f'"speedScale":{speed:.2f}', query_data)

    url_synthesis = "http://127.0.0.1:50021/synthesis"
    headers = {
        "Content-Type": "application/json"
    }
    params2 = {
        "speaker": speaker,
    }
    response = requests.post(url_synthesis, headers=headers, params=params2, data=query_data)
    response.raise_for_status()
    with open(output_path, "wb") as file:
        file.write(response.content)

if __name__ == "__main__":
    parser = get_parser()
    args = parser.parse_args()
    main(args.text, args.output, args.speaker, args.speed)

此代码可通过调用函数 main("こんにちは、音声合成の世界へようこそ", "./audio.wav", 2),指定使用 2 号角色的声线,将“こんにちは、音声合成の世界へようこそ”转换为语音,并保存到 audio.wav 文件中,也可在外部使用 Python 调用文件:

python get_audio.py -t こんにちは、音声合成の世界へようこそ -o ./audio.wav -s 2
C++ 实现

使用 Python 时,由于 Python 是解释型语言,可能延迟较高。我们可以通过导入轻量级的 HTTP 包 httplib.h,实现通过 C++ 发送 HTTP 请求,降低延迟。

httplib 的头文件可从 Github 仓库 下载,这是一个单头文件(Header Only)的库,直接 include 便可以在程序内使用。

完整 C++ 代码如下,本程序没有导入过多的第三方库,因此不需要借助 Cmake 等工具,可以使用 g++ 直接编译。

Windows CMD 或 Powershell 默认使用 GBK 对字符进行编码,因此通过 char *argv[] 传入字符时也是 GBK,而我们需要使用 UTF-8 编码字符并发送 http 请求,因此使用函数 convert_gbk_to_uft8 来处理字符编码的转换。

#include <iostream>
#include <fstream>
#include <sstream>
#include <regex>
#include "httplib.h"
#include <windows.h>

std::string convert_gbk_to_utf8(const std::string &gbk_str) {
    // Step 1: Convert GBK to Wide String (UTF-16)
    int wide_size = MultiByteToWideChar(936, 0, gbk_str.c_str(), -1, nullptr, 0);
    if (wide_size == 0) {
        return "";
    }

    std::wstring wide_str(wide_size, 0);
    MultiByteToWideChar(936, 0, gbk_str.c_str(), -1, &wide_str[0], wide_size);

    // Step 2: Convert Wide String (UTF-16) to UTF-8
    int utf8_size = WideCharToMultiByte(65001, 0, wide_str.c_str(), -1, nullptr, 0, nullptr, nullptr);
    if (utf8_size == 0) {
        return "";
    }

    std::string utf8_str(utf8_size, 0);
    WideCharToMultiByte(65001, 0, wide_str.c_str(), -1, &utf8_str[0], utf8_size, nullptr, nullptr);

    return utf8_str;
}

void process(const std::string& text, const std::string& output_path, int speaker, float speed) {
    httplib::Client cli("http://127.0.0.1:50021");

    // Perform the audio query
    httplib::Params params;
    params.emplace("speaker", std::to_string(speaker));
    params.emplace("text", text);

    std::string query_string = "/audio_query?speaker=" + std::to_string(speaker) + "&text=" + text;

    httplib::Result res = cli.Post(query_string);
    if (!res || res->status != 200) {
        throw std::runtime_error("Failed to perform audio query: " + std::to_string(res->status));
    }
    std::cout << res->body << std::endl;
    std::string query_data = res->body;

    // Adjust the speed scale in the query data
    std::regex speed_regex("\"speedScale\":[0-9.]+");
    query_data = std::regex_replace(query_data, speed_regex, "\"speedScale\":" + std::to_string(speed));

    // Perform the synthesis request
    httplib::Headers headers = {
        {"Content-Type", "application/json"}
    };

    std::string synthesis_string = "/synthesis?speaker=" + std::to_string(speaker);

    res = cli.Post(synthesis_string, headers, query_data, "application/json");
    if (!res || res->status != 200) {
        throw std::runtime_error("Failed to perform synthesis: " + std::to_string(res->status));
    }

    // Write the output to a file
    std::ofstream output_file(output_path, std::ios::binary);
    output_file.write(res->body.c_str(), res->body.size());
}

int main(int argc, char* argv[]) {
    std::string text;
    std::string output = "audio.wav";
    int speaker = 2;
    float speed = 1.0f;

    for (int i = 1; i < argc; ++i) {
        std::string arg = argv[i];
        if (arg == "-t" && i + 1 < argc) {
            text = convert_gbk_to_utf8(argv[++i]);
        } else if (arg == "-o" && i + 1 < argc) {
            output = argv[++i];
        } else if (arg == "-s" && i + 1 < argc) {
            speaker = std::stoi(argv[++i]);
        } else if (arg == "-S" && i + 1 < argc) {
            speed = std::stof(argv[++i]);
        }
    }

    if (text.empty()) {
        std::cerr << "Usage: get_audio -t TEXT -o OUTPUT [-s SPEAKER] [-S SPEED]" << std::endl;
        return 1;
    }

    try {
        process(text, output, speaker, speed);
    } catch (const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

程序的编译命令如下:

g++ -std=c++17 -o get_audio get_audio.cpp -lws2_32

其中 -lws2_32 表示链接 Windows 上的 ws2_32.lib 库,用于网络请求。

编译完成后,使用方法和 Python 脚本类似,在 VOICEVOX 软件打开的状态下,通过如下命令启动语音转换:

.\get_audio.exe -t "音声合成の世界へようこそ" -o audio.wav -s 2

在视觉小说中调用脚本

打开【高级设置—语音合成引擎…—目标语言:Japanese】,如下图所示。根据游戏的不同,下图可能是全英语或全日语,但按钮的位置都相同,不影响配置。

语音合成窗口
首先在①处选择安装好的 Python 解释器路径(或编译好的 C++ 程序路径),然后点击②,设置调用文字转语音的命令,我们直接让解释器调用 Python 脚本(或者直接使用 C++ 程序)即可。这里需要使用 "%t" 作为占位符表示要输入的文字,"%f" 作为输出文件的路径。依照前面所写脚本 get_audio.py 或源代码 get_audio.cpp,还可以用 -s 指定哪个角色说话,-S 指定说话语速,命令为:

python path/to/get_audio.py -t "%t" -o "%f" -s 11 -S 0.9

配置如下图所示:

命令设置
此时,点击左下角的“播放样本”,若发出合成的声音就是成功了。注意,此时 VOICEVOX 软件必须保持打开的状态,否则无法合成声音。

如果希望可移植性,可以将这个 Python 脚本使用 pyinstaller 等工具打包为单个 exe 文件,然后直接用游戏调用这个 exe。但是经过我自己的测试,打包后的可执行文件,似乎比直接用 Python 调用脚本的速度稍慢,游戏内的延迟稍高,因此最终我没有这样配置。

游戏内配置

在“播放样本”顺利合成声音后,退出设置并进入游戏剧情,会发现可能游戏此时仍然无法自动合成声音。

这是因为,此时游戏的主语言必须是日语。在有官方中文版的游戏(例如《千恋万花》和《ATRI》)内,可以设置游戏的主语言为日语,字幕语言为中文,此时就可以正常合成声音了。

评价和扩展

使用 VOICEVOX 进行单句语音合成还相对较自然,但放在游戏里与其他声优的语气比较,就会发现合成音的语气较为僵硬,属于无感情棒读。但是用于练日语听力也足够了。

这种方法可以扩展到其他的语音合成引擎。只要能正确的写出一个根据文字生成 wav 音频文件的程序,游戏就可以通过这个程序和合成引擎交互。

参考

本文同步发表于 知乎文章

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值