"""CPU-friendly faster-whisper entry point used by the Go media worker. The process writes JSON only to stdout. Model/runtime diagnostics go to stderr so the Go caller never mistakes logs for transcript data. """ from __future__ import annotations import json import os import sys import tempfile def configure_model_cache() -> None: cache_root = os.getenv("JCF_ASR_CACHE_DIR") or os.path.join(tempfile.gettempdir(), "jcf-asr-cache") os.makedirs(cache_root, mode=0o700, exist_ok=True) hf_home = os.getenv("HF_HOME") or os.path.join(cache_root, "huggingface") os.makedirs(hf_home, mode=0o700, exist_ok=True) os.environ.setdefault("XDG_CACHE_HOME", cache_root) os.environ.setdefault("HF_HOME", hf_home) def main() -> int: if len(sys.argv) < 2: print(json.dumps({"error": "missing_audio_path", "segments": []}, ensure_ascii=False)) return 2 configure_model_cache() from faster_whisper import WhisperModel audio_path = sys.argv[1] language = (sys.argv[2] if len(sys.argv) > 2 else "").strip() or None model_name = os.getenv("FASTER_WHISPER_MODEL_PATH") or os.getenv("FASTER_WHISPER_MODEL_SIZE", "small") device = os.getenv("FASTER_WHISPER_DEVICE", "cpu") compute_type = os.getenv("FASTER_WHISPER_COMPUTE_TYPE", "int8") model = WhisperModel(model_name, device=device, compute_type=compute_type) segments, info = model.transcribe( audio_path, language=language, beam_size=5, best_of=5, temperature=0.0, condition_on_previous_text=True, vad_filter=True, vad_parameters={"min_silence_duration_ms": 700}, word_timestamps=True, ) result = [] for segment in segments: text = (segment.text or "").strip() if not text: continue words = [] probabilities = [] for word in segment.words or []: if word.start is None or word.end is None or not word.word: continue probability = float(word.probability) if word.probability is not None else None if probability is not None: probabilities.append(probability) words.append({ "start": round(float(word.start), 3), "end": round(float(word.end), 3), "text": str(word.word), "probability": round(probability, 4) if probability is not None else None, }) mean_probability = sum(probabilities) / len(probabilities) if probabilities else None avg_logprob = float(getattr(segment, "avg_logprob", -1.0) or -1.0) no_speech = float(getattr(segment, "no_speech_prob", 0.0) or 0.0) confidence = max(0.0, min(1.0, (1.0 + avg_logprob) * 0.45 + (1.0 - no_speech) * 0.2 + (mean_probability if mean_probability is not None else max(0.0, 1.0 + avg_logprob)) * 0.35)) result.append({ "start": round(float(segment.start), 3), "end": round(float(segment.end), 3), "text": text, "words": words, "confidence": round(confidence, 3), "needs_review": confidence < 0.62 or (mean_probability is not None and mean_probability < 0.55), }) print(json.dumps({"language": getattr(info, "language", language), "segments": result}, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())