Files

229 lines
18 KiB
Markdown
Raw Permalink Normal View History

2026-10-14 12:12:13 +00:00
# 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](TASK-v0.2.5.md), [ACCEPTANCE-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](AMBIENCE-SOURCES-v0.2.3.json)의 SHA-256은
`3529c8534be204208ac504a0f4c1a69b312131f9bbff92aa9a3135d61d24db65`다.
- 새 [정규화 계획](AMBIENCE-NORMALIZATION-v0.2.5.json)을 분리했다. 기존 계획의 모든 항목을 새 manifest와
비교하고, source SHA-256/bytes를 실제 원본과 대조했다. 원본 다운로드나 외부 TTS 호출은 없었다.
- 기존 8 WAV/manifest/notice는 [baseline snapshot](evidence-v0.2.5/baseline/ambience/)에 보존했다.
기존 `.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](evidence-v0.2.5/preservation.json),
[기준 해시 목록](evidence-v0.2.5/baseline.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은 유지한다. 이 차이의 내부 원인을 확정했다고 주장하지 않는다.
- [독립 before/after 전체 측정·hash](evidence-v0.2.5/independent-loudness.json)
- [독립 측정 실행 로그](evidence-v0.2.5/35-independent-measurement.log)
- [public manifest 전체 pass 통계](../public/ambience/manifest.json)
- [재현성 결과](evidence-v0.2.5/reproducibility.json): 원본에서 별도 디렉터리로 두 번 생성해
현재 public WAV/notice와 바이트 동일, manifest는 실행 시각 `generatedAtUtc`만 제외하고 완전 동일.
- [재현 스크립트](evidence-v0.2.5/reproduce.py), [실행 로그](evidence-v0.2.5/25-reproducibility.log).
재현성 범위는 기록된 FFmpeg 버전/입력/플랫폼이다. 다른 FFmpeg 빌드의 바이트 동일성을 보장하지 않는다.
실제 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**이다.
```text
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](evidence-v0.2.5/gain-math.json), [전수 GREEN](evidence-v0.2.5/26-exhaustive-gains-GREEN.log).
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](evidence-v0.2.5/browser-summary.json)에 원문 첨부 경로가 있다.
## Strict vertical-slice TDD 기록
기대 실패를 먼저 실행하고 원문을 보존한 뒤 해당 구현을 진행했다. 실패 로그를 덮어쓰지 않았다.
| Slice | 실제 RED 증거 | 변경 후 GREEN 증거 |
| --- | --- | --- |
| 실제 WAV loudness/spread/metadata | [02-loudness-RED](evidence-v0.2.5/02-loudness-RED.log): 16.49 LU 및 metadata 누락 | [07-loudness-GREEN](evidence-v0.2.5/07-loudness-GREEN.log), [35 독립 재측정](evidence-v0.2.5/35-independent-measurement.log) |
| 측정 실패/목표·TP·NaN 원자 발행 차단 | [03-generator-RED](evidence-v0.2.5/03-generator-RED.log): 4 실패 | [29-python](evidence-v0.2.5/29-python.log): 40 통과 |
| 고정 gain/실제 전수 peak | [04-runtime-RED](evidence-v0.2.5/04-runtime-RED.log): market 추가 감쇠·gain 불일치 | [09-runtime-GREEN](evidence-v0.2.5/09-runtime-GREEN.log), [26 전수](evidence-v0.2.5/26-exhaustive-gains-GREEN.log) |
| loader 배경 원본 PCM/peak 차단 | [08-loader-RED](evidence-v0.2.5/08-loader-RED.log) | [13-integrity-runtime-GREEN](evidence-v0.2.5/13-integrity-runtime-GREEN.log) |
| 제품/자산 0.2.5 정렬 | [11-version-RED](evidence-v0.2.5/11-version-RED.log): 8 실패 | [24-release-GREEN](evidence-v0.2.5/24-release-GREEN.log) |
| 실제 응답 bytes/hash 무결성 | [12-integrity-RED](evidence-v0.2.5/12-integrity-RED.log): 새 경계 모듈 없음 | [13-integrity-runtime-GREEN](evidence-v0.2.5/13-integrity-runtime-GREEN.log), [37 Chrome](evidence-v0.2.5/37-browser.log) |
| 기준 대비 SNR UI 설명 | [17-snr-copy-RED](evidence-v0.2.5/17-snr-copy-RED.log) | [24-release-GREEN](evidence-v0.2.5/24-release-GREEN.log) |
| 빌드 manifest 계약 위조 거부 | [22-build-contract-RED](evidence-v0.2.5/22-build-contract-RED.log): 5 실패 | [24-release-GREEN](evidence-v0.2.5/24-release-GREEN.log), [36-build](evidence-v0.2.5/36-build.log) |
중간 실행의 실수/실패도 남겼다. `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](evidence-v0.2.5/environment.json).
| 검사 | 결과 | 증거 |
| --- | --- | --- |
| `npm run lint` (ESLint + tsc) | PASS | [32-lint.log](evidence-v0.2.5/32-lint.log) |
| `npm test` | 15 files / **139 tests PASS** | [34-unit.log](evidence-v0.2.5/34-unit.log) |
| `python3 -m unittest discover -s tests/python -v` | **40 tests PASS**, 64.954초 | [29-python.log](evidence-v0.2.5/29-python.log) |
| `npm run build` | PASS, 109 WAV와 notice/manifest 검증 | [36-build.log](evidence-v0.2.5/36-build.log) |
| `npm audit --json` | **0 vulnerabilities** | [npm-audit.json](evidence-v0.2.5/npm-audit.json) |
| system Chrome desktop/mobile 전체 | **36 PASS**, skipped/flaky/unexpected=0, 296.138초 | [37-browser.log](evidence-v0.2.5/37-browser.log), [JSON](evidence-v0.2.5/browser-results.json) |
| 정상 실제 오디오 흐름 | console/page errors=0, failed requests=0, external runtime requests=0, axe violations=0 | [browser-summary.json](evidence-v0.2.5/browser-summary.json) |
| 보존/원본/배포물 byte 대조 | PASS | [38-preservation.log](evidence-v0.2.5/38-preservation.log), [preservation.json](evidence-v0.2.5/preservation.json) |
| secret scan | PASS, 저장소/새 증거의 텍스트 검사 | [40-final-checks.log](evidence-v0.2.5/40-final-checks.log) |
| `git diff --check` | PASS | [40-final-checks.log](evidence-v0.2.5/40-final-checks.log) |
```bash
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](evidence-v0.2.5/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](evidence-v0.2.5/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 변경·배포를 하지 않았다.