2024年10月11日星期五

ChatTTS 初遇:开源语音合成模型体验与音色克隆

ChatTTS 是一款专门为对话场景设计的文本转语音(TTS)模型。它生成的语音非常自然,甚至包含口语化的语气词。

安装步骤

  1. 获取代码
1
2
git clone https://github.com/2noise/ChatTTS
cd ChatTTS
  1. 安装依赖
1
pip install --upgrade -r requirements.txt

假如有用conda,使用以下代码安装依赖

1
2
3
conda create -n chattts
conda activate chattts
pip install -r requirements.txt
  1. 启动Webui(可选,直接使用下面的代码)
1
python examples/web/webui.py

(在webui中,假如想用个性化音色,需要注意你的录音要和Sample Text一样)

实验

实用系统内置的音色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import torch
import torchaudio
import ChatTTS
from tools.audio import float_to_int16, has_ffmpeg_installed, load_audio

chat = ChatTTS.Chat()
chat.load(compile=False) # Set compile=True for optimized performance

rand_spk = chat.sample_random_speaker()
print('=================')
print(rand_spk) # save it for later timbre recovery

params_refine_text = ChatTTS.Chat.RefineTextParams(
prompt='[oral_2][laugh_0][break_6]',
)

texts = ["哈克和迪克这两个小东西看起来古灵精怪,其实是两个野心勃勃的危险分子——他们在牙齿上挖洞建房,不仅要修建自己的舒适小窝,还梦想着修建可以出租的豪华公寓……",
"就在他们的梦想快要实现的时候,一把大刷子带着很多警察出现在牙齿大街上。哈克和迪克贮藏的粮食几乎被一扫而空。更可怕的事情还在后面,一个巨大的钩子从天而降,伸向了哈克和迪克的家……",
"哈克和迪克的命运将会怎样呢?那些警察是从哪里来的呢?牙齿大街还能恢复往日的平静吗?",
"看完这本图画书之后,聪明的小朋友们肯定会知道,怎样做才能不让哈克和迪克这样的小东西在我们的嘴巴里干坏事!"]

refine_texts = chat.infer(texts,
skip_refine_text=False,
refine_text_only=True,
params_refine_text=params_refine_text,)
print('======refine===========')
for i in range(len(refine_texts)):
print(refine_texts[i])

params_infer_code = ChatTTS.Chat.InferCodeParams(
spk_emb = rand_spk, # add sampled speaker
temperature = .3, # using custom temperature
top_P = 0.7, # top P decode
top_K = 20, # top K decode
)

print('======infer===========')
wavs = chat.infer(refine_texts,
skip_refine_text=True,
params_infer_code=params_infer_code,)

for i in range(len(wavs)):
"""
In some versions of torchaudio, the first line works but in other versions, so does the second line.
"""
try:
torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]).unsqueeze(0), 24000)
except:
torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]), 24000)

主要就是使用 chat.infer 这个方法,它有一个参数refine_text_only,可以控制是否只是断句。调用完之后返回的是文字列表,中间会增加断句标识。

第二次调用infer方法的时候就可以传入skip_refine_text,因为我们已经实现做好断句了。

另外,rand_spk 这个的输出有点意思,需要留意一下,因为下面我们去做自定义音色的时候会用到,这个方便理解。这个就是编码后音色编码,看起来像一堆中文乱码,不是什么错误,不要紧张。

自定义音色

首先要录制一小段语音,可以念一句话,你可以用系统的录音机,或者用我做的一个录音工具,采样率用24000,录制个5-10秒即可。

假如你不知道念什么,很奇怪,突然让你说一句话,脑子就一片空白,至少我是这样,当时我就找了这么一行。

1
哈克和迪克这两个小东西看起来古灵精怪

下载录音为wav文件,注意看下码率对不对。准备好之后就用下面的方法来生成那个奇怪的文本。复制保存下来就好。

1
2
3
4
sample_audio = load_audio('/Users/sam/Music/sample-me.wav', 24000)
sample_spk_smp = chat.sample_audio_speaker(sample_audio)
print('================')
print(sample_spk_smp)

然后修改params_infer_code参数

1
2
3
4
5
6
7
8
9
10
11

params_infer_code = ChatTTS.Chat.InferCodeParams(
spk_emb = None, # add sampled speaker
temperature = .3, # using custom temperature
top_P = 0.7, # top P decode
top_K = 20, # top K decode
)

params_infer_code.txt_smp = sample_text
params_infer_code.spk_smp = sample_spk_smp
params_infer_code.spk_emb = None

再重新调用infer 生成wav就可以了。

实验结果

[牙齿大街的新鲜事介绍.mp3](ChatTTS初遇 11c0ba99e0cb8072b8f3f286b815a0c2/牙齿大街的新鲜事介绍.mp3)

完整代码供你复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import torch
import torchaudio
import ChatTTS
from tools.audio import float_to_int16, has_ffmpeg_installed, load_audio

chat = ChatTTS.Chat()
chat.load(compile=False) # Set compile=True for optimized performance

rand_spk = chat.sample_random_speaker()
print('=================')
print(rand_spk) # save it for later timbre recovery

params_infer_code = ChatTTS.Chat.InferCodeParams(
spk_emb = None, # add sampled speaker
temperature = .3, # using custom temperature
top_P = 0.7, # top P decode
top_K = 20, # top K decode
)

params_refine_text = ChatTTS.Chat.RefineTextParams(
prompt='[oral_2][laugh_0][break_6]',
)

sample_text = "哈克和迪克这两个小东西看起来古灵精怪"
sample_text = "哈克和迪克这两个小东西看起来古灵精怪,其实是两个野心勃勃危险分子——他们在牙齿上挖洞建房,不仅要修建自己的舒适小窝,还梦想着修建出可以出租的豪华公寓……"
sample_audio = load_audio('/Users/sam/Music/sample-me.wav', 24000)
sample_spk_smp = chat.sample_audio_speaker(sample_audio)
print('================')
print(sample_spk_smp)
sample_spk_smp = "伀嫠冀呯伟叐乸乐絁稃蒌棩芡籵囮潁贅缕匰卅窃檯泫懳潱盾簫旬跊恫挝弫熠哦襛劍県娑虅荙偂瑦尾絖诧扐硝件埇唲贸屗榃孥嗇圇岨师袋衟丑睽婢牦砑簹摱稞儎蓌渤吷厹片曌怸劸秴勊浍攙懄名檃脴獙畗弁跷璤嫥兘螵煻壕捻丏焒蚘愢墁冧奰庽脼侲忱莼弣啸礛姿早谯漉吞呙伏穞澭崰枑芮纟嫤桻祙腉谱痚蝱欇橇拂彼丿磜蠛胀柑搪珙嫩宇捶粣凱瞒嵼琅嶷綯橧觽焓諛僂焽貛崶熝烥糱蕪瓏欞觼杞蒏檟蠠敫佩旐趚葘濬劘濺趵俴焔咀螂曻裘巋艋蜆矣倾愆狨簞耺帐瓭懯椱囘恗奵徻圫拏炶犑漧琺璥獮砫綀灡嚂蔁挬滂嶷盁湉掂爗蜷賸胅萳洴捥楌猥圑此跂艳莞矴謦蝖妌為炇筷檷菏纳汥咙攘狣怊棽秔箚翹玑吼荣杮僉副薣浖蜺犡匛劀懫筩誰謙福椅縬孊日胶徇礹濯榤劽睺碀儦懄瞥蕳亄缁谉盰櫜吏句蠲栵倡奮叴枬搶篽全貛瓮攪掉摃媅孎勜垻篇篽岌藡稠猵暄犾葠妘俺菢謩硼虲撖槞撳枆猲岪怯穤誋杫狀紽曁秕蜲祷愡赽獽勦咄汄渀媭漘晬檻晕嶩令皑婪拂程姪棱赑琍縲撑梂趝岌脧琸曯殜嬊翖廏勱荪簝爄匱蛴悔兰爙讒孁谮熌劐诱竾帉搾瓚忪弒斪賾疹猘衻薎蜃绖匙蛧珱续噂峖数芛憓胠咮梻禨惇谲塩穸狃嗍苜磳粩檚去峅匛蟆侏襆赿纝勀捤觇澉炜兯哫拵誣畡舙斬本僡犔籲螘螸藆噎庬上樹瘞悫憥梺潛槵槢寒爬绥帪呮綳獴嗆裧杚滖蟱唳啶晘裫凮灥蝖江贉播景豫僞蓋覐蒤挰坽惪艒證珜蓧劫艒箁瀯垩簃溷藺婺氐熸抻籓洽瓃圷箼縍學澦蔉寋脶筠棳崿匊誅葽戜劀绀芻肿褁喕巤聗沼汯昩毂篨挴直戵耪珴瞭挬舅眠弹擗诈朡服眜瀬屡住啍倩罷眨炦硓憸梒剢嘇衅盔穸捬搩翅蜸煁箚渶嫭慱擡璩碧涺倅簨戒媮滏蕱擉蟼蕝詬詍悍碟妇倪生扫始砟艓埰堷悻莃孩捂環詆係喱儢晝哐塻櫚睡稼媦菤夂綏螫埲膹为緮筱板堿茆仾嚳典甞刴茥火彔堤砑芩熵諅惚襺胑暀昹庮攈粊痛樀㴅"

texts = ["哈克和迪克这两个小东西看起来古灵精怪,其实是两个野心勃勃的危险分子——他们在牙齿上挖洞建房,不仅要修建自己的舒适小窝,还梦想着修建可以出租的豪华公寓……",
"就在他们的梦想快要实现的时候,一把大刷子带着很多警察出现在牙齿大街上。哈克和迪克贮藏的粮食几乎被一扫而空。更可怕的事情还在后面,一个巨大的钩子从天而降,伸向了哈克和迪克的家……",
"哈克和迪克的命运将会怎样呢?那些警察是从哪里来的呢?牙齿大街还能恢复往日的平静吗?",
"看完这本图画书之后,聪明的小朋友们肯定会知道,怎样做才能不让哈克和迪克这样的小东西在我们的嘴巴里干坏事!"]

refine_texts = chat.infer(texts,
skip_refine_text=False,
refine_text_only=True,
params_refine_text=params_refine_text,)
print('======refine===========')
for i in range(len(refine_texts)):
print(refine_texts[i])

params_infer_code.txt_smp = sample_text
params_infer_code.spk_smp = sample_spk_smp
params_infer_code.spk_emb = None

print('======infer===========')
wavs = chat.infer(refine_texts,
skip_refine_text=True,
params_infer_code=params_infer_code,)

for i in range(len(wavs)):
"""
In some versions of torchaudio, the first line works but in other versions, so does the second line.
"""
try:
torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]).unsqueeze(0), 24000)
except:
torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]), 24000)

2024年9月25日星期三

Dify 踩坑记录(续):长文翻译与沙盒优化

在前文中,我们实现了一个反思翻译工作流。下面开始挑战长文翻译。

长文翻译的思路是:由于大模型的单次 Token 限制,对于长文章需要分段翻译再整合。

这里着重要讲的是我遇到的两个核心问题:Sandbox 权限与工作池限制。

问题 1: Sandbox 问题

安装三方库

在分片的节点中,我们需要用到三方的python库。

1
2
3
import tiktoken
from icecream import ic
from langchain_text_splitters import RecursiveCharacterTextSplitter

由于是sandbox镜像,事先并没有我们要的三方库。实际上sandbox项目留了这个配置。

dify/docker/volumes/sandbox/dependencies/python-requirements.txt中增加就可以。

1
2
3
tiktoken==0.6.0
langchain-text-splitters>=0.0.1
icecream==2.1.3

沙盒权限问题

如何在sandbox限制了一些系统调用,即时你安装了三方库,也用不了。这个花了好好几个小时,希望能够给你一些帮助。在github上,发现很多人都有遇到这个问题。https://github.com/langgenius/dify/issues/4993https://github.com/langgenius/dify/issues/4344,我也尝试一堆方案,比如典型的一个建议就是,类似这样,增加`security_opt`配置,然而并没有成功。假如你也遇到这样的问题,就不要再花时间了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.8
restart: always
privileged: true
user: "root"
read_only: false
cap_add:
- SYS_ADMIN
security_opt:
- no-new-privileges:false
- "seccomp=unconfined"
environment:
# The DifySandbox configurations
# Make sure you are changing this key for your deployment with a strong key.
# You can generate a strong key using `openssl rand -base64 42`.
API_KEY: ${SANDBOX_API_KEY:-dify-sandbox}
GIN_MODE: ${SANDBOX_GIN_MODE:-release}
WORKER_TIMEOUT: ${SANDBOX_WORKER_TIMEOUT:-15}
ENABLE_NETWORK: ${SANDBOX_ENABLE_NETWORK:-true}
HTTP_PROXY: ${SANDBOX_HTTP_PROXY:-http://ssrf_proxy:3128}
HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128}
SANDBOX_PORT: ${SANDBOX_PORT:-8194}
volumes:
- ./volumes/sandbox/dependencies:/dependencies
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8194/health" ]
networks:
- ssrf_proxy_network

我的做法是直接改镜像,这也是在官方的faq里面的处理方法。https://github.com/langgenius/dify-sandbox/blob/main/FAQ.md#2-my-python-code-returns-an-operation-not-permitted-error。

下载dify-sandbox源代码

打开internal/static/python_syscall/syscalls_arm64.go,修改其中ALLOW_SYSCALLS

注意,faq里面写的探测需要哪些权限的步骤我并没有做成功,有知道的小伙伴请分享你的经验。由于我不知道究竟缺少什么权限,所以就直接全部写上0-500。

这是我修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var ALLOW_SYSCALLS = []int{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
100, 101, 102, 103, 104, 105, 106, 107, 108, 109,
110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
130, 131, 132, 133, 134, 135, 136, 137, 138, 139,
140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
150, 151, 152, 153, 154, 155, 156, 157, 158, 159,
160, 161, 162, 163, 164, 165, 166, 167, 168, 169,
170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
180, 181, 182, 183, 184, 185, 186, 187, 188, 189,
190, 191, 192, 193, 194, 195, 196, 197, 198, 199,
200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
210, 211, 212, 213, 214, 215, 216, 217, 218, 219,
220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
250, 251, 252, 253, 254, 255, 256, 257, 258, 259,
260, 261, 262, 263, 264, 265, 266, 267, 268, 269,
270, 271, 272, 273, 274, 275, 276, 277, 278, 279,
280, 281, 282, 283, 284, 285, 286, 287, 288, 289,
290, 291, 292, 293, 294, 295, 296, 297, 298, 299,
300, 301, 302, 303, 304, 305, 306, 307, 308, 309,
310, 311, 312, 313, 314, 315, 316, 317, 318, 319,
320, 321, 322, 323, 324, 325, 326, 327, 328, 329,
330, 331, 332, 333, 334, 335, 336, 337, 338, 339,
340, 341, 342, 343, 344, 345, 346, 347, 348, 349,
350, 351, 352, 353, 354, 355, 356, 357, 358, 359,
360, 361, 362, 363, 364, 365, 366, 367, 368, 369,
370, 371, 372, 373, 374, 375, 376, 377, 378, 379,
380, 381, 382, 383, 384, 385, 386, 387, 388, 389,
390, 391, 392, 393, 394, 395, 396, 397, 398, 399,
400, 401, 402, 403, 404, 405, 406, 407, 408, 409,
410, 411, 412, 413, 414, 415, 416, 417, 418, 419,
420, 421, 422, 423, 424, 425, 426, 427, 428, 429,
430, 431, 432, 433, 434, 435, 436, 437, 438, 439,
440, 441, 442, 443, 444, 445, 446, 447, 448, 449,
450, 451, 452, 453, 454, 455, 456, 457, 458, 459,
460, 461, 462, 463, 464, 465, 466, 467, 468, 469,
470, 471, 472, 473, 474, 475, 476, 477, 478, 479,
480, 481, 482, 483, 484, 485, 486, 487, 488, 489,
490, 491, 492, 493, 494, 495, 496, 497, 498, 499,
500,
}

修改了之后,需要执行install.sh,以及./build/build_[amd64|arm64].sh完成go的构建。

1
2
./install.sh 
./build/build_arm64.sh

构建完成,会生成envmain两个可执行程序。

下一步就是就用docker重新构建镜像。

1
docker build  --progress=plain  -t mysandbox -f docker/arm64/dockerfile .

修改docker/docker-compose.yaml的sandbox的image,改成刚刚构建的镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# The DifySandbox
sandbox:
image: mysandbox
restart: always
privileged: true
cap_add:
- SYS_ADMIN
security_opt:
- no-new-privileges:false
- "seccomp=unconfined"
environment:
# The DifySandbox configurations
# Make sure you are changing this key for your deployment with a strong key.
# You can generate a strong key using `openssl rand -base64 42`.
API_KEY: ${SANDBOX_API_KEY:-dify-sandbox}
GIN_MODE: ${SANDBOX_GIN_MODE:-release}
WORKER_TIMEOUT: ${SANDBOX_WORKER_TIMEOUT:-15}
ENABLE_NETWORK: ${SANDBOX_ENABLE_NETWORK:-true}
HTTP_PROXY: ${SANDBOX_HTTP_PROXY:-http://ssrf_proxy:3128}
HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128}
SANDBOX_PORT: ${SANDBOX_PORT:-8194}
volumes:
- ./volumes/sandbox/dependencies:/dependencies
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8194/health" ]
networks:
- ssrf_proxy_network

运行新的容器,回界面测试

1
docker compose up -d

临时目录权限问题

到目前为止,权限的问题解决了,但马上就会有新的问题。错误看起来像这样

1
An error occurred: [Errno 2] No usable temporary directory found in ['/tmp', '/var/tmp', '/usr/tmp', '/']

这个问题的原因是sandbox的执行的用户没有写tmp目录的权限。https://dify.ai/blog/dify-ai-blog-introducing-difysandbox,在官方博客中介绍大致是这个意思,每一个沙盒会在`/var/sandbox/`隔离。sandbox的执行用户是sandbox,我不知道是什么原因原始镜像中并没有给这个设置好权限。

于是就手工在容器中执行

1
2
cd /var/sandbox/
chmod 1777 -R *

工作池问题

现在你可以去测试工作流了,马上就会遇到新的问题。

1
Max submit count 100 of workflow thread pool reached.

为什么是100,这个问题比较容易,通过google搜索社区的内容,按照api容器就好。

https://github.com/langgenius/dify/issues/8659#issuecomment-2367260559

修改完成之后重新构建docker镜像,修改docker-compose文件,使用新的镜像,注意里面有两个容器,apiworker,都用api的镜像。

结束

到此为止,长文翻译的工作流也就算配置完成了。整个事情大概忙了我10多个小时,中间那个权限问题消耗了7个小时。很多概念都不清楚,比如什么seccomp,只是马虎的把问题趟过去了。

另外,这是一个很有用的工具,后边想想能用它来做点什么,或者你有什么好点子也欢迎一起。

附录

工作流文件

长文翻译.yml