190 lines
15 KiB
Markdown
190 lines
15 KiB
Markdown
# Find-Word v0.1.1 — 한국어 neural voice 업그레이드 결과
|
||
|
||
작성일: 2026-10-14 UTC. worktree: `/home/hermes/worktrees/find-word-v0.1.1`.
|
||
브랜치: `fix/v0.1.1-neural-voice`.
|
||
입력 `.agent/TASK-v0.1.1.md`, `.agent/ACCEPTANCE-v0.1.1.md` 및 기존 맥락을 읽었다.
|
||
입력 문서는 수정하지 않았다. **Git commit/push, main merge, 서버 배포는 실행하지 않았다.**
|
||
|
||
## 구현 결과
|
||
|
||
- package·lock·UI·index·README·LICENSE·현재 버전 문서·manifest를 **0.1.1**로 맞췄다.
|
||
- 정확한 크레딧 `Designed by OZ at Heat-T Medical Inc.`와 canonical repository
|
||
`https://source.ibit.kr/OZ-Group/Find-Word`를 유지했다.
|
||
- **Microsoft Edge online TTS**, **ko-KR-HyunsuMultilingualNeural**,
|
||
**edge-tts 7.3.1**, rate **-6%**, pitch **-4Hz**로 안내 1개·목표 100개를
|
||
모두 실제로 새로 생성했다. 기존 WAV를 재포장하거나 재사용하지 않았다.
|
||
- pinned `.venv-audio/bin/python`과 `requirements-audio.txt`로 생성 절차를 고정했다.
|
||
전역 Python의 다른 버전으로 fallback하지 않는다. FFmpeg 실측 버전은
|
||
`6.1.1-3ubuntu5`이다.
|
||
- 임시 MP3를 FFmpeg로 24kHz mono PCM16으로 디코드하고 선형 gain만 적용한다.
|
||
파일별 RMS 목표 0.10, 증폭 최대 2배, peak ceiling 0.89. 압축·게이트·음절 편집은 없다.
|
||
- 임시 디렉터리에서 101개 전체 검증 후 Linux `renameat2(RENAME_EXCHANGE)`로
|
||
단일 원자 교환한다. provider 최대 3회, 1/2초 backoff, 시도당 60초 제한.
|
||
변환 실패·검증 실패·중단·원자 교체 실패는 기존 자산을 보존한다.
|
||
- manifest에 원문·SHA-256·bytes·duration·RMS·peak와 provider·voice·prosody·버전·
|
||
sample rate·생성 시각·실측 도구 버전을 남겼다. 인증 헤더·쿠키·API 키는 저장하지 않는다.
|
||
- 기존 생성 도구의 캐시/다운로드/문서/manifest 의존을 제거했다. 기존 v0.1 기록은 보존했다.
|
||
- 10개 mechanic, 100문장, 8환경, 5SNR, 객관식·주관식, IME·재생 취소·재청취·
|
||
실패 복구 흐름은 기존 소스와 테스트를 보존했다. 앱은 로컬 WAV만 요청한다.
|
||
|
||
## 구현 전에 확인한 실제 RED → GREEN
|
||
|
||
RED를 모두 실행한 뒤 구현했다. 생성기의 새 함수 계약은 `NotImplementedError`만
|
||
발생시키는 비동작 stub으로 선언했고, 기존 명령 진입점은 RED 때 그대로 두어 import
|
||
부작용도 실패로 확인했다. stub에 성공 경로를 미리 구현하지 않았다.
|
||
RED는 각 명령 종료 코드 **1**, GREEN은 **0**이다. 실패 로그를 덮어쓰지 않았다.
|
||
|
||
| 계약 | 실제 RED | 실제 GREEN | 원문 evidence |
|
||
| --- | --- | --- | --- |
|
||
| UI/package/lock/index/문서 버전 및 pinned 생성·권리 문서 | `npm test -- tests/neural-release.test.ts tests/App.test.tsx -t '0.1.1\|브랜드\|고정 venv'`: 3 failed, 기존 12개는 필터로 제외 | 동일 명령 3 passed | [RED](evidence-v0.1.1/01-version-docs-red.log), [GREEN](evidence-v0.1.1/08-version-docs-green.log) |
|
||
| neural provider/voice/prosody/실측 버전, 101개 PCM·원문·hash·bytes·duration | `npm test -- tests/assets.test.ts`: 2 failed. 기존 16kHz·누락 metadata/duration을 실제 검출 | 최종 전체 suite에서 두 계약 모두 통과 | [RED](evidence-v0.1.1/02-assets-red.log), [GREEN](evidence-v0.1.1/11-full-test.log) |
|
||
| 원자 교체·초기 공개·bounded retry·timeout·기존 자산 보존·secret redaction·실제 MP3 변환·PCM 검증·import 부작용 | Python unittest 16개 모두 RED (subtest 포함 failures=4, errors=20) | 아래 추가 계약을 포함한 18개 통과 | [RED](evidence-v0.1.1/03-generator-red.log), [GREEN](evidence-v0.1.1/05-generator-green.log) |
|
||
| 앞서 생성한 파일 손상 감지, 다른 edge-tts 버전의 외부 요청 전 거부 | 구현 전 전체 18개 RED (subtest 포함 failures=5, errors=21) | `python3 -m unittest discover -s tests/python -v`: 18 passed | [추가 RED](evidence-v0.1.1/04-generator-additional-red.log), [GREEN](evidence-v0.1.1/05-generator-green.log) |
|
||
|
||
Python 계약은 fake provider/작은 PCM fixture로 실패를 강제한다. 원자 디렉터리 교체와
|
||
FFmpeg MP3 변환은 테스트에서도 실제 실행한다. 이 fixture는 public 음성에 사용하지
|
||
않았다. 실제 provider의 101개 생성은 다음 별도 로그에 있으며 실패/재시도는 0회였다.
|
||
전체 `npm test`는 Python 계약 runner도 포함하며 **skip 없이** 통과했다.
|
||
|
||
## 실제 음성 생성·전수 독립 검사
|
||
|
||
명령: `npm run audio:generate`.
|
||
생성 완료: **2026-10-14T06:55:41.751382+00:00**.
|
||
[101개 실제 생성 로그](evidence-v0.1.1/06-audio-generate.log).
|
||
|
||
| 항목 | 실제 결과 |
|
||
| --- | --- |
|
||
| WAV 개수 | 101개, 101개 해시 모두 고유하고 v0.1의 같은 파일과 모두 다름 |
|
||
| WAV 합계 | **34,898,524바이트** (약 33.282MiB), 48MiB 예산 이내 |
|
||
| PCM | 전부 **24,000Hz · mono · signed 16-bit PCM** |
|
||
| duration | **3.816–11.256초**, 안내 7.056초 |
|
||
| RMS 범위 | **0.0999998534–0.1000000486** |
|
||
| peak 범위 | **0.3692016602–0.6843261719**, full-scale 0개 |
|
||
| 원문·bytes·duration·SHA-256 | 101/101 일치 |
|
||
| FFprobe 포맷 확인 + FFmpeg 전체 디코드 | 101/101 성공 |
|
||
| Chrome 실제 WAV 디코드·해시·레벨 검사 | 101/101 성공, 외부 페이지 요청 0·JS 예외 0 |
|
||
| 남은 임시 MP3/공개 staging 폴더 | 0개 |
|
||
|
||
[독립 검사 스크립트](evidence-v0.1.1/verify_audio.py)는 생성기의 검증 함수를
|
||
import하지 않는다. Python wave·해시·앱 원문과 FFprobe/FFmpeg를 별도로 대조한다.
|
||
[원문 로그](evidence-v0.1.1/12-independent-audio.log),
|
||
[파일별 수치 JSON](evidence-v0.1.1/audio-integrity.json).
|
||
|
||
[Chrome 전수 검사 스크립트](evidence-v0.1.1/verify_browser.mjs)는 production preview의
|
||
WAV를 실제 `OfflineAudioContext`로 디코드한다. 응답 bytes/hash, 채널, rate, duration,
|
||
RMS, peak와 UI/index 버전을 검사한다.
|
||
[Chrome 로그](evidence-v0.1.1/15-browser-audio.log),
|
||
[101개 디코드 결과 및 sandbox 상태](evidence-v0.1.1/browser-audio-integrity.json).
|
||
|
||
청취용: [intro.wav](../public/audio/intro.wav), [luck-apple.wav](../public/audio/luck-apple.wav).
|
||
|
||
## 최종 검증
|
||
|
||
환경: Node **26.7.0**, npm **11.19.0**, Python **3.11.16**, Chrome **146.0.7680.80**,
|
||
Linux 6.8.0, glibc 2.39. [실측 환경과 pip check](evidence-v0.1.1/18-environment.log).
|
||
|
||
| 검증 | 결과 | evidence |
|
||
| --- | --- | --- |
|
||
| `npm ci` | PASS, 238 packages 설치 | [로그](evidence-v0.1.1/00-npm-ci.log) |
|
||
| `npm run lint` | PASS, ESLint + TypeScript strict | [최종 로그](evidence-v0.1.1/16-final-lint.log) |
|
||
| `npm test` | **11 files / 60 passed**, skip 0; Python 18개 계약 포함 | [로그](evidence-v0.1.1/11-full-test.log) |
|
||
| `npm run audio:generate` | **101개 신규 생성·검증·원자 교체 성공** | [로그](evidence-v0.1.1/06-audio-generate.log) |
|
||
| `npm run build` | PASS, 101개 해시·런타임 MIT notice 전문 검증 | [로그](evidence-v0.1.1/13-build.log) |
|
||
| sandboxed system Chrome desktop/mobile suite | **12 passed (2.3m)**, skip 0 | [로그](evidence-v0.1.1/14-browser.log) |
|
||
| sandboxed Chrome 101개 전수 디코드 | PASS, Namespace/PID/Network/Seccomp sandbox 활성 | [로그](evidence-v0.1.1/15-browser-audio.log) |
|
||
| `npm audit --audit-level=high` | **0 vulnerabilities**, npm 개발 의존성 포함 | [로그](evidence-v0.1.1/10-audit.log) |
|
||
| 독립 101개 원문/hash/PCM/FFmpeg 검사 | PASS | [JSON](evidence-v0.1.1/audio-integrity.json) |
|
||
| 소스·문서·manifest 비밀값 패턴 검사 | 40개 텍스트 파일, 0 matches | [기록](evidence-v0.1.1/17-security-history.json) |
|
||
| `git diff --check`·문서 링크·sandbox 실행 인자·HEAD·역사 기록 최종 검사 | PASS | [최종 무결성](evidence-v0.1.1/19-final-integrity.json) |
|
||
| 과거 evidence + RESULT 해시 대조 | **115개 전부 동일** | [baseline](evidence-v0.1.1/baseline.json), [검사](evidence-v0.1.1/17-security-history.json) |
|
||
|
||
브라우저 전체 명령:
|
||
`env -u PLAYWRIGHT_NO_SANDBOX CHROME_PATH=/usr/bin/google-chrome DEBUG=pw:browser npm run test:browser`.
|
||
두 viewport(1280×1000, 360×800)에서 실제 음성으로 10문제 완주·재청취·404 재시도·
|
||
키보드·레이아웃·reduced motion·axe·크레딧·MIT HTTP 응답이 모두 통과했다.
|
||
시작·게임·결과 axe 위반 0, 외부 페이지 요청 0, 페이지 JS 예외 0이다.
|
||
로컬 preview만 테스트 중 열었으며 서버 배포는 하지 않았다.
|
||
Chrome 전수 검사에서는 `chrome://sandbox`의 Namespace·Seccomp 상태도 실제 확인했다.
|
||
headless 브라우저는 실제 파일을 디코드하고 Web Audio를 실행하지만 물리적 스피커
|
||
청취를 대신하지 않는다. Chrome stderr의 시스템 GPU/GCM 진단과 페이지 JS 예외는 구분한다.
|
||
|
||
스크린샷: [desktop 시작](evidence-v0.1.1/desktop-home.png),
|
||
[게임](evidence-v0.1.1/desktop-game.png), [결과](evidence-v0.1.1/desktop-results.png),
|
||
[mobile 시작](evidence-v0.1.1/mobile-home.png), [게임](evidence-v0.1.1/mobile-game.png),
|
||
[결과](evidence-v0.1.1/mobile-results.png).
|
||
|
||
## 변경 파일과 기록 보존
|
||
|
||
- 앱 표기: `src/App.tsx`, `index.html`, `package.json`, `package-lock.json`.
|
||
- 생성: `scripts/generate_audio.py`, 신규 `scripts/neural_audio.py`,
|
||
`requirements-audio.txt`, `.gitignore`, `eslint.config.js`의 venv 제외.
|
||
- 자산: `public/audio/*.wav` 101개와 `manifest.json`.
|
||
- 계약 테스트: `tests/assets.test.ts`, `tests/App.test.tsx`, 신규
|
||
`tests/neural-release.test.ts`, `tests/generator.test.ts`, `tests/python/test_neural_audio.py`.
|
||
- 기존 browser tests는 새 스크린샷 경로와 v0.1.1 실측 길이 주석만 변경했다.
|
||
`tests/browser/game.spec.ts`, `tests/browser/ui.spec.ts`. 기존 assertion/timeout은 유지했다.
|
||
- 문서: `README.md`, `LICENSE`, `docs/AUDIO_PROVENANCE.md`,
|
||
`docs/ARCHITECTURE.md`, `docs/DEPLOYMENT.md`.
|
||
- 이번 기록: `.agent/RESULT-v0.1.1.md`, `.agent/evidence-v0.1.1/`.
|
||
|
||
이전 `.agent/RESULT.md`, `.agent/evidence/`의 파일은 수정하지 않았다.
|
||
스크린샷도 새 경로에만 저장한다. 입력 TASK/ACCEPTANCE의 시작 상태는 untracked였으며
|
||
Codex가 새로 작성한 입력 문서가 아니다. 전체 파일 목록은
|
||
[changed-files.txt](evidence-v0.1.1/changed-files.txt)에 별도로 남긴다.
|
||
|
||
## 남은 청감·provider 한계
|
||
|
||
- 자동 검사와 실제 브라우저 재생은 완료했지만 **사람의 청감 평가를 수행한 것은 아니다**.
|
||
한국어 발음·유사 자모 구분·웃음·소음 혼합 난이도는 대표 샘플과 게임으로 직접 평가해야 한다.
|
||
- 원격 모델/서비스는 고정할 수 없다. pinned client로 절차를 재현해도 다음 생성의
|
||
바이트가 같다는 보장은 없다. provider 장애·제한·음성 목록 변경 시 명확히 실패한다.
|
||
- edge-tts의 LGPLv3(일부 MIT)와 FFmpeg의 빌드별 LGPL/GPL은 provider 서비스 조건 및
|
||
음성 출력의 권리와 별개다. 생성 음성의 독점 권리나 포괄적 상업 이용 허락을 주장하지 않는다.
|
||
근거 링크·확인일·조건의 한계는 [AUDIO_PROVENANCE](../docs/AUDIO_PROVENANCE.md)에 기록했다.
|
||
- 원자 교체는 Linux와 해당 파일시스템의 지원이 필요하다. SIGKILL/전원 손실 때 임시
|
||
숨김 폴더는 남을 수 있고 전원 손실 내구성까지 보장하지 않는다. 일반 실패·Ctrl-C는 정리한다.
|
||
- 실제 모바일 Safari/물리적 오디오 장치 청취는 이번 Chrome 검증 범위 밖이다.
|
||
|
||
## 2026-10-14 추가 — 배포 후 오디오 캐시 회귀 수정
|
||
|
||
제품 버전 **0.1.1**을 유지하면서 intro와 목표 100개 WAV 모두
|
||
`?v=0.1.1`을 붙이도록 수정했다. `src/audio/assets.ts`의
|
||
`AUDIO_ASSET_VERSION`과 `audioAssetUrl`을 출제·오디오 엔진이 함께 사용한다.
|
||
WAV/manifest 및 package/lock은 변경하지 않았다.
|
||
|
||
발견 원인은 **음성 내용 교체 후에도 요청 URL이 같았던 것**이다. 사용자 보고의
|
||
배포 Nginx `/audio` 7일 캐시에서 기존 사용자는 이전 음성을 계속 받을 수 있었다.
|
||
기존 배포 문서의 재검증 가정과 실제 설정이 달랐으며, 브라우저 테스트의
|
||
전체 URL `.endsWith('.wav')`와 404 glob도 query string을 놓쳤다.
|
||
원격 서버에는 접속하지 않고 소스·로컬 production build에서 재현했다.
|
||
|
||
앱 구현을 수정하기 전에 unit/browser 계약을 작성하고 실제 RED를 확인했다.
|
||
RED 때 앱 소스가 그대로였다는 기록은 [04-red-state.json](evidence-v0.1.1/cache-regression/04-red-state.json)에 있다.
|
||
|
||
| 계약/검증 | 결과 | 새 evidence |
|
||
| --- | --- | --- |
|
||
| 목표 100개 버전·intro/target fetch·각각 404 retry·query pathname 판정 | RED 5 failed / 12 passed → GREEN 17 passed | [RED](evidence-v0.1.1/cache-regression/01-unit-red.log), [GREEN](evidence-v0.1.1/cache-regression/05-unit-green.log) |
|
||
| 실제 브라우저 요청/정답 mapping·intro/target 404·101개 URL | 구현 전 desktop RED 4 failed → 전체 desktop/mobile GREEN 16 passed (2.6m) | [RED](evidence-v0.1.1/cache-regression/03-browser-red.log), [GREEN](evidence-v0.1.1/cache-regression/09-browser-green.log) |
|
||
| `npm run lint` | PASS | [로그](evidence-v0.1.1/cache-regression/06-lint.log) |
|
||
| `npm test` | 12 files / 65 passed, skip 0; Python 18개 계약 포함 | [로그](evidence-v0.1.1/cache-regression/07-full-test.log) |
|
||
| `npm run build` | PASS, 101개 WAV·notice 검증 | [로그](evidence-v0.1.1/cache-regression/08-build.log) |
|
||
| 실제 Chrome sandbox | Namespace·PID·Network·Seccomp-BPF 활성, `--no-sandbox` 없음 | [로그](evidence-v0.1.1/cache-regression/11-sandbox-green.log) |
|
||
| query URL 전수 Chrome 해시·bytes·24kHz mono 디코드·duration·RMS·peak | 두 viewport 각각 101/101, 외부 요청 0·JS 예외 0 | [desktop](evidence-v0.1.1/cache-regression/artifacts/desktop-browser-audio-integrity.json), [mobile](evidence-v0.1.1/cache-regression/artifacts/mobile-browser-audio-integrity.json) |
|
||
| 과거 evidence·자산·제품 버전 파일 보존 및 HEAD·diff 검사 | 254개 해시 동일, 과거 evidence 149개 포함 | [최종 무결성](evidence-v0.1.1/cache-regression/13-final-integrity.json) |
|
||
|
||
전체 브라우저 명령은
|
||
`env -u PLAYWRIGHT_NO_SANDBOX CHROME_PATH=/usr/bin/google-chrome DEBUG=pw:browser npm run test:browser`이다.
|
||
실제 재생·재청취·10문제 완주·404 복구 후 정답·접근성 검증을 유지했다.
|
||
101개 query URL 검사를 `tests/browser/audio.spec.ts`로 정규 suite에 편입했고,
|
||
URL 판정 helper의 query 순서·가짜 WAV 접미사도 단위 테스트로 고정했다.
|
||
런타임 외부 요청이나 manifest 조회는 추가하지 않았다.
|
||
|
||
브라우저 스크린샷은 이제 `test-results/`를 사용하며 이번 결과는
|
||
[새 artifacts 경로](evidence-v0.1.1/cache-regression/artifacts/)에 복사했다.
|
||
과거 evidence와 기존 이 보고서 본문은 보존하고 이번 기록만 추가했다.
|
||
보조 sandbox probe의 최초 라벨 판정 오류와 수정 결과도
|
||
[원인·RED/GREEN·최종 결과 로그](evidence-v0.1.1/cache-regression/12-cache-regression-result.log)에 기록했다.
|
||
|
||
README·ARCHITECTURE·DEPLOYMENT에 자산 버전 갱신 규칙과 7일 캐시 예시를 반영했다.
|
||
이 변경의 검증 대상은 로컬 build이며 **Git commit/push/tag/merge와 서버 배포는 실행하지 않았다.**
|