Files

229 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 변경·배포를 하지 않았다.