FunASR is an all-in-one multilingual ASR (50+ languages, punctuation, optional
cam++ speaker diarization). ASR is already pluggable, so this is a new
ASRBackend:
- FunASRBackend (id 'funasr'): deferred funasr import in is_available() (reports
an install hint when absent — opt-in, NOT a hard dep); _ensure_model loads
AutoModel(SenseVoiceSmall + fsmn-vad); transcribe() normalises output.
- _normalize_funasr(): pure, defensive normaliser → OmniVoice's
{chunks, segments, language} shape (handles VAD sentence_info with ms
timestamps + optional speaker, single-utterance fallback, strips SenseVoice
rich tokens). Unit-tested without funasr installed.
- Registered in _REGISTRY → auto-appears in /system/asr-backends → the Settings
ASR picker, with availability/install hint. WhisperX stays the default.
Phase 2 (future): wire FunASR's cam++ speaker ids into dub diarization.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
44 lines
1.7 KiB
Python
44 lines
1.7 KiB
Python
"""FunASR ASR backend — opt-in alternative to WhisperX (#182).
|
|
|
|
Tests the pure output-normaliser (no funasr install needed) + registration.
|
|
"""
|
|
from services.asr_backend import _normalize_funasr, FunASRBackend, list_backends
|
|
|
|
|
|
def test_normalize_sentence_info_with_timestamps_and_speaker():
|
|
res = [{
|
|
"language": "en",
|
|
"sentence_info": [
|
|
{"text": "<|en|><|NEUTRAL|>Hello there", "start": 0, "end": 1200, "spk": 0},
|
|
{"text": "Goodbye", "start": 1500, "end": 2300, "spk": 1},
|
|
],
|
|
}]
|
|
out = _normalize_funasr(res)
|
|
assert out["language"] == "en"
|
|
assert out["chunks"][0] == {"text": "Hello there", "timestamp": (0.0, 1.2)} # ms → s, tokens stripped
|
|
assert out["chunks"][1]["timestamp"] == (1.5, 2.3)
|
|
assert out["segments"][0]["speaker"] == "Speaker 1" # spk 0 → 1-based label
|
|
assert out["segments"][1]["speaker"] == "Speaker 2"
|
|
|
|
|
|
def test_normalize_single_utterance_fallback():
|
|
res = [{"text": "<|en|><|HAPPY|>Hello world", "timestamp": [[0, 500], [500, 1000]]}]
|
|
out = _normalize_funasr(res)
|
|
assert out["chunks"] == [{"text": "Hello world", "timestamp": (0.0, 1.0)}]
|
|
|
|
|
|
def test_normalize_empty_and_tokens_only():
|
|
assert _normalize_funasr([]) == {"chunks": [], "segments": [], "language": None}
|
|
assert _normalize_funasr([{"text": "<|en|>"}])["chunks"] == [] # only rich tokens → nothing spoken
|
|
|
|
|
|
def test_is_available_reports_install_hint_when_absent():
|
|
ok, msg = FunASRBackend.is_available()
|
|
if not ok: # funasr is not a hard dependency; absent in CI
|
|
assert "funasr" in msg.lower()
|
|
|
|
|
|
def test_registered_in_picker():
|
|
ids = [b["id"] for b in list_backends()]
|
|
assert "funasr" in ids
|