import base64 import pytest from typing import Any, Optional from evalscope.api.benchmark import BenchmarkMeta from evalscope.api.dataset import Sample from evalscope.api.evaluator import TaskState from evalscope.api.messages import ChatMessageUser, ContentAudio, ContentText from evalscope.api.model import ChatCompletionChoice, ModelOutput from evalscope.benchmarks.seed_tts_eval.seed_tts_eval_adapter import PROMPT_TEMPLATE, SeedTTSEvalAdapter from evalscope.config import TaskConfig from evalscope.metrics.audio.metrics import AudioWER def make_adapter(metric_list: Optional[list[Any]] = None) -> SeedTTSEvalAdapter: meta = BenchmarkMeta( name='seed_tts_eval', dataset_id='evalscope/Seed-TTS-Eval', subset_list=['en'], default_subset='en', eval_split='train', prompt_template=PROMPT_TEMPLATE, metric_list=metric_list or [], ) cfg = TaskConfig(datasets=['seed_tts_eval']) adapter = SeedTTSEvalAdapter(benchmark_meta=meta, task_config=cfg) adapter.current_subset_name = 'en' return adapter def test_record_to_sample_builds_tts_prompt_with_reference_audio() -> None: adapter = make_adapter() sample = adapter.record_to_sample({ 'filename': 'sample-1', 'prompt_text': 'This is the reference speaker.', 'WavPath': 'prompt-wavs/sample.wav', 'text': 'Please synthesize this sentence.', 'ans': 'wavs/sample.wav', 'audio': { 'bytes': b'RIFF....WAVE', 'path': 'sample.wav', }, }) assert sample.target == 'Please synthesize this sentence.' assert sample.metadata['wer_language'] == 'en' assert isinstance(sample.input[0], ChatMessageUser) assert isinstance(sample.input[0].content[0], ContentAudio) assert sample.input[0].content[0].audio.startswith('data:audio/wav;base64,') assert isinstance(sample.input[0].content[1], ContentText) assert 'Target text: Please synthesize this sentence.' in sample.input[0].content[1].text def test_inference_end_saves_audio_output(tmp_path: Any) -> None: adapter = make_adapter() sample = Sample( input=[ChatMessageUser(content='hello')], target='hello', id=0, group_id=0, metadata={'filename': 'sample-1'}, ) audio = base64.b64encode(b'fake-audio').decode('utf-8') output = ModelOutput( model='mock', choices=[ChatCompletionChoice.from_content([ContentAudio(audio=audio, format='wav')])], ) class MockModel: name = 'mock' state = adapter._on_inference_end(MockModel(), sample, output, str(tmp_path)) generated_audio = state.metadata['generated_audio_path'] assert generated_audio.endswith('sample-1_0.wav') assert output.completion == generated_audio with open(generated_audio, 'rb') as f: assert f.read() == b'fake-audio' def test_match_score_uses_generated_audio_path(monkeypatch: Any) -> None: adapter = make_adapter(metric_list=[{'audio_wer': {'api_key': 'test-key', 'api_base': 'https://asr.test/v1'}}]) sample = Sample(input=[ChatMessageUser(content='hello')], target='hello', metadata={'wer_language': 'en'}) state = TaskState(model='mock', sample=sample, completed=True) state.metadata['generated_audio_path'] = 'data:audio/wav;base64,' + base64.b64encode(b'audio').decode('utf-8') def mock_post( self: Any, url: str, headers: dict[str, str], files: dict[str, Any], data: dict[str, str], timeout: float, ) -> Any: class Response: text = '{"text": "hello"}' def raise_for_status(self) -> None: pass def json(self) -> dict[str, str]: return {'text': 'hello'} assert url == 'https://asr.test/v1/audio/transcriptions' assert headers['Authorization'] == 'Bearer test-key' assert data['model'] == 'whisper-1' return Response() monkeypatch.setattr('requests.Session.post', mock_post) score = adapter.match_score('', '', 'hello', state) assert score.value['audio_wer'] == 0.0 assert score.metadata['transcription'] == 'hello' def test_audio_wer_accepts_full_transcription_endpoint(monkeypatch: Any) -> None: def mock_post( self: Any, url: str, headers: dict[str, str], files: dict[str, Any], data: dict[str, str], timeout: float, ) -> Any: class Response: text = 'hello' def raise_for_status(self) -> None: pass def json(self) -> dict[str, str]: return {'text': 'hello'} assert url == 'https://asr.test/v1/audio/transcriptions' return Response() monkeypatch.setattr('requests.Session.post', mock_post) metric = AudioWER(api_base='https://asr.test/v1/audio/transcriptions', api_key='test-key') score = metric('data:audio/wav;base64,' + base64.b64encode(b'audio').decode('utf-8'), 'hello') assert score == 0.0 assert metric.transcriptions == ['hello'] def test_audio_wer_supports_responses_protocol(monkeypatch: Any) -> None: def mock_post( self: Any, url: str, headers: dict[str, str], json: dict[str, Any], timeout: float, ) -> Any: class Response: def raise_for_status(self) -> None: pass def json(self) -> dict[str, Any]: return { 'output': [{ 'type': 'message', 'content': [{ 'type': 'output_text', 'text': 'hello', }], }] } assert url == 'https://ark.test/api/v3/responses' assert headers['Authorization'] == 'Bearer test-key' content = json['input'][0]['content'] assert content[0]['type'] == 'input_audio' assert content[0]['audio_url'].startswith('data:audio/wav;base64,') assert content[1]['type'] == 'input_text' return Response() monkeypatch.setattr('requests.Session.post', mock_post) metric = AudioWER( api_base='https://ark.test/api/v3', api_key='test-key', model='doubao-seed-2-0-lite-260428', api_protocol='responses', ) score = metric('data:audio/wav;base64,' + base64.b64encode(b'audio').decode('utf-8'), 'hello') assert score == 0.0 assert metric.transcriptions == ['hello'] def test_audio_wer_rejects_unsupported_protocol() -> None: with pytest.raises(ValueError, match='Unsupported audio_wer api_protocol'): AudioWER(api_base='https://asr.test/v1', api_key='test-key', api_protocol='streaming')