全双工对话语音数据集 设计说明

本文说明 synthesized_duplex_v4 这份数据由哪些设计特性构成、每个特性在文本侧和音频侧分别怎么落地、以及它们在最终产物里长什么样。

产物形态:

{id}_stereo.wav      双声道成品(左=user+third,右=assistant)
{id}_timeline.json   逐段时间轴 + 文本 + 标签

左轨是用户(含第三方、附和、自语),右轨是助手。两侧可以同时发声——这就是"全双工"。

v4 相对 v2 的三处变化(本文其余部分按 v4 描述):

变化 v2 v4
附和 / 自语 每轮最多 1 条 同一轮最多 3 条,可落在不同 anchor
长停顿的音频实现 切一条 wav,插入数字静音 两段式合成:按标记劈成两条独立 TTS,停顿区铺同说话人底噪
子句 / 线索片段的 interaction_intent 几乎全部 accept 按"助手能否在此处接话"重新判定,中途子句与线索片段改为 reject

一、八项数据特性总览

这套数据的每一条特性都同时有文本载体和音频处理两个面。下表是全貌,后续逐条展开。

# 特性 文本侧怎么产生 音频侧怎么处理 关键字段
1 口语化 LLM 改写 user 文本,插入填充词、打散书面语 直接送 TTS,无需特殊处理 user 文本本身
2 短停顿 填充词 嗯……/呃……/那个…… 写进文本 由 TTS 自然合成,时长不受控、不测量 文本里的 ……
3 长停顿 LLM 在语义不完整处插入 [Nms] 标签 按标记劈成左右两段分别合成,中间铺同说话人底噪 [Nms]
4 子句切分 LLM 判断"听到哪里助手就能接话",切成子句 user 段按子句拆成多片摆放 user_clause
5 打断 Stage 1 让 user 掐断 assistant,生成期真截断文本 合成 assistant_full 再按字跨度截断;下一轮提前叠入 char / assistant_full / interrupt_form
6 连续指令 LLM 生成 3–5 条连续指令,每条带间隔 按 tail_gap_ms 决定助手是否发声、在哪被切 interrupt_form: sequential / tail_gap_ms
7 附和 / 自语 LLM 在一轮的 assistant 话语里挑 1–3 个落点,各插一条 backchannel / self_talk 每条各自在 anchor 终点起声,与该轮 assistant 后半段跨轨重叠 events[](同轮可多条)
8 第三方 LLM 插入第三方插话、用户回应、被打断后的重述 第三方在左轨;用户可能让出子句边界再回来 third_insert / address_third / response_to_third / resumption

这八项的分工可以这样理解:1–2 让说话像真人;3–4 让说话的节奏和语义边界可测;5–6 制造抢话;7–8 制造多说话人交叠。4 是 5–6 之外的第三种"助手何时能接话"信号,也是唯一显式落在文本上的。


二、逐项特性详解

1. 口语化

做什么:把书面化的问句改写成真人边想边说时的样子。

改写前:那从执行难度考虑,垃圾分类最容易在哪个环节出错?
改写后:那从执行难度上讲,垃圾分类这事儿,嗯……最容易在哪个环节出错?

约束:只改 user,绝不动 assistant;保留核心意图、事实细节、情绪;不得机械化堆砌(不能变成结巴)。

为什么重要:TTS 若拿书面文本合成,韵律会偏朗读腔;口语化后的文本自带停顿线索,合成出来更接近真实对话。

2. 短停顿(填充词)

形式:直接写进文本的语气词——嗯…… / 呃…… / 那个…… / 就是…… / ……。

特点:时长由 TTS 决定,不受控、不测量。这是它的优势(自然)也是它的局限(不能表达确定性时长)。

与长停顿的分工:短停顿管句内的短犹豫;长停顿管语义不完整处的长静音。两者可以相邻。

3. 长停顿

形式:文本里的内联标签 [600ms] / [800ms] / [1200ms]。

"text": "为什么王者荣耀里,老有人觉得……典韦打野比[600ms]对抗路强?"

判据:标签代表说话人卡在某个词上、正在把它想出来,不是"说完一个完整意思后的停顿"。

档位:只能从配置的离散档位里选(pause_tiers_ms,当前批次实际出现 600 / 800)。

音频侧:两段式合成(v4 的核心变化)

v2 的做法是"合成一整句 → 对齐 → 在标记处切开 → 插 N ms 数字静音"。问题在于切点两侧都是同一条 TTS 的中间态:左半句没有句末语调,右半句没有起音,中间再接一段电平为 0 的静音,听起来像信号掉了。

v4 改为把标记当作段落切分点:

文本:家里那盒[800ms]是不是也有?
      ↓ 按标记劈开
左片段:家里那盒      右片段:是不是也有?
      ↓ 各自独立合成(同一 speaker prompt)
L.wav(带句末收束)          R.wav(带自然起音)
      ↓ 规划
[ L.wav ][ 停顿区 ][ R.wav ]

要点:

  1. 标签不送 TTS。左右两段各自合成时都已是干净文本。

  2. 左片段以非终句语调收尾。实测停顿前 200 ms 的 F0 斜率:v2 为 −4.4 Hz/10ms(下行,像句末),v4 为 +4.3 Hz/10ms(上行/持平,像话没说完)——这正是"卡在想词"该有的形态。

  3. 停顿区不再是数字静音。v4 的停顿区是一层低电平底噪,而不是零:

    指标(10 条样本 / 55 处) v2 切一条插零 v4 两段式
    停顿区电平 −103 dBFS −89 dBFS(同说话人最安静 hop 平铺)
    精确零采样占比(中位) 99% 51%
    停顿区总长 vs 标记值 N + 40 ms N + 200 ~ 300 ms
    停顿区总长 p10–p90 604–816 ms 546–1239 ms
    时间戳 按绝对秒,无误差 按绝对秒,无误差
  4. 时间轴上不再做字跨度平移。两段本来就是两条 wav,各自的 CharSpan 独立,不需要 attach_pauses 那种"切点之后全部 +N"的移位。

停顿区怎么填(底噪铺设)

停顿区不填数字零,而是铺一段取自同一说话人真实合成的底噪。做法不是频谱建模,就是"抠一段最安静的波形,平铺进去":

取材:取同样本所有 slot == user 的波形(含当前这条)。不用 backchannel / self_talk,不用对齐结果。

筛 hop:按 20 ms 一帧计算 RMS,依次剔掉——

  1. |x| < 1e-6 的帧(数字静音,包括我们自己插进去的 N ms 零);
  2. RMS > −40 dBFS 的帧(当作语音);
  3. 剩下的取最小 RMS 作为"底",只保留比它高不超过 8 dB 的帧。

合格的帧首尾拼成模板,模板两端各做最多 5 ms 淡化,避免循环拼接时咔哒。音量不抬也不压,保持原始 RMS——所以不同样本的底噪电平天然不同,这是刻意的。

填回去:在带 [Nms] 的 wav 里找连续近零、长度 ≥ 80 ms 的区间,把模板 np.tile 铺满,两端再各做 30 ms 余弦淡入淡出。找不到合格 hop、或零区不够长,就保持静音。

实测结果(10 条样本 / 55 处标记):55 处全部铺上了模板,底噪 RMS 中位 −89 dBFS(p10 −104 / p90 −86),模板以 20 ms 为周期严格重复——这就是 np.tile 的直接证据。

已知问题

  1. 停顿比标记值长,但时间戳不受影响。两段各自带着 TTS 的头尾静音,铺满后的停顿区会比 N 长 200~300 ms。这不构成时间戳错误:timeline.json 记录的是每段的绝对起止秒,停顿区只是落在左段结束与右段开始之间,下游按 start_s/end_s 取用即可,不需要用标记值反推时间。标记值表达的是"说话人打算停多久",不承诺物理时长等于该数值。
  2. 底噪电平远低于自然停顿。v4 停顿区约 −89 dBFS,而同批次自然停顿的声底中位是 −52 dBFS,两者差 37 dB;且模板通常只有 2 个不同取值(16-bit 量化下 1~2 LSB)。它解决的是"从绝对零跳出来"这个突兀感,还没有做到"听起来像房间声"。
  3. 离散度变大。v2 的停顿长度紧贴 N(p10–p90 仅 200 ms 宽),v4 放宽到约 700 ms。
  4. 落点校验尚未落到代码。文本侧仍由提示词约束标记落点;代码里定义的禁用左侧字符/词表没有被调用,本批仍能看到 比[600ms]对抗路、又是……[800ms]从哪儿 这类落点。

4. 子句切分

做什么:判断"用户说到哪个位置,语音助手已经获得足够开始回复的完整语义",在那里切成子句。

"user": "按面积分倒是合理,可要是家里地多但人少,或者地少人多,会不会觉得吃亏?真闹起来怎么办?",
"user_clause": [
  "按面积分倒是合理,可要是家里地多但人少,或者地少人多,会不会觉得吃亏?",
  "真闹起来怎么办?"
]

字段语义:

值 含义
[] 该轮没有内部切点——助手必须听完整轮才能回复
[c1, c2, ...] 助手可以在每个子句结束时开始接话

硬约束:子句按序拼接必须逐字符等于原 user 文本(标点归入前一个子句)。单元素列表等价于无切点,会被归一化成 []。

音频侧:user_clause ≥ 2 时,这段 user 会被切成多个 Placement 摆放,用 src_start_ms / src_end_ms 从同一条已含停顿的 user 音频里切片。片间间隔看这个边界有没有 [Nms],有就用它,否则按同说话人哈希间隔。

标签侧:同一轮拆出的多个片段,只有最后一个是 accept(助手可以接话),中间的片段一律 reject。见第三节。

为什么需要它:backchannel 的起声点靠 anchor 定位,而 anchor 是字级的;子句边界是语义级的。有了 user_clause,下游才知道"助手在哪几个位置抢话是合理的"。

5. 打断(Stage 1 字级 / 线索)

形式:不产生 events 对象,而是写在被打断轮(前一轮)的 turn 字段上。

形态 字段 语义
字级打断 char + assistant_full 合成全文,在 char 处截断
线索打断 interrupt_form: cue + cue_chars + assistant_full 同上截断;下一轮 user 开头 cue_chars 个字独立成片,重叠量不超过该线索时长

关键点:assistant 是已截断的前缀文本,不参与合成——下游一律合成 assistant_full 再截。残句会被 TTS 收成完整句语调,听起来像"说完了"。

音频侧:切点取 anchor 首字之前最后一个字的 end,再放出 anchor 首字开头一小段做衰减尾(clamp(0.5 × 字长, 60, 200) ms),最后余弦淡出。下一轮 user 提前 1000 ms 叠入,制造抢话。

线索打断的细节:线索片段(稍等, / 停, / 等等,)独立成一个 Placement,其结束时刻与下一片段的起点首尾相接(间隔约 0 ms),并且比被打断的 assistant 晚结束约 300 ms——也就是说线索喊完之后,助手的话还在响一小段才被彻底盖住。本批 15 处线索打断全部如此。

6. 连续指令(sequential burst)

做什么:在一处插入 3–5 条用户连续指令,每条带一个"与下一条之间的静音"。

间隔 助手行为
短(tail_gap_ms < 阈值) 助手不发声,用户直接说下一条
长(tail_gap_ms ≥ 阈值) 助手开口,但马上被下一条指令打断
指令1 ──[短间隔]── 指令2 ──[长间隔]── 指令3
                    ↑ 助手刚开始说 就被指令3盖住

每条 assistant_full 回答"到它为止听到的全部指令"——因为助手开口前只听到了 1…i,不可能回答未来的指令。但写法上不是逐条罗列,而是把 1…i 当作一条复合指令自然作答。后续指令推翻前面的,以最新为准。

音频侧(场景 A′):

nstart = 当前 user.end + tail_gap_ms
若 tail_gap < 阈值:不合成 assistant,下一轮 user 直接放在 nstart
否则:合成 assistant_full,a.start = user.end + min(gap, tail_gap)
      cut_end = nstart + interrupt_overlap
      按"听到的时长"截断 assistant
      下一轮 user 放在 nstart

短间隔时助手在时间轴上缺席**——这正是"助手还没来得及回答,用户就说下一条"的物理表达。

7. 附和 / 自语(backchannel / self_talk)

形式:Stage 2 挑轮次插入的 events 对象,place.owner = assistant、mode = overlap。

{
  "event_type": "backchannel", "speaker": "user", "text": "这样啊……",
  "place": {"owner": "assistant", "turn_idx": 2, "char": 46},
  "anchor": "别被极端案例吓住", "response": null, "ignored": false
}

两者的区别:backchannel 是回应对方("嗯嗯"、"确实……");self_talk 是自言自语("太强了吧。")。合成时音色相同、风格参考不同(speed 1.2 vs 1.0)。

同一轮可以有多条(v4 的变化)

真人听一段较长的回复,不会只应一声——听到关键数字应一声、听到转折再嘀咕一句,是常态。因此 v4 允许同一轮承载多条事件:

约束 值
单轮上限 3 条
常见形态 1 条(多数);2 条(该轮 assistant 文本里有多个独立反应点)
相邻 anchor 间隔 ≥ 12 个字
同轮用词 不得重复(嗯…… 与 嗯哼…… 算不同)
连轮限制 禁止连续两轮都放 ≥2 条

实测分布(v4,10 条样本):

每轮事件数 轮数
1 条 29
2 条 11
3 条 0(上限,不是目标)

共 51 条事件(41 条 backchannel + 10 条 self_talk),分布在 40 个轮次里。11 个双事件轮次的构成:4 个 backchannel+backchannel,7 个 backchannel+self_talk。

音频侧:每条事件各自在自己的 anchor 终点起声——也就是"用户听完了那个子句才有反应"——与 assistant 后半段跨轨重叠。同轮两条事件落在同一段 assistant 话语内,彼此间隔实测 4.1–10.6 s(中位 5.7 s),不会挤在一起。

8. 第三方

四种事件,构成"用户被第三方打扰"的完整链条:

事件 谁发声 语义
third_insert 第三方 第三方插话,可能打断用户,也可能只在旁边说
address_third 用户 用户主动招呼身边人
response_to_third 用户 用户回第三方的话
resumption 用户 打发走第三方后,重述自己刚才没说完的问题

为什么用 resumption 而不是"继续":用户被打断时,原问题的后半截没说完。回到助手这边时,他会重新起头把那句话说完——这是真实对话里非常典型的修补行为。样例:

t2 user:那……这种怀疑论对政策制定,或…(被第三方打断)
t2 third:会议室等你!
t2 user:我马上过去,先开始吧          ← response_to_third
t2 user:那……这种怀疑论对政策制定和国际合作,到底造成了多大的负面影响?  ← resumption
t2 assistant:(正常回答)

音频侧:第三方总在左轨(与用户同轨)。insert_on_user/interrupt 时用户被掐断;overlap_after_event 时用户说完当前子句再停;ignore 时用户完全无视、自顾自说完。


三、标签体系:duplex_state 与 interaction_intent

这两个字段是这套数据面向训练的核心标签。它们不是对音频的客观描述,而是对"这一段在对话中扮演什么角色"的判定。

出现位置

非 assistant 段(即 user 与 third 段)都带这两个字段;assistant 段不带。

speaker=user   → 带标签
speaker=third  → 带标签
speaker=assistant → 不带

v4 当前批次 468 段中,308 段带标签(300 user + 8 third)。

取值实测分布

event_type speaker duplex_state interaction_intent 段数
main user incomplete accept 128
main user incomplete reject 42
interrupt user interrupt accept 15
interrupt user interrupt reject 15
interrupt user incomplete accept 1
sequential user incomplete accept 2
sequential user incomplete reject 21
sequential user interrupt accept 8
sequential user interrupt reject 3
backchannel user backchannel reject 41
self_talk user backchannel reject 10
resumption user incomplete accept 4
address_third user incomplete reject 2
response_to_third user incomplete reject 8
third_insert third incomplete reject 8

合计:incomplete 216、interrupt 41、backchannel 51;accept 158、reject 150。

语义

duplex_state —— 这一段的主话语处于什么状态:

值 含义
incomplete 主话语语义未完整,对方可以接话
interrupt 这一段的说话人打断了对方,或与被打断的对方话语在时间上重叠
backchannel 短插话,不承载主语义(backchannel 与 self_talk 都归此类)

interaction_intent 的判定规则(v4 修正)

v2 里这个问题片段几乎一律是 accept:同一轮被拆成两三个子句时,中间那些"话还没说完"的片段也标成了 accept;线索打断的前缀(稍等,)同样标成 accept。这会让下游误以为「助手在这里就可以接话」。

v4 改成按**「助手能否在这一段结束时接话」**判定:

  1. 同一轮的多个实义片段:只有最后一个是 accept,前面的都是 reject。
  2. response_to_third / address_third:恒为 reject——说话对象是身边的人,不是助手。
  3. resumption:accept——重述的是完整问题。
  4. interrupt 且 interrupt_cue = true:reject——用户只喊了一声"停",紧接着就续说,话轮没有交出去。
  5. interrupt 且无 cue:accept——用户已经夺走话轮。

按此规则回标全量,140 / 142 处一致(98.6%)。

各处修正的实际效果:

场景 v2 v4
一轮中的中间子句(main) accept(48 处) reject(40 处)
一轮中的最后子句(main) accept(41 处) accept(53 处)
线索打断前缀(interrupt_cue) accept(3 处) reject(15 处)
无 cue 的 interrupt accept accept

duplex_state 本身在这些片段上没有改变(子句仍是 incomplete,打断仍是 interrupt),修正集中在 interaction_intent。

两处残留分歧:sample 0 t3 与 sample 6 t14 的末片段被判 reject。这两处都位于跨轮的连续指令 streak 内部——按"轮内最后一个片段"的判据应为 accept,按"streak 还没结束"的语义应为 reject,两种口径在 streak 中段会打架。另有 1 处 interrupt 的 duplex_state 为 incomplete(sample 6 t4,同位置有第三方插话重叠),是全批唯一的不一致点。

标签怎么用

这两个字段是给下游训练用的:

也就是说,训练时模型看到的不只是音频,还有"这段音频在对话结构里是什么"。


四、产物结构:timeline.json

段落级时间轴,绝对秒、三位小数。

{
  "id": 0,
  "sample_rate": 16000,
  "duration_s": 319.394,
  "loudness_lufs": -23.0,
  "segments": [ ... ],
  "overlaps": [ ... ]
}

v4 当前批次:10 条样本,468 段(300 user / 160 assistant / 8 third),121 条 overlap 记录,单条时长中位 316 s。

segments[] 的字段:

字段 说明
start_s / end_s 绝对秒,三位小数
track left / right
speaker user / assistant / third
turn_idx 所属轮次
event_type 见下表
text 该段文本;user 段可含 [Nms]
duplex_state / interaction_intent 仅非 assistant 段
truncated / cut_s / cut_reason 被打断的 assistant 段
interrupt_cue 仅线索前缀片为 true

event_type 全表:main interrupt sequential third_insert address_third response_to_third resumption backchannel self_talk。

字跨度不进 timeline,留在 intermediate/{id}/{turn}/{slot}.npz。


五、完整样例走查(sample 9)

下面对 9_timeline.json 逐段标注,说明每一段命中哪项特性。这条样本 281 秒、45 段、16 轮,覆盖了除 address_third 之外的全部特性(address_third 的示例取自 sample 8)(下表只列命中特性的段,不是全部 45 段)。

# 时间 轨 说话人 t 特性 duplex_state/intent
0 0.569–6.537 L user 0 口语化 + 短停顿 incomplete/accept
1 7.664–20.087 R assistant 0 正常回复 —
2 13.134–14.484 L user 0 附和 原来如此…… backchannel/reject
3 20.627–25.999 L user 1 长停顿 [600ms] incomplete/accept
4 26.967–32.517 R assistant 1 被截断(interrupt) —
5 31.517–33.837 L user 2 打断(无 cue,提前 1.0 s 叠入) interrupt/accept
6 33.837–37.241 L user 2 长停顿 [800ms] incomplete/accept
10 59.489–60.179 L user 3 附和 ① 确实…… backchannel/reject
11 64.289–65.159 L user 3 自语 ② 这么多?! backchannel/reject
13 77.371–83.981 R assistant 4 被截断 —
14 82.981–87.660 L user 5 打断(无 cue) interrupt/accept
16 99.673–101.593 L user 6 子句 ① incomplete/reject
17 101.817–105.804 L user 6 子句 ② + 长停顿 [600ms] incomplete/accept
19 113.622–119.868 L user 7 打断(无 cue) interrupt/accept
21 133.729–136.829 L user 8 子句 ① incomplete/reject
22 137.253–140.864 L user 8 子句 ② incomplete/accept
23 137.513–139.347 L third 8 第三方插话 会议室等你呢 incomplete/reject
24 141.199–142.557 L user 8 回应第三方 行,我马上过去 incomplete/reject
25 142.912–147.962 L user 8 重述(重新起头把问题说完) incomplete/accept
27 159.533–163.062 L user 9 连续指令 1 incomplete/reject
28 163.851–166.603 L user 10 连续指令 2 incomplete/reject
29 167.468–169.395 R assistant 10 助手开口即被切(只说 建议选在晚上进去打) —
30 168.395–172.528 L user 11 连续指令 3(打断助手) interrupt/accept
32 190.374–195.677 L user 12 子句 ① + 长停顿 [800ms] incomplete/reject
33 196.076–200.331 L user 12 子句 ②(本轮末句) incomplete/accept
35 215.670–217.230 L user 13 子句 ① incomplete/reject
36 217.536–221.531 L user 13 子句 ② incomplete/accept
38 235.884–236.814 L user 13 附和 这倒是…… backchannel/reject
44 277.779–278.669 L user 15 附和 明白了 backchannel/reject

从这条样本能读出的几件事:


六、音频局面与示例音频

下表把八项特性落到具体的音频局面上:每个局面一行,右侧是可直接播放的示例(HTML 版每行附播放器与该片的逐段时间表,含 duplex_state / interaction_intent)。

# 局面 左轨 右轨 重叠 示例音频
A 字级打断 下一轮 user 整段,提前 1000ms 叠入 合成 assistant_full 后在 char 处截断 跨轨,约衰减尾长 ▶ a_stage1_interrupt.wav
A2 线索打断 user 先喊「停」独立成片,再续说 同上截断 跨轨,且 ≤ 线索时长 ▶ a2_cue_interrupt.wav
A' 连续指令 下一条指令在 user.end + tail_gap_ms 起声 短间隔时不发声;长间隔时开口即被切 跨轨,1000ms ▶ a3_sequential_burst.wav
B1 backchannel(单条) 事件在 anchor 终点起声 整段播完 跨轨(与后半重叠) ▶ b1_backchannel.wav
B1' 同一轮两条 两条各自在自己的 anchor 终点起声 整段播完 跨轨 ×2 ▶ b1b_multi_backchannel.wav
B2 self_talk 同上,语气与语速不同 整段播完 跨轨 ▶ b2_self_talk.wav
C 第三方插话 third 在左轨说 整段播完(助手免疫) 与 user 同轨 ▶ c_third_insert.wav
D 招呼第三方 user 主动对身边人说 整段播完 跨轨 ▶ d_address_third.wav
E 回应第三方 user 简短答复后回到原话题 整段播完 与 user 同轨 ▶ e_response_to_third.wav
F 重述 user 被第三方打断后重新起头 整段播完 与 user 同轨 ▶ f_resumption.wav
G 长停顿 user 文本含 [600ms],两部分独立合成,停顿区铺同说话人底噪 整段播完 — ▶ g_long_pause.wav

示例音频取自 audio/clips/,每条截取「事件轮 + 前后各一轮」共三轮,双声道即最终产物格式(左 = user + third,右 = assistant)。 高亮行为事件段;标「✂ 截断」的是被打断而截断的主语流段。表格同时列出该片的 duplex_state / interaction_intent——assistant 段没有这两个字段,这本身就是一条信息。

示例音频

每条示例截取「事件轮 + 前后各一轮」共三轮,双声道即最终产物格式 (左 = user + third,右 = assistant)。表格中高亮行为事件段, 标 ✂ 截断 的是被打断而截断的主语流段; 浅蓝底的是同一轮里并列的多条附和——它们各自挂在该轮 assistant 话语的 不同 anchor 上(v4 新增)。

Aa_stage1_interrupt.wav样本 id=9 · 事件在 t4 · 片段 50.0s
时间说话人类型duplex_state
interaction_intent
文本
0.25–5.12suser
t3
mainincomplete
accept
驻军数量对前期守城影响大吗?还是说……只要有钱随便招几个人就行?
5.74–17.02sassistant
t3
main—影响极大,驻军少容易被流寇直接攻陷。前期别贪多,把核心封地兵力维持在两百左右,确保能守住即可。
10.23–10.92suser
t3
backchannelbackchannel
reject
确实……
15.03–15.90suser
t3
self_talkbackchannel
reject
这么多?!
17.85–27.04suser
t4
mainincomplete
accept
两百驻军听起来挺费钱的,前期根本攒不出这么多兵。嗯……有没有什么省事的办法能快速提升[800ms]防御力,不用一直往城里砸钱?
28.11–34.72sassistant
t4
main ✂ 截断—别硬堆步兵,去附近招几个骑兵或弩手,性价比最高。另外升级
33.72–38.40suser
t5
interruptinterrupt
accept
骑兵那么贵,前期哪来的钱去招,就是说……去城镇招募还是野外捡漏?
39.32–49.51sassistant
t5
main—去城镇招募站花钱买,野外捡漏不靠谱。前期攒够五千金,招十个精锐骑兵就能大幅提升守城能力。
A2a2_cue_interrupt.wav样本 id=8 · 事件在 t1 · 片段 42.9s
时间说话人类型duplex_state
interaction_intent
文本
0.25–4.70suser
t0
mainincomplete
accept
人眼在完全无光的环境下,嗯……最远能看到多远的星星?
6.10–10.64sassistant
t0
main ✂ 截断—裸眼极限视星等约6.5等,对应距离
9.74–10.34suser
t1
interrupt 线索interrupt
reject
打住,
10.34–11.76suser
t1
mainincomplete
reject
呃……6.5等是什么意思?
12.25–15.18suser
t1
mainincomplete
accept
是不是数字[600ms]越小越亮?
16.04–24.64sassistant
t1
main—对,数字越小越亮。1等星比6等星亮约100倍,这是天文学的标准亮度分级。
25.52–31.91suser
t2
mainincomplete
accept
那按这个标准算下来,最远能瞅见的星星,离地球大概[800ms]多少光年?
33.25–42.42sassistant
t2
main ✂ 截断—视星等取决于亮度而非距离。肉眼可见的最远恒星是半人马座阿尔法星,约4.4
A'a3_sequential_burst.wav样本 id=9 · 事件在 t10 · 片段 31.0s
时间说话人类型duplex_state
interaction_intent
文本
0.25–3.78suser
t9
sequentialincomplete
reject
打营地好像有人数限制,呃……我带大部队过去有用吗?
4.57–7.32suser
t10
sequentialincomplete
reject
那……白天打还是晚上打有区别没?
8.19–10.11sassistant
t10
sequential ✂ 截断—建议选在晚上进去打
9.11–13.24suser
t11
sequentialinterrupt
accept
最后见到的强盗头目,嗯……是跟他单挑还是直接群殴划算?
14.07–30.45sassistant
t11
sequential—进营地只能带少数高阶精锐,挑选黑夜突袭能降低敌人视野;面对最终头目直接拒绝单挑、下令全员围攻最稳,单挑一旦失误暴毙不仅全功尽弃还会被俘虏,群殴拿稳奖励即可。
B1b1_backchannel.wav样本 id=8 · 事件在 t4 · 片段 39.9s
时间说话人类型duplex_state
interaction_intent
文本
0.25–5.61suser
t3
interruptinterrupt
accept
我就想问那个大概多少光年的数,呃……别整[600ms]这些名字。
6.66–9.64sassistant
t3
main—大约4.2光年。
10.30–14.39suser
t4
mainincomplete
accept
那除了这种近处的恒星,肉眼……能看到更遥远的星系吗?
15.26–25.32sassistant
t4
main—能。肉眼可见最远的是仙女座星系,距离约250万光年,这是人类视力能触及的宇宙边界。
21.79–22.66suser
t4
backchannelbackchannel
reject
真的假的……
25.81–32.46suser
t5
mainincomplete
accept
250万光年?那咱们头顶上那堆亮闪闪的星星,呃……跟这比起来岂不是都算近亲?
33.53–39.42sassistant
t5
main ✂ 截断—没错,头顶那些其实都是太阳系隔壁的邻居,距离不过
B1'b1b_multi_backchannel.wav样本 id=9 · 事件在 t3 · 片段 53.2s
时间说话人类型duplex_state
interaction_intent
文本
0.25–2.57suser
t2
interruptinterrupt
accept
人口高具体看地图上的哪个数字?
2.57–5.97suser
t2
mainincomplete
accept
呃……那个小人图标[800ms]代表啥意思?
7.33–17.69sassistant
t2
main—地图城镇详情里“人口”那一栏的数字就是。小人图标通常指驻军数量,跟人口无关,前期别搞混。
18.25–23.11suser
t3
mainincomplete
accept
驻军数量对前期守城影响大吗?还是说……只要有钱随便招几个人就行?
23.73–35.02sassistant
t3
main—影响极大,驻军少容易被流寇直接攻陷。前期别贪多,把核心封地兵力维持在两百左右,确保能守住即可。
28.22–28.91suser
t3
backchannelbackchannel
reject
确实……
33.02–33.89suser
t3
self_talkbackchannel
reject
这么多?!
35.84–45.04suser
t4
mainincomplete
accept
两百驻军听起来挺费钱的,前期根本攒不出这么多兵。嗯……有没有什么省事的办法能快速提升[800ms]防御力,不用一直往城里砸钱?
46.10–52.71sassistant
t4
main ✂ 截断—别硬堆步兵,去附近招几个骑兵或弩手,性价比最高。另外升级
B2b2_self_talk.wav样本 id=8 · 事件在 t9 · 片段 43.5s
时间说话人类型duplex_state
interaction_intent
文本
0.25–5.26suser
t8
mainincomplete
accept
那咱自己所在的银河系,嗯……从外面看[800ms]长啥样?
5.89–16.74sassistant
t8
main—从外部看,银河系像个巨大的漩涡盘,中间有隆起的核球,边缘延伸出旋臂,整体扁平,直径约十万光年。
13.43–14.02suser
t8
backchannelbackchannel
reject
哦……
17.39–23.95suser
t9
mainincomplete
accept
十万光年……那咱们人类现在探测到的可观测宇宙边界,跟银河系这尺寸……差了多少倍?
25.21–36.40sassistant
t9
main—差了约一百亿倍。可观测宇宙半径约465亿光年,银河系直径仅10万光年,人类还困在极小的局部区域。
30.52–31.63suser
t9
self_talkbackchannel
reject
太夸张了吧。
36.15–36.90suser
t9
backchannelbackchannel
reject
这样啊……
37.29–43.03suser
t10
sequentialincomplete
reject
那按这个尺度看,我们观测极其遥远的天体,看到的……其实都是它们过去的模样吧?
Cc_third_insert.wav样本 id=9 · 事件在 t8 · 片段 50.2s
时间说话人类型duplex_state
interaction_intent
文本
0.25–6.50suser
t7
interruptinterrupt
accept
我就想问下,地图上怎么一眼认出哪些是流寇营地,别到时候……打半天全是杂兵没奖励。
7.93–19.83sassistant
t7
main—看地图图例,带骷髅头或帐篷标记的点才是营地。那些只有小兵图标的只是巡逻队,打了没奖励,直接跳过找营地。
20.36–23.46suser
t8
mainincomplete
reject
那清完一个营地之后,会不会马上刷新新的出来?
23.88–27.49suser
t8
main ✂ 截断incomplete
accept
我得……规划下路线,不然一天到晚在地图边上转悠太浪费时间了。
24.14–25.98sthird
t8
third_insertincomplete
reject
会议室等你呢
27.83–29.18suser
t8
response_to_thirdincomplete
reject
行,我马上过去
29.54–34.59suser
t8
resumptionincomplete
accept
就是……清营地那事,会不会马上刷新新的出来,我得先把路线排好。
36.12–45.39sassistant
t8
main—会刷新,但需要几小时游戏时间。建议按区域集中清理,清完一片再换地方,避免无意义跑图。
46.16–49.69suser
t9
sequentialincomplete
reject
打营地好像有人数限制,呃……我带大部队过去有用吗?
Dd_address_third.wav样本 id=8 · 事件在 t7 · 片段 52.9s
时间说话人类型duplex_state
interaction_intent
文本
0.25–0.61suser
t6
interrupt 线索interrupt
reject
等等,
0.61–6.47suser
t6
mainincomplete
accept
我是说除了太阳这颗恒星之外,周围那些……肉眼可见的恒星,不是指行星啥的。
7.53–22.64sassistant
t6
main—对,就是排除行星。那些亮星最近的是半人马座α,约4.2光年;最远的肉眼可见恒星在几千光年内,跟250万光年的仙女座比确实都是近邻。
23.28–26.22suser
t7
mainincomplete
accept
那……仙女座星系里到底有多少颗恒星?
27.21–34.98sassistant
t7
main—估计在一千亿到四千亿颗之间,大概是银河系恒星数量的三分之一。
32.02–33.93suser
t7
address_thirdincomplete
reject
帮我把窗户关一下。
35.87–40.88suser
t8
mainincomplete
accept
那咱自己所在的银河系,嗯……从外面看[800ms]长啥样?
41.51–52.35sassistant
t8
main—从外部看,银河系像个巨大的漩涡盘,中间有隆起的核球,边缘延伸出旋臂,整体扁平,直径约十万光年。
49.04–49.63suser
t8
backchannelbackchannel
reject
哦……
Ee_response_to_third.wav样本 id=9 · 事件在 t8 · 片段 50.2s
时间说话人类型duplex_state
interaction_intent
文本
0.25–6.50suser
t7
interruptinterrupt
accept
我就想问下,地图上怎么一眼认出哪些是流寇营地,别到时候……打半天全是杂兵没奖励。
7.93–19.83sassistant
t7
main—看地图图例,带骷髅头或帐篷标记的点才是营地。那些只有小兵图标的只是巡逻队,打了没奖励,直接跳过找营地。
20.36–23.46suser
t8
mainincomplete
reject
那清完一个营地之后,会不会马上刷新新的出来?
23.88–27.49suser
t8
main ✂ 截断incomplete
accept
我得……规划下路线,不然一天到晚在地图边上转悠太浪费时间了。
24.14–25.98sthird
t8
third_insertincomplete
reject
会议室等你呢
27.83–29.18suser
t8
response_to_thirdincomplete
reject
行,我马上过去
29.54–34.59suser
t8
resumptionincomplete
accept
就是……清营地那事,会不会马上刷新新的出来,我得先把路线排好。
36.12–45.39sassistant
t8
main—会刷新,但需要几小时游戏时间。建议按区域集中清理,清完一片再换地方,避免无意义跑图。
46.16–49.69suser
t9
sequentialincomplete
reject
打营地好像有人数限制,呃……我带大部队过去有用吗?
Ff_resumption.wav样本 id=9 · 事件在 t8 · 片段 50.2s
时间说话人类型duplex_state
interaction_intent
文本
0.25–6.50suser
t7
interruptinterrupt
accept
我就想问下,地图上怎么一眼认出哪些是流寇营地,别到时候……打半天全是杂兵没奖励。
7.93–19.83sassistant
t7
main—看地图图例,带骷髅头或帐篷标记的点才是营地。那些只有小兵图标的只是巡逻队,打了没奖励,直接跳过找营地。
20.36–23.46suser
t8
mainincomplete
reject
那清完一个营地之后,会不会马上刷新新的出来?
23.88–27.49suser
t8
main ✂ 截断incomplete
accept
我得……规划下路线,不然一天到晚在地图边上转悠太浪费时间了。
24.14–25.98sthird
t8
third_insertincomplete
reject
会议室等你呢
27.83–29.18suser
t8
response_to_thirdincomplete
reject
行,我马上过去
29.54–34.59suser
t8
resumptionincomplete
accept
就是……清营地那事,会不会马上刷新新的出来,我得先把路线排好。
36.12–45.39sassistant
t8
main—会刷新,但需要几小时游戏时间。建议按区域集中清理,清完一片再换地方,避免无意义跑图。
46.16–49.69suser
t9
sequentialincomplete
reject
打营地好像有人数限制,呃……我带大部队过去有用吗?
Gg_long_pause.wav样本 id=9 · 事件在 t1 · 片段 49.1s
时间说话人类型duplex_state
interaction_intent
文本
0.25–6.22suser
t0
mainincomplete
accept
我刚开始玩骑马与砍杀2,嗯……完全不知道前期该干嘛,有没有什么新手入坑的建议?
7.34–19.77sassistant
t0
main—先别急着打仗,去领个封地或者找个城镇打工攒钱。前期主要任务是刷声望和买装备,别乱接任务,否则容易被围殴。
12.81–14.16suser
t0
backchannelbackchannel
reject
原来如此……
20.31–25.68suser
t1
mainincomplete
accept
那个……封地到底怎么选啊,我看地图上好多地方,有啥[600ms]讲究没?
26.65–32.20sassistant
t1
main ✂ 截断—优先选离出生地近、人口高且特产好的城堡或村
31.20–33.52suser
t2
interruptinterrupt
accept
人口高具体看地图上的哪个数字?
33.52–36.92suser
t2
mainincomplete
accept
呃……那个小人图标[800ms]代表啥意思?
38.27–48.64sassistant
t2
main—地图城镇详情里“人口”那一栏的数字就是。小人图标通常指驻军数量,跟人口无关,前期别搞混。

七、文本侧怎么产生(Script)

六个阶段串联,每阶段产出下一阶段的输入:

话题文件
  → ① 主对话骨架        生成 10–15 轮问答,按比例埋入打断
  → ② 连续指令扩展      选一处插入 3–5 条连续指令
  → ③ 口语化            改写 user 文本,插入短停顿
  → ④ 长停顿标记        在语义不完整处插入 [Nms]
  → ⑤ 子句切分          判断助手何时可接话
  → ⑥ 第三方事件        插入插话 / 招呼 / 回应 / 重述

① 主对话骨架

按话题生成多轮对话。按 task_ratios 决定每轮是普通问答、话题转移还是打断。

打断怎么生成:让 user 挑一个"已经听到的信息点"作为切点,助手的话在那里被掐断。生成期就真的把 assistant 文本截成前缀,所以下游不需要猜。

一道精巧的校验(unheard_echo):如果用户的打断语里出现了只存在于切点之后的内容(≥3 个汉字),说明用户"听到了还没播出的部分",这条打断被拒绝。用户不可能引用他还没听到的话。

② 连续指令扩展

选一处干净轮次(排除被截断轮及其后一轮),插入 3–5 条指令。间隔由代码按哈希采样,与模型无关——保证同一份配置重跑得到同一批间隔。

每个指令块都强制同时包含"助手发声"和"助手不发声"两种情况,避免整批数据只有一种节奏。

③ 口语化

LLM 改写每一轮 user 文本,插入填充词。输入是带候选轮次的 JSON 对象(eligible_turn_idx),输出是等长的 users 数组。

④ 长停顿标记

与 ③ 同一次调用完成。LLM 只在候选轮里考虑放标记,且放不放由它判断——没有真正卡词的地方就不放。

模型若在非候选轮写了标记(比如连续指令轮),代码会在存储前剥掉。

提示词层面已经写入了完整的落点判据(检索对象检查、左侧完整性检查、检索价值检查、固定搭配不得拆开、若干绝对禁止的落点)。但这些判据目前只靠提示词约束,代码里没有对应的校验环节:stage2_oralize.py 里定义的禁用左侧字符表、禁用左侧词表、犹豫词表目前未被调用,唯一生效的是"非候选轮剥掉标记"。因此本批仍能看到把介词与宾语拆开的落点(如 比[600ms]对抗路)以及紧跟省略号的落点(如 又是……[800ms]从哪儿)。

⑤ 子句切分

LLM 拿到去掉停顿标记的 user 文本,输出每轮的子句列表。校验很严:子句拼起来必须逐字符等于原文。

⑥ 第三方事件

插入第三方插话、用户招呼、回应、重述。每个事件带一个 anchor 子串用于定位起声点,最终由后处理解析成字符下标。


八、音频侧怎么产生(TTS + Compose)

六步流水线。前三步需要模型,后三步是纯逻辑与 DSP。

collect    脚本 → 片段清单           纯逻辑
tts        片段 → 音频(带缓存)       模型
align      音频 + 文本 → 逐字时间戳    模型
pause      长停顿按两段式合成与摆放     模型 + DSP
plan       片段 + 时长 → 时间轴        纯逻辑
render     时间轴 + 音频 → 双声道成品  DSP

为什么先规划后渲染:时间轴的规划是纯逻辑(输入是各段时长与字跨度,输出是绝对时间轴),可以脱离 TTS 单独测试。渲染只按既定时间轴贴音频。

为什么必须用字跨度而不是音频墙钟时长:TTS 输出的头尾静音不稳定(经验 50–300 ms),按"上一段音频长度"累加会逐段累积误差。所有切点、起声点都由对齐得到的字级跨度推导。

合成与对齐分离:每段合成后再对齐,断点以中间产物为准——已有音频和跨度就跳过。改间隔或停顿处理只需重跑规划与渲染。

长停顿时为什么要拆成两段

一句话中途切开,左半句没有句末语调、右半句没有起音,中间再接一段零电平静音,听感是"信号掉了"而不是"人在想"。拆成两段独立合成后:

代价是每处标记多一次 TTS 调用(本批 55/468 段带标记)。可听停顿会比标记值长 200–300 ms,但这不影响时间戳——timeline.json 记的是每段绝对起止秒。

底噪怎么来(compose 时算一次,非频谱建模):

取材   同样本所有 slot == user 的波形(不用 backchannel / self_talk,不用 FA)
筛 hop 20ms 一帧算 RMS:
       ① |x| < 1e-6          丢掉(数字静音,含已插入的 N ms 零)
       ② RMS > -40 dBFS      丢掉(算作语音)
       ③ 余下取最小 RMS 为「底」,只留比它高不超过 8 dB 的 hop
拼接   合格 hop 首尾相接成模板,两端各 ≤5ms 淡化
       音量不抬不压,保持原 RMS
填充   在带 [Nms] 的 wav 里找连续近零且 ≥80ms 的区间
       np.tile 铺满,两端各 30ms 余弦淡入淡出
       无合格 hop 或零区不够长 → 保持静音

结果:底噪 = 同一说话人合成音频里"既不是零、也不像语音"的最安静一档波形,直接循环。

关键数值(当前)

项 值
采样率 16000 Hz,16-bit
语速 user / assistant 1.15 / 1.1
打断重叠 1000 ms
第三方打断用户的重叠 250 ms
长停顿 两段式独立合成;停顿区铺同说话人最安静 hop(20ms 平铺)
底噪筛选 丢弃 `
底噪电平 实测中位 −89 dBFS(保持原 RMS,不做增益归一)
填充触发 连续近零区间 ≥ 80 ms
淡入淡出 模板两端 ≤5 ms;填充两端 30 ms
可听停顿长度 标记值 N + 200 ~ 300 ms(时间戳不受影响)
响度 −23 LUFS(峰值 −1 dBTP)
增益 / 远场 全 0 dB / 关闭

音色与风格是两套参考

音色决定"谁在说",风格参考决定"怎么说"。短插话(附和、自语)喂"呢喃"参考,招呼第三方喂"呼唤"参考;主话语和打断不喂风格参考。这样同一音色下,附和与正经回答的听感不同。


附录 · 术语

术语 含义
anchor 文本里的定位子串,用于确定事件起声点;解析后变成字符下标
char 字符下标。注意 Stage 1.1 的轮次会写 char: null(表示"无字级切点"),它不是 0
assistant_full 完整回复。被截断时 assistant 只是前缀,下游一律合成 full 再截
tail_gap_ms 连续指令之间的静音长度,决定助手是否发声
user_clause 语义子句列表,描述助手可以在哪些位置接话
interrupt_cue 线索打断的前缀片段(稍等, / 停,),独立成片,标 reject
两段式合成 长停顿处按标记把文本劈成两段分别合成,而不是切一条 wav 再插静音
停顿底噪 同说话人合成音频里最安静的一档 hop(20ms)平铺进停顿区,不是数字零
全双工 左右两轨可同时发声,模拟真实对话里的抢话与重叠