最近刷社区时又看到了一类很上头的创意玩法:把一段歌词丢进机器翻译,在不同语言之间来回翻译二十遍,等文本面目全非以后,再配上原曲旋律把它“唱”出来。这类视频的标题往往写成类似“谷歌翻译 20 次《人是猫》然后自己唱出来”的形式,喜剧效果基本来自认真演唱和荒诞歌词之间的反差。
这类实验看起来像整活,但拆开之后其实是一个很完整的小型技术项目,会涉及机器翻译接口调用、多语言回环逻辑、歌词文本对齐、TTS 语音合成、音频时间伸缩和伴奏混合。非常适合作为周末练手项目,也能帮你把“调用在线 API”“处理音频文件”这些技能串起来。
本文会从零开始,带你做一套可以本地运行的“翻译回环 + 语音合成”实操方案。我们不依赖某个固定浏览器页面,也不需要讨论怎样访问境外网站,而是使用国内开发者可以正常申请的云翻译 API 和一个开源 TTS 库来复刻同款效果。如果你能翻到原版的视频,会发现核心玩法完全一致:先让机器把歌词翻译到面目全非,最后再想办法唱出来。
为了尊重歌词版权,本文不会整段复制某首歌曲的原文。示例中会使用四句自拟的、长度接近中文歌曲主歌的文本作为演示输入,你只需要把自己的目标歌词保存到input/lyrics.txt,之后所有步骤都可以直接复用。
1. 这个创意玩法到底在做什么
1.1 什么是“翻译回环”
翻译回环,英文里常叫 Translation Loop 或 Round-trip Translation。最简形式是:把一段文本从 A 语言翻译到 B 语言,再把 B 语言结果翻译回 A 语言,观察前后两次文本的差异。
这个玩法最早的流行场景,就是“谷歌翻译 XX 次”系列。人们把一段歌词、名言或新闻丢进翻译器,在中文、英文、日文、法文等语言之间反复横跳。机器翻译并不是逐字查找词典,而是基于上下文重新生成目标语言。每一次翻译都可能造成细微的歧义丢失、指代漂移和语气变化。次数多了以后,原文就会变得非常离谱,有时候甚至会产生一种“AI 大脑中经过多次折射后的梦境感”。
简而言之:翻译回环不是单纯地“翻过去再翻回来”,而是让文本在不同语言模型中反复被重新解释,最终产生意外的语义失真。这个特性和语言模型的学习方式有关,也和源语言与目标语言之间的语序差异、文化差异有关。
网上常见的版本一般是“翻译 20 次”。次数太少,语义可能变化不明显;次数太多,文本又容易坍缩成一句简单且无趣的短句。20 次是一个比较平衡的经验值,既能让内容产生足够多的失真,又保留一些残存的情绪与意象。
1.2 为什么要拿歌词来做实验
歌词和新闻稿不同,它天然带有三个属性:
- 有节奏
- 有旋律
- 有情绪意象
即使歌词经过 20 次翻译后变得荒诞,只要你还记得原曲旋律,依然能感受到某种“节奏上的熟悉感”。例如“晚风轻轻吹过城市的屋顶”经过多次翻译后,可能变成“晚上的风慢慢越过城市的顶部”,甚至变成一句更奇怪的表达。但当演唱者用严肃认真的情绪去唱这句荒诞歌词时,听感上的反差就会非常强烈。
所以这个实验的重点不是“机器翻得好不好”,而是“糟糕翻译后的文本与原始旋律重新组合,能产生怎样的审美冲突”。这也是它能成为热门创意玩法的主要原因。
1.3 “自己唱出来”具体指什么
“自己唱出来”在技术上有两种理解:
第一种是自己真人跟伴奏演唱。操作流程是:拿到第 20 轮翻译后的中文文本,听着原曲伴奏,一句一句对准节拍唱出来,然后录音。这种方式依赖人的节奏感和音准,不需要额外写太多代码。
第二种是让机器帮你生成一个参考音频。我们可以用 TTS 把最终歌词逐句读出来,再通过时间伸缩让每句话的长度与对应乐句接近。这样即使是“朗读腔”,也能在节拍上与伴奏发生关系,方便你反复练习后自己献声。
本文会把两条路径都覆盖到。代码部分重点实现技术链路,最终成品是一份“对准节奏的参考人声轨”,以及一个把它叠进伴奏的演示音频。真正的“真人演唱”部分,我保留给你来完成。
1.4 项目整体输出物
按本文步骤完成后,你会得到如下文件:
| 输出文件 | 作用 |
|---|---|
output/history.json | 记录每一句、每一轮翻译的完整历史 |
output/final_lines.txt | 第 20 轮翻译后得到的歌词行 |
output/voice/line*.mp3 | 逐句 TTS 生成的朗读音频 |
output/voice/aligned_line*.mp3 | 时间伸缩对齐后的句子音频 |
output/voice/final_lines.mp3 | 拼接后的完整人声参考轨 |
output/demo.mp3 | 伴奏与人声叠加后的演示音频 |
这套文件链路非常适合复盘:你随时可以回到中间某一步,替换歌词、替换语言路线,重新生成不同版本。
2. 技术选型与整体架构
2.1 核心流程
整个实验可以拆成下面几步:
- 准备原始歌词,按唱歌时的换气位置拆成若干行。
- 对每一行歌词,独立执行 20 轮翻译。
- 每轮翻译的语言由“语言路由表”决定,第 20 轮强制回到中文。
- 保存每一步的中间结果,形成完整的翻译历史。
- 把第 20 轮得到的中文歌词写入
final_lines.txt。 - 使用 TTS 对最终歌词逐句合成语音。
- 根据原曲乐句时长,使用时间伸缩对齐每一句。
- 把所有对齐后的句子拼接成一条完整人声轨。
- 将人声轨叠加到伴奏上,导出演示音频。
从架构上看,这是典型的“数据管道 + 外部服务调用 + 音频后处理”项目。翻译部分是一个文本处理管道,语音部分是一个音频处理管道。两者通过文本文件串联,拆解清晰,也便于出问题时定位。
文本流方向可以简单记为:
原始歌词行 -> 多语言翻译循环 20 轮 -> 最终歌词行 -> TTS 生成人声 -> 时间对齐 -> 伴奏混合2.2 翻译接口应该怎么选
这个玩法的核心是“机器翻译循环”,而不是某一个具体的翻译产品。你在 B 站或抖音上看到的标题虽然叫“谷歌翻译 20 次”,但它只是为了说明玩法来源。真正落地时,你完全可以使用任何支持多语言互译的云翻译 API。
本文示例选择百度翻译开放平台,理由有三个:
- 国内环境可以直接访问。
- 注册后可以申请免费额度,适合小体量实验。
- 提供中、英、日、韩、法、西等多种语言,足够完成 20 轮循环。
需要说明的是,各厂商的语言代码并不统一。例如“日语”在百度翻译中通常写作jp,而不是 ISO 标准的ja。项目代码里会做一个内部语言名到平台参数名的映射,这样后面调整路线时更直观,也不容易把代码写死。
翻译接口的密钥属于敏感信息,不要提交到公开的 Git 仓库。建议之后把appid和密钥放到环境变量或者本地配置文件中。
2.3 语音合成与音频处理
语音合成部分选择了edge-tts。它依赖微软 Edge 的在线语音合成能力,可以用很短的代码把文本转成 MP3,音色也比较自然,支持中文、英文、日文等。
为什么要用 TTS 而不是直接找真人唱?
因为大多数读者不会因为看了这篇文章就马上开始录音。先生成一个“机器参考人声”,可以让你在不对着麦克风的情况下,先感受到翻译后的歌词配上原有节奏是什么效果。如果效果不好,你可以调整歌词、调整语言路线,重新跑一遍,成本很低。
音频对齐部分使用librosa和soundfile。librosa提供了时间伸缩功能,可以在不改变音高的前提下,把一段音频拉长或缩短。但要注意,时间伸缩和“修音高”是两回事。时间伸缩只改变发音快慢,不会把人声变成歌唱旋律。真正想要“机器演唱”,需要更复杂的声码器或歌声合成引擎,那属于进阶玩法。
3. 环境准备与项目结构
3.1 运行环境说明
建议使用 Python 3.9 或更高版本。操作系统不限,Windows、macOS、Linux 均可。为了不污染系统 Python,建议先创建一个虚拟环境。
python -m venv venvWindows 下激活虚拟环境:
venv\Scripts\activatemacOS / Linux 下激活虚拟环境:
source venv/bin/activate3.2 安装依赖
创建一个requirements.txt文件,内容如下:
requests==2.31.0 edge-tts==6.1.10 librosa==0.10.1 soundfile==0.12.1 pydub==0.25.1版本号只是参考。不同系统下部分音频库的依赖可能略有差异,如果你安装时遇到版本冲突,可以去掉版本号,让 pip 自动选择当前环境较合适的版本。
执行安装:
pip install -r requirements.txt另外,pydub在混音时需要依赖 FFmpeg。如果你电脑还没安装 FFmpeg,需要提前装好。安装完成后,在终端执行:
ffmpeg -version能正常打印版本信息即可。
3.3 项目目录结构
建议整个项目按照下面的结构组织:
translate-sing-demo/ ├── input/ │ ├── backing.mp3 │ └── lyrics.txt ├── output/ │ ├── history.json │ ├── final_lines.txt │ ├── voice/ │ ├── aligned_voice/ │ └── demo/ ├── requirements.txt ├── translator.py ├── run_loop.py └── make_voice.pyinput/backing.mp3是原曲伴奏。input/lyrics.txt是原始歌词,每行一句。可以先用很短的文本测试,等流程跑通后再换完整的歌曲。
3.4 示例歌词的准备
由于版权原因,本文不直接贴原曲完整歌词。我准备了一段自拟占位文本,格式和中文歌曲主歌比较接近:
晚风轻轻吹过城市的屋顶 人们穿过亮着灯的路口 一只猫在墙头放慢脚步 谁也不知道它来自哪里把这四行保存到input/lyrics.txt。如果你的《人是猫》原文是一首日文歌,也可以改成日文原文,但语言路由需要相应调整。下面先以中文源文本为例。
实际做实验时,建议不要一次性放入太多行。四到六行比较合适,因为每跑一行都需要执行 20 次翻译请求。行数越多,等待时间越长,也越容易触发翻译接口的调用频率限制。
4. 实现多语言翻译循环
4.1 翻译接口封装
在项目根目录新建translator.py,把翻译接口封装成一个独立模块。这样后续主流程只需要调用translate()函数,不需要关心签名怎么计算、请求怎么发送。
# 文件路径:translator.py import hashlib import random import requests # ========== 配置区 ========== # 请在百度翻译开放平台注册并开通“通用文本翻译”服务 APPID = "请替换为你的APPID" SECRET_KEY = "请替换为你的SECRET_KEY" API_URL = "https://fanyi-api.baidu.com/api/trans/vip/translate" # 内部语言名 -> 百度翻译平台语言代码 LANG_MAP = { "zh": "zh", "en": "en", "ja": "jp", "ko": "kor", "fr": "fra", "es": "spa", } # ========================== def to_platform_lang(lang: str) -> str: """把内部语言名转换成具体平台的入参语言代码。""" return LANG_MAP.get(lang, lang) def translate(text: str, target_lang: str, source_lang: str = "zh") -> str: """ 调用百度翻译开放平台的通用文本翻译接口。 :param text: 待翻译文本 :param target_lang: 目标语言内部名,例如 en / ja / ko :param source_lang: 源语言内部名,默认 zh :return: 翻译后的文本 """ if not text.strip(): return text.strip() # 百度翻译要求根据 appid + q + salt + 密钥 拼接后计算 MD5 salt = str(random.randint(32768, 65536)) sign_str = APPID + text + salt + SECRET_KEY sign = hashlib.md5(sign_str.encode("utf-8")).hexdigest() params = { "q": text, "from": to_platform_lang(source_lang), "to": to_platform_lang(target_lang), "appid": APPID, "salt": salt, "sign": sign, } resp = requests.get(API_URL, params=params, timeout=10) data = resp.json() # 接口出错时返回 error_code 和 error_msg if "error_code" in data: raise RuntimeError( f"翻译接口错误:{data.get('error_msg')}({data.get('error_code')})" ) return data["trans_result"][0]["dst"]这里的关键部分是签名算法。每个提供开放能力的翻译平台都有自己的签名规则,百度翻译使用的是 MD5 拼接摘要。拼接顺序是:
appid + 待翻译文本 + salt + 密钥然后对拼出来的字符串做 MD5,得到sign。
请求成功后,返回的 JSON 结构大致如下:
{ "from": "zh", "to": "en", "trans_result": [ { "src": "晚风轻轻吹过城市的屋顶", "dst": "The evening breeze gently blows over the roof of the city" } ] }所以代码里取data["trans_result"][0]["dst"]就是翻译结果。
4.2 翻译语言路由设计
第 1 轮从中文开始,第 20 轮要回到中文,中间 18 轮要在不同语言之间不断切换。语言路由如果设置得太规律,例如“中日中日中日”反复交替,会让文本频繁回到中文,语义失真可能不够明显。
更好的方式是让语言在互不接近的语系之间跳跃。默认路由可以这样设计:
ko -> en -> ja -> fr -> es也就是:
- 中文翻译成韩文
- 韩文翻译成英文
- 英文翻译成日文
- 日文翻译成法文
- 法文翻译成西班牙文
- 西班牙文再翻译成韩文
这样循环到第 19 轮,第 20 轮强制回到中文。
4.3 主循环脚本
接下来写主循环run_loop.py。它负责读取歌词、逐句翻译、保存中间历史。
# 文件路径:run_loop.py import json import time from translator import translate ROUNDS = 20 SOURCE_LANG = "zh" ROUTE = ["ko", "en", "ja", "fr", "es"] HISTORY_PATH = "output/history.json" FINAL_LINES_PATH = "output/final_lines.txt" def run_one_sentence(sentence: str): """对单个句子执行 20 轮翻译,返回最终中文结果和完整历史。""" current = sentence.strip() current_lang = SOURCE_LANG history = [] for step in range(1, ROUNDS + 1): if step == ROUNDS: target_lang = SOURCE_LANG else: target_lang = ROUTE[(step - 1) % len(ROUTE)] print(f"[step {step:02d}] {current_lang} -> {target_lang}") print(f" 输入:{current}") current = translate( current, target_lang=target_lang, source_lang=current_lang ) current_lang = target_lang history.append({ "round": step, "target_lang": target_lang, "text": current, }) return current, history def main(): with open("input/lyrics.txt", "r", encoding="utf-8") as f: sentences = [line.strip() for line in f if line.strip()] all_history = [] final_lines = [] for idx, sentence in enumerate(sentences, start=1): print(f"\n===== 正在处理第 {idx} 句 =====") final_result, history = run_one_sentence(sentence) final_lines.append(final_result) all_history.append({ "original": sentence, "final": final_result, "history": history, }) # 尽量降低请求频率,避免触发接口限流 time.sleep(1.2) os.makedirs("output", exist_ok=True) with open(HISTORY_PATH, "w", encoding="utf-8") as f: json.dump(all_history, f, ensure_ascii=False, indent=2) with open(FINAL_LINES_PATH, "w", encoding="utf-8") as f: f.write("\n".join(final_lines)) print("\n===== 翻译循环完成 =====") for i, line in enumerate(final_lines, start=1): print(f"{i}. {line}") if __name__ == "__main__": import os main()注意脚本开头用到os,但导入语句放在模块顶部更规范。实际代码中需要把import os提升到顶部。上面main()内部写import os也可以运行,不过更好的写法是在文件开头统一导入。
运行命令:
python run_loop.py如果你申请的是标准版翻译服务,免费额度下的调用频率比较低。每翻译一句之间加入time.sleep(1.2),可以显著降低限流概率。实际运行中如果遇到超时,可以把timeout调大,或者增加失败重试逻辑。
由于每个句子都要跑 20 次翻译,四句歌词大约要请求 80 次。即使每次间隔 1.2 秒,加上网络耗时,整体也需要数分钟。这属于正常现象,不必担心。
执行结束后,打开output/final_lines.txt,你会看到类似这样的最终歌词行:
晚间的风轻轻穿过城市的顶部 人们走过一条点燃的街道 一只猫在墙壁上慢慢移动着脚步 没有人知道它来自何处这里不放真实运行结果是因为每次请求、每次网络状况都会带来细微差异。更重要的是,不同线上的翻译模型可能已经更新,生成结果会随时间变化。这也是这个玩法的魅力所在:它每个版本都不可复现,错过就没了。
5. 用 TTS 生成逐句人声
5.1 TTS 脚本
得到最终中文歌词后,可以调用edge-tts为每一句生成语音。
# 文件路径:make_voice.py import asyncio import os import edge_tts VOICE = "zh-CN-YunxiNeural" INPUT_PATH = "output/final_lines.txt" VOICE_DIR = "output/voice" async def synth_line(text: str, output_path: str): communicate = edge_tts.Communicate(text=text, voice=VOICE) await communicate.save(output_path) def main(): os.makedirs(VOICE_DIR, exist_ok=True) with open(INPUT_PATH, "r", encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()] for idx, line in enumerate(lines, start=1): out_path = os.path.join(VOICE_DIR, f"line{idx}.mp3") print(f"正在合成第 {idx} 句:{line}") asyncio.run(synth_line(line, out_path)) print("逐句语音合成完成。") if __name__ == "__main__": main()运行:
python make_voice.pyedge-tts连接的是在线语音合成服务,对网络环境有要求。如果某一句合成失败,可以先重新运行一次,看是否是瞬时网络抖动。如果反复失败,可以尝试更换其他音色。查看支持音色列表:
edge-tts --list-voices输出中会列出大量音色,中文音色常见的有zh-CN-XiaoxiaoNeural、zh-CN-XiaoyiNeural、zh-CN-YunxiNeural、zh-CN-YunjianNeural