Files
Find-Word/.agent/evidence-v0.1.1/verify_audio.py
T

72 lines
4.1 KiB
Python

"""생성기 함수를 재사용하지 않는 v0.1.1 인수 검사: 원문·이전 해시·PCM·FFmpeg."""
from array import array
import hashlib
import json
import math
from pathlib import Path
import subprocess
import sys
import wave
root = Path(__file__).resolve().parents[2]
evidence = root / ".agent/evidence-v0.1.1"
manifest = json.loads((root / "public/audio/manifest.json").read_text())
baseline = json.loads((evidence / "baseline.json").read_text())
old = {entry["file"]: entry for entry in baseline["audio"]["files"]}
expected = json.loads(subprocess.check_output([
"node", "--experimental-strip-types", "--input-type=module", "-e",
"import {INTRO,PATTERNS,WORDS,renderSentence} from './src/content.ts'; "
"console.log(JSON.stringify([{file:'intro.wav',text:INTRO},"
"...PATTERNS.flatMap(p=>WORDS.map(w=>({file:p.id+'-'+w.id+'.wav',text:renderSentence(p,w)})))]));"
], cwd=root, text=True))
assert manifest["version"] == "0.1.1"
assert manifest["provider"] == "Microsoft Edge online TTS"
assert manifest["voice"] == "ko-KR-HyunsuMultilingualNeural"
assert manifest["rate"] == "-6%" and manifest["pitch"] == "-4Hz"
assert manifest["toolVersions"]["edge-tts"] == "7.3.1"
assert [{"file": e["file"], "text": e["text"]} for e in manifest["files"]] == expected
assert len(expected) == 101
assert {p.name for p in (root / "public/audio").iterdir()} == {e["file"] for e in expected} | {"manifest.json"}
rows = []
for entry in manifest["files"]:
path = root / "public/audio" / entry["file"]
data = path.read_bytes()
digest = hashlib.sha256(data).hexdigest()
assert digest == entry["sha256"] and digest != old[entry["file"]]["sha256"]
assert len(data) == entry["bytes"]
with wave.open(str(path), "rb") as stream:
assert (stream.getframerate(), stream.getnchannels(), stream.getsampwidth(), stream.getcomptype()) == (24000, 1, 2, "NONE")
duration = stream.getnframes()/24000
assert 1 <= duration <= 20 and duration == entry["duration"]
samples = array("h", stream.readframes(stream.getnframes()))
if sys.byteorder != "little":
samples.byteswap()
rms = math.sqrt(sum(x*x for x in samples)/len(samples))/32768
peak = max(abs(x) for x in samples)/32768
assert rms > 100/32768 and peak < 32767/32768
assert peak/rms*0.03*2 < 0.8
assert abs(entry["rms"]-rms) < 1e-12 and abs(entry["peak"]-peak) < 1e-12
probe = json.loads(subprocess.check_output([
"ffprobe", "-v", "error", "-show_entries",
"stream=codec_name,sample_fmt,sample_rate,channels,bits_per_sample,duration", "-of", "json", str(path)
], text=True))["streams"]
assert len(probe) == 1
stream = probe[0]
assert (stream["codec_name"], stream["sample_fmt"], stream["sample_rate"], stream["channels"], stream["bits_per_sample"]) == ("pcm_s16le", "s16", "24000", 1, 16)
assert abs(float(stream["duration"])-duration) < 1e-5
subprocess.run(["ffmpeg", "-v", "error", "-xerror", "-i", str(path), "-f", "null", "-"], check=True, capture_output=True)
rows.append({"file": path.name, "sha256": digest, "bytes": len(data), "duration": duration,
"rms": rms, "peak": peak, "ffprobe": stream, "newVsV01": True, "ffmpegDecoded": True})
print(f"PASS {path.name}: {duration:.3f}s, RMS={rms:.6f}, peak={peak:.6f}", flush=True)
assert len({r["sha256"] for r in rows}) == 101
assert sum(r["bytes"] for r in rows) < 48*1024*1024
for path, digest in baseline["historical"].items():
assert hashlib.sha256((root / path).read_bytes()).hexdigest() == digest, path
summary = {"count": len(rows), "newVsV01": 101, "uniqueHashes": 101, "bytes": sum(r["bytes"] for r in rows),
"durationRange": [min(r["duration"] for r in rows), max(r["duration"] for r in rows)],
"rmsRange": [min(r["rms"] for r in rows), max(r["rms"] for r in rows)],
"peakRange": [min(r["peak"] for r in rows), max(r["peak"] for r in rows)],
"historicalUnchanged": len(baseline["historical"]), "files": rows}
(evidence / "audio-integrity.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2)+"\n")
print(json.dumps({k:v for k,v in summary.items() if k != "files"}, indent=2))