mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
i3 config was filed under periodic_background, which it never was. It is now linux_configuration/i3/ (config + installer) and linux_configuration/i3blocks/ (the status scripts), both at depth 2. misc/testsAndMisc-bash/ was a directory named after the repo it already lived in, so its contents moved up to misc/. Its stray .gitignore came along, keeping the media rules working from the new location; its .vscode/tasks.json did not, because it invoked Bash/transcribe.sh, a path this repo dropped long ago. Most of this commit is the references the previous flatten missed. Its regex skipped anything with a variable prefix, which left 16 files -- including check_and_enable_services.sh and install_core_system.sh, the orchestrators -- still naming $CONFIG_DIR/scripts/periodic_background/... It had also rewritten three i3 paths into scripts/i3blocks/, a directory that has never existed; the i3blocks suites caught that by failing. 13 more symlinks broke on the i3 move. Repointed, and cpu_monitor/volume/ disk were run again to confirm the bar still reports. Depth 139 -> 104. Roughly 47 of the remainder sit in digital_wellbeing and hosts, still deferred. The rest encode real structure -- a C project, Python package directories, artgate's routes/styles, systemd unit dirs -- so flattening them further would destroy information rather than taxonomy. Verified: 1798 tests at 100%, both i3blocks suites pass, the shell coverage gate is clean, and the relocated media rules still ignore what they should while test_fw.srt stays tracked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
233 lines
6.2 KiB
Python
Executable File
233 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Transcribe audio with faster-whisper and write .txt and .srt."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from _transcribe_diarize import diarize_segments
|
|
from _transcribe_media import get_media_duration
|
|
from _transcribe_model import download_model_with_progress
|
|
from _transcribe_output import (
|
|
hhmmss,
|
|
write_rttm,
|
|
write_srt,
|
|
write_srt_with_speakers,
|
|
write_txt,
|
|
write_txt_with_speakers,
|
|
)
|
|
from _transcribe_progress import _run_progress_loop
|
|
|
|
if TYPE_CHECKING:
|
|
import types
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _try_import(name: str) -> types.ModuleType | None:
|
|
"""Attempt to import a module, returning None on failure."""
|
|
try:
|
|
return importlib.import_module(name)
|
|
except ImportError:
|
|
return None
|
|
|
|
|
|
def _parse_args() -> argparse.Namespace:
|
|
"""Parse command-line arguments."""
|
|
parser = argparse.ArgumentParser(
|
|
description=("Transcribe audio with faster-whisper and write .txt and .srt"),
|
|
)
|
|
parser.add_argument("input", help="Path to audio/video file")
|
|
parser.add_argument(
|
|
"--model",
|
|
default=os.environ.get("FW_MODEL", "large-v3"),
|
|
help="Model size or path (default: large-v3)",
|
|
)
|
|
parser.add_argument(
|
|
"--language",
|
|
default=None,
|
|
help="Language code (e.g., en). None=auto",
|
|
)
|
|
parser.add_argument(
|
|
"--device",
|
|
default=os.environ.get("FW_DEVICE", "auto"),
|
|
choices=["auto", "cpu", "cuda"],
|
|
help="Device to run on",
|
|
)
|
|
parser.add_argument(
|
|
"--compute-type",
|
|
dest="compute_type",
|
|
default=os.environ.get("FW_COMPUTE", "auto"),
|
|
help="Compute type (auto,int8,float16,...)",
|
|
)
|
|
parser.add_argument(
|
|
"--outdir",
|
|
default=None,
|
|
help="Output dir (default: next to input)",
|
|
)
|
|
parser.add_argument(
|
|
"--no-progress",
|
|
action="store_true",
|
|
help="Disable live progress output",
|
|
)
|
|
parser.add_argument(
|
|
"--diarize",
|
|
action="store_true",
|
|
help="Enable speaker diarization (labels)",
|
|
)
|
|
parser.add_argument(
|
|
"--num-speakers",
|
|
type=int,
|
|
default=int(os.environ.get("FW_NUM_SPEAKERS", "2")),
|
|
help="Number of speakers (default: 2)",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def _resolve_device_and_compute(
|
|
args: argparse.Namespace,
|
|
) -> tuple[str, str]:
|
|
"""Resolve device and compute_type from args."""
|
|
device = args.device
|
|
compute_type = args.compute_type
|
|
if device == "auto":
|
|
device = "cpu"
|
|
if compute_type == "auto":
|
|
compute_type = "float16" if device == "cuda" else "float32"
|
|
return device, compute_type
|
|
|
|
|
|
def _write_diarized_outputs(
|
|
args: argparse.Namespace,
|
|
inp: str,
|
|
outdir: Path,
|
|
base: str,
|
|
collected: list[Any],
|
|
) -> None:
|
|
"""Optionally diarize and write speaker outputs."""
|
|
if not args.diarize:
|
|
return
|
|
|
|
labels = diarize_segments(
|
|
inp,
|
|
collected,
|
|
num_speakers=args.num_speakers,
|
|
)
|
|
if labels is not None and len(labels) == len(collected):
|
|
diar_srt = str(outdir / (base + ".diar.srt"))
|
|
diar_txt = str(outdir / (base + ".diar.txt"))
|
|
rttm_path = str(outdir / (base + ".rttm"))
|
|
write_srt_with_speakers(collected, labels, diar_srt)
|
|
write_txt_with_speakers(collected, labels, diar_txt)
|
|
write_rttm(
|
|
collected,
|
|
labels,
|
|
rttm_path,
|
|
file_id=base,
|
|
)
|
|
logger.info("Wrote: %s", diar_txt)
|
|
logger.info("Wrote: %s", diar_srt)
|
|
logger.info("Wrote: %s", rttm_path)
|
|
else:
|
|
logger.warning(
|
|
"Diarization failed or returned mismatched labels; writing plain.",
|
|
)
|
|
|
|
|
|
def _load_whisper_model(
|
|
fw: types.ModuleType,
|
|
args: argparse.Namespace,
|
|
device: str,
|
|
compute_type: str,
|
|
) -> object:
|
|
"""Resolve the model path, configure logging, and load the model."""
|
|
model_path: str = args.model
|
|
if not Path(args.model).is_dir():
|
|
model_path = download_model_with_progress(args.model)
|
|
|
|
ct2_logger = logging.getLogger("faster_whisper")
|
|
ct2_logger.setLevel(logging.INFO)
|
|
|
|
logger.info("Initializing model...")
|
|
model = fw.WhisperModel(
|
|
model_path,
|
|
device=device,
|
|
compute_type=compute_type,
|
|
)
|
|
logger.info("Model loaded successfully.")
|
|
return model
|
|
|
|
|
|
def main() -> int:
|
|
"""Run the main transcription pipeline."""
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(message)s",
|
|
)
|
|
|
|
args = _parse_args()
|
|
|
|
fw = _try_import("faster_whisper")
|
|
if fw is None:
|
|
logger.error(
|
|
"faster-whisper is not installed in this environment.",
|
|
)
|
|
return 2
|
|
|
|
inp_path = Path(args.input).resolve()
|
|
if not inp_path.exists():
|
|
logger.error("Input file not found: %s", inp_path)
|
|
return 2
|
|
|
|
inp = str(inp_path)
|
|
outdir = Path(args.outdir or str(inp_path.parent) or ".").resolve()
|
|
outdir.mkdir(parents=True, exist_ok=True)
|
|
base = inp_path.stem
|
|
srt_path = str(outdir / (base + ".srt"))
|
|
txt_path = str(outdir / (base + ".txt"))
|
|
|
|
device, compute_type = _resolve_device_and_compute(args)
|
|
|
|
logger.info(
|
|
"Loading model='%s', device='%s', compute_type='%s'",
|
|
args.model,
|
|
device,
|
|
compute_type,
|
|
)
|
|
|
|
model = _load_whisper_model(fw, args, device, compute_type)
|
|
|
|
total_duration = get_media_duration(inp)
|
|
if total_duration:
|
|
logger.info(
|
|
"Media duration: %s",
|
|
hhmmss(total_duration),
|
|
)
|
|
|
|
collected, info = _run_progress_loop(args, model, inp, total_duration)
|
|
|
|
logger.info(
|
|
"Detected language: %s (prob=%s)",
|
|
getattr(info, "language", None),
|
|
getattr(info, "language_probability", None),
|
|
)
|
|
logger.info("Segments: %d", len(collected))
|
|
|
|
_write_diarized_outputs(args, inp, outdir, base, collected)
|
|
|
|
write_txt(collected, txt_path)
|
|
write_srt(collected, srt_path)
|
|
logger.info("Wrote: %s", txt_path)
|
|
logger.info("Wrote: %s", srt_path)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|