| import os |
| import json |
| import csv |
| import time |
| import logging |
| import httpx |
| from pathlib import Path |
| from dataclasses import dataclass |
| from typing import List, Optional |
|
|
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| DEEPGRAM_API_KEY = os.environ.get("DEEPGRAM_API_KEY", "") |
|
|
| @dataclass |
| class AnnotationSegment: |
| speaker: str |
| start: float |
| end: float |
| text: str |
|
|
| @property |
| def start_fmt(self): |
| m, s = divmod(int(self.start), 60) |
| return f"{m:02d}:{s:02d}" |
|
|
| @property |
| def end_fmt(self): |
| m, s = divmod(int(self.end), 60) |
| return f"{m:02d}:{s:02d}" |
|
|
| def to_abab(self): |
| return f"{self.speaker} {self.start_fmt} {self.text}" |
|
|
| SPEAKER_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
|
|
| def _speaker_label(idx): |
| return SPEAKER_LETTERS[idx] if idx < len(SPEAKER_LETTERS) else f"SPK{idx}" |
|
|
| class SpeechAnnotationPipeline: |
| def process(self, audio_path, num_speakers=0, **kwargs): |
| if not DEEPGRAM_API_KEY: |
| raise EnvironmentError("DEEPGRAM_API_KEY not set.") |
| logger.info(f"Sending to Deepgram: {audio_path}") |
| t0 = time.time() |
| audio_bytes = Path(audio_path).read_bytes() |
| ext = Path(audio_path).suffix.lower().lstrip(".") |
| mime = {"wav":"audio/wav","mp3":"audio/mpeg","mp4":"audio/mp4", |
| "m4a":"audio/mp4","flac":"audio/flac","ogg":"audio/ogg"}.get(ext,"audio/wav") |
| params = {"model":"nova-2","diarize":"true","punctuate":"true", |
| "utterances":"true","smart_format":"true"} |
| if num_speakers and num_speakers > 0: |
| params["diarize_version"] = "latest" |
| params["num_speakers"] = str(num_speakers) |
| headers = {"Authorization": f"Token {DEEPGRAM_API_KEY}", "Content-Type": mime} |
| response = httpx.post("https://api.deepgram.com/v1/listen", |
| params=params, headers=headers, |
| content=audio_bytes, timeout=300) |
| response.raise_for_status() |
| data = response.json() |
| utterances = data.get("results", {}).get("utterances", []) |
| if not utterances: |
| words = data.get("results",{}).get("channels",[{}])[0]\ |
| .get("alternatives",[{}])[0].get("words",[]) |
| utterances = self._words_to_utterances(words) |
| segments = [] |
| for utt in utterances: |
| segments.append(AnnotationSegment( |
| speaker=_speaker_label(utt.get("speaker", 0)), |
| start=utt.get("start", 0), |
| end=utt.get("end", 0), |
| text=utt.get("transcript", "").strip(), |
| )) |
| logger.info(f"Done in {time.time()-t0:.1f}s — {len(segments)} segments") |
| return segments |
|
|
| def _words_to_utterances(self, words): |
| if not words: |
| return [] |
| utterances = [] |
| current = {"speaker": words[0].get("speaker",0), "start": words[0].get("start",0), |
| "end": words[0].get("end",0), |
| "transcript": words[0].get("punctuated_word", words[0].get("word",""))} |
| for word in words[1:]: |
| if word.get("speaker") == current["speaker"]: |
| current["end"] = word.get("end", current["end"]) |
| current["transcript"] += " " + word.get("punctuated_word", word.get("word","")) |
| else: |
| utterances.append(current) |
| current = {"speaker": word.get("speaker",0), "start": word.get("start",0), |
| "end": word.get("end",0), |
| "transcript": word.get("punctuated_word", word.get("word",""))} |
| utterances.append(current) |
| return utterances |
|
|
| def to_json(segments, path): |
| data = [{"speaker": s.speaker, "start": round(s.start,3), "end": round(s.end,3), |
| "start_fmt": s.start_fmt, "end_fmt": s.end_fmt, "text": s.text} for s in segments] |
| with open(path, "w", encoding="utf-8") as f: |
| json.dump(data, f, indent=2, ensure_ascii=False) |
|
|
| def to_csv(segments, path): |
| with open(path, "w", newline="", encoding="utf-8") as f: |
| writer = csv.writer(f) |
| writer.writerow(["speaker","start","end","text"]) |
| for s in segments: |
| writer.writerow([s.speaker, s.start_fmt, s.end_fmt, s.text]) |
|
|
| def to_abab_text(segments): |
| return "\n".join(s.to_abab() for s in segments) |
|
|
| _instance = None |
|
|
| def get_pipeline(): |
| global _instance |
| if _instance is None: |
| _instance = SpeechAnnotationPipeline() |
| return _instance |