Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/app/services/note.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
37 changes: 26 additions & 11 deletions backend/app/transcriber/transcriber_provider.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import platform
import threading
from enum import Enum

from app.transcriber.groq import GroqTranscriber
Expand Down Expand Up @@ -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():
Expand All @@ -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 使用

返回:
Expand All @@ -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)
Expand Down
120 changes: 120 additions & 0 deletions backend/tests/test_whisper_config_forwarding.py
Original file line number Diff line number Diff line change
@@ -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"
"""
)