"""ASR 解析器单元测试。""" import os from unittest.mock import MagicMock, patch import pytest from yuxi.knowledge.parser.asr import ASRProcessor from yuxi.knowledge.parser.base import DocumentProcessorException @pytest.fixture def asr(): return ASRProcessor() class TestASRProcessorBasics: def test_service_name(self, asr): assert asr.get_service_name() == "asr" def test_supported_extensions(self, asr): exts = asr.get_supported_extensions() assert ".mp3" in exts assert ".wav" in exts assert ".m4a" in exts assert ".flac" in exts def test_supports_file_type(self, asr): assert asr.supports_file_type(".mp3") assert asr.supports_file_type(".wav") assert not asr.supports_file_type(".pdf") class TestSegmentsToMarkdown: def test_basic_formatting(self): segments = [ {"start": 0, "end": 5, "text": "Hello world"}, {"start": 65, "end": 70, "text": "Second segment"}, ] result = ASRProcessor._segments_to_markdown(segments, "test.mp3") assert "# 音频转写" in result assert "> 来源: test.mp3" in result assert "**[00:00]** Hello world" in result assert "**[01:05]** Second segment" in result def test_empty_segments(self): result = ASRProcessor._segments_to_markdown([], "test.mp3") assert "# 音频转写" in result assert "> 来源: test.mp3" in result def test_skip_empty_text(self): segments = [ {"start": 0, "end": 5, "text": ""}, {"start": 5, "end": 10, "text": " "}, {"start": 10, "end": 15, "text": "valid"}, ] result = ASRProcessor._segments_to_markdown(segments, "test.mp3") assert "valid" in result assert result.count("**[") == 1 class TestProcessFile: @patch("yuxi.knowledge.parser.asr.probe_audio_duration", return_value=60.0) @patch("yuxi.knowledge.parser.asr.convert_to_wav") @patch("yuxi.knowledge.parser.asr.ASRProcessor._transcribe") def test_success(self, mock_transcribe, mock_convert, mock_duration, asr, tmp_path): test_file = tmp_path / "test.mp3" test_file.write_bytes(b"fake audio data") mock_transcribe.return_value = [{"start": 0, "end": 5, "text": "Hello"}] result = asr.process_file(str(test_file)) assert "# 音频转写" in result assert "Hello" in result @patch("yuxi.knowledge.parser.asr.probe_audio_duration", return_value=60.0) def test_file_too_large(self, mock_duration, asr, tmp_path): test_file = tmp_path / "big.mp3" test_file.write_bytes(b"x" * (16 * 1024 * 1024 + 1)) with pytest.raises(DocumentProcessorException, match="大小"): asr.process_file(str(test_file)) @patch("yuxi.knowledge.parser.asr.probe_audio_duration", return_value=1500.0) def test_duration_too_long(self, mock_duration, asr, tmp_path): test_file = tmp_path / "long.mp3" test_file.write_bytes(b"fake audio") with pytest.raises(DocumentProcessorException, match="时长"): asr.process_file(str(test_file)) @patch("yuxi.knowledge.parser.asr.probe_audio_duration", return_value=60.0) @patch("yuxi.knowledge.parser.asr.convert_to_wav") def test_unsupported_asr_provider(self, mock_convert, mock_duration, asr, tmp_path): test_file = tmp_path / "test.mp3" test_file.write_bytes(b"fake audio") with pytest.raises(DocumentProcessorException, match="不支持的 ASR 提供者"): asr.process_file(str(test_file), {"asr_provider": "unknown"}) class TestCheckHealth: @patch.dict(os.environ, {"ASR_PROVIDER": "whisper", "WHISPER_API_KEY": "test-key"}) def test_whisper_configured(self, asr): result = asr.check_health() assert result["status"] == "healthy" @patch.dict(os.environ, {"ASR_PROVIDER": "whisper"}, clear=False) def test_whisper_not_configured(self, asr): with patch.dict(os.environ, {"WHISPER_API_KEY": ""}): result = asr.check_health() assert result["status"] == "unavailable" @patch.dict(os.environ, {"ASR_PROVIDER": "funasr"}) @patch("yuxi.knowledge.parser.asr.httpx.Client") def test_funasr_unreachable(self, mock_client_cls, asr): mock_client = MagicMock() mock_client.__enter__ = MagicMock(return_value=mock_client) mock_client.__exit__ = MagicMock(return_value=False) mock_client.get.side_effect = Exception("connection refused") mock_client_cls.return_value = mock_client result = asr.check_health() assert result["status"] == "unhealthy"