Files
audio2text/app/services/asr_service.py
audio2text dev 78b87bfb24 feat: 进度条颗粒度优化 — ASR/翻译按批次细分进度
原来进度只在阶段结束时跳一次(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),属调度器层面
2026-07-06 23:18:30 +08:00

95 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""语音识别服务faster-whisper输出带词级时间戳的 segments。
CPU dev: tiny.en + int8GPU 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_lengthBatchedInferencePipeline 按 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("开始转写 %smodel=%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(
"音频时长 %.1fsVAD 后 %.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