最近无意发现,许多基于 kirikiri 的视觉小说(包括 Galgame)有一个给无语音的文字(如男主所说的话、旁白)添加配音的接口。使用语音合成软件后,游戏中男主将可以顺利发出语音(医 学 奇 迹):
因为马上要考日语 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
音频文件的程序,游戏就可以通过这个程序和合成引擎交互。
参考
- 【图片】减少啃生肉难度,使用ai音声引擎让男主自然说话【柚子社吧】_百度贴吧 ——该文章的软件和方法很有启发性,但很可惜《千恋万花》和《ATRI》没有显示对接 VOICEVOX 接口的功能
本文同步发表于 知乎文章