Files
diodiogod-TTS-Audio-Suite/utils/asr/pipeline.py

185 lines
7.8 KiB
Python

"""
Unified ASR pipeline: adapter selection + result normalization.
"""
from typing import Dict, Any
from utils.asr.srt_heuristic_profiles import (
ENGLISH_DANGLING_TAIL_ALLOWLIST,
ENGLISH_INCOMPLETE_KEYWORDS,
)
from utils.asr.types import ASRRequest, ASRResult, ASRSegment, ASRWord
from utils.asr.srt_builder import build_srt
from utils.asr.adapter_registry import get_asr_adapter_class
def _format_timestamps(result: ASRResult) -> str:
if not result.segments:
return "No timestamps generated."
lines = []
for seg in result.segments:
if seg.words:
for w in seg.words:
lines.append(f"[{w.start:.2f} - {w.end:.2f}] {w.text}")
else:
lines.append(f"[{seg.start:.2f} - {seg.end:.2f}] {seg.text}")
return "\n".join(lines) if lines else "No timestamps generated."
def run_asr(engine_data: Dict[str, Any], request: ASRRequest) -> ASRResult:
engine_type = engine_data.get("engine_type")
adapter_cls = get_asr_adapter_class(engine_type)
adapter = adapter_cls(engine_data)
return adapter.transcribe(request)
def append_info_items(info: str, heading: str, items) -> str:
if not items:
return info
if isinstance(items, str):
items = [items]
prefix = "" if info else ""
if f"\n{heading}:" in info or info.startswith(f"{heading}:"):
return info + "".join(f"\n - {item}" for item in items)
return info + f"{prefix}\n{heading}:" + "".join(f"\n - {item}" for item in items)
def _compose_asr_info(result: ASRResult, stats: Dict[str, Any] = None) -> str:
stats = stats or {}
info_parts = [f"language={result.language or 'unknown'}", f"segments={len(result.segments)}"]
if stats.get("deduped", 0) > 0:
info_parts.append(f"deduped={stats['deduped']}")
info = " | ".join(info_parts)
if stats.get("total", 0) > 0:
match_ratio = stats["matched"] / max(1, stats["total"])
if match_ratio < 0.6:
print(f"⚠️ ASR punctuation alignment matched {stats['matched']}/{stats['total']} words "
f"({match_ratio:.0%}). SRT punctuation may be imperfect.")
align_info = f"punct_align={match_ratio:.0%} ({stats['matched']}/{stats['total']})"
if stats.get("missed"):
align_info += f" | missed={' '.join(stats['missed'])}"
info += f"\n{align_info}"
if stats.get("punct"):
issues = [p for p in stats["punct"] if p.get("fallback")]
if issues:
info += "\nPUNCTUATION (fallbacks):"
for evt in issues:
punct = evt.get("punct", "")
target = evt.get("attached_to", "")
if evt.get("start") is not None:
timing = f"{evt['start']:.2f}-{evt['end']:.2f}"
else:
timing = "n/a"
info += f"\n {punct:<2} -> '{target}'".ljust(34) + f" | {timing:>11}s"
if stats.get("dedupe_events"):
info += "\nDEDUPE (removed overlaps):"
for evt in stats["dedupe_events"]:
phrase = evt.get("phrase", "")
timing = f"{evt.get('start', 0.0):.2f}-{evt.get('end', 0.0):.2f}"
ratio = evt.get("overlap_ratio", 0.0)
count = evt.get("count", 0)
info += f"\n {phrase[:60]}".ljust(34) + f" | {timing:>11}s | {count}w | {ratio:.0%}"
warnings = (result.raw or {}).get("warnings") if result.raw else None
if warnings:
info = append_info_items(info, "WARNINGS", warnings)
notes = (result.raw or {}).get("notes") if result.raw else None
if notes:
info = append_info_items(info, "NOTES", notes)
return info
def format_asr_info(result: ASRResult) -> str:
return _compose_asr_info(result)
def format_asr_output(result: ASRResult,
srt_mode: str = "smart",
max_chars_per_line: int = 42,
max_lines: int = 2,
max_duration: float = 6.0,
min_duration: float = 1.0,
min_gap: float = 0.6,
max_cps: float = 20.0,
tts_ready_mode: bool = False,
tts_ready_paragraph_mode: bool = False,
dedupe_overlaps: bool = True,
dedupe_window_ms: int = 1500,
dedupe_min_words: int = 2,
dedupe_overlap_ratio: float = 0.6,
punctuation_grace_chars: int = 12,
min_words_per_segment: int = 2,
min_segment_seconds: float = 0.4,
merge_trailing_punct_word: bool = True,
merge_trailing_punct_max_gap: float = 1.0,
merge_leading_short_phrase: bool = True,
merge_leading_short_max_words: int = 2,
merge_leading_short_max_gap: float = 2.0,
merge_dangling_tail: bool = True,
merge_dangling_tail_max_words: int = 3,
merge_dangling_tail_max_gap: float = 3.0,
merge_dangling_tail_allowlist: str = ENGLISH_DANGLING_TAIL_ALLOWLIST,
merge_leading_short_no_punct: bool = True,
merge_leading_short_no_punct_max_words: int = 2,
merge_leading_short_no_punct_max_gap: float = 1.5,
merge_incomplete_sentence: bool = True,
merge_incomplete_max_gap: float = 1.2,
merge_incomplete_keywords: str = ENGLISH_INCOMPLETE_KEYWORDS,
merge_incomplete_split_next: bool = True,
merge_allow_overlong: bool = False,
normalize_cue_end_punctuation: bool = False) -> Dict[str, Any]:
srt, stats = build_srt(
result.segments,
mode=srt_mode,
full_text=result.text or "",
max_chars_per_line=max_chars_per_line,
max_lines=max_lines,
max_duration=max_duration,
min_duration=min_duration,
min_gap=min_gap,
max_cps=max_cps,
tts_ready_mode=tts_ready_mode,
tts_ready_paragraph_mode=tts_ready_paragraph_mode,
dedupe_overlaps=dedupe_overlaps,
dedupe_window_ms=dedupe_window_ms,
dedupe_min_words=dedupe_min_words,
dedupe_overlap_ratio=dedupe_overlap_ratio,
punctuation_grace_chars=punctuation_grace_chars,
min_words_per_segment=min_words_per_segment,
min_segment_seconds=min_segment_seconds,
merge_trailing_punct_word=merge_trailing_punct_word,
merge_trailing_punct_max_gap=merge_trailing_punct_max_gap,
merge_leading_short_phrase=merge_leading_short_phrase,
merge_leading_short_max_words=merge_leading_short_max_words,
merge_leading_short_max_gap=merge_leading_short_max_gap,
merge_dangling_tail=merge_dangling_tail,
merge_dangling_tail_max_words=merge_dangling_tail_max_words,
merge_dangling_tail_max_gap=merge_dangling_tail_max_gap,
merge_dangling_tail_allowlist=merge_dangling_tail_allowlist,
merge_leading_short_no_punct=merge_leading_short_no_punct,
merge_leading_short_no_punct_max_words=merge_leading_short_no_punct_max_words,
merge_leading_short_no_punct_max_gap=merge_leading_short_no_punct_max_gap,
merge_incomplete_sentence=merge_incomplete_sentence,
merge_incomplete_max_gap=merge_incomplete_max_gap,
merge_incomplete_keywords=merge_incomplete_keywords,
merge_incomplete_split_next=merge_incomplete_split_next,
merge_allow_overlong=merge_allow_overlong,
normalize_cue_end_punctuation=normalize_cue_end_punctuation,
return_stats=True,
)
info = _compose_asr_info(result, stats)
return {
"text": result.text or "",
"timestamps": _format_timestamps(result),
"srt": srt,
"info": info,
"language": result.language or "",
}