"""Isolated actual whisper.cpp CPU diagnostic; never downloads or builds tools.""" import argparse import hashlib import json import math import os from pathlib import Path import signal import subprocess import tempfile import time import wave UPSTREAM = "d1be6fde11ac6e0407606b4e42fe72d34add8037" MODELS = { "base": (147951465, "60ed5bc3dd14eea856493d334349b405782ddcaf0028d4b5df4088345fba2efe"), "small": (487601967, "1be3a9b2063867b937e64e2ec7483364a79917e157fa98c5d94b5c1fffea987b"), } ASSETS = ("clip04", "music01", "music02", "sound01", "sound02") FIXTURE_SHA = "b4e465a0eb9582b826eebd2a20797e1680e4907e336d02d0ef2194e2ba0af393" MAX_OUTPUT = 4 * 1024 * 1024 def sha256(path): digest = hashlib.sha256() with path.open("rb") as stream: for block in iter(lambda: stream.read(1024 * 1024), b""): digest.update(block) return digest.hexdigest() def stop(child): if os.name == "nt": subprocess.run(["taskkill", "/PID", str(child.pid), "/T", "/F"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NO_WINDOW, check=False) else: os.killpg(child.pid, signal.SIGKILL) child.wait() def run_owned(command, scratch, name, outputs, timeout): """Bound time/output; terminate only our process tree on failure/interruption.""" log = scratch / f"{name}.log" kwargs = {"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {"start_new_session": True} started = time.perf_counter() with log.open("wb") as stream: child = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=stream, stderr=subprocess.STDOUT, **kwargs) try: while child.poll() is None: if time.perf_counter() - started > timeout: raise TimeoutError(f"{name} exceeded {timeout}s") if any(path.exists() and path.stat().st_size > limit for path, limit in [(log, MAX_OUTPUT), *outputs]): raise ValueError(f"{name} exceeded output budget") time.sleep(0.05) if child.returncode: raise RuntimeError(f"{name} failed with exit {child.returncode}; owned diagnostic log: {log}") if any(path.exists() and path.stat().st_size > limit for path, limit in [(log, MAX_OUTPUT), *outputs]): raise ValueError(f"{name} exceeded output budget") except BaseException as error: if child.poll() is None: stop(child) stream.flush() # Preserve bounded actual diagnostic bytes outside ephemeral scratch. with tempfile.NamedTemporaryFile(prefix=f"speech-{name}-failure-", suffix=".log", dir=scratch.parent, delete=False) as retained: with log.open("rb") as source: retained.write(source.read(MAX_OUTPUT)) print(f"Owned failure diagnostic retained: {retained.name}") diagnostic = {"stage": name, "elapsed_ms": (time.perf_counter() - started) * 1000, "exit_code": child.returncode, "owned_pid": child.pid, "error_type": type(error).__name__, "bounded_log": retained.name, "complete": False} Path(retained.name).with_suffix(".json").write_text(json.dumps(diagnostic, indent=2) + "\n", encoding="utf-8") raise finally: if child.poll() is None: stop(child) return (time.perf_counter() - started) * 1000 def segments(document, offset_ms, duration_ms): raw = document.get("transcription") if not isinstance(raw, list) or len(raw) > 1000: raise ValueError("Missing or oversized transcription array") parsed = [] previous = 0 for row in raw: start, end = row["offsets"]["from"], row["offsets"]["to"] text = row["text"] if type(start) is not int or type(end) is not int or not 0 <= start <= end <= duration_ms or start < previous: raise ValueError("Invalid local ASR timestamps") if not isinstance(text, str) or len(text.encode("utf-8")) > 4096: raise ValueError("Invalid recognized text") previous = start if text.strip(): parsed.append({"start_ms": offset_ms + start, "end_ms": offset_ms + end, "text": text}) return parsed def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--whisper", type=Path, required=True) parser.add_argument("--whisper-sha256", required=True, help="Previously recorded pinned build SHA, not computed for acceptance here") parser.add_argument("--ffmpeg", type=Path, required=True) parser.add_argument("--ffprobe", type=Path, required=True) parser.add_argument("--model", type=Path, required=True) parser.add_argument("--candidate", choices=MODELS, required=True) parser.add_argument("--fixture", type=Path, default=Path("fixtures/media/fixture.json")) parser.add_argument("--scratch-parent", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() size, digest = MODELS[args.candidate] if args.model.stat().st_size != size or sha256(args.model) != digest: raise ValueError("Model size/SHA verification failed") if sha256(args.whisper) != args.whisper_sha256: raise ValueError("Pinned worker build SHA verification failed") if sha256(args.fixture) != FIXTURE_SHA: raise ValueError("Frozen media-v1 fixture SHA mismatch") fixture = json.loads(args.fixture.read_text(encoding="utf-8")) if not set(ASSETS).issubset({asset["id"] for asset in fixture["assets"]}): raise ValueError("Missing proof assets") rows = [] args.scratch_parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix="speech-proof-", dir=args.scratch_parent) as directory: scratch = Path(directory) for asset in fixture["assets"]: if asset["id"] not in ASSETS: continue source = args.fixture.parent / asset["path"] if source.stat().st_size != asset["bytes"] or sha256(source) != asset["sha256"]: raise ValueError(f"Frozen asset mismatch: {asset['id']}") # Probe stdout is bounded by the same owned log, then parsed directly. run_owned([str(args.ffprobe.resolve()), "-v", "error", "-protocol_whitelist", "file,pipe", "-show_entries", "format=duration", "-of", "json", str(source.resolve())], scratch, "probe", [], 30) probe = json.loads((scratch / "probe.log").read_text(encoding="utf-8")) duration_s = float(probe["format"]["duration"]) if not math.isfinite(duration_s) or not 0 < duration_s <= 600: raise ValueError("Proof duration exceeds ten-minute bound") duration_ms = math.ceil(duration_s * 1000) result = {"asset": asset["id"], "source_sha256": asset["sha256"], "duration_ms": duration_ms, "windows": []} for offset in range(0, duration_ms, 30000): length = min(30000, duration_ms - offset) wav = scratch / "window.wav" prefix = scratch / "recognized" output = prefix.with_suffix(".json") output.unlink(missing_ok=True) decode_ms = run_owned([str(args.ffmpeg.resolve()), "-nostdin", "-hide_banner", "-v", "error", "-protocol_whitelist", "file,pipe", "-threads", "1", "-ss", str(offset / 1000), "-t", str(length / 1000), "-i", str(source.resolve()), "-map", "0:a:0", "-vn", "-ac", "1", "-ar", "16000", "-af", f"atrim=end_sample={length * 16}", "-c:a", "pcm_s16le", "-threads", "1", "-y", str(wav)], scratch, "decode", [(wav, 970000)], 60) with wave.open(str(wav), "rb") as audio: if audio.getframerate() != 16000 or audio.getnchannels() != 1 or audio.getsampwidth() != 2: raise ValueError("Invalid decoded PCM") asr_ms = run_owned([str(args.whisper.resolve()), "--no-gpu", "--threads", "1", "--processors", "1", "--language", "auto", "--model", str(args.model.resolve()), "--file", str(wav), "--output-json", "--output-file", str(prefix)], scratch, "asr", [(output, MAX_OUTPUT)], 600) raw = json.loads(output.read_text(encoding="utf-8")) result["windows"].append({"offset_ms": offset, "duration_ms": length, "decode_ms": decode_ms, "asr_process_ms": asr_ms, "segments": segments(raw, offset, length), "actual_whisper_json": raw}) args.output.parent.mkdir(parents=True, exist_ok=True) checkpoint = {"complete": False, "fixture_sha256": sha256(args.fixture), "model_sha256": digest, "worker_sha256": args.whisper_sha256, "rows": [*rows, result]} args.output.write_text(json.dumps(checkpoint, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") if source.stat().st_size != asset["bytes"] or sha256(source) != asset["sha256"]: raise ValueError("Source changed during recognition") rows.append(result) report = {"version": 1, "complete": True, "scope": "Actual ASR diagnostic, not Google retrieval or production integration", "fixture_sha256": sha256(args.fixture), "upstream_commit": UPSTREAM, "worker_sha256": args.whisper_sha256, "model_sha256": digest, "candidate": args.candidate, "ffmpeg_sha256": sha256(args.ffmpeg), "ffprobe_sha256": sha256(args.ffprobe), "backend": "forced CPU", "threads": 1, "policy": "nonoverlapping30s;independent-process-per-window;auto-language;segment-times", "rows": rows} args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") if __name__ == "__main__": main()