simplify(compat): tools/transcription_tools — drop 47 re-exports/aliases, repoint 4 callers + 9 test files
This commit is contained in:
@@ -246,7 +246,7 @@ CONFIG_SCHEMA = _config_schema_with_virtual_fields()
|
||||
def _is_command_provider_block(value: Any) -> bool:
|
||||
"""True when *value* declares a command-type voice provider.
|
||||
|
||||
Mirrors the runtime discriminators (``tools.tts_tool._is_command_provider_config`` /
|
||||
Mirrors the runtime discriminators (``tools.tts_command_provider._is_command_provider_config`` /
|
||||
``tools.transcription_tools._is_command_stt_provider_config``) and the desktop's
|
||||
``isCommandProvider``: ``type`` is OPTIONAL and case/space-insensitive (absent or
|
||||
normalizing to ``"command"``); ``command`` MUST be a non-empty string.
|
||||
@@ -278,7 +278,7 @@ def _custom_provider_options(kind: str, builtin_names: List[str], cfg: Dict[str,
|
||||
if kind == "tts":
|
||||
from tools.tts_tool import BUILTIN_TTS_PROVIDERS as _runtime_builtins
|
||||
else:
|
||||
from tools.transcription_tools import BUILTIN_STT_PROVIDERS as _runtime_builtins
|
||||
from tools.transcription_common import BUILTIN_STT_PROVIDERS as _runtime_builtins
|
||||
|
||||
def _add(name: Any) -> None:
|
||||
stripped = name.strip() if isinstance(name, str) else ""
|
||||
@@ -319,7 +319,6 @@ def _custom_provider_options(kind: str, builtin_names: List[str], cfg: Dict[str,
|
||||
def _memory_provider_schema_options(cfg: Dict[str, Any]) -> List[str]:
|
||||
"""Discovered memory providers plus the currently-configured one, so a value that is no
|
||||
longer discoverable (e.g. plugin removed from disk) never vanishes from the dropdown."""
|
||||
from hermes_cli.web_server import _memory_provider_options
|
||||
options = _memory_provider_options()
|
||||
memory = cfg.get("memory")
|
||||
current = _normalize_memory_provider_name(memory.get("provider") if isinstance(memory, dict) else None)
|
||||
@@ -343,7 +342,8 @@ def _schema_with_dynamic_provider_options() -> Dict[str, Dict[str, Any]]:
|
||||
that reads the schema. ``CONFIG_SCHEMA`` is never mutated; changed entries are
|
||||
shallow-copied onto a copied mapping.
|
||||
"""
|
||||
from hermes_cli.web_server import _plugin_terminal_backend_rows, load_config
|
||||
from hermes_cli.web_server_profiles import _plugin_terminal_backend_rows
|
||||
from hermes_cli.config import load_config
|
||||
try:
|
||||
cfg = load_config()
|
||||
except Exception: # pragma: no cover - schema must survive config errors
|
||||
@@ -393,7 +393,7 @@ def _normalize_main_model_assignment(provider: str, model: str) -> tuple[str, st
|
||||
2. Model-format normalization for the resolved provider via
|
||||
``normalize_model_for_provider`` (custom/user providers keep the model verbatim).
|
||||
"""
|
||||
from hermes_cli.web_server import load_config
|
||||
from hermes_cli.config import load_config
|
||||
from hermes_cli.config import get_compatible_custom_providers
|
||||
from hermes_cli.models import _AGGREGATOR_PROVIDERS, _KNOWN_PROVIDER_NAMES, normalize_provider
|
||||
from hermes_cli.model_normalize import normalize_model_for_provider
|
||||
@@ -595,7 +595,7 @@ def _register_custom_endpoint(base_url: str, api_key: str, model: str) -> None:
|
||||
``hermes model`` custom flow) so the picker gets a proper ready row instead of a "needs
|
||||
setup" dead-end. Dedups by base_url; never blocks the already-persisted assignment."""
|
||||
try:
|
||||
from hermes_cli.main import _auto_provider_name, _save_custom_provider
|
||||
from hermes_cli.main_provider_setup import _auto_provider_name, _save_custom_provider
|
||||
|
||||
_save_custom_provider(base_url, api_key, model, name=_auto_provider_name(base_url))
|
||||
except Exception:
|
||||
@@ -627,7 +627,7 @@ def _stale_aux_pins(cfg: dict, new_provider: str) -> list:
|
||||
|
||||
|
||||
def _cron_model_impact(cfg: dict, provider: str, model: str) -> Any:
|
||||
from hermes_cli.web_server import load_config
|
||||
from hermes_cli.config import load_config
|
||||
try:
|
||||
effective_config = load_config()
|
||||
effective_provider, effective_model = resolve_cron_model_drift_defaults(effective_config)
|
||||
@@ -642,7 +642,7 @@ def _cron_model_impact(cfg: dict, provider: str, model: str) -> Any:
|
||||
|
||||
|
||||
def _apply_main_assignment_sync(cfg: dict, provider: str, model: str, base_url: str, api_key: str) -> dict:
|
||||
from hermes_cli.web_server import save_config
|
||||
from hermes_cli.config import save_config
|
||||
if not provider or not model:
|
||||
raise HTTPException(status_code=400, detail="provider and model required for main")
|
||||
provider, model = _normalize_main_model_assignment(provider, model)
|
||||
@@ -673,7 +673,7 @@ def _apply_main_assignment_sync(cfg: dict, provider: str, model: str, base_url:
|
||||
|
||||
|
||||
def _apply_aux_assignment_sync(cfg: dict, provider: str, model: str, task: str, base_url: str, api_key: str) -> dict:
|
||||
from hermes_cli.web_server import save_config
|
||||
from hermes_cli.config import save_config
|
||||
aux = cfg.get("auxiliary")
|
||||
if not isinstance(aux, dict):
|
||||
aux = {}
|
||||
@@ -736,7 +736,7 @@ def _apply_model_assignment_sync(
|
||||
Runs inside ``_profile_scope`` (worker thread) so every load_config/save_config lands in
|
||||
the requested profile. Raises HTTPException for validation errors.
|
||||
"""
|
||||
from hermes_cli.web_server import load_config
|
||||
from hermes_cli.config import load_config
|
||||
cfg = load_config()
|
||||
if scope == "main":
|
||||
return _apply_main_assignment_sync(cfg, provider, model, base_url, api_key)
|
||||
@@ -790,7 +790,7 @@ def _denormalize_config_from_web(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
removed). A partial update (Settings autosave diff) that OMITS the key means "unchanged"
|
||||
and must leave the on-disk override alone — not be treated as an explicit 0.
|
||||
"""
|
||||
from hermes_cli.web_server import load_config
|
||||
from hermes_cli.config import load_config
|
||||
config = dict(config)
|
||||
config.pop("_model_meta", None)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""ffmpeg discovery for Discord voice: ``tools.transcription_tools`` owns the shared lookup
|
||||
"""ffmpeg discovery for Discord voice: ``tools.transcription_audio`` owns the shared lookup
|
||||
(PATH + Homebrew/local prefixes); this layers an explicit ``FFMPEG_PATH`` override and a
|
||||
Windows winget fallback (installs that never touch PATH) on top."""
|
||||
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
def _shared_find_ffmpeg():
|
||||
"""Delegate to the repo-wide ffmpeg discovery helper when importable."""
|
||||
try:
|
||||
from tools.transcription_tools import _find_ffmpeg_binary
|
||||
from tools.transcription_audio import _find_ffmpeg_binary
|
||||
except ImportError: # standalone plugin import (tests / sandboxes)
|
||||
return shutil.which("ffmpeg")
|
||||
return _find_ffmpeg_binary()
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestActiveDetection:
|
||||
class TestModelPicker:
|
||||
|
||||
def test_catalog_matches_runtime_model_sets(self):
|
||||
from tools.transcription_tools import GROQ_MODELS, OPENAI_MODELS
|
||||
from tools.transcription_common import GROQ_MODELS, OPENAI_MODELS
|
||||
|
||||
assert set(STT_MODEL_CATALOG["openai"]) == OPENAI_MODELS
|
||||
assert set(STT_MODEL_CATALOG["groq"]) == GROQ_MODELS
|
||||
|
||||
@@ -383,9 +383,10 @@ class TestSttPromptConfig:
|
||||
|
||||
assert result["success"] is True # truncation never errors
|
||||
_, kwargs = backend.call_args
|
||||
from tools import transcription_command
|
||||
max_chars = (
|
||||
transcription_tools._WHISPER_PROMPT_TOKEN_CAP
|
||||
* transcription_tools._PROMPT_CHARS_PER_TOKEN
|
||||
transcription_command._WHISPER_PROMPT_TOKEN_CAP
|
||||
* transcription_command._PROMPT_CHARS_PER_TOKEN
|
||||
)
|
||||
assert len(kwargs["prompt"]) == max_chars
|
||||
# Tail survives — whisper conditions on the final context window.
|
||||
|
||||
@@ -212,7 +212,7 @@ class TestSttStrictSelection:
|
||||
|
||||
with patch.object(tt, "_load_stt_config", return_value={"openai": {"api_key": "sk-direct"}}), \
|
||||
patch("tools.tool_backend_helpers.read_selection", return_value="nous"), \
|
||||
patch.object(tt, "resolve_managed_tool_gateway", return_value=MANAGED):
|
||||
patch("tools.managed_tool_gateway.resolve_managed_tool_gateway", return_value=MANAGED):
|
||||
api_key, base_url = tt._resolve_openai_audio_client_config()
|
||||
assert api_key == "managed-token"
|
||||
assert base_url.startswith("https://gateway.nousresearch.com")
|
||||
@@ -222,8 +222,8 @@ class TestSttStrictSelection:
|
||||
|
||||
with patch.object(tt, "_load_stt_config", return_value={}), \
|
||||
patch("tools.tool_backend_helpers.read_selection", return_value="openai"), \
|
||||
patch.object(tt, "resolve_openai_audio_api_key", return_value=""), \
|
||||
patch.object(tt, "resolve_managed_tool_gateway") as gw:
|
||||
patch("tools.tool_backend_helpers.resolve_openai_audio_api_key", return_value=""), \
|
||||
patch("tools.managed_tool_gateway.resolve_managed_tool_gateway") as gw:
|
||||
with pytest.raises(ValueError) as exc:
|
||||
tt._resolve_openai_audio_client_config()
|
||||
gw.assert_not_called()
|
||||
@@ -235,7 +235,7 @@ class TestSttStrictSelection:
|
||||
|
||||
with patch.object(tt, "_load_stt_config", return_value={}), \
|
||||
patch("tools.tool_backend_helpers.read_selection", return_value=None), \
|
||||
patch.object(tt, "resolve_openai_audio_api_key", return_value="sk-env"):
|
||||
patch("tools.tool_backend_helpers.resolve_openai_audio_api_key", return_value="sk-env"):
|
||||
api_key, base_url = tt._resolve_openai_audio_client_config()
|
||||
assert api_key == "sk-env"
|
||||
|
||||
|
||||
@@ -36,18 +36,17 @@ if "faster_whisper" not in sys.modules:
|
||||
faster_whisper_stub.__spec__ = ModuleSpec("faster_whisper", loader=None)
|
||||
sys.modules["faster_whisper"] = faster_whisper_stub
|
||||
|
||||
from tools.transcription_tools import (
|
||||
CLOUD_STT_PROVIDERS,
|
||||
BUILTIN_STT_PROVIDERS,
|
||||
from tools.transcription_common import BUILTIN_STT_PROVIDERS, CLOUD_STT_PROVIDERS
|
||||
from tools.transcription_audio import (
|
||||
_cloud_trim_settings,
|
||||
_CLOUD_TRIM_KEEP_MS_DEFAULT,
|
||||
_CLOUD_TRIM_MIN_INPUT_SECONDS,
|
||||
_CLOUD_TRIM_THRESHOLD_DB_DEFAULT,
|
||||
_trim_silence_for_cloud_stt,
|
||||
)
|
||||
import tools.transcription_tools as tt_module
|
||||
|
||||
# The E2E fixtures below must be past the short-clip input gate.
|
||||
_GATE = tt_module._CLOUD_TRIM_MIN_INPUT_SECONDS
|
||||
_GATE = _CLOUD_TRIM_MIN_INPUT_SECONDS
|
||||
|
||||
_HAS_FFMPEG = bool(shutil.which("ffmpeg")) and bool(shutil.which("ffprobe"))
|
||||
|
||||
@@ -231,13 +230,13 @@ class TestTrimFallbacks:
|
||||
|
||||
def test_missing_ffmpeg_returns_none(self, tmp_path):
|
||||
wav = _write_wav(tmp_path / "a.wav", [("tone", 1)])
|
||||
with patch("tools.transcription_tools._find_ffmpeg_binary", return_value=None):
|
||||
with patch("tools.transcription_audio._find_ffmpeg_binary", return_value=None):
|
||||
assert _trim_silence_for_cloud_stt(wav, {}) is None
|
||||
|
||||
def test_missing_ffprobe_returns_none(self, tmp_path):
|
||||
wav = _write_wav(tmp_path / "a.wav", [("tone", 1)])
|
||||
with patch("tools.transcription_tools._find_ffmpeg_binary", return_value="/bin/ffmpeg"), \
|
||||
patch("tools.transcription_tools._find_ffprobe_binary", return_value=None):
|
||||
with patch("tools.transcription_audio._find_ffmpeg_binary", return_value="/bin/ffmpeg"), \
|
||||
patch("tools.transcription_audio._find_ffprobe_binary", return_value=None):
|
||||
assert _trim_silence_for_cloud_stt(wav, {}) is None
|
||||
|
||||
def test_ffmpeg_failure_returns_none_and_cleans_up(self, tmp_path):
|
||||
@@ -247,16 +246,16 @@ class TestTrimFallbacks:
|
||||
def probe(path):
|
||||
return 60.0 # past the short-clip gate so the encode is attempted
|
||||
|
||||
with patch("tools.transcription_tools._find_ffmpeg_binary", return_value="/bin/ffmpeg"), \
|
||||
patch("tools.transcription_tools._probe_audio_duration", side_effect=probe), \
|
||||
patch("tools.transcription_tools.subprocess.run",
|
||||
with patch("tools.transcription_audio._find_ffmpeg_binary", return_value="/bin/ffmpeg"), \
|
||||
patch("tools.transcription_audio._probe_audio_duration", side_effect=probe), \
|
||||
patch("tools.transcription_audio.subprocess.run",
|
||||
side_effect=sp.CalledProcessError(1, "ffmpeg")):
|
||||
assert _trim_silence_for_cloud_stt(wav, {}) is None
|
||||
|
||||
def test_unprobeable_source_returns_none(self, tmp_path):
|
||||
wav = _write_wav(tmp_path / "a.wav", [("tone", 1)])
|
||||
with patch("tools.transcription_tools._find_ffmpeg_binary", return_value="/bin/ffmpeg"), \
|
||||
patch("tools.transcription_tools._probe_audio_duration", return_value=None):
|
||||
with patch("tools.transcription_audio._find_ffmpeg_binary", return_value="/bin/ffmpeg"), \
|
||||
patch("tools.transcription_audio._probe_audio_duration", return_value=None):
|
||||
assert _trim_silence_for_cloud_stt(wav, {}) is None
|
||||
|
||||
|
||||
@@ -274,7 +273,7 @@ class TestTrimE2E:
|
||||
tmp_path / "pauses.wav",
|
||||
[("tone", 2), ("silence", 6), ("tone", 2), ("silence", 4)],
|
||||
)
|
||||
from tools.transcription_tools import _probe_audio_duration
|
||||
from tools.transcription_audio import _probe_audio_duration
|
||||
trimmed = _trim_silence_for_cloud_stt(wav, {})
|
||||
assert trimmed is not None
|
||||
try:
|
||||
@@ -306,7 +305,7 @@ class TestTrimE2E:
|
||||
wav = _write_wav(
|
||||
tmp_path / "short.wav", [("tone", 2), ("silence", 4), ("tone", 2)]
|
||||
)
|
||||
with patch.object(tt_module, "_run_ffmpeg_stt_encode") as mock_encode:
|
||||
with patch("tools.transcription_audio._run_ffmpeg_stt_encode") as mock_encode:
|
||||
assert _trim_silence_for_cloud_stt(wav, {}) is None
|
||||
mock_encode.assert_not_called()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ three-layer fix at every local whisper call site:
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from tools.transcription_tools import (
|
||||
from tools.transcription_local import (
|
||||
_LOGPROB_THRESHOLD_DEFAULT,
|
||||
_NO_SPEECH_PROB_THRESHOLD_DEFAULT,
|
||||
_is_hallucinated_segment,
|
||||
|
||||
@@ -71,7 +71,8 @@ class TestValidateAudioFile:
|
||||
def test_too_large(self, tmp_path):
|
||||
f = tmp_path / "big.ogg"
|
||||
f.write_bytes(b"x")
|
||||
from tools.transcription_tools import _validate_audio_file, MAX_FILE_SIZE
|
||||
from tools.transcription_tools import _validate_audio_file
|
||||
from tools.transcription_common import MAX_FILE_SIZE
|
||||
real_stat = f.stat()
|
||||
with patch.object(type(f), "stat", return_value=os.stat_result((
|
||||
real_stat.st_mode, real_stat.st_ino, real_stat.st_dev,
|
||||
|
||||
@@ -25,8 +25,8 @@ from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
from tools.transcription_tools import (
|
||||
BUILTIN_STT_PROVIDERS,
|
||||
from tools.transcription_common import BUILTIN_STT_PROVIDERS
|
||||
from tools.transcription_command import (
|
||||
COMMAND_STT_OUTPUT_FORMATS,
|
||||
DEFAULT_COMMAND_STT_LANGUAGE,
|
||||
DEFAULT_COMMAND_STT_OUTPUT_FORMAT,
|
||||
@@ -37,6 +37,8 @@ from tools.transcription_tools import (
|
||||
_render_command_stt_template,
|
||||
_resolve_command_stt_provider_config,
|
||||
_transcribe_command_stt,
|
||||
)
|
||||
from tools.transcription_tools import (
|
||||
transcribe_audio,
|
||||
)
|
||||
|
||||
|
||||
@@ -212,7 +212,8 @@ class TestTranscribeAudioE2E:
|
||||
transcription_registry.register_provider(provider)
|
||||
audio_path = tmp_path / "oversized.mp3"
|
||||
with audio_path.open("wb") as audio_file:
|
||||
audio_file.seek(transcription_tools.MAX_FILE_SIZE)
|
||||
from tools.transcription_common import MAX_FILE_SIZE
|
||||
audio_file.seek(MAX_FILE_SIZE)
|
||||
audio_file.write(b"\0")
|
||||
|
||||
with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "openrouter"}), \
|
||||
|
||||
@@ -65,7 +65,7 @@ def sample_silk(tmp_path):
|
||||
@pytest.fixture
|
||||
def oversized_wav(tmp_path):
|
||||
"""Create a sparse WAV-shaped file just above the remote upload cap."""
|
||||
from tools.transcription_tools import MAX_FILE_SIZE
|
||||
from tools.transcription_common import MAX_FILE_SIZE
|
||||
|
||||
wav_path = tmp_path / "oversized.wav"
|
||||
with wav_path.open("wb") as audio_file:
|
||||
@@ -248,7 +248,7 @@ class TestTranscribeLocalCommand:
|
||||
|
||||
monkeypatch.setattr("tools.tts_command_provider.subprocess.Popen", fake_popen)
|
||||
|
||||
from tools.transcription_tools import _run_command_stt
|
||||
from tools.transcription_command import _run_command_stt
|
||||
|
||||
result = _run_command_stt("echo hi", timeout=1)
|
||||
|
||||
@@ -292,10 +292,10 @@ class TestTranscribeLocalCommand:
|
||||
returncode = 0
|
||||
return R()
|
||||
|
||||
monkeypatch.setattr("tools.transcription_tools.tempfile.TemporaryDirectory", fake_tempdir)
|
||||
monkeypatch.setattr("tools.transcription_tools.subprocess.run", fake_run)
|
||||
monkeypatch.setattr("tools.transcription_local.tempfile.TemporaryDirectory", fake_tempdir)
|
||||
monkeypatch.setattr("tools.transcription_audio.subprocess.run", fake_run)
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools._prepare_local_audio",
|
||||
"tools.transcription_local._prepare_local_audio",
|
||||
lambda *a, **k: (str(sample_wav), None),
|
||||
)
|
||||
|
||||
@@ -334,9 +334,9 @@ class TestTranscribeLocalCommand:
|
||||
(out_dir / "test.txt").write_text("hello from local command\n", encoding="utf-8")
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr("tools.transcription_tools.tempfile.TemporaryDirectory", fake_tempdir)
|
||||
monkeypatch.setattr("tools.transcription_tools._find_ffmpeg_binary", lambda: "/opt/homebrew/bin/ffmpeg")
|
||||
monkeypatch.setattr("tools.transcription_tools.subprocess.run", fake_run)
|
||||
monkeypatch.setattr("tools.transcription_local.tempfile.TemporaryDirectory", fake_tempdir)
|
||||
monkeypatch.setattr("tools.transcription_audio._find_ffmpeg_binary", lambda: "/opt/homebrew/bin/ffmpeg")
|
||||
monkeypatch.setattr("tools.transcription_audio.subprocess.run", fake_run)
|
||||
|
||||
from tools.transcription_tools import _transcribe_local_command
|
||||
|
||||
@@ -509,7 +509,8 @@ class TestValidateAudioFileEdgeCases:
|
||||
|
||||
|
||||
def test_all_supported_formats_accepted(self, tmp_path):
|
||||
from tools.transcription_tools import _validate_audio_file, SUPPORTED_FORMATS
|
||||
from tools.transcription_tools import _validate_audio_file
|
||||
from tools.transcription_common import SUPPORTED_FORMATS
|
||||
for fmt in SUPPORTED_FORMATS:
|
||||
f = tmp_path / f"test{fmt}"
|
||||
f.write_bytes(b"data")
|
||||
@@ -925,7 +926,7 @@ class TestTranscribeAudioElevenLabsDispatch:
|
||||
|
||||
class TestExtractTranscriptText:
|
||||
def test_strips_qwen3_asr_language_envelope(self):
|
||||
from tools.transcription_tools import _extract_transcript_text
|
||||
from tools.transcription_cloud import _extract_transcript_text
|
||||
|
||||
result = _extract_transcript_text(
|
||||
"language zh\n<audio_language>zh</audio_language>\n<asr_text>你好,世界",
|
||||
@@ -934,7 +935,7 @@ class TestExtractTranscriptText:
|
||||
assert result == "你好,世界"
|
||||
|
||||
def test_keeps_non_envelope_marker_literal(self):
|
||||
from tools.transcription_tools import _extract_transcript_text
|
||||
from tools.transcription_cloud import _extract_transcript_text
|
||||
|
||||
result = _extract_transcript_text(
|
||||
"The user literally said <asr_text> while reading markup.",
|
||||
@@ -951,10 +952,10 @@ class TestShellSafety:
|
||||
import shlex
|
||||
monkeypatch.delenv("HERMES_LOCAL_STT_COMMAND", raising=False)
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools._find_whisper_binary",
|
||||
"tools.transcription_local._find_whisper_binary",
|
||||
lambda: "/usr/bin/whisper",
|
||||
)
|
||||
from tools.transcription_tools import _get_local_command_template
|
||||
from tools.transcription_local import _get_local_command_template
|
||||
template = _get_local_command_template()
|
||||
assert template is not None
|
||||
cmd = template.format(
|
||||
@@ -970,10 +971,10 @@ class TestShellSafety:
|
||||
def test_env_var_template_metacharacters_are_literal_argv(
|
||||
self, monkeypatch, sample_wav, tmp_path
|
||||
):
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
from tools.transcription_tools import (
|
||||
LOCAL_STT_COMMAND_ENV,
|
||||
_transcribe_local_command,
|
||||
windows_hide_flags,
|
||||
)
|
||||
|
||||
output_dir = tmp_path / "transcript-output"
|
||||
@@ -1005,9 +1006,9 @@ class TestShellSafety:
|
||||
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools.tempfile.TemporaryDirectory", fake_tempdir
|
||||
"tools.transcription_local.tempfile.TemporaryDirectory", fake_tempdir
|
||||
)
|
||||
monkeypatch.setattr("tools.transcription_tools.subprocess.run", fake_run)
|
||||
monkeypatch.setattr("tools.transcription_audio.subprocess.run", fake_run)
|
||||
|
||||
result = _transcribe_local_command(sample_wav, "base")
|
||||
|
||||
@@ -1102,7 +1103,7 @@ class TestLocalBaseUrlNoApiKey:
|
||||
|
||||
|
||||
def test_is_local_or_private_url(self):
|
||||
from tools.transcription_tools import _is_local_or_private_url
|
||||
from tools.transcription_cloud import _is_local_or_private_url
|
||||
assert _is_local_or_private_url("http://localhost:8504/v1")
|
||||
assert _is_local_or_private_url("http://127.0.0.1:9000")
|
||||
assert _is_local_or_private_url("http://10.0.0.5/v1")
|
||||
@@ -1131,7 +1132,7 @@ class TestCafConversion:
|
||||
return MagicMock(returncode=0)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools._find_ffmpeg_binary",
|
||||
"tools.transcription_audio._find_ffmpeg_binary",
|
||||
lambda: "/usr/bin/ffmpeg",
|
||||
)
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
@@ -1199,7 +1200,7 @@ class TestRunCommandSttIdleTimeout:
|
||||
def test_stderr_progress_extends_beyond_timeout(self, tmp_path):
|
||||
"""A slow-but-alive command that keeps emitting output survives an
|
||||
idle timeout shorter than its total runtime."""
|
||||
from tools.transcription_tools import _run_command_stt
|
||||
from tools.transcription_command import _run_command_stt
|
||||
|
||||
script = tmp_path / "progress_then_exit.py"
|
||||
script.write_text(
|
||||
@@ -1225,7 +1226,7 @@ class TestRunCommandSttIdleTimeout:
|
||||
def test_silent_stall_still_times_out(self, tmp_path):
|
||||
"""A silently stalled command is killed once the idle window elapses,
|
||||
and pre-stall output is preserved on the TimeoutExpired."""
|
||||
from tools.transcription_tools import _run_command_stt
|
||||
from tools.transcription_command import _run_command_stt
|
||||
|
||||
script = tmp_path / "progress_then_hang.py"
|
||||
script.write_text(
|
||||
@@ -1265,11 +1266,11 @@ class TestExplicitOpenaiSelectionError:
|
||||
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools.resolve_openai_audio_api_key",
|
||||
"tools.tool_backend_helpers.resolve_openai_audio_api_key",
|
||||
lambda: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools.resolve_managed_tool_gateway",
|
||||
"tools.managed_tool_gateway.resolve_managed_tool_gateway",
|
||||
lambda vendor: None,
|
||||
)
|
||||
|
||||
@@ -1278,7 +1279,7 @@ class TestExplicitOpenaiSelectionError:
|
||||
):
|
||||
self._no_openai_credentials(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools.managed_nous_tools_enabled", lambda: True
|
||||
"tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: True
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools._load_stt_config", lambda: {}
|
||||
@@ -1303,7 +1304,7 @@ class TestExplicitOpenaiSelectionError:
|
||||
hint."""
|
||||
self._no_openai_credentials(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools.managed_nous_tools_enabled", lambda: True
|
||||
"tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: True
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"tools.transcription_tools._load_stt_config", lambda: {}
|
||||
@@ -1311,7 +1312,7 @@ class TestExplicitOpenaiSelectionError:
|
||||
with patch("tools.transcription_tools._HAS_OPENAI", True), \
|
||||
patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
|
||||
patch(
|
||||
"tools.transcription_tools.nous_tool_gateway_unavailable_message",
|
||||
"tools.tool_backend_helpers.nous_tool_gateway_unavailable_message",
|
||||
lambda what: f"managed route down for {what}; run `hermes tools`",
|
||||
):
|
||||
from tools.transcription_tools import _dispatch_stt_provider
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
Binary discovery, the shared ffmpeg m4a encode (transcode + silence trim),
|
||||
source/format validation, WeChat .silk decoding, CAF conversion and the
|
||||
best-effort cloud pre-upload silence trim. Every name is re-imported by
|
||||
``tools/transcription_tools.py`` (patch surface), which is imported lazily here
|
||||
so origin patches still intercept.
|
||||
best-effort cloud pre-upload silence trim. Facade-owned state (``_HAS_PILK``,
|
||||
``_safe_find_spec``) is read lazily from ``tools.transcription_tools``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -66,7 +65,6 @@ def _transcode_audio_for_stt(file_path: str, work_dir: str) -> tuple[Optional[st
|
||||
"""Transcode to a compact 16 kHz mono AAC/m4a for STT upload; ``(converted_path, None)`` or ``(None, error)``.
|
||||
Newer OpenAI models reject containers ``whisper-1`` accepted (notably Ogg/Opus voice notes) and
|
||||
gateway downloads may carry a misleading extension."""
|
||||
from tools.transcription_tools import _find_ffmpeg_binary, _run_ffmpeg_stt_encode
|
||||
ffmpeg = _find_ffmpeg_binary()
|
||||
if not ffmpeg:
|
||||
return None, "audio needs transcoding for the STT API, but ffmpeg was not found"
|
||||
@@ -144,7 +142,6 @@ def _prepare_audio_for_transcription(file_path: str) -> tuple[Optional[str], Opt
|
||||
|
||||
def _prepare_local_audio(file_path: str, work_dir: str) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Normalize audio for local CLI STT when needed."""
|
||||
from tools.transcription_tools import _find_ffmpeg_binary
|
||||
audio_path = Path(file_path)
|
||||
if audio_path.suffix.lower() in LOCAL_NATIVE_AUDIO_FORMATS:
|
||||
return file_path, None
|
||||
@@ -166,7 +163,6 @@ def _prepare_local_audio(file_path: str, work_dir: str) -> tuple[Optional[str],
|
||||
|
||||
def _convert_caf_to_wav(file_path: str) -> Optional[str]:
|
||||
"""Convert CAF to WAV using ffmpeg or afconvert (macOS)."""
|
||||
from tools.transcription_tools import _find_ffmpeg_binary
|
||||
audio_path = Path(file_path)
|
||||
wav_path = os.path.join(audio_path.parent, f"{audio_path.stem}.wav")
|
||||
ffmpeg = _find_ffmpeg_binary()
|
||||
@@ -209,7 +205,6 @@ def _probe_audio_duration(file_path: str) -> Optional[float]:
|
||||
"""Return the audio duration in seconds via ffprobe, or None. Canonical sync probe;
|
||||
``gateway/run.py._probe_audio_duration`` and the Telegram adapter carry local variants — keep
|
||||
the command shape in sync."""
|
||||
from tools.transcription_tools import _find_ffprobe_binary
|
||||
ffprobe = _find_ffprobe_binary()
|
||||
if not ffprobe:
|
||||
return None
|
||||
@@ -234,7 +229,6 @@ def _cloud_trim_settings(stt_config: Dict[str, Any]) -> tuple[bool, int, int]:
|
||||
def _trim_silence_for_cloud_stt(file_path: str, stt_config: Dict[str, Any]) -> Optional[str]:
|
||||
"""Return a silence-trimmed copy of *file_path* for cloud upload, or None (= upload the original).
|
||||
On success the caller owns deleting the returned file's parent directory."""
|
||||
from tools.transcription_tools import _find_ffmpeg_binary, _probe_audio_duration, _run_ffmpeg_stt_encode
|
||||
enabled, threshold_db, keep_ms = _cloud_trim_settings(stt_config)
|
||||
if not enabled:
|
||||
return None
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
OpenAI-SDK-shaped backends (groq, openai, deepinfra), Mistral Voxtral, REST multipart
|
||||
backends (xAI, ElevenLabs), and OpenAI audio credential resolution (config > keyless
|
||||
local server > env > managed Nous gateway). Every name is re-imported by
|
||||
``tools/transcription_tools.py`` (patch surface), imported lazily here so origin
|
||||
patches still intercept.
|
||||
local server > env > managed Nous gateway). Facade-owned state and helpers
|
||||
(``_HAS_OPENAI``, ``_resolve_provider_key``, ``_resolve_stt_language``, ``_load_stt_config``,
|
||||
``get_env_value``) are read lazily from ``tools.transcription_tools``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -111,7 +111,7 @@ def _transcribe_openai(
|
||||
"""Transcribe via the OpenAI ``audio.transcriptions.create`` SDK shape, shared by every
|
||||
OpenAI-compatible endpoint (DeepInfra etc.): explicit ``api_key``/``base_url`` skip the
|
||||
OpenAI-only auth chain; ``provider_label`` names the response's provider."""
|
||||
from tools.transcription_tools import _HAS_OPENAI, _resolve_openai_audio_client_config, _resolve_stt_language
|
||||
from tools.transcription_tools import _HAS_OPENAI, _resolve_stt_language
|
||||
if api_key is None:
|
||||
try:
|
||||
api_key, fallback_base = _resolve_openai_audio_client_config()
|
||||
@@ -329,7 +329,7 @@ def _transcribe_deepinfra(
|
||||
file_path: str, model_name: str, *, language: Optional[str] = None, prompt: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Resolve DeepInfra credentials/model (shared ``hermes_cli.models`` helpers), then delegate to :func:`_transcribe_openai`."""
|
||||
from tools.transcription_tools import _load_stt_config, _resolve_provider_key, _transcribe_openai
|
||||
from tools.transcription_tools import _load_stt_config, _resolve_provider_key
|
||||
api_key = _resolve_provider_key("DEEPINFRA_API_KEY", "deepinfra")
|
||||
if not api_key:
|
||||
return _error_result("DEEPINFRA_API_KEY not set")
|
||||
@@ -364,7 +364,7 @@ def _is_local_or_private_url(url: str) -> bool:
|
||||
def _direct_openai_credentials(cfg_api_key: str, cfg_base_url: str) -> Optional[tuple[str, str]]:
|
||||
"""Direct-credential ladder: config key > keyless local base_url (placeholder key so the SDK
|
||||
constructs a client) > env key; None if none apply."""
|
||||
from tools.transcription_tools import resolve_openai_audio_api_key
|
||||
from tools.tool_backend_helpers import resolve_openai_audio_api_key
|
||||
if cfg_api_key:
|
||||
return cfg_api_key, (cfg_base_url or OPENAI_BASE_URL)
|
||||
# A local OpenAI-compatible server needs no key — send a placeholder so the SDK doesn't refuse to
|
||||
@@ -380,10 +380,11 @@ def _resolve_openai_audio_client_config() -> tuple[str, str]:
|
||||
``"nous"`` -> managed gateway ONLY (a direct OPENAI_API_KEY must NOT override it); any other
|
||||
stored provider -> direct credentials ONLY (no silent managed fallback); never-configured ->
|
||||
legacy ladder: direct credentials, then the managed gateway. Failures raise ValueError."""
|
||||
from tools.transcription_tools import (
|
||||
_load_stt_config, managed_nous_tools_enabled, nous_tool_gateway_unavailable_message,
|
||||
resolve_managed_tool_gateway)
|
||||
from tools.tool_backend_helpers import NOUS_MANAGED_PROVIDER, read_selection, selection_error
|
||||
from tools.transcription_tools import _load_stt_config
|
||||
from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
||||
from tools.tool_backend_helpers import (
|
||||
NOUS_MANAGED_PROVIDER, managed_nous_tools_enabled, nous_tool_gateway_unavailable_message,
|
||||
read_selection, selection_error)
|
||||
openai_cfg = _load_stt_config().get("openai") or {}
|
||||
selected = read_selection("stt")
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
``stt.providers.<name>: type: command`` registry, plugin-registered
|
||||
``TranscriptionProvider`` dispatch, and the ``pre_transcription`` hook that
|
||||
threads prompt/language/model overrides into every backend. Every name is
|
||||
re-imported by ``tools/transcription_tools.py`` (patch surface), which is
|
||||
imported lazily here so origin patches still intercept.
|
||||
threads prompt/language/model overrides into every backend. ``_resolve_stt_language``
|
||||
is read lazily from ``tools.transcription_tools``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
"""Constants, result envelopes and tiny config readers shared by every STT module.
|
||||
|
||||
Every name is re-imported by ``tools/transcription_tools.py``, so
|
||||
``tools.transcription_tools.<name>`` keeps resolving (and monkeypatching).
|
||||
"""
|
||||
"""Constants, result envelopes and tiny config readers shared by every STT module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -11,7 +7,7 @@ import os
|
||||
import subprocess # noqa: F401 (type annotation only)
|
||||
from typing import Any, Dict
|
||||
|
||||
from tools.tts_command_provider import _get_provider_section as _get_stt_section # noqa: F401 (re-exported)
|
||||
from tools.tts_command_provider import _get_provider_section as _get_stt_section
|
||||
|
||||
# Log-record parity with the origin module.
|
||||
logger = logging.getLogger("tools.transcription_tools")
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
faster-whisper loading (CUDA->CPU fallback, Apple Silicon pinning), the
|
||||
anti-hallucination transcribe kwargs and segment gate, and the local whisper CLI
|
||||
(``local_command``) provider. The cached-model singleton and idle-unload watcher
|
||||
stay in ``transcription_tools`` (module state), which re-imports every name here
|
||||
(patch surface) and is imported lazily so origin patches still intercept.
|
||||
stay in ``transcription_tools`` (module state) and are read from it lazily.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,7 +18,7 @@ import importlib.util as _ilu
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from tools.transcription_audio import _run_quiet
|
||||
from tools.transcription_audio import _find_whisper_binary, _prepare_local_audio, _run_quiet
|
||||
from tools.transcription_common import (
|
||||
DEFAULT_LOCAL_MODEL, DEFAULT_LOCAL_STT_LANGUAGE, GROQ_MODELS, LOCAL_STT_COMMAND_ENV,
|
||||
OPENAI_MODELS, _config_number, _error_result, _log_prompt_unsupported, _ok_result,
|
||||
@@ -30,7 +29,6 @@ logger = logging.getLogger("tools.transcription_tools")
|
||||
|
||||
|
||||
def _get_local_command_template() -> Optional[str]:
|
||||
from tools.transcription_tools import _find_whisper_binary
|
||||
configured = os.getenv(LOCAL_STT_COMMAND_ENV, "").strip()
|
||||
if configured:
|
||||
return configured
|
||||
@@ -225,7 +223,7 @@ def _transcribe_local_command(
|
||||
file_path: str, model_name: str, *, language: Optional[str] = None, prompt: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Run the configured local STT command template and read back a .txt transcript."""
|
||||
from tools.transcription_tools import _prepare_local_audio, _resolve_stt_language
|
||||
from tools.transcription_tools import _resolve_stt_language
|
||||
if prompt:
|
||||
_log_prompt_unsupported("STT provider 'local_command'")
|
||||
command_template = _get_local_command_template()
|
||||
|
||||
@@ -6,55 +6,39 @@ Built-in providers: local (faster-whisper, default/free), local_command, groq, o
|
||||
user-declared command providers and plugin providers. ``transcribe_audio(path)`` returns
|
||||
``{"success", "transcript", "error"?, "provider"?}``. This module owns provider resolution,
|
||||
the dispatcher and the cached local model + idle-unload state; backends live in
|
||||
``transcription_{common,audio,local,cloud,command}`` and are re-imported here so
|
||||
``tools.transcription_tools.<name>`` stays the patch surface.
|
||||
``transcription_{common,audio,local,cloud,command}``.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess # noqa: F401 (tests patch tools.transcription_tools.subprocess.run)
|
||||
import tempfile # noqa: F401 (tests patch tools.transcription_tools.tempfile.TemporaryDirectory)
|
||||
import threading
|
||||
import time
|
||||
import importlib.util as _ilu
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags # noqa: F401 (imported by tests)
|
||||
from utils import is_truthy_value
|
||||
from tools.managed_tool_gateway import resolve_managed_tool_gateway # noqa: F401 (patched by tests)
|
||||
from tools.tool_backend_helpers import ( # noqa: F401 (patched by tests; read lazily by transcription_cloud)
|
||||
managed_nous_tools_enabled, nous_tool_gateway_unavailable_message, resolve_openai_audio_api_key)
|
||||
from tools.transcription_common import ( # noqa: F401 (re-exported; tests patch tools.transcription_tools.<name>)
|
||||
from tools.transcription_common import (
|
||||
BUILTIN_STT_PROVIDERS, CLOUD_STT_PROVIDERS, DEFAULT_ELEVENLABS_STT_MODEL,
|
||||
DEFAULT_GROQ_STT_MODEL, DEFAULT_LOCAL_MODEL, DEFAULT_MISTRAL_STT_MODEL, DEFAULT_PROVIDER,
|
||||
DEFAULT_STT_MODEL, ELEVENLABS_STT_BASE_URL, GROQ_MODELS, LOCAL_STT_COMMAND_ENV,
|
||||
LOCAL_STT_LANGUAGE_ENV, MAX_FILE_SIZE, OPENAI_MODELS, SUPPORTED_FORMATS, XAI_STT_BASE_URL,
|
||||
_error_result, _get_stt_section, _ok_result)
|
||||
from tools.transcription_audio import ( # noqa: F401 (re-exported; tests patch tools.transcription_tools.<name>)
|
||||
_CLOUD_TRIM_KEEP_MS_DEFAULT, _CLOUD_TRIM_MIN_INPUT_SECONDS, _CLOUD_TRIM_THRESHOLD_DB_DEFAULT,
|
||||
_cloud_trim_settings, _convert_caf_to_wav, _find_ffmpeg_binary, _find_ffprobe_binary,
|
||||
_find_whisper_binary, _prepare_audio_for_transcription, _prepare_local_audio,
|
||||
_probe_audio_duration, _run_ffmpeg_stt_encode, _trim_silence_for_cloud_stt,
|
||||
DEFAULT_STT_MODEL, LOCAL_STT_COMMAND_ENV, LOCAL_STT_LANGUAGE_ENV, _error_result,
|
||||
_get_stt_section, _ok_result)
|
||||
from tools.transcription_audio import (
|
||||
_convert_caf_to_wav, _prepare_audio_for_transcription, _trim_silence_for_cloud_stt,
|
||||
_validate_audio_file, _validate_audio_file_size, _validate_audio_source_file)
|
||||
from tools.transcription_local import ( # noqa: F401 (re-exported; tests patch tools.transcription_tools.<name>)
|
||||
_LOGPROB_THRESHOLD_DEFAULT, _NO_SPEECH_PROB_THRESHOLD_DEFAULT, _get_idle_unload_seconds,
|
||||
_get_local_command_template, _has_local_command, _is_hallucinated_segment,
|
||||
_join_confident_segments, _load_local_whisper_model, _looks_like_cuda_lib_error,
|
||||
_normalize_local_model, _transcribe_local_command, _try_lazy_install_stt,
|
||||
build_local_transcribe_kwargs)
|
||||
from tools.transcription_cloud import ( # noqa: F401 (re-exported; tests patch tools.transcription_tools.<name>)
|
||||
_extract_transcript_text, _has_xai_stt_credentials, _is_local_or_private_url,
|
||||
_resolve_openai_audio_client_config, _transcribe_deepinfra, _transcribe_elevenlabs,
|
||||
_transcribe_groq, _transcribe_mistral, _transcribe_openai, _transcribe_xai)
|
||||
from tools.transcription_command import ( # noqa: F401 (re-exported; tests patch tools.transcription_tools.<name>)
|
||||
COMMAND_STT_OUTPUT_FORMATS, DEFAULT_COMMAND_STT_LANGUAGE, DEFAULT_COMMAND_STT_OUTPUT_FORMAT,
|
||||
DEFAULT_COMMAND_STT_TIMEOUT_SECONDS, _PROMPT_CHARS_PER_TOKEN, _WHISPER_PROMPT_TOKEN_CAP,
|
||||
from tools.transcription_local import (
|
||||
_get_idle_unload_seconds, _has_local_command, _join_confident_segments,
|
||||
_load_local_whisper_model, _looks_like_cuda_lib_error, _normalize_local_model,
|
||||
_transcribe_local_command, _try_lazy_install_stt, build_local_transcribe_kwargs)
|
||||
# The ``_transcribe_<provider>`` handlers are looked up in this module's globals by _dispatch_stt_provider.
|
||||
from tools.transcription_cloud import ( # noqa: F401 (handlers dispatched via globals())
|
||||
_has_xai_stt_credentials, _resolve_openai_audio_client_config, _transcribe_deepinfra,
|
||||
_transcribe_elevenlabs, _transcribe_groq, _transcribe_mistral, _transcribe_openai,
|
||||
_transcribe_xai)
|
||||
from tools.transcription_command import (
|
||||
_apply_pre_transcription_hook, _dispatch_to_plugin_provider, _enforce_prompt_length_limit,
|
||||
_get_command_stt_output_format, _get_command_stt_timeout, _get_named_stt_provider_config,
|
||||
_render_command_stt_template, _resolve_command_stt_provider_config, _run_command_stt,
|
||||
_transcribe_command_stt, _unregistered_stt_provider_error)
|
||||
_resolve_command_stt_provider_config, _transcribe_command_stt, _unregistered_stt_provider_error)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@ def _deepinfra_model(section: Dict[str, Any], kind: str) -> Optional[str]:
|
||||
|
||||
|
||||
# ── STT ──
|
||||
# provider -> (env var, default-model attr on transcription_tools, base_url).
|
||||
# ``base_url`` is a transcription_tools attr name or a literal URL.
|
||||
# provider -> (env var, default-model attr on transcription_common, base_url).
|
||||
# ``base_url`` is a transcription_common attr name or a literal URL.
|
||||
_STT_KEYED: Dict[str, tuple[str, str, str]] = {
|
||||
"groq": ("GROQ_API_KEY", "DEFAULT_GROQ_STT_MODEL", "GROQ_BASE_URL"),
|
||||
"mistral": ("MISTRAL_API_KEY", "DEFAULT_MISTRAL_STT_MODEL", "https://api.mistral.ai/v1"),
|
||||
@@ -78,6 +78,7 @@ _STT_KEYED: Dict[str, tuple[str, str, str]] = {
|
||||
|
||||
|
||||
def _resolve_stt_client_config() -> Dict[str, Any]:
|
||||
from tools import transcription_common as tc
|
||||
from tools import transcription_tools as tt
|
||||
|
||||
stt_config = tt._load_stt_config()
|
||||
@@ -88,7 +89,7 @@ def _resolve_stt_client_config() -> Dict[str, Any]:
|
||||
# declared command providers, and anything plugin-registered.
|
||||
if tt._is_local_stt_provider(provider, stt_config):
|
||||
return _relay("local provider")
|
||||
if provider not in tt.BUILTIN_STT_PROVIDERS:
|
||||
if provider not in tc.BUILTIN_STT_PROVIDERS:
|
||||
return _relay("command/plugin provider")
|
||||
|
||||
language = tt._resolve_stt_language(
|
||||
@@ -106,8 +107,8 @@ def _resolve_stt_client_config() -> Dict[str, Any]:
|
||||
api_key = tt._resolve_provider_key(env_var, provider)
|
||||
if not api_key:
|
||||
return _relay("no credentials")
|
||||
return direct(STT_WIRE_OPENAI, getattr(tt, base, base), api_key,
|
||||
section.get("model") or getattr(tt, default_model))
|
||||
return direct(STT_WIRE_OPENAI, getattr(tc, base, base), api_key,
|
||||
section.get("model") or getattr(tc, default_model))
|
||||
if provider == "openai":
|
||||
# Covers the Nous-managed selection too: the resolver returns the user's
|
||||
# own gateway token + managed base URL — exactly what the client should use.
|
||||
@@ -115,20 +116,20 @@ def _resolve_stt_client_config() -> Dict[str, Any]:
|
||||
api_key, base_url = tt._resolve_openai_audio_client_config()
|
||||
except ValueError as exc:
|
||||
return _relay(f"openai resolution failed: {exc}")
|
||||
return direct(STT_WIRE_OPENAI, base_url, api_key, section.get("model") or tt.DEFAULT_STT_MODEL)
|
||||
return direct(STT_WIRE_OPENAI, base_url, api_key, section.get("model") or tc.DEFAULT_STT_MODEL)
|
||||
if provider == "xai":
|
||||
# API key only: an xAI OAuth bearer refreshes server-side mid-session and
|
||||
# would strand the client on the first 401.
|
||||
api_key = str(tt.get_env_value("XAI_API_KEY") or "").strip()
|
||||
if not api_key:
|
||||
return _relay("xai oauth (server-managed) or no credentials")
|
||||
return direct(STT_WIRE_XAI, env_base_url("XAI_STT_BASE_URL", tt.XAI_STT_BASE_URL), api_key, None)
|
||||
return direct(STT_WIRE_XAI, env_base_url("XAI_STT_BASE_URL", tc.XAI_STT_BASE_URL), api_key, None)
|
||||
if provider == "elevenlabs":
|
||||
api_key = tt._resolve_provider_key("ELEVENLABS_API_KEY", "elevenlabs")
|
||||
if not api_key:
|
||||
return _relay("no credentials")
|
||||
return direct(STT_WIRE_ELEVENLABS, env_base_url("ELEVENLABS_STT_BASE_URL", tt.ELEVENLABS_STT_BASE_URL),
|
||||
api_key, section.get("model") or tt.DEFAULT_ELEVENLABS_STT_MODEL)
|
||||
return direct(STT_WIRE_ELEVENLABS, env_base_url("ELEVENLABS_STT_BASE_URL", tc.ELEVENLABS_STT_BASE_URL),
|
||||
api_key, section.get("model") or tc.DEFAULT_ELEVENLABS_STT_MODEL)
|
||||
if provider == "deepinfra":
|
||||
api_key = tt._resolve_provider_key("DEEPINFRA_API_KEY", "deepinfra")
|
||||
if not api_key:
|
||||
@@ -144,6 +145,7 @@ def _resolve_stt_client_config() -> Dict[str, Any]:
|
||||
# ── TTS ──
|
||||
def _resolve_tts_client_config() -> Dict[str, Any]:
|
||||
from tools import tts_tool as tts
|
||||
from tools import tts_tool_openai, tts_tool_providers
|
||||
|
||||
tts_config = tts._load_tts_config()
|
||||
provider = tts._get_provider(tts_config)
|
||||
@@ -153,24 +155,24 @@ def _resolve_tts_client_config() -> Dict[str, Any]:
|
||||
if provider == "openai":
|
||||
# Covers the direct-key, custom-base_url, and Nous-managed selections.
|
||||
try:
|
||||
api_key, base_url, is_managed = tts._resolve_openai_audio_client_config()
|
||||
api_key, base_url, is_managed = tts_tool_openai._resolve_openai_audio_client_config()
|
||||
except ValueError as exc:
|
||||
return _relay(f"openai resolution failed: {exc}")
|
||||
oai = _section(tts_config, "openai")
|
||||
model = oai.get("model") or tts.DEFAULT_OPENAI_MODEL
|
||||
model = oai.get("model") or tts_tool_openai.DEFAULT_OPENAI_MODEL
|
||||
config_base = oai.get("base_url")
|
||||
base_url = config_base or base_url
|
||||
# The managed gateway only proxies MANAGED_OPENAI_TTS_MODELS — same
|
||||
# coercion text_to_speech applies server-side.
|
||||
if is_managed and not config_base and model not in tts.MANAGED_OPENAI_TTS_MODELS:
|
||||
model = tts.DEFAULT_OPENAI_MODEL
|
||||
if is_managed and not config_base and model not in tts_tool_openai.MANAGED_OPENAI_TTS_MODELS:
|
||||
model = tts_tool_openai.DEFAULT_OPENAI_MODEL
|
||||
speed_default = tts_config.get("speed", 1.0) if isinstance(tts_config, dict) else 1.0
|
||||
try:
|
||||
speed = float(oai.get("speed", speed_default))
|
||||
except (TypeError, ValueError):
|
||||
speed = 1.0
|
||||
return _direct(TTS_WIRE_OPENAI, "openai", base_url, api_key, model,
|
||||
voice=oai.get("voice") or tts.DEFAULT_OPENAI_VOICE, speed=speed)
|
||||
voice=oai.get("voice") or tts_tool_openai.DEFAULT_OPENAI_VOICE, speed=speed)
|
||||
if provider == "elevenlabs":
|
||||
api_key = tts._resolve_provider_key("ELEVENLABS_API_KEY", "elevenlabs")
|
||||
if not api_key:
|
||||
@@ -178,8 +180,8 @@ def _resolve_tts_client_config() -> Dict[str, Any]:
|
||||
el = _section(tts_config, "elevenlabs")
|
||||
return _direct(TTS_WIRE_ELEVENLABS, "elevenlabs",
|
||||
str(el.get("base_url") or "https://api.elevenlabs.io/v1").rstrip("/"),
|
||||
api_key, el.get("model_id") or tts.DEFAULT_ELEVENLABS_MODEL_ID,
|
||||
voice=el.get("voice_id") or tts.DEFAULT_ELEVENLABS_VOICE_ID, speed=None)
|
||||
api_key, el.get("model_id") or tts_tool_providers.DEFAULT_ELEVENLABS_MODEL_ID,
|
||||
voice=el.get("voice_id") or tts_tool_providers.DEFAULT_ELEVENLABS_VOICE_ID, speed=None)
|
||||
if provider == "deepinfra":
|
||||
api_key = tts._resolve_provider_key("DEEPINFRA_API_KEY", "deepinfra")
|
||||
if not api_key:
|
||||
|
||||
+2
-1
@@ -848,7 +848,8 @@ def create_audio_recorder() -> AudioRecorder | TermuxAudioRecorder:
|
||||
def transcribe_recording(wav_path: str, model: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Transcribe a WAV via ``transcribe_audio()``, filtering Whisper hallucinations;
|
||||
returns ``{success, transcript[, error]}``."""
|
||||
from tools.transcription_tools import MAX_FILE_SIZE, transcribe_audio
|
||||
from tools.transcription_common import MAX_FILE_SIZE
|
||||
from tools.transcription_tools import transcribe_audio
|
||||
|
||||
result = transcribe_audio(wav_path, model=model, source="voice_mode")
|
||||
# Only chunk when the provider itself reports "File too large" — local
|
||||
|
||||
Reference in New Issue
Block a user