原来进度只在阶段结束时跳一次(ASR 5%→55%、翻译 60%→98%),长视频时 进度条卡住不动。现在每完成一个批次就更新进度。 ASR(asr_service.py): - 用 info.duration_after_vad 算总 chunk 数(ceil(时长/30s)) - 消费生成器时按 seg.end 跨 30s chunk 边界回调 on_progress - 30s 粒度自然节流,长视频约几十次更新 翻译(translate_service.py): - _translate_sorted / _translate_sequential 每批完成后回调 on_progress(done, total) - 批数循环前已知(len(batches)),每批都回调 pipeline.py: - asr_phase / translate_phase 定义闭包回调,把 (current,total) 占比映射到 对应进度区间(ASR 5%→55%、翻译 60%→98%),调 _set_status 写 DB(DEBUG 级) 验证(test/55.mp4, 640 条字幕): - ASR: 5%→19.9%→23.8%→33.7%→48.6%→55% 平滑增长 ✅ - 翻译: 20 批,65.3%→68.9%→74.2%→79.6%→84.9%→90.2%→93.8%→98% ✅ - 翻译卡在 60% 的 ~50s 是模型加载时间(卸载ASR+加载NLLB),属调度器层面
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
"""语音识别服务:faster-whisper,输出带词级时间戳的 segments。
|
||
|
||
CPU dev: tiny.en + int8;GPU prod: large-v3-turbo + float16。同一份代码。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import math
|
||
from pathlib import Path
|
||
from typing import Callable
|
||
|
||
from ..config import get_settings
|
||
from .model_manager import get_model_manager
|
||
from .types import Segment, Word
|
||
|
||
logger = logging.getLogger("audio2text.asr")
|
||
|
||
# Whisper 默认 chunk_length(秒):BatchedInferencePipeline 按 30s 窗口切音频
|
||
_CHUNK_SECONDS = 30.0
|
||
|
||
|
||
def transcribe(
|
||
wav_path: Path,
|
||
on_progress: Callable[[int, int], None] | None = None,
|
||
) -> list[Segment]:
|
||
"""转写 wav,返回 segments(含词级时间戳)。
|
||
|
||
Args:
|
||
wav_path: 16kHz mono PCM wav
|
||
on_progress: 可选进度回调 (current_chunk, total_chunks)。
|
||
每 transcribe 完一个 30s chunk 调一次,用于细分进度条。
|
||
|
||
Returns:
|
||
list[Segment],每个 Segment 带词级 words(若 word_timestamps 启用)。
|
||
"""
|
||
s = get_settings().asr
|
||
if not wav_path.is_file():
|
||
raise FileNotFoundError(f"音频不存在:{wav_path}")
|
||
|
||
model = get_model_manager().get_asr()
|
||
logger.debug("开始转写 %s(model=%s language=%s batch_size=%d beam_size=%d)",
|
||
wav_path.name, s.model, s.language, s.batch_size, s.beam_size)
|
||
|
||
segments_gen, info = model.transcribe(
|
||
str(wav_path),
|
||
language=s.language,
|
||
word_timestamps=s.word_timestamps,
|
||
vad_filter=s.vad_filter,
|
||
beam_size=s.beam_size, # beam search 宽度(config 可调);GPU turbo 建议降到 2 加速
|
||
batch_size=s.batch_size, # 批量解码:多音频块一次性送 GPU
|
||
without_timestamps=False, # BatchedInferencePipeline 默认 True,需显式关闭以生成段级时间戳
|
||
)
|
||
|
||
# VAD 过滤后的实际语音时长 → 算总 chunk 数(进度颗粒度细分用)
|
||
duration = info.duration_after_vad or info.duration
|
||
total_chunks = max(1, math.ceil(duration / _CHUNK_SECONDS))
|
||
logger.debug(
|
||
"音频时长 %.1fs(VAD 后 %.1fs),检测语言=%s(置信度 %.2f),约 %d 个 chunk",
|
||
info.duration, duration, info.language, info.language_probability, total_chunks,
|
||
)
|
||
if on_progress is not None:
|
||
on_progress(0, total_chunks)
|
||
|
||
segments: list[Segment] = []
|
||
last_chunk = 0 # 已报进度的 chunk 序号(避免同 chunk 内多个 segment 重复回调)
|
||
for seg in segments_gen:
|
||
words: list[Word] = []
|
||
if s.word_timestamps and getattr(seg, "words", None):
|
||
for w in seg.words:
|
||
words.append(Word(
|
||
text=w.word.strip(),
|
||
start=float(w.start),
|
||
end=float(w.end),
|
||
probability=float(getattr(w, "probability", 1.0)),
|
||
))
|
||
segments.append(Segment(
|
||
text=seg.text.strip(),
|
||
start=float(seg.start),
|
||
end=float(seg.end),
|
||
words=words,
|
||
))
|
||
|
||
# 按 30s chunk 边界报进度:seg.end 跨过 chunk 边界时回调
|
||
if on_progress is not None:
|
||
cur_chunk = min(total_chunks, int(seg.end / _CHUNK_SECONDS) + 1)
|
||
if cur_chunk > last_chunk:
|
||
last_chunk = cur_chunk
|
||
on_progress(cur_chunk, total_chunks)
|
||
|
||
logger.debug("转写完成:%d 段,%d 词。",
|
||
len(segments), sum(len(seg.words) for seg in segments))
|
||
return segments
|
||
|