Wjs Transcribing Audio — Wjs 音频转录
v1当用户拥有音频或视频,并希望获得带时间戳的源语言转录文本(SRT)时使用此功能。根据源语言路由 —— 中文默认使用 Volcano (豆包) ASR;其他语言(西班牙语、英语、葡萄牙语、法语、意大利语、日语、韩语等)使用 OpenAI Whisper API,具有词级时间戳和自组装的提示。输出带有标点符号边界的 SRT 文本,适合在屏幕上阅读。触发器 —— "转写"、"转成字幕"、"做 SRT"、"transcribe"、"make subtitles"、"speech to text"、"出字幕"。
运行时依赖
安装命令
点击复制技能文档
wjs-transcribing-audio 将口语音频转换为同语言的时间戳SRT。该技能在源语言SRT处停止,翻译到其他语言是下一个技能(/wjs-translating-subtitles)。何时使用:用户提供视频或音频文件,并希望获得源语言的转录/SRT。用户已经有翻译后的SRT,但缺少源SRT。用户请求“做SRT”/“make subtitles”/“出逐字稿”,但尚未请求翻译步骤。何时不使用:源语言SRT已经存在 → 直接跳转到/wjs-translating-subtitles。用户希望获得不同于口语语言的转录 → 先运行此技能,然后运行/wjs-translating-subtitles。用户只想要dub或burn-in → 如果SRT存在,则跳过;否则先运行此技能。
路由:哪个引擎 源语言 默认引擎 为什么 中文(zh-CN、zh-HK、zh-TW) Volcano(豆包)ASR 在中文方面比Whisper具有更高的准确率 —— 用户的首选 其他语言(es、en、pt、fr、it、ja、ko等) OpenAI Whisper API具有词级粒度 Whisper的多语言支持强大;词时间戳允许我们自己组装提示 离线/无API访问 本地openai-whisper(中等) 质量底线;相同的循环/块故障模式适用 对于中文,不要默认使用Whisper,除非用户明确要求或Volcano不可用。这是一个故意的路由决策 —— 参见用户的中文ASR优先级记忆。
OpenAI Whisper API路径(非中文和中文回退) 关键原则:不要请求response_format=srt。Whisper的cue分段在长篇独白(30秒blob提示)和安静的音频段(循环幻觉)中会失败。请求词级时间戳并自己组装提示 —— 后处理是确定性的且免费的。
为什么不使用response_format=srt 两个故障模式会破坏whisper-1 SRT输出在长内容上:30秒blob提示。在长篇独白中,whisper-1具有response_format=srt会发出一个覆盖整个30秒条件_on_previous_text窗口的提示。转录是正确的;但是时间戳对于屏幕阅读来说是不可用的。 在安静的音频尾部发生循环幻觉。greedy temperature=0在低能量音频上会产生“你如果不把拥抱浪费写在这上面,你很难的”重复50次。两者都源于让Whisper决定提示边界。解决方案:词级时间戳 + 自己的标点符号感知组装器。
调用API
- 压缩上传 —— 64kbps单声道MP3对于语音来说已经足够。
- 请求词级时间戳。不要请求response_format=srt。
惊喜:words[]中没有标点,segments[]不一致 Whisper的words[]数组通常不包含标点;每个条目都是一个裸词元,如“做”、“个”、“测”、“试”。标点符号仅存在于segments[]文本字段中。更糟糕的是,segments[]文本在同一文件的不同块中不一致地标注了标点:文件的第0块可能会发出285个不带标点的段(“做个测试”“你在”“呵呵”),每个段1-2秒;第7块可能会发出34个带标点的段,每个段14-30秒。两种行为都出现在同一个API响应中。因此,正确的配方是结合两者:使用segments[]来确定自然的暂停边界(已经与呼吸对齐),但将其视为原始输入传递给自己的提示组装器,该组装器使用词时间戳来分割任何过长的段。
提示组装配方 TARGET_DUR = 3.0 # 尽量使提示的长度达到此值 MAX_CUE_DUR = 5.0 # 永远不要超过此值 MAX_CHARS = 18 # ≈一行字体大小14的1080宽垂直屏幕 MAX_GAP = 1.0 # 静默阈值 → 强制提示边界 MIN_PIECE = 0.3 # 低于此值则与邻居合并 SPLIT_PUNCT = set(",。!?;,.;!?") # 步骤A:合并短的segments[]以接近TARGET_DUR(使用segments,而不是words —— Whisper的段边界已经与暂停对齐)。 def assemble(segments, offset): cues, buf = [], [] def flush(): if buf: cues.append((buf[0]["start"]+offset, buf[-1]["end"]+offset, "".join(s["text"].strip() for s in buf)))