diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 7e23466..5a6a04a 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -11,7 +11,13 @@ import typer from rich.console import Console from rich.status import Status -from .config import ConfigValue, apply_device_defaults, load_config, resolve_defaults +from .config import ( + CliValues, + ResolvedConfig, + apply_device_defaults, + load_config, + resolve_defaults, +) from .context_menu import install_menu as install_context_menu from .context_menu import uninstall_menu as uninstall_context_menu from .diarization import build_speaker_transcript @@ -337,7 +343,7 @@ def main( try: config = load_config() - cli_values = { + cli_values: CliValues = { "model": model, "language": language, "device": device, @@ -417,7 +423,7 @@ def main( def _run_single( file: Path, - defaults: dict[str, ConfigValue], + defaults: ResolvedConfig, output: Path | None, verbose: bool, compute_type_explicit: bool = False, @@ -561,7 +567,7 @@ def _run_single( def _run_batch( files: list[Path], - defaults: dict[str, ConfigValue], + defaults: ResolvedConfig, verbose: bool, force: bool, compute_type_explicit: bool = False, diff --git a/src/local_transcriber/config.py b/src/local_transcriber/config.py index e3dce72..0f36361 100644 --- a/src/local_transcriber/config.py +++ b/src/local_transcriber/config.py @@ -3,10 +3,43 @@ import tomllib import warnings from pathlib import Path +from typing import TypedDict, TypeVar, cast type ConfigValue = str | bool +_T = TypeVar("_T") -HARDCODED_DEFAULTS: dict[str, ConfigValue] = { + +class ConfigValues(TypedDict, total=False): + """Проверенные значения из TOML-конфига.""" + + model: str + language: str + device: str + compute_type: str + diarize: bool + + +class CliValues(TypedDict, total=False): + """Явные значения CLI до разрешения каскада.""" + + model: str | None + language: str | None + device: str | None + compute_type: str | None + diarize: bool | None + + +class ResolvedConfig(TypedDict): + """Полная конфигурация после разрешения каскада.""" + + model: str + language: str + device: str + compute_type: str + diarize: bool + + +HARDCODED_DEFAULTS: ResolvedConfig = { "model": "medium", "language": "ru", "device": "auto", @@ -49,7 +82,7 @@ def find_config_file() -> Path | None: return None -def load_config(path: Path | None = None) -> dict[str, ConfigValue]: +def load_config(path: Path | None = None) -> ConfigValues: """Загружает и валидирует TOML-конфиг. Неизвестные ключи вызывают предупреждение (а не ошибку) для forward @@ -99,31 +132,53 @@ def load_config(path: Path | None = None) -> dict[str, ConfigValue]: raise ValueError(f"Значение 'language' в {path} не может быть пустым") result[key] = value - return result + return cast(ConfigValues, result) -def resolve_defaults( - cli_values: dict[str, ConfigValue | None], config: dict[str, ConfigValue] -) -> dict[str, ConfigValue]: +def resolve_defaults(cli_values: CliValues, config: ConfigValues) -> ResolvedConfig: """Каскад приоритетов: CLI > конфиг-файл > hardcoded-дефолты.""" - result: dict[str, ConfigValue] = {} - for key in HARDCODED_DEFAULTS: - cli_val = cli_values.get(key) - if cli_val is not None: - result[key] = cli_val - elif key in config: - result[key] = config[key] - else: - result[key] = HARDCODED_DEFAULTS[key] - return result + return { + "model": _resolve_value( + cli_values.get("model"), config.get("model"), HARDCODED_DEFAULTS["model"] + ), + "language": _resolve_value( + cli_values.get("language"), + config.get("language"), + HARDCODED_DEFAULTS["language"], + ), + "device": _resolve_value( + cli_values.get("device"), + config.get("device"), + HARDCODED_DEFAULTS["device"], + ), + "compute_type": _resolve_value( + cli_values.get("compute_type"), + config.get("compute_type"), + HARDCODED_DEFAULTS["compute_type"], + ), + "diarize": _resolve_value( + cli_values.get("diarize"), + config.get("diarize"), + HARDCODED_DEFAULTS["diarize"], + ), + } + + +def _resolve_value(cli_value: _T | None, config_value: _T | None, default: _T) -> _T: + """Выбирает первое явно заданное значение каскада.""" + if cli_value is not None: + return cli_value + if config_value is not None: + return config_value + return default def apply_device_defaults( - defaults: dict[str, ConfigValue], + defaults: ResolvedConfig, resolved_device: str, - cli_values: dict[str, ConfigValue | None], - config: dict[str, ConfigValue], -) -> dict[str, ConfigValue]: + cli_values: CliValues, + config: ConfigValues, +) -> ResolvedConfig: """Применяет device-aware дефолты для model и compute_type, если они не были явно заданы через CLI или конфиг.""" device_defs = DEVICE_DEFAULTS.get(resolved_device, {}) @@ -135,4 +190,4 @@ def apply_device_defaults( if cli_values.get(key) is None and key not in config: if key in device_defs: result[key] = device_defs[key] - return result + return cast(ResolvedConfig, result) diff --git a/tests/test_config.py b/tests/test_config.py index 69fe8a2..2408a01 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,4 +1,3 @@ -from pathlib import Path from unittest.mock import patch import pytest