From e94357ab61cf8a1cf70e185bfa0040a028589753 Mon Sep 17 00:00:00 2001 From: aduoge <53458900+aduoge@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:48:41 +0800 Subject: [PATCH] fix(asr): keep source audio files and per-client credentials - convert_wav_with_path deleted the caller's input file after a successful ffmpeg conversion. The converted audio only lives in memory, so the original recording was lost. Stop deleting it. - AsrWsClient wrote its credentials into the module-level config and every connection read them from there, so creating a second client made the first one connect with the second one's app key and token. Keep credentials on the instance and pass them to new_auth_headers; the default argument keeps existing callers working. --- tests/toolkits/test_asr_client.py | 79 ++++++++++++++++++++++++++ veadk/toolkits/audio/asr/asr_client.py | 21 +++---- 2 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 tests/toolkits/test_asr_client.py diff --git a/tests/toolkits/test_asr_client.py b/tests/toolkits/test_asr_client.py new file mode 100644 index 000000000..60a3ac156 --- /dev/null +++ b/tests/toolkits/test_asr_client.py @@ -0,0 +1,79 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import wave +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import pytest + +from veadk.toolkits.audio.asr import asr_client +from veadk.toolkits.audio.asr.asr_client import AsrWsClient, RequestBuilder + + +def make_wav(pcm): + output = io.BytesIO() + with wave.open(output, "wb") as audio: + audio.setnchannels(1) + audio.setsampwidth(2) + audio.setframerate(16000) + audio.writeframes(pcm) + return output.getvalue() + + +@pytest.mark.asyncio +async def test_read_audio_data_preserves_non_wav_source(tmp_path, monkeypatch): + source = tmp_path / "recording.mp3" + original = b"test compressed audio" + source.write_bytes(original) + converted = make_wav(b"\x01\x00" * 160) + run = Mock(return_value=SimpleNamespace(stdout=converted)) + monkeypatch.setattr(asr_client.subprocess, "run", run) + + client = AsrWsClient("test-app", "test-token") + assert await client.read_audio_data(str(source)) == converted + run.assert_called_once() + assert source.exists(), "ASR conversion must preserve the caller's source file" + assert source.read_bytes() == original + + +@pytest.mark.asyncio +async def test_connections_use_each_clients_credentials(): + first = AsrWsClient("test-app-one", "test-token-one") + second = AsrWsClient("test-app-two", "test-token-two") + first.session = SimpleNamespace(ws_connect=AsyncMock()) + second.session = SimpleNamespace(ws_connect=AsyncMock()) + + await first.create_connection() + await second.create_connection() + + for client, app_id, token in ( + (first, "test-app-one", "test-token-one"), + (second, "test-app-two", "test-token-two"), + ): + headers = client.session.ws_connect.call_args.kwargs["headers"] + assert headers["X-Api-App-Key"] == app_id + assert headers["X-Api-Access-Key"] == token + assert ( + first.session.ws_connect.call_args.kwargs["headers"]["X-Api-Request-Id"] + != second.session.ws_connect.call_args.kwargs["headers"]["X-Api-Request-Id"] + ) + + +def test_auth_header_builder_keeps_default_call_compatible(): + AsrWsClient("test-app", "test-token") + headers = RequestBuilder.new_auth_headers() + assert headers["X-Api-App-Key"] == "test-app" + assert headers["X-Api-Access-Key"] == "test-token" diff --git a/veadk/toolkits/audio/asr/asr_client.py b/veadk/toolkits/audio/asr/asr_client.py index 71dc4e414..791f469d4 100644 --- a/veadk/toolkits/audio/asr/asr_client.py +++ b/veadk/toolkits/audio/asr/asr_client.py @@ -15,7 +15,6 @@ import asyncio import gzip import json -import os import struct import subprocess import uuid @@ -115,12 +114,6 @@ def convert_wav_with_path( cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) - # 尝试删除原始文件 - try: - os.remove(audio_path) - except OSError as e: - logger.warning(f"Failed to remove original file: {e}") - return result.stdout except subprocess.CalledProcessError as e: logger.error(f"FFmpeg conversion failed: {e.stderr.decode()}") @@ -208,13 +201,13 @@ def default_header() -> "AsrRequestHeader": class RequestBuilder: @staticmethod - def new_auth_headers() -> Dict[str, str]: + def new_auth_headers(auth_config: Config = config) -> Dict[str, str]: reqid = str(uuid.uuid4()) return { "X-Api-Resource-Id": "volc.bigasr.sauc.duration", "X-Api-Request-Id": reqid, - "X-Api-Access-Key": config.access_key, - "X-Api-App-Key": config.app_key, + "X-Api-Access-Key": auth_config.access_key, + "X-Api-App-Key": auth_config.app_key, } @staticmethod @@ -372,11 +365,13 @@ def __init__( self.conn = None self.session = None - global config - config.auth = { + self._auth_config = Config() + self._auth_config.auth = { "app_key": app_id, "access_key": access_token, } + # Preserve the default RequestBuilder credentials for existing callers. + config.auth = self._auth_config.auth async def __aenter__(self): self.session = aiohttp.ClientSession() @@ -417,7 +412,7 @@ def get_segment_size(self, content: bytes) -> int: raise async def create_connection(self) -> None: - headers = RequestBuilder.new_auth_headers() + headers = RequestBuilder.new_auth_headers(self._auth_config) try: self.conn = await self.session.ws_connect( # 使用self.session self.url, headers=headers