Files

90 lines
7.2 KiB
Python

"""새 v0.2.5 WAV만 오프라인 ASR로 검사한다. 과거 승인이나 법적 인수를 승계하지 않는다."""
from dataclasses import asdict
from datetime import datetime, timezone
import hashlib
from importlib.metadata import version
import inspect
import json
import math
from pathlib import Path
import platform
import sys
import ctranslate2
from faster_whisper import WhisperModel
ROOT = Path(__file__).resolve().parents[2]
EVIDENCE = Path(__file__).resolve().parent
# 과거 실패·no-VAD 증거는 읽기만 하고 현재 결과를 독립 파일에 기록한다.
PRIOR_EVIDENCE = ROOT / ".agent/evidence-v0.2.3/privacy-screening"
REVISION = 'd90ca5fe260221311c53c58e660288d3deb8d356'
SNAPSHOT = Path.home() / '.cache/huggingface/hub/models--Systran--faster-whisper-tiny/snapshots' / REVISION
def json_safe(value):
"""JSON의 비유한 수는 명시적 객체로 보존하며 반환 segment를 삭제하거나 고치지 않는다."""
if isinstance(value, float) and not math.isfinite(value):
return {'nonFiniteFloat': 'NaN' if math.isnan(value) else 'Infinity' if value > 0 else '-Infinity'}
if isinstance(value, dict): return {key: json_safe(item) for key, item in value.items()}
if isinstance(value, (list, tuple)): return [json_safe(item) for item in value]
return value
def sha(path):
"""원본/모델/실행 스크립트의 연결은 전체 바이트 SHA-256으로 검증한다."""
with path.open('rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest()
assert version('faster-whisper') == '1.2.1'
started = datetime.now(timezone.utc).isoformat()
manifest_path = ROOT / 'public/ambience/manifest.json'
manifest_hash = sha(manifest_path)
manifest = json.loads(manifest_path.read_text())
# Preserve all effective API defaults rather than relying on undocumented invocation settings.
parameters = {name: parameter.default for name, parameter in inspect.signature(WhisperModel.transcribe).parameters.items()
if name not in ('self', 'audio')}
parameters.update(language=None, task='transcribe', beam_size=5, best_of=5, vad_filter=True, vad_parameters=None, condition_on_previous_text=True)
prior_path = PRIOR_EVIDENCE / 'prior-attempt-market60-cafe180/final-asr.json'
prior = json.loads(prior_path.read_text())
assert prior['requiredEmptyPassed'] is False
assert json_safe(parameters) == prior['method']['parameters'], 'Decoder/VAD settings must remain identical'
ctranslate2.set_random_seed(0)
model = WhisperModel(str(SNAPSHOT), device='cpu', compute_type='int8', cpu_threads=4, num_workers=1, local_files_only=True)
assert model.model.is_multilingual
results = []
for entry in manifest['files']:
path = ROOT / 'public/ambience' / entry['file']
before = sha(path)
assert before == entry['sha256'] and path.stat().st_size == entry['bytes']
segments, info = model.transcribe(str(path), **parameters)
segments = [asdict(segment) for segment in segments] # Exhaust lazy inference; retain exact unedited results.
assert sha(path) == before
record = {'environmentId': entry['environmentId'], 'file': str(path.relative_to(ROOT)), 'sha256': before,
'bytes': path.stat().st_size, 'durationSeconds': entry['duration'],
'selectedSegmentStartSeconds': entry['selectedSegmentStartSeconds'], 'sourceSha256': entry['sourceSha256'],
'language': info.language, 'languageProbability': info.language_probability,
'segments': segments, 'text': ''.join(segment['text'] for segment in segments), 'info': asdict(info)}
results.append(record)
print(json.dumps({k: record[k] for k in ('environmentId','sha256','language','languageProbability','segments','text')}, ensure_ascii=False), flush=True)
assert sha(manifest_path) == manifest_hash
required = ['mall', 'market', 'cafe']
selected = {result['environmentId']: result for result in results if result['environmentId'] in required}
assert set(selected) == set(required)
assert {key: result['selectedSegmentStartSeconds'] for key, result in selected.items()} == {'mall': 36, 'market': 24, 'cafe': 0}
passed = all(result['segments'] == [] and result['text'] == '' for result in selected.values())
report = {'schemaVersion': 1, 'startedAtUtc': started, 'completedAtUtc': datetime.now(timezone.utc).isoformat(),
'tool': {'name': 'faster-whisper', 'version': version('faster-whisper'), 'python': platform.python_version(),
'dependencies': {name: version(name) for name in ('ctranslate2', 'av', 'numpy', 'tokenizers', 'huggingface-hub')}},
'model': {'name': 'tiny', 'multilingual': True, 'repository': 'Systran/faster-whisper-tiny', 'revision': REVISION,
'files': {path.name: sha(path) for path in sorted(SNAPSHOT.iterdir()) if path.is_file()}},
'method': {'input': 'Exact final 12-second public/ambience WAV files, hash verified before and after inference; built-in Silero VAD with package defaults, no text editing or manual filtering.',
'device': 'cpu', 'computeType': 'int8', 'cpuThreads': 4, 'numWorkers': 1, 'randomSeed': 0,
'nonFiniteNumberEncoding': 'Non-finite default options use {nonFiniteFloat: Infinity|-Infinity|NaN} tagged objects; no ASR segments or text are discarded.', 'offline': True, 'parameters': parameters, 'manifestSha256': manifest_hash,
'vadModelSha256': sha(Path(__import__('faster_whisper').__file__).parent / 'assets/silero_vad_v6.onnx'), 'scriptSha256': sha(Path(__file__)), 'command': '/tmp/findword-asr-venv/bin/python .agent/evidence-v0.2.5/screen-final.py'},
'limitations': {'screeningOnly': True, 'humanListeningAcceptance': False, 'legalPrivacyClearance': False,
'textIsVerifiedTranscript': False,
'explanation': 'Tiny-model ASR is only a screening signal. Empty segments do not prove absence of speech, music or private conversation. Candidate text and language guesses may be hallucinated and are not faithful transcripts. This is NOT human listening acceptance or legal/privacy clearance.'},
'priorFailedAttempt': {'file': 'prior-attempt-market60-cafe180/final-asr.json', 'sha256': sha(prior_path), 'requiredEmptyPassed': False},
'supplementaryNoVadRun': {'file': 'no-vad-asr.json', 'sha256': sha(PRIOR_EVIDENCE / 'no-vad-asr.json'), 'requiredEmptyPassed': False, 'explanation': 'Historical no-VAD run on mall 36s, market 60s and cafe 180s emitted candidate text that may be hallucinated. It is retained unchanged, is not a screen of the replacement market/cafe WAVs, and does not override the specified VAD gate. Neither empty VAD results nor no-VAD text proves absence or presence of speech/music or provides human listening acceptance or legal/privacy clearance.'}, 'requiredEmptyEnvironmentIds': required, 'requiredEmptyPassed': passed, 'files': results}
serialized = json.dumps(json_safe(report), ensure_ascii=False, indent=2, allow_nan=False) + '\n'
with (EVIDENCE / 'final-asr.json').open('x') as stream:
stream.write(serialized)
if not passed: sys.exit('Required changed excerpts produced ASR segments; screening criterion failed.')
print('All three changed excerpts have zero emitted segments. Screening signal only; no listening acceptance or legal/privacy clearance.', flush=True)