mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +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
165 lines
4.5 KiB
Python
Executable File
165 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Speaker diarization: embed each segment, then cluster the embeddings.
|
|
|
|
Every heavy dependency (torch, speechbrain, soundfile) is reached through
|
|
``_try_import``, so a machine without them degrades to no speaker labels
|
|
rather than failing the transcription.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from _transcribe_cluster import _kmeans_cosine, _resample_linear
|
|
from _transcribe_media import (
|
|
_cleanup_temp,
|
|
_ffmpeg_transcode_to_wav16_mono,
|
|
_try_import,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
import types
|
|
|
|
import numpy as np
|
|
import numpy.typing as npt
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_NDIM_2D = 2
|
|
_SAMPLE_RATE_16K = 16000
|
|
_MIN_SAMPLES_DIAR = 1600
|
|
|
|
|
|
def _load_audio(
|
|
audio_path: str,
|
|
) -> tuple[Any, int, str | None] | None:
|
|
"""Load audio, with ffmpeg fallback.
|
|
|
|
Returns (wav, sample_rate, temp_path) or None.
|
|
"""
|
|
sf = _try_import("soundfile")
|
|
if sf is None:
|
|
return None
|
|
|
|
try:
|
|
wav, sr = sf.read(
|
|
audio_path,
|
|
dtype="float32",
|
|
always_2d=False,
|
|
)
|
|
except OSError as exc:
|
|
alt = _ffmpeg_transcode_to_wav16_mono(audio_path)
|
|
if alt is None:
|
|
logger.warning(
|
|
"Could not read audio for diarization and no ffmpeg fallback: %s",
|
|
exc,
|
|
)
|
|
return None
|
|
try:
|
|
wav, sr = sf.read(
|
|
alt,
|
|
dtype="float32",
|
|
always_2d=False,
|
|
)
|
|
except OSError as exc2:
|
|
logger.warning(
|
|
"Could not read transcoded audio: %s",
|
|
exc2,
|
|
)
|
|
_cleanup_temp(alt)
|
|
return None
|
|
return wav, sr, alt
|
|
return wav, sr, None
|
|
|
|
|
|
def _load_speaker_classifier(
|
|
temp_to_cleanup: str | None,
|
|
) -> object | None:
|
|
"""Load the ECAPA speaker embedding classifier."""
|
|
sb_inf = _try_import("speechbrain.inference")
|
|
if sb_inf is None:
|
|
return None
|
|
try:
|
|
cache_dir = Path.home() / ".cache" / "speechbrain_ecapa"
|
|
classifier = sb_inf.EncoderClassifier.from_hparams(
|
|
source="speechbrain/spkrec-ecapa-voxceleb",
|
|
run_opts={"device": "cpu"},
|
|
savedir=str(cache_dir),
|
|
)
|
|
except (OSError, RuntimeError) as exc:
|
|
logger.warning(
|
|
"Could not load speaker embedding model: %s",
|
|
exc,
|
|
)
|
|
_cleanup_temp(temp_to_cleanup)
|
|
return None
|
|
return classifier
|
|
|
|
|
|
def _extract_embeddings(
|
|
segments: list[Any],
|
|
wav16: npt.NDArray[np.float32],
|
|
classifier: object,
|
|
torch_mod: types.ModuleType,
|
|
) -> list[Any]:
|
|
"""Extract speaker embeddings per segment."""
|
|
embs: list[Any] = []
|
|
for seg in segments:
|
|
s = float(getattr(seg, "start", 0.0) or 0.0)
|
|
e = float(getattr(seg, "end", s) or s)
|
|
if e <= s:
|
|
e = s + 0.2
|
|
i0 = int(s * _SAMPLE_RATE_16K)
|
|
i1 = int(e * _SAMPLE_RATE_16K)
|
|
pad = int(0.05 * _SAMPLE_RATE_16K)
|
|
i0 = max(0, i0 - pad)
|
|
i1 = min(len(wav16), i1 + pad)
|
|
if i1 - i0 < _MIN_SAMPLES_DIAR:
|
|
i1 = min(len(wav16), i0 + _MIN_SAMPLES_DIAR)
|
|
seg_wav = torch_mod.tensor(wav16[i0:i1]).unsqueeze(0)
|
|
with torch_mod.no_grad():
|
|
emb = classifier.encode_batch(seg_wav).squeeze(0).squeeze(0).cpu().numpy()
|
|
embs.append(emb.astype("float32"))
|
|
return embs
|
|
|
|
|
|
def diarize_segments(
|
|
audio_path: str,
|
|
segments: list[Any],
|
|
num_speakers: int = 2,
|
|
) -> list[int] | None:
|
|
"""Compute speaker embeddings per segment and cluster.
|
|
|
|
Returns speaker labels aligned with segments,
|
|
or None on failure.
|
|
"""
|
|
torch_mod = _try_import("torch")
|
|
if torch_mod is None:
|
|
logger.warning(
|
|
"Diarization dependencies missing; skipping speaker labels.",
|
|
)
|
|
return None
|
|
|
|
audio_result = _load_audio(audio_path)
|
|
if audio_result is None:
|
|
return None
|
|
wav, sr, temp_to_cleanup = audio_result
|
|
|
|
if wav.ndim == _NDIM_2D:
|
|
wav = wav.mean(axis=1)
|
|
wav16 = _resample_linear(wav, sr, _SAMPLE_RATE_16K)
|
|
|
|
classifier = _load_speaker_classifier(temp_to_cleanup)
|
|
if classifier is None:
|
|
return None
|
|
|
|
embs = _extract_embeddings(segments, wav16, classifier, torch_mod)
|
|
|
|
if len(embs) == 0:
|
|
return None
|
|
labels = _kmeans_cosine(embs, k=max(1, int(num_speakers)))
|
|
_cleanup_temp(temp_to_cleanup)
|
|
return labels.tolist()
|