Files

18 KiB
Raw Permalink Blame History

Find-Word v0.2.5 구현·검증 결과

작성일: 2026-10-14 UTC. 작업 경로: /home/hermes/worktrees/find-word-v0.2.5. 브랜치: fix/v0.2.5-ambience-loudness. 기준/현재 HEAD: 812540c7438639f25b886cfd620201d92de15362 (변경 없음).

결과와 인수 경계

요청한 로컬 구현을 완료했다. 선언한 FFmpeg 6.1.1 loudnorm 입력 측정 기준으로 실제 8 WAV의 integrated loudness는 −38.69~−22.20 LUFS, spread 16.49 LU → −32.01~−32.00 LUFS, spread 0.01 LU다. 고정 목표 −32 LUFS, 허용오차 ±0.2 LU, spread≤0.4 LU를 통과한다. 최대 true peak는 −6.62 dBTP, 선언 ceiling −6 dBTP 이하다. 런타임 배경 gain은 환경·현재 SNR·intro 유무에 무관한 0.4198884518664126이다.

lint/typecheck, 139 unit/component/release tests, 40 Python tests, build, npm audit, sandboxed system Chrome desktop/mobile 36 tests를 통과했다. 이전 증거 660개와 voice WAV 101개, 원본 8개·구간·creator·license·attribution을 보존했다. commit/push/merge/tag/deploy는 하지 않았다.

인수 시 다음 두 제한을 반드시 함께 읽어야 한다.

  1. 별도 FFmpeg ebur128=peak=true 진단 계기는 최종 −33.5~−31.5 LUFS, spread 2.0 LU를 보고한다. 이 계기에서는 ±0.2/0.4 LU 조건을 통과하지 않는다. 계약 계기는 사용자 baseline 16.49 LU와 같은 loudnorm input_i로 명시했다. 독립 검증은 생성기/manifest 수치를 재사용하지 않고 실제 파일을 별도 프로세스로 분석한다는 뜻이며, 서로 다른 EBU 계기의 상호 일치를 뜻하지 않는다.
  2. 새 exact WAV의 추가 ASR 재검사는 market 1 segment, requiredEmptyPassed=false다. 이를 성공으로 바꾸거나 원본 구간/ASR 설정을 변경하지 않았다. 이전 Oz 승인은 exact v0.2.3 WAV에 한정되며 새 바이트의 승인으로 확대하지 않았다. humanListeningAcceptance=false, legalPrivacyClearance=false이며 새 자산의 privacy/청취 인수와 릴리스 결정은 남아 있다.

입력·출처·버전 보존

  • .agent/TASK-v0.2.5.md, ACCEPTANCE-v0.2.5.md를 읽고 현재 코드/테스트/문서와 v0.2.3 결과·privacy/원자 발행 증거를 확인했다. 원본 task/checklist는 수정하지 않았다.
  • 원본은 /tmp/findword-ambience-sources의 8개 파일만 사용했다. 불변 선정 계획 AMBIENCE-SOURCES-v0.2.3.json의 SHA-256은 3529c8534be204208ac504a0f4c1a69b312131f9bbff92aa9a3135d61d24db65다.
  • 새 정규화 계획을 분리했다. 기존 계획의 모든 항목을 새 manifest와 비교하고, source SHA-256/bytes를 실제 원본과 대조했다. 원본 다운로드나 외부 TTS 호출은 없었다.
  • 기존 8 WAV/manifest/notice는 baseline snapshot에 보존했다. 기존 .agent/evidence*/RESULT와 다른 추적 .agent 파일 660개의 SHA-256이 모두 동일하다.
  • 101개 voice WAV는 바이트 동일하며 voice manifest의 제품 버전만 0.2.5로 갱신했다.
  • package/lock/HTML/UI/README/LICENSE/문서/두 manifest 및 voice·ambience query가 0.2.5다. 정확한 Designed by OZ at Heat-T Medical Inc.와 MIT 제3자 notice, 시장 CC BY-SA 4.0 등 기존 creator·권리·출처·변경 고지를 유지했다. 이전 CHANGELOG 본문은 그대로다.
  • 보존 결과: preservation.json, 기준 해시 목록. public 전체와 dist의 대응 파일도 바이트 동일하다.

오프라인 생성과 최종 파일 독립 측정

scripts/ambience_audio.py는 해시 검증 → 승인 구간 추출/mono/24kHz → 기존 50ms 양끝 선형 fade → 2-pass loudnorm → PCM16 인코딩 → 최종 파일 재측정 → 전체 staging 검증 → 원자 발행 순서다. Ogg seek가 짧은 PCM을 반환하면 기존 full decode/sample trim fallback을 사용하며, padding이나 구간 변경으로 길이를 맞추지 않는다.

공통 필터는 loudnorm=I=-32:TP=-6.1:LRA=50:dual_mono=false:print_format=json이다. 목표 −32는 crest factor가 큰 market까지 선형 gain만으로 −6 dBTP ceiling을 지킬 수 있도록 정했다. 필터 ceiling −6.1은 최종 ceiling −6보다 여유를 둔다. LRA=50은 원본 다이내믹을 압축하지 않기 위한 허용 상한이며 최종 LRA를 50으로 만드는 목표가 아니다.

pass1과 pass2에는 동일한 fade 적용 float PCM을 입력한다. pass2는 pass1의 measured_I/TP/LRA/thresh, offset=0:linear=true를 사용한다. 동적 1-pass 출력 예측의 target_offset은 선형 입력 gain 보정에 사용하지 않는다. FFmpeg의 0 LRA sentinel은 전달 인자만 0.01로 올리고 원래 통계를 보존한다. 실제 8개 입력은 모두 LRA>0이다. pass2가 dynamic으로 fallback하면 생성 실패로 처리한다.

round(sample×32767)로 PCM16을 만들고 clipping clamp/dither 없이 범위를 검증한다. 각 파일은 288,000 frames, 정확히 12초, 24,000Hz, mono, PCM16, 576,044 bytes, non-silent이고 고유 hash다. 시작/마지막 sample은 0이며 50ms fade를 유지한다. 생성기는 최종 WAV를 새 FFmpeg 프로세스로 읽고 input_i/input_lra/input_tp를 검사한다. 측정 필터의 output_*는 가상 재정규화 결과라서 번들 측정값으로 사용하지 않는다. pass1/pass2/final 전체 JSON과 필터는 manifest에 보존한다.

환경 보존 구간(초) 이전 LUFS 최종 LUFS 최종 LRA LU 최종 dBTP 최종 sample peak
street 35–47 −30.54 −32.00 4.00 −9.49 0.3353271484375
mall 36–48 −22.65 −32.01 1.30 −11.68 0.2449951171875
theater 45–57 −22.66 −32.00 0.90 −15.44 0.16900634765625
department 90–102 −22.22 −32.00 3.70 −16.18 0.15399169921875
market 24–36 −26.51 −32.01 7.10 −6.62 0.45123291015625
subway 65–77 −24.14 −32.00 4.90 −15.29 0.171966552734375
cafe 0–12 −22.20 −32.00 0.30 −17.59 0.131317138671875
rain 180–192 −38.69 −32.01 10.10 −13.09 0.220947265625

tests/python/test_loudness_audio.py는 생성기 함수를 import하지 않고 실제 WAV를 FFmpeg로 독립 분석한다. 같은 계기 기준 최대 목표 오차 0.01 LU, spread 0.01 LU, 최대 true peak −6.62 dBTP다. 별도 ebur128의 true peak도 모두 ceiling을 통과한다. 초기 독립 테스트의 추가 ebur128 integrated 동등성 assertion은 계기 간 불일치를 확인한 뒤 제거했고, 해당 값/로그와 제한은 그대로 공개했다. 계약 계기의 target/spread assertion은 유지한다. 이 차이의 내부 원인을 확정했다고 주장하지 않는다.

실제 FFmpeg 측정 프로세스 실패, NaN, 목표 이탈, peak 초과, 원본 mismatch, 잘못된 출처/notice, 쓰기 실패, 검증 실패, KeyboardInterrupt, renameat2 실패에서 이전 공개 디렉터리를 보존한다. 원자 교환을 지원하지 않으면 실패하며 비원자 교체로 우회하지 않는다. 전원 손실 내구성 보장은 별도다.

런타임 고정 gain과 fail-closed 경계

이전 +6dB/luck-apple/intro 조건의 market 배경 gain은 0.7259647911927253, 최종 RMS는 0.021778943734156713이었으며 다른 환경은 gain=1/RMS=.03이었다. 기존 peak 의존 gain은 전체 비교에서도 1,084개 서로 다른 값을 만들어 문제를 재현했다.

새 런타임은 배경 PCM을 그대로 복사하고, voice/intro만 기존 RMS .03으로 정규화한다. 검증된 보수적 peak 상한은 B=.55, V=.25, I=.20, intro 최대 상대 gain은 1.8, 허용 SNR_MAX는 12dB, master≤1에서 합성 ceiling은 .8이다.

r_max = 10^(12/20) = 3.9810717055349722
D = B + V*r_max + I*1.8 = 1.905267926383743
G = min(1, 0.8/D) = 0.4198884518664126
backgroundGain(db, environment, introPresent) = G
voiceGain(db) = G*10^(db/20)
introGain = G*1.8
|mixedSample| ≤ master*G*(B + V*r_max + I*1.8) ≤ 0.8

삼각부등식으로 모든 위상/동시 overlap의 최악값까지 포함한다. 현재 파일의 실측 peak, 환경, 현재 SNR, intro 유무는 G 계산에 들어가지 않는다. 배경 fade envelope는 기존과 같으며 그 최대 gain만 G다. SNR은 voice의 기준 대비 gain 차이이지 실제 음성/배경 RMS 동등성을 뜻하지 않는다. UI 설명도 이에 맞게 수정했다. master와 intro fade, 3문항/단계/정답/점수/재청취 동작은 유지한다.

실제 101 voice × 8 ambience × −12~+12의 모든 정수 25 SNR = 20,200개를 검사했다. 실측 최대 voice peak=.2052977830171585, intro=.14995422959327698, background=.45123291015625. 이를 사용한 최악 합성 peak 상계는 .6459797899491659 < .8이며 배경 gain 집합의 크기는 1이다. gain-math.json, 전수 GREEN.

Chrome에서 109 WAV를 24k/44.1k/48k/96kHz로 모두 디코드했다(프로젝트별 436개 측정). 리샘플 후 최대 background=.466545045375824, voice=.20585650454525403, intro=.14999594059820148로 모든 보수적 상한 이하다. 실제 장치 rate에서 상한을 넘는 파일은 gain을 낮추지 않고 재생을 거부한다. URL/bytes/SHA-256은 앱에 번들한 manifest를 기준으로 디코드 전에 확인한다. 외부/알 수 없는/이전 query, 변조·축약 응답, 잘못된 채널/길이, nonfinite/무음/과도 peak는 fail closed한다. HTTP 200 WAV 한 sample 변조도 실제 Chrome에서 소스 시작 전에 차단하고, 원본 재시도는 성공한다.

실제 native Web Audio 메서드를 실행하면서 8환경 × SNR −12/0/+12의 GainNode를 프로젝트별 24회 관찰했다. 예약 값은 모두 .4198884518664126, AudioParam.value 실측은 모두 .4198884665966034(float32 반올림)다. 값에 맞춰 기다리지 않고 실제 AudioContext 시계가 fade 종료 뒤에 도달했을 때 한 번 읽는다. master=1, voice/intro RMS .03, 같은 환경의 배경 RMS가 SNR별로 동일함도 확인했다. 최종 browser summary에 원문 첨부 경로가 있다.

Strict vertical-slice TDD 기록

기대 실패를 먼저 실행하고 원문을 보존한 뒤 해당 구현을 진행했다. 실패 로그를 덮어쓰지 않았다.

Slice 실제 RED 증거 변경 후 GREEN 증거
실제 WAV loudness/spread/metadata 02-loudness-RED: 16.49 LU 및 metadata 누락 07-loudness-GREEN, 35 독립 재측정
측정 실패/목표·TP·NaN 원자 발행 차단 03-generator-RED: 4 실패 29-python: 40 통과
고정 gain/실제 전수 peak 04-runtime-RED: market 추가 감쇠·gain 불일치 09-runtime-GREEN, 26 전수
loader 배경 원본 PCM/peak 차단 08-loader-RED 13-integrity-runtime-GREEN
제품/자산 0.2.5 정렬 11-version-RED: 8 실패 24-release-GREEN
실제 응답 bytes/hash 무결성 12-integrity-RED: 새 경계 모듈 없음 13-integrity-runtime-GREEN, 37 Chrome
기준 대비 SNR UI 설명 17-snr-copy-RED 24-release-GREEN
빌드 manifest 계약 위조 거부 22-build-contract-RED: 5 실패 24-release-GREEN, 36-build

중간 실행의 실수/실패도 남겼다. 01-loudness-RED.log는 harness 경로 오류여서 유효한 제품 RED로 세지 않는다. 06-generator-GREEN.log는 이름과 달리 실패한 중간 실행이다(0 LRA fixture의 FFmpeg sentinel); 수정 후 16-converter-GREEN.log와 최종 Python이 통과했다. 15-lint.log의 관측 코드 타입 오류를 수정했다. 21-browser-initial.log는 33 통과/1 실패이며 wall clock과 audio clock의 차이 때문에 너무 일찍 GainNode를 읽었다. clock 기반 관측으로 수정한 focused 6 tests(33-browser-gains-resample.log)와 최종 36 tests가 통과했다. 30-unit.log는 138 통과/1 실패였으며, 늘어난 실제 Python 계약 검사가 기존 subprocess 60초 제한을 넘었다. 180초로 조정 후 동일 테스트를 포함한 34-unit.log가 전부 통과했다. 20-asr.log는 현재도 실패 기록이며 GREEN으로 분류하지 않는다.

최종 실행 결과와 재검증 명령

환경: Node v26.7.0, npm 11.19.0, Python 3.11.16, FFmpeg 6.1.1-3ubuntu5, Google Chrome 146.0.7680.80. environment.json.

검사 결과 증거
npm run lint (ESLint + tsc) PASS 32-lint.log
npm test 15 files / 139 tests PASS 34-unit.log
python3 -m unittest discover -s tests/python -v 40 tests PASS, 64.954초 29-python.log
npm run build PASS, 109 WAV와 notice/manifest 검증 36-build.log
npm audit --json 0 vulnerabilities npm-audit.json
system Chrome desktop/mobile 전체 36 PASS, skipped/flaky/unexpected=0, 296.138초 37-browser.log, JSON
정상 실제 오디오 흐름 console/page errors=0, failed requests=0, external runtime requests=0, axe violations=0 browser-summary.json
보존/원본/배포물 byte 대조 PASS 38-preservation.log, preservation.json
secret scan PASS, 저장소/새 증거의 텍스트 검사 40-final-checks.log
git diff --check PASS 40-final-checks.log
npm run lint
npm test
python3 -m unittest discover -s tests/python -v
npm run build
npm audit --json
CHROME_PATH=/usr/bin/google-chrome PLAYWRIGHT_NO_SANDBOX=0 \
  PLAYWRIGHT_JSON_OUTPUT_FILE=.agent/evidence-v0.2.5/browser-results.json \
  npx playwright test --reporter=line,json --output=.agent/evidence-v0.2.5/browser-final
python3 .agent/evidence-v0.2.5/measure.py
python3 .agent/evidence-v0.2.5/reproduce.py
python3 .agent/evidence-v0.2.5/check-preservation.py
python3 .agent/evidence-v0.2.5/secret-scan.py
git diff --check

Chrome은 sandbox를 켠 system executable로 실행했다. 작업 디렉터리로 범위를 제한한 실제 Chrome 프로세스 인자에 --no-sandbox가 없음을 chrome-sandbox-final.json에 보존했다. 109 WAV의 해시/24kHz decode, 모든 환경 실제 loop/start/stop, 3문항 고정 단계·다음 단계· 시작 단계·SNR·중도 종료·무응답·재청취·음량·404 재시도·layout/axe를 검사했다. 의도적으로 주입한 404/변조 시나리오의 오류는 정상 흐름 0건 통계에서 제외한다. desktop/mobile은 같은 system Chrome의 viewport/touch emulation이며 물리 모바일/Safari 인증이 아니다.

Privacy, 문서와 릴리스 인계

추가 ASR은 로컬 캐시의 동일 tiny multilingual 모델, faster-whisper 1.2.1 CPU int8, beam_size=5, best_of=5, 기본 VAD, condition_on_previous_text=true로 새 정확한 해시를 검사했다. 시장 1개 segment와 나머지 7개 0개 결과를 원문 그대로 final-asr.json에 보존했다. ASR text는 검증된 전사가 아니며 환각일 수 있고, 성공/실패 모두 실제 대화·음악의 유무나 법적 허용 여부를 확정하지 않는다. 이전 실패/no-VAD/최종 결과/승인은 원본 파일 그대로다. 원문 ASR의 prior/supplementary 경로는 기존 v0.2.3 privacy-screening 증거 기준이다. 새 privacy gate 실패를 숨기기 위해 segment를 삭제하거나 모델/VAD/threshold/원본 구간을 바꾸지 않았다.

README, ARCHITECTURE, AUDIO_PROVENANCE, DEPLOYMENT, WORDS, CHANGELOG, license notice를 갱신했다. 변경한 DSP/loader/무결성/생성기/빌드 검증과 테스트에는 핵심 매개변수·상한·공식·실패 경계의 한국어 설계 주석을 추가했다. 과거 문서의 v0.2.3 출처/측정/승인은 역사 기록으로 표시했다.

수용 기준 중 로컬 구현/검증은 위 선언한 loudnorm 측정 범위에서 완료했다. 별도 ebur128 계기 불일치와 새 ASR 실패는 인수 제한으로 남는다. ACCEPTANCE의 릴리스 3항(Neo 독립 검증 후 commit-addressed 원자 배포, 공개 WAV LUFS/true peak/hash/MIME·production Chrome, feature push/main fast-forward/annotated tag/fresh clone)은 사용자 지시에 따라 미실행이다. secrets를 읽거나 사용하지 않았고 외부 메시지·원격 Git 변경·배포를 하지 않았다.