chore(repo): ветка синхронизирована с origin/master

- Зачем:
  - source commit эксперимента должен включать актуальное состояние master.
- Что:
  - в рабочую ветку влиты изменения конфигурации диаризации и документации.
- Проверка:
  - слияние завершено без конфликтов, рабочее дерево чистое.
This commit is contained in:
Dmitry Dementiev committed 2026-09-04 17:21:42 +03:00
commit 0cd105927b
7 files changed
+237 -49

No files matched your search

+24 -9
View File
@@ -11,7 +11,13 @@ import typer
from rich.console import Console
from rich.status import Status
from .config import 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
@@ -261,9 +267,9 @@ def main(
min=0,
help="Потоки CPU (0 = дефолт библиотеки; рекомендуется = число физ. ядер)",
),
diarize: bool = typer.Option(
False,
"--diarize",
diarize: bool | None = typer.Option(
None,
"--diarize/--no-diarize",
help="Разделить транскрипт на реплики говорящих",
),
speakers: int | None = typer.Option(
@@ -328,15 +334,24 @@ def main(
)
raise SystemExit(2)
if diarize is False and speakers is not None:
console.print(
"--no-diarize и --speakers несовместимы.",
style="red bold",
)
raise SystemExit(2)
try:
config = load_config()
cli_values = {
cli_values: CliValues = {
"model": model,
"language": language,
"device": device,
"compute_type": compute_type,
"diarize": diarize,
}
defaults = resolve_defaults(cli_values, config)
diarize_enabled = defaults["diarize"] is True or speakers is not None
resolved_device = detect_device(defaults["device"])
defaults = apply_device_defaults(defaults, resolved_device, cli_values, config)
@@ -363,7 +378,7 @@ def main(
force,
ct_explicit,
cpu_threads=threads,
diarize=diarize or speakers is not None,
diarize=diarize_enabled,
speakers=speakers,
)
else:
@@ -374,7 +389,7 @@ def main(
verbose,
ct_explicit,
cpu_threads=threads,
diarize=diarize or speakers is not None,
diarize=diarize_enabled,
speakers=speakers,
)
except KeyboardInterrupt:
@@ -408,7 +423,7 @@ def main(
def _run_single(
file: Path,
defaults: dict[str, str],
defaults: ResolvedConfig,
output: Path | None,
verbose: bool,
compute_type_explicit: bool = False,
@@ -552,7 +567,7 @@ def _run_single(
def _run_batch(
files: list[Path],
defaults: dict[str, str],
defaults: ResolvedConfig,
verbose: bool,
force: bool,
compute_type_explicit: bool = False,
+88 -22
View File
@@ -3,12 +3,48 @@
import tomllib
import warnings
from pathlib import Path
from typing import TypedDict, TypeVar, cast
HARDCODED_DEFAULTS: dict[str, str] = {
type ConfigValue = str | bool
_T = TypeVar("_T")
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",
"compute_type": "float32",
"diarize": False,
}
DEVICE_DEFAULTS: dict[str, dict[str, str]] = {
@@ -46,7 +82,7 @@ def find_config_file() -> Path | None:
return None
def load_config(path: Path | None = None) -> dict[str, str]:
def load_config(path: Path | None = None) -> ConfigValues:
"""Загружает и валидирует TOML-конфиг.
Неизвестные ключи вызывают предупреждение (а не ошибку) для forward
@@ -70,11 +106,19 @@ def load_config(path: Path | None = None) -> dict[str, str]:
stacklevel=2,
)
result: dict[str, str] = {}
result: dict[str, ConfigValue] = {}
for key in _VALID_KEYS:
if key not in data:
continue
value = data[key]
if key == "diarize":
if not isinstance(value, bool):
raise ValueError(
f"Значение 'diarize' в {path} должно быть логическим, "
f"получено {type(value).__name__}"
)
result[key] = value
continue
if not isinstance(value, str):
raise ValueError(
f"Значение '{key}' в {path} должно быть строкой, получено {type(value).__name__}"
@@ -88,31 +132,53 @@ def load_config(path: Path | None = None) -> dict[str, str]:
raise ValueError(f"Значение 'language' в {path} не может быть пустым")
result[key] = value
return result
return cast(ConfigValues, result)
def resolve_defaults(
cli_values: dict[str, str | None], config: dict[str, str]
) -> dict[str, str]:
def resolve_defaults(cli_values: CliValues, config: ConfigValues) -> ResolvedConfig:
"""Каскад приоритетов: CLI > конфиг-файл > hardcoded-дефолты."""
result: dict[str, str] = {}
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, str],
defaults: ResolvedConfig,
resolved_device: str,
cli_values: dict[str, str | None],
config: dict[str, str],
) -> dict[str, str]:
cli_values: CliValues,
config: ConfigValues,
) -> ResolvedConfig:
"""Применяет device-aware дефолты для model и compute_type,
если они не были явно заданы через CLI или конфиг."""
device_defs = DEVICE_DEFAULTS.get(resolved_device, {})
@@ -124,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)