transcription/run.py
2026-05-29 18:56:13 +03:00

169 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""CLI entrypoint для транскрибации совещаний."""
import argparse
import os
import sys
from pathlib import Path
from src.audio_utils import check_ffmpeg, is_audio_file, is_video_file
from src.config import get_profile, load_config
from src.document import build_document
from src.pipeline import run_pipeline
def resolve_device(preferred: str) -> str:
"""Определяет доступное устройство."""
import torch
if preferred == "cuda" and torch.cuda.is_available():
return "cuda"
if preferred == "mps" and torch.backends.mps.is_available():
return "mps"
return "cpu"
def print_first_run_info():
"""Показывает информацию о первом запуске и скачивании моделей."""
print("=" * 60)
print("ПЕРВЫЙ ЗАПУСК: скачивание моделей")
print("=" * 60)
print("При первом запуске будут скачаны модели ИИ (~45 GB):")
print(" • Whisper large-v3 ~3.0 GB (распознавание речи)")
print(" • Pyannote диаризация ~0.4 GB (разделение спикеров)")
print(" • Wav2Vec2 alignment ~1.0 GB (точные таймкоды)")
print("")
print("Это займёт время в зависимости от скорости интернета.")
print("При последующих запусках модели загружаются с диска.")
print("=" * 60)
print("")
def parse_formats(fmt_arg: str | None, config_formats: list) -> list[str]:
"""Парсит строку форматов в список."""
if fmt_arg:
return [f.strip().lower() for f in fmt_arg.split(",")]
return config_formats
def main():
parser = argparse.ArgumentParser(
description="Транскрибация совещаний с диаризацией и таймкодами. "
"Поддерживает аудио (wav, mp3, m4a, ogg, flac) и видео (mp4, webm, avi, mkv, mov, etc.)."
)
parser.add_argument("--input", "-i", required=True, help="Путь к аудио или видео файлу")
parser.add_argument("--output", "-o", default=None, help="Путь к выходному файлу (если указан, переопределяет формат)")
parser.add_argument("--profile", "-p", default=None, help="Профиль конфигурации (mac_m4, gpu_8gb, cpu_best)")
parser.add_argument("--config", "-c", default=None, help="Путь к config.yaml")
parser.add_argument("--device", "-d", default=None, help="Принудительно: cpu/cuda/mps")
parser.add_argument("--model", "-m", default=None, help="Принудительно: tiny/base/small/medium/large-v3")
parser.add_argument("--language", "-l", default=None, help="Язык (ru, en, ...)")
parser.add_argument("--format", "-f", default=None, help="Форматы через запятую: docx,md,txt (по умолчанию из конфига)")
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"Ошибка: файл не найден: {args.input}", file=sys.stderr)
sys.exit(1)
# Проверка ffmpeg для видео
if is_video_file(args.input):
if not check_ffmpeg():
print(
"Ошибка: для обработки видео нужен ffmpeg.\n"
"Установите ffmpeg:\n"
" Mac: brew install ffmpeg\n"
" Linux: sudo apt-get install ffmpeg\n"
" Windows: https://ffmpeg.org/download.html",
file=sys.stderr,
)
sys.exit(1)
print(f"[Info] Обнаружен видео файл: {input_path.suffix}")
print("[Info] Аудио будет извлечено автоматически.")
elif not is_audio_file(args.input):
print(f"[Warning] Неизвестный формат: {input_path.suffix}. Попытка обработать...")
# Загрузка конфига
config = load_config(args.config)
profile = get_profile(config, args.profile)
# Переопределения из CLI
if args.device:
profile["device"] = resolve_device(args.device)
else:
profile["device"] = resolve_device(profile.get("device", "cpu"))
if args.model:
profile["model"] = args.model
if args.language:
profile["language"] = args.language
output_cfg = config.get("output", {})
config_formats = output_cfg.get("formats", ["docx"])
formats = parse_formats(args.format, config_formats)
# Определение путей выходных файлов
output_paths: list[str] = []
if args.output:
# Если указан --output, используем его для первого формата
# Остальные форматы — рядом с тем же именем
base = Path(args.output)
output_dir = base.parent
stem = base.stem
first_ext = base.suffix.lstrip(".")
# Первый файл с явным путём
output_paths.append(str(base))
# Остальные форматы рядом
for fmt in formats[1:]:
output_paths.append(str(output_dir / f"{stem}.{fmt}"))
else:
stem = input_path.stem
output_dir = Path(config.get("paths", {}).get("output_dir", "./output"))
for fmt in formats:
output_paths.append(str(output_dir / f"{stem}.{fmt}"))
# Проверка HF токена
hf_token = os.environ.get("HF_TOKEN") or config.get("hf_token")
if profile.get("diarize", True) and not hf_token:
print(
"Ошибка: для диаризации нужен HuggingFace токен.\n"
"Установите env HF_TOKEN или укажите hf_token в config.yaml\n"
"Инструкция: https://huggingface.co/docs/hub/security-tokens",
file=sys.stderr,
)
sys.exit(1)
# Информация о первом запуске
print_first_run_info()
print(f"Профиль: {args.profile or config.get('active_profile')}")
print(f"Устройство: {profile['device']}")
print(f"Модель: {profile['model']}")
print(f"Язык: {profile['language']}")
print(f"Вход: {args.input}")
print(f"Форматы: {', '.join(formats)}")
print(f"Выход: {', '.join(output_paths)}")
print("-" * 40)
# Запуск пайплайна (один раз для всех форматов)
result = run_pipeline(
input_path=args.input,
profile_name=args.profile,
config_path=args.config,
output_path=output_paths[0] if output_paths else None,
)
# Генерация документов для всех форматов
print("-" * 40)
print("Генерация документов...")
for out_path in output_paths:
build_document(result["segments"], out_path, config)
print(f"{out_path}")
print("-" * 40)
print(f"Готово! Сохранено {len(output_paths)} файл(а):")
for p in output_paths:
print(f"{p}")
if __name__ == "__main__":
main()