diff --git a/backend/app/services/note.py b/backend/app/services/note.py index ebbe83a6..f493c910 100644 --- a/backend/app/services/note.py +++ b/backend/app/services/note.py @@ -262,7 +262,10 @@ def _init_transcriber(self) -> Transcriber: raise Exception(f"不支持的转写器:{self.transcriber_type}") logger.info(f"使用转写器:{self.transcriber_type}") - return get_transcriber(transcriber_type=self.transcriber_type) + return get_transcriber( + transcriber_type=self.transcriber_type, + model_size=self.model_size, + ) def _get_gpt(self, model_name: Optional[str], provider_id: Optional[str]) -> GPT: """ diff --git a/backend/app/transcriber/transcriber_provider.py b/backend/app/transcriber/transcriber_provider.py index 0440bc84..af29c173 100644 --- a/backend/app/transcriber/transcriber_provider.py +++ b/backend/app/transcriber/transcriber_provider.py @@ -1,5 +1,6 @@ import os import platform +import threading from enum import Enum from app.transcriber.groq import GroqTranscriber @@ -38,17 +39,29 @@ class TranscriberType(str, Enum): TranscriberType.GROQ: None, } +# Cache instances together with their constructor configuration. The +# transcriber choice and Whisper model size can be changed from the frontend, +# so caching by transcriber type alone would keep using the first loaded model. +_transcriber_configs = {key: None for key in _transcribers} +_transcriber_init_lock = threading.Lock() + # 公共实例初始化函数 def _init_transcriber(key: TranscriberType, cls, *args, **kwargs): - if _transcribers[key] is None: - logger.info(f'创建 {cls.__name__} 实例: {key}') - try: - _transcribers[key] = cls(*args, **kwargs) + init_config = (args, tuple(sorted(kwargs.items()))) + with _transcriber_init_lock: + instance = _transcribers[key] + if instance is None or _transcriber_configs[key] != init_config: + action = "创建" if instance is None else "按新配置重新创建" + logger.info(f'{action} {cls.__name__} 实例: {key}') + try: + new_instance = cls(*args, **kwargs) + except Exception as e: + logger.error(f"{cls.__name__} 创建失败: {e}") + raise + _transcribers[key] = new_instance + _transcriber_configs[key] = init_config logger.info(f'{cls.__name__} 创建成功') - except Exception as e: - logger.error(f"{cls.__name__} 创建失败: {e}") - raise - return _transcribers[key] + return _transcribers[key] # 各类型获取方法 def get_groq_transcriber(): @@ -70,13 +83,13 @@ def get_mlx_whisper_transcriber(model_size="base"): return _init_transcriber(TranscriberType.MLX_WHISPER, MLXWhisperTranscriber, model_size=model_size) # 通用入口 -def get_transcriber(transcriber_type="fast-whisper", model_size="base", device="cuda"): +def get_transcriber(transcriber_type="fast-whisper", model_size=None, device="cuda"): """ 获取指定类型的转录器实例 参数: transcriber_type: 支持 "fast-whisper", "mlx-whisper", "bcut", "kuaishou", "groq" - model_size: 模型大小,适用于 whisper 类 + model_size: 模型大小,适用于 whisper 类;未提供时才读取环境变量默认值 device: 设备类型(如 cuda / cpu),仅 whisper 使用 返回: @@ -90,7 +103,9 @@ def get_transcriber(transcriber_type="fast-whisper", model_size="base", device=" logger.warning(f'未知转录器类型 "{transcriber_type}",默认使用 fast-whisper') transcriber_enum = TranscriberType.FAST_WHISPER - whisper_model_size = os.environ.get("WHISPER_MODEL_SIZE", model_size) + # The explicit value normally comes from the persisted frontend setting and + # must take precedence over Docker's startup default. + whisper_model_size = model_size or os.environ.get("WHISPER_MODEL_SIZE", "base") if transcriber_enum == TranscriberType.FAST_WHISPER: return get_whisper_transcriber(whisper_model_size, device=device) diff --git a/backend/tests/test_whisper_config_forwarding.py b/backend/tests/test_whisper_config_forwarding.py new file mode 100644 index 00000000..2887541c --- /dev/null +++ b/backend/tests/test_whisper_config_forwarding.py @@ -0,0 +1,120 @@ +import os +import pathlib +import subprocess +import sys +import textwrap + + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +def _run_isolated(script: str) -> None: + env = os.environ.copy() + env["PYTHONPATH"] = os.pathsep.join( + filter(None, [str(ROOT), env.get("PYTHONPATH")]) + ) + result = subprocess.run( + [sys.executable, "-c", textwrap.dedent(script)], + cwd=ROOT, + env=env, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_note_generator_forwards_configured_whisper_model_size(): + _run_isolated( + """ + from app.services import note as note_service + + calls = [] + + def fake_get_transcriber(**kwargs): + calls.append(kwargs) + return object() + + note_service.get_transcriber = fake_get_transcriber + + generator = note_service.NoteGenerator.__new__(note_service.NoteGenerator) + generator.transcriber_type = "fast-whisper" + generator.model_size = "large-v3-turbo" + + generator._init_transcriber() + + assert calls == [ + { + "transcriber_type": "fast-whisper", + "model_size": "large-v3-turbo", + } + ] + """ + ) + + +def test_whisper_cache_is_rebuilt_when_model_size_changes(): + _run_isolated( + """ + from app.transcriber import transcriber_provider as provider + + class FakeWhisperTranscriber: + def __init__(self, model_size, device): + self.model_size = model_size + self.device = device + + provider._transcribers = {key: None for key in provider._transcribers} + provider._transcriber_configs = { + key: None for key in provider._transcribers + } + provider.WhisperTranscriber = FakeWhisperTranscriber + + base = provider.get_whisper_transcriber("base", device="cpu") + turbo = provider.get_whisper_transcriber("large-v3-turbo", device="cpu") + turbo_again = provider.get_whisper_transcriber( + "large-v3-turbo", device="cpu" + ) + + assert base.model_size == "base" + assert turbo.model_size == "large-v3-turbo" + assert turbo is not base + assert turbo_again is turbo + """ + ) + + +def test_explicit_model_size_wins_over_environment_default(): + _run_isolated( + """ + import os + + from app.transcriber import transcriber_provider as provider + + class FakeWhisperTranscriber: + def __init__(self, model_size, device): + self.model_size = model_size + self.device = device + + os.environ["WHISPER_MODEL_SIZE"] = "tiny" + provider._transcribers = {key: None for key in provider._transcribers} + provider._transcriber_configs = { + key: None for key in provider._transcribers + } + provider.WhisperTranscriber = FakeWhisperTranscriber + + transcriber = provider.get_transcriber( + transcriber_type="fast-whisper", + model_size="large-v3-turbo", + device="cpu", + ) + + assert transcriber.model_size == "large-v3-turbo" + + fallback = provider.get_transcriber( + transcriber_type="fast-whisper", + model_size=None, + device="cpu", + ) + + assert fallback.model_size == "tiny" + """ + )