文档整理: - 重写 game/操作说明.md 为项目主文档(6游戏/6手势/旁白/模型生成音频/TTS两步流程) - 归档过时文档至 other_docs/(林夏原始设计、旧执行文档) - 新增 docs/音频制作执行手册.md(从零制作游戏音频的完整流程) - docs/ 下保留旁白音色提示词、音频脚本 代码清理: - 删除旧架构死代码: story.js / gestures.js / profile.js - 删除耦合旧架构的过时测试目录 game/tests/ 脚本配置修正: - check_tts_env.sh: 启动脚本路径 AutoVideo → tts-server(实际位置) - gen_narrator_samples.py: 硬编码地址改为环境变量,默认对齐 tts-server 端口 8000
1249 lines
58 KiB
Python
1249 lines
58 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
盲游 · 旁白 TTS 批量生产脚本 — VoxCPM2 版
|
||
|
||
用法:
|
||
# 先确保 VoxCPM2 服务已启动:
|
||
bash audio/check_tts_env.sh
|
||
|
||
# 生产所有游戏旁白
|
||
python audio/batch_narrator_tts.py --source voice_mp3
|
||
|
||
# 只生产指定游戏
|
||
python audio/batch_narrator_tts.py --source voice_mp3 --game shouye
|
||
|
||
# 只生产 P0(narrate/tap/dt/intro)
|
||
python audio/batch_narrator_tts.py --source voice_mp3 --priority p0
|
||
|
||
# 干跑(只看任务列表)
|
||
python audio/batch_narrator_tts.py --source voice_mp3 --dry-run
|
||
|
||
输出目录:
|
||
audio/mp3/narrator/{game_id}/{game_id}_{scene_id}_{type}.mp3
|
||
|
||
参考音色目录(需预先准备):
|
||
voice_mp3/narrator/{game_id}/ 或 voice/narrator/{game_id}/
|
||
"""
|
||
import os
|
||
import sys
|
||
import time
|
||
import argparse
|
||
import subprocess
|
||
import tempfile
|
||
import base64
|
||
import glob
|
||
import shutil
|
||
|
||
import requests
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
# 配置
|
||
# ──────────────────────────────────────────────────────────────
|
||
PROJECT_DIR = "/home/xsl/blind"
|
||
OUT_BASE = f"{PROJECT_DIR}/audio/mp3/narrator"
|
||
|
||
HOST = os.environ.get("VOXCPM_HOST", "127.0.0.1")
|
||
PORT = os.environ.get("VOXCPM_PORT", "8000")
|
||
BASE_URL = f"http://{HOST}:{PORT}"
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
# 旁白角色配置
|
||
# dir: 在 voice_mp3/narrator/ 或 voice/narrator/ 下的子目录
|
||
# speed: 语速倍率(通过 ffmpeg atempo 后处理)
|
||
# priority: 角色级别,用于 --game 过滤
|
||
# ──────────────────────────────────────────────────────────────
|
||
NARRATORS = {
|
||
"linxia": {
|
||
"dir": "linxia",
|
||
"speed": 0.85,
|
||
"note": "纪录片旁白·女声·成熟·平静中带悲悯",
|
||
},
|
||
"yinian": {
|
||
"dir": "yinian",
|
||
"speed": 0.80,
|
||
"note": "内心独白·女声·接近耳语但清晰",
|
||
},
|
||
"shouye": {
|
||
"dir": "shouye",
|
||
"speed": 0.90,
|
||
"note": "对讲机提示·男声·中年感·略带沙哑",
|
||
},
|
||
"houzhen": {
|
||
"dir": "houzhen",
|
||
"speed": 0.95,
|
||
"note": "时间之声·播音腔·完全中性·精确",
|
||
},
|
||
"yisheng": {
|
||
"dir": "yisheng",
|
||
"speed": 0.92,
|
||
"note": "手机系统语音·女声·偏年轻·咬字清晰",
|
||
},
|
||
"fu": {
|
||
"dir": "fu",
|
||
"speed": 0.75,
|
||
"note": "梦里的声音·女声·气声·偏空灵",
|
||
},
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
# 台词表
|
||
# 格式: (输出文件名不含扩展名, 旁白角色key, 台词, 优先级)
|
||
# 优先级: p0=核心 p1=结局 p2=条件变体 p3=动态分片
|
||
# ──────────────────────────────────────────────────────────────
|
||
LINES = [
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 《林夏》 — 纪录片旁白
|
||
# ══════════════════════════════════════════════════════════
|
||
|
||
# 开场
|
||
("linxia/linxia_intro", "linxia",
|
||
"今晚九点。你是林夏,二十四岁,在北京独居。手机屏幕碎了,但还能用。"
|
||
"今晚有十六条消息,你来决定接谁,不接谁。单击接听或回复,双击拒接或忽略。",
|
||
"p0"),
|
||
|
||
# 消息01 · 妈妈来电
|
||
("linxia/linxia_msg01_narrate", "linxia",
|
||
"晚上九点零三分。手机震动,铃声响起。屏幕显示:妈妈。",
|
||
"p0"),
|
||
("linxia/linxia_msg01_tap", "linxia",
|
||
"你接了。妈妈的声音从电话里传来,带着一点日常的关心,问你今天怎么样,吃了没有。"
|
||
"你说还好。挂掉之后,房间里又安静了。",
|
||
"p0"),
|
||
("linxia/linxia_msg01_dt", "linxia",
|
||
"铃声响了又停。妈妈没再打来。",
|
||
"p0"),
|
||
|
||
# 消息02 · 小美语音×3
|
||
("linxia/linxia_msg02_notify", "linxia",
|
||
"九点一刻。微信震了两下。小美给你发来三条语音消息。",
|
||
"p0"),
|
||
("linxia/linxia_msg02_notify_dt", "linxia",
|
||
"你把手机屏幕朝下放着。消息还在那里,但你不想听。",
|
||
"p0"),
|
||
("linxia/linxia_msg02_play_narrate", "linxia",
|
||
"小美说了很多,大意是关于上次那件事,她一直想跟你说清楚,她说她只是太担心你了。"
|
||
"最后她问:你最近还好吗?",
|
||
"p0"),
|
||
("linxia/linxia_msg02_play_tap", "linxia",
|
||
"你回了消息。小美很快回说:谢谢你愿意回我。",
|
||
"p0"),
|
||
("linxia/linxia_msg02_play_dt", "linxia",
|
||
"你听完了,但没有回。消息就这样放着。",
|
||
"p0"),
|
||
|
||
# 消息03 · 外卖来电
|
||
("linxia/linxia_msg03_narrate", "linxia",
|
||
"九点二十分。来电话了,是一个陌生号码——是外卖小哥,说你的外卖到了,问你在哪层。",
|
||
"p0"),
|
||
("linxia/linxia_msg03_tap", "linxia",
|
||
"你接了,告诉他楼层,外卖放门口就好。过了一会儿,门铃响了,外卖到了。",
|
||
"p0"),
|
||
("linxia/linxia_msg03_dt", "linxia",
|
||
"你没接。不知道外卖最后怎么样了。",
|
||
"p0"),
|
||
|
||
# 消息04 · 阿哲语音
|
||
("linxia/linxia_msg04_notify", "linxia",
|
||
"九点半。微信震了一下。阿哲——你的前男友——给你发来一条语音。",
|
||
"p0"),
|
||
("linxia/linxia_msg04_notify_dt", "linxia",
|
||
"你没有打开。阿哲的消息放在那里。",
|
||
"p0"),
|
||
("linxia/linxia_msg04_play_narrate", "linxia",
|
||
"阿哲说,他有一件东西落在你这里了,问你方便不方便他过来拿一下。"
|
||
"语气很平,不像是有别的意思。",
|
||
"p0"),
|
||
("linxia/linxia_msg04_play_tap", "linxia",
|
||
"你回了条消息说可以。阿哲说谢谢,说到时候再联系。你放下手机,有点说不清的感觉。",
|
||
"p0"),
|
||
("linxia/linxia_msg04_play_dt", "linxia",
|
||
"你没有回。消息就这样。",
|
||
"p0"),
|
||
|
||
# 消息05 · HR王姐
|
||
("linxia/linxia_msg05_notify", "linxia",
|
||
"九点四十五分。HR王姐发来语音,是关于下个月调岗的事,问你有没有空聊一聊。",
|
||
"p0"),
|
||
("linxia/linxia_msg05_notify_dt", "linxia",
|
||
"你把消息标记了已读,回头再说。",
|
||
"p0"),
|
||
("linxia/linxia_msg05_play_narrate", "linxia",
|
||
"王姐说有个岗位的变动,你可能会受到影响,想提前跟你说一声,让你有个准备。"
|
||
"语气是友善的,像是帮你。",
|
||
"p0"),
|
||
("linxia/linxia_msg05_play_tap", "linxia",
|
||
"你回了消息说谢谢告知,改天约时间聊。王姐说好的。",
|
||
"p0"),
|
||
("linxia/linxia_msg05_play_dt", "linxia",
|
||
"你没有回。这件事先放一放。",
|
||
"p0"),
|
||
|
||
# 消息06 · 安安邀约
|
||
("linxia/linxia_msg06_notify", "linxia",
|
||
"晚上十点。安安发来语音,她说她们今晚在酒吧,问你要不要过来,说来了就是了,不用提前说。",
|
||
"p0"),
|
||
("linxia/linxia_msg06_notify_dt", "linxia",
|
||
"你没有打开消息。今晚不想动。",
|
||
"p0"),
|
||
("linxia/linxia_msg06_play_narrate", "linxia",
|
||
"安安的声音很热闹,背景里有音乐。她说来嘛来嘛,好久没见了,就几个人,很轻松的那种。",
|
||
"p0"),
|
||
("linxia/linxia_msg06_play_tap", "linxia",
|
||
"你回了条消息说行,等等到。安安说耶!你站起来换了件外套,但后来不知道为什么又坐下了。",
|
||
"p0"),
|
||
("linxia/linxia_msg06_play_dt", "linxia",
|
||
"你回了消息说今晚状态不太好,下次吧。安安说没事,下次一定。",
|
||
"p0"),
|
||
|
||
# 消息07 · 寝室群×5
|
||
("linxia/linxia_msg07_notify", "linxia",
|
||
"晚上十点一刻。寝室群活了,连着震了好几下,是大学室友们在聊。你们已经很久没联系了。",
|
||
"p0"),
|
||
("linxia/linxia_msg07_notify_dt", "linxia",
|
||
"你把群消息静音了。不是今晚。",
|
||
"p0"),
|
||
("linxia/linxia_msg07_play_narrate", "linxia",
|
||
"她们在聊上周聚会的事,有人说想你了,有人问你最近怎么样,有人说快来啊。"
|
||
"那种熟悉的语气,你有点想念。",
|
||
"p0"),
|
||
("linxia/linxia_msg07_play_tap", "linxia",
|
||
"你发了一条消息,说最近还好,想你们了。群里一下子热闹起来,大家说你终于出现了。"
|
||
"你看着那些消息,嘴角动了一下。",
|
||
"p0"),
|
||
("linxia/linxia_msg07_play_dt", "linxia",
|
||
"你看完了,但没有回复。你把手机放下。那种热闹是她们的,你在这边。",
|
||
"p0"),
|
||
|
||
# 消息08 · 未知号码
|
||
("linxia/linxia_msg08_narrate", "linxia",
|
||
"晚上十点半。手机响了,来电显示:未知号码。铃声有点不一样,低沉的那种。",
|
||
"p0"),
|
||
("linxia/linxia_msg08_tap", "linxia",
|
||
"你接了。里面没有声音,只有轻微的呼吸声,然后断了。你不知道是谁,心跳加快了一下。",
|
||
"p0"),
|
||
("linxia/linxia_msg08_dt", "linxia",
|
||
"未知号码,不接。铃声停了。",
|
||
"p0"),
|
||
|
||
# 消息09 · 小美醉酒×4
|
||
("linxia/linxia_msg09_notify", "linxia",
|
||
"十点四十五分。小美又发来消息,连续四条。语音很长,而且发送时间是一条接一条。",
|
||
"p0"),
|
||
("linxia/linxia_msg09_notify_dt", "linxia",
|
||
"消息放那里,你没有打开。",
|
||
"p0"),
|
||
("linxia/linxia_msg09_play_narrate", "linxia",
|
||
"小美喝多了,说话有点不清楚,但每一句都很认真。她说她一直有件事想跟你说:"
|
||
"那年她告诉了别人,是她不对,她知道,她一直很愧疚,她说她怕你恨她。",
|
||
"p0"),
|
||
("linxia/linxia_msg09_play_tap", "linxia",
|
||
"你打了几个字发过去。小美隔了很久才回,说谢谢你,然后说她要先睡了,明天聊。你把手机放下。",
|
||
"p0"),
|
||
("linxia/linxia_msg09_play_dt", "linxia",
|
||
"你没有回。这件事太重了,不是今晚能说清楚的。",
|
||
"p0"),
|
||
|
||
# 消息10 · 妈妈文字
|
||
("linxia/linxia_msg10_narrate", "linxia",
|
||
"深夜十一点刚过。妈妈发来一条文字消息,就五个字:睡了吗,夏夏。",
|
||
"p0"),
|
||
("linxia/linxia_msg10_tap", "linxia",
|
||
"你回了:还没呢,妈你睡吧。妈妈说好,早点睡,然后发了个晚安的表情。"
|
||
"你盯着那个表情看了一会儿。",
|
||
"p0"),
|
||
("linxia/linxia_msg10_dt", "linxia",
|
||
"你没有回妈妈的消息。她那边大概以为你已经睡了。",
|
||
"p0"),
|
||
|
||
# 消息11 · 阿哲关键来电
|
||
("linxia/linxia_msg11_narrate", "linxia",
|
||
"十一点一刻。手机响了,专属铃声——是阿哲打来的。",
|
||
"p0"),
|
||
("linxia/linxia_msg11_tap", "linxia",
|
||
"阿哲说他就在附近,问你方不方便他上来取一下那件东西。他的声音平静,但停顿了一下。",
|
||
"p0"),
|
||
("linxia/linxia_msg11_dt", "linxia",
|
||
"铃声停了。阿哲没再打来。",
|
||
"p0"),
|
||
("linxia/linxia_msg11_choice_narrate", "linxia",
|
||
"他在等你说行不行。",
|
||
"p0"),
|
||
("linxia/linxia_msg11_choice_tap", "linxia",
|
||
"你说好,上来吧。阿哲说谢谢,说马上到。你放下手机,在房间里站了一会儿。",
|
||
"p0"),
|
||
("linxia/linxia_msg11_choice_dt", "linxia",
|
||
"你说今晚不太方便。阿哲沉默了一秒,说行,那之后再说。挂掉了。",
|
||
"p0"),
|
||
|
||
# 消息12 · 周南初次语音
|
||
("linxia/linxia_msg12_notify", "linxia",
|
||
"十一点半。一个名字出现在屏幕上:周南。你有三年没联系了,他突然发来一条语音。",
|
||
"p0"),
|
||
("linxia/linxia_msg12_notify_dt", "linxia",
|
||
"你没有打开周南的消息。三年了,不急这一会儿。",
|
||
"p0"),
|
||
("linxia/linxia_msg12_play_narrate", "linxia",
|
||
"周南说:林夏你好,是我周南,不知道你还记不记得,就是突然想起你,"
|
||
"也不知道你还在不在这个号,就发一条。他的声音还是老样子,只是低沉了一些。",
|
||
"p0"),
|
||
("linxia/linxia_msg12_play_tap", "linxia",
|
||
"你回了:记得,你好啊。消息发出去了,你盯着那两个字,觉得说多说少都不对,就这样。",
|
||
"p0"),
|
||
("linxia/linxia_msg12_play_dt", "linxia",
|
||
"你听完了,没有回。也许三年的空白不是一条消息能填上的。",
|
||
"p0"),
|
||
|
||
# 消息13 · 周南长语音×4
|
||
("linxia/linxia_msg13_notify", "linxia",
|
||
"深夜十一点四十五分。周南又发来四条,比刚才长很多。",
|
||
"p0"),
|
||
("linxia/linxia_msg13_notify_dt", "linxia",
|
||
"你把手机放到一边了。今晚已经够多了。",
|
||
"p0"),
|
||
("linxia/linxia_msg13_play_narrate", "linxia",
|
||
"周南说了他这十年:换了几个城市,做了几份工作,有一段感情,然后结束了。"
|
||
"他说他今晚喝了一点酒,就想到你,想到你们高中的那些事,说他一直不知道当年为什么就断了联系。"
|
||
"然后他问:你在北京过得好吗?",
|
||
"p0"),
|
||
("linxia/linxia_msg13_play_tap", "linxia",
|
||
"你想了很久,然后把这段时间的事告诉了周南。说不上好不好,就是在过。"
|
||
"他说谢谢你告诉我这些。你们聊了很久。",
|
||
"p0"),
|
||
("linxia/linxia_msg13_play_dt", "linxia",
|
||
"你回了说还好,然后问了他现在在哪里。你们就这样聊着,没有太深入,但很平静。",
|
||
"p0"),
|
||
|
||
# 消息14 · 妈妈第二次来电(条件分支)
|
||
("linxia/linxia_msg14_narrate", "linxia",
|
||
"凌晨零点一刻。妈妈又打来电话了,这么晚——她还没睡。",
|
||
"p0"),
|
||
("linxia/linxia_msg14_tap_a", "linxia",
|
||
"你接了。妈妈说:我就是想再听到你的声音。你们聊了一会儿,说了一些很日常的话,但你都记住了。",
|
||
"p2"),
|
||
("linxia/linxia_msg14_tap_b", "linxia",
|
||
"你接了。妈妈说:没事,就是问问你睡了没。你说没呢,妈你睡吧。她说想听你的声音。",
|
||
"p2"),
|
||
("linxia/linxia_msg14_dt", "linxia",
|
||
"太晚了,挂掉了。妈妈没有再打来。",
|
||
"p0"),
|
||
|
||
# 消息15 · 时光胶囊
|
||
("linxia/linxia_msg15_narrate", "linxia",
|
||
"凌晨零点半。手机弹出一个系统通知:三年前的今天,你给三年后的自己录了一条语音,今天到了。",
|
||
"p0"),
|
||
("linxia/linxia_msg15_tap", "linxia",
|
||
"你听到了三年前的自己的声音。她说:如果你在听这条消息,说明你撑过来了。"
|
||
"不管现在怎么样,你撑过来了。然后她停顿了很久,说:辛苦了。",
|
||
"p0"),
|
||
("linxia/linxia_msg15_dt", "linxia",
|
||
"你把那个通知划掉了。也许不是现在。",
|
||
"p0"),
|
||
|
||
# 消息16 · 结局触发点(条件分支)
|
||
("linxia/linxia_msg16_narrate_a", "linxia",
|
||
"凌晨两点。妈妈发来一段很长的语音,她说她一直有些话想说给你听。",
|
||
"p2"),
|
||
("linxia/linxia_msg16_narrate_b", "linxia",
|
||
"凌晨两点。房间很安静,窗外偶尔有车声。今晚就这样了。",
|
||
"p2"),
|
||
("linxia/linxia_msg16_tap_a", "linxia",
|
||
"妈妈说了很多——关于你小时候,关于她一直担心的事,关于她说不出口但一直想说的话。"
|
||
"你把手机贴在耳边,听完了。",
|
||
"p2"),
|
||
("linxia/linxia_msg16_tap_b", "linxia",
|
||
"你就这样坐着,今晚的声音慢慢变少了。",
|
||
"p2"),
|
||
("linxia/linxia_msg16_dt", "linxia",
|
||
"你把手机放到床头,闭上眼睛。今晚就这样了。",
|
||
"p0"),
|
||
|
||
# 结局
|
||
("linxia/linxia_ending_return", "linxia",
|
||
"《回归》。阿哲来了,或者你们说好了见面。不管那个门开不开,今晚你做了一个决定。",
|
||
"p1"),
|
||
("linxia/linxia_ending_link", "linxia",
|
||
"《连接》。今晚你和妈妈说了话,不止一次。那种联系很细,但是在。也许明天你会想打电话回去。",
|
||
"p1"),
|
||
("linxia/linxia_ending_past", "linxia",
|
||
"《旧路》。周南在深夜找到了你,你们聊了很久。那条从高中就断掉的线,今晚接上了一点点。",
|
||
"p1"),
|
||
("linxia/linxia_ending_self", "linxia",
|
||
"《自省》。今晚你听了三年前自己的声音。她说辛苦了,你接受了这句话。这不算小事。",
|
||
"p1"),
|
||
("linxia/linxia_ending_island", "linxia",
|
||
"《孤岛》。今晚大部分的消息,你都没有回应。你一个人度过了这个夜晚。这也是一种选择,你心里清楚。",
|
||
"p1"),
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 《一念》 — 内心独白
|
||
# ══════════════════════════════════════════════════════════
|
||
|
||
# 开场
|
||
("yinian/yinian_intro", "yinian",
|
||
"你坐在黑暗里,独自一人。今晚你必须做一个决定——你自己知道是什么。"
|
||
"七个声音在等着和你说话。单击,让它说完。双击,让它走。",
|
||
"p0"),
|
||
|
||
# 7个声音
|
||
("yinian/yinian_v01_narrate", "yinian",
|
||
"第一个声音来了。是三年前的你。那时候你刚做了一个选择,你觉得那是对的。"
|
||
"那时候的你说:我们当时想要的,你还记得吗?",
|
||
"p0"),
|
||
("yinian/yinian_v01_tap", "yinian",
|
||
"你让三年前的自己说完了。她说,你当时很想要那件事。你知道那是真的。",
|
||
"p0"),
|
||
("yinian/yinian_v01_dt", "yinian",
|
||
"三年前的声音淡去了。你选择了不看它。",
|
||
"p0"),
|
||
|
||
("yinian/yinian_v02_narrate", "yinian",
|
||
"第二个声音。是一个你很久没联系的朋友,她留了条语音。"
|
||
"她说:你最近怎么样,我有点担心你。很久了,你一直没回。",
|
||
"p0"),
|
||
("yinian/yinian_v02_tap", "yinian",
|
||
"你把这份担心收进来了。她还在关心你,这件事是真实的。",
|
||
"p0"),
|
||
("yinian/yinian_v02_dt", "yinian",
|
||
"留言消失了。你没有听完。",
|
||
"p0"),
|
||
|
||
("yinian/yinian_v03_narrate", "yinian",
|
||
"第三个声音。是关于钱的恐惧。它说:你知道你现在能撑多久吗?"
|
||
"如果这件事不成,你怎么办?它问得很具体,很冷静。",
|
||
"p0"),
|
||
("yinian/yinian_v03_tap", "yinian",
|
||
"你承认了:这份恐惧是真实的。你带着它,而不是假装它不存在。",
|
||
"p0"),
|
||
("yinian/yinian_v03_dt", "yinian",
|
||
"恐惧的声音淡下去了。你知道它还在,但今晚先不看。",
|
||
"p0"),
|
||
|
||
("yinian/yinian_v04_narrate", "yinian",
|
||
"第四个声音。是一种渴望——想要被人理解,想要有人在身边,想要不那么孤独。它来得轻,但是很深。",
|
||
"p0"),
|
||
("yinian/yinian_v04_tap", "yinian",
|
||
"你承认了这份渴望。它是真实的,不是软弱。",
|
||
"p0"),
|
||
("yinian/yinian_v04_dt", "yinian",
|
||
"渴望漂走了。你告诉自己,一个人也没什么。",
|
||
"p0"),
|
||
|
||
("yinian/yinian_v05_narrate", "yinian",
|
||
"第五个声音。是一个你一直没有说出口的道歉。你知道是对谁的,你知道说的是什么。"
|
||
"这个声音只是在等你。",
|
||
"p0"),
|
||
("yinian/yinian_v05_tap", "yinian",
|
||
"你在黑暗里,说了那句对不起。没有人听见,但你说了。有什么东西松开了。",
|
||
"p0"),
|
||
("yinian/yinian_v05_dt", "yinian",
|
||
"道歉的声音还在那里。你没有说。它等着。",
|
||
"p0"),
|
||
|
||
("yinian/yinian_v06_narrate", "yinian",
|
||
"第六个声音。是今晚那个决定本身。它把两种可能都摆在你面前,不说话,只是等着你看清楚。",
|
||
"p0"),
|
||
("yinian/yinian_v06_tap", "yinian",
|
||
"你看了。两条路都有代价,你知道了。这不让选择变得更容易,但让它变得更真实。",
|
||
"p0"),
|
||
("yinian/yinian_v06_dt", "yinian",
|
||
"你把眼睛闭上了。还没准备好看那两条路。",
|
||
"p0"),
|
||
|
||
# 第七声音(条件分支)
|
||
("yinian/yinian_v07_narrate_a", "yinian",
|
||
"第七个声音,是现在的你。她说:你今晚接住了很多,你没有回避它们。这需要力气。",
|
||
"p2"),
|
||
("yinian/yinian_v07_narrate_b", "yinian",
|
||
"第七个声音,是现在的你。她沉默了很久,然后说:你把它们都推走了。"
|
||
"也许你需要的不是答案,是喘息。",
|
||
"p2"),
|
||
("yinian/yinian_v07_narrate_c", "yinian",
|
||
"第七个声音,是现在的你。她说:你把恐惧留下了,但把渴望推走了。你知道这意味着什么吗?",
|
||
"p2"),
|
||
("yinian/yinian_v07_narrate_d", "yinian",
|
||
"第七个声音,是现在的你。她说:你接住了一些,放走了一些。这就是你今晚的状态。",
|
||
"p0"),
|
||
("yinian/yinian_v07_tap", "yinian",
|
||
"你听完了现在的自己说话。她说:不管你今晚做什么决定,你都已经在这里待过了。这算数。",
|
||
"p0"),
|
||
("yinian/yinian_v07_dt", "yinian",
|
||
"你没有听完。你觉得你已经知道了你需要知道的。",
|
||
"p0"),
|
||
|
||
# 结局
|
||
("yinian/yinian_ending_accept", "yinian",
|
||
"《接纳》。你今晚接住了很多,你没有选择只是轻装。也许你的决定,就是带着这些往前走。",
|
||
"p1"),
|
||
("yinian/yinian_ending_release", "yinian",
|
||
"《出发》。你今晚把大部分都放走了。你想要的也许是轻。轻装,也是一种勇气。",
|
||
"p1"),
|
||
("yinian/yinian_ending_stuck", "yinian",
|
||
"《困住》。你接受了恐惧,但推走了渴望。你知道风险,但忘了你想要的是什么。"
|
||
"这个决定,也许需要再等一等。",
|
||
"p1"),
|
||
("yinian/yinian_ending_reconcile", "yinian",
|
||
"《和解》。你说出了那声道歉。不管今晚的决定是什么,你和自己和解了一点点。这很重要。",
|
||
"p1"),
|
||
("yinian/yinian_ending_between", "yinian",
|
||
"《悬在中间》。你接住了一些,放走了一些。你还悬在两个答案之间。"
|
||
"也许这就是你今晚真实的状态。",
|
||
"p1"),
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 《守夜》 — 对讲机提示
|
||
# ══════════════════════════════════════════════════════════
|
||
|
||
# 开场
|
||
("shouye/shouye_intro", "shouye",
|
||
"凌晨零点。你是这栋楼的夜班保安,独自坐在一楼监控室。"
|
||
"你不能离开座位,但你有一部对讲机和一部电话。今晚五点交班。"
|
||
"单击,去查那个声音。双击,按兵不动。",
|
||
"p0"),
|
||
|
||
# 8个事件
|
||
("shouye/shouye_s01_narrate", "shouye",
|
||
"凌晨零点十分。地下室传来水声,不大,但是持续的,一直在响。",
|
||
"p0"),
|
||
("shouye/shouye_s01_tap", "shouye",
|
||
"你拨了地下室的内线。没人接。你下去看了一眼,是一根管子在漏水,用旧布堵住了。"
|
||
"回到监控室,零点二十三分。",
|
||
"p0"),
|
||
("shouye/shouye_s01_dt", "shouye",
|
||
"你听了一会儿。应该不碍事。你把注意力放回到监控屏幕上。",
|
||
"p0"),
|
||
|
||
("shouye/shouye_s02_narrate", "shouye",
|
||
"零点四十七分。四楼传来争吵声,一男一女,断断续续,能听出来语气很激动,但听不清说什么。",
|
||
"p0"),
|
||
("shouye/shouye_s02_tap", "shouye",
|
||
"你拨了四楼的号码。男的接了,说没事,就是聊天,让你别担心。"
|
||
"声音还是有点紧,但他们关掉了。你记下来了:四楼,零点五十二分,双方均在场。",
|
||
"p0"),
|
||
("shouye/shouye_s02_dt", "shouye",
|
||
"家务事,你管不着。你转过身去继续盯着屏幕。争吵后来停了,你不知道是什么时候。",
|
||
"p0"),
|
||
|
||
("shouye/shouye_s03_narrate", "shouye",
|
||
"凌晨一点十七分。三楼有脚步声,很轻,走走停停。"
|
||
"监控记录:三楼只有七零二亮着灯,住户是老陈,八十岁,独居。",
|
||
"p0"),
|
||
("shouye/shouye_s03_tap", "shouye",
|
||
"电话通了,老陈说他在找眼镜,眼镜掉床底下了。你说没事我就是例行确认一下。"
|
||
"他说谢谢你小伙子,然后你们聊了几句,关于他年轻时候也做过工程。你挂掉电话,一点二十八分。",
|
||
"p0"),
|
||
("shouye/shouye_s03_dt", "shouye",
|
||
"老人家睡不好,很正常。你记了一笔,继续监控。脚步声后来停了。",
|
||
"p0"),
|
||
|
||
("shouye/shouye_s04_narrate", "shouye",
|
||
"凌晨两点零三分。大门门铃响了,不是访客系统,是有人在外面按了手动铃,"
|
||
"按了两下,然后停了。没有人应答。监控画面:门口没有人了。",
|
||
"p0"),
|
||
("shouye/shouye_s04_tap", "shouye",
|
||
"你走到门口,什么都没有,只有一个快递包裹靠在墙边,大概是外卖或者什么。"
|
||
"也许是送错了。你把包裹推到角落,拍了张照,两点一刻回到座位。",
|
||
"p0"),
|
||
("shouye/shouye_s04_dt", "shouye",
|
||
"无应答,无入侵,记录在案,按流程忽略。你回到屏幕前。",
|
||
"p0"),
|
||
|
||
# s05条件分支
|
||
("shouye/shouye_s05_narrate_a", "shouye",
|
||
"凌晨两点五十五分。你不知道从什么时候开始,眼皮变得很重……你睡着了一会儿。"
|
||
"醒来的时候,你看到监控里,四楼门口放着一个箱子,不知道是谁的,不知道什么时候放的。",
|
||
"p2"),
|
||
("shouye/shouye_s05_narrate_b", "shouye",
|
||
"凌晨两点五十五分。你不知道从什么时候开始,眼皮变得很重……你睡着了一会儿。"
|
||
"醒来的时候,监控里一切正常。你记录:三点十二分,短暂失守。",
|
||
"p2"),
|
||
|
||
("shouye/shouye_s06_narrate", "shouye",
|
||
"凌晨三点半。你的私人手机响了。是家里的人打来的。这么晚打来——可能是什么事,也可能只是弄错时间了。",
|
||
"p0"),
|
||
("shouye/shouye_s06_tap", "shouye",
|
||
"是你妈,她说睡不着,看见你今天发的朋友圈,就想打个电话。"
|
||
"你们聊了十分钟,说了些不重要的事。挂掉之后,监控室比之前暖了一点点。",
|
||
"p0"),
|
||
("shouye/shouye_s06_dt", "shouye",
|
||
"这么晚不方便。你把手机调成静音,准备待会儿发条消息。你没有发。",
|
||
"p0"),
|
||
|
||
("shouye/shouye_s07_narrate", "shouye",
|
||
"凌晨四点十二分。屋顶传来动静,断断续续,不像是机械设备,更像是有什么东西在走动。",
|
||
"p0"),
|
||
("shouye/shouye_s07_tap", "shouye",
|
||
"你拿着手电上了顶层,推开屋顶的铁门,风吹过来,你看见了天边刚刚开始发白的颜色——"
|
||
"还不是黎明,但是快了。然后你看见了一只流浪猫,坐在水箱上看着你。"
|
||
"你们看了对方一会儿,然后你下来了。你是今晚唯一上了屋顶的人。",
|
||
"p0"),
|
||
("shouye/shouye_s07_dt", "shouye",
|
||
"你记了一笔,继续监控。动静后来停了。",
|
||
"p0"),
|
||
|
||
("shouye/shouye_s08_narrate", "shouye",
|
||
"五点整。接班的人进来了,你们交接了几句,递了记录本,你背上包走出去。外面已经有一点点亮了。",
|
||
"p0"),
|
||
|
||
# 结局
|
||
("shouye/shouye_ending_dawn", "shouye",
|
||
"《你看到了什么》。今晚你一个人上了屋顶,你看到了那只猫,你看到了天边开始变色。"
|
||
"只有你看到了。这算不上什么,但只有你知道。",
|
||
"p1"),
|
||
("shouye/shouye_ending_solid", "shouye",
|
||
"《平凡守夜》。你今晚查了应该查的,接了应该接的电话。没有大事,没有遗漏。"
|
||
"这栋楼平安了一夜,有一部分是因为你在。",
|
||
"p1"),
|
||
("shouye/shouye_ending_detached", "shouye",
|
||
"《这不是我的事》。你今晚保持了距离。声音来了,你记录,你不动。"
|
||
"你守住了一夜,但不知道楼里发生了什么。这也是一种守夜的方式。",
|
||
"p1"),
|
||
("shouye/shouye_ending_oversight", "shouye",
|
||
"《疏忽》。你今晚漏掉了四楼的事。那个女人在凌晨两点多拎着箱子离开了,你没有注意到。"
|
||
"也许和你没有关系。也许有。你不知道。",
|
||
"p1"),
|
||
("shouye/shouye_ending_ordinary", "shouye",
|
||
"《普通的夜班》。你做了你能做的,漏掉了一些,照顾到了一些。和所有普通的夜班一样。",
|
||
"p1"),
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 《候诊》 — 时间之声
|
||
# ══════════════════════════════════════════════════════════
|
||
|
||
# 开场
|
||
("houzhen/houzhen_intro", "houzhen",
|
||
"下午一点半,你坐在医院候诊室,等一个检查结果。叫号机已经叫到了八十四号。"
|
||
"你是九十四号。单击,把注意力放到外面。双击,退回到自己里面。",
|
||
"p0"),
|
||
|
||
# 9个时间切片
|
||
("houzhen/houzhen_h01_narrate", "houzhen",
|
||
"叫号机叫到了八十五号。你的号是九十四。等待的时间被切成了一小块一小块的,每一块都很结实。",
|
||
"p0"),
|
||
|
||
("houzhen/houzhen_h02_narrate", "houzhen",
|
||
"你左边坐着一个老人。他在看窗外,窗外是停车场,没有什么特别的。但他看得很专注。",
|
||
"p0"),
|
||
("houzhen/houzhen_h02_tap", "houzhen",
|
||
"你看了他一会儿。他感觉到了,朝你点了点头。你也点了点头。没有说话,但有什么东西发生了。",
|
||
"p0"),
|
||
("houzhen/houzhen_h02_dt", "houzhen",
|
||
"你低下头,看手机。屏幕亮着,什么都有,但你没有真的看进去什么。",
|
||
"p0"),
|
||
|
||
("houzhen/houzhen_h03_wait", "houzhen",
|
||
"叫号机叫到了八十八号。",
|
||
"p0"),
|
||
("houzhen/houzhen_h03_narrate", "houzhen",
|
||
"你手机上有一条两天前的消息,你当时看到了,但没有回。那个人说:最近怎么样。",
|
||
"p0"),
|
||
("houzhen/houzhen_h03_tap", "houzhen",
|
||
"你打开来,打了几个字。发出去了。不知道他会不会在意这么晚才回。",
|
||
"p0"),
|
||
("houzhen/houzhen_h03_dt", "houzhen",
|
||
"你把手机翻过去,屏幕朝下放在大腿上。等等再说。",
|
||
"p0"),
|
||
|
||
("houzhen/houzhen_h04_wait", "houzhen",
|
||
"护士从走廊那头走过来,叫了一个名字,不是你。",
|
||
"p0"),
|
||
("houzhen/houzhen_h04_narrate", "houzhen",
|
||
"对面有个小孩在哭,大约三四岁,蹲在椅子旁边哭。旁边的大人在轻声说什么,但他还是哭。",
|
||
"p0"),
|
||
("houzhen/houzhen_h04_tap", "houzhen",
|
||
"你看着那个孩子。他哭了一会儿就停了,大概是哭够了。你们没有交流,但你陪着他哭完了。",
|
||
"p0"),
|
||
("houzhen/houzhen_h04_dt", "houzhen",
|
||
"你把视线移开了。每个人都有自己的事,你也有你的。",
|
||
"p0"),
|
||
|
||
("houzhen/houzhen_h05_wait", "houzhen",
|
||
"叫号机叫到了九十号。还有四个人。",
|
||
"p0"),
|
||
("houzhen/houzhen_h05_narrate", "houzhen",
|
||
"窗外的光线变了。下午的阳光斜进来,在地板上画了一块长方形的亮。你注意到了。",
|
||
"p0"),
|
||
("houzhen/houzhen_h05_tap", "houzhen",
|
||
"你盯着那块光看了一会儿。它在慢慢移动,非常慢,但是在动。时间在过。",
|
||
"p0"),
|
||
("houzhen/houzhen_h05_dt", "houzhen",
|
||
"你闭上眼睛,想了想别的事。",
|
||
"p0"),
|
||
|
||
("houzhen/houzhen_h06_wait", "houzhen",
|
||
"叫号机叫到了九十一号。",
|
||
"p0"),
|
||
("houzhen/houzhen_h06_narrate", "houzhen",
|
||
"你的手机响了。是一个你不常打电话的人——是你家里的人。也许只是普通的问候。",
|
||
"p0"),
|
||
("houzhen/houzhen_h06_tap", "houzhen",
|
||
"你接了。对方说:你在哪啊,吃了吗。你说在外面,没事。就这样。挂掉之后候诊室里还是那么安静。",
|
||
"p0"),
|
||
("houzhen/houzhen_h06_dt", "houzhen",
|
||
"你看着屏幕亮了又暗。现在不是时候。等会儿再说。",
|
||
"p0"),
|
||
|
||
("houzhen/houzhen_h07_wait", "houzhen",
|
||
"叫号机叫到了九十二号。",
|
||
"p0"),
|
||
("houzhen/houzhen_h07_narrate", "houzhen",
|
||
"你右边的人一直在看同一页的杂志,翻来翻去都是那页。你不确定她在看,还是只是拿着。",
|
||
"p0"),
|
||
("houzhen/houzhen_h07_tap", "houzhen",
|
||
"你朝她看了一眼。你们的眼神碰了一下,然后都移开了。没有什么,就是两个陌生人在等待。",
|
||
"p0"),
|
||
("houzhen/houzhen_h07_dt", "houzhen",
|
||
"你没有看。你在想等会儿检查结果会说什么。",
|
||
"p0"),
|
||
|
||
("houzhen/houzhen_h08_narrate", "houzhen",
|
||
"九十三号。就差一个了。",
|
||
"p0"),
|
||
|
||
("houzhen/houzhen_h09_narrate", "houzhen",
|
||
"九十四号。是你。你站起来,整理了一下衣服,走向那扇门。",
|
||
"p0"),
|
||
("houzhen/houzhen_h09_tap", "houzhen",
|
||
"你推开门走进去了。门关上了。等待结束了。",
|
||
"p0"),
|
||
("houzhen/houzhen_h09_dt", "houzhen",
|
||
"你停了一秒,深呼吸,然后走进去了。门关上了。",
|
||
"p0"),
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 《遗声》 — 手机系统语音
|
||
# ══════════════════════════════════════════════════════════
|
||
|
||
# 开场
|
||
("yisheng/yisheng_intro", "yisheng",
|
||
"你收到了一部手机。手机的主人去世了,一个月前。他留下了很多录音,按时间排列。"
|
||
"你不认识他。或者你以为你不认识他。单击,听这条录音。双击,跳过,永远不知道这条说了什么。",
|
||
"p0"),
|
||
|
||
# 15条录音的元数据播报
|
||
("yisheng/yisheng_r01_narrate", "yisheng",
|
||
"第一条录音。录制时间:三月五日,早上八点。时长:二十二秒。文件名:买橙子。",
|
||
"p0"),
|
||
("yisheng/yisheng_r02_narrate", "yisheng",
|
||
"第二条录音。三月十二日,下午。时长:三十八秒。文件名:发工资了。",
|
||
"p0"),
|
||
("yisheng/yisheng_r03_narrate", "yisheng",
|
||
"第三条录音。三月十八日,晚上。时长:一分零七秒。文件名:说说工作的事。",
|
||
"p0"),
|
||
("yisheng/yisheng_r04_narrate", "yisheng",
|
||
"第四条录音。四月二日。时长:四十九秒。文件名:吵了一架。",
|
||
"p0"),
|
||
("yisheng/yisheng_r05_narrate", "yisheng",
|
||
"第五条录音。四月十五日,深夜。时长:二十六秒。文件名:电影。",
|
||
"p0"),
|
||
("yisheng/yisheng_r06_narrate", "yisheng",
|
||
"第六条录音。五月三日。时长:一分二十秒。文件名:小林的事。",
|
||
"p0"),
|
||
("yisheng/yisheng_r07_narrate", "yisheng",
|
||
"第七条录音。五月二十日。时长:三十三秒。文件名:小林说的。",
|
||
"p0"),
|
||
("yisheng/yisheng_r08_narrate", "yisheng",
|
||
"第八条录音。六月八日,凌晨。时长:两分十四秒。文件名:结束了。",
|
||
"p0"),
|
||
("yisheng/yisheng_r09_narrate", "yisheng",
|
||
"第九条录音。六月二十日。时长:一分五十秒。文件名:在反思。",
|
||
"p0"),
|
||
("yisheng/yisheng_r10_narrate", "yisheng",
|
||
"第十条录音。七月四日。时长:四十二秒。文件名:有人问起你。",
|
||
"p0"),
|
||
("yisheng/yisheng_r11_narrate", "yisheng",
|
||
"第十一条录音。七月十一日。时长:一分零八秒。文件名:我查了一下。",
|
||
"p0"),
|
||
("yisheng/yisheng_r12_narrate", "yisheng",
|
||
"第十二条录音。八月二日。时长:五十七秒。文件名:给不认识我的人(草稿)。",
|
||
"p0"),
|
||
("yisheng/yisheng_r13_narrate", "yisheng",
|
||
"第十三条录音。九月一日。时长:两分三十八秒。文件名:我有一件事想说。",
|
||
"p0"),
|
||
("yisheng/yisheng_r14_narrate", "yisheng",
|
||
"第十四条录音。十月十五日,录制时间是他去世前三天。时长:三分二十秒。文件名:最后一条,我想讲完整。",
|
||
"p0"),
|
||
("yisheng/yisheng_r15_narrate", "yisheng",
|
||
"还有一条录音,没有标日期。文件名:给不认识我的人(正式版)。这条需要你听了很多条才能解锁。你解锁了。",
|
||
"p0"),
|
||
|
||
# 听完后的旁白(转述录音内容)
|
||
("yisheng/yisheng_r01_tap", "yisheng",
|
||
"他说:今天买了一袋橙子,是脐橙,比上次的甜很多。建议大家有机会可以试试。就这些。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r02_tap", "yisheng",
|
||
"他说:今天发工资了。然后列了他这个月的支出清单,很详细——"
|
||
"房租、电费、手机月供、还有给他妈买了一件外套。他说最后剩了不多,但够。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r03_tap", "yisheng",
|
||
"他说他们组里来了个新同事,总是在会议上抢着发言,说了很多他本来准备说的话。"
|
||
"他说他当时没说话,他不确定那是不是对的选择。"
|
||
"他在录音里问自己:我是懦弱了,还是留有余地?他没有答案。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r04_tap", "yisheng",
|
||
"他说他今天和那个新同事正面冲突了,是在走廊,两个人都没控制住。"
|
||
"他说他说了一句话,事后觉得说得太重了。"
|
||
"他在录音里把那句话又说了一遍,想听听是不是真的太重。"
|
||
"录音里他沉默了一会儿,然后说:是的,是太重了。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r05_tap", "yisheng",
|
||
"他说他去看了一部很老的电影,黑白的,在一个小影院,观众只有六个人。"
|
||
"他说他中途哭了,但他不确定是因为电影,还是因为电影院里那种空荡荡的感觉。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r06_tap", "yisheng",
|
||
"他开始讲一个叫小林的朋友。他说小林是他认识最久的人,从小一起长大的,"
|
||
"后来各自去了不同的城市,联系越来越少。"
|
||
"他说他最近一直在想,那种联系的稀薄,是谁的责任?还是说这就是长大的代价?录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r07_tap", "yisheng",
|
||
"他说小林打电话来了。小林在电话里说了一句话,让他很难受。"
|
||
"小林说:感觉你现在不太一样了,变得……不知道,挺难接近的。"
|
||
"他说他当时说了没什么,你想多了。"
|
||
"他说他那时候应该说:我只是最近状态不好。但他没说。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r08_tap", "yisheng",
|
||
"他说一段感情结束了,他没有细说是谁,说了有多久,只说:结束了,是我提的,"
|
||
"我觉得这是对的,但今晚我睡不着。"
|
||
"他说他在厨房坐着,泡了杯茶,但茶凉了也没喝。"
|
||
"他说:我只是想把今晚的感觉记下来,不想让它消失。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r09_tap", "yisheng",
|
||
"他说他一直在想那段感情里他做错了什么。他列了很多条,每一条都很具体。"
|
||
"然后他说:但是我说不清,这些错是因为我是这样的人,"
|
||
"还是因为那时候我在经历别的事情,所以才这样。"
|
||
"他说:这个问题很重要,但我没有答案。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r10_tap", "yisheng",
|
||
"他说:今天有人跟我提到了你的名字。我以为我早就忘了,但我发现我还记得很多细节。"
|
||
"他没有解释「你」是谁。"
|
||
"他说:我觉得我应该把这件事录下来,因为我发现我不想忘记。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r11_tap", "yisheng",
|
||
"他说:我查了你现在在哪里,在做什么。不是恶意,就是想知道。"
|
||
"他说他找到了一些信息,知道了你现在的大致状态。"
|
||
"他说:你好像过得还不错,我很高兴,也有一点……他停了,然后说:没什么。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r12_tap", "yisheng",
|
||
"他说:如果你听到这条,你肯定不认识我。我在想,把这些录音留给一个陌生人,有什么意义。"
|
||
"然后他说:也许是因为我希望有人知道我活过,而不是认识我的人,"
|
||
"而是真正陌生的人——你不需要对我有感情,你只是听了。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r13_tap", "yisheng",
|
||
"他深吸一口气,说:有一件事我从没对任何人说过。"
|
||
"他停了很久,然后说:我有时候觉得活着的意义我不太找得到,"
|
||
"不是说我要做什么,只是我不知道我在这里有什么用。"
|
||
"他说:我知道这句话很沉,但我必须把它说出来,因为放在心里太久了。"
|
||
"然后他说:我今天状态还可以,就是今天有这个力气把它说出来。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r14_tap", "yisheng",
|
||
"他说:我知道这是最后一条了。我不是悲观,我只是知道。"
|
||
"他说:我想对听这些录音的人说,我不后悔我活过的大多数时候。"
|
||
"橙子是甜的,老电影是值得哭的,小林的声音我还记得,"
|
||
"那个人,不管怎样,我不后悔爱过。"
|
||
"他说:如果你听了很多条,谢谢你。你是陌生人,但你陪着我说完了。录音结束。",
|
||
"p0"),
|
||
("yisheng/yisheng_r15_tap", "yisheng",
|
||
"他说:你好。你不认识我,我也不认识你。"
|
||
"但你坐在那里,听完了我这些年说的话。"
|
||
"我不知道你是谁,我不知道你们那边现在是白天还是夜晚。"
|
||
"我只是想说:谢谢你愿意听一个陌生人说话。这很重要,比你知道的还要重要。"
|
||
"录音结束。这是最后一条了。",
|
||
"p0"),
|
||
|
||
# 跳过时的旁白
|
||
("yisheng/yisheng_skip_generic", "yisheng",
|
||
"你跳过了这条。你永远不会知道这条说了什么了。",
|
||
"p0"),
|
||
("yisheng/yisheng_skip_short", "yisheng",
|
||
"你跳过了。",
|
||
"p0"),
|
||
("yisheng/yisheng_skip_last", "yisheng",
|
||
"你跳过了第十四条,也就是最后一条。手机里没有更多录音了。",
|
||
"p0"),
|
||
("yisheng/yisheng_skip_r15", "yisheng",
|
||
"你连最后这条也跳过了。",
|
||
"p0"),
|
||
|
||
# 结局
|
||
("yisheng/yisheng_ending_known", "yisheng",
|
||
"《我认识你》。你听完了他大部分的录音,你听到了那条隐藏的最后一条。"
|
||
"你不认识他,但你陪他把该说的都说完了。这已经足够了。",
|
||
"p1"),
|
||
("yisheng/yisheng_ending_stranger", "yisheng",
|
||
"《陌生人》。你跳过了所有关于他和你之间联系的录音。"
|
||
"他对你来说,还是一个完全的陌生人。也许这样也好。",
|
||
"p1"),
|
||
("yisheng/yisheng_ending_passing", "yisheng",
|
||
"《路过》。你路过了他的大部分录音,只停留了几条。你知道他存在过,但你不知道他是谁。",
|
||
"p1"),
|
||
("yisheng/yisheng_ending_outline", "yisheng",
|
||
"《轮廓》。你听了他生活的碎片,知道了一个大致的形状。"
|
||
"你不知道他所有的事,但你知道了一个人活着的样子。",
|
||
"p1"),
|
||
("yisheng/yisheng_ending_brief", "yisheng",
|
||
"《简短的相遇》。你听了几条,然后停下了。"
|
||
"也许你听到了你需要听的,也许你还没准备好听更多。这也是一种结果。",
|
||
"p1"),
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 《浮》 — 梦里的声音
|
||
# ══════════════════════════════════════════════════════════
|
||
|
||
# 开场
|
||
("fu/fu_intro", "fu",
|
||
"你漂浮在某处。不知道是水,还是梦,还是别的什么。声音会漂过来。单击,把手伸过去。双击,看着它走。",
|
||
"p0"),
|
||
|
||
# 12个漂流物
|
||
("fu/fu_f01_narrate", "fu",
|
||
"第一个。像夏天傍晚的风,带着草的气味,你说不清是哪年夏天的。它停在你身边了。",
|
||
"p0"),
|
||
("fu/fu_f01_tap", "fu",
|
||
"它停在你手心里,有点暖。",
|
||
"p0"),
|
||
("fu/fu_f01_dt", "fu",
|
||
"你看着它慢慢飘走了。",
|
||
"p0"),
|
||
|
||
("fu/fu_f02_narrate", "fu",
|
||
"第二个。某人的笑声。你认识这个笑,但你说不清是谁的。",
|
||
"p0"),
|
||
("fu/fu_f02_tap", "fu",
|
||
"笑声停在你耳边,还在回响。",
|
||
"p0"),
|
||
("fu/fu_f02_dt", "fu",
|
||
"笑声远了。",
|
||
"p0"),
|
||
|
||
("fu/fu_f03_narrate", "fu",
|
||
"第三个。一种说不清楚的遗憾。它没有形状,但是很重。",
|
||
"p0"),
|
||
("fu/fu_f03_tap", "fu",
|
||
"你把遗憾抱在怀里了。它很重,但你没有放开。",
|
||
"p0"),
|
||
("fu/fu_f03_dt", "fu",
|
||
"遗憾漂过去了。你呼了口气。",
|
||
"p0"),
|
||
|
||
("fu/fu_f04_narrate", "fu",
|
||
"第四个。很久没有过的安全感。轻轻的,像一床旧被子的气味。",
|
||
"p0"),
|
||
("fu/fu_f04_tap", "fu",
|
||
"你把它裹在身上了。",
|
||
"p0"),
|
||
("fu/fu_f04_dt", "fu",
|
||
"安全感飘走了。你看着它走。",
|
||
"p0"),
|
||
|
||
("fu/fu_f05_narrate", "fu",
|
||
"第五个。想哭但没哭出来的那种感觉。像喉咙里卡着什么。",
|
||
"p0"),
|
||
("fu/fu_f05_tap", "fu",
|
||
"你承认了。那个东西松动了一点点。",
|
||
"p0"),
|
||
("fu/fu_f05_dt", "fu",
|
||
"它漂走了。喉咙还是有点紧。",
|
||
"p0"),
|
||
|
||
("fu/fu_f06_narrate", "fu",
|
||
"第六个。一首旋律,只有开头几个音,后面的你想不起来了。",
|
||
"p0"),
|
||
("fu/fu_f06_tap", "fu",
|
||
"你在心里哼了那几个音。后面的还是想不起来,但这几个音够了。",
|
||
"p0"),
|
||
("fu/fu_f06_dt", "fu",
|
||
"旋律消失了。",
|
||
"p0"),
|
||
|
||
("fu/fu_f07_narrate", "fu",
|
||
"第七个。被需要的感觉。有人曾对你说过你很重要,你还记得那句话。",
|
||
"p0"),
|
||
("fu/fu_f07_tap", "fu",
|
||
"你把那句话放进口袋里了。",
|
||
"p0"),
|
||
("fu/fu_f07_dt", "fu",
|
||
"那句话漂走了。",
|
||
"p0"),
|
||
|
||
("fu/fu_f08_narrate", "fu",
|
||
"第八个。一个人的时候的自在。没人看着,没人要你做什么,只是你自己。",
|
||
"p0"),
|
||
("fu/fu_f08_tap", "fu",
|
||
"你把这份自在收进来了。",
|
||
"p0"),
|
||
("fu/fu_f08_dt", "fu",
|
||
"它漂走了。也许你现在不需要。",
|
||
"p0"),
|
||
|
||
("fu/fu_f09_narrate", "fu",
|
||
"第九个。害怕。不知道怕什么,就是有点怕。",
|
||
"p0"),
|
||
("fu/fu_f09_tap", "fu",
|
||
"你承认了:我有点害怕。说出来之后,它小了一点。",
|
||
"p0"),
|
||
("fu/fu_f09_dt", "fu",
|
||
"你没有看它,害怕漂走了。也许。",
|
||
"p0"),
|
||
|
||
("fu/fu_f10_narrate", "fu",
|
||
"第十个。好奇心。对某件事、某个人、某个地方,你还有好奇。",
|
||
"p0"),
|
||
("fu/fu_f10_tap", "fu",
|
||
"好奇心跳进你手里了,很活跃。",
|
||
"p0"),
|
||
("fu/fu_f10_dt", "fu",
|
||
"好奇心漂走了,也许它去找别人了。",
|
||
"p0"),
|
||
|
||
("fu/fu_f11_narrate", "fu",
|
||
"第十一个。疲惫。真实的、身体里的疲惫,它已经跟了你很久了。",
|
||
"p0"),
|
||
("fu/fu_f11_tap", "fu",
|
||
"你说:我累了。疲惫点了点头,说我知道。",
|
||
"p0"),
|
||
("fu/fu_f11_dt", "fu",
|
||
"疲惫漂走了。你感觉稍微轻了一点点。",
|
||
"p0"),
|
||
|
||
("fu/fu_f12_narrate", "fu",
|
||
"最后一个。它没有名字。是你漂浮在这里时特有的某种感觉,说不清,但它在。",
|
||
"p0"),
|
||
("fu/fu_f12_tap", "fu",
|
||
"你把它揣在怀里了,连同它的没有名字。",
|
||
"p0"),
|
||
("fu/fu_f12_dt", "fu",
|
||
"你看着它留在了这片水里,这片梦里。",
|
||
"p0"),
|
||
|
||
# 动态结局分片(p3,用于拼接)
|
||
("fu/fu_ending_open", "fu", "你漂回来了。", "p3"),
|
||
("fu/fu_ending_kept_prefix", "fu", "你带走了:", "p3"),
|
||
("fu/fu_ending_released_prefix", "fu", "你放走了:", "p3"),
|
||
("fu/fu_ending_close", "fu", "这就是今天的你。", "p3"),
|
||
("fu/fu_item_wind", "fu", "夏天的风", "p3"),
|
||
("fu/fu_item_laugh", "fu", "笑声", "p3"),
|
||
("fu/fu_item_regret", "fu", "遗憾", "p3"),
|
||
("fu/fu_item_safe", "fu", "安全感", "p3"),
|
||
("fu/fu_item_tears", "fu", "没哭出来的眼泪", "p3"),
|
||
("fu/fu_item_melody", "fu", "那段旋律", "p3"),
|
||
("fu/fu_item_needed", "fu", "被需要的感觉", "p3"),
|
||
("fu/fu_item_alone", "fu", "独处的自在", "p3"),
|
||
("fu/fu_item_fear", "fu", "害怕", "p3"),
|
||
("fu/fu_item_curious", "fu", "好奇心", "p3"),
|
||
("fu/fu_item_tired", "fu", "疲惫", "p3"),
|
||
("fu/fu_item_nameless","fu", "无名之物", "p3"),
|
||
]
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
# 工具函数
|
||
# ──────────────────────────────────────────────────────────────
|
||
|
||
def find_ref_audio(source_dir, narrator_key):
|
||
narrator_dir = os.path.join(source_dir, "narrator", NARRATORS[narrator_key]["dir"])
|
||
if not os.path.isdir(narrator_dir):
|
||
return None
|
||
for ext in ("*.wav", "*.mp3", "*.flac"):
|
||
files = sorted(glob.glob(os.path.join(narrator_dir, ext)))
|
||
if files:
|
||
return files[0]
|
||
return None
|
||
|
||
|
||
def convert_to_wav(src_path, dst_path):
|
||
cmd = ["ffmpeg", "-y", "-i", src_path,
|
||
"-ar", "16000", "-ac", "1", "-acodec", "pcm_s16le", dst_path]
|
||
subprocess.run(cmd, check=True, capture_output=True)
|
||
|
||
|
||
def wav_to_base64(wav_path):
|
||
with open(wav_path, "rb") as f:
|
||
return base64.b64encode(f.read()).decode("utf-8")
|
||
|
||
|
||
def register_voice(audio_path):
|
||
b64 = wav_to_base64(audio_path)
|
||
r = requests.post(f"{BASE_URL}/v1/voices", json={"wav_base64": b64}, timeout=60)
|
||
r.raise_for_status()
|
||
return r.json()["voice_id"]
|
||
|
||
|
||
def synthesize_voxcpm(voice_id, text, cfg_value=2.0, inference_timesteps=10):
|
||
r = requests.post(
|
||
f"{BASE_URL}/v1/speech",
|
||
json={"text": text, "voice_id": voice_id,
|
||
"cfg_value": cfg_value, "inference_timesteps": inference_timesteps},
|
||
timeout=120,
|
||
)
|
||
r.raise_for_status()
|
||
return r.content
|
||
|
||
|
||
def apply_speed_and_convert_mp3(wav_path, speed, out_mp3):
|
||
"""调速 + 转 MP3(44100Hz mono 128kbps),添加首尾静音"""
|
||
filter_chain = f"aresample=44100,atempo={speed}" if abs(speed - 1.0) >= 0.01 else "aresample=44100"
|
||
# 添加 100ms 头部静音 + 200ms 尾部静音
|
||
filter_chain += ",adelay=100|100,apad=pad_dur=0.2"
|
||
cmd = [
|
||
"ffmpeg", "-y", "-i", wav_path,
|
||
"-filter:a", filter_chain,
|
||
"-ac", "1",
|
||
"-b:a", "128k",
|
||
out_mp3,
|
||
]
|
||
subprocess.run(cmd, check=True, capture_output=True)
|
||
|
||
|
||
def check_server():
|
||
try:
|
||
r = requests.get(f"{BASE_URL}/health", timeout=5)
|
||
return r.status_code == 200
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
# 主流程
|
||
# ──────────────────────────────────────────────────────────────
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="盲游 · 旁白 TTS 批量生产 — VoxCPM2 版")
|
||
parser.add_argument("--source", choices=["voice", "voice_mp3"], default="voice_mp3",
|
||
help="音色参考目录 (默认: voice_mp3)")
|
||
parser.add_argument("--game", help="只生产指定游戏 (linxia/yinian/shouye/houzhen/yisheng/fu)")
|
||
parser.add_argument("--priority", choices=["p0", "p1", "p2", "p3", "all"], default="all",
|
||
help="只生产指定优先级 (p0=核心 p1=结局 p2=条件变体 p3=动态分片)")
|
||
parser.add_argument("--test", action="store_true", help="只合成第一条")
|
||
parser.add_argument("--dry-run", action="store_true", help="只打印任务列表")
|
||
parser.add_argument("--cfg", type=float, default=2.0, help="VoxCPM cfg_value (默认: 2.0)")
|
||
parser.add_argument("--steps", type=int, default=10, help="VoxCPM inference_timesteps (默认: 10)")
|
||
args = parser.parse_args()
|
||
|
||
source_dir = os.path.join(PROJECT_DIR, args.source)
|
||
|
||
# 过滤
|
||
lines = LINES
|
||
if args.game:
|
||
lines = [(f, n, t, p) for f, n, t, p in lines if n == args.game]
|
||
if args.priority != "all":
|
||
lines = [(f, n, t, p) for f, n, t, p in lines if p == args.priority]
|
||
if args.test:
|
||
lines = lines[:1]
|
||
|
||
print(f"\n{'=' * 60}")
|
||
print(f" 盲游旁白 TTS 批量合成 — VoxCPM2")
|
||
print(f" 音色源: {args.source}/narrator/")
|
||
print(f" 输出: {OUT_BASE}/{{game_id}}/")
|
||
print(f" 共 {len(lines)} 条")
|
||
print(f"{'=' * 60}")
|
||
|
||
if args.dry_run:
|
||
game_counts = {}
|
||
for f, n, t, p in lines:
|
||
game_id = f.split("/")[0]
|
||
game_counts[game_id] = game_counts.get(game_id, 0) + 1
|
||
ref = find_ref_audio(source_dir, n)
|
||
ref_status = "✓" if ref else "✗缺参考"
|
||
print(f" [{ref_status}][{p}] {f}.mp3 speed={NARRATORS[n]['speed']} {t[:40]}...")
|
||
print(f"\n 统计:")
|
||
for g, c in sorted(game_counts.items()):
|
||
print(f" {g}: {c} 条")
|
||
return
|
||
|
||
# 检查服务
|
||
if not check_server():
|
||
print(f"\n[错误] VoxCPM 服务未就绪: {BASE_URL}/health")
|
||
print(f"请先启动: bash /home/xsl/AutoVideo/start-voxcpm.sh")
|
||
sys.exit(1)
|
||
print(f" 服务就绪: {BASE_URL}\n")
|
||
|
||
# 预处理:注册音色
|
||
voice_ids = {}
|
||
tmp_wav_dir = tempfile.mkdtemp(prefix="narrator_ref_")
|
||
|
||
needed_narrators = set(n for _, n, _, _ in lines)
|
||
for nar_key in sorted(needed_narrators):
|
||
ref_path = find_ref_audio(source_dir, nar_key)
|
||
if ref_path is None:
|
||
print(f" [跳过] {nar_key}: 未找到参考音频 ({source_dir}/narrator/{nar_key}/)")
|
||
print(f" 请在该目录放入参考录音(wav/mp3/flac),音色来源:{NARRATORS[nar_key]['note']}")
|
||
continue
|
||
|
||
if ref_path.lower().endswith(".wav"):
|
||
wav_path = ref_path
|
||
else:
|
||
wav_path = os.path.join(tmp_wav_dir, f"{nar_key}_ref.wav")
|
||
print(f" 转换参考音频: {os.path.basename(ref_path)} → WAV...", end=" ", flush=True)
|
||
convert_to_wav(ref_path, wav_path)
|
||
print("ok")
|
||
|
||
print(f" 注册音色: {nar_key} ({NARRATORS[nar_key]['note']})...", end=" ", flush=True)
|
||
try:
|
||
vid = register_voice(wav_path)
|
||
voice_ids[nar_key] = vid
|
||
print(f"ok voice_id={vid}")
|
||
except Exception as e:
|
||
print(f"失败: {e}")
|
||
|
||
print()
|
||
|
||
# 合成
|
||
ok_count, skip_count, fail_count = 0, 0, 0
|
||
|
||
for i, (filename, nar_key, text, priority) in enumerate(lines, 1):
|
||
game_id = filename.split("/")[0]
|
||
out_dir = os.path.join(OUT_BASE, game_id)
|
||
out_name = os.path.basename(filename) + ".mp3"
|
||
out_path = os.path.join(out_dir, out_name)
|
||
|
||
os.makedirs(out_dir, exist_ok=True)
|
||
|
||
if os.path.exists(out_path):
|
||
print(f" [{i}/{len(lines)}] [跳过,已存在] {filename}")
|
||
ok_count += 1
|
||
continue
|
||
|
||
if nar_key not in voice_ids:
|
||
print(f" [{i}/{len(lines)}] [跳过,无音色] {nar_key}: {filename}")
|
||
skip_count += 1
|
||
continue
|
||
|
||
print(f" [{i}/{len(lines)}][{priority}] {filename} [{nar_key}]...", end=" ", flush=True)
|
||
t0 = time.time()
|
||
|
||
try:
|
||
wav_bytes = synthesize_voxcpm(
|
||
voice_ids[nar_key], text,
|
||
cfg_value=args.cfg,
|
||
inference_timesteps=args.steps,
|
||
)
|
||
|
||
tmp_wav = out_path + ".raw.wav"
|
||
with open(tmp_wav, "wb") as f:
|
||
f.write(wav_bytes)
|
||
|
||
speed = NARRATORS[nar_key]["speed"]
|
||
apply_speed_and_convert_mp3(tmp_wav, speed, out_path)
|
||
|
||
if os.path.exists(tmp_wav):
|
||
os.remove(tmp_wav)
|
||
|
||
elapsed = time.time() - t0
|
||
print(f"✓ {elapsed:.1f}s speed={speed}")
|
||
ok_count += 1
|
||
|
||
except Exception as e:
|
||
print(f"✗ {e}")
|
||
fail_count += 1
|
||
|
||
shutil.rmtree(tmp_wav_dir, ignore_errors=True)
|
||
|
||
print(f"\n{'=' * 60}")
|
||
print(f" 完成:{ok_count} 条 跳过:{skip_count} 条 失败:{fail_count} 条")
|
||
print(f" 输出:{OUT_BASE}/{{game_id}}/*.mp3")
|
||
print(f"{'=' * 60}\n")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|