2024年12月13日星期五

用 Claude 整理音乐文件:尝试 MCP

引言

前几天整理电脑时,发现音乐文件夹里躺着一堆从各个渠道收集来的歌曲。

文件名五花八门,有的是繁体中文带英文,有的带着各种特殊符号,看起来很是混乱。正好我在使用 Claude Desktop,突然想到也许可以让 AI 帮我处理这个问题。

结果这一尝试不仅完美解决了我的问题,还让我发现了 AI 助手的更多可能性。今天想和大家分享这个有趣的经历。

Claude Desktop 能做什么

很多人可能和我之前一样,觉得 AI 助手就是用来聊天、问问题的。但其实 Claude Desktop 版本具备文件系统访问能力,这意味着它可以:

  • 读取和写入文件
  • 重命名文件
  • 创建和管理目录
  • 搜索文件
  • 分析文件内容

简单说,它就像一个懂编程的助手,可以帮你处理实际的文件操作任务。

整理音乐文件的实践

第一步是让 Claude 列出所有文件。看着长长的文件列表,我发现了各种命名风格:

1
2
3
張震嶽 A-Yue【愛我別走 Love me,don't go】Official Music Video.mp3
周華健 Wakin Chau【其實不想走 I didn't intend to go】Official Music Video.mp3
Beyond 05: 光輝歲月.mp3

我告诉 Claude 我想要简单的”歌手-歌名”格式。它立即理解了我的需求,并提出了完整的处理方案:

  1. 统一使用简体中文
  2. 去掉所有额外信息(如 Official Music Video、动态歌词等)
  3. 移除特殊符号
  4. 对于合唱歌曲用”&”连接歌手名
  5. 保持文件扩展名不变

在实际操作过程中,我们遇到了一些挑战。文件名中的单引号导致重命名操作失败,Claude 尝试了多种解决方案:

  • 使用转义字符
  • 尝试 encodeURIComponent
  • 使用 Buffer 处理路径
  • 使用通配符匹配

但由于 filesystem mcp 的实现还不够完善,这些方法都没有成功。最后是我手动清理了文件名中的特殊字符,Claude 才成功完成了重命名操作。这个过程也反映出目前 AI 工具在文件系统操作上还有提升空间。

处理后的文件变成了:

1
2
3
张震岳-爱我别走.mp3
周华健-其实不想走.mp3
Beyond-光辉岁月.mp3

不仅如此,Claude 还能识别一些特殊情况:

  • 对串烧歌曲使用”金曲串烧”命名
  • 正确处理合唱歌曲(如”王馨平&王杰&王韵婵-祈祷.mp3”)
  • 保留乐队名称(如 Beyond)不做改动

整个过程就像和一个细心的助手在合作,它会清楚地告诉我每一步的想法,在遇到问题时积极尝试不同的解决方案,遇到自身限制时也会实事求是地告诉我。这种互动方式让问题解决变得更加透明和有趣。

启发与思考

这次经历让我意识到,AI 助手不仅仅是一个对话工具,它可以真正参与到我们的实际工作中。关键是要学会如何描述问题,以及如何和它互动。

有趣的是,Claude 往往会提供多个解决方案,并且会解释每种方案的优缺点。这种交互方式不仅帮助解决问题,还能学到新的知识。

更多有趣的应用场景

通过这次整理音乐文件的经历,我们看到了 AI 助手在文件管理方面的能力。虽然目前 filesystem mcp 还比较基础,只能进行文件重命名、移动等基本操作,但已经能帮我们解决不少实际问题。

期待未来随着功能的扩展,它能够处理更多复杂的文件操作任务。但目前,我们也要清楚地认识到它的局限性 —— 比如无法读取和分析 MP3 文件的元数据。

写给读者的建议

做好备份,从小任务开始尝试。更重要的是要学会与 AI 助手对话,清晰地描述你的需求,让它成为你解决实际问题的好帮手。

2024年10月11日星期五

ChatTTS 初遇:开启 AI 配音新篇章

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)