#5603 保存字幕前格式化srt失败:file='D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4/en.srt'Tr

103.151* Posted at: 21 days ago

保存字幕前格式化srt失败:file='D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4/en.srt'
Traceback (most recent call last):
File "videotrans\task\_base.py", line 81, in _save_srt_target
File "videotrans\util\help_srt.py", line 255, in get_srt_from_list
ValueError: not enough values to unpack (expected 2, got 1)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "videotrans\task\only_one.py", line 46, in run
File "videotrans\task\trans_create.py", line 341, in recogn
File "videotrans\task\_base.py", line 86, in _save_srt_target
videotrans.configure.excepts.VideoTransError: 保存字幕前格式化srt失败:file='D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4/en.srt'

cfg=[TaskCfgVTT]当前工作模式: 翻译视频或音频 单视频模式
原始输入文件名: C:/Users/30453/Desktop/YouTubeDownloads/Minecraft but I get sent to PARKOUR PRISON.mp4,
输出结果保存到文件夹: D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4,
临时文件夹: D:/pyvideotrans/win-pyvideotrans-v4.03-0626/tmp/130388/ccbd2420ea
已选 清理已存在
已选 启用CUDA加速
未选 降噪
已选 识别说话人,最大说话人数量5
语音识别:WhisperX(本地API), model_name: distil-large-v3.5, 发音语言: 英语, 断句方式:LLM重新断句
翻译渠道:DeepSeek,原始语言:英语,目标语言:简体中文, 已选 发送完整字幕
配音渠道:Edge-TTS(免费), 角色:Yunxi(Male/CN), 配音语言:简体中文, 已选 二次语音识别
音量:+0%, 语速:+10%, 未选 音频加速, 未选 视频慢速
未选 移除字幕间空隙, 已选 强制对齐字幕和声音
字幕: 嵌入硬字幕
未选 分离人声与背景声, 手动添加了背景音频:C:/Users/30453/Desktop/Consistency.mp3

代理地址:http://127.0.0.1:7890

21 Replies

这个错误是由于语音识别返回的字幕数据格式异常,导致软件在拼接 SRT 文件时无法正确解析(遇到了既不是时间行也不是文本行的无效行)。

主要原因:
WhisperX(本地API) + LLM 重新断句 组合下,LLM 可能返回了格式错乱的内容(例如缺少时间戳、多出空行、或者将时间轴和文本合并到一行),导致 get_srt_from_list 函数在解包时发现数量不匹配。

解决方案:

  1. 立即尝试取消勾选“LLM重新断句”,使用默认断句方式重新执行。这是最直接的排查方法。
  2. 检查原始字幕文件:打开 D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4/en.srt,查看是否有异常行(例如某行只有时间戳没有文本,或只有文本没有时间戳),截图或复制异常内容。
  3. 检查 WhisperX 服务配置:确认本地 WhisperX API 接口返回的字幕数据是标准 SRT 格式(行号、时间轴、文本三行一组),没有额外的 JSON 包裹或格式错误。
  4. 临时替换渠道:将语音识别渠道改为 faster-whisper(本地内置) + large-v3 模型,关闭 LLM 断句,测试是否正常。如果正常,说明问题出在 WhisperX 或 LLM 断句上。

如果上述步骤无法解决:提供 en.srt 文件的前 20 行内容,以及 WhisperX 服务返回的原始日志片段,以便进一步分析。

保存字幕前格式化srt失败:file='D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4/en.srt'
Traceback (most recent call last):
File "videotrans\task\_base.py", line 81, in _save_srt_target
File "videotrans\util\help_srt.py", line 255, in get_srt_from_list
ValueError: not enough values to unpack (expected 2, got 1)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "videotrans\task\only_one.py", line 46, in run
File "videotrans\task\trans_create.py", line 341, in recogn
File "videotrans\task\_base.py", line 86, in _save_srt_target
videotrans.configure.excepts.VideoTransError: 保存字幕前格式化srt失败:file='D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4/en.srt'

cfg=[TaskCfgVTT]当前工作模式: 翻译视频或音频 单视频模式
原始输入文件名: C:/Users/30453/Desktop/YouTubeDownloads/Minecraft but I get sent to PARKOUR PRISON.mp4,
输出结果保存到文件夹: D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4,
临时文件夹: D:/pyvideotrans/win-pyvideotrans-v4.03-0626/tmp/130388/ccbd2420ea
已选 清理已存在
已选 启用CUDA加速
未选 降噪
已选 识别说话人,最大说话人数量5
语音识别:WhisperX(本地API), model_name: distil-large-v3.5, 发音语言: 英语, 断句方式:LLM重新断句
翻译渠道:DeepSeek,原始语言:英语,目标语言:简体中文, 已选 发送完整字幕
配音渠道:Edge-TTS(免费), 角色:Yunxi(Male/CN), 配音语言:简体中文, 已选 二次语音识别
音量:+0%, 语速:+10%, 未选 音频加速, 未选 视频慢速
未选 移除字幕间空隙, 已选 强制对齐字幕和声音
字幕: 嵌入硬字幕
未选 分离人声与背景声, 手动添加了背景音频:C:/Users/30453/Desktop/Consistency.mp3

代理地址:http://127.0.0.1:7890

ValueError: not enough values to unpack (expected 2, got 1) 说明 WhisperX(本地API) 返回的字幕数据中,某一行既不是标准时间轴(如 00:01,000 --> 00:03,000),也不是纯文本,导致解析时解包失败。这通常由LLM重新断句产生的格式错乱引起,或 WhisperX 服务本身返回了不规范的输出。

解决方案(按优先级尝试)

  1. 立即尝试:取消LLM重新断句
    在主界面将“断句方式”从“LLM重新断句”改为“默认断句”,然后重新执行任务。这是最直接的排查方法,因为LLM可能返回了拼合了时间轴的文本、多余空行或缺少行号。
  2. 检查并修正 en.srt 文件
    打开 D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4/en.srt,查看是否有以下异常行:

    • 某行只包含时间轴但没有文本(或相反)
    • 两行文本被错误合并到一行中
    • 行号重复或缺失
      如果存在此类问题,手动调整为标准格式(三行一组:序号、时间轴、文本,然后空行),重新保存后再运行。
  3. 更换ASR渠道
    将“语音识别渠道”改为 faster-whisper(本地内置) + large-v3 模型,取消“LLM重新断句”并保持“默认断句”,观察是否正常。如果正常,则问题出在 WhisperX 或 LLM 断句上。
  4. 检查 WhisperX 服务配置
    确保本地 WhisperX API 返回的是纯 SRT 格式文本,没有额外 JSON 包裹或调试信息。可尝试直接调用 WhisperX API 测试返回值是否格式正确。

如果以上步骤仍无法解决

请提供:

  • en.srt 文件的前 30 行 内容(粘贴到回复中)
  • WhisperX 服务端返回的原始日志(或控制台输出)

以便进一步定位。

请查阅相关文档:

我按你说的,关了llm断句啊

理解了。关闭LLM断句后问题依然存在,说明根源不在LLM断句,而在于 WhisperX(本地API) 返回的字幕内容本身格式异常,或者“说话人识别”功能干扰了SRT行解析。

请按以下步骤排查:

  1. 暂时关闭“识别说话人”
    在“设置更多参数”中,取消勾选“识别说话人”,然后重新执行任务。如果正常,说明是说话人识别添加的 [SPEAKER_XX] 标签或格式导致解析失败。
  2. 检查 en.srt 文件内容
    打开 D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4/en.srt,查看前30行内容。
    典型的异常情况

    • 某一行只有时间轴没有文本(或只有文本没有时间轴)
    • 两行文本错误地合并到一行(例如 [SPEAKER_00]Hello world 与时间戳放在同一行)
    • 行号丢失或乱序
      请将前30行内容粘贴出来,我帮你判断具体格式问题。
  3. 更换语音识别渠道
    暂时改用 faster-whisper(本地内置) + large-v3 渠道,保持其他设置不变(关闭LLM断句、关闭说话人识别),看是否能成功生成字幕。如果成功,则问题锁定在 WhisperX 服务或模型 distil-large-v3.5 上。
  4. 确认 WhisperX 服务返回值
    检查你的WhisperX本地服务日志,看是否返回了非标准SRT格式的数据(例如JSON包裹、空行过多等)。确保API返回的是纯文本SRT格式。
  5. 手动修复 en.srt 后重试
    如果急用,可以手动编辑 en.srt,将明显错乱的行修正为标准格式(三行一组:序号、时间轴、文本,然后空行)。保存后重新运行软件,软件会跳过ASR阶段直接进入后续流程(前提是文件路径正确)。

请先尝试 步骤1步骤2 ,提供结果后进一步分析。

关了说话人识别还是不行,D:/pyvideotrans/win-pyvideotrans-v4.03-0626/Minecraft but I get sent to PARKOUR PRISON-mp4/里面是空的,faster-whisper(本地内置) + large-v3没问题,但我就要用x

问题明确在 WhisperX(本地API) 服务返回的字幕格式不规范,导致软件解析失败(not enough values to unpack (expected 2, got 1))。由于 faster-whisper 正常,说明软件本身无问题,请检查和修复 WhisperX 服务端配置。

解决步骤(按顺序尝试):

  1. 直接测试 WhisperX 的 API 返回值
    打开浏览器或使用 curl 等工具,手动请求你的 WhisperX API 端点(例如 http://127.0.0.1:端口/transcribe 或类似路径),上传一段短音频,查看返回的原始文本。

    • 正常格式:标准 SRT(行号、时间轴、文本三行一组,空行分隔)。
    • 错误格式:JSON 包裹、多余调试信息、时间轴和文本合并到一行、缺失行号等。
    • 若返回 JSON,需要在 WhisperX 服务配置中设置 output_format=srt 或类似参数(参考其文档)。
  2. 更换 WhisperX 模型
    distil-large-v3.5 是蒸馏模型,可能与你的 WhisperX 版本不完全兼容。换成 large-v3base 测试。

    • 如果换模型后正常,则原模型文件损坏或与 WhisperX 版本不匹配。
  3. 检查 WhisperX 服务日志
    查看运行 WhisperX 的控制台窗口或日志文件,确认在转录过程中有无显存不足、模型加载失败、格式转换报错等信息。
  4. 关闭“说话人识别”相关全局选项
    即使主界面未勾选,检查 WhisperX 服务本身的配置是否启用了说话人分离。

    • 某些 WhisperX 部署版默认会添加说话人标签(如 [SPEAKER_00]),即使软件未启用,WhisperX 返回的数据中仍可能夹杂这些标记,导致解析错乱。
    • 请在 WhisperX 命令行或配置文件中添加 --no_diarizationdiarize=False 参数。
  5. 临时回退到标准 WhisperX 端点
    如果你的 WhisperX 服务使用了自定义输出格式(如 word_timestamps=True 导致每行包含单词级时间戳),请关闭此选项,保持标准 SRT 输出。

如果上述步骤仍无法解决,请提供以下信息以便精准定位:

  • 你用 curl 测试返回的原始完整前30行文本(不是日志,是 API 返回的 body)。
  • WhisperX 服务的启动命令或配置文件内容(隐藏密钥部分)。

2026-07-03 14:44:52,108 - INFO - 模型 'base' 加载成功。
开始转录
转录结束,准备对齐
2026-07-03 14:45:06,580 - INFO - 检测到的说话人: set()。单说话人模式: 是
{'segments': [{'start': 0.031, 'end': 0.572, 'text': " Let's go!", 'words': [{'word': "Let's", 'start': np.float64(0.031), 'end': np.float64(0.331), 'score': np.float64(0.665)}, {'word': 'go!', 'start': np.float64(0.372), 'end': np.float64(0.572), 'score': np.float64(0.995)}]}, {'start': 0.612, 'end': 1.233, 'text': 'Open up!', 'words': [{'word': 'Open', 'start': np.float64(0.612), 'end': np.float64(1.033), 'score': np.float64(0.884)}, {'word': 'up!', 'start': np.float64(1.133), 'end': np.float64(1.233), 'score': np.float64(0.927)}]}, {'start': 1.353, 'end': 2.215, 'text': "It's time for parkour.", 'words': [{'word': "It's", 'start': np.float64(1.353), 'end': np.float64(1.453), 'score': np.float64(0.858)}, {'word': 'time', 'start': np.float64(1.473), 'end': np.float64(1.614), 'score': np.float64(0.891)}, {'word': 'for', 'start': np.float64(1.654), 'end': np.floa
......
e barrier is if they've earned a ticket.", 'words': [{'word': 'The', 'start': np.float64(64.684), 'end': np.float64(64.784), 'score': np.float64(0.822)}, {'word': 'inside', 'start': np.float64(64.884), 'end': np.float64(65.205), 'score': np.float64(0.985)}, {'word': 'of', 'start': np.float64(65.265), 'end': np.float64(65.305), 'score': np.float64(1.0)}, {'word': 'the', 'start': np.float64(65.325), 'end': np.float64(65.425), 'score': np.float64(0.77)}, {'word': 'temple', 'start': np.float64(65.485), 'end': np.float64(65.805), 'score': np.float64(0.806)}, {'word': 'is', 'start': np.float64(65.906), 'end': np.float64(65.966), 'score': np.float64(0.738)}, {'word': 'protected', 'start': np.float64(66.026), 'end': np.float64(66.486), 'score': np.float64(0.892)}, {'word': 'by', 'start': np.float64(66.546), 'end': np.float64(66.666), 'score': np.float64(0.964)}, {'word': 'a', 'start': np.float64(66.707), 'end': np.float64(66.747), 'score': np.float64(0.002)}, {'word': 'barrier,', 'start': np.float64(66.827), 'end': np.float64(67.207), 'score': np.float64(0.948)}, {'word': 'and', 'start': np.float64(67.307), 'end': np.float64(67.367), 'score': np.float64(0.965)}, {'word': 'the', 'start': np.float64(67.387), 'end': np.float64(67.488), 'score': np.float64(0.667)}, {'word': 'only', 'start': np.float64(67.588), 'end': np.float64(67.768), 'score': np.float64(0.632)}, {'word': 'way', 'start': np.float64(67.828), 'end': np.float64(67.968), 'score': np.float64(0.867)}, {'word': 'a', 'start': np.float64(68.028), 'end': np.float64(68.048), 'score': np.float64(0.891)}, {'word': 'parkour', 'start': np.float64(68.108), 'end': np.float64(68.529), 'score': np.float64(0.695)}, {'word': 'noob', 'start': np.float64(68.549), 'end': np.float64(68.769), 'score': np.float64(0.614)}, {'word': 'gets', 'start': np.float64(68.829), 'end': np.float64(68.989), 'score': np.float64(0.854)}, {'word': 'past', 'start': np.float64(69.089), 'end': np.float64(69.37), 'score': np.float64(0.874)}, {'word': 'the', 'start': np.float64(69.41), 'end': np.float64(69.49), 'score': np.float64(0.942)}, {'word': 'barrier', 'start': np.float64(69.53), 'end': np.float64(69.931), 'score': np.float64(0.791)}, {'word': 'is', 'start': np.float64(70.071), 'end': np.float64(70.131), 'score': np.float64(0.839)}, {'word': 'if', 'start': np.float64(70.231), 'end': np.float64(70.291), 'score': np.float64(0.994)}, {'word': "they've", 'start': np.float64(70.331), 'end': np.float64(70.511), 'score': np.float64(0.83)}, {'word': 'earned', 'start': np.float64(70.611), 'end': np.float64(70.792), 'score': np.float64(0.79)}, {'word': 'a', 'start': np.float64(70.872), 'end': np.float64(70.912), 'score': np.float64(0.976)}, {'word': 'ticket.', 'start': np.float64(70.952), 'end': np.float64(71.372), 'score': np.float64(0.798)}]}, {'start': 71.392, 'end': 75.938, 'text': "I've never even tried getting a ticket before, but if I'm gonna rank up to a parkour pro one day,", 'words': [{'word': "I've", 'start': np.float64(71.392),

问题根源:你的 WhisperX(本地API) 服务返回的是完整的 JSON 对象(包含 segments、words 等),但 pyVideoTrans 的 WhisperX 渠道预期接收的是 标准 SRT 纯文本格式(行号、时间轴、文本三行一组)。当前 JSON 格式无法被解析,导致 get_srt_from_list 解包失败。解决方案:在启动 WhisperX 服务时,设置输出格式为 SRT。根据你的部署方式选择对应命令:若通过 CLI 启动(如 whisperx --output_format srt) 在原命令后添加 --output_format srt,例如:

whisperx audio.wav --model base --output_format srt

如果是通过 API 服务器(如 gradio 或 fastapi 封装),需要修改服务端代码,在返回前将 JSON 转换为 SRT 字符串,或者调用 whisperx 内部函数时指定 output_format="srt"。如果当前使用的是 Docker 或第三方整合包,请查阅其文档找到输出格式设置,设为 srt 或 plaintext。临时验证法: 打开浏览器访问你的 WhisperX API 端点(例如 http://127.0.0.1:端口/transcribe),上传一段短音频,查看返回的原始内容。若仍为 JSON,说明服务未按 SRT 输出,需按上述步骤修正。如果无法修改服务端格式,请改用软件内置的 faster-whisper(已验证正常),或者尝试其他支持 SRT 输出的本地 ASR 渠道(如 openai-whisper 本地内置)。请调整 WhisperX 服务输出为 SRT 格式后重新运行,问题应能解决。

uv run app.py我是这样启动的,按照https://pyvideotrans.com/whisperx-api#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95,这里的说法,用API调用的

问题在于你的 WhisperX API 服务(uv run app.py)默认以 JSON 格式返回识别结果,而不是 pyVideoTrans 期望的标准 SRT 纯文本格式。你需要修改服务端代码,将返回体改为 SRT 字符串。修改步骤(以常见的 Flask/FastAPI 为例):找到 API 处理函数(通常是接收音频并返回转录结果的那个路由)。在返回前,将 segments 列表转换为 SRT 格式:pythondef segments_to_srt(segments): lines = [] for i, seg in enumerate(segments, 1): start = seg['start'] end = seg['end'] text = seg['text'].strip() # 格式化时间轴(HH:MM:SS,mmm) start_srt = f"{int(start//3600):02d}:{int(start%3600//60):02d}:{start%60:06.3f}".replace('.', ',') end_srt = f"{int(end//3600):02d}:{int(end%3600//60):02d}:{end%60:06.3f}".replace('.', ',') lines.append(f"{i}\n{start_srt} --> {end_srt}\n{text}\n") return '\n'.join(lines)然后将原本的 return {"segments": segments} 改为 return segments_to_srt(segments),并确保响应头设置 Content-Type: text/plain。重启服务后,回归测试:用手动请求(curl 或浏览器)上传音频,检查返回的 body 是否为标准 SRT 文本。如果不想修改代码,还有一个更简单的变通方案:在 pyVideoTrans 中暂时换用其他支持 SRT 输出的本地 ASR 渠道(如 faster-whisper(本地内置)),待确认 WhisperX 服务修改后再切回。请修改 WhisperX 服务端返回格式为纯文本 SRT 后重试,问题即可解决。

怎么搞,教我

请打开你的 app.py 文件,找到接收音频并返回结果的 API 路由函数(通常是 @app.post("/transcribe") 或类似装饰器下的 async def transcribe())。

在函数内部,在得到 segments 列表之后、return 之前,添加一个将 segments 转换为 SRT 字符串的函数,并将返回类型改为纯文本。

以下是修改步骤和完整示例:


1. 添加 SRT 转换函数(放在文件顶部附近)

def segments_to_srt(segments):
    lines = []
    for i, seg in enumerate(segments, 1):
        start = seg['start']
        end = seg['end']
        text = seg['text'].strip()
        # 格式化为 HH:MM:SS,mmm
        start_srt = f"{int(start//3600):02d}:{int(start%3600//60):02d}:{start%60:06.3f}".replace('.', ',')
        end_srt = f"{int(end//3600):02d}:{int(end%3600//60):02d}:{end%60:06.3f}".replace('.', ',')
        lines.append(f"{i}\n{start_srt} --> {end_srt}\n{text}\n")
    return '\n'.join(lines)

2. 修改 API 路由函数的返回部分

假设原来返回的是类似这样的 JSON:

return {"segments": segments}

将其改为:

from fastapi.responses import PlainTextResponse  # 如果尚未导入

# ... 在函数内部,获得 segments 后
srt_content = segments_to_srt(segments)
return PlainTextResponse(srt_content)

如果使用的是 Flask,则:

from flask import Response

# ... 
srt_content = segments_to_srt(segments)
return Response(srt_content, mimetype='text/plain')

3. 完整示例(以 FastAPI 为例)

from fastapi import FastAPI, UploadFile, File
from fastapi.responses import PlainTextResponse
import whisperx

app = FastAPI()

def segments_to_srt(segments):
    lines = []
    for i, seg in enumerate(segments, 1):
        start = seg['start']
        end = seg['end']
        text = seg['text'].strip()
        start_srt = f"{int(start//3600):02d}:{int(start%3600//60):02d}:{start%60:06.3f}".replace('.', ',')
        end_srt = f"{int(end//3600):02d}:{int(end%3600//60):02d}:{end%60:06.3f}".replace('.', ',')
        lines.append(f"{i}\n{start_srt} --> {end_srt}\n{text}\n")
    return '\n'.join(lines)

@app.post("/transcribe")
async def transcribe(audio: UploadFile = File(...)):
    # 你的 WhisperX 处理逻辑...
    # 得到 segments 列表
    result = whisperx.transcribe(...)  # 假设 result 包含 segments
    segments = result['segments']
    
    srt_content = segments_to_srt(segments)
    return PlainTextResponse(srt_content)

4. 重启服务并验证

修改保存后,重新运行 uv run app.py。然后打开浏览器,访问你的 API 端点(例如 http://127.0.0.1:8000/docs 用 Swagger 测试),或者用 curl 测试:

curl -X POST -F "[email protected]" http://127.0.0.1:8000/transcribe

返回内容应为标准 SRT 纯文本,例如:

1
00:00:00,031 --> 00:00:00,572
Let's go!

2
00:00:00,612 --> 00:00:01,233
Open up!

...

如果返回仍然是 JSON,请检查是否正确导入了 PlainTextResponseResponse,并确保没有其他中间件修改响应格式。

完成以上修改后,回到 pyVideoTrans 重新执行任务,问题即可解决。

import os
import tempfile
import torch
import whisperx
from flask import Flask, request, jsonify, render_template
from waitress import serve
import logging
import webbrowser
from threading import Timer
import shutil
import sys
import ffmpeg
from whisperx.diarize import DiarizationPipeline


全局配置与初始化

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def get_hf_token():

"""
获取 Hugging Face 令牌。
优先从当前目录的 'token.txt' 文件读取,如果失败则从环境变量 'HUGGING_FACE_TOKEN' 读取。
"""
token = None
token_file = 'token.txt'
if os.path.exists(token_file):
    try:
        with open(token_file, 'r', encoding='utf-8') as f:
            token = f.read().strip()
        if token:
            logging.info(f"成功从 {token_file} 文件中读取 Hugging Face 令牌。")
            return token
    except Exception as e:
        logging.warning(f"无法从 {token_file} 读取令牌: {e}")

token = os.environ.get("HUGGING_FACE_TOKEN")
if token:
    logging.info("成功从环境变量中读取 Hugging Face 令牌。")
else:
    logging.warning("在 token.txt 或环境变量中均未找到 Hugging Face 令牌。说话人分离功能将被禁用。")
return token

HF_TOKEN = get_hf_token()

设备和计算类型配置

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
COMPUTE_TYPE = "float16" if torch.cuda.is_available() else "int8"
BATCH_SIZE = 16

logging.info(f"使用设备: {DEVICE},计算类型: {COMPUTE_TYPE}")

模型配置

ALLOWED_MODELS = ['tiny', 'base', 'small', 'medium', 'large-v1', 'large-v2', 'large-v3', 'large-v3-turbo']
DEFAULT_MODEL = 'large-v3-turbo'

模型缓存

whisper_models_cache = {}
diarize_model = None
diarize_model_loaded = False

def get_whisper_model(model_name: str):

if model_name not in whisper_models_cache:
    logging.info(f"正在加载 Whisper 模型 '{model_name}'...")
    try:
        model = whisperx.load_model(model_name, DEVICE, compute_type=COMPUTE_TYPE)
        whisper_models_cache[model_name] = model
        logging.info(f"模型 '{model_name}' 加载成功。")
    except Exception as e:
        logging.error(f"加载 Whisper 模型 '{model_name}' 失败: {e}")
        if str(e).find('huggingface'):
            print(f"\n\n=======可能模型下载失败,请尝试科学上网后再次重试=======\n\n")
        raise
return whisper_models_cache[model_name]

def get_diarize_model():

global diarize_model, diarize_model_loaded


if not diarize_model_loaded:
    logging.info("正在尝试加载说话人分离模型...")
    if not HF_TOKEN:
        return None
    try:
        diarize_model = DiarizationPipeline(use_auth_token=HF_TOKEN, device=DEVICE)
        diarize_model_loaded = True
        logging.info("说话人分离模型加载成功。")
    except Exception as e:
        logging.error(f"严重错误: 说话人分离模型加载失败。此功能将被禁用。错误信息: {e}")
        diarize_model = None 
        diarize_model_loaded = True
return diarize_model


Flask 应用

app = Flask(__name__, template_folder='.')

@app.route('/', methods=['GET'])
def index():

return render_template('index.html')

@app.route('/v1/audio/transcriptions', methods=['POST'])
def audio_transcriptions():

if 'file' not in request.files:
    return jsonify({"error": "请求中未包含文件部分"}), 400
file = request.files['file']
if file.filename == '':
    return jsonify({"error": "未选择任何文件"}), 400

print(request.form)
model_id = request.form.get('model', DEFAULT_MODEL)
model_name = model_id
if model_name not in ALLOWED_MODELS:
    model_name = DEFAULT_MODEL

请按照以下步骤修改你的 app.py 文件,让 API 返回 SRT 纯文本而非 JSON。

第一步:添加导入语句
在文件顶部 from flask import Flask, request, jsonify, render_template 这一行的末尾加上 , Response(或者另起一行写 from flask import Response)。

第二步:添加 SRT 转换函数
app = Flask(__name__) 之前任意位置插入以下函数:

def segments_to_srt(segments):
    lines = []
    for i, seg in enumerate(segments, 1):
        start = seg['start']
        end = seg['end']
        text = seg['text'].strip()
        # 时间格式 HH:MM:SS,mmm
        start_srt = f"{int(start//3600):02d}:{int(start%3600//60):02d}:{start%60:06.3f}".replace('.', ',')
        end_srt = f"{int(end//3600):02d}:{int(end%3600//60):02d}:{end%60:06.3f}".replace('.', ',')
        lines.append(f"{i}\n{start_srt} --> {end_srt}\n{text}\n")
    return '\n'.join(lines)

第三步:修改 audio_transcriptions 函数的返回部分
找到这个函数的末尾(一般在处理完识别、对齐等之后,有类似 return jsonify(...) 的地方)。假设原来返回的是:

return jsonify({"segments": segments})

或者

return jsonify(result)

请将这一行替换为:

srt_content = segments_to_srt(segments)  # 假设变量 namesegments 就是最终的段落列表
return Response(srt_content, mimetype='text/plain')
如果代码中 segments 变量名不是 segments(比如是 result['segments']align_segments),请根据实际变量名调整。

第四步:重启服务
保存文件后,重新运行 uv run app.py。然后再用 pyVideoTrans 重新执行任务,应该就不会再报“格式化srt失败”的错误了。

如果还有问题,请检查你的 audio_transcriptions 函数中 segments 变量是否存在,并贴出该函数完整的返回部分代码(最后几行),我可以帮你精确修改。

import os
import tempfile
import torch
import whisperx
from flask import Flask, request, jsonify, render_template
from flask import Response
from waitress import serve
import logging
import webbrowser
from threading import Timer
import shutil
import sys
import ffmpeg
from whisperx.diarize import DiarizationPipeline


全局配置与初始化

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def get_hf_token():

"""
获取 Hugging Face 令牌。
优先从当前目录的 'token.txt' 文件读取,如果失败则从环境变量 'HUGGING_FACE_TOKEN' 读取。
"""
token = None
token_file = 'token.txt'
if os.path.exists(token_file):
    try:
        with open(token_file, 'r', encoding='utf-8') as f:
            token = f.read().strip()
        if token:
            logging.info(f"成功从 {token_file} 文件中读取 Hugging Face 令牌。")
            return token
    except Exception as e:
        logging.warning(f"无法从 {token_file} 读取令牌: {

......
(end//3600):02d}:{int(end%3600//60):02d}:{end%60:06.3f}".replace('.', ',')

    lines.append(f"{i}\n{start_srt} --> {end_srt}\n{text}\n")
return '\n'.join(lines)


Flask 应用

app = Flask(__name__, template_folder='.')

@app.route('/', methods=['GET'])
def index():

return render_template('index.html')

@app.route('/v1/audio/transcriptions', methods=['POST'])
def audio_transcriptions():

if 'file' not in request.files:
    return jsonify({"error": "请求中未包含文件部分"}), 400
file = request.files['file']
if file.filename == '':
    return jsonify({"error": "未选择任何文件"}), 400

print(request.form)
model_id = request.form.get('model', DEFAULT_MODEL)
model_name = model_id
if model_name not in ALLOWED_MODELS:
    model_name = DEFAULT_MODEL

language = request.form.get('language') or None
prompt = request.form.get('prompt')
max_speakers=int(request.form.get('max_speakers',-1))
min_speakers=int(request.form.get('min_speakers',0))

logging.info(f"收到请求: 模型='{model_id}', 语言='{language or '自动检测'}', 提示词='{'有' if prompt else '无'}'")

input_file_path = None
processed_wav_path = None
try:
    suffix = os.path.splitext(file.filename)[1]
    with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
        file.save(tmp.name)
        input_file_path = tmp.name

    logging.info(f"正在将上传的文件 '{file.filename}' 转换为标准的 16kHz 单声道 WAV 格式...")
    with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_wav:
        processed_wav_path = tmp_wav.name
    
    try:
        (
            ffmpeg
            .input(input_file_path)
            .output(processed_wav_path, ac=1, ar=16000, acodec='pcm_s16le', vn=None)
            .run(capture_stdout=True, capture_stderr=True, overwrite_output=True)
        )
        logging.info("文件格式转换成功。")
    except ffmpeg.Error as e:
        error_details = e.stderr.decode('utf-8', errors='ignore')
        logging.error(f"FFmpeg 文件转换失败: {error_details}")
        return jsonify({"error": f"音频/视频文件处理失败,可能是文件已损坏或格式不受支持。"}), 400

    audio = whisperx.load_audio(processed_wav_path)
    model = get_whisper_model(model_name)
    
    # ---
    # *** FIX IS HERE ***
    # ---
    transcribe_options = {}
    if language:
        transcribe_options['language'] = language
    if prompt:
        # 使用正确的参数名 'prompt'
        transcribe_options['prompt'] = prompt
    print('开始转录')
    result = model.transcribe(audio, batch_size=BATCH_SIZE, **transcribe_options)
    print('转录结束,准备对齐')        
    model_a, metadata = whisperx.load_align_model(language_code=result["language"], device=DEVICE)
    result = whisperx.align(result["segments"], model_a, metadata, audio, DEVICE, return_char_alignments=False)
    

然后呢,第三部怎么搞,前面的对么

你的代码修改到目前为止是正确的:已添加 from flask import Response,也已在 segments_to_srt 函数定义。现在只需完成第三步:将返回部分改为 SRT 纯文本。

在你的 audio_transcriptions 函数的 try 代码块中,在执行完对齐后,需要获取对齐后的 segments 并调用 segments_to_srt,最后用 Response 返回。你的代码目前对齐后只赋值给了 result,但没有后续处理。请找到对齐后的这行:

result = whisperx.align(result["segments"], model_a, metadata, audio, DEVICE, return_char_alignments=False)

在这行之后,添加以下代码(替换原来的返回逻辑):

    # 将 segments 转换为 SRT 字符串
    segments = result["segments"]
    srt_content = segments_to_srt(segments)
    return Response(srt_content, mimetype='text/plain')

同时,请检查整个 try 块中是否还有任何 return jsonify(...) 的语句(尤其是异常处理部分),确保异常情况仍然返回 JSON 错误(保留原有逻辑),但正常流程返回 SRT 纯文本。

修改后关键部分示意图:

try:
    # ... 之前的转换、识别、对齐代码 ...
    result = whisperx.align(result["segments"], model_a, metadata, audio, DEVICE, return_char_alignments=False)
    
    # ======= 新增返回 SRT 的代码 =======
    segments = result["segments"]
    srt_content = segments_to_srt(segments)
    return Response(srt_content, mimetype='text/plain')

except Exception as e:
    # 异常处理保持原样,返回 JSON 错误
    logging.error(...)
    return jsonify({"error": str(e)}), 500

最后,在 finally 块中(如果你的代码有清理工作),继续保留。

保存并重启服务,然后用 pyVideoTrans 重试之前失败的任务,应该不会再出现“格式化srt失败”的错误。

如果你不确定 segments 的变量名是否正确,可以在 return 前加一行 print(segments) 确认格式。完成修改后请运行测试,如仍有问题请将修改后的完整返回部分代码粘贴过来。

    
    # ---
    # *** FIX IS HERE ***
    # ---
    transcribe_options = {}
    if language:
        transcribe_options['language'] = language
    if prompt:
        # 使用正确的参数名 'prompt'
        transcribe_options['prompt'] = prompt
    print('开始转录')
    result = model.transcribe(audio, batch_size=BATCH_SIZE, **transcribe_options)
    print('转录结束,准备对齐')        
    model_a, metadata = whisperx.load_align_model(language_code=result["language"], device=DEVICE)
    result = whisperx.align(result["segments"], model_a, metadata, audio, DEVICE, return_char_alignments=False)
    
    if max_speakers>-1:
        print('进入说话人识别')
        diar_model = get_diarize_model()
        if diar_model:
            try:
                diarize_segments = diar_model(audio,max_speakers=max_speakers if max_speakers>0 else None,min_speakers=min_speakers if min_speakers>0 else None)
                result = whisperx.assign_word_speakers(diarize_segments, result)
            except Exception as e:
                logging.error(f"说话人分离运行时失败: {e}。将回退到单说话人模式。")
    
    speakers = {segment.get('speaker') for segment in result["segments"] if 'speaker' in segment}
    is_single_speaker = len(speakers) 

你的前面步骤(导入 Response、定义 segments_to_srt)都已正确完成,现在只需在说话人分离处理后添加最后的返回转换。

请在 if max_speakers > -1: 整个段落结束后(即 setup speakers 和 is_single_speaker 计算后),将原本返回 jsonify(...) 的地方替换为下面两行:

srt_content = segments_to_srt(result["segments"])
return Response(srt_content, mimetype='text/plain')

完整修改后的关键代码段如下(位于 try 块内,紧接在你已贴出的 speakersis_single_speaker 计算之后):

    # 上面是你的说话人分离处理(保持不变)
    # ...

    # 计算 speakers 和 is_single_speaker(保持不变)
    speakers = {segment.get('speaker') for segment in result["segments"] if 'speaker' in segment}
    is_single_speaker = len(speakers) <= 1

    # ---- 新增:返回 SRT 纯文本 ----
    srt_content = segments_to_srt(result["segments"])
    return Response(srt_content, mimetype='text/plain')

额外提醒:
检查你的 segments_to_srt 函数中时间轴箭头的写法。你粘贴的代码里出现了 --&amp;gt;,这在程序中会被理解为错误。正确写法是 -->(两个短横加一个大于号)。如果文件中实际写错了,请修改为:

lines.append(f"{i}\n{start_srt} --> {end_srt}\n{text}\n")

保存后重新运行 uv run app.py,再回到 pyVideoTrans 执行之前失败的任务即可。

如果仍有问题,请提供修改后 audio_transcriptions 函数的完整返回部分(从 # ---- 新增:返回 SRT 纯文本 ---- 起往后的所有代码),以便进一步检查。

    
    speakers = {segment.get('speaker') for segment in result["segments"] if 'speaker' in segment}
    is_single_speaker = len(speakers) 
Post Your Reply
Open source and free maintenance is not easy. If this project is helpful to you, please consider making a small donation to help the project continue to maintain and update.

Dondate via ko-fi.com

Dondate via PayPal

Related resource