| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- #!/usr/bin/env python3
- import argparse
- import multiprocessing as mp
- import os
- import sqlite3
- import time
- from pathlib import Path
- DB = Path("data/recording_index.sqlite3")
- OUT = Path("data/transcripts")
- MODEL = os.environ.get("WHISPER_MODEL", "small")
- OUT.mkdir(parents=True, exist_ok=True)
- def worker(worker_id, jobs):
- from faster_whisper import WhisperModel
- print(
- f"[Worker {worker_id}] "
- f"starte Whisper {MODEL}",
- flush=True,
- )
- model = WhisperModel(
- MODEL,
- device="cuda",
- compute_type="int8",
- num_workers=1,
- )
- con = sqlite3.connect(DB)
- for job_id, audio_path, output_path in jobs:
- try:
- # Andere Worker können denselben Job nicht übernehmen,
- # solange wir ihn atomar auf RUNNING setzen.
- cur = con.execute("""
- UPDATE recordings
- SET status = 'TRANSCRIBING'
- WHERE id = ?
- AND status = 'NEW'
- """, (job_id,))
- con.commit()
- if cur.rowcount != 1:
- continue
- print(
- f"[W{worker_id}] "
- f"{audio_path}",
- flush=True,
- )
- segments, info = model.transcribe(
- audio_path,
- language="de",
- beam_size=5,
- vad_filter=True,
- vad_parameters={
- "min_silence_duration_ms": 500,
- },
- )
- text_parts = []
- for segment in segments:
- text = segment.text.strip()
- if text:
- text_parts.append(text)
- text = "\n".join(text_parts).strip()
- output = Path(output_path)
- output.parent.mkdir(
- parents=True,
- exist_ok=True,
- )
- output.write_text(
- text,
- encoding="utf-8",
- )
- status = (
- "TRANSCRIBED"
- if text
- else "NO_SPEECH"
- )
- con.execute("""
- UPDATE recordings
- SET status = ?,
- transcript_path = ?
- WHERE id = ?
- """, (
- status,
- str(output),
- job_id,
- ))
- con.commit()
- except Exception as exc:
- print(
- f"[W{worker_id}] FEHLER "
- f"{audio_path}: {exc}",
- flush=True,
- )
- con.execute("""
- UPDATE recordings
- SET status = 'TRANSCRIPTION_ERROR'
- WHERE id = ?
- """, (job_id,))
- con.commit()
- con.close()
- print(
- f"[Worker {worker_id}] fertig",
- flush=True,
- )
- def main():
- parser = argparse.ArgumentParser()
- parser.add_argument(
- "--workers",
- type=int,
- default=2,
- )
- args = parser.parse_args()
- if args.workers < 1:
- raise SystemExit(
- "--workers muss >= 1 sein"
- )
- con = sqlite3.connect(DB)
- con.row_factory = sqlite3.Row
- # Persönliche/interne Aufzeichnungen ausschließen.
- con.execute("""
- UPDATE recordings
- SET status = 'EXCLUDED_INTERNAL'
- WHERE extension = '42'
- AND status IN ('NEW', 'TRANSCRIBING')
- """)
- # Sehr kurze Aufnahmen ausschließen.
- con.execute("""
- UPDATE recordings
- SET status = 'TOO_SHORT'
- WHERE duration IS NOT NULL
- AND duration < 2
- AND status IN ('NEW', 'TRANSCRIBING')
- """)
- con.commit()
- rows = con.execute("""
- SELECT id, path, recording_id
- FROM recordings
- WHERE status = 'NEW'
- ORDER BY recorded_at, id
- """).fetchall()
- con.close()
- print("=" * 72)
- print("PARALLELER WHISPER-LAUF")
- print("=" * 72)
- print(f"Modell: {MODEL}")
- print(f"Worker: {args.workers}")
- print(f"Kandidaten: {len(rows)}")
- print("Device: CUDA")
- print("Compute: int8")
- print()
- if not rows:
- print("Keine neuen Aufnahmen.")
- return
- # Jobs deterministisch auf Worker verteilen.
- jobs = [[] for _ in range(args.workers)]
- for index, row in enumerate(rows):
- output = (
- OUT /
- f"{row['id']}_{row['recording_id']}.txt"
- )
- jobs[index % args.workers].append(
- (
- row["id"],
- row["path"],
- str(output),
- )
- )
- processes = []
- started = time.time()
- for worker_id, worker_jobs in enumerate(
- jobs,
- 1,
- ):
- if not worker_jobs:
- continue
- process = mp.Process(
- target=worker,
- args=(
- worker_id,
- worker_jobs,
- ),
- )
- process.start()
- processes.append(process)
- try:
- for process in processes:
- process.join()
- except KeyboardInterrupt:
- print("\nAbbruch angefordert – Worker werden beendet ...", flush=True)
- for process in processes:
- if process.is_alive():
- process.terminate()
- for process in processes:
- process.join(timeout=10)
- for process in processes:
- if process.is_alive():
- process.kill()
- # Jobs, die beim Abbruch noch liefen, wieder freigeben.
- cleanup = sqlite3.connect(DB)
- cleanup.execute("""
- UPDATE recordings
- SET status = 'NEW'
- WHERE status = 'TRANSCRIBING'
- """)
- cleanup.commit()
- cleanup.close()
- raise
- elapsed = time.time() - started
- con = sqlite3.connect(DB)
- print()
- print("=" * 72)
- print("WHISPER-LAUF BEENDET")
- print("=" * 72)
- for status, count in con.execute("""
- SELECT status, COUNT(*)
- FROM recordings
- GROUP BY status
- ORDER BY status
- """):
- print(
- f"{status:24} {count}"
- )
- print(
- f"\nLaufzeit: "
- f"{elapsed / 60:.1f} Minuten"
- )
- con.close()
- if __name__ == "__main__":
- mp.set_start_method(
- "spawn",
- force=True,
- )
- main()
|