Audio Forensics · 2026-07-14

韩语句末词为什么会被切掉,
又为什么会跑到下一段

两个成功任务,症状看起来不同。一个少了 아닌데? 的尾巴;一个把 완료! 放到下一段开头。把 Fish 原始时间戳、整段 TTS、切片和最终轨叠起来后,它们指向同一个错误模型。

combo · gw_e1f63c98…superdub · gw_5c8101fb…Fish voice · fish_8cf5ee4c…只读取证 · 零任务重跑
20 段全部 raw vs final 可播放
109 msA continue 被切掉的词尾
816 msB 下一段首词前的异常前导
0 ms漏词被 runner 压成的时长

一句话结论

不是 atempo 吃词,也不是最终混音乱序。问题发生得更早:Fish 的词时间戳会整词缺失,也会比真实收音早;runner 却把“最后一个字的 end”直接当音频刀口。词时间本来适合做字幕锚点,不该单独决定声学切片。

Fish 给错了什么

  • 父 A 的原始响应没有 아닌데
  • B 的原始响应没有 완료
  • 当前 A 虽有 아닌데,end 仍比能量尾早 108.73ms。

runner 又做了什么

  • 没对上的 prompt 字符,被填到 previous end。
  • 于是整词变成 0ms,但流程继续成功。
  • atrim 没尾垫,也不看真实静音,直接硬切。

两个任务,两个表象

Task A · Combo Continue

句尾差一个词

gw_e1f63c98-2436-492c-8010-135f853a91a6
04018196-7d0e-4d60-abd4-4a6bd375edf1 · run 6a7cad8c…

  • 9 段,问题在 segment 7,时间槽 44.5–52.0s。
  • 句尾:중독성이 진짜 장난 아닌데?
  • 父任务本来就同样缺词;continue 后仍少词尾。
  • 最终 result 记录 9 段改词。

Task B · Voiceover Align

词晚到下一段

gw_5c8101fb-f849-4e68-bb5d-1398747e85b0
7f9b3494-5dc2-4897-8aab-6298812be115 · run 41d8aa4b…

  • 11 段,问题跨 segment 9 → 10。
  • 句尾:아주 완벽하게 설치 완료!
  • 완료! 在 54.2s 没接上,却在 55.0s 左右出现。
  • 最终 result 记录 11 段改词。

问题 A:修过一次,为什么仍会少词

父任务和 continue 展示了两种失败。第一次是整词没时间戳;第二次有时间戳,但声学尾音仍越过 end。两种都被同一个硬切逻辑放大。

PARENT+449.75ms

父 take 在刀口后仍有非静音能量。整段被丢弃。

CONTINUE+108.73ms

新 take 的词尾仍越过 Fish end。

ATEMPO× 1.0000

问题段根本没调速,atempo 可排除。

父任务:`아닌데?` 整词不在 Fish 时间戳里源音频 41.195–51.195s
父任务:`아닌데?` 整词不在 Fish 时间戳里 波形图硬切 46.194989s能量到 46.64474s下一段 `드디어`
播放这段取证音频
continue:词有时间戳,但 end 仍早了 109ms源音频 42.026–52.026s
continue:词有时间戳,但 end 仍早了 109ms 波形图硬切 47.025737s能量到 47.13447s下一段 `드디어`
播放这段取证音频
最终混音:句尾在 50.980s 被截住最终轨 45.980–55.980s
最终混音:句尾在 50.980s 被截住 波形图对齐 end / 实际刀口 50.980s下一段从 52.000s 开始
播放这段取证音频
句尾词Fish / accepted 时间原始音频真实尾部刀口最终轨
진짜46.065737–46.385737s约 46.385737s47.025737s
factor 1.0000
50.020–50.340s
장난46.385737–46.705737s约 46.705737s50.340–50.660s
아닌데?46.705737–47.025737s能量到 47.134470s50.660–50.980s 后硬停;约 109ms 词尾消失
아닌데?(父任务)Fish 原始响应缺词;accepted 6.160–6.160s能量到 46.644740s46.194989s整词所在约 450ms 被丢弃

问题 B:不是同一份采样被搬走,而是下一段切片带进了上一句

每个段可以从三个整段 take 里各自选最合适的一份。segment 9 选 take C,segment 10 选 take D。C 在 설치 后硬切,丢掉自己的 완료!;D 的下一段切片却从 완료! 前就开始,所以又带进一个 완료!

SEGMENT 92.960s

완료! 被压成 0ms,刀口停在 설치 end。

SEGMENT 100.816009s

切片开始后,要过这么久才到本段第一个

FINAL TRACK+0.800s

54.2s 本该接上的词,约 55.0s 才出现。

段 9 的 take:`완료!` 在切点后还有声音源音频 43.784–53.211s
段 9 的 take:`완료!` 在切点后还有声音 波形图硬切 48.783537s能量到 49.15807s下一段 `자`
播放这段取证音频
段 10 的 take:开头先带着上一句 `완료!`源音频 44.206–53.603s
段 10 的 take:开头先带着上一句 `완료!` 波形图段 10 切片起点`완료!` 能量结束本段 `자` 才开始
播放这段取证音频
最终混音:词在下一段起点重新出现最终轨 49.200–59.200s
最终混音:词在下一段起点重新出现 波形图本该接在 54.200s 后实际 55.000s 出现约 55.333s 结束CTA `자` 55.713s
播放这段取证音频
句尾词seg9 take C 原始时间原始音频真实尾部seg9 刀口最终轨实际出现
완벽하게47.983537–48.463537s约 48.463537s48.783537s
factor 0.9250
53.335–53.854s
설치48.463537–48.783537s约 48.783537s53.854–54.200s
완료!Fish 原始响应缺词;accepted 2.960–2.960stake C 能量到 49.158070sseg9 没有;seg10 take D 在 55.000–55.333s 带入
자(seg10)take D: 50.021769–50.181769s本段切片起点 49.205760s;先有 381ms 上一句能量seg10 factor 1.144155.713–55.853s

代码怎么把“词时间”变成“音频刀口”

PROMPT整段韩语脚本9 / 11 段拼成一段 FISH TTS长音频 + 词时间会漏词,也会 end 偏早 NORMALIZE词时间拆成字时间漏掉的字压成 0ms BOUNDARY最后一个字的 end直接当成音频刀口 ATRIM硬切 + atempo没有尾垫,没有声学检查 SEGMENT段音频被切掉的采样回不来 TIMELINE按脚本时间摆放下一段可带前一段尾词 真正出错的区间:字幕时间被当成声学切点

关键判断:后面的 atempo 和时间轴只是处理已经切好的段。采样一旦被 atrim 丢掉,后面没有一步能把它恢复。

run_combo_workflow.py:1198

_fish_alignment_to_char 把 Fish 词时间铺到 prompt 字符。

run_combo_workflow.py:1183–1186

未覆盖字符沿用 previous end,于是漏掉的整词变成 0ms。

run_combo_workflow.py:1277–1279

最后一个字符的 end 直接成为 audio_end

run_combo_workflow.py:1287–1288

atrim=start:end 后马上 atempo,没有尾垫或能量检查。

run_combo_workflow.py:1501

选中的 whole take 按这个边界生成最终段。

run_combo_workflow.py:1604–1607

段音频按脚本 start 覆盖到时间轴;异常前导也会原样带过去。

任务 A:9 段原始切片 vs 调速后段音频

段 0–6、8 来自父任务缓存;段 7 是 continue 新生成。原始列按 Fish 对齐边界从对应 whole take 重切,候选时长与 accepted 记录逐段一致。

段 / 时间最终脚本时槽原始切片调速后原始可听最终段可听
000.0–2.5s 헉! 저기 봐, 석유 폭발 사고가 제대로 터졌잖아!parent take 0 (cache reused) 2.50s目标 2.15s 2.240spacket 2.273s 2.194s× 1.0419
整段 TTS 原始切片
调速后最终段
012.5–7.5s 이건 완전 돈 냄새가 폴폴 나는데? [slight pause] 이런 기회를 놓치면 절대 안 되지, 당장 시작해 보자고!parent take 0 (cache reused) 5.00s目标 4.65s 4.961spacket 4.989s 4.676s× 1.0669
整段 TTS 原始切片
调速后最终段
027.5–12.0s 자, 보트를 타고 바다 위로 쭉 나가서 [slight pause] 저 기름을 싹 다, 한 방울도 남김없이 빨아들이는 거야!parent take 0 (cache reused) 4.50s目标 4.15s 4.160spacket 4.206s 4.180s× 1.0024
整段 TTS 原始切片
调速后最终段
0312.0–18.0s 손님들 진정하세요, 다들 진정하세요! [slight pause] 지금부터 공짜 기름 주유 서비스 들어갑니다, 마음껏 가득 받아 가세요!parent take 1 (cache reused) 6.00s目标 5.65s 5.440spacket 5.486s 5.486s× 1.0000
整段 TTS 原始切片
调速后最终段
0418.0–27.5s 와, 이것 좀 봐, 현금이 아주 폭포처럼 쏟아지고 있잖아! [long pause] 아무도 안 쓰던 버려진 기름으로 이렇게 돈을 쓸어 담다니, [slight pause] 이게 바로 진짜 창조 경제 달성 아니겠어?parent take 1 (cache reused) 9.50s目标 9.15s 7.760spacket 7.811s 8.333s× 0.9349
整段 TTS 原始切片
调速后最终段
0527.5–36.5s 이렇게 번 돈으로 보트 용량을 아낌없이 풀업그레이드 해주고, [long pause] 자, 이 어마어마하게 거대한 기름 더미도 이제 단 한 방에 싹 다 정리해 버리는 거야!parent take 1 (cache reused) 9.00s目标 8.65s 7.520spacket 7.549s 7.837s× 0.9641
整段 TTS 原始切片
调速后最终段
0636.5–44.5s 기름통 스케일이 이제 완전 대박으로 커진 거 다들 보이지? [long pause] 그럼 이 기세를 몰아서, 바로 옆에 세차장까지 한번 시원하게 오픈해 볼까?parent take 1 (cache reused) 8.00s目标 7.65s 6.629spacket 6.661s 6.818s× 0.9749
整段 TTS 原始切片
调速后最终段
0744.5–52.0s 들어오는 차들 땟물까지 쫙쫙 빼주면서 [slight pause] 현금을 아주 그냥 쓸어 담는 거야! 와, 이거 직접 해보면 중독성이 진짜 장난 아닌데? [slight pause]continue take 1 7.50s目标 7.15s 6.480spacket 6.531s 6.504s× 1.0000
整段 TTS 原始切片
调速后最终段
0852.0–59.0s 드디어 자동 정유소 완공! [slight pause] 이제부터는 그냥 누워서 편하게 떼돈 버는 거야! 더 고민하지 말고, 지금 당장 다운로드해봐!parent take 1 (cache reused) 7.00s目标 6.65s 5.920spacket 5.956s 5.956s× 1.0000
整段 TTS 原始切片
调速后最终段

任务 B:11 段原始切片 vs 调速后段音频

segment 0–7、9–10 来自第二轮 whole-content take;segment 8 来自最后一轮单段修订。两个问题段分别选了不同 take,这正是“词晚到下一段”的原因。

段 / 时间最终脚本时槽原始切片调速后原始可听最终段可听
000.0–3.2s [shocked] 헐, 대박! 저기 유전이 갑자기 펑 하고 폭발했잖아요!taskB-r2-c 3.20s目标 2.85s 2.880spacket 2.926s 2.900s× 1.0105
整段 TTS 原始切片
调速后最终段
013.2–7.0s [excited] 자, 그럼 우리가 빨리 출동해서 저 기름 싹 다 치워주자고요!taskB-r2-c 3.80s目标 3.45s 3.040spacket 3.082s 3.082s× 1.0000
整段 TTS 原始切片
调速后最终段
027.0–12.0s [cheerful] 요 작은 보트로 기름을 쓱싹 빨아들이기만 하면요, 넓은 바다 청소도 완전 순식간에 끝!taskB-r2-d 5.00s目标 4.65s 4.480spacket 4.519s 4.519s× 1.0000
整段 TTS 原始切片
调速后最终段
0312.0–16.0s [excited] 자, 이렇게 모은 기름으로 이번엔 나만의 주유소를 딱 오픈해 볼까요!taskB-r2-c 4.00s目标 3.65s 3.775spacket 3.814s 3.683s× 1.0344
整段 TTS 原始切片
调速后最终段
0416.0–20.0s [playful] 어때요, 벌써부터 손님들이 저렇게 길게 줄 서는 거 다들 보이죠?taskB-r2-c 4.00s目标 3.65s 3.280spacket 3.318s 3.318s× 1.0000
整段 TTS 原始切片
调速后最终段
0520.0–28.0s [excited] 자, 여기서 이렇게 쫙 벌어들인 돈으로 곧바로 우리 장비를 시원하게 쫙 업그레이드하고, 더 넓은 저 바다로 다시 한번 힘차게 출발해 볼까요!taskB-r2-a 8.00s目标 7.65s 7.803spacket 7.837s 7.680s× 1.0200
整段 TTS 原始切片
调速后最终段
0628.0–38.0s [amazed] 와, 다들 보세요! 보트 용량이 이렇게 확 커진 거 진짜 완전 대박이죠! 넓은 바다도 순식간에 아주 깨끗하게 청소하고, 돈도 계속해서 무한으로 복사되는 이거 진짜 갓겜 아니냐고요, 다들 인정하죠?taskB-r2-a 10.00s目标 9.65s 10.281spacket 10.318s 9.691s× 1.0654
整段 TTS 原始切片
调速后最终段
0738.0–44.0s [excited] 와, 저기 좀 봐요! 저렇게 우수수 쏟아지는 돈다발들 좀 보라니까요! 이 손맛 진짜 쾌감 완전 쩔어요!taskB-r2-c 6.00s目标 5.65s 5.644spacket 5.695s 5.669s× 1.0000
整段 TTS 原始切片
调速后最终段
0844.0–51.0s [confident] 자, 그럼 이번엔 여기다가 반짝반짝 세차장까지 쫙 지어 올려서, 우리 수익을 아주 확실하게 두 배로 쭉쭉 늘려 볼 거예요!taskB-run0 7.00s目标 6.65s 5.840spacket 5.878s 5.878s× 1.0000
整段 TTS 原始切片
调速后最终段
0951.0–55.0s [excited] 자, 이제 저 거대한 자동 펌프까지 아주 완벽하게 설치 완료!taskB-r2-c 4.00s目标 3.65s 2.960spacket 3.004s 3.213s× 0.9250
整段 TTS 原始切片
调速后最终段
1055.0–59.0s [excited] 자, 여러분도 이제 더 고민하지 말고, 지금 바로 이 게임 한번 시작해 보세요!taskB-r2-d 4.00s目标 3.65s 4.176spacket 4.206s 3.683s× 1.1441
整段 TTS 原始切片
调速后最终段

修复方向:别再给时间戳补丁,要换掉切片准则

路径怎么做判断
Conservative path所有段尾固定加 200ms padding。能缓解 109ms 尾音,但救不了整词漏 timestamp,还可能把下一词叠进来。不能单独采用。
Clean target词时间只服务字幕;切片边界由词覆盖检查 + 相邻词空档 + 实际能量共同决定。accepted 同时保存 raw alignment、chosen take、audio_start/end。正确目标。
Staged clean path先让非标点词缺失 fail closed,并把 end 延伸到安全静音;随后补全可追溯字段和回归样本。建议这样落地。

先做的三件事

  • 对 prompt 词和 Fish segments 做覆盖检查。非标点词缺失,不许静默填 0ms。
  • 在当前词 end 与下一词 start 之间找能量低谷;前一段延到真实收音结束。
  • 把 raw alignment、Unified task id、chosen take、绝对 cut 写进 accepted。

不要做的三件事

  • 不要靠追加 [slight pause] 当长期修复。当前 A 已证明它仍会 0ms。
  • 不要调 atempo 阈值。它不是根因。
  • 不要用 SRT “字都在”证明音频完整。字幕沿用的正是坏时间戳。

第一证明点:拿本页两条已存 whole take 重跑新切片器,아닌데?완료! 都应留在正确段,且相邻段不重复。

推翻条件:如果原始 whole TTS 里本来就没有该词,切片器救不回来,应转为 TTS 生成失败。本案不满足这个条件:波形和 B 的最终轨都证明词声学内容存在。

取证链

  • Tobatsu runs / events / outputs / artifact 全部来自生产只读 GET。
  • Unified task id 来自 agent.progress;终态 GET 给出 Fish 原始 alignment.segments
  • 每个 chosen take 由 runner 同款映射重放确认,candidate_duration 与 accepted 差值为 0。
  • 音频时长用 packet PTS 核对;波形用 showwavespic;尾部用 -35dB silencedetect
GET /tasks/<task_id>/runs
GET /tasks/<task_id>/events?limit=500
GET /tasks/<task_id>/artifact?path=outputs/voiceover/...
GET https://unified-generation-api.../task/<unified_task_id>

ffmpeg -i wholegen-source-1.mp3   -af "atrim=start=45.5:end=48.2,asetpts=PTS-STARTPTS,silencedetect=noise=-35dB:d=0.03" -f null -