transcribe_parallel.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. #!/usr/bin/env python3
  2. import argparse
  3. import multiprocessing as mp
  4. import os
  5. import sqlite3
  6. import time
  7. from pathlib import Path
  8. DB = Path("data/recording_index.sqlite3")
  9. OUT = Path("data/transcripts")
  10. MODEL = os.environ.get("WHISPER_MODEL", "small")
  11. OUT.mkdir(parents=True, exist_ok=True)
  12. def worker(worker_id, jobs):
  13. from faster_whisper import WhisperModel
  14. print(
  15. f"[Worker {worker_id}] "
  16. f"starte Whisper {MODEL}",
  17. flush=True,
  18. )
  19. model = WhisperModel(
  20. MODEL,
  21. device="cuda",
  22. compute_type="int8",
  23. num_workers=1,
  24. )
  25. con = sqlite3.connect(DB)
  26. for job_id, audio_path, output_path in jobs:
  27. try:
  28. # Andere Worker können denselben Job nicht übernehmen,
  29. # solange wir ihn atomar auf RUNNING setzen.
  30. cur = con.execute("""
  31. UPDATE recordings
  32. SET status = 'TRANSCRIBING'
  33. WHERE id = ?
  34. AND status = 'NEW'
  35. """, (job_id,))
  36. con.commit()
  37. if cur.rowcount != 1:
  38. continue
  39. print(
  40. f"[W{worker_id}] "
  41. f"{audio_path}",
  42. flush=True,
  43. )
  44. segments, info = model.transcribe(
  45. audio_path,
  46. language="de",
  47. beam_size=5,
  48. vad_filter=True,
  49. vad_parameters={
  50. "min_silence_duration_ms": 500,
  51. },
  52. )
  53. text_parts = []
  54. for segment in segments:
  55. text = segment.text.strip()
  56. if text:
  57. text_parts.append(text)
  58. text = "\n".join(text_parts).strip()
  59. output = Path(output_path)
  60. output.parent.mkdir(
  61. parents=True,
  62. exist_ok=True,
  63. )
  64. output.write_text(
  65. text,
  66. encoding="utf-8",
  67. )
  68. status = (
  69. "TRANSCRIBED"
  70. if text
  71. else "NO_SPEECH"
  72. )
  73. con.execute("""
  74. UPDATE recordings
  75. SET status = ?,
  76. transcript_path = ?
  77. WHERE id = ?
  78. """, (
  79. status,
  80. str(output),
  81. job_id,
  82. ))
  83. con.commit()
  84. except Exception as exc:
  85. print(
  86. f"[W{worker_id}] FEHLER "
  87. f"{audio_path}: {exc}",
  88. flush=True,
  89. )
  90. con.execute("""
  91. UPDATE recordings
  92. SET status = 'TRANSCRIPTION_ERROR'
  93. WHERE id = ?
  94. """, (job_id,))
  95. con.commit()
  96. con.close()
  97. print(
  98. f"[Worker {worker_id}] fertig",
  99. flush=True,
  100. )
  101. def main():
  102. parser = argparse.ArgumentParser()
  103. parser.add_argument(
  104. "--workers",
  105. type=int,
  106. default=2,
  107. )
  108. args = parser.parse_args()
  109. if args.workers < 1:
  110. raise SystemExit(
  111. "--workers muss >= 1 sein"
  112. )
  113. con = sqlite3.connect(DB)
  114. con.row_factory = sqlite3.Row
  115. # Persönliche/interne Aufzeichnungen ausschließen.
  116. con.execute("""
  117. UPDATE recordings
  118. SET status = 'EXCLUDED_INTERNAL'
  119. WHERE extension = '42'
  120. AND status IN ('NEW', 'TRANSCRIBING')
  121. """)
  122. # Sehr kurze Aufnahmen ausschließen.
  123. con.execute("""
  124. UPDATE recordings
  125. SET status = 'TOO_SHORT'
  126. WHERE duration IS NOT NULL
  127. AND duration < 2
  128. AND status IN ('NEW', 'TRANSCRIBING')
  129. """)
  130. con.commit()
  131. rows = con.execute("""
  132. SELECT id, path, recording_id
  133. FROM recordings
  134. WHERE status = 'NEW'
  135. ORDER BY recorded_at, id
  136. """).fetchall()
  137. con.close()
  138. print("=" * 72)
  139. print("PARALLELER WHISPER-LAUF")
  140. print("=" * 72)
  141. print(f"Modell: {MODEL}")
  142. print(f"Worker: {args.workers}")
  143. print(f"Kandidaten: {len(rows)}")
  144. print("Device: CUDA")
  145. print("Compute: int8")
  146. print()
  147. if not rows:
  148. print("Keine neuen Aufnahmen.")
  149. return
  150. # Jobs deterministisch auf Worker verteilen.
  151. jobs = [[] for _ in range(args.workers)]
  152. for index, row in enumerate(rows):
  153. output = (
  154. OUT /
  155. f"{row['id']}_{row['recording_id']}.txt"
  156. )
  157. jobs[index % args.workers].append(
  158. (
  159. row["id"],
  160. row["path"],
  161. str(output),
  162. )
  163. )
  164. processes = []
  165. started = time.time()
  166. for worker_id, worker_jobs in enumerate(
  167. jobs,
  168. 1,
  169. ):
  170. if not worker_jobs:
  171. continue
  172. process = mp.Process(
  173. target=worker,
  174. args=(
  175. worker_id,
  176. worker_jobs,
  177. ),
  178. )
  179. process.start()
  180. processes.append(process)
  181. try:
  182. for process in processes:
  183. process.join()
  184. except KeyboardInterrupt:
  185. print("\nAbbruch angefordert – Worker werden beendet ...", flush=True)
  186. for process in processes:
  187. if process.is_alive():
  188. process.terminate()
  189. for process in processes:
  190. process.join(timeout=10)
  191. for process in processes:
  192. if process.is_alive():
  193. process.kill()
  194. # Jobs, die beim Abbruch noch liefen, wieder freigeben.
  195. cleanup = sqlite3.connect(DB)
  196. cleanup.execute("""
  197. UPDATE recordings
  198. SET status = 'NEW'
  199. WHERE status = 'TRANSCRIBING'
  200. """)
  201. cleanup.commit()
  202. cleanup.close()
  203. raise
  204. elapsed = time.time() - started
  205. con = sqlite3.connect(DB)
  206. print()
  207. print("=" * 72)
  208. print("WHISPER-LAUF BEENDET")
  209. print("=" * 72)
  210. for status, count in con.execute("""
  211. SELECT status, COUNT(*)
  212. FROM recordings
  213. GROUP BY status
  214. ORDER BY status
  215. """):
  216. print(
  217. f"{status:24} {count}"
  218. )
  219. print(
  220. f"\nLaufzeit: "
  221. f"{elapsed / 60:.1f} Minuten"
  222. )
  223. con.close()
  224. if __name__ == "__main__":
  225. mp.set_start_method(
  226. "spawn",
  227. force=True,
  228. )
  229. main()