diff --git a/.agent/ACCEPTANCE-v0.1.1.md b/.agent/ACCEPTANCE-v0.1.1.md new file mode 100644 index 0000000..a5489ca --- /dev/null +++ b/.agent/ACCEPTANCE-v0.1.1.md @@ -0,0 +1,21 @@ +# ACCEPTANCE — Find-Word v0.1.1 + +- [ ] package/UI/index/README/manifest 버전 0.1.1 +- [ ] 정확한 크레딧 유지 +- [ ] `ko-KR-HyunsuMultilingualNeural`, edge-tts 7.3.1 기반 101개 신규 WAV +- [ ] 24kHz mono 16-bit PCM +- [ ] 모든 WAV 비무음, full-scale peak 없음, 합리적 duration +- [ ] manifest 원문·hash·bytes·duration·provider·voice·prosody·tool version +- [ ] 임시 디렉터리 전체 생성 후 원자 교체 +- [ ] bounded retry와 실패 시 기존 자산 보존 +- [ ] runtime 외부 TTS/API 요청 없음 +- [ ] eSpeak 생성 의존 제거 +- [ ] audio provenance와 생성 도구 라이선스·provider 한계 문서 +- [ ] 기존 10개 mechanic/100문장/8환경/5SNR/choice+text 보존 +- [ ] lint PASS +- [ ] 전체 tests PASS +- [ ] build PASS +- [ ] desktop/mobile browser tests PASS +- [ ] audit high 0 +- [ ] `.agent/RESULT-v0.1.1.md` +- [ ] Codex는 commit/push/deploy 미실행 diff --git a/.agent/RESULT-v0.1.1.md b/.agent/RESULT-v0.1.1.md new file mode 100644 index 0000000..d40aaab --- /dev/null +++ b/.agent/RESULT-v0.1.1.md @@ -0,0 +1,146 @@ +# 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 검증 범위 밖이다. diff --git a/.agent/TASK-v0.1.1.md b/.agent/TASK-v0.1.1.md new file mode 100644 index 0000000..abaf78a --- /dev/null +++ b/.agent/TASK-v0.1.1.md @@ -0,0 +1,59 @@ +# TASK — Find-Word v0.1.1 Korean neural voice upgrade + +## 사용자 지시 + +현재 v0.1의 한국어 음성이 제대로 만들어지지 않았다. 음성 품질을 높이고 샘플을 다시 생성하며 버전을 0.1.1로 올린다. 모든 소스는 Gitea에 push 및 main merge할 예정이다. Codex는 구현만 하고 commit/push/deploy는 하지 않는다. + +## 고정 계약 + +- 정확한 크레딧: `Designed by OZ at Heat-T Medical Inc.` +- canonical repository: `https://source.ibit.kr/OZ-Group/Find-Word` +- 작업 브랜치: `fix/v0.1.1-neural-voice` +- 버전: UI/package/docs/manifest 모두 `0.1.1` +- 한국어 상세 소스 주석 유지 +- 기존 10개 mechanic, 100개 문장 조합, 8개 환경, 5단계 SNR, 객관식·주관식 기능 보존 + +## 선택한 생성 방식 + +- 직접 생성한 사전 샘플에서 사용 가능한 고품질 음성: `ko-KR-HyunsuMultilingualNeural` +- 생성 클라이언트: `edge-tts==7.3.1` +- 권장 prosody: rate `-6%`, pitch `-4Hz` +- 앱 런타임에는 외부 TTS/API 요청이 없어야 한다. +- 생성 시 임시 MP3를 받아 ffmpeg로 24kHz mono PCM s16 WAV로 변환한다. +- 음성 간 레벨 편차와 peak를 완화하되 발음을 훼손하는 과도한 DSP를 하지 않는다. +- 네트워크·provider 오류에 bounded retry와 명확한 실패를 제공한다. +- 부분 실패가 기존 public/audio를 훼손하지 않도록 임시 디렉터리에서 전체 생성·검증 후 원자 교체한다. +- API 키·쿠키·인증정보를 저장하지 않는다. + +## 품질·재현성 요구사항 + +- intro 1개 + 목표 문장 100개, 총 101개를 모두 새로 생성한다. +- manifest에 version, provider, voice, rate, pitch, edge-tts version, ffmpeg version, sample rate, SHA-256, bytes, duration, 원문을 기록한다. +- 24kHz mono 16-bit PCM, 비무음, peak full-scale 미도달, 비정상적으로 짧거나 긴 파일 없음. +- `audio:generate`는 pinned venv의 Python으로 재현 가능해야 하고 README/AUDIO_PROVENANCE에 설치·생성 절차와 provider 의존성·한계를 기록한다. +- `public/audio/intro.wav`와 대표 샘플 `luck-apple.wav`가 청취용 샘플이다. +- eSpeak cache/download 경로·문서·manifest 의존성을 제거한다. 과거 evidence는 당시 기록이므로 내용 변조 없이 보존한다. +- 생성 도구의 오픈소스 라이선스와 서비스 사용 조건을 과장 없이 문서화한다. 프로젝트가 음성 자체에 독점 권리를 주장한다고 쓰지 않는다. + +## TDD + +1. 버전 0.1.1 계약 테스트 RED +2. neural provider/voice/manifest 계약 RED +3. 24kHz·비무음·peak·duration·101개 integrity RED +4. generator atomicity/retry/unsafe secret absence 테스트 RED +5. 기존 full suite와 browser suite GREEN + +## 최종 검증 + +- npm ci +- npm run lint +- npm test +- npm run audio:generate 또는 명시적 생성 명령 +- npm run build +- sandboxed system Chrome browser tests +- npm audit --audit-level=high +- 101개 manifest/hash/PCM 독립 검사 + +## 기록 + +`.agent/RESULT-v0.1.1.md`에 RED/GREEN, 변경 파일, 생성 provider/voice, 자산 크기, 최종 결과, 남은 청감·provider 위험을 기록한다. diff --git a/.agent/evidence-v0.1.1/00-audio-venv.log b/.agent/evidence-v0.1.1/00-audio-venv.log new file mode 100644 index 0000000..2a0294d --- /dev/null +++ b/.agent/evidence-v0.1.1/00-audio-venv.log @@ -0,0 +1,44 @@ +Collecting edge-tts==7.3.1 + Using cached edge_tts-7.3.1-py3-none-any.whl.metadata (6.1 kB) +Collecting aiohttp<4.0.0,>=3.8.0 (from edge-tts==7.3.1) + Using cached aiohttp-3.14.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (8.3 kB) +Collecting certifi>=2023.11.17 (from edge-tts==7.3.1) + Using cached certifi-2026.7.22-py3-none-any.whl.metadata (2.5 kB) +Collecting tabulate<1.0.0,>=0.4.4 (from edge-tts==7.3.1) + Using cached tabulate-0.10.0-py3-none-any.whl.metadata (40 kB) +Collecting typing-extensions<5.0.0,>=4.1.0 (from edge-tts==7.3.1) + Using cached typing_extensions-4.16.0-py3-none-any.whl.metadata (3.3 kB) +Collecting aiohappyeyeballs>=2.5.0 (from aiohttp<4.0.0,>=3.8.0->edge-tts==7.3.1) + Using cached aiohappyeyeballs-2.7.1-py3-none-any.whl.metadata (5.9 kB) +Collecting aiosignal>=1.4.0 (from aiohttp<4.0.0,>=3.8.0->edge-tts==7.3.1) + Using cached aiosignal-1.4.0-py3-none-any.whl.metadata (3.7 kB) +Collecting attrs>=17.3.0 (from aiohttp<4.0.0,>=3.8.0->edge-tts==7.3.1) + Using cached attrs-26.1.0-py3-none-any.whl.metadata (8.8 kB) +Collecting frozenlist>=1.1.1 (from aiohttp<4.0.0,>=3.8.0->edge-tts==7.3.1) + Using cached frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (20 kB) +Collecting multidict<8.0,>=4.5 (from aiohttp<4.0.0,>=3.8.0->edge-tts==7.3.1) + Using cached multidict-7.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (6.2 kB) +Collecting propcache>=0.2.0 (from aiohttp<4.0.0,>=3.8.0->edge-tts==7.3.1) + Using cached propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (25 kB) +Collecting yarl<2.0,>=1.25.1 (from aiohttp<4.0.0,>=3.8.0->edge-tts==7.3.1) + Using cached yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (111 kB) +Collecting idna>=2.0 (from yarl<2.0,>=1.25.1->aiohttp<4.0.0,>=3.8.0->edge-tts==7.3.1) + Using cached idna-3.20-py3-none-any.whl.metadata (7.2 kB) +Using cached edge_tts-7.3.1-py3-none-any.whl (38 kB) +Using cached aiohttp-3.14.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (1.9 MB) +Using cached certifi-2026.7.22-py3-none-any.whl (136 kB) +Using cached tabulate-0.10.0-py3-none-any.whl (39 kB) +Using cached typing_extensions-4.16.0-py3-none-any.whl (45 kB) +Using cached aiohappyeyeballs-2.7.1-py3-none-any.whl (15 kB) +Using cached aiosignal-1.4.0-py3-none-any.whl (7.5 kB) +Using cached attrs-26.1.0-py3-none-any.whl (67 kB) +Using cached frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl (231 kB) +Using cached multidict-7.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (480 kB) +Using cached propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (235 kB) +Using cached yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (117 kB) +Using cached idna-3.20-py3-none-any.whl (69 kB) +Installing collected packages: typing-extensions, tabulate, propcache, multidict, idna, frozenlist, certifi, attrs, aiohappyeyeballs, yarl, aiosignal, aiohttp, edge-tts +Successfully installed aiohappyeyeballs-2.7.1 aiohttp-3.14.5 aiosignal-1.4.0 attrs-26.1.0 certifi-2026.7.22 edge-tts-7.3.1 frozenlist-1.8.0 idna-3.20 multidict-7.1.0 propcache-0.5.4 tabulate-0.10.0 typing-extensions-4.16.0 yarl-1.25.1 + +[notice] A new release of pip is available: 24.0 -> 26.2.1 +[notice] To update, run: python -m pip install --upgrade pip diff --git a/.agent/evidence-v0.1.1/00-npm-ci.log b/.agent/evidence-v0.1.1/00-npm-ci.log new file mode 100644 index 0000000..d06cb33 --- /dev/null +++ b/.agent/evidence-v0.1.1/00-npm-ci.log @@ -0,0 +1,7 @@ + +added 238 packages, and audited 239 packages in 3s + +56 packages are looking for funding + run `npm fund` for details + +found 0 vulnerabilities diff --git a/.agent/evidence-v0.1.1/01-version-docs-red.log b/.agent/evidence-v0.1.1/01-version-docs-red.log new file mode 100644 index 0000000..6a344e7 --- /dev/null +++ b/.agent/evidence-v0.1.1/01-version-docs-red.log @@ -0,0 +1,2745 @@ + +> find-word@0.1.0 test +> vitest run tests/neural-release.test.ts tests/App.test.tsx -t 0.1.1|브랜드|고정 venv + + + RUN v5.0.4 /home/hermes/worktrees/find-word-v0.1.1 + + ❯ tests/neural-release.test.ts (2 tests | 2 failed) 55ms + × 0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다 23ms + × 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 30ms + ❯ tests/App.test.tsx (13 tests | 1 failed | 12 skipped) 242ms + ❯ 시작부터 결과까지 사용자 흐름 (13) + × 브랜드·버전·크레딧을 보여주며 직접 시작하기 전에는 재생하지 않는다 241ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/App.test.tsx > 시작부터 결과까지 사용자 흐름 > 브랜드·버전·크레딧을 보여주며 직접 시작하기 전에는 재생하지 않는다 +TestingLibraryElementError: Unable to find an element with the text: v0.1.1. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. + +Ignored nodes: comments, script, style + + 
 +  + 본문으로 건너뛰기 +  +  +  +  +  +  +  +  +  + 

 + 단어를 맞춰라 + 

 +  + v0.1 +  + 
 +  +  + 플레이 방법  +  + ↓ +  +  +  + 소리에 집중하는 시간 +  +  +  +  +  +  +  +  귀를 기울이면, 발견하는 즐거움 + 

 +  + 소음 속 작은 단어, + 
 +  + 들리시나요? +  +  +  + 도시의 소리 사이에 에스가 숨겨 둔 단어. + 
 + 조금 더 귀 기울여, 오늘의 행운을 찾아보세요. + 

 +  +  + 10개의 질문 +  +  + 5단계 도전 +  +  + 나만의 속도로 +  +  +  +  +  +  +  +  에스의 사운드 룸 +  +  + 8 + 가지 환경 +  +  +  +  +  + ♪ + 
 +  + ✳ +  +  + 행운의 단어를 찾아보거라! +  +  +  +  +  +  +  +  + S +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 소음 사이, 숨겨진 한마디 +  +  +  +  +  +  +  + 01 +  + 
 +  + 나의 소리 탐험 준비 +  + 

 + 편한 방식으로 시작하세요. 난이도는 조금씩 높아져요. + 

 + 
 +  + 약 3–5분 +  +  +  +  +  + 어떻게 답할까요? +  +  +  +  +  + ◈ +  +  + 골고루 풀기 +  +  + 선택 + 직접 입력 +  +  +  +  +  + ☷ +  +  + 보기에서 선택 +  +  + 4개 단어 중 하나 +  +  +  +  +  + ✎ +  +  + 직접 입력 +  +  + 기억한 단어 쓰기 +  +  +  +  +  +  + 배경 환경 +  +  +  + 여덟 가지 일상 속으로 +  +  + 길거리 +  +  + 쇼핑몰 +  +  + 극장 +  +  + 백화점 +  +  + 시장 +  +  + 지하철 +  +  + 카페 +  +  + 비 오는 거리 +  +  + 

 + 길거리부터 비 오는 거리까지, 매번 다른 공간을 만나요. + 

 +  +  + 길거리 +  +  + 카페 +  +  + 지하철 +  +  + +5개 환경 +  +  +  +  +  +  +  + ◖)) +  +  전체 음량 +  +  +  + 35 + % +  +  +  + 

 +  + ♬ +  +  편안한 작은 음량으로 시작해 보세요. + 

 +  + 게임 시작 · 소리 듣기  +  + → +  +  +  +  +  +  + 작은 단어를 발견하는 방법 +  +  + 
 +  + 01 +  + 

 + 먼저, 귀 기울이기 + 

 + 

 + 에스의 안내와 소음 속 문장을 들어요. + 
 + 처음에는 또렷하게, 점점 더 작게. + 

 + 
 + 
 +  + 02 +  + 

 + 기억한 단어 찾기 + 

 + 

 + 소리가 끝나면 질문이 나타나요. + 
 + 보기에서 고르거나 직접 입력해요. + 

 + 
 + 
 +  + 03 +  + 

 + 나의 발견 돌아보기 + 

 + 

 + 열 문제 뒤 나의 결과를 확인해요. + 
 + 놓쳤다면 언제든 다시 들어도 좋아요. + 

 + 
 +  +  +  +  + 

 + Designed by OZ at Heat-T Medical Inc. + 

 + 

 + 청각적 주의·식별 게임 · 의료적 진단이나 치료를 제공하지 않습니다. + 

 +  + 개인정보 수집 없이, 오직 플레이만. +  +  +  + + ❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:37:19 + ❯ node_modules/@testing-library/dom/dist/query-helpers.js:76:38 + ❯ node_modules/@testing-library/dom/dist/query-helpers.js:52:17 + ❯ node_modules/@testing-library/dom/dist/query-helpers.js:95:19 + ❯ tests/App.test.tsx:204:19 + 202| }), + 203| ).toBeInTheDocument(); + 204| expect(screen.getByText("v0.1.1")).toBeInTheDocument(); + | ^ + 205| expect(f.audio.play).not.toHaveBeenCalled(); + 206| expect( + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/21]⎯ + + FAIL tests/neural-release.test.ts > 0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다 +AssertionError: expected '0.1.0' to be '0.1.1' // Object.is equality + +Expected: "0.1.1" +Received: "0.1.0" + + ❯ tests/neural-release.test.ts:9:28 + 7| const pkg = JSON.parse(readFileSync("package.json", "utf8")); + 8| const lock = JSON.parse(readFileSync("package-lock.json", "utf8")); + 9| expect.soft(pkg.version).toBe("0.1.1"); + | ^ + 10| expect.soft(lock.version).toBe("0.1.1"); + 11| expect.soft(lock.packages[""].version).toBe("0.1.1"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/21]⎯ + + FAIL tests/neural-release.test.ts > 0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다 +AssertionError: expected '0.1.0' to be '0.1.1' // Object.is equality + +Expected: "0.1.1" +Received: "0.1.0" + + ❯ tests/neural-release.test.ts:10:29 + 8| const lock = JSON.parse(readFileSync("package-lock.json", "utf8")); + 9| expect.soft(pkg.version).toBe("0.1.1"); + 10| expect.soft(lock.version).toBe("0.1.1"); + | ^ + 11| expect.soft(lock.packages[""].version).toBe("0.1.1"); + 12| for (const path of ["src/App.tsx", "index.html", "README.md", "LICEN… + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/21]⎯ + + FAIL tests/neural-release.test.ts > 0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다 +AssertionError: expected '0.1.0' to be '0.1.1' // Object.is equality + +Expected: "0.1.1" +Received: "0.1.0" + + ❯ tests/neural-release.test.ts:11:42 + 9| expect.soft(pkg.version).toBe("0.1.1"); + 10| expect.soft(lock.version).toBe("0.1.1"); + 11| expect.soft(lock.packages[""].version).toBe("0.1.1"); + | ^ + 12| for (const path of ["src/App.tsx", "index.html", "README.md", "LICEN… + 13| expect.soft(readFileSync(path, "utf8"), path).toContain("0.1.1"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/21]⎯ + + FAIL tests/neural-release.test.ts > 0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다 +AssertionError: src/App.tsx: expected 'import { useEffect, useRef, useState …' to contain '0.1.1' + +- Expected ++ Received + +- 0.1.1 ++ import { useEffect, useRef, useState } from "react"; ++ import { AudioEngine, type AudioPort } from "./audio/engine"; ++ import { ENVIRONMENTS, LEVELS } from "./content"; ++ import { ++ createQuestions, ++ gameReducer, ++ initialGame, ++ normalizeAnswer, ++ summarize, ++ type Action, ++ type GameState, ++ type Mode, ++ type Question, ++ } from "./game"; ++ ++ // 화면은 순수 reducer의 단계만 표시한다. 엔진과 난수 seed를 주입할 수 있어 ++ // 사용자 흐름을 실제 장치 없이도 재현하고 별도로 실제 브라우저에서 검증한다. ++ export interface AppProps { ++ audio?: AudioPort; ++ seed?: number; ++ } ++ ++ function SoundWave({ active = false }: { active?: boolean }) { ++ // 파형은 장식이며 답의 글자 수나 실제 음성 데이터를 노출하지 않는다. ++ return ( ++ ++ ); ++ } ++ ++ function Mascot({ small = false }: { small?: boolean }) { ++ // CSS로 직접 그린 캐릭터라 외부 이미지·폰트·저작권 불명 자산이 필요 없다. ++ return ( ++ ++ ); ++ } ++ ++ // 난이도의 dB는 배경 대비 음성의 상대 크기다. 게임·결과에서 같은 표기를 ++ // 쓰고 부호의 의미를 풀어 설명하되 엔진에 전달하는 수치는 바꾸지 않는다. ++ function formatSnr(db: number) { ++ return `SNR ${db > 0 ? "+" : ""}${db} dB`; ++ } ++ ++ function describeSnr(db: number) { ++ if (db === 0) return "음성과 배경의 크기가 같아요."; ++ return `음성이 배경보다 ${Math.abs(db)} dB ${db > 0 ? "커요" : "작아요"}.`; ++ } ++ ++ export default function App({ audio, seed }: AppProps) { ++ const [engine] = useState(() => audio ?? new AudioEngine()); ++ const [mode, setMode] = useState("mixed"); ++ const [environment, setEnvironment] = useState("rotate"); ++ const [volume, setVolume] = useState(35); ++ const [game, setGame] = useState(null); ++ const [input, setInput] = useState(""); ++ const [attempt, setAttempt] = useState(0); ++ const token = useRef(0); ++ const playing = useRef(false); ++ const introDone = useRef(false); ++ // 한글 IME의 Enter는 글자 조합 확정에도 사용된다. 조합 중 form 제출은 ++ // 막고 확정 뒤 명시적인 제출만 채점하여 모바일·데스크톱 입력을 보호한다. ++ const composing = useRef(false); ++ const focusTarget = useRef(null); ++ const startButton = useRef(null); ++ const dispatch = (action: Action) => ++ setGame((previous) => ++ previous ? gameReducer(previous, action) : previous, ++ ); ++ ++ useEffect( ++ () => () => { ++ token.current++; ++ playing.current = false; ++ engine.dispose(); ++ }, ++ [engine], ++ ); ++ useEffect(() => { ++ // 단계가 바뀌면 제목으로 초점을 옮겨 스크린리더·키보드 사용자 모두 새 ++ // 질문/피드백의 위치를 알게 한다. 입력 중 재렌더에는 초점을 빼앗지 않는다. ++ if ( ++ game?.phase === "answer" || ++ game?.phase === "feedback" || ++ game?.phase === "results" || ++ game?.phase === "ready" ++ ) ++ focusTarget.current?.focus(); ++ }, [game?.phase, game?.index]); ++ ++ const run = async (question: Question) => { ++ if (playing.current) return; ++ // 다시 듣기로 입력 DOM이 사라질 때 compositionend가 생략되는 브라우저도 ++ // 있으므로 재생 경계에서 조합 잠금을 해제한다. 입력 값 자체는 유지한다. ++ composing.current = false; ++ playing.current = true; ++ const currentToken = ++token.current; ++ dispatch({ type: "PLAY" }); ++ try { ++ await engine.play(question, !introDone.current, () => { ++ if (currentToken === token.current) dispatch({ type: "STARTED" }); ++ }); ++ if (currentToken !== token.current) return; ++ introDone.current = true; ++ dispatch({ type: "ENDED" }); ++ } catch (error) { ++ if (currentToken !== token.current) return; ++ // 내부 URL·예외 스택 대신 이해할 수 있는 복구 동작을 안내한다. ++ const detail = ++ error instanceof Error && error.name === "AbortError" ++ ? "듣기를 중단했습니다. 다시 시도해 주세요." ++ : "소리를 재생하지 못했어요. 브라우저의 소리 허용과 연결 상태를 확인한 뒤 다시 시도해 주세요."; ++ dispatch({ type: "ERROR", message: detail }); ++ } finally { ++ if (currentToken === token.current) playing.current = false; ++ } ++ }; ++ ++ const begin = () => { ++ const questions = createQuestions( ++ (seed ?? Date.now()) + attempt, ++ mode, ++ environment, ++ ); ++ introDone.current = false; ++ composing.current = false; ++ setInput(""); ++ setGame(initialGame(questions)); ++ // 사용자 클릭 호출 스택에서 엔진을 열어 자동 재생 정책을 준수한다. ++ void run(questions[0]!); ++ }; ++ const home = () => { ++ token.current++; ++ playing.current = false; ++ engine.stop(); ++ composing.current = false; ++ setGame(null); ++ setInput(""); ++ setAttempt((a) => a + 1); ++ requestAnimationFrame(() => startButton.current?.focus()); ++ }; ++ const next = () => { ++ composing.current = false; ++ setInput(""); ++ dispatch({ type: "NEXT" }); ++ }; ++ const question = game?.questions[game.index]; ++ const result = game ? summarize(game) : null; ++ const busy = game?.phase === "loading" || game?.phase === "playing"; ++ ++ // 상태와 엔진 연결은 공유하고 각 화면에는 컨트롤 하나만 배치한다. ++ // 위치가 바뀌어도 기존 접근성 이름·label 연결·사용자가 정한 음량을 유지한다. ++ const volumeControl = ( ++
++ ++ { ++ const value = Number(event.target.value); ++ setVolume(value); ++ engine.setVolume(value / 100); ++ }} ++ /> ++ {volume}% ++
++ ); ++ ++ return ( ++ <> ++ ++ 본문으로 건너뛰기 ++ ++
++
++ ++

단어를 맞춰라

++ v0.1 ++
++
++ {game ? ( ++ ++ ) : ( ++ ++ 플레이 방법 ++ ++ )} ++ ++ 소리에 집중하는 시간 ++
++
++ ++
++ {!game ? ( ++ <> ++
++
++

++ 귀를 기울이면, 발견하는 즐거움 ++

++

++ 소음 속 작은 단어, ++
++ 들리시나요? ++

++

++ 도시의 소리 사이에 에스가 숨겨 둔 단어. ++
++ 조금 더 귀 기울여, 오늘의 행운을 찾아보세요. ++

++
++ 10개의 질문 ++ 5단계 도전 ++ 나만의 속도로 ++
++
++
++
++ ++ 에스의 사운드 룸 ++ ++ {ENVIRONMENTS.length}가지 환경 ++
++
++
++ ++ ++
행운의 단어를 찾아보거라!
++ ++
++ ++ 소음 사이, 숨겨진 한마디 ++
++
++
++ ++
++
++ 01 ++
++

나의 소리 탐험 준비

++

편한 방식으로 시작하세요. 난이도는 조금씩 높아져요.

++
++ 약 3–5분 ++
++
++
++ 어떻게 답할까요? ++
++ {( ++ [ ++ { ++ value: "mixed", ++ name: "골고루 풀기", ++ sub: "선택 + 직접 입력", ++ icon: "◈", ++ }, ++ { ++ value: "choice", ++ name: "보기에서 선택", ++ sub: "4개 단어 중 하나", ++ icon: "☷", ++ }, ++ { ++ value: "text", ++ name: "직접 입력", ++ sub: "기억한 단어 쓰기", ++ icon: "✎", ++ }, ++ ] as const ++ ).map((option) => ( ++ ++ ))} ++
++
++
++ ++ ++

++ {ENVIRONMENTS.find((e) => e.id === environment) ++ ?.description ?? ++ "길거리부터 비 오는 거리까지, 매번 다른 공간을 만나요."} ++

++
++ 길거리 ++ 카페 ++ 지하철 ++ +5개 환경 ++
++
++
++ {volumeControl} ++
++

++ 편안한 작은 음량으로 시작해 ++ 보세요. ++

++ ++
++
++ ++
++

작은 단어를 발견하는 방법

++
++
++ 01 ++

먼저, 귀 기울이기

++

++ 에스의 안내와 소음 속 문장을 들어요. ++
++ 처음에는 또렷하게, 점점 더 작게. ++

++
++
++ 02 ++

기억한 단어 찾기

++

++ 소리가 끝나면 질문이 나타나요. ++
++ 보기에서 고르거나 직접 입력해요. ++

++
++
++ 03 ++

나의 발견 돌아보기

++

++ 열 문제 뒤 나의 결과를 확인해요. ++
++ 놓쳤다면 언제든 다시 들어도 좋아요. ++

++
++
++
++ ++ ) : game.phase === "results" && result ? ( ++
++ 열 번의 귀 기울임, 하나의 발견 ++
++ ++
++

++ 오늘의 소리 탐험을 마쳤어요. ++

++

++ 작은 소리에 집중한 당신, 다음 탐험에서도 만나요. ++

++
++
++ {result.score.toLocaleString()} ++ 총 점수 ++
++
++ {result.accuracy}% ++ 정답률 · {result.correct}/10 ++
++
++ ++ {game.bestStreak} ++ 회 ++ ++ 최고 연속 정답 ++
++
++ ++ ++ ++ ++ ++ ++ ++ ++ ++ {result.byLevel.map((row) => ( ++ ++ ++ ++ ++ ++ ))} ++ ++
난이도SNR맞힌 문제
{row.level}단계 ++ {formatSnr(LEVELS[row.level - 1]!.db)} ++ ++ {row.correct} / {row.total} ++
++

단계 × 100점 + 연속 정답 보너스 (최대 40점)

++ ++
++ ) : question && result ? ( ++
++
++ 나의 소리 탐험 ++ ++ 문제 {String(game.index + 1).padStart(2, "0")} / 10 ++ ++
++ ++
++ ++
++
++ {question.pattern.title} ++ ++ {question.mode === "choice" ? "객관식" : "주관식"} ++ ++
++ {game.phase === "answer" ? ( ++ <> ++

++ {question.pattern.question} ++

++

++ {question.mode === "choice" ++ ? "가장 또렷하게 기억나는 단어를 골라 주세요." ++ : "기억나는 단어 하나를 입력해 주세요."} ++

++ {question.mode === "choice" ? ( ++
++ {question.options.map((option, i) => ( ++ ++ ))} ++
++ ) : ( ++
{ ++ event.preventDefault(); ++ if (composing.current) return; ++ dispatch({ type: "ANSWER", input }); ++ }} ++ > ++ ++ { ++ composing.current = true; ++ }} ++ onCompositionEnd={() => { ++ composing.current = false; ++ }} ++ onKeyDown={(event) => { ++ if ( ++ event.key === "Enter" && ++ (event.nativeEvent.isComposing || ++ event.nativeEvent.keyCode === 229) ++ ) ++ event.preventDefault(); ++ }} ++ autoComplete="off" ++ maxLength={60} ++ value={input} ++ onChange={(event) => setInput(event.target.value)} ++ placeholder="여기에 단어를 적어 주세요" ++ /> ++ ++
++ )} ++ ++ ) : game.phase === "feedback" ? ( ++
++ ++

++ {game.answers.at(-1)!.correct ++ ? "정답이에요!" ++ : "다음엔 찾을 수 있어요."} ++

++

++ 숨겨진 단어는 {question.word.answer} ++

++
{question.sentence}
++

++ 내 답: {game.answers.at(-1)!.input} · + ++ {game.answers.at(-1)!.points}점 ++

++ ++
++ ) : ( ++
++ ++

++ {busy ++ ? "소리 사이에 집중해 보세요." ++ : "다음 단어를 만나 볼까요?"} ++

++

++ 문장이 끝나면 질문이 나타나요. ++
++ 작은 단어 하나를 기억해 주세요. ++

++

++ {game.phase === "loading" ++ ? "소리를 준비하고 있어요…" ++ : game.phase === "playing" ++ ? "에스의 목소리를 듣고 있어요…" ++ : "준비되면 문장 듣기를 눌러 주세요."} ++

++
++ )} ++ {game.error && ( ++
++

{game.error}

++ ++
++ )} ++ {game.phase !== "feedback" && !game.error && ( ++
++ ++ {busy && ( ++ ++ )} ++ 다시 들어도 점수는 줄어들지 않아요. ++
++ )} ++
++
++
++ ) : null} ++ {game?.phase === "results" && volumeControl} ++
++
++

Designed by OZ at Heat-T Medical Inc.

++

청각적 주의·식별 게임 · 의료적 진단이나 치료를 제공하지 않습니다.

++ 개인정보 수집 없이, 오직 플레이만. ++
++ ++ ); ++ } ++ + + ❯ tests/neural-release.test.ts:13:51 + 11| expect.soft(lock.packages[""].version).toBe("0.1.1"); + 12| for (const path of ["src/App.tsx", "index.html", "README.md", "LICEN… + 13| expect.soft(readFileSync(path, "utf8"), path).toContain("0.1.1"); + | ^ + 14| } + 15| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/21]⎯ + + FAIL tests/neural-release.test.ts > 0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다 +AssertionError: index.html: expected '\n ++ ++ ++ ++ ++ ++ ++ 단어를 맞춰라 · Find-Word v0.1 ++ ++ ++
++ ++ ++ ++ + + ❯ tests/neural-release.test.ts:13:51 + 11| expect.soft(lock.packages[""].version).toBe("0.1.1"); + 12| for (const path of ["src/App.tsx", "index.html", "README.md", "LICEN… + 13| expect.soft(readFileSync(path, "utf8"), path).toContain("0.1.1"); + | ^ + 14| } + 15| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/21]⎯ + + FAIL tests/neural-release.test.ts > 0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다 +AssertionError: README.md: expected '# 단어를 맞춰라 · Find-Word v0.1\n\n소음 속에 숨…' to contain '0.1.1' + +- Expected ++ Received + +- 0.1.1 ++ # 단어를 맞춰라 · Find-Word v0.1 ++ ++ 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. ++ ++ **Designed by OZ at Heat-T Medical Inc.** ++ ++ Canonical repository: [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word). ++ 프로젝트 자체는 **private / all-rights-reserved**이며 `package.json`의 ++ `license`는 `UNLICENSED`입니다. [LICENSE](LICENSE)를 참고하세요. ++ 런타임 의존성 React, ReactDOM, scheduler의 MIT 전문은 ++ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)에 보존하며, ++ `public/THIRD_PARTY_NOTICES.md`가 빌드 시 `dist/THIRD_PARTY_NOTICES.md`로 복사됩니다. ++ 빌드 검증기는 설치된 의존성의 라이선스 전문·버전과 세 사본을 대조합니다. ++ ++ React + TypeScript strict + Vite로 만든 정적 SPA입니다. 로그인, 개인정보 저장, ++ 분석 도구, 외부 CDN, 런타임 TTS/API 요청이 없습니다. 청각적 주의·식별 게임이며 ++ 의료적 진단·치료나 효과를 주장하지 않습니다. ++ ++ ## 실행 ++ ++ Node.js 최소 버전은 **22.22.2** (`>=22.22.2`)입니다. 잠긴 jsdom 계열 의존성이 ++ 요구하는 지원 범위는 22.x의 22.22.2 이상, 24.x의 24.15.0 이상 또는 26.0.0 이상입니다. ++ 이 중 지원되는 LTS 버전을 사용하세요. `package-lock.json`을 ++ 포함하므로 새 환경에서는 `npm ci`를 사용합니다. 이번 검증 환경은 Node 26.7.0입니다. ++ ++ ```bash ++ npm ci ++ npm run dev ++ ``` ++ ++ 표시되는 `http://127.0.0.1:5173`에서 사용합니다. 소리는 사용자가 **게임 시작 · 소리 듣기**를 ++ 누른 뒤에만 재생합니다. 낮은 음량부터 조절하고 헤드폰이나 스피커를 사용할 수 있습니다. ++ ++ ## 플레이 ++ ++ - 골고루 풀기, 보기에서 선택, 직접 입력 중 선택합니다. ++ - 8개 환경을 순환하거나 하나를 지정합니다. ++ - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다. ++ - 10문제 동안 2문제마다 난이도가 올라갑니다: **SNR +6, +3, 0, −3, −6 dB**. ++ 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다. ++ - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 최대 40점의 보너스를 받습니다. ++ - 결과에서 점수·정답률·최고 연속 정답·단계별 결과를 확인하고 다시 시작할 수 있습니다. ++ - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다. ++ ++ 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과 ++ 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은 ++ [WORDS.md](docs/WORDS.md)에 설명했습니다. ++ 청취 과제는 직접 목표 식별, 연습 암호와 진짜 암호 구별, 앞 단어를 잊고 다음 ++ 단어 기억, 첫째·마지막·가운데 선택, 반복 찾기, 정정 반영, 신호 조건 적용, ++ 오른쪽 표지 선택의 10가지입니다. 문장에 실제 보기의 오답도 포함됩니다. ++ ++ ## 검증과 정적 빌드 ++ ++ ```bash ++ npm run lint ++ npm test ++ npm run build ++ npm run preview ++ ``` ++ ++ `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기 ++ 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개가 원본 해시 그대로 ++ 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다. ++ 런타임 MIT notice가 `dist/`에 전문 그대로 포함되는지도 검증합니다. ++ ++ 실제 오디오·360px/1280px 화면·키보드·접근성·오류 재시도 검증: ++ ++ ```bash ++ # Chromium이 없는 개발 환경에서 한 번만 실행합니다. ++ npx playwright install chromium ++ npm run test:browser ++ # 이미 설치된 Chrome을 사용할 때의 대안입니다. ++ CHROME_PATH=/usr/bin/google-chrome npm run test:browser ++ ``` ++ ++ Chromium sandbox는 기본으로 켭니다. sandbox를 지원하지 않는 격리된 개발 ++ 환경에서만 `PLAYWRIGHT_NO_SANDBOX=1`을 명시해 끌 수 있습니다. `0`, `false`, ++ 빈 값이나 미설정은 sandbox를 유지합니다. `CHROME_PATH` 지정은 sandbox를 끄지 않습니다. ++ ++ 브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요. ++ 실제 음성 재생이 끝나기를 기다려 약 2–3분이 걸립니다. 로컬 4173 포트를 ++ 테스트 중에만 열고 종료합니다. 스크린샷은 `.agent/evidence/`에 남습니다. ++ ++ ## 음성과 문서 ++ ++ 한국어 WAV는 직접 쓴 문장을 로컬 eSpeak NG로 합성했습니다. 환경음은 ++ 브라우저에서 고정 seed로 합성합니다. 외부 음원이나 신경망 음성 모델을 사용하지 않습니다. ++ 생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를 ++ 참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다. ++ ++ - [설계와 상태 전이](docs/ARCHITECTURE.md) ++ - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) ++ - [RED/GREEN 및 실제 검증 결과](.agent/RESULT.md) ++ ++ 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. ++ + + ❯ tests/neural-release.test.ts:13:51 + 11| expect.soft(lock.packages[""].version).toBe("0.1.1"); + 12| for (const path of ["src/App.tsx", "index.html", "README.md", "LICEN… + 13| expect.soft(readFileSync(path, "utf8"), path).toContain("0.1.1"); + | ^ + 14| } + 15| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/21]⎯ + + FAIL tests/neural-release.test.ts > 0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다 +AssertionError: LICENSE: expected 'Find-Word v0.1\nPrivate / proprietary…' to contain '0.1.1' + +- Expected ++ Received + +- 0.1.1 ++ Find-Word v0.1 ++ Private / proprietary. All rights reserved. ++ ++ No license is granted to use, copy, modify, publish, distribute, sublicense, ++ or sell the original project code, documentation, or assets without prior ++ written permission from the respective rights holders. ++ ++ Third-party components remain subject to their own licenses. Their notices ++ are reproduced in THIRD_PARTY_NOTICES.md and accompany the distribution. ++ + + ❯ tests/neural-release.test.ts:13:51 + 11| expect.soft(lock.packages[""].version).toBe("0.1.1"); + 12| for (const path of ["src/App.tsx", "index.html", "README.md", "LICEN… + 13| expect.soft(readFileSync(path, "utf8"), path).toContain("0.1.1"); + | ^ + 14| } + 15| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/21]⎯ + + FAIL tests/neural-release.test.ts > 0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다 +AssertionError: docs/AUDIO_PROVENANCE.md: expected '# 오디오 출처와 재현\n\n## 포함 자산\n\n| 대상 | 생성…' to contain '0.1.1' + +- Expected ++ Received + +- 0.1.1 ++ # 오디오 출처와 재현 ++ ++ ## 포함 자산 ++ ++ | 대상 | 생성 방식 | 원문/설정 | 배포 방식 | ++ | --- | --- | --- | --- | ++ | `public/audio/intro.wav` | 로컬 eSpeak NG 한국어 포먼트 합성 | `src/content.ts`의 INTRO | PCM WAV | ++ | 목표 음성 100개 | 같은 로컬 엔진 | 10 패턴 × 10 단어 | PCM WAV | ++ | 길거리·쇼핑몰·극장·백화점·시장·지하철·카페·비 오는 거리 | 직접 작성한 결정론적 DSP | `ENVIRONMENTS`, 고정 seed | 브라우저 메모리에서 합성 | ++ | 화면 파형·S 캐릭터 | 직접 작성한 CSS | `App.tsx`, `styles.css` | CSS, 외부 이미지 없음 | ++ ++ 모든 한국어 문장은 이번 앱을 위해 직접 작성했다. 다른 사람의 녹음, 노래, ++ 샘플 라이브러리, 음성 복제, 신경망 모델을 포함하지 않는다. 웃음도 INTRO의 ++ ‘으하하하~~’ 문자열을 동일 엔진으로 합성한 것이다. 효과를 위해 타인 음원을 ++ 다운로드하지 않았다. 런타임에 speechSynthesis 또는 네트워크 TTS도 호출하지 않는다. ++ ++ WAV는 16kHz, mono, signed 16-bit little-endian PCM이다. 101개 합계는 ++ **20,497,222바이트**다. 파일별 원문·바이트 수·SHA-256은 ++ `public/audio/manifest.json`에 있다. 생성 시 실행 도구에서 읽은 eSpeak NG 1.51, ++ FFmpeg 6.1.1-3ubuntu5의 실제 버전도 기록했다. `tests/assets.test.ts`는 원문 대응, PCM ++ 포맷, 비무음 신호, 20MiB 이내 크기, gain 적용 후 피크 여유를 검사한다. ++ 빌드 마지막에는 `dist/audio` 파일의 해시도 확인한다. ++ 청취 연산 변경에 맞춰 101개 WAV와 manifest를 모두 재생성했다. 정답과 경쟁 ++ 단어는 `renderSentence`로 채우며, 순서·반복·정정·조건·방향 단서가 실제 음성에 들어간다. ++ ++ ## 사용 도구와 라이선스 ++ ++ - [eSpeak NG 공식 저장소](https://github.com/espeak-ng/espeak-ng): GPL-3.0-or-later. ++ 이번에는 Ubuntu 24.04 패키지 `1.51+dfsg-12build1`, `ko` 포먼트 음성을 사용했다. ++ [공식 언어 목록](https://github.com/espeak-ng/espeak-ng/blob/master/docs/languages.md)에 한국어가 있다. ++ - [FFmpeg 공식 라이선스 안내](https://ffmpeg.org/legal.html): 빌드 구성에 따라 ++ LGPL/GPL. 생성 과정에서 PCM 리샘플링만 사용했으며 실행 파일을 배포하지 않는다. ++ - 합성 엔진의 프로그램 라이선스와 그 출력물은 구분한다. ++ [GPLv3 §2](https://www.gnu.org/licenses/gpl-3.0.html#section2) 및 ++ [GNU 출력물 설명](https://www.gnu.org/licenses/gpl-faq.html#GPLOutput)에 따르면 ++ 출력이 그 프로그램을 바탕으로 한 저작물인 경우에만 프로그램 라이선스가 ++ 출력에 적용된다. 이 작업의 산출물은 직접 작성한 텍스트의 포먼트 합성 파형이며, ++ 프로그램 코드·외부 녹음·모델을 포함하지 않는 자체 생성 오디오로 취급한다. ++ - 엔진·라이브러리·다운로드한 Debian 패키지는 `.cache/`에서만 생성 도구로 ++ 사용하며 Git 대상과 `dist/`에 들어가지 않는다. 별도 외부 음원 라이선스는 없다. ++ 프로젝트 자체의 소스·문서·자체 생성 자산은 private / all-rights-reserved다. ++ [루트 LICENSE](../LICENSE)를 따른다. 별도로 번들에 포함되는 React, ReactDOM, ++ scheduler의 MIT 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 ++ 있으며 `dist/THIRD_PARTY_NOTICES.md`로 함께 배포한다. ++ ++ ## 생성 절차 ++ ++ 일반적인 Ubuntu 개발 환경에는 `espeak-ng`, `ffmpeg`, Python 3, Node.js가 ++ 필요하다. **이미 포함된 WAV로 빌드·실행할 때는 이 도구가 필요 없다.** ++ ++ ```bash ++ npm run audio:generate ++ npm test -- tests/assets.test.ts ++ npm run build ++ ``` ++ ++ 현재 작업에서는 시스템 설치 없이 다음 명령으로 worktree 내부에 도구를 풀었다. ++ 이 명령은 문서화용이며 서버에서 실행한 이력이 없다. ++ ++ ```bash ++ mkdir -p .cache/espeak ++ cd .cache/espeak ++ apt download espeak-ng espeak-ng-data libespeak-ng1 libpcaudio0 libsonic0 ++ for pkg in *.deb; do dpkg-deb -x "$pkg" root; done ++ cd ../.. ++ npm run audio:generate ++ ``` ++ ++ `scripts/generate_audio.py`가 시스템 엔진 또는 `.cache/espeak/root/usr`를 ++ 자동 선택한다. `ko`, 속도 145, 피치 45, 진폭 100을 고정하고 FFmpeg로 16kHz ++ PCM을 만든 뒤 Python wave 모듈로 메타데이터 없는 WAV를 쓴다. 런타임 데이터는 ++ Node의 type stripping으로 읽어 텍스트 중복을 피한다. 같은 엔진·버전·플랫폼에서는 ++ 해시로 재현 여부를 비교할 수 있다. 다른 버전은 새 출처와 해시를 검토해야 한다. ++ ++ ## 음량·품질 한계 ++ ++ 보이스와 배경은 같은 디지털 RMS로 정규화되고 음성 경로만 dB gain을 적용한다. ++ 안내의 gain은 1.8에서 0.12로 감소한다. 기본 전체 음량은 35%이다. ++ 물리적 음압을 측정하거나 장치 출력을 보정하지 않는다. ++ ++ 포먼트 합성 특성상 한국어 발음·억양과 웃음이 기계적으로 들린다. 배경도 실제 ++ 장소 녹음처럼 세밀하지 않다. 객관식의 자모 차이와 단어 친숙도, 발음의 자연스러움은 ++ 검수자가 직접 들어 평가해야 한다. 현재 단계 수치는 임상 검증이나 청력 판정용이 아니다. ++ + + ❯ tests/neural-release.test.ts:13:51 + 11| expect.soft(lock.packages[""].version).toBe("0.1.1"); + 12| for (const path of ["src/App.tsx", "index.html", "README.md", "LICEN… + 13| expect.soft(readFileSync(path, "utf8"), path).toContain("0.1.1"); + | ^ + 14| } + 15| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: expected 'python3 scripts/generate_audio.py' to be '.venv-audio/bin/python scripts/genera…' // Object.is equality + +Expected: ".venv-audio/bin/python scripts/generate_audio.py" +Received: "python3 scripts/generate_audio.py" + + ❯ tests/neural-release.test.ts:19:46 + 17| it("고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다", () => { + 18| const pkg = JSON.parse(readFileSync("package.json", "utf8")); + 19| expect.soft(pkg.scripts["audio:generate"]).toBe(".venv-audio/bin/pyt… + | ^ + 20| for (const path of ["README.md", "docs/AUDIO_PROVENANCE.md"]) { + 21| const doc = readFileSync(path, "utf8"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[10/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: README.md: expected '# 단어를 맞춰라 · Find-Word v0.1\n\n소음 속에 숨…' to contain 'requirements-audio.txt' + +- Expected ++ Received + +- requirements-audio.txt ++ # 단어를 맞춰라 · Find-Word v0.1 ++ ++ 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. ++ ++ **Designed by OZ at Heat-T Medical Inc.** ++ ++ Canonical repository: [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word). ++ 프로젝트 자체는 **private / all-rights-reserved**이며 `package.json`의 ++ `license`는 `UNLICENSED`입니다. [LICENSE](LICENSE)를 참고하세요. ++ 런타임 의존성 React, ReactDOM, scheduler의 MIT 전문은 ++ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)에 보존하며, ++ `public/THIRD_PARTY_NOTICES.md`가 빌드 시 `dist/THIRD_PARTY_NOTICES.md`로 복사됩니다. ++ 빌드 검증기는 설치된 의존성의 라이선스 전문·버전과 세 사본을 대조합니다. ++ ++ React + TypeScript strict + Vite로 만든 정적 SPA입니다. 로그인, 개인정보 저장, ++ 분석 도구, 외부 CDN, 런타임 TTS/API 요청이 없습니다. 청각적 주의·식별 게임이며 ++ 의료적 진단·치료나 효과를 주장하지 않습니다. ++ ++ ## 실행 ++ ++ Node.js 최소 버전은 **22.22.2** (`>=22.22.2`)입니다. 잠긴 jsdom 계열 의존성이 ++ 요구하는 지원 범위는 22.x의 22.22.2 이상, 24.x의 24.15.0 이상 또는 26.0.0 이상입니다. ++ 이 중 지원되는 LTS 버전을 사용하세요. `package-lock.json`을 ++ 포함하므로 새 환경에서는 `npm ci`를 사용합니다. 이번 검증 환경은 Node 26.7.0입니다. ++ ++ ```bash ++ npm ci ++ npm run dev ++ ``` ++ ++ 표시되는 `http://127.0.0.1:5173`에서 사용합니다. 소리는 사용자가 **게임 시작 · 소리 듣기**를 ++ 누른 뒤에만 재생합니다. 낮은 음량부터 조절하고 헤드폰이나 스피커를 사용할 수 있습니다. ++ ++ ## 플레이 ++ ++ - 골고루 풀기, 보기에서 선택, 직접 입력 중 선택합니다. ++ - 8개 환경을 순환하거나 하나를 지정합니다. ++ - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다. ++ - 10문제 동안 2문제마다 난이도가 올라갑니다: **SNR +6, +3, 0, −3, −6 dB**. ++ 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다. ++ - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 최대 40점의 보너스를 받습니다. ++ - 결과에서 점수·정답률·최고 연속 정답·단계별 결과를 확인하고 다시 시작할 수 있습니다. ++ - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다. ++ ++ 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과 ++ 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은 ++ [WORDS.md](docs/WORDS.md)에 설명했습니다. ++ 청취 과제는 직접 목표 식별, 연습 암호와 진짜 암호 구별, 앞 단어를 잊고 다음 ++ 단어 기억, 첫째·마지막·가운데 선택, 반복 찾기, 정정 반영, 신호 조건 적용, ++ 오른쪽 표지 선택의 10가지입니다. 문장에 실제 보기의 오답도 포함됩니다. ++ ++ ## 검증과 정적 빌드 ++ ++ ```bash ++ npm run lint ++ npm test ++ npm run build ++ npm run preview ++ ``` ++ ++ `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기 ++ 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개가 원본 해시 그대로 ++ 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다. ++ 런타임 MIT notice가 `dist/`에 전문 그대로 포함되는지도 검증합니다. ++ ++ 실제 오디오·360px/1280px 화면·키보드·접근성·오류 재시도 검증: ++ ++ ```bash ++ # Chromium이 없는 개발 환경에서 한 번만 실행합니다. ++ npx playwright install chromium ++ npm run test:browser ++ # 이미 설치된 Chrome을 사용할 때의 대안입니다. ++ CHROME_PATH=/usr/bin/google-chrome npm run test:browser ++ ``` ++ ++ Chromium sandbox는 기본으로 켭니다. sandbox를 지원하지 않는 격리된 개발 ++ 환경에서만 `PLAYWRIGHT_NO_SANDBOX=1`을 명시해 끌 수 있습니다. `0`, `false`, ++ 빈 값이나 미설정은 sandbox를 유지합니다. `CHROME_PATH` 지정은 sandbox를 끄지 않습니다. ++ ++ 브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요. ++ 실제 음성 재생이 끝나기를 기다려 약 2–3분이 걸립니다. 로컬 4173 포트를 ++ 테스트 중에만 열고 종료합니다. 스크린샷은 `.agent/evidence/`에 남습니다. ++ ++ ## 음성과 문서 ++ ++ 한국어 WAV는 직접 쓴 문장을 로컬 eSpeak NG로 합성했습니다. 환경음은 ++ 브라우저에서 고정 seed로 합성합니다. 외부 음원이나 신경망 음성 모델을 사용하지 않습니다. ++ 생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를 ++ 참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다. ++ ++ - [설계와 상태 전이](docs/ARCHITECTURE.md) ++ - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) ++ - [RED/GREEN 및 실제 검증 결과](.agent/RESULT.md) ++ ++ 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. ++ + + ❯ tests/neural-release.test.ts:22:28 + 20| for (const path of ["README.md", "docs/AUDIO_PROVENANCE.md"]) { + 21| const doc = readFileSync(path, "utf8"); + 22| expect.soft(doc, path).toContain("requirements-audio.txt"); + | ^ + 23| expect.soft(doc, path).toContain("ko-KR-HyunsuMultilingualNeural"); + 24| expect.soft(doc, path).toContain("7.3.1"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[11/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: README.md: expected '# 단어를 맞춰라 · Find-Word v0.1\n\n소음 속에 숨…' to contain 'ko-KR-HyunsuMultilingualNeural' + +- Expected ++ Received + +- ko-KR-HyunsuMultilingualNeural ++ # 단어를 맞춰라 · Find-Word v0.1 ++ ++ 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. ++ ++ **Designed by OZ at Heat-T Medical Inc.** ++ ++ Canonical repository: [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word). ++ 프로젝트 자체는 **private / all-rights-reserved**이며 `package.json`의 ++ `license`는 `UNLICENSED`입니다. [LICENSE](LICENSE)를 참고하세요. ++ 런타임 의존성 React, ReactDOM, scheduler의 MIT 전문은 ++ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)에 보존하며, ++ `public/THIRD_PARTY_NOTICES.md`가 빌드 시 `dist/THIRD_PARTY_NOTICES.md`로 복사됩니다. ++ 빌드 검증기는 설치된 의존성의 라이선스 전문·버전과 세 사본을 대조합니다. ++ ++ React + TypeScript strict + Vite로 만든 정적 SPA입니다. 로그인, 개인정보 저장, ++ 분석 도구, 외부 CDN, 런타임 TTS/API 요청이 없습니다. 청각적 주의·식별 게임이며 ++ 의료적 진단·치료나 효과를 주장하지 않습니다. ++ ++ ## 실행 ++ ++ Node.js 최소 버전은 **22.22.2** (`>=22.22.2`)입니다. 잠긴 jsdom 계열 의존성이 ++ 요구하는 지원 범위는 22.x의 22.22.2 이상, 24.x의 24.15.0 이상 또는 26.0.0 이상입니다. ++ 이 중 지원되는 LTS 버전을 사용하세요. `package-lock.json`을 ++ 포함하므로 새 환경에서는 `npm ci`를 사용합니다. 이번 검증 환경은 Node 26.7.0입니다. ++ ++ ```bash ++ npm ci ++ npm run dev ++ ``` ++ ++ 표시되는 `http://127.0.0.1:5173`에서 사용합니다. 소리는 사용자가 **게임 시작 · 소리 듣기**를 ++ 누른 뒤에만 재생합니다. 낮은 음량부터 조절하고 헤드폰이나 스피커를 사용할 수 있습니다. ++ ++ ## 플레이 ++ ++ - 골고루 풀기, 보기에서 선택, 직접 입력 중 선택합니다. ++ - 8개 환경을 순환하거나 하나를 지정합니다. ++ - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다. ++ - 10문제 동안 2문제마다 난이도가 올라갑니다: **SNR +6, +3, 0, −3, −6 dB**. ++ 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다. ++ - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 최대 40점의 보너스를 받습니다. ++ - 결과에서 점수·정답률·최고 연속 정답·단계별 결과를 확인하고 다시 시작할 수 있습니다. ++ - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다. ++ ++ 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과 ++ 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은 ++ [WORDS.md](docs/WORDS.md)에 설명했습니다. ++ 청취 과제는 직접 목표 식별, 연습 암호와 진짜 암호 구별, 앞 단어를 잊고 다음 ++ 단어 기억, 첫째·마지막·가운데 선택, 반복 찾기, 정정 반영, 신호 조건 적용, ++ 오른쪽 표지 선택의 10가지입니다. 문장에 실제 보기의 오답도 포함됩니다. ++ ++ ## 검증과 정적 빌드 ++ ++ ```bash ++ npm run lint ++ npm test ++ npm run build ++ npm run preview ++ ``` ++ ++ `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기 ++ 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개가 원본 해시 그대로 ++ 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다. ++ 런타임 MIT notice가 `dist/`에 전문 그대로 포함되는지도 검증합니다. ++ ++ 실제 오디오·360px/1280px 화면·키보드·접근성·오류 재시도 검증: ++ ++ ```bash ++ # Chromium이 없는 개발 환경에서 한 번만 실행합니다. ++ npx playwright install chromium ++ npm run test:browser ++ # 이미 설치된 Chrome을 사용할 때의 대안입니다. ++ CHROME_PATH=/usr/bin/google-chrome npm run test:browser ++ ``` ++ ++ Chromium sandbox는 기본으로 켭니다. sandbox를 지원하지 않는 격리된 개발 ++ 환경에서만 `PLAYWRIGHT_NO_SANDBOX=1`을 명시해 끌 수 있습니다. `0`, `false`, ++ 빈 값이나 미설정은 sandbox를 유지합니다. `CHROME_PATH` 지정은 sandbox를 끄지 않습니다. ++ ++ 브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요. ++ 실제 음성 재생이 끝나기를 기다려 약 2–3분이 걸립니다. 로컬 4173 포트를 ++ 테스트 중에만 열고 종료합니다. 스크린샷은 `.agent/evidence/`에 남습니다. ++ ++ ## 음성과 문서 ++ ++ 한국어 WAV는 직접 쓴 문장을 로컬 eSpeak NG로 합성했습니다. 환경음은 ++ 브라우저에서 고정 seed로 합성합니다. 외부 음원이나 신경망 음성 모델을 사용하지 않습니다. ++ 생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를 ++ 참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다. ++ ++ - [설계와 상태 전이](docs/ARCHITECTURE.md) ++ - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) ++ - [RED/GREEN 및 실제 검증 결과](.agent/RESULT.md) ++ ++ 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. ++ + + ❯ tests/neural-release.test.ts:23:28 + 21| const doc = readFileSync(path, "utf8"); + 22| expect.soft(doc, path).toContain("requirements-audio.txt"); + 23| expect.soft(doc, path).toContain("ko-KR-HyunsuMultilingualNeural"); + | ^ + 24| expect.soft(doc, path).toContain("7.3.1"); + 25| expect.soft(doc, path).not.toMatch(/espeak|포먼트/i); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[12/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: README.md: expected '# 단어를 맞춰라 · Find-Word v0.1\n\n소음 속에 숨…' to contain '7.3.1' + +- Expected ++ Received + +- 7.3.1 ++ # 단어를 맞춰라 · Find-Word v0.1 ++ ++ 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. ++ ++ **Designed by OZ at Heat-T Medical Inc.** ++ ++ Canonical repository: [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word). ++ 프로젝트 자체는 **private / all-rights-reserved**이며 `package.json`의 ++ `license`는 `UNLICENSED`입니다. [LICENSE](LICENSE)를 참고하세요. ++ 런타임 의존성 React, ReactDOM, scheduler의 MIT 전문은 ++ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)에 보존하며, ++ `public/THIRD_PARTY_NOTICES.md`가 빌드 시 `dist/THIRD_PARTY_NOTICES.md`로 복사됩니다. ++ 빌드 검증기는 설치된 의존성의 라이선스 전문·버전과 세 사본을 대조합니다. ++ ++ React + TypeScript strict + Vite로 만든 정적 SPA입니다. 로그인, 개인정보 저장, ++ 분석 도구, 외부 CDN, 런타임 TTS/API 요청이 없습니다. 청각적 주의·식별 게임이며 ++ 의료적 진단·치료나 효과를 주장하지 않습니다. ++ ++ ## 실행 ++ ++ Node.js 최소 버전은 **22.22.2** (`>=22.22.2`)입니다. 잠긴 jsdom 계열 의존성이 ++ 요구하는 지원 범위는 22.x의 22.22.2 이상, 24.x의 24.15.0 이상 또는 26.0.0 이상입니다. ++ 이 중 지원되는 LTS 버전을 사용하세요. `package-lock.json`을 ++ 포함하므로 새 환경에서는 `npm ci`를 사용합니다. 이번 검증 환경은 Node 26.7.0입니다. ++ ++ ```bash ++ npm ci ++ npm run dev ++ ``` ++ ++ 표시되는 `http://127.0.0.1:5173`에서 사용합니다. 소리는 사용자가 **게임 시작 · 소리 듣기**를 ++ 누른 뒤에만 재생합니다. 낮은 음량부터 조절하고 헤드폰이나 스피커를 사용할 수 있습니다. ++ ++ ## 플레이 ++ ++ - 골고루 풀기, 보기에서 선택, 직접 입력 중 선택합니다. ++ - 8개 환경을 순환하거나 하나를 지정합니다. ++ - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다. ++ - 10문제 동안 2문제마다 난이도가 올라갑니다: **SNR +6, +3, 0, −3, −6 dB**. ++ 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다. ++ - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 최대 40점의 보너스를 받습니다. ++ - 결과에서 점수·정답률·최고 연속 정답·단계별 결과를 확인하고 다시 시작할 수 있습니다. ++ - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다. ++ ++ 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과 ++ 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은 ++ [WORDS.md](docs/WORDS.md)에 설명했습니다. ++ 청취 과제는 직접 목표 식별, 연습 암호와 진짜 암호 구별, 앞 단어를 잊고 다음 ++ 단어 기억, 첫째·마지막·가운데 선택, 반복 찾기, 정정 반영, 신호 조건 적용, ++ 오른쪽 표지 선택의 10가지입니다. 문장에 실제 보기의 오답도 포함됩니다. ++ ++ ## 검증과 정적 빌드 ++ ++ ```bash ++ npm run lint ++ npm test ++ npm run build ++ npm run preview ++ ``` ++ ++ `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기 ++ 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개가 원본 해시 그대로 ++ 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다. ++ 런타임 MIT notice가 `dist/`에 전문 그대로 포함되는지도 검증합니다. ++ ++ 실제 오디오·360px/1280px 화면·키보드·접근성·오류 재시도 검증: ++ ++ ```bash ++ # Chromium이 없는 개발 환경에서 한 번만 실행합니다. ++ npx playwright install chromium ++ npm run test:browser ++ # 이미 설치된 Chrome을 사용할 때의 대안입니다. ++ CHROME_PATH=/usr/bin/google-chrome npm run test:browser ++ ``` ++ ++ Chromium sandbox는 기본으로 켭니다. sandbox를 지원하지 않는 격리된 개발 ++ 환경에서만 `PLAYWRIGHT_NO_SANDBOX=1`을 명시해 끌 수 있습니다. `0`, `false`, ++ 빈 값이나 미설정은 sandbox를 유지합니다. `CHROME_PATH` 지정은 sandbox를 끄지 않습니다. ++ ++ 브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요. ++ 실제 음성 재생이 끝나기를 기다려 약 2–3분이 걸립니다. 로컬 4173 포트를 ++ 테스트 중에만 열고 종료합니다. 스크린샷은 `.agent/evidence/`에 남습니다. ++ ++ ## 음성과 문서 ++ ++ 한국어 WAV는 직접 쓴 문장을 로컬 eSpeak NG로 합성했습니다. 환경음은 ++ 브라우저에서 고정 seed로 합성합니다. 외부 음원이나 신경망 음성 모델을 사용하지 않습니다. ++ 생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를 ++ 참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다. ++ ++ - [설계와 상태 전이](docs/ARCHITECTURE.md) ++ - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) ++ - [RED/GREEN 및 실제 검증 결과](.agent/RESULT.md) ++ ++ 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. ++ + + ❯ tests/neural-release.test.ts:24:28 + 22| expect.soft(doc, path).toContain("requirements-audio.txt"); + 23| expect.soft(doc, path).toContain("ko-KR-HyunsuMultilingualNeural"); + 24| expect.soft(doc, path).toContain("7.3.1"); + | ^ + 25| expect.soft(doc, path).not.toMatch(/espeak|포먼트/i); + 26| } + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[13/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: README.md: expected '# 단어를 맞춰라 · Find-Word v0.1\n\n소음 속에 숨…' not to match /espeak|포먼트/i + +- Expected: +/espeak|포먼트/i + ++ Received: +"# 단어를 맞춰라 · Find-Word v0.1 + +소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. + +**Designed by OZ at Heat-T Medical Inc.** + +Canonical repository: [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word). +프로젝트 자체는 **private / all-rights-reserved**이며 `package.json`의 +`license`는 `UNLICENSED`입니다. [LICENSE](LICENSE)를 참고하세요. +런타임 의존성 React, ReactDOM, scheduler의 MIT 전문은 +[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)에 보존하며, +`public/THIRD_PARTY_NOTICES.md`가 빌드 시 `dist/THIRD_PARTY_NOTICES.md`로 복사됩니다. +빌드 검증기는 설치된 의존성의 라이선스 전문·버전과 세 사본을 대조합니다. + +React + TypeScript strict + Vite로 만든 정적 SPA입니다. 로그인, 개인정보 저장, +분석 도구, 외부 CDN, 런타임 TTS/API 요청이 없습니다. 청각적 주의·식별 게임이며 +의료적 진단·치료나 효과를 주장하지 않습니다. + +## 실행 + +Node.js 최소 버전은 **22.22.2** (`>=22.22.2`)입니다. 잠긴 jsdom 계열 의존성이 +요구하는 지원 범위는 22.x의 22.22.2 이상, 24.x의 24.15.0 이상 또는 26.0.0 이상입니다. +이 중 지원되는 LTS 버전을 사용하세요. `package-lock.json`을 +포함하므로 새 환경에서는 `npm ci`를 사용합니다. 이번 검증 환경은 Node 26.7.0입니다. + +```bash +npm ci +npm run dev +``` + +표시되는 `http://127.0.0.1:5173`에서 사용합니다. 소리는 사용자가 **게임 시작 · 소리 듣기**를 +누른 뒤에만 재생합니다. 낮은 음량부터 조절하고 헤드폰이나 스피커를 사용할 수 있습니다. + +## 플레이 + +- 골고루 풀기, 보기에서 선택, 직접 입력 중 선택합니다. +- 8개 환경을 순환하거나 하나를 지정합니다. +- 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다. +- 10문제 동안 2문제마다 난이도가 올라갑니다: **SNR +6, +3, 0, −3, −6 dB**. + 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다. +- 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 최대 40점의 보너스를 받습니다. +- 결과에서 점수·정답률·최고 연속 정답·단계별 결과를 확인하고 다시 시작할 수 있습니다. +- 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다. + +10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과 +단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은 +[WORDS.md](docs/WORDS.md)에 설명했습니다. +청취 과제는 직접 목표 식별, 연습 암호와 진짜 암호 구별, 앞 단어를 잊고 다음 +단어 기억, 첫째·마지막·가운데 선택, 반복 찾기, 정정 반영, 신호 조건 적용, +오른쪽 표지 선택의 10가지입니다. 문장에 실제 보기의 오답도 포함됩니다. + +## 검증과 정적 빌드 + +```bash +npm run lint +npm test +npm run build +npm run preview +``` + +`lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기 +테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개가 원본 해시 그대로 +포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다. +런타임 MIT notice가 `dist/`에 전문 그대로 포함되는지도 검증합니다. + +실제 오디오·360px/1280px 화면·키보드·접근성·오류 재시도 검증: + +```bash +# Chromium이 없는 개발 환경에서 한 번만 실행합니다. +npx playwright install chromium +npm run test:browser +# 이미 설치된 Chrome을 사용할 때의 대안입니다. +CHROME_PATH=/usr/bin/google-chrome npm run test:browser +``` + +Chromium sandbox는 기본으로 켭니다. sandbox를 지원하지 않는 격리된 개발 +환경에서만 `PLAYWRIGHT_NO_SANDBOX=1`을 명시해 끌 수 있습니다. `0`, `false`, +빈 값이나 미설정은 sandbox를 유지합니다. `CHROME_PATH` 지정은 sandbox를 끄지 않습니다. + +브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요. +실제 음성 재생이 끝나기를 기다려 약 2–3분이 걸립니다. 로컬 4173 포트를 +테스트 중에만 열고 종료합니다. 스크린샷은 `.agent/evidence/`에 남습니다. + +## 음성과 문서 + +한국어 WAV는 직접 쓴 문장을 로컬 eSpeak NG로 합성했습니다. 환경음은 +브라우저에서 고정 seed로 합성합니다. 외부 음원이나 신경망 음성 모델을 사용하지 않습니다. +생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를 +참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다. + +- [설계와 상태 전이](docs/ARCHITECTURE.md) +- [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) +- [RED/GREEN 및 실제 검증 결과](.agent/RESULT.md) + +이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. +" + + ❯ tests/neural-release.test.ts:25:32 + 23| expect.soft(doc, path).toContain("ko-KR-HyunsuMultilingualNeural"); + 24| expect.soft(doc, path).toContain("7.3.1"); + 25| expect.soft(doc, path).not.toMatch(/espeak|포먼트/i); + | ^ + 26| } + 27| const provenance = readFileSync("docs/AUDIO_PROVENANCE.md", "utf8"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[14/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: docs/AUDIO_PROVENANCE.md: expected '# 오디오 출처와 재현\n\n## 포함 자산\n\n| 대상 | 생성…' to contain 'requirements-audio.txt' + +- Expected ++ Received + +- requirements-audio.txt ++ # 오디오 출처와 재현 ++ ++ ## 포함 자산 ++ ++ | 대상 | 생성 방식 | 원문/설정 | 배포 방식 | ++ | --- | --- | --- | --- | ++ | `public/audio/intro.wav` | 로컬 eSpeak NG 한국어 포먼트 합성 | `src/content.ts`의 INTRO | PCM WAV | ++ | 목표 음성 100개 | 같은 로컬 엔진 | 10 패턴 × 10 단어 | PCM WAV | ++ | 길거리·쇼핑몰·극장·백화점·시장·지하철·카페·비 오는 거리 | 직접 작성한 결정론적 DSP | `ENVIRONMENTS`, 고정 seed | 브라우저 메모리에서 합성 | ++ | 화면 파형·S 캐릭터 | 직접 작성한 CSS | `App.tsx`, `styles.css` | CSS, 외부 이미지 없음 | ++ ++ 모든 한국어 문장은 이번 앱을 위해 직접 작성했다. 다른 사람의 녹음, 노래, ++ 샘플 라이브러리, 음성 복제, 신경망 모델을 포함하지 않는다. 웃음도 INTRO의 ++ ‘으하하하~~’ 문자열을 동일 엔진으로 합성한 것이다. 효과를 위해 타인 음원을 ++ 다운로드하지 않았다. 런타임에 speechSynthesis 또는 네트워크 TTS도 호출하지 않는다. ++ ++ WAV는 16kHz, mono, signed 16-bit little-endian PCM이다. 101개 합계는 ++ **20,497,222바이트**다. 파일별 원문·바이트 수·SHA-256은 ++ `public/audio/manifest.json`에 있다. 생성 시 실행 도구에서 읽은 eSpeak NG 1.51, ++ FFmpeg 6.1.1-3ubuntu5의 실제 버전도 기록했다. `tests/assets.test.ts`는 원문 대응, PCM ++ 포맷, 비무음 신호, 20MiB 이내 크기, gain 적용 후 피크 여유를 검사한다. ++ 빌드 마지막에는 `dist/audio` 파일의 해시도 확인한다. ++ 청취 연산 변경에 맞춰 101개 WAV와 manifest를 모두 재생성했다. 정답과 경쟁 ++ 단어는 `renderSentence`로 채우며, 순서·반복·정정·조건·방향 단서가 실제 음성에 들어간다. ++ ++ ## 사용 도구와 라이선스 ++ ++ - [eSpeak NG 공식 저장소](https://github.com/espeak-ng/espeak-ng): GPL-3.0-or-later. ++ 이번에는 Ubuntu 24.04 패키지 `1.51+dfsg-12build1`, `ko` 포먼트 음성을 사용했다. ++ [공식 언어 목록](https://github.com/espeak-ng/espeak-ng/blob/master/docs/languages.md)에 한국어가 있다. ++ - [FFmpeg 공식 라이선스 안내](https://ffmpeg.org/legal.html): 빌드 구성에 따라 ++ LGPL/GPL. 생성 과정에서 PCM 리샘플링만 사용했으며 실행 파일을 배포하지 않는다. ++ - 합성 엔진의 프로그램 라이선스와 그 출력물은 구분한다. ++ [GPLv3 §2](https://www.gnu.org/licenses/gpl-3.0.html#section2) 및 ++ [GNU 출력물 설명](https://www.gnu.org/licenses/gpl-faq.html#GPLOutput)에 따르면 ++ 출력이 그 프로그램을 바탕으로 한 저작물인 경우에만 프로그램 라이선스가 ++ 출력에 적용된다. 이 작업의 산출물은 직접 작성한 텍스트의 포먼트 합성 파형이며, ++ 프로그램 코드·외부 녹음·모델을 포함하지 않는 자체 생성 오디오로 취급한다. ++ - 엔진·라이브러리·다운로드한 Debian 패키지는 `.cache/`에서만 생성 도구로 ++ 사용하며 Git 대상과 `dist/`에 들어가지 않는다. 별도 외부 음원 라이선스는 없다. ++ 프로젝트 자체의 소스·문서·자체 생성 자산은 private / all-rights-reserved다. ++ [루트 LICENSE](../LICENSE)를 따른다. 별도로 번들에 포함되는 React, ReactDOM, ++ scheduler의 MIT 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 ++ 있으며 `dist/THIRD_PARTY_NOTICES.md`로 함께 배포한다. ++ ++ ## 생성 절차 ++ ++ 일반적인 Ubuntu 개발 환경에는 `espeak-ng`, `ffmpeg`, Python 3, Node.js가 ++ 필요하다. **이미 포함된 WAV로 빌드·실행할 때는 이 도구가 필요 없다.** ++ ++ ```bash ++ npm run audio:generate ++ npm test -- tests/assets.test.ts ++ npm run build ++ ``` ++ ++ 현재 작업에서는 시스템 설치 없이 다음 명령으로 worktree 내부에 도구를 풀었다. ++ 이 명령은 문서화용이며 서버에서 실행한 이력이 없다. ++ ++ ```bash ++ mkdir -p .cache/espeak ++ cd .cache/espeak ++ apt download espeak-ng espeak-ng-data libespeak-ng1 libpcaudio0 libsonic0 ++ for pkg in *.deb; do dpkg-deb -x "$pkg" root; done ++ cd ../.. ++ npm run audio:generate ++ ``` ++ ++ `scripts/generate_audio.py`가 시스템 엔진 또는 `.cache/espeak/root/usr`를 ++ 자동 선택한다. `ko`, 속도 145, 피치 45, 진폭 100을 고정하고 FFmpeg로 16kHz ++ PCM을 만든 뒤 Python wave 모듈로 메타데이터 없는 WAV를 쓴다. 런타임 데이터는 ++ Node의 type stripping으로 읽어 텍스트 중복을 피한다. 같은 엔진·버전·플랫폼에서는 ++ 해시로 재현 여부를 비교할 수 있다. 다른 버전은 새 출처와 해시를 검토해야 한다. ++ ++ ## 음량·품질 한계 ++ ++ 보이스와 배경은 같은 디지털 RMS로 정규화되고 음성 경로만 dB gain을 적용한다. ++ 안내의 gain은 1.8에서 0.12로 감소한다. 기본 전체 음량은 35%이다. ++ 물리적 음압을 측정하거나 장치 출력을 보정하지 않는다. ++ ++ 포먼트 합성 특성상 한국어 발음·억양과 웃음이 기계적으로 들린다. 배경도 실제 ++ 장소 녹음처럼 세밀하지 않다. 객관식의 자모 차이와 단어 친숙도, 발음의 자연스러움은 ++ 검수자가 직접 들어 평가해야 한다. 현재 단계 수치는 임상 검증이나 청력 판정용이 아니다. ++ + + ❯ tests/neural-release.test.ts:22:28 + 20| for (const path of ["README.md", "docs/AUDIO_PROVENANCE.md"]) { + 21| const doc = readFileSync(path, "utf8"); + 22| expect.soft(doc, path).toContain("requirements-audio.txt"); + | ^ + 23| expect.soft(doc, path).toContain("ko-KR-HyunsuMultilingualNeural"); + 24| expect.soft(doc, path).toContain("7.3.1"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[15/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: docs/AUDIO_PROVENANCE.md: expected '# 오디오 출처와 재현\n\n## 포함 자산\n\n| 대상 | 생성…' to contain 'ko-KR-HyunsuMultilingualNeural' + +- Expected ++ Received + +- ko-KR-HyunsuMultilingualNeural ++ # 오디오 출처와 재현 ++ ++ ## 포함 자산 ++ ++ | 대상 | 생성 방식 | 원문/설정 | 배포 방식 | ++ | --- | --- | --- | --- | ++ | `public/audio/intro.wav` | 로컬 eSpeak NG 한국어 포먼트 합성 | `src/content.ts`의 INTRO | PCM WAV | ++ | 목표 음성 100개 | 같은 로컬 엔진 | 10 패턴 × 10 단어 | PCM WAV | ++ | 길거리·쇼핑몰·극장·백화점·시장·지하철·카페·비 오는 거리 | 직접 작성한 결정론적 DSP | `ENVIRONMENTS`, 고정 seed | 브라우저 메모리에서 합성 | ++ | 화면 파형·S 캐릭터 | 직접 작성한 CSS | `App.tsx`, `styles.css` | CSS, 외부 이미지 없음 | ++ ++ 모든 한국어 문장은 이번 앱을 위해 직접 작성했다. 다른 사람의 녹음, 노래, ++ 샘플 라이브러리, 음성 복제, 신경망 모델을 포함하지 않는다. 웃음도 INTRO의 ++ ‘으하하하~~’ 문자열을 동일 엔진으로 합성한 것이다. 효과를 위해 타인 음원을 ++ 다운로드하지 않았다. 런타임에 speechSynthesis 또는 네트워크 TTS도 호출하지 않는다. ++ ++ WAV는 16kHz, mono, signed 16-bit little-endian PCM이다. 101개 합계는 ++ **20,497,222바이트**다. 파일별 원문·바이트 수·SHA-256은 ++ `public/audio/manifest.json`에 있다. 생성 시 실행 도구에서 읽은 eSpeak NG 1.51, ++ FFmpeg 6.1.1-3ubuntu5의 실제 버전도 기록했다. `tests/assets.test.ts`는 원문 대응, PCM ++ 포맷, 비무음 신호, 20MiB 이내 크기, gain 적용 후 피크 여유를 검사한다. ++ 빌드 마지막에는 `dist/audio` 파일의 해시도 확인한다. ++ 청취 연산 변경에 맞춰 101개 WAV와 manifest를 모두 재생성했다. 정답과 경쟁 ++ 단어는 `renderSentence`로 채우며, 순서·반복·정정·조건·방향 단서가 실제 음성에 들어간다. ++ ++ ## 사용 도구와 라이선스 ++ ++ - [eSpeak NG 공식 저장소](https://github.com/espeak-ng/espeak-ng): GPL-3.0-or-later. ++ 이번에는 Ubuntu 24.04 패키지 `1.51+dfsg-12build1`, `ko` 포먼트 음성을 사용했다. ++ [공식 언어 목록](https://github.com/espeak-ng/espeak-ng/blob/master/docs/languages.md)에 한국어가 있다. ++ - [FFmpeg 공식 라이선스 안내](https://ffmpeg.org/legal.html): 빌드 구성에 따라 ++ LGPL/GPL. 생성 과정에서 PCM 리샘플링만 사용했으며 실행 파일을 배포하지 않는다. ++ - 합성 엔진의 프로그램 라이선스와 그 출력물은 구분한다. ++ [GPLv3 §2](https://www.gnu.org/licenses/gpl-3.0.html#section2) 및 ++ [GNU 출력물 설명](https://www.gnu.org/licenses/gpl-faq.html#GPLOutput)에 따르면 ++ 출력이 그 프로그램을 바탕으로 한 저작물인 경우에만 프로그램 라이선스가 ++ 출력에 적용된다. 이 작업의 산출물은 직접 작성한 텍스트의 포먼트 합성 파형이며, ++ 프로그램 코드·외부 녹음·모델을 포함하지 않는 자체 생성 오디오로 취급한다. ++ - 엔진·라이브러리·다운로드한 Debian 패키지는 `.cache/`에서만 생성 도구로 ++ 사용하며 Git 대상과 `dist/`에 들어가지 않는다. 별도 외부 음원 라이선스는 없다. ++ 프로젝트 자체의 소스·문서·자체 생성 자산은 private / all-rights-reserved다. ++ [루트 LICENSE](../LICENSE)를 따른다. 별도로 번들에 포함되는 React, ReactDOM, ++ scheduler의 MIT 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 ++ 있으며 `dist/THIRD_PARTY_NOTICES.md`로 함께 배포한다. ++ ++ ## 생성 절차 ++ ++ 일반적인 Ubuntu 개발 환경에는 `espeak-ng`, `ffmpeg`, Python 3, Node.js가 ++ 필요하다. **이미 포함된 WAV로 빌드·실행할 때는 이 도구가 필요 없다.** ++ ++ ```bash ++ npm run audio:generate ++ npm test -- tests/assets.test.ts ++ npm run build ++ ``` ++ ++ 현재 작업에서는 시스템 설치 없이 다음 명령으로 worktree 내부에 도구를 풀었다. ++ 이 명령은 문서화용이며 서버에서 실행한 이력이 없다. ++ ++ ```bash ++ mkdir -p .cache/espeak ++ cd .cache/espeak ++ apt download espeak-ng espeak-ng-data libespeak-ng1 libpcaudio0 libsonic0 ++ for pkg in *.deb; do dpkg-deb -x "$pkg" root; done ++ cd ../.. ++ npm run audio:generate ++ ``` ++ ++ `scripts/generate_audio.py`가 시스템 엔진 또는 `.cache/espeak/root/usr`를 ++ 자동 선택한다. `ko`, 속도 145, 피치 45, 진폭 100을 고정하고 FFmpeg로 16kHz ++ PCM을 만든 뒤 Python wave 모듈로 메타데이터 없는 WAV를 쓴다. 런타임 데이터는 ++ Node의 type stripping으로 읽어 텍스트 중복을 피한다. 같은 엔진·버전·플랫폼에서는 ++ 해시로 재현 여부를 비교할 수 있다. 다른 버전은 새 출처와 해시를 검토해야 한다. ++ ++ ## 음량·품질 한계 ++ ++ 보이스와 배경은 같은 디지털 RMS로 정규화되고 음성 경로만 dB gain을 적용한다. ++ 안내의 gain은 1.8에서 0.12로 감소한다. 기본 전체 음량은 35%이다. ++ 물리적 음압을 측정하거나 장치 출력을 보정하지 않는다. ++ ++ 포먼트 합성 특성상 한국어 발음·억양과 웃음이 기계적으로 들린다. 배경도 실제 ++ 장소 녹음처럼 세밀하지 않다. 객관식의 자모 차이와 단어 친숙도, 발음의 자연스러움은 ++ 검수자가 직접 들어 평가해야 한다. 현재 단계 수치는 임상 검증이나 청력 판정용이 아니다. ++ + + ❯ tests/neural-release.test.ts:23:28 + 21| const doc = readFileSync(path, "utf8"); + 22| expect.soft(doc, path).toContain("requirements-audio.txt"); + 23| expect.soft(doc, path).toContain("ko-KR-HyunsuMultilingualNeural"); + | ^ + 24| expect.soft(doc, path).toContain("7.3.1"); + 25| expect.soft(doc, path).not.toMatch(/espeak|포먼트/i); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[16/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: docs/AUDIO_PROVENANCE.md: expected '# 오디오 출처와 재현\n\n## 포함 자산\n\n| 대상 | 생성…' to contain '7.3.1' + +- Expected ++ Received + +- 7.3.1 ++ # 오디오 출처와 재현 ++ ++ ## 포함 자산 ++ ++ | 대상 | 생성 방식 | 원문/설정 | 배포 방식 | ++ | --- | --- | --- | --- | ++ | `public/audio/intro.wav` | 로컬 eSpeak NG 한국어 포먼트 합성 | `src/content.ts`의 INTRO | PCM WAV | ++ | 목표 음성 100개 | 같은 로컬 엔진 | 10 패턴 × 10 단어 | PCM WAV | ++ | 길거리·쇼핑몰·극장·백화점·시장·지하철·카페·비 오는 거리 | 직접 작성한 결정론적 DSP | `ENVIRONMENTS`, 고정 seed | 브라우저 메모리에서 합성 | ++ | 화면 파형·S 캐릭터 | 직접 작성한 CSS | `App.tsx`, `styles.css` | CSS, 외부 이미지 없음 | ++ ++ 모든 한국어 문장은 이번 앱을 위해 직접 작성했다. 다른 사람의 녹음, 노래, ++ 샘플 라이브러리, 음성 복제, 신경망 모델을 포함하지 않는다. 웃음도 INTRO의 ++ ‘으하하하~~’ 문자열을 동일 엔진으로 합성한 것이다. 효과를 위해 타인 음원을 ++ 다운로드하지 않았다. 런타임에 speechSynthesis 또는 네트워크 TTS도 호출하지 않는다. ++ ++ WAV는 16kHz, mono, signed 16-bit little-endian PCM이다. 101개 합계는 ++ **20,497,222바이트**다. 파일별 원문·바이트 수·SHA-256은 ++ `public/audio/manifest.json`에 있다. 생성 시 실행 도구에서 읽은 eSpeak NG 1.51, ++ FFmpeg 6.1.1-3ubuntu5의 실제 버전도 기록했다. `tests/assets.test.ts`는 원문 대응, PCM ++ 포맷, 비무음 신호, 20MiB 이내 크기, gain 적용 후 피크 여유를 검사한다. ++ 빌드 마지막에는 `dist/audio` 파일의 해시도 확인한다. ++ 청취 연산 변경에 맞춰 101개 WAV와 manifest를 모두 재생성했다. 정답과 경쟁 ++ 단어는 `renderSentence`로 채우며, 순서·반복·정정·조건·방향 단서가 실제 음성에 들어간다. ++ ++ ## 사용 도구와 라이선스 ++ ++ - [eSpeak NG 공식 저장소](https://github.com/espeak-ng/espeak-ng): GPL-3.0-or-later. ++ 이번에는 Ubuntu 24.04 패키지 `1.51+dfsg-12build1`, `ko` 포먼트 음성을 사용했다. ++ [공식 언어 목록](https://github.com/espeak-ng/espeak-ng/blob/master/docs/languages.md)에 한국어가 있다. ++ - [FFmpeg 공식 라이선스 안내](https://ffmpeg.org/legal.html): 빌드 구성에 따라 ++ LGPL/GPL. 생성 과정에서 PCM 리샘플링만 사용했으며 실행 파일을 배포하지 않는다. ++ - 합성 엔진의 프로그램 라이선스와 그 출력물은 구분한다. ++ [GPLv3 §2](https://www.gnu.org/licenses/gpl-3.0.html#section2) 및 ++ [GNU 출력물 설명](https://www.gnu.org/licenses/gpl-faq.html#GPLOutput)에 따르면 ++ 출력이 그 프로그램을 바탕으로 한 저작물인 경우에만 프로그램 라이선스가 ++ 출력에 적용된다. 이 작업의 산출물은 직접 작성한 텍스트의 포먼트 합성 파형이며, ++ 프로그램 코드·외부 녹음·모델을 포함하지 않는 자체 생성 오디오로 취급한다. ++ - 엔진·라이브러리·다운로드한 Debian 패키지는 `.cache/`에서만 생성 도구로 ++ 사용하며 Git 대상과 `dist/`에 들어가지 않는다. 별도 외부 음원 라이선스는 없다. ++ 프로젝트 자체의 소스·문서·자체 생성 자산은 private / all-rights-reserved다. ++ [루트 LICENSE](../LICENSE)를 따른다. 별도로 번들에 포함되는 React, ReactDOM, ++ scheduler의 MIT 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 ++ 있으며 `dist/THIRD_PARTY_NOTICES.md`로 함께 배포한다. ++ ++ ## 생성 절차 ++ ++ 일반적인 Ubuntu 개발 환경에는 `espeak-ng`, `ffmpeg`, Python 3, Node.js가 ++ 필요하다. **이미 포함된 WAV로 빌드·실행할 때는 이 도구가 필요 없다.** ++ ++ ```bash ++ npm run audio:generate ++ npm test -- tests/assets.test.ts ++ npm run build ++ ``` ++ ++ 현재 작업에서는 시스템 설치 없이 다음 명령으로 worktree 내부에 도구를 풀었다. ++ 이 명령은 문서화용이며 서버에서 실행한 이력이 없다. ++ ++ ```bash ++ mkdir -p .cache/espeak ++ cd .cache/espeak ++ apt download espeak-ng espeak-ng-data libespeak-ng1 libpcaudio0 libsonic0 ++ for pkg in *.deb; do dpkg-deb -x "$pkg" root; done ++ cd ../.. ++ npm run audio:generate ++ ``` ++ ++ `scripts/generate_audio.py`가 시스템 엔진 또는 `.cache/espeak/root/usr`를 ++ 자동 선택한다. `ko`, 속도 145, 피치 45, 진폭 100을 고정하고 FFmpeg로 16kHz ++ PCM을 만든 뒤 Python wave 모듈로 메타데이터 없는 WAV를 쓴다. 런타임 데이터는 ++ Node의 type stripping으로 읽어 텍스트 중복을 피한다. 같은 엔진·버전·플랫폼에서는 ++ 해시로 재현 여부를 비교할 수 있다. 다른 버전은 새 출처와 해시를 검토해야 한다. ++ ++ ## 음량·품질 한계 ++ ++ 보이스와 배경은 같은 디지털 RMS로 정규화되고 음성 경로만 dB gain을 적용한다. ++ 안내의 gain은 1.8에서 0.12로 감소한다. 기본 전체 음량은 35%이다. ++ 물리적 음압을 측정하거나 장치 출력을 보정하지 않는다. ++ ++ 포먼트 합성 특성상 한국어 발음·억양과 웃음이 기계적으로 들린다. 배경도 실제 ++ 장소 녹음처럼 세밀하지 않다. 객관식의 자모 차이와 단어 친숙도, 발음의 자연스러움은 ++ 검수자가 직접 들어 평가해야 한다. 현재 단계 수치는 임상 검증이나 청력 판정용이 아니다. ++ + + ❯ tests/neural-release.test.ts:24:28 + 22| expect.soft(doc, path).toContain("requirements-audio.txt"); + 23| expect.soft(doc, path).toContain("ko-KR-HyunsuMultilingualNeural"); + 24| expect.soft(doc, path).toContain("7.3.1"); + | ^ + 25| expect.soft(doc, path).not.toMatch(/espeak|포먼트/i); + 26| } + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[17/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: docs/AUDIO_PROVENANCE.md: expected '# 오디오 출처와 재현\n\n## 포함 자산\n\n| 대상 | 생성…' not to match /espeak|포먼트/i + +- Expected: +/espeak|포먼트/i + ++ Received: +"# 오디오 출처와 재현 + +## 포함 자산 + +| 대상 | 생성 방식 | 원문/설정 | 배포 방식 | +| --- | --- | --- | --- | +| `public/audio/intro.wav` | 로컬 eSpeak NG 한국어 포먼트 합성 | `src/content.ts`의 INTRO | PCM WAV | +| 목표 음성 100개 | 같은 로컬 엔진 | 10 패턴 × 10 단어 | PCM WAV | +| 길거리·쇼핑몰·극장·백화점·시장·지하철·카페·비 오는 거리 | 직접 작성한 결정론적 DSP | `ENVIRONMENTS`, 고정 seed | 브라우저 메모리에서 합성 | +| 화면 파형·S 캐릭터 | 직접 작성한 CSS | `App.tsx`, `styles.css` | CSS, 외부 이미지 없음 | + +모든 한국어 문장은 이번 앱을 위해 직접 작성했다. 다른 사람의 녹음, 노래, +샘플 라이브러리, 음성 복제, 신경망 모델을 포함하지 않는다. 웃음도 INTRO의 +‘으하하하~~’ 문자열을 동일 엔진으로 합성한 것이다. 효과를 위해 타인 음원을 +다운로드하지 않았다. 런타임에 speechSynthesis 또는 네트워크 TTS도 호출하지 않는다. + +WAV는 16kHz, mono, signed 16-bit little-endian PCM이다. 101개 합계는 +**20,497,222바이트**다. 파일별 원문·바이트 수·SHA-256은 +`public/audio/manifest.json`에 있다. 생성 시 실행 도구에서 읽은 eSpeak NG 1.51, +FFmpeg 6.1.1-3ubuntu5의 실제 버전도 기록했다. `tests/assets.test.ts`는 원문 대응, PCM +포맷, 비무음 신호, 20MiB 이내 크기, gain 적용 후 피크 여유를 검사한다. +빌드 마지막에는 `dist/audio` 파일의 해시도 확인한다. +청취 연산 변경에 맞춰 101개 WAV와 manifest를 모두 재생성했다. 정답과 경쟁 +단어는 `renderSentence`로 채우며, 순서·반복·정정·조건·방향 단서가 실제 음성에 들어간다. + +## 사용 도구와 라이선스 + +- [eSpeak NG 공식 저장소](https://github.com/espeak-ng/espeak-ng): GPL-3.0-or-later. + 이번에는 Ubuntu 24.04 패키지 `1.51+dfsg-12build1`, `ko` 포먼트 음성을 사용했다. + [공식 언어 목록](https://github.com/espeak-ng/espeak-ng/blob/master/docs/languages.md)에 한국어가 있다. +- [FFmpeg 공식 라이선스 안내](https://ffmpeg.org/legal.html): 빌드 구성에 따라 + LGPL/GPL. 생성 과정에서 PCM 리샘플링만 사용했으며 실행 파일을 배포하지 않는다. +- 합성 엔진의 프로그램 라이선스와 그 출력물은 구분한다. + [GPLv3 §2](https://www.gnu.org/licenses/gpl-3.0.html#section2) 및 + [GNU 출력물 설명](https://www.gnu.org/licenses/gpl-faq.html#GPLOutput)에 따르면 + 출력이 그 프로그램을 바탕으로 한 저작물인 경우에만 프로그램 라이선스가 + 출력에 적용된다. 이 작업의 산출물은 직접 작성한 텍스트의 포먼트 합성 파형이며, + 프로그램 코드·외부 녹음·모델을 포함하지 않는 자체 생성 오디오로 취급한다. +- 엔진·라이브러리·다운로드한 Debian 패키지는 `.cache/`에서만 생성 도구로 + 사용하며 Git 대상과 `dist/`에 들어가지 않는다. 별도 외부 음원 라이선스는 없다. + 프로젝트 자체의 소스·문서·자체 생성 자산은 private / all-rights-reserved다. + [루트 LICENSE](../LICENSE)를 따른다. 별도로 번들에 포함되는 React, ReactDOM, + scheduler의 MIT 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 + 있으며 `dist/THIRD_PARTY_NOTICES.md`로 함께 배포한다. + +## 생성 절차 + +일반적인 Ubuntu 개발 환경에는 `espeak-ng`, `ffmpeg`, Python 3, Node.js가 +필요하다. **이미 포함된 WAV로 빌드·실행할 때는 이 도구가 필요 없다.** + +```bash +npm run audio:generate +npm test -- tests/assets.test.ts +npm run build +``` + +현재 작업에서는 시스템 설치 없이 다음 명령으로 worktree 내부에 도구를 풀었다. +이 명령은 문서화용이며 서버에서 실행한 이력이 없다. + +```bash +mkdir -p .cache/espeak +cd .cache/espeak +apt download espeak-ng espeak-ng-data libespeak-ng1 libpcaudio0 libsonic0 +for pkg in *.deb; do dpkg-deb -x \"$pkg\" root; done +cd ../.. +npm run audio:generate +``` + +`scripts/generate_audio.py`가 시스템 엔진 또는 `.cache/espeak/root/usr`를 +자동 선택한다. `ko`, 속도 145, 피치 45, 진폭 100을 고정하고 FFmpeg로 16kHz +PCM을 만든 뒤 Python wave 모듈로 메타데이터 없는 WAV를 쓴다. 런타임 데이터는 +Node의 type stripping으로 읽어 텍스트 중복을 피한다. 같은 엔진·버전·플랫폼에서는 +해시로 재현 여부를 비교할 수 있다. 다른 버전은 새 출처와 해시를 검토해야 한다. + +## 음량·품질 한계 + +보이스와 배경은 같은 디지털 RMS로 정규화되고 음성 경로만 dB gain을 적용한다. +안내의 gain은 1.8에서 0.12로 감소한다. 기본 전체 음량은 35%이다. +물리적 음압을 측정하거나 장치 출력을 보정하지 않는다. + +포먼트 합성 특성상 한국어 발음·억양과 웃음이 기계적으로 들린다. 배경도 실제 +장소 녹음처럼 세밀하지 않다. 객관식의 자모 차이와 단어 친숙도, 발음의 자연스러움은 +검수자가 직접 들어 평가해야 한다. 현재 단계 수치는 임상 검증이나 청력 판정용이 아니다. +" + + ❯ tests/neural-release.test.ts:25:32 + 23| expect.soft(doc, path).toContain("ko-KR-HyunsuMultilingualNeural"); + 24| expect.soft(doc, path).toContain("7.3.1"); + 25| expect.soft(doc, path).not.toMatch(/espeak|포먼트/i); + | ^ + 26| } + 27| const provenance = readFileSync("docs/AUDIO_PROVENANCE.md", "utf8"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[18/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: expected '# 오디오 출처와 재현\n\n## 포함 자산\n\n| 대상 | 생성…' to contain 'https://www.microsoft.com/en-us/servi…' + +- Expected ++ Received + +- https://www.microsoft.com/en-us/servicesagreement ++ # 오디오 출처와 재현 ++ ++ ## 포함 자산 ++ ++ | 대상 | 생성 방식 | 원문/설정 | 배포 방식 | ++ | --- | --- | --- | --- | ++ | `public/audio/intro.wav` | 로컬 eSpeak NG 한국어 포먼트 합성 | `src/content.ts`의 INTRO | PCM WAV | ++ | 목표 음성 100개 | 같은 로컬 엔진 | 10 패턴 × 10 단어 | PCM WAV | ++ | 길거리·쇼핑몰·극장·백화점·시장·지하철·카페·비 오는 거리 | 직접 작성한 결정론적 DSP | `ENVIRONMENTS`, 고정 seed | 브라우저 메모리에서 합성 | ++ | 화면 파형·S 캐릭터 | 직접 작성한 CSS | `App.tsx`, `styles.css` | CSS, 외부 이미지 없음 | ++ ++ 모든 한국어 문장은 이번 앱을 위해 직접 작성했다. 다른 사람의 녹음, 노래, ++ 샘플 라이브러리, 음성 복제, 신경망 모델을 포함하지 않는다. 웃음도 INTRO의 ++ ‘으하하하~~’ 문자열을 동일 엔진으로 합성한 것이다. 효과를 위해 타인 음원을 ++ 다운로드하지 않았다. 런타임에 speechSynthesis 또는 네트워크 TTS도 호출하지 않는다. ++ ++ WAV는 16kHz, mono, signed 16-bit little-endian PCM이다. 101개 합계는 ++ **20,497,222바이트**다. 파일별 원문·바이트 수·SHA-256은 ++ `public/audio/manifest.json`에 있다. 생성 시 실행 도구에서 읽은 eSpeak NG 1.51, ++ FFmpeg 6.1.1-3ubuntu5의 실제 버전도 기록했다. `tests/assets.test.ts`는 원문 대응, PCM ++ 포맷, 비무음 신호, 20MiB 이내 크기, gain 적용 후 피크 여유를 검사한다. ++ 빌드 마지막에는 `dist/audio` 파일의 해시도 확인한다. ++ 청취 연산 변경에 맞춰 101개 WAV와 manifest를 모두 재생성했다. 정답과 경쟁 ++ 단어는 `renderSentence`로 채우며, 순서·반복·정정·조건·방향 단서가 실제 음성에 들어간다. ++ ++ ## 사용 도구와 라이선스 ++ ++ - [eSpeak NG 공식 저장소](https://github.com/espeak-ng/espeak-ng): GPL-3.0-or-later. ++ 이번에는 Ubuntu 24.04 패키지 `1.51+dfsg-12build1`, `ko` 포먼트 음성을 사용했다. ++ [공식 언어 목록](https://github.com/espeak-ng/espeak-ng/blob/master/docs/languages.md)에 한국어가 있다. ++ - [FFmpeg 공식 라이선스 안내](https://ffmpeg.org/legal.html): 빌드 구성에 따라 ++ LGPL/GPL. 생성 과정에서 PCM 리샘플링만 사용했으며 실행 파일을 배포하지 않는다. ++ - 합성 엔진의 프로그램 라이선스와 그 출력물은 구분한다. ++ [GPLv3 §2](https://www.gnu.org/licenses/gpl-3.0.html#section2) 및 ++ [GNU 출력물 설명](https://www.gnu.org/licenses/gpl-faq.html#GPLOutput)에 따르면 ++ 출력이 그 프로그램을 바탕으로 한 저작물인 경우에만 프로그램 라이선스가 ++ 출력에 적용된다. 이 작업의 산출물은 직접 작성한 텍스트의 포먼트 합성 파형이며, ++ 프로그램 코드·외부 녹음·모델을 포함하지 않는 자체 생성 오디오로 취급한다. ++ - 엔진·라이브러리·다운로드한 Debian 패키지는 `.cache/`에서만 생성 도구로 ++ 사용하며 Git 대상과 `dist/`에 들어가지 않는다. 별도 외부 음원 라이선스는 없다. ++ 프로젝트 자체의 소스·문서·자체 생성 자산은 private / all-rights-reserved다. ++ [루트 LICENSE](../LICENSE)를 따른다. 별도로 번들에 포함되는 React, ReactDOM, ++ scheduler의 MIT 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 ++ 있으며 `dist/THIRD_PARTY_NOTICES.md`로 함께 배포한다. ++ ++ ## 생성 절차 ++ ++ 일반적인 Ubuntu 개발 환경에는 `espeak-ng`, `ffmpeg`, Python 3, Node.js가 ++ 필요하다. **이미 포함된 WAV로 빌드·실행할 때는 이 도구가 필요 없다.** ++ ++ ```bash ++ npm run audio:generate ++ npm test -- tests/assets.test.ts ++ npm run build ++ ``` ++ ++ 현재 작업에서는 시스템 설치 없이 다음 명령으로 worktree 내부에 도구를 풀었다. ++ 이 명령은 문서화용이며 서버에서 실행한 이력이 없다. ++ ++ ```bash ++ mkdir -p .cache/espeak ++ cd .cache/espeak ++ apt download espeak-ng espeak-ng-data libespeak-ng1 libpcaudio0 libsonic0 ++ for pkg in *.deb; do dpkg-deb -x "$pkg" root; done ++ cd ../.. ++ npm run audio:generate ++ ``` ++ ++ `scripts/generate_audio.py`가 시스템 엔진 또는 `.cache/espeak/root/usr`를 ++ 자동 선택한다. `ko`, 속도 145, 피치 45, 진폭 100을 고정하고 FFmpeg로 16kHz ++ PCM을 만든 뒤 Python wave 모듈로 메타데이터 없는 WAV를 쓴다. 런타임 데이터는 ++ Node의 type stripping으로 읽어 텍스트 중복을 피한다. 같은 엔진·버전·플랫폼에서는 ++ 해시로 재현 여부를 비교할 수 있다. 다른 버전은 새 출처와 해시를 검토해야 한다. ++ ++ ## 음량·품질 한계 ++ ++ 보이스와 배경은 같은 디지털 RMS로 정규화되고 음성 경로만 dB gain을 적용한다. ++ 안내의 gain은 1.8에서 0.12로 감소한다. 기본 전체 음량은 35%이다. ++ 물리적 음압을 측정하거나 장치 출력을 보정하지 않는다. ++ ++ 포먼트 합성 특성상 한국어 발음·억양과 웃음이 기계적으로 들린다. 배경도 실제 ++ 장소 녹음처럼 세밀하지 않다. 객관식의 자모 차이와 단어 친숙도, 발음의 자연스러움은 ++ 검수자가 직접 들어 평가해야 한다. 현재 단계 수치는 임상 검증이나 청력 판정용이 아니다. ++ + + ❯ tests/neural-release.test.ts:28:27 + 26| } + 27| const provenance = readFileSync("docs/AUDIO_PROVENANCE.md", "utf8"); + 28| expect.soft(provenance).toContain("https://www.microsoft.com/en-us/s… + | ^ + 29| expect.soft(provenance).toContain("LGPL"); + 30| expect.soft(provenance).toContain("독점 권리를 주장하지 않는다"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[19/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: expected '# 오디오 출처와 재현\n\n## 포함 자산\n\n| 대상 | 생성…' to contain '독점 권리를 주장하지 않는다' + +- Expected ++ Received + +- 독점 권리를 주장하지 않는다 ++ # 오디오 출처와 재현 ++ ++ ## 포함 자산 ++ ++ | 대상 | 생성 방식 | 원문/설정 | 배포 방식 | ++ | --- | --- | --- | --- | ++ | `public/audio/intro.wav` | 로컬 eSpeak NG 한국어 포먼트 합성 | `src/content.ts`의 INTRO | PCM WAV | ++ | 목표 음성 100개 | 같은 로컬 엔진 | 10 패턴 × 10 단어 | PCM WAV | ++ | 길거리·쇼핑몰·극장·백화점·시장·지하철·카페·비 오는 거리 | 직접 작성한 결정론적 DSP | `ENVIRONMENTS`, 고정 seed | 브라우저 메모리에서 합성 | ++ | 화면 파형·S 캐릭터 | 직접 작성한 CSS | `App.tsx`, `styles.css` | CSS, 외부 이미지 없음 | ++ ++ 모든 한국어 문장은 이번 앱을 위해 직접 작성했다. 다른 사람의 녹음, 노래, ++ 샘플 라이브러리, 음성 복제, 신경망 모델을 포함하지 않는다. 웃음도 INTRO의 ++ ‘으하하하~~’ 문자열을 동일 엔진으로 합성한 것이다. 효과를 위해 타인 음원을 ++ 다운로드하지 않았다. 런타임에 speechSynthesis 또는 네트워크 TTS도 호출하지 않는다. ++ ++ WAV는 16kHz, mono, signed 16-bit little-endian PCM이다. 101개 합계는 ++ **20,497,222바이트**다. 파일별 원문·바이트 수·SHA-256은 ++ `public/audio/manifest.json`에 있다. 생성 시 실행 도구에서 읽은 eSpeak NG 1.51, ++ FFmpeg 6.1.1-3ubuntu5의 실제 버전도 기록했다. `tests/assets.test.ts`는 원문 대응, PCM ++ 포맷, 비무음 신호, 20MiB 이내 크기, gain 적용 후 피크 여유를 검사한다. ++ 빌드 마지막에는 `dist/audio` 파일의 해시도 확인한다. ++ 청취 연산 변경에 맞춰 101개 WAV와 manifest를 모두 재생성했다. 정답과 경쟁 ++ 단어는 `renderSentence`로 채우며, 순서·반복·정정·조건·방향 단서가 실제 음성에 들어간다. ++ ++ ## 사용 도구와 라이선스 ++ ++ - [eSpeak NG 공식 저장소](https://github.com/espeak-ng/espeak-ng): GPL-3.0-or-later. ++ 이번에는 Ubuntu 24.04 패키지 `1.51+dfsg-12build1`, `ko` 포먼트 음성을 사용했다. ++ [공식 언어 목록](https://github.com/espeak-ng/espeak-ng/blob/master/docs/languages.md)에 한국어가 있다. ++ - [FFmpeg 공식 라이선스 안내](https://ffmpeg.org/legal.html): 빌드 구성에 따라 ++ LGPL/GPL. 생성 과정에서 PCM 리샘플링만 사용했으며 실행 파일을 배포하지 않는다. ++ - 합성 엔진의 프로그램 라이선스와 그 출력물은 구분한다. ++ [GPLv3 §2](https://www.gnu.org/licenses/gpl-3.0.html#section2) 및 ++ [GNU 출력물 설명](https://www.gnu.org/licenses/gpl-faq.html#GPLOutput)에 따르면 ++ 출력이 그 프로그램을 바탕으로 한 저작물인 경우에만 프로그램 라이선스가 ++ 출력에 적용된다. 이 작업의 산출물은 직접 작성한 텍스트의 포먼트 합성 파형이며, ++ 프로그램 코드·외부 녹음·모델을 포함하지 않는 자체 생성 오디오로 취급한다. ++ - 엔진·라이브러리·다운로드한 Debian 패키지는 `.cache/`에서만 생성 도구로 ++ 사용하며 Git 대상과 `dist/`에 들어가지 않는다. 별도 외부 음원 라이선스는 없다. ++ 프로젝트 자체의 소스·문서·자체 생성 자산은 private / all-rights-reserved다. ++ [루트 LICENSE](../LICENSE)를 따른다. 별도로 번들에 포함되는 React, ReactDOM, ++ scheduler의 MIT 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 ++ 있으며 `dist/THIRD_PARTY_NOTICES.md`로 함께 배포한다. ++ ++ ## 생성 절차 ++ ++ 일반적인 Ubuntu 개발 환경에는 `espeak-ng`, `ffmpeg`, Python 3, Node.js가 ++ 필요하다. **이미 포함된 WAV로 빌드·실행할 때는 이 도구가 필요 없다.** ++ ++ ```bash ++ npm run audio:generate ++ npm test -- tests/assets.test.ts ++ npm run build ++ ``` ++ ++ 현재 작업에서는 시스템 설치 없이 다음 명령으로 worktree 내부에 도구를 풀었다. ++ 이 명령은 문서화용이며 서버에서 실행한 이력이 없다. ++ ++ ```bash ++ mkdir -p .cache/espeak ++ cd .cache/espeak ++ apt download espeak-ng espeak-ng-data libespeak-ng1 libpcaudio0 libsonic0 ++ for pkg in *.deb; do dpkg-deb -x "$pkg" root; done ++ cd ../.. ++ npm run audio:generate ++ ``` ++ ++ `scripts/generate_audio.py`가 시스템 엔진 또는 `.cache/espeak/root/usr`를 ++ 자동 선택한다. `ko`, 속도 145, 피치 45, 진폭 100을 고정하고 FFmpeg로 16kHz ++ PCM을 만든 뒤 Python wave 모듈로 메타데이터 없는 WAV를 쓴다. 런타임 데이터는 ++ Node의 type stripping으로 읽어 텍스트 중복을 피한다. 같은 엔진·버전·플랫폼에서는 ++ 해시로 재현 여부를 비교할 수 있다. 다른 버전은 새 출처와 해시를 검토해야 한다. ++ ++ ## 음량·품질 한계 ++ ++ 보이스와 배경은 같은 디지털 RMS로 정규화되고 음성 경로만 dB gain을 적용한다. ++ 안내의 gain은 1.8에서 0.12로 감소한다. 기본 전체 음량은 35%이다. ++ 물리적 음압을 측정하거나 장치 출력을 보정하지 않는다. ++ ++ 포먼트 합성 특성상 한국어 발음·억양과 웃음이 기계적으로 들린다. 배경도 실제 ++ 장소 녹음처럼 세밀하지 않다. 객관식의 자모 차이와 단어 친숙도, 발음의 자연스러움은 ++ 검수자가 직접 들어 평가해야 한다. 현재 단계 수치는 임상 검증이나 청력 판정용이 아니다. ++ + + ❯ tests/neural-release.test.ts:30:27 + 28| expect.soft(provenance).toContain("https://www.microsoft.com/en-us/s… + 29| expect.soft(provenance).toContain("LGPL"); + 30| expect.soft(provenance).toContain("독점 권리를 주장하지 않는다"); + | ^ + 31| expect.soft(readFileSync("LICENSE", "utf8")).toContain("does not cla… + 32| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[20/21]⎯ + + FAIL tests/neural-release.test.ts > 고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다 +AssertionError: expected 'Find-Word v0.1\nPrivate / proprietary…' to contain 'does not claim exclusive rights to ge…' + +- Expected ++ Received + +- does not claim exclusive rights to generated speech ++ Find-Word v0.1 ++ Private / proprietary. All rights reserved. ++ ++ No license is granted to use, copy, modify, publish, distribute, sublicense, ++ or sell the original project code, documentation, or assets without prior ++ written permission from the respective rights holders. ++ ++ Third-party components remain subject to their own licenses. Their notices ++ are reproduced in THIRD_PARTY_NOTICES.md and accompany the distribution. ++ + + ❯ tests/neural-release.test.ts:31:48 + 29| expect.soft(provenance).toContain("LGPL"); + 30| expect.soft(provenance).toContain("독점 권리를 주장하지 않는다"); + 31| expect.soft(readFileSync("LICENSE", "utf8")).toContain("does not cla… + | ^ + 32| }); + 33| + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[21/21]⎯ + + + Test Files 2 failed (2) + Tests 3 failed | 12 skipped (15) + Start at 06:47:57 + Duration 1.28s (environment 48%, tests 22%, setup 17%, transform 10%, import 3%, worker 1%) diff --git a/.agent/evidence-v0.1.1/02-assets-red.log b/.agent/evidence-v0.1.1/02-assets-red.log new file mode 100644 index 0000000..d38c28f --- /dev/null +++ b/.agent/evidence-v0.1.1/02-assets-red.log @@ -0,0 +1,4009 @@ + +> find-word@0.1.0 test +> vitest run tests/assets.test.ts + + + RUN v5.0.4 /home/hermes/worktrees/find-word-v0.1.1 + + ❯ tests/assets.test.ts (2 tests | 2 failed) 243ms + ❯ 저장한 한국어 neural 음성 자산 (2) + × 0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다 15ms + × 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 227ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다 +AssertionError: expected undefined to be '0.1.1' // Object.is equality + +- Expected: +"0.1.1" + ++ Received: +undefined + + ❯ tests/assets.test.ts:26:35 + 24| describe("저장한 한국어 neural 음성 자산", () => { + 25| it("0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다", () => { + 26| expect.soft(manifest.version).toBe("0.1.1"); + | ^ + 27| expect.soft(manifest.provider).toBe("Microsoft Edge online TTS"); + 28| expect.soft(manifest.voice).toBe("ko-KR-HyunsuMultilingualNeural"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다 +AssertionError: expected undefined to be 'Microsoft Edge online TTS' // Object.is equality + +- Expected: +"Microsoft Edge online TTS" + ++ Received: +undefined + + ❯ tests/assets.test.ts:27:36 + 25| it("0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다", () => { + 26| expect.soft(manifest.version).toBe("0.1.1"); + 27| expect.soft(manifest.provider).toBe("Microsoft Edge online TTS"); + | ^ + 28| expect.soft(manifest.voice).toBe("ko-KR-HyunsuMultilingualNeural"); + 29| expect.soft(manifest.rate).toBe("-6%"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다 +AssertionError: expected undefined to be 'ko-KR-HyunsuMultilingualNeural' // Object.is equality + +- Expected: +"ko-KR-HyunsuMultilingualNeural" + ++ Received: +undefined + + ❯ tests/assets.test.ts:28:33 + 26| expect.soft(manifest.version).toBe("0.1.1"); + 27| expect.soft(manifest.provider).toBe("Microsoft Edge online TTS"); + 28| expect.soft(manifest.voice).toBe("ko-KR-HyunsuMultilingualNeural"); + | ^ + 29| expect.soft(manifest.rate).toBe("-6%"); + 30| expect.soft(manifest.pitch).toBe("-4Hz"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다 +AssertionError: expected undefined to be '-6%' // Object.is equality + +- Expected: +"-6%" + ++ Received: +undefined + + ❯ tests/assets.test.ts:29:32 + 27| expect.soft(manifest.provider).toBe("Microsoft Edge online TTS"); + 28| expect.soft(manifest.voice).toBe("ko-KR-HyunsuMultilingualNeural"); + 29| expect.soft(manifest.rate).toBe("-6%"); + | ^ + 30| expect.soft(manifest.pitch).toBe("-4Hz"); + 31| expect.soft(manifest.sampleRate).toBe(24000); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다 +AssertionError: expected undefined to be '-4Hz' // Object.is equality + +- Expected: +"-4Hz" + ++ Received: +undefined + + ❯ tests/assets.test.ts:30:33 + 28| expect.soft(manifest.voice).toBe("ko-KR-HyunsuMultilingualNeural"); + 29| expect.soft(manifest.rate).toBe("-6%"); + 30| expect.soft(manifest.pitch).toBe("-4Hz"); + | ^ + 31| expect.soft(manifest.sampleRate).toBe(24000); + 32| expect.soft(manifest.toolVersions["edge-tts"]).toBe("7.3.1"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다 +AssertionError: expected undefined to be 24000 // Object.is equality + +- Expected: +24000 + ++ Received: +undefined + + ❯ tests/assets.test.ts:31:38 + 29| expect.soft(manifest.rate).toBe("-6%"); + 30| expect.soft(manifest.pitch).toBe("-4Hz"); + 31| expect.soft(manifest.sampleRate).toBe(24000); + | ^ + 32| expect.soft(manifest.toolVersions["edge-tts"]).toBe("7.3.1"); + 33| expect.soft(manifest.toolVersions.ffmpeg).toMatch(/^ffmpeg version… + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다 +AssertionError: expected undefined to be '7.3.1' // Object.is equality + +- Expected: +"7.3.1" + ++ Received: +undefined + + ❯ tests/assets.test.ts:32:52 + 30| expect.soft(manifest.pitch).toBe("-4Hz"); + 31| expect.soft(manifest.sampleRate).toBe(24000); + 32| expect.soft(manifest.toolVersions["edge-tts"]).toBe("7.3.1"); + | ^ + 33| expect.soft(manifest.toolVersions.ffmpeg).toMatch(/^ffmpeg version… + 34| expect(JSON.stringify(manifest)).not.toMatch(/espeak|cookie|author… + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다 +AssertionError: expected '{"generator":"eSpeak NG ko, speed 145…' not to match /espeak|cookie|authorization|trustedc…/i + +- Expected: +/espeak|cookie|authorization|trustedclienttoken/i + ++ Received: +"{\"generator\":\"eSpeak NG ko, speed 145, pitch 45, amplitude 100; FFmpeg PCM 16kHz mono s16\",\"toolVersions\":{\"espeak\":\"eSpeak NG text-to-speech: 1.51\",\"ffmpeg\":\"ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers\"},\"source\":\"https://github.com/espeak-ng/espeak-ng\",\"method\":\"자체 작성 문장의 오프라인 포먼트 합성; 외부 녹음·네트워크 TTS 없음\",\"files\":[{\"file\":\"intro.wav\",\"text\":\"으하하하~~ 나는 에스다. 널 위한 행운의 단어를 불러주마, 잘 찾아보거라.\",\"sha256\":\"4c271551d99cd9dffc01f35c176e98b54b4879549543ff8e2c79c6af8d4dc53a\",\"bytes\":259900},{\"file\":\"luck-apple.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘사과’입니다.\",\"sha256\":\"51e6cf35b219f13c0d72ec9633f0c108c3393bc5ef6ae6aca6aa5d00153b4177\",\"bytes\":125658},{\"file\":\"luck-sea.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘바다’입니다.\",\"sha256\":\"6585cb8d50eea173395417fc5fb357f55901665fe45dd07d942f79e202729111\",\"bytes\":123366},{\"file\":\"luck-tree.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘나무’입니다.\",\"sha256\":\"6d188188d1478d0298536e0e03a58883629459dee56c6ae8ab88b4d7170c1def\",\"bytes\":121224},{\"file\":\"luck-hat.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘모자’입니다.\",\"sha256\":\"be8d4287ebce15e221a8908f6b2d7478e789ecc51ba95d65fdf34f130954e7e5\",\"bytes\":121644},{\"file\":\"luck-umbrella.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘우산’입니다.\",\"sha256\":\"188e24932e5e68fb0de8270e5b48128d486da956ad4069afbbcd0ad9af5913ce\",\"bytes\":122178},{\"file\":\"luck-market.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘시장’입니다.\",\"sha256\":\"e58cdfd5a43074179e65edaf5f223dd15ad1accdd2fd547a6d3311de57b78eca\",\"bytes\":124352},{\"file\":\"luck-oil.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘기름’입니다.\",\"sha256\":\"8b1070414191ff87b9531caf0782b13458b246a1f07b51f7ddcff2cd613930f7\",\"bytes\":120286},{\"file\":\"luck-sound.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘소리’입니다.\",\"sha256\":\"052e2c2724c2fcf9d4c68ba3b746a430af0cfc74b42ed6f8317ae2cc5af1c2ad\",\"bytes\":122660},{\"file\":\"luck-meat.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘고기’입니다.\",\"sha256\":\"dcf3d1c84a9ee00ea8a1ea5552ac69b3dc46b9c4160ee3375a4afe38426c0b8d\",\"bytes\":121754},{\"file\":\"luck-furniture.wav\",\"text\":\"오늘의 행운을 담은 단어는 ‘가구’입니다.\",\"sha256\":\"6265e3807ea10eb82e9bb15a7e72dcab5e148277bc49bacfdb2663817d151ddf\",\"bytes\":122164},{\"file\":\"password-apple.wav\",\"text\":\"연습 암호는 ‘다과’, 진짜 암호는 ‘사과’입니다.\",\"sha256\":\"14b7c8c0253784f319a04b568da594d8e46077d03931a36a23e8a7689dd4b928\",\"bytes\":161688},{\"file\":\"password-sea.wav\",\"text\":\"연습 암호는 ‘사다’, 진짜 암호는 ‘바다’입니다.\",\"sha256\":\"832178b1eee4fe29653ca8dfda5bfb356cab83f4f880a704ee9eab937443f872\",\"bytes\":160256},{\"file\":\"password-tree.wav\",\"text\":\"연습 암호는 ‘나루’, 진짜 암호는 ‘나무’입니다.\",\"sha256\":\"ef25aa09f89acadcbb2c43a1f18d0c2da292fae4d87d052868942d08b3825a8d\",\"bytes\":156642},{\"file\":\"password-hat.wav\",\"text\":\"연습 암호는 ‘포자’, 진짜 암호는 ‘모자’입니다.\",\"sha256\":\"aef3f595e1b2d36212d4611f9998d841303e96ee5385639094aebcc984ff8a94\",\"bytes\":158616},{\"file\":\"password-umbrella.wav\",\"text\":\"연습 암호는 ‘부산’, 진짜 암호는 ‘우산’입니다.\",\"sha256\":\"6bc1bf13c3fd1739bc9e490525ecd5839b9a4032faecb9f101a2c68824d57b82\",\"bytes\":162668},{\"file\":\"password-market.wav\",\"text\":\"연습 암호는 ‘지장’, 진짜 암호는 ‘시장’입니다.\",\"sha256\":\"33036e08a06dd33b6aa86e5b78352d2f90e35ac1c7f9f202af2a32e2ceffcfc1\",\"bytes\":166360},{\"file\":\"password-oil.wav\",\"text\":\"연습 암호는 ‘이름’, 진짜 암호는 ‘기름’입니다.\",\"sha256\":\"d88914ab7302112beecb6b2edbe114c3c119ee375985eca9716bf239aea94038\",\"bytes\":157396},{\"file\":\"password-sound.wav\",\"text\":\"연습 암호는 ‘오리’, 진짜 암호는 ‘소리’입니다.\",\"sha256\":\"d3df570902e1f1fb5b2a7c573d57857bf4da547fb97ebdf27f04fbbf29c1170f\",\"bytes\":155298},{\"file\":\"password-meat.wav\",\"text\":\"연습 암호는 ‘보기’, 진짜 암호는 ‘고기’입니다.\",\"sha256\":\"59c422dbf3c2ed7a397e7bf3455a2bf59132f85477765c6ec13673633cdb1e8c\",\"bytes\":157754},{\"file\":\"password-furniture.wav\",\"text\":\"연습 암호는 ‘하구’, 진짜 암호는 ‘가구’입니다.\",\"sha256\":\"c349bd57598eec8543ce488dd86a4c4ddd4ec661ac9e8d7f42d914d545f38f61\",\"bytes\":158998},{\"file\":\"remember-apple.wav\",\"text\":\"‘다과’. 이 단어는 잊으세요. 이제 ‘사과’, 이 단어를 기억하세요.\",\"sha256\":\"3123d84c546ff4e630a8b93d59bd5e668a5de30ea651e56e134d47690aead28b\",\"bytes\":205094},{\"file\":\"remember-sea.wav\",\"text\":\"‘사다’. 이 단어는 잊으세요. 이제 ‘바다’, 이 단어를 기억하세요.\",\"sha256\":\"d802055500865e38b9beffe0213b70314aa59ec181cb1af1897dd010bb5875e3\",\"bytes\":204654},{\"file\":\"remember-tree.wav\",\"text\":\"‘나루’. 이 단어는 잊으세요. 이제 ‘나무’, 이 단어를 기억하세요.\",\"sha256\":\"1e3f340609eb9844b7fc727f0c17a7f9f60245d4e00df2330d734efa97d26a33\",\"bytes\":201206},{\"file\":\"remember-hat.wav\",\"text\":\"‘포자’. 이 단어는 잊으세요. 이제 ‘모자’, 이 단어를 기억하세요.\",\"sha256\":\"8a3cd1ef975d8a6782c018bbe23d9e69b4876fe8e64dcb9971f5025bb7072a4c\",\"bytes\":202530},{\"file\":\"remember-umbrella.wav\",\"text\":\"‘부산’. 이 단어는 잊으세요. 이제 ‘우산’, 이 단어를 기억하세요.\",\"sha256\":\"5c5ab3c30f02d3a3b8188f1d81f4d8f617a9e9783c18da38bf6d4bee9d03709e\",\"bytes\":209448},{\"file\":\"remember-market.wav\",\"text\":\"‘지장’. 이 단어는 잊으세요. 이제 ‘시장’, 이 단어를 기억하세요.\",\"sha256\":\"eb582873b55043a61b80e34b25302f8d52ae9c7d641032ce65ddca8fb9bfb181\",\"bytes\":212988},{\"file\":\"remember-oil.wav\",\"text\":\"‘이름’. 이 단어는 잊으세요. 이제 ‘기름’, 이 단어를 기억하세요.\",\"sha256\":\"d56d8639bf462b1c341c07252666975ff6f3428de74f0bef886019fe0085be57\",\"bytes\":204388},{\"file\":\"remember-sound.wav\",\"text\":\"‘오리’. 이 단어는 잊으세요. 이제 ‘소리’, 이 단어를 기억하세요.\",\"sha256\":\"c3d8e3e75086fd52a38806ea2122542d4fe495e381d4aa4c36e72277449451bf\",\"bytes\":200100},{\"file\":\"remember-meat.wav\",\"text\":\"‘보기’. 이 단어는 잊으세요. 이제 ‘고기’, 이 단어를 기억하세요.\",\"sha256\":\"be4cc7dc1af803a23eb4319d076ecf4c5ab37b56913287f49c3b3c96eb4a75ce\",\"bytes\":201730},{\"file\":\"remember-furniture.wav\",\"text\":\"‘하구’. 이 단어는 잊으세요. 이제 ‘가구’, 이 단어를 기억하세요.\",\"sha256\":\"acc3efbba9ef6bf53a3992a669946b6c58e90c9821e2fb5ef8e8c4a738f8bdf7\",\"bytes\":202972},{\"file\":\"last-apple.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘다과’, ‘사고’, ‘사과’.\",\"sha256\":\"5f583e9af9213cd5dc78abb68e5fcba3dbb7c65d7d8131a53210d8326e5cbb87\",\"bytes\":187732},{\"file\":\"last-sea.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘사다’, ‘보다’, ‘바다’.\",\"sha256\":\"21e589a6f7c262cca5533a8599a9d4fb257028a6e0c42732f6147209ad35ecb6\",\"bytes\":190468},{\"file\":\"last-tree.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘나루’, ‘너무’, ‘나무’.\",\"sha256\":\"915249a9860aa29c36149fc57b04057b2fa6ab9253fdfa1e0837236f84cb7aab\",\"bytes\":186820},{\"file\":\"last-hat.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘포자’, ‘모조’, ‘모자’.\",\"sha256\":\"0ce70943449ec5fb18c57d4ba7ea09a6e995f4bc991db07c70c9a846e16679a1\",\"bytes\":189216},{\"file\":\"last-umbrella.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘부산’, ‘오산’, ‘우산’.\",\"sha256\":\"8e5a3a57fc55a7deae5df5be5425a970b0a4926b74844f0f6fba74d882d8ea73\",\"bytes\":192394},{\"file\":\"last-market.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘지장’, ‘사장’, ‘시장’.\",\"sha256\":\"2419dbb03e18d3528a615b4977405699ab5285ea4b392e45164b625ebb5ff0db\",\"bytes\":201286},{\"file\":\"last-oil.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘이름’, ‘거름’, ‘기름’.\",\"sha256\":\"8a7108a6b23dddb5f2ae1c72edf6c488335ccd322acdb6d96a371e7613d47eb2\",\"bytes\":191000},{\"file\":\"last-sound.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘오리’, ‘수리’, ‘소리’.\",\"sha256\":\"c2b77970b477fd05671c833daaf2fb48aace796e1da54a1f9286fca0a8b7f484\",\"bytes\":185296},{\"file\":\"last-meat.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘보기’, ‘고개’, ‘고기’.\",\"sha256\":\"f1bbcef87d1b2fda6d36ecc484c62c6f1a4a182c12e2e0ac8bb58f494809ddf8\",\"bytes\":185540},{\"file\":\"last-furniture.wav\",\"text\":\"세 단어 중 마지막을 고르세요. ‘하구’, ‘가교’, ‘가구’.\",\"sha256\":\"5e625527e69a3f9a9c508cb9da26252ab621949d7ae9ff41bdd9336627234de0\",\"bytes\":185132},{\"file\":\"first-apple.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘사과’, ‘다과’, ‘사고’.\",\"sha256\":\"d539d03875121570650cac2a2bf800cc67e0a72effaf755e02c870f9569e5028\",\"bytes\":203904},{\"file\":\"first-sea.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘바다’, ‘사다’, ‘보다’.\",\"sha256\":\"2d789349b36dab9bbe784e2fcb8412704f2dd15b22ad63937bad9948720aa5fb\",\"bytes\":207002},{\"file\":\"first-tree.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘나무’, ‘나루’, ‘너무’.\",\"sha256\":\"af4ad056b18af0d376a1df7dda3d7b8419e76cda6ffe57dd71054692aa653fd9\",\"bytes\":203354},{\"file\":\"first-hat.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘모자’, ‘포자’, ‘모조’.\",\"sha256\":\"e34fd5edff5323e45f8621a2adb79ed871d1ce671bd3ac8c4a49a8c703878811\",\"bytes\":205388},{\"file\":\"first-umbrella.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘우산’, ‘부산’, ‘오산’.\",\"sha256\":\"4be32c725583f1579b15be4bbe110b4ab42705221a63621125aa1aa98f7f061c\",\"bytes\":210768},{\"file\":\"first-market.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘시장’, ‘지장’, ‘사장’.\",\"sha256\":\"37925d65ce12429905a1b185824722a34af8d77b15eb7bec3b52665b5c1fbc7c\",\"bytes\":217794},{\"file\":\"first-oil.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘기름’, ‘이름’, ‘거름’.\",\"sha256\":\"0991123aee956c26ca658382102c9b6d0a164fbf2dea45f07a978d2cc1fc6b80\",\"bytes\":205784},{\"file\":\"first-sound.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘소리’, ‘오리’, ‘수리’.\",\"sha256\":\"5b3063285cddd8f46a2c2cbbee1ecc9b64ecdbd287df76df65dec854679ec884\",\"bytes\":201840},{\"file\":\"first-meat.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘고기’, ‘보기’, ‘고개’.\",\"sha256\":\"acf07b6db586616e67e2841a6f122120a5b0b18543047271bd5b8c55e33786e3\",\"bytes\":201712},{\"file\":\"first-furniture.wav\",\"text\":\"세 단어 중 첫 번째를 고르세요. ‘가구’, ‘하구’, ‘가교’.\",\"sha256\":\"acba37fc2641c7073210855d294eb7c6e5f3328652c606186b1b0e569e2d0bf6\",\"bytes\":203872},{\"file\":\"repeat-apple.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘사과’, ‘다과’, ‘사과’, ‘사고’.\",\"sha256\":\"5fcef242c63dc7e86c02a1a5813e409e6392511aad5834510696b0d0499a5ef1\",\"bytes\":215994},{\"file\":\"repeat-sea.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘바다’, ‘사다’, ‘바다’, ‘보다’.\",\"sha256\":\"ef675bfa38f4eb4abac04cc64aa2c3ec416d405f9c67646356c9a8f39fe17d21\",\"bytes\":211180},{\"file\":\"repeat-tree.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘나무’, ‘나루’, ‘나무’, ‘너무’.\",\"sha256\":\"b6a1d2923d90f9dd35b324aea419daf3891b76cc8c5c9551fe5ec1140c2ab0bf\",\"bytes\":206504},{\"file\":\"repeat-hat.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘모자’, ‘포자’, ‘모자’, ‘모조’.\",\"sha256\":\"32cbc9823f5af5f897416bec2c377a8559c82fd3fae66273152826dd000cdc4c\",\"bytes\":208872},{\"file\":\"repeat-umbrella.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘우산’, ‘부산’, ‘우산’, ‘오산’.\",\"sha256\":\"4830662f39bc0c62ee9be89de80a44a837c50063fad8fb63129b865673de5525\",\"bytes\":220408},{\"file\":\"repeat-market.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘시장’, ‘지장’, ‘시장’, ‘사장’.\",\"sha256\":\"0e146387377dc38fc10435e06e8eb8cd30a595092887bf8ee4c9d0e7b577650d\",\"bytes\":232742},{\"file\":\"repeat-oil.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘기름’, ‘이름’, ‘기름’, ‘거름’.\",\"sha256\":\"94de2ef7f7d7f9132f3646c3dc09399de23b81909f3edf4bbd1c5b01fad4d8fe\",\"bytes\":216502},{\"file\":\"repeat-sound.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘소리’, ‘오리’, ‘소리’, ‘수리’.\",\"sha256\":\"7f12b6f49a7b4f64af796aa4ad8cfffc85a6b39e2bc6698f74b420b74af6c7c8\",\"bytes\":205404},{\"file\":\"repeat-meat.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘고기’, ‘보기’, ‘고기’, ‘고개’.\",\"sha256\":\"ba8beeb33bcbe4771aaea9b3832a4c25868f6e34e37b3e3d026e891c07b8e71a\",\"bytes\":203544},{\"file\":\"repeat-furniture.wav\",\"text\":\"두 번 들린 단어를 고르세요. ‘가구’, ‘하구’, ‘가구’, ‘가교’.\",\"sha256\":\"581a94726b08b6cfd3c688f08838a05787ed902ab6d1d4a038e0ff6b34f63c3a\",\"bytes\":208274},{\"file\":\"correction-apple.wav\",\"text\":\"정답은 ‘다과’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘사과’입니다.\",\"sha256\":\"53d1c1b207607e610964e68134a3d35ff521b569e228366ec4eb7c03691724bb\",\"bytes\":256484},{\"file\":\"correction-sea.wav\",\"text\":\"정답은 ‘사다’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘바다’입니다.\",\"sha256\":\"df61f91374b9fbb5817249ae3b6cadfae914d3e97ee2c6d07366ef82080a11db\",\"bytes\":255204},{\"file\":\"correction-tree.wav\",\"text\":\"정답은 ‘나루’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘나무’입니다.\",\"sha256\":\"837c24b18f63a73e135511b2edd48ba12593317de3be4d225622e6d11b1a57da\",\"bytes\":251316},{\"file\":\"correction-hat.wav\",\"text\":\"정답은 ‘포자’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘모자’입니다.\",\"sha256\":\"00d7c21caccbbb6c80ec6218415be1b0a6683a2d509b60821d3556e43353b5dc\",\"bytes\":253208},{\"file\":\"correction-umbrella.wav\",\"text\":\"정답은 ‘부산’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘우산’입니다.\",\"sha256\":\"9bb3ce5b00ccc1632c37638550f329cbb6cda01864433e068f85eb7ccab86370\",\"bytes\":255400},{\"file\":\"correction-market.wav\",\"text\":\"정답은 ‘지장’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘시장’입니다.\",\"sha256\":\"c8152db42c956701cd5e21c588ef62aedaefa922658d4bc0fecf8243548d7054\",\"bytes\":257262},{\"file\":\"correction-oil.wav\",\"text\":\"정답은 ‘이름’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘기름’입니다.\",\"sha256\":\"228759e87b1c80be72ea34ff5b77cb5e4557afca1b73434ca8f6f1c2fedaa836\",\"bytes\":247864},{\"file\":\"correction-sound.wav\",\"text\":\"정답은 ‘오리’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘소리’입니다.\",\"sha256\":\"24f2d52bb865c71fc4735a4aebd69ce0e429a9f68646e73e77adababbe34af93\",\"bytes\":250170},{\"file\":\"correction-meat.wav\",\"text\":\"정답은 ‘보기’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘고기’입니다.\",\"sha256\":\"821caf897ddab107df8435b00b686bf9223e043c91a311edcaa0c3c5eb8c6511\",\"bytes\":253432},{\"file\":\"correction-furniture.wav\",\"text\":\"정답은 ‘하구’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘가구’입니다.\",\"sha256\":\"5d69426f3319fa435ee60b234540dd8d9a07f00900177f03da2770f44511ade8\",\"bytes\":254342},{\"file\":\"middle-apple.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘다과’, ‘사과’, ‘사고’.\",\"sha256\":\"0ecc7a7d27508f73c9064a35609c922bbdd6fce8ebfc155e74f7b78d4e897dae\",\"bytes\":191056},{\"file\":\"middle-sea.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘사다’, ‘바다’, ‘보다’.\",\"sha256\":\"c7828af4e64745d89a6f23fda6c86efb76febe379bab0901c2972a4569947604\",\"bytes\":194176},{\"file\":\"middle-tree.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘나루’, ‘나무’, ‘너무’.\",\"sha256\":\"756d13453c7e13dcfaa65c8cb3497ac8d3a5d12bedb56dcd74e7f1c022392416\",\"bytes\":190516},{\"file\":\"middle-hat.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘포자’, ‘모자’, ‘모조’.\",\"sha256\":\"6ad5ec1e2c2c0de568c55a0c1f0e8361885725a2515cf396c8fe5d2106ae22d0\",\"bytes\":192548},{\"file\":\"middle-umbrella.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘부산’, ‘우산’, ‘오산’.\",\"sha256\":\"a507896dd50f98909c6a9fc0a4bfddfde5c975836e494cd9b7f40b69bc8ec33b\",\"bytes\":196102},{\"file\":\"middle-market.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘지장’, ‘시장’, ‘사장’.\",\"sha256\":\"900024616e512ea6777d08f80e3e2c83815ea1f03b6eeb537d212995474e1048\",\"bytes\":204956},{\"file\":\"middle-oil.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘이름’, ‘기름’, ‘거름’.\",\"sha256\":\"c42389a5ccd3b39a5877a38e647a6abbec6ff6a7eabf3d4c95c0507fa65d6d2f\",\"bytes\":194772},{\"file\":\"middle-sound.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘오리’, ‘소리’, ‘수리’.\",\"sha256\":\"b05ae2042bc973c3d1873fe15b5bc7f2af7e803794315023486efa788048342c\",\"bytes\":188984},{\"file\":\"middle-meat.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘보기’, ‘고기’, ‘고개’.\",\"sha256\":\"d54d6f9b4bd90b840443b9e0b0390a93909eeb81c37af6a9b57ce545bede532d\",\"bytes\":188870},{\"file\":\"middle-furniture.wav\",\"text\":\"세 단어 중 가운데를 고르세요. ‘하구’, ‘가구’, ‘가교’.\",\"sha256\":\"d9318ef33ae1c60630ddda5abeefcbe43626d8f8a9abd5d62efd132eeb77c8fc\",\"bytes\":191024},{\"file\":\"message-apple.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘다과’, ‘사과’. 지금은 빨간 불입니다.\",\"sha256\":\"3867563d5acd2530c72cbe6f98ca2b463ec0c92285069c99c8ba084c45aabfd6\",\"bytes\":316834},{\"file\":\"message-sea.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘사다’, ‘바다’. 지금은 빨간 불입니다.\",\"sha256\":\"b7d8d89d93247eca9aec124f80bf6328d6d88511d05b513ce6d9fa32b3211848\",\"bytes\":317816},{\"file\":\"message-tree.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘나루’, ‘나무’. 지금은 빨간 불입니다.\",\"sha256\":\"9539e5ee87068e241598db2e4fbacb240daa011a2238c9864da8ffde6b644918\",\"bytes\":315930},{\"file\":\"message-hat.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘포자’, ‘모자’. 지금은 빨간 불입니다.\",\"sha256\":\"ccbba6a22c9cbc1231f45fbd7420c6257ac22ac72de8e8c0c97f1cc31bb2b16e\",\"bytes\":317258},{\"file\":\"message-umbrella.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘부산’, ‘우산’. 지금은 빨간 불입니다.\",\"sha256\":\"e980f4476f7275cc5ae24e7ce0e6d9c1e6db027f7cbd1eb39cc5bc4bc07cbf6e\",\"bytes\":320532},{\"file\":\"message-market.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘지장’, ‘시장’. 지금은 빨간 불입니다.\",\"sha256\":\"35432a3ed8dd1e15894ace5aad6f5a66cf28509ba92b8ee47bcaae424cb25ba1\",\"bytes\":325564},{\"file\":\"message-oil.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘이름’, ‘기름’. 지금은 빨간 불입니다.\",\"sha256\":\"814fc749006ed040de58b5236a8d92f0d79683c8eea3b9d095386344f41a47bd\",\"bytes\":317786},{\"file\":\"message-sound.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘오리’, ‘소리’. 지금은 빨간 불입니다.\",\"sha256\":\"e25c867e6c18d906c010816475253d2b10aa3cc4192b5551ba78055d65c461c8\",\"bytes\":313270},{\"file\":\"message-meat.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘보기’, ‘고기’. 지금은 빨간 불입니다.\",\"sha256\":\"f1863e1527b7714ae64f5d752b542e8768b8ed89b5885012283f7ca20742ee00\",\"bytes\":314464},{\"file\":\"message-furniture.wav\",\"text\":\"초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘하구’, ‘가구’. 지금은 빨간 불입니다.\",\"sha256\":\"43f87d0e3d92bd132f9ea46e024b9a940736b7399e899866ed42af367a550f78\",\"bytes\":316152},{\"file\":\"mission-apple.wav\",\"text\":\"오른쪽 표지는 ‘사과’, 왼쪽 표지는 ‘다과’입니다.\",\"sha256\":\"36697151a1a57645f5acd9fcca2d13bffa21937e1bbcde08ab2d8d485730145a\",\"bytes\":166540},{\"file\":\"mission-sea.wav\",\"text\":\"오른쪽 표지는 ‘바다’, 왼쪽 표지는 ‘사다’입니다.\",\"sha256\":\"2741f404e39ec64af7f73452f82c68b3ae1d377e9f40a586034f9b147fbfb114\",\"bytes\":165122},{\"file\":\"mission-tree.wav\",\"text\":\"오른쪽 표지는 ‘나무’, 왼쪽 표지는 ‘나루’입니다.\",\"sha256\":\"8a7bb2c795d8877433be2621b69a48c2b6c5c1e192c094c01257fb93eb5d9449\",\"bytes\":160834},{\"file\":\"mission-hat.wav\",\"text\":\"오른쪽 표지는 ‘모자’, 왼쪽 표지는 ‘포자’입니다.\",\"sha256\":\"26dfa26bd8dcb9d63c231dfc37bf25970f4a8dadcaa40347dd15974887e948b1\",\"bytes\":163102},{\"file\":\"mission-umbrella.wav\",\"text\":\"오른쪽 표지는 ‘우산’, 왼쪽 표지는 ‘부산’입니다.\",\"sha256\":\"79be98918c3fb4e5e4e2e51b2bfa5e5aad6c600478ae1a34c055b16ce71d5264\",\"bytes\":167480},{\"file\":\"mission-market.wav\",\"text\":\"오른쪽 표지는 ‘시장’, 왼쪽 표지는 ‘지장’입니다.\",\"sha256\":\"8f18144a91072be37dc31a161fe6e9bb1980ab36af0edee8d7a359397b8bb8ac\",\"bytes\":170724},{\"file\":\"mission-oil.wav\",\"text\":\"오른쪽 표지는 ‘기름’, 왼쪽 표지는 ‘이름’입니다.\",\"sha256\":\"1ceabc58f445f7bd5609b2f9de80b309ad64d11c888c7261cf84315a7637a748\",\"bytes\":161758},{\"file\":\"mission-sound.wav\",\"text\":\"오른쪽 표지는 ‘소리’, 왼쪽 표지는 ‘오리’입니다.\",\"sha256\":\"6651b14caf4e54069730f137e5d8c3c1e5a9c66aa72f15623cc442edb967cadb\",\"bytes\":160162},{\"file\":\"mission-meat.wav\",\"text\":\"오른쪽 표지는 ‘고기’, 왼쪽 표지는 ‘보기’입니다.\",\"sha256\":\"5d2ab3071fd27e850d99c50ad37905096ef28267b660a35992559bdc126c4931\",\"bytes\":162626},{\"file\":\"mission-furniture.wav\",\"text\":\"오른쪽 표지는 ‘가구’, 왼쪽 표지는 ‘하구’입니다.\",\"sha256\":\"080307bb46bb47378c0c03437d9ef44118a5d4ea17b2b6d70e5cbc710c71ba78\",\"bytes\":163884}]}" + + ❯ tests/assets.test.ts:34:42 + 32| expect.soft(manifest.toolVersions["edge-tts"]).toBe("7.3.1"); + 33| expect.soft(manifest.toolVersions.ffmpeg).toMatch(/^ffmpeg version… + 34| expect(JSON.stringify(manifest)).not.toMatch(/espeak|cookie|author… + | ^ + 35| }); + 36| it("101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다", (… + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: intro.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: intro.wav: expected undefined to be 8.1205 // Object.is equality + +- Expected: +8.1205 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[10/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[11/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-apple.wav: expected undefined to be 3.9254375 // Object.is equality + +- Expected: +3.9254375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[12/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[13/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-sea.wav: expected undefined to be 3.8538125 // Object.is equality + +- Expected: +3.8538125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[14/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[15/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-tree.wav: expected undefined to be 3.786875 // Object.is equality + +- Expected: +3.786875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[16/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[17/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-hat.wav: expected undefined to be 3.8 // Object.is equality + +- Expected: +3.8 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[18/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[19/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-umbrella.wav: expected undefined to be 3.8166875 // Object.is equality + +- Expected: +3.8166875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[20/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[21/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-market.wav: expected undefined to be 3.884625 // Object.is equality + +- Expected: +3.884625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[22/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[23/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-oil.wav: expected undefined to be 3.7575625 // Object.is equality + +- Expected: +3.7575625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[24/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[25/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-sound.wav: expected undefined to be 3.83175 // Object.is equality + +- Expected: +3.83175 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[26/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[27/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-meat.wav: expected undefined to be 3.8034375 // Object.is equality + +- Expected: +3.8034375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[28/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[29/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: luck-furniture.wav: expected undefined to be 3.81625 // Object.is equality + +- Expected: +3.81625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[30/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[31/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-apple.wav: expected undefined to be 5.051375 // Object.is equality + +- Expected: +5.051375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[32/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[33/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-sea.wav: expected undefined to be 5.006625 // Object.is equality + +- Expected: +5.006625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[34/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[35/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-tree.wav: expected undefined to be 4.8936875 // Object.is equality + +- Expected: +4.8936875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[36/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[37/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-hat.wav: expected undefined to be 4.955375 // Object.is equality + +- Expected: +4.955375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[38/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[39/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-umbrella.wav: expected undefined to be 5.082 // Object.is equality + +- Expected: +5.082 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[40/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[41/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-market.wav: expected undefined to be 5.197375 // Object.is equality + +- Expected: +5.197375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[42/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[43/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-oil.wav: expected undefined to be 4.91725 // Object.is equality + +- Expected: +4.91725 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[44/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[45/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-sound.wav: expected undefined to be 4.8516875 // Object.is equality + +- Expected: +4.8516875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[46/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[47/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-meat.wav: expected undefined to be 4.9284375 // Object.is equality + +- Expected: +4.9284375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[48/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[49/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: password-furniture.wav: expected undefined to be 4.9673125 // Object.is equality + +- Expected: +4.9673125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[50/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[51/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-apple.wav: expected undefined to be 6.4078125 // Object.is equality + +- Expected: +6.4078125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[52/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[53/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-sea.wav: expected undefined to be 6.3940625 // Object.is equality + +- Expected: +6.3940625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[54/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[55/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-tree.wav: expected undefined to be 6.2863125 // Object.is equality + +- Expected: +6.2863125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[56/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[57/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-hat.wav: expected undefined to be 6.3276875 // Object.is equality + +- Expected: +6.3276875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[58/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[59/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-umbrella.wav: expected undefined to be 6.543875 // Object.is equality + +- Expected: +6.543875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[60/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[61/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-market.wav: expected undefined to be 6.6545 // Object.is equality + +- Expected: +6.6545 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[62/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[63/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-oil.wav: expected undefined to be 6.38575 // Object.is equality + +- Expected: +6.38575 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[64/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[65/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-sound.wav: expected undefined to be 6.25175 // Object.is equality + +- Expected: +6.25175 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[66/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[67/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-meat.wav: expected undefined to be 6.3026875 // Object.is equality + +- Expected: +6.3026875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[68/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[69/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: remember-furniture.wav: expected undefined to be 6.3415 // Object.is equality + +- Expected: +6.3415 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[70/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[71/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-apple.wav: expected undefined to be 5.86525 // Object.is equality + +- Expected: +5.86525 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[72/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[73/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-sea.wav: expected undefined to be 5.95075 // Object.is equality + +- Expected: +5.95075 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[74/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[75/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-tree.wav: expected undefined to be 5.83675 // Object.is equality + +- Expected: +5.83675 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[76/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[77/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-hat.wav: expected undefined to be 5.911625 // Object.is equality + +- Expected: +5.911625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[78/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[79/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-umbrella.wav: expected undefined to be 6.0109375 // Object.is equality + +- Expected: +6.0109375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[80/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[81/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-market.wav: expected undefined to be 6.2888125 // Object.is equality + +- Expected: +6.2888125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[82/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[83/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-oil.wav: expected undefined to be 5.967375 // Object.is equality + +- Expected: +5.967375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[84/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[85/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-sound.wav: expected undefined to be 5.789125 // Object.is equality + +- Expected: +5.789125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[86/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[87/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-meat.wav: expected undefined to be 5.79675 // Object.is equality + +- Expected: +5.79675 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[88/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[89/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: last-furniture.wav: expected undefined to be 5.784 // Object.is equality + +- Expected: +5.784 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[90/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[91/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-apple.wav: expected undefined to be 6.370625 // Object.is equality + +- Expected: +6.370625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[92/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[93/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-sea.wav: expected undefined to be 6.4674375 // Object.is equality + +- Expected: +6.4674375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[94/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[95/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-tree.wav: expected undefined to be 6.3534375 // Object.is equality + +- Expected: +6.3534375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[96/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[97/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-hat.wav: expected undefined to be 6.417 // Object.is equality + +- Expected: +6.417 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[98/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[99/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-umbrella.wav: expected undefined to be 6.585125 // Object.is equality + +- Expected: +6.585125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[100/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[101/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-market.wav: expected undefined to be 6.8046875 // Object.is equality + +- Expected: +6.8046875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[102/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[103/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-oil.wav: expected undefined to be 6.429375 // Object.is equality + +- Expected: +6.429375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[104/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[105/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-sound.wav: expected undefined to be 6.306125 // Object.is equality + +- Expected: +6.306125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[106/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[107/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-meat.wav: expected undefined to be 6.302125 // Object.is equality + +- Expected: +6.302125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[108/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[109/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: first-furniture.wav: expected undefined to be 6.369625 // Object.is equality + +- Expected: +6.369625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[110/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[111/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-apple.wav: expected undefined to be 6.7484375 // Object.is equality + +- Expected: +6.7484375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[112/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[113/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-sea.wav: expected undefined to be 6.598 // Object.is equality + +- Expected: +6.598 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[114/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[115/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-tree.wav: expected undefined to be 6.451875 // Object.is equality + +- Expected: +6.451875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[116/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[117/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-hat.wav: expected undefined to be 6.525875 // Object.is equality + +- Expected: +6.525875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[118/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[119/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-umbrella.wav: expected undefined to be 6.886375 // Object.is equality + +- Expected: +6.886375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[120/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[121/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-market.wav: expected undefined to be 7.2718125 // Object.is equality + +- Expected: +7.2718125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[122/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[123/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-oil.wav: expected undefined to be 6.7643125 // Object.is equality + +- Expected: +6.7643125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[124/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[125/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-sound.wav: expected undefined to be 6.4175 // Object.is equality + +- Expected: +6.4175 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[126/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[127/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-meat.wav: expected undefined to be 6.359375 // Object.is equality + +- Expected: +6.359375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[128/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[129/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: repeat-furniture.wav: expected undefined to be 6.5071875 // Object.is equality + +- Expected: +6.5071875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[130/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[131/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-apple.wav: expected undefined to be 8.01375 // Object.is equality + +- Expected: +8.01375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[132/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[133/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-sea.wav: expected undefined to be 7.97375 // Object.is equality + +- Expected: +7.97375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[134/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[135/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-tree.wav: expected undefined to be 7.85225 // Object.is equality + +- Expected: +7.85225 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[136/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[137/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-hat.wav: expected undefined to be 7.911375 // Object.is equality + +- Expected: +7.911375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[138/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[139/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-umbrella.wav: expected undefined to be 7.979875 // Object.is equality + +- Expected: +7.979875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[140/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[141/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-market.wav: expected undefined to be 8.0380625 // Object.is equality + +- Expected: +8.0380625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[142/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[143/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-oil.wav: expected undefined to be 7.744375 // Object.is equality + +- Expected: +7.744375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[144/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[145/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-sound.wav: expected undefined to be 7.8164375 // Object.is equality + +- Expected: +7.8164375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[146/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[147/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-meat.wav: expected undefined to be 7.918375 // Object.is equality + +- Expected: +7.918375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[148/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[149/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: correction-furniture.wav: expected undefined to be 7.9468125 // Object.is equality + +- Expected: +7.9468125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[150/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[151/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-apple.wav: expected undefined to be 5.969125 // Object.is equality + +- Expected: +5.969125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[152/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[153/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-sea.wav: expected undefined to be 6.066625 // Object.is equality + +- Expected: +6.066625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[154/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[155/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-tree.wav: expected undefined to be 5.95225 // Object.is equality + +- Expected: +5.95225 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[156/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[157/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-hat.wav: expected undefined to be 6.01575 // Object.is equality + +- Expected: +6.01575 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[158/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[159/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-umbrella.wav: expected undefined to be 6.1268125 // Object.is equality + +- Expected: +6.1268125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[160/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[161/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-market.wav: expected undefined to be 6.4035 // Object.is equality + +- Expected: +6.4035 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[162/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[163/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-oil.wav: expected undefined to be 6.08525 // Object.is equality + +- Expected: +6.08525 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[164/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[165/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-sound.wav: expected undefined to be 5.904375 // Object.is equality + +- Expected: +5.904375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[166/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[167/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-meat.wav: expected undefined to be 5.9008125 // Object.is equality + +- Expected: +5.9008125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[168/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[169/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: middle-furniture.wav: expected undefined to be 5.968125 // Object.is equality + +- Expected: +5.968125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[170/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[171/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-apple.wav: expected undefined to be 9.8996875 // Object.is equality + +- Expected: +9.8996875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[172/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[173/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-sea.wav: expected undefined to be 9.930375 // Object.is equality + +- Expected: +9.930375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[174/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[175/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-tree.wav: expected undefined to be 9.8714375 // Object.is equality + +- Expected: +9.8714375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[176/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[177/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-hat.wav: expected undefined to be 9.9129375 // Object.is equality + +- Expected: +9.9129375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[178/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[179/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-umbrella.wav: expected undefined to be 10.01525 // Object.is equality + +- Expected: +10.01525 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[180/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[181/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-market.wav: expected undefined to be 10.1725 // Object.is equality + +- Expected: +10.1725 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[182/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[183/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-oil.wav: expected undefined to be 9.9294375 // Object.is equality + +- Expected: +9.9294375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[184/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[185/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-sound.wav: expected undefined to be 9.7883125 // Object.is equality + +- Expected: +9.7883125 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[186/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[187/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-meat.wav: expected undefined to be 9.825625 // Object.is equality + +- Expected: +9.825625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[188/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[189/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: message-furniture.wav: expected undefined to be 9.878375 // Object.is equality + +- Expected: +9.878375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[190/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-apple.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[191/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-apple.wav: expected undefined to be 5.203 // Object.is equality + +- Expected: +5.203 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[192/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-sea.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[193/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-sea.wav: expected undefined to be 5.1586875 // Object.is equality + +- Expected: +5.1586875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[194/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-tree.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[195/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-tree.wav: expected undefined to be 5.0246875 // Object.is equality + +- Expected: +5.0246875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[196/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-hat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[197/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-hat.wav: expected undefined to be 5.0955625 // Object.is equality + +- Expected: +5.0955625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[198/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-umbrella.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[199/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-umbrella.wav: expected undefined to be 5.232375 // Object.is equality + +- Expected: +5.232375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[200/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-market.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[201/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-market.wav: expected undefined to be 5.33375 // Object.is equality + +- Expected: +5.33375 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[202/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-oil.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[203/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-oil.wav: expected undefined to be 5.0535625 // Object.is equality + +- Expected: +5.0535625 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[204/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-sound.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[205/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-sound.wav: expected undefined to be 5.0036875 // Object.is equality + +- Expected: +5.0036875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[206/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-meat.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[207/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-meat.wav: expected undefined to be 5.0806875 // Object.is equality + +- Expected: +5.0806875 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[208/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-furniture.wav: expected 16000 to be 24000 // Object.is equality + +- Expected ++ Received + +- 24000 ++ 16000 + + ❯ tests/assets.test.ts:51:54 + 49| expect(file.readUInt16LE(20)).toBe(1); + 50| expect(file.readUInt16LE(22)).toBe(1); + 51| expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); + | ^ + 52| expect(file.readUInt16LE(34)).toBe(16); + 53| expect(file.toString("ascii", 36, 40)).toBe("data"); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[209/210]⎯ + + FAIL tests/assets.test.ts > 저장한 한국어 neural 음성 자산 > 101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다 +AssertionError: mission-furniture.wav: expected undefined to be 5.12 // Object.is equality + +- Expected: +5.12 + ++ Received: +undefined + + ❯ tests/assets.test.ts:56:47 + 54| expect(file.readUInt32LE(40)).toBe(file.length - 44); + 55| const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + 56| expect.soft(entry.duration, entry.file).toBe(duration); + | ^ + 57| expect(duration).toBeGreaterThanOrEqual(1); + 58| expect(duration).toBeLessThanOrEqual(20); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[210/210]⎯ + + + Test Files 1 failed (1) + Tests 2 failed (2) + Start at 06:48:22 + Duration 1.66s (tests 57%, setup 28%, transform 12%, import 3%, worker 1%) diff --git a/.agent/evidence-v0.1.1/03-generator-red.log b/.agent/evidence-v0.1.1/03-generator-red.log new file mode 100644 index 0000000..69c7c86 --- /dev/null +++ b/.agent/evidence-v0.1.1/03-generator-red.log @@ -0,0 +1,305 @@ +test_all_101_are_validated_before_single_atomic_publication (test_neural_audio.GeneratorContract.test_all_101_are_validated_before_single_atomic_publication) ... ERROR +test_atomic_exchange_keeps_old_tree_at_staging_path (test_neural_audio.GeneratorContract.test_atomic_exchange_keeps_old_tree_at_staging_path) ... ERROR +test_atomic_first_publication (test_neural_audio.GeneratorContract.test_atomic_first_publication) ... ERROR +test_cli_import_has_no_generation_or_filesystem_side_effects (test_neural_audio.GeneratorContract.test_cli_import_has_no_generation_or_filesystem_side_effects) ... FAIL +test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell (test_neural_audio.GeneratorContract.test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell) ... ERROR +test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets (test_neural_audio.GeneratorContract.test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets) ... FAIL +test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24 (test_neural_audio.GeneratorContract.test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24) ... ERROR +test_ffmpeg_failure_is_not_retried_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_ffmpeg_failure_is_not_retried_and_preserves_existing_files) ... FAIL +test_interruption_preserves_existing_files_and_cleans_temporary_data (test_neural_audio.GeneratorContract.test_interruption_preserves_existing_files_and_cleans_temporary_data) ... ERROR +test_invalid_pcm_never_reaches_publication (test_neural_audio.GeneratorContract.test_invalid_pcm_never_reaches_publication) ... ERROR +test_publication_failure_preserves_existing_files (test_neural_audio.GeneratorContract.test_publication_failure_preserves_existing_files) ... ERROR +test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) ... + test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': 'intro.wav', 'text': '안내 문장'}) ... ERROR + test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': 'intro.wav', 'text': '안내 문장'}) ... ERROR + test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': '../escape.wav', 'text': '외부'}) ... ERROR +test_timeout_is_bounded_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_timeout_is_bounded_and_preserves_existing_files) ... FAIL +test_transient_provider_failures_retry_cleanly_with_bounded_backoff (test_neural_audio.GeneratorContract.test_transient_provider_failures_retry_cleanly_with_bounded_backoff) ... ERROR +test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) ... + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'amplitude': 0}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'amplitude': 32767}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'rate': 16000}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'channels': 2}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'width': 1}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'duration': 0.2}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'duration': 21}) ... ERROR +test_valid_pcm_returns_measured_hash_bytes_duration_and_levels (test_neural_audio.PCMContract.test_valid_pcm_returns_measured_hash_bytes_duration_and_levels) ... ERROR + +====================================================================== +ERROR: test_all_101_are_validated_before_single_atomic_publication (test_neural_audio.GeneratorContract.test_all_101_are_validated_before_single_atomic_publication) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 78, in test_all_101_are_validated_before_single_atomic_publication + result = self.run_generate() + ^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_atomic_exchange_keeps_old_tree_at_staging_path (test_neural_audio.GeneratorContract.test_atomic_exchange_keeps_old_tree_at_staging_path) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 202, in test_atomic_exchange_keeps_old_tree_at_staging_path + audio.atomic_publish(staged, self.output) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 13, in atomic_publish + raise NotImplementedError("원자 디렉터리 교체 미구현") +NotImplementedError: 원자 디렉터리 교체 미구현 + +====================================================================== +ERROR: test_atomic_first_publication (test_neural_audio.GeneratorContract.test_atomic_first_publication) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 211, in test_atomic_first_publication + audio.atomic_publish(staged, output) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 13, in atomic_publish + raise NotImplementedError("원자 디렉터리 교체 미구현") +NotImplementedError: 원자 디렉터리 교체 미구현 + +====================================================================== +ERROR: test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell (test_neural_audio.GeneratorContract.test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 177, in test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell + audio.synthesize_edge("한국어 ; $(literal)", path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 17, in synthesize_edge + raise NotImplementedError("pinned edge-tts 실행 미구현") +NotImplementedError: pinned edge-tts 실행 미구현 + +====================================================================== +ERROR: test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24 (test_neural_audio.GeneratorContract.test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 190, in test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24 + audio.convert_mp3(mp3, output) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 21, in convert_mp3 + raise NotImplementedError("ffmpeg 변환 및 선형 레벨 보정 미구현") +NotImplementedError: ffmpeg 변환 및 선형 레벨 보정 미구현 + +====================================================================== +ERROR: test_interruption_preserves_existing_files_and_cleans_temporary_data (test_neural_audio.GeneratorContract.test_interruption_preserves_existing_files_and_cleans_temporary_data) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 149, in test_interruption_preserves_existing_files_and_cleans_temporary_data + self.run_generate(synthesize=interrupt) + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_invalid_pcm_never_reaches_publication (test_neural_audio.GeneratorContract.test_invalid_pcm_never_reaches_publication) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 132, in test_invalid_pcm_never_reaches_publication + self.run_generate(convert=invalid) + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_publication_failure_preserves_existing_files (test_neural_audio.GeneratorContract.test_publication_failure_preserves_existing_files) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 155, in test_publication_failure_preserves_existing_files + self.run_generate() + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': 'intro.wav', 'text': '안내 문장'}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 163, in test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests + audio.generate(entries, self.output, synthesize=self.synthesize) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': 'intro.wav', 'text': '안내 문장'}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 163, in test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests + audio.generate(entries, self.output, synthesize=self.synthesize) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': '../escape.wav', 'text': '외부'}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 163, in test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests + audio.generate(entries, self.output, synthesize=self.synthesize) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_transient_provider_failures_retry_cleanly_with_bounded_backoff (test_neural_audio.GeneratorContract.test_transient_provider_failures_retry_cleanly_with_bounded_backoff) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 97, in test_transient_provider_failures_retry_cleanly_with_bounded_backoff + self.run_generate(synthesize=flaky) + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'amplitude': 0}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 225, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'amplitude': 32767}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 225, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'rate': 16000}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 225, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'channels': 2}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 225, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'width': 1}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 225, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'duration': 0.2}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 225, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'duration': 21}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 225, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_valid_pcm_returns_measured_hash_bytes_duration_and_levels (test_neural_audio.PCMContract.test_valid_pcm_returns_measured_hash_bytes_duration_and_levels) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 231, in test_valid_pcm_returns_measured_hash_bytes_duration_and_levels + result = audio.inspect_wav(path) + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +FAIL: test_cli_import_has_no_generation_or_filesystem_side_effects (test_neural_audio.GeneratorContract.test_cli_import_has_no_generation_or_filesystem_side_effects) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 172, in test_cli_import_has_no_generation_or_filesystem_side_effects + spec.loader.exec_module(module) + File "", line 940, in exec_module + File "", line 241, in _call_with_frames_removed + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/generate_audio.py", line 27, in + source = subprocess.check_output([ + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/lib/python3.11/unittest/mock.py", line 1124, in __call__ + return self._mock_call(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/lib/python3.11/unittest/mock.py", line 1128, in _mock_call + return self._execute_mock_call(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/lib/python3.11/unittest/mock.py", line 1183, in _execute_mock_call + raise effect +AssertionError: import 실행 금지 + +====================================================================== +FAIL: test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets (test_neural_audio.GeneratorContract.test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets) +---------------------------------------------------------------------- +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 111, in test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets + with self.assertRaisesRegex(RuntimeError, "target-1.wav.*3") as failure: +AssertionError: "target-1.wav.*3" does not match "neural 생성·retry·검증·원자 교체 미구현" + +====================================================================== +FAIL: test_ffmpeg_failure_is_not_retried_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_ffmpeg_failure_is_not_retried_and_preserves_existing_files) +---------------------------------------------------------------------- +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 138, in test_ffmpeg_failure_is_not_retried_and_preserves_existing_files + with self.assertRaisesRegex(RuntimeError, "intro.wav.*변환"): +AssertionError: "intro.wav.*변환" does not match "neural 생성·retry·검증·원자 교체 미구현" + +====================================================================== +FAIL: test_timeout_is_bounded_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_timeout_is_bounded_and_preserves_existing_files) +---------------------------------------------------------------------- +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 123, in test_timeout_is_bounded_and_preserves_existing_files + with self.assertRaisesRegex(RuntimeError, "intro.wav.*3"): +AssertionError: "intro.wav.*3" does not match "neural 생성·retry·검증·원자 교체 미구현" + +---------------------------------------------------------------------- +Ran 16 tests in 0.633s + +FAILED (failures=4, errors=20) diff --git a/.agent/evidence-v0.1.1/04-generator-additional-red.log b/.agent/evidence-v0.1.1/04-generator-additional-red.log new file mode 100644 index 0000000..97c9f92 --- /dev/null +++ b/.agent/evidence-v0.1.1/04-generator-additional-red.log @@ -0,0 +1,332 @@ +test_all_101_are_validated_before_single_atomic_publication (test_neural_audio.GeneratorContract.test_all_101_are_validated_before_single_atomic_publication) ... ERROR +test_atomic_exchange_keeps_old_tree_at_staging_path (test_neural_audio.GeneratorContract.test_atomic_exchange_keeps_old_tree_at_staging_path) ... ERROR +test_atomic_first_publication (test_neural_audio.GeneratorContract.test_atomic_first_publication) ... ERROR +test_cli_import_has_no_generation_or_filesystem_side_effects (test_neural_audio.GeneratorContract.test_cli_import_has_no_generation_or_filesystem_side_effects) ... FAIL +test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell (test_neural_audio.GeneratorContract.test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell) ... ERROR +test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets (test_neural_audio.GeneratorContract.test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets) ... FAIL +test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24 (test_neural_audio.GeneratorContract.test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24) ... ERROR +test_ffmpeg_failure_is_not_retried_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_ffmpeg_failure_is_not_retried_and_preserves_existing_files) ... FAIL +test_full_validation_detects_changed_earlier_file_before_publication (test_neural_audio.GeneratorContract.test_full_validation_detects_changed_earlier_file_before_publication) ... ERROR +test_interruption_preserves_existing_files_and_cleans_temporary_data (test_neural_audio.GeneratorContract.test_interruption_preserves_existing_files_and_cleans_temporary_data) ... ERROR +test_invalid_pcm_never_reaches_publication (test_neural_audio.GeneratorContract.test_invalid_pcm_never_reaches_publication) ... ERROR +test_publication_failure_preserves_existing_files (test_neural_audio.GeneratorContract.test_publication_failure_preserves_existing_files) ... ERROR +test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) ... + test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': 'intro.wav', 'text': '안내 문장'}) ... ERROR + test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': 'intro.wav', 'text': '안내 문장'}) ... ERROR + test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': '../escape.wav', 'text': '외부'}) ... ERROR +test_timeout_is_bounded_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_timeout_is_bounded_and_preserves_existing_files) ... FAIL +test_transient_provider_failures_retry_cleanly_with_bounded_backoff (test_neural_audio.GeneratorContract.test_transient_provider_failures_retry_cleanly_with_bounded_backoff) ... ERROR +test_wrong_edge_version_fails_before_network_or_asset_changes (test_neural_audio.GeneratorContract.test_wrong_edge_version_fails_before_network_or_asset_changes) ... FAIL +test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) ... + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'amplitude': 0}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'amplitude': 32767}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'rate': 16000}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'channels': 2}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'width': 1}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'duration': 0.2}) ... ERROR + test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'duration': 21}) ... ERROR +test_valid_pcm_returns_measured_hash_bytes_duration_and_levels (test_neural_audio.PCMContract.test_valid_pcm_returns_measured_hash_bytes_duration_and_levels) ... ERROR + +====================================================================== +ERROR: test_all_101_are_validated_before_single_atomic_publication (test_neural_audio.GeneratorContract.test_all_101_are_validated_before_single_atomic_publication) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 78, in test_all_101_are_validated_before_single_atomic_publication + result = self.run_generate() + ^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_atomic_exchange_keeps_old_tree_at_staging_path (test_neural_audio.GeneratorContract.test_atomic_exchange_keeps_old_tree_at_staging_path) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 218, in test_atomic_exchange_keeps_old_tree_at_staging_path + audio.atomic_publish(staged, self.output) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 13, in atomic_publish + raise NotImplementedError("원자 디렉터리 교체 미구현") +NotImplementedError: 원자 디렉터리 교체 미구현 + +====================================================================== +ERROR: test_atomic_first_publication (test_neural_audio.GeneratorContract.test_atomic_first_publication) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 227, in test_atomic_first_publication + audio.atomic_publish(staged, output) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 13, in atomic_publish + raise NotImplementedError("원자 디렉터리 교체 미구현") +NotImplementedError: 원자 디렉터리 교체 미구현 + +====================================================================== +ERROR: test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell (test_neural_audio.GeneratorContract.test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 193, in test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell + audio.synthesize_edge("한국어 ; $(literal)", path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 17, in synthesize_edge + raise NotImplementedError("pinned edge-tts 실행 미구현") +NotImplementedError: pinned edge-tts 실행 미구현 + +====================================================================== +ERROR: test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24 (test_neural_audio.GeneratorContract.test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 206, in test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24 + audio.convert_mp3(mp3, output) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 21, in convert_mp3 + raise NotImplementedError("ffmpeg 변환 및 선형 레벨 보정 미구현") +NotImplementedError: ffmpeg 변환 및 선형 레벨 보정 미구현 + +====================================================================== +ERROR: test_full_validation_detects_changed_earlier_file_before_publication (test_neural_audio.GeneratorContract.test_full_validation_detects_changed_earlier_file_before_publication) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 164, in test_full_validation_detects_changed_earlier_file_before_publication + self.run_generate(convert=tamper) + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_interruption_preserves_existing_files_and_cleans_temporary_data (test_neural_audio.GeneratorContract.test_interruption_preserves_existing_files_and_cleans_temporary_data) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 149, in test_interruption_preserves_existing_files_and_cleans_temporary_data + self.run_generate(synthesize=interrupt) + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_invalid_pcm_never_reaches_publication (test_neural_audio.GeneratorContract.test_invalid_pcm_never_reaches_publication) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 132, in test_invalid_pcm_never_reaches_publication + self.run_generate(convert=invalid) + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_publication_failure_preserves_existing_files (test_neural_audio.GeneratorContract.test_publication_failure_preserves_existing_files) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 155, in test_publication_failure_preserves_existing_files + self.run_generate() + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': 'intro.wav', 'text': '안내 문장'}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 179, in test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests + audio.generate(entries, self.output, synthesize=self.synthesize) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': 'intro.wav', 'text': '안내 문장'}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 179, in test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests + audio.generate(entries, self.output, synthesize=self.synthesize) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) (entries={'file': '../escape.wav', 'text': '외부'}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 179, in test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests + audio.generate(entries, self.output, synthesize=self.synthesize) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_transient_provider_failures_retry_cleanly_with_bounded_backoff (test_neural_audio.GeneratorContract.test_transient_provider_failures_retry_cleanly_with_bounded_backoff) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 97, in test_transient_provider_failures_retry_cleanly_with_bounded_backoff + self.run_generate(synthesize=flaky) + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 69, in run_generate + return audio.generate(ENTRIES, self.output, synthesize=synthesize or self.synthesize, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 5, in generate + raise NotImplementedError("neural 생성·retry·검증·원자 교체 미구현") +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'amplitude': 0}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 241, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'amplitude': 32767}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 241, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'rate': 16000}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 241, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'channels': 2}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 241, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'width': 1}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 241, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'duration': 0.2}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 241, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) (options={'duration': 21}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 241, in test_rejects_silence_clipping_bad_format_and_abnormal_duration + audio.inspect_wav(path) + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +ERROR: test_valid_pcm_returns_measured_hash_bytes_duration_and_levels (test_neural_audio.PCMContract.test_valid_pcm_returns_measured_hash_bytes_duration_and_levels) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 247, in test_valid_pcm_returns_measured_hash_bytes_duration_and_levels + result = audio.inspect_wav(path) + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/neural_audio.py", line 9, in inspect_wav + raise NotImplementedError("PCM 품질 검사 미구현") +NotImplementedError: PCM 품질 검사 미구현 + +====================================================================== +FAIL: test_cli_import_has_no_generation_or_filesystem_side_effects (test_neural_audio.GeneratorContract.test_cli_import_has_no_generation_or_filesystem_side_effects) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 188, in test_cli_import_has_no_generation_or_filesystem_side_effects + spec.loader.exec_module(module) + File "", line 940, in exec_module + File "", line 241, in _call_with_frames_removed + File "/home/hermes/worktrees/find-word-v0.1.1/scripts/generate_audio.py", line 27, in + source = subprocess.check_output([ + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/lib/python3.11/unittest/mock.py", line 1124, in __call__ + return self._mock_call(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/lib/python3.11/unittest/mock.py", line 1128, in _mock_call + return self._execute_mock_call(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/hermes/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/lib/python3.11/unittest/mock.py", line 1183, in _execute_mock_call + raise effect +AssertionError: import 실행 금지 + +====================================================================== +FAIL: test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets (test_neural_audio.GeneratorContract.test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets) +---------------------------------------------------------------------- +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 111, in test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets + with self.assertRaisesRegex(RuntimeError, "target-1.wav.*3") as failure: +AssertionError: "target-1.wav.*3" does not match "neural 생성·retry·검증·원자 교체 미구현" + +====================================================================== +FAIL: test_ffmpeg_failure_is_not_retried_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_ffmpeg_failure_is_not_retried_and_preserves_existing_files) +---------------------------------------------------------------------- +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 138, in test_ffmpeg_failure_is_not_retried_and_preserves_existing_files + with self.assertRaisesRegex(RuntimeError, "intro.wav.*변환"): +AssertionError: "intro.wav.*변환" does not match "neural 생성·retry·검증·원자 교체 미구현" + +====================================================================== +FAIL: test_timeout_is_bounded_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_timeout_is_bounded_and_preserves_existing_files) +---------------------------------------------------------------------- +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 123, in test_timeout_is_bounded_and_preserves_existing_files + with self.assertRaisesRegex(RuntimeError, "intro.wav.*3"): +AssertionError: "intro.wav.*3" does not match "neural 생성·retry·검증·원자 교체 미구현" + +====================================================================== +FAIL: test_wrong_edge_version_fails_before_network_or_asset_changes (test_neural_audio.GeneratorContract.test_wrong_edge_version_fails_before_network_or_asset_changes) +---------------------------------------------------------------------- +NotImplementedError: neural 생성·retry·검증·원자 교체 미구현 + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/home/hermes/worktrees/find-word-v0.1.1/tests/python/test_neural_audio.py", line 169, in test_wrong_edge_version_fails_before_network_or_asset_changes + with self.assertRaisesRegex(RuntimeError, "7.3.1"): +AssertionError: "7.3.1" does not match "neural 생성·retry·검증·원자 교체 미구현" + +---------------------------------------------------------------------- +Ran 18 tests in 0.488s + +FAILED (failures=5, errors=21) diff --git a/.agent/evidence-v0.1.1/05-generator-green.log b/.agent/evidence-v0.1.1/05-generator-green.log new file mode 100644 index 0000000..6e24b5b --- /dev/null +++ b/.agent/evidence-v0.1.1/05-generator-green.log @@ -0,0 +1,432 @@ +test_all_101_are_validated_before_single_atomic_publication (test_neural_audio.GeneratorContract.test_all_101_are_validated_before_single_atomic_publication) ... [001/101] intro.wav 1.000s +[002/101] target-0.wav 1.000s +[003/101] target-1.wav 1.000s +[004/101] target-2.wav 1.000s +[005/101] target-3.wav 1.000s +[006/101] target-4.wav 1.000s +[007/101] target-5.wav 1.000s +[008/101] target-6.wav 1.000s +[009/101] target-7.wav 1.000s +[010/101] target-8.wav 1.000s +[011/101] target-9.wav 1.000s +[012/101] target-10.wav 1.000s +[013/101] target-11.wav 1.000s +[014/101] target-12.wav 1.000s +[015/101] target-13.wav 1.000s +[016/101] target-14.wav 1.000s +[017/101] target-15.wav 1.000s +[018/101] target-16.wav 1.000s +[019/101] target-17.wav 1.000s +[020/101] target-18.wav 1.000s +[021/101] target-19.wav 1.000s +[022/101] target-20.wav 1.000s +[023/101] target-21.wav 1.000s +[024/101] target-22.wav 1.000s +[025/101] target-23.wav 1.000s +[026/101] target-24.wav 1.000s +[027/101] target-25.wav 1.000s +[028/101] target-26.wav 1.000s +[029/101] target-27.wav 1.000s +[030/101] target-28.wav 1.000s +[031/101] target-29.wav 1.000s +[032/101] target-30.wav 1.000s +[033/101] target-31.wav 1.000s +[034/101] target-32.wav 1.000s +[035/101] target-33.wav 1.000s +[036/101] target-34.wav 1.000s +[037/101] target-35.wav 1.000s +[038/101] target-36.wav 1.000s +[039/101] target-37.wav 1.000s +[040/101] target-38.wav 1.000s +[041/101] target-39.wav 1.000s +[042/101] target-40.wav 1.000s +[043/101] target-41.wav 1.000s +[044/101] target-42.wav 1.000s +[045/101] target-43.wav 1.000s +[046/101] target-44.wav 1.000s +[047/101] target-45.wav 1.000s +[048/101] target-46.wav 1.000s +[049/101] target-47.wav 1.000s +[050/101] target-48.wav 1.000s +[051/101] target-49.wav 1.000s +[052/101] target-50.wav 1.000s +[053/101] target-51.wav 1.000s +[054/101] target-52.wav 1.000s +[055/101] target-53.wav 1.000s +[056/101] target-54.wav 1.000s +[057/101] target-55.wav 1.000s +[058/101] target-56.wav 1.000s +[059/101] target-57.wav 1.000s +[060/101] target-58.wav 1.000s +[061/101] target-59.wav 1.000s +[062/101] target-60.wav 1.000s +[063/101] target-61.wav 1.000s +[064/101] target-62.wav 1.000s +[065/101] target-63.wav 1.000s +[066/101] target-64.wav 1.000s +[067/101] target-65.wav 1.000s +[068/101] target-66.wav 1.000s +[069/101] target-67.wav 1.000s +[070/101] target-68.wav 1.000s +[071/101] target-69.wav 1.000s +[072/101] target-70.wav 1.000s +[073/101] target-71.wav 1.000s +[074/101] target-72.wav 1.000s +[075/101] target-73.wav 1.000s +[076/101] target-74.wav 1.000s +[077/101] target-75.wav 1.000s +[078/101] target-76.wav 1.000s +[079/101] target-77.wav 1.000s +[080/101] target-78.wav 1.000s +[081/101] target-79.wav 1.000s +[082/101] target-80.wav 1.000s +[083/101] target-81.wav 1.000s +[084/101] target-82.wav 1.000s +[085/101] target-83.wav 1.000s +[086/101] target-84.wav 1.000s +[087/101] target-85.wav 1.000s +[088/101] target-86.wav 1.000s +[089/101] target-87.wav 1.000s +[090/101] target-88.wav 1.000s +[091/101] target-89.wav 1.000s +[092/101] target-90.wav 1.000s +[093/101] target-91.wav 1.000s +[094/101] target-92.wav 1.000s +[095/101] target-93.wav 1.000s +[096/101] target-94.wav 1.000s +[097/101] target-95.wav 1.000s +[098/101] target-96.wav 1.000s +[099/101] target-97.wav 1.000s +[100/101] target-98.wav 1.000s +[101/101] target-99.wav 1.000s +ok +test_atomic_exchange_keeps_old_tree_at_staging_path (test_neural_audio.GeneratorContract.test_atomic_exchange_keeps_old_tree_at_staging_path) ... ok +test_atomic_first_publication (test_neural_audio.GeneratorContract.test_atomic_first_publication) ... ok +test_cli_import_has_no_generation_or_filesystem_side_effects (test_neural_audio.GeneratorContract.test_cli_import_has_no_generation_or_filesystem_side_effects) ... ok +test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell (test_neural_audio.GeneratorContract.test_edge_adapter_uses_pinned_python_prosody_timeout_and_no_shell) ... ok +test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets (test_neural_audio.GeneratorContract.test_exhausted_provider_failure_preserves_existing_files_and_redacts_secrets) ... ok +test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24 (test_neural_audio.GeneratorContract.test_ffmpeg_adapter_actually_decodes_mp3_to_normalized_pcm24) ... ok +test_ffmpeg_failure_is_not_retried_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_ffmpeg_failure_is_not_retried_and_preserves_existing_files) ... ok +test_full_validation_detects_changed_earlier_file_before_publication (test_neural_audio.GeneratorContract.test_full_validation_detects_changed_earlier_file_before_publication) ... [001/101] intro.wav 1.000s +[002/101] target-0.wav 1.000s +[003/101] target-1.wav 1.000s +[004/101] target-2.wav 1.000s +[005/101] target-3.wav 1.000s +[006/101] target-4.wav 1.000s +[007/101] target-5.wav 1.000s +[008/101] target-6.wav 1.000s +[009/101] target-7.wav 1.000s +[010/101] target-8.wav 1.000s +[011/101] target-9.wav 1.000s +[012/101] target-10.wav 1.000s +[013/101] target-11.wav 1.000s +[014/101] target-12.wav 1.000s +[015/101] target-13.wav 1.000s +[016/101] target-14.wav 1.000s +[017/101] target-15.wav 1.000s +[018/101] target-16.wav 1.000s +[019/101] target-17.wav 1.000s +[020/101] target-18.wav 1.000s +[021/101] target-19.wav 1.000s +[022/101] target-20.wav 1.000s +[023/101] target-21.wav 1.000s +[024/101] target-22.wav 1.000s +[025/101] target-23.wav 1.000s +[026/101] target-24.wav 1.000s +[027/101] target-25.wav 1.000s +[028/101] target-26.wav 1.000s +[029/101] target-27.wav 1.000s +[030/101] target-28.wav 1.000s +[031/101] target-29.wav 1.000s +[032/101] target-30.wav 1.000s +[033/101] target-31.wav 1.000s +[034/101] target-32.wav 1.000s +[035/101] target-33.wav 1.000s +[036/101] target-34.wav 1.000s +[037/101] target-35.wav 1.000s +[038/101] target-36.wav 1.000s +[039/101] target-37.wav 1.000s +[040/101] target-38.wav 1.000s +[041/101] target-39.wav 1.000s +[042/101] target-40.wav 1.000s +[043/101] target-41.wav 1.000s +[044/101] target-42.wav 1.000s +[045/101] target-43.wav 1.000s +[046/101] target-44.wav 1.000s +[047/101] target-45.wav 1.000s +[048/101] target-46.wav 1.000s +[049/101] target-47.wav 1.000s +[050/101] target-48.wav 1.000s +[051/101] target-49.wav 1.000s +[052/101] target-50.wav 1.000s +[053/101] target-51.wav 1.000s +[054/101] target-52.wav 1.000s +[055/101] target-53.wav 1.000s +[056/101] target-54.wav 1.000s +[057/101] target-55.wav 1.000s +[058/101] target-56.wav 1.000s +[059/101] target-57.wav 1.000s +[060/101] target-58.wav 1.000s +[061/101] target-59.wav 1.000s +[062/101] target-60.wav 1.000s +[063/101] target-61.wav 1.000s +[064/101] target-62.wav 1.000s +[065/101] target-63.wav 1.000s +[066/101] target-64.wav 1.000s +[067/101] target-65.wav 1.000s +[068/101] target-66.wav 1.000s +[069/101] target-67.wav 1.000s +[070/101] target-68.wav 1.000s +[071/101] target-69.wav 1.000s +[072/101] target-70.wav 1.000s +[073/101] target-71.wav 1.000s +[074/101] target-72.wav 1.000s +[075/101] target-73.wav 1.000s +[076/101] target-74.wav 1.000s +[077/101] target-75.wav 1.000s +[078/101] target-76.wav 1.000s +[079/101] target-77.wav 1.000s +[080/101] target-78.wav 1.000s +[081/101] target-79.wav 1.000s +[082/101] target-80.wav 1.000s +[083/101] target-81.wav 1.000s +[084/101] target-82.wav 1.000s +[085/101] target-83.wav 1.000s +[086/101] target-84.wav 1.000s +[087/101] target-85.wav 1.000s +[088/101] target-86.wav 1.000s +[089/101] target-87.wav 1.000s +[090/101] target-88.wav 1.000s +[091/101] target-89.wav 1.000s +[092/101] target-90.wav 1.000s +[093/101] target-91.wav 1.000s +[094/101] target-92.wav 1.000s +[095/101] target-93.wav 1.000s +[096/101] target-94.wav 1.000s +[097/101] target-95.wav 1.000s +[098/101] target-96.wav 1.000s +[099/101] target-97.wav 1.000s +[100/101] target-98.wav 1.000s +[101/101] target-99.wav 1.000s +ok +test_interruption_preserves_existing_files_and_cleans_temporary_data (test_neural_audio.GeneratorContract.test_interruption_preserves_existing_files_and_cleans_temporary_data) ... ok +test_invalid_pcm_never_reaches_publication (test_neural_audio.GeneratorContract.test_invalid_pcm_never_reaches_publication) ... ok +test_publication_failure_preserves_existing_files (test_neural_audio.GeneratorContract.test_publication_failure_preserves_existing_files) ... [001/101] intro.wav 1.000s +[002/101] target-0.wav 1.000s +[003/101] target-1.wav 1.000s +[004/101] target-2.wav 1.000s +[005/101] target-3.wav 1.000s +[006/101] target-4.wav 1.000s +[007/101] target-5.wav 1.000s +[008/101] target-6.wav 1.000s +[009/101] target-7.wav 1.000s +[010/101] target-8.wav 1.000s +[011/101] target-9.wav 1.000s +[012/101] target-10.wav 1.000s +[013/101] target-11.wav 1.000s +[014/101] target-12.wav 1.000s +[015/101] target-13.wav 1.000s +[016/101] target-14.wav 1.000s +[017/101] target-15.wav 1.000s +[018/101] target-16.wav 1.000s +[019/101] target-17.wav 1.000s +[020/101] target-18.wav 1.000s +[021/101] target-19.wav 1.000s +[022/101] target-20.wav 1.000s +[023/101] target-21.wav 1.000s +[024/101] target-22.wav 1.000s +[025/101] target-23.wav 1.000s +[026/101] target-24.wav 1.000s +[027/101] target-25.wav 1.000s +[028/101] target-26.wav 1.000s +[029/101] target-27.wav 1.000s +[030/101] target-28.wav 1.000s +[031/101] target-29.wav 1.000s +[032/101] target-30.wav 1.000s +[033/101] target-31.wav 1.000s +[034/101] target-32.wav 1.000s +[035/101] target-33.wav 1.000s +[036/101] target-34.wav 1.000s +[037/101] target-35.wav 1.000s +[038/101] target-36.wav 1.000s +[039/101] target-37.wav 1.000s +[040/101] target-38.wav 1.000s +[041/101] target-39.wav 1.000s +[042/101] target-40.wav 1.000s +[043/101] target-41.wav 1.000s +[044/101] target-42.wav 1.000s +[045/101] target-43.wav 1.000s +[046/101] target-44.wav 1.000s +[047/101] target-45.wav 1.000s +[048/101] target-46.wav 1.000s +[049/101] target-47.wav 1.000s +[050/101] target-48.wav 1.000s +[051/101] target-49.wav 1.000s +[052/101] target-50.wav 1.000s +[053/101] target-51.wav 1.000s +[054/101] target-52.wav 1.000s +[055/101] target-53.wav 1.000s +[056/101] target-54.wav 1.000s +[057/101] target-55.wav 1.000s +[058/101] target-56.wav 1.000s +[059/101] target-57.wav 1.000s +[060/101] target-58.wav 1.000s +[061/101] target-59.wav 1.000s +[062/101] target-60.wav 1.000s +[063/101] target-61.wav 1.000s +[064/101] target-62.wav 1.000s +[065/101] target-63.wav 1.000s +[066/101] target-64.wav 1.000s +[067/101] target-65.wav 1.000s +[068/101] target-66.wav 1.000s +[069/101] target-67.wav 1.000s +[070/101] target-68.wav 1.000s +[071/101] target-69.wav 1.000s +[072/101] target-70.wav 1.000s +[073/101] target-71.wav 1.000s +[074/101] target-72.wav 1.000s +[075/101] target-73.wav 1.000s +[076/101] target-74.wav 1.000s +[077/101] target-75.wav 1.000s +[078/101] target-76.wav 1.000s +[079/101] target-77.wav 1.000s +[080/101] target-78.wav 1.000s +[081/101] target-79.wav 1.000s +[082/101] target-80.wav 1.000s +[083/101] target-81.wav 1.000s +[084/101] target-82.wav 1.000s +[085/101] target-83.wav 1.000s +[086/101] target-84.wav 1.000s +[087/101] target-85.wav 1.000s +[088/101] target-86.wav 1.000s +[089/101] target-87.wav 1.000s +[090/101] target-88.wav 1.000s +[091/101] target-89.wav 1.000s +[092/101] target-90.wav 1.000s +[093/101] target-91.wav 1.000s +[094/101] target-92.wav 1.000s +[095/101] target-93.wav 1.000s +[096/101] target-94.wav 1.000s +[097/101] target-95.wav 1.000s +[098/101] target-96.wav 1.000s +[099/101] target-97.wav 1.000s +[100/101] target-98.wav 1.000s +[101/101] target-99.wav 1.000s +ok +test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests (test_neural_audio.GeneratorContract.test_rejects_incomplete_or_duplicate_or_unsafe_inventory_before_requests) ... ok +test_timeout_is_bounded_and_preserves_existing_files (test_neural_audio.GeneratorContract.test_timeout_is_bounded_and_preserves_existing_files) ... intro.wav: provider 시도 1/3 실패 +intro.wav: provider 시도 2/3 실패 +intro.wav: provider 시도 3/3 실패 +ok +test_transient_provider_failures_retry_cleanly_with_bounded_backoff (test_neural_audio.GeneratorContract.test_transient_provider_failures_retry_cleanly_with_bounded_backoff) ... intro.wav: provider 시도 1/3 실패 +intro.wav: provider 시도 2/3 실패 +[001/101] intro.wav 1.000s +[002/101] target-0.wav 1.000s +[003/101] target-1.wav 1.000s +[004/101] target-2.wav 1.000s +[005/101] target-3.wav 1.000s +[006/101] target-4.wav 1.000s +[007/101] target-5.wav 1.000s +[008/101] target-6.wav 1.000s +[009/101] target-7.wav 1.000s +[010/101] target-8.wav 1.000s +[011/101] target-9.wav 1.000s +[012/101] target-10.wav 1.000s +[013/101] target-11.wav 1.000s +[014/101] target-12.wav 1.000s +[015/101] target-13.wav 1.000s +[016/101] target-14.wav 1.000s +[017/101] target-15.wav 1.000s +[018/101] target-16.wav 1.000s +[019/101] target-17.wav 1.000s +[020/101] target-18.wav 1.000s +[021/101] target-19.wav 1.000s +[022/101] target-20.wav 1.000s +[023/101] target-21.wav 1.000s +[024/101] target-22.wav 1.000s +[025/101] target-23.wav 1.000s +[026/101] target-24.wav 1.000s +[027/101] target-25.wav 1.000s +[028/101] target-26.wav 1.000s +[029/101] target-27.wav 1.000s +[030/101] target-28.wav 1.000s +[031/101] target-29.wav 1.000s +[032/101] target-30.wav 1.000s +[033/101] target-31.wav 1.000s +[034/101] target-32.wav 1.000s +[035/101] target-33.wav 1.000s +[036/101] target-34.wav 1.000s +[037/101] target-35.wav 1.000s +[038/101] target-36.wav 1.000s +[039/101] target-37.wav 1.000s +[040/101] target-38.wav 1.000s +[041/101] target-39.wav 1.000s +[042/101] target-40.wav 1.000s +[043/101] target-41.wav 1.000s +[044/101] target-42.wav 1.000s +[045/101] target-43.wav 1.000s +[046/101] target-44.wav 1.000s +[047/101] target-45.wav 1.000s +[048/101] target-46.wav 1.000s +[049/101] target-47.wav 1.000s +[050/101] target-48.wav 1.000s +[051/101] target-49.wav 1.000s +[052/101] target-50.wav 1.000s +[053/101] target-51.wav 1.000s +[054/101] target-52.wav 1.000s +[055/101] target-53.wav 1.000s +[056/101] target-54.wav 1.000s +[057/101] target-55.wav 1.000s +[058/101] target-56.wav 1.000s +[059/101] target-57.wav 1.000s +[060/101] target-58.wav 1.000s +[061/101] target-59.wav 1.000s +[062/101] target-60.wav 1.000s +[063/101] target-61.wav 1.000s +[064/101] target-62.wav 1.000s +[065/101] target-63.wav 1.000s +[066/101] target-64.wav 1.000s +[067/101] target-65.wav 1.000s +[068/101] target-66.wav 1.000s +[069/101] target-67.wav 1.000s +[070/101] target-68.wav 1.000s +[071/101] target-69.wav 1.000s +[072/101] target-70.wav 1.000s +[073/101] target-71.wav 1.000s +[074/101] target-72.wav 1.000s +[075/101] target-73.wav 1.000s +[076/101] target-74.wav 1.000s +[077/101] target-75.wav 1.000s +[078/101] target-76.wav 1.000s +[079/101] target-77.wav 1.000s +[080/101] target-78.wav 1.000s +[081/101] target-79.wav 1.000s +[082/101] target-80.wav 1.000s +[083/101] target-81.wav 1.000s +[084/101] target-82.wav 1.000s +[085/101] target-83.wav 1.000s +[086/101] target-84.wav 1.000s +[087/101] target-85.wav 1.000s +[088/101] target-86.wav 1.000s +[089/101] target-87.wav 1.000s +[090/101] target-88.wav 1.000s +[091/101] target-89.wav 1.000s +[092/101] target-90.wav 1.000s +[093/101] target-91.wav 1.000s +[094/101] target-92.wav 1.000s +[095/101] target-93.wav 1.000s +[096/101] target-94.wav 1.000s +[097/101] target-95.wav 1.000s +[098/101] target-96.wav 1.000s +[099/101] target-97.wav 1.000s +[100/101] target-98.wav 1.000s +[101/101] target-99.wav 1.000s +ok +test_wrong_edge_version_fails_before_network_or_asset_changes (test_neural_audio.GeneratorContract.test_wrong_edge_version_fails_before_network_or_asset_changes) ... ok +test_rejects_silence_clipping_bad_format_and_abnormal_duration (test_neural_audio.PCMContract.test_rejects_silence_clipping_bad_format_and_abnormal_duration) ... ok +test_valid_pcm_returns_measured_hash_bytes_duration_and_levels (test_neural_audio.PCMContract.test_valid_pcm_returns_measured_hash_bytes_duration_and_levels) ... ok + +---------------------------------------------------------------------- +Ran 18 tests in 2.699s + +OK diff --git a/.agent/evidence-v0.1.1/06-audio-generate.log b/.agent/evidence-v0.1.1/06-audio-generate.log new file mode 100644 index 0000000..75cca1f --- /dev/null +++ b/.agent/evidence-v0.1.1/06-audio-generate.log @@ -0,0 +1,106 @@ + +> find-word@0.1.1 audio:generate +> .venv-audio/bin/python scripts/generate_audio.py + +[001/101] intro.wav 7.056s +[002/101] luck-apple.wav 3.936s +[003/101] luck-sea.wav 3.984s +[004/101] luck-tree.wav 3.816s +[005/101] luck-hat.wav 3.984s +[006/101] luck-umbrella.wav 4.008s +[007/101] luck-market.wav 4.056s +[008/101] luck-oil.wav 3.960s +[009/101] luck-sound.wav 3.840s +[010/101] luck-meat.wav 3.912s +[011/101] luck-furniture.wav 3.888s +[012/101] password-apple.wav 4.896s +[013/101] password-sea.wav 4.848s +[014/101] password-tree.wav 4.872s +[015/101] password-hat.wav 4.896s +[016/101] password-umbrella.wav 4.992s +[017/101] password-market.wav 4.800s +[018/101] password-oil.wav 5.016s +[019/101] password-sound.wav 4.920s +[020/101] password-meat.wav 5.016s +[021/101] password-furniture.wav 4.872s +[022/101] remember-apple.wav 8.232s +[023/101] remember-sea.wav 8.328s +[024/101] remember-tree.wav 8.520s +[025/101] remember-hat.wav 8.376s +[026/101] remember-umbrella.wav 8.400s +[027/101] remember-market.wav 8.304s +[028/101] remember-oil.wav 8.328s +[029/101] remember-sound.wav 8.208s +[030/101] remember-meat.wav 8.232s +[031/101] remember-furniture.wav 8.400s +[032/101] last-apple.wav 7.416s +[033/101] last-sea.wav 7.200s +[034/101] last-tree.wav 7.968s +[035/101] last-hat.wav 7.392s +[036/101] last-umbrella.wav 7.728s +[037/101] last-market.wav 7.488s +[038/101] last-oil.wav 7.032s +[039/101] last-sound.wav 7.392s +[040/101] last-meat.wav 7.224s +[041/101] last-furniture.wav 7.440s +[042/101] first-apple.wav 7.488s +[043/101] first-sea.wav 7.272s +[044/101] first-tree.wav 7.896s +[045/101] first-hat.wav 7.440s +[046/101] first-umbrella.wav 7.632s +[047/101] first-market.wav 7.488s +[048/101] first-oil.wav 6.984s +[049/101] first-sound.wav 7.368s +[050/101] first-meat.wav 7.200s +[051/101] first-furniture.wav 7.584s +[052/101] repeat-apple.wav 8.280s +[053/101] repeat-sea.wav 7.968s +[054/101] repeat-tree.wav 8.832s +[055/101] repeat-hat.wav 8.256s +[056/101] repeat-umbrella.wav 8.544s +[057/101] repeat-market.wav 8.376s +[058/101] repeat-oil.wav 7.704s +[059/101] repeat-sound.wav 8.232s +[060/101] repeat-meat.wav 7.920s +[061/101] repeat-furniture.wav 8.280s +[062/101] correction-apple.wav 8.376s +[063/101] correction-sea.wav 8.304s +[064/101] correction-tree.wav 8.352s +[065/101] correction-hat.wav 8.304s +[066/101] correction-umbrella.wav 8.472s +[067/101] correction-market.wav 8.448s +[068/101] correction-oil.wav 8.472s +[069/101] correction-sound.wav 8.280s +[070/101] correction-meat.wav 8.400s +[071/101] correction-furniture.wav 8.304s +[072/101] middle-apple.wav 7.512s +[073/101] middle-sea.wav 7.224s +[074/101] middle-tree.wav 7.968s +[075/101] middle-hat.wav 7.368s +[076/101] middle-umbrella.wav 7.680s +[077/101] middle-market.wav 7.416s +[078/101] middle-oil.wav 6.960s +[079/101] middle-sound.wav 7.368s +[080/101] middle-meat.wav 7.128s +[081/101] middle-furniture.wav 7.392s +[082/101] message-apple.wav 10.776s +[083/101] message-sea.wav 10.776s +[084/101] message-tree.wav 11.256s +[085/101] message-hat.wav 10.800s +[086/101] message-umbrella.wav 10.896s +[087/101] message-market.wav 10.896s +[088/101] message-oil.wav 10.608s +[089/101] message-sound.wav 10.872s +[090/101] message-meat.wav 10.680s +[091/101] message-furniture.wav 10.872s +[092/101] mission-apple.wav 5.136s +[093/101] mission-sea.wav 5.064s +[094/101] mission-tree.wav 5.040s +[095/101] mission-hat.wav 5.112s +[096/101] mission-umbrella.wav 5.160s +[097/101] mission-market.wav 5.184s +[098/101] mission-oil.wav 5.040s +[099/101] mission-sound.wav 5.016s +[100/101] mission-meat.wav 5.088s +[101/101] mission-furniture.wav 5.040s +101개 신규 WAV 원자 교체 완료, 합계 34,898,524 바이트 diff --git a/.agent/evidence-v0.1.1/07-format.log b/.agent/evidence-v0.1.1/07-format.log new file mode 100644 index 0000000..d29ca14 --- /dev/null +++ b/.agent/evidence-v0.1.1/07-format.log @@ -0,0 +1,7 @@ +tests/assets.test.ts 130ms +tests/neural-release.test.ts 12ms +tests/generator.test.ts 5ms +tests/App.test.tsx 91ms (unchanged) +tests/browser/game.spec.ts 30ms +tests/browser/ui.spec.ts 19ms +eslint.config.js 21ms (unchanged) diff --git a/.agent/evidence-v0.1.1/08-version-docs-green.log b/.agent/evidence-v0.1.1/08-version-docs-green.log new file mode 100644 index 0000000..46f874c --- /dev/null +++ b/.agent/evidence-v0.1.1/08-version-docs-green.log @@ -0,0 +1,12 @@ + +> find-word@0.1.1 test +> vitest run tests/neural-release.test.ts tests/App.test.tsx -t 0.1.1|브랜드|고정 venv + + + RUN v5.0.4 /home/hermes/worktrees/find-word-v0.1.1 + + + Test Files 2 passed (2) + Tests 3 passed | 12 skipped (15) + Start at 06:52:28 + Duration 1.30s (environment 47%, tests 20%, setup 20%, transform 8%, import 4%, worker 1%) diff --git a/.agent/evidence-v0.1.1/09-lint.log b/.agent/evidence-v0.1.1/09-lint.log new file mode 100644 index 0000000..bc96f27 --- /dev/null +++ b/.agent/evidence-v0.1.1/09-lint.log @@ -0,0 +1,3 @@ + +> find-word@0.1.1 lint +> eslint . && tsc --noEmit diff --git a/.agent/evidence-v0.1.1/10-audit.log b/.agent/evidence-v0.1.1/10-audit.log new file mode 100644 index 0000000..f372dee --- /dev/null +++ b/.agent/evidence-v0.1.1/10-audit.log @@ -0,0 +1 @@ +found 0 vulnerabilities diff --git a/.agent/evidence-v0.1.1/11-full-test.log b/.agent/evidence-v0.1.1/11-full-test.log new file mode 100644 index 0000000..be3ea27 --- /dev/null +++ b/.agent/evidence-v0.1.1/11-full-test.log @@ -0,0 +1,15 @@ + +> find-word@0.1.1 test +> vitest run + + + RUN v5.0.4 /home/hermes/worktrees/find-word-v0.1.1 + + + Test Files 11 passed (11) + Tests 60 passed (60) + Start at 06:56:13 + Duration 13.30s (tests 64%, environment 24%, setup 9%, transform 1%, import 1%, worker 1%) + + Isolate 11 workers spawned · ~999ms startup each (spawn + environment, per file) + at least ~2.66s faster with isolate: false — reuses workers across files instead of one per file diff --git a/.agent/evidence-v0.1.1/12-independent-audio.log b/.agent/evidence-v0.1.1/12-independent-audio.log new file mode 100644 index 0000000..4663f25 --- /dev/null +++ b/.agent/evidence-v0.1.1/12-independent-audio.log @@ -0,0 +1,120 @@ +PASS intro.wav: 7.056s, RMS=0.100000, peak=0.499847 +PASS luck-apple.wav: 3.936s, RMS=0.100000, peak=0.401642 +PASS luck-sea.wav: 3.984s, RMS=0.100000, peak=0.388702 +PASS luck-tree.wav: 3.816s, RMS=0.100000, peak=0.369202 +PASS luck-hat.wav: 3.984s, RMS=0.100000, peak=0.417084 +PASS luck-umbrella.wav: 4.008s, RMS=0.100000, peak=0.385651 +PASS luck-market.wav: 4.056s, RMS=0.100000, peak=0.402222 +PASS luck-oil.wav: 3.960s, RMS=0.100000, peak=0.381714 +PASS luck-sound.wav: 3.840s, RMS=0.100000, peak=0.386322 +PASS luck-meat.wav: 3.912s, RMS=0.100000, peak=0.371124 +PASS luck-furniture.wav: 3.888s, RMS=0.100000, peak=0.387054 +PASS password-apple.wav: 4.896s, RMS=0.100000, peak=0.483429 +PASS password-sea.wav: 4.848s, RMS=0.100000, peak=0.533905 +PASS password-tree.wav: 4.872s, RMS=0.100000, peak=0.490387 +PASS password-hat.wav: 4.896s, RMS=0.100000, peak=0.505890 +PASS password-umbrella.wav: 4.992s, RMS=0.100000, peak=0.477661 +PASS password-market.wav: 4.800s, RMS=0.100000, peak=0.481689 +PASS password-oil.wav: 5.016s, RMS=0.100000, peak=0.494507 +PASS password-sound.wav: 4.920s, RMS=0.100000, peak=0.462097 +PASS password-meat.wav: 5.016s, RMS=0.100000, peak=0.497864 +PASS password-furniture.wav: 4.872s, RMS=0.100000, peak=0.483643 +PASS remember-apple.wav: 8.232s, RMS=0.100000, peak=0.563568 +PASS remember-sea.wav: 8.328s, RMS=0.100000, peak=0.546539 +PASS remember-tree.wav: 8.520s, RMS=0.100000, peak=0.548645 +PASS remember-hat.wav: 8.376s, RMS=0.100000, peak=0.541779 +PASS remember-umbrella.wav: 8.400s, RMS=0.100000, peak=0.557831 +PASS remember-market.wav: 8.304s, RMS=0.100000, peak=0.572815 +PASS remember-oil.wav: 8.328s, RMS=0.100000, peak=0.524384 +PASS remember-sound.wav: 8.208s, RMS=0.100000, peak=0.537781 +PASS remember-meat.wav: 8.232s, RMS=0.100000, peak=0.519287 +PASS remember-furniture.wav: 8.400s, RMS=0.100000, peak=0.561890 +PASS last-apple.wav: 7.416s, RMS=0.100000, peak=0.622528 +PASS last-sea.wav: 7.200s, RMS=0.100000, peak=0.591034 +PASS last-tree.wav: 7.968s, RMS=0.100000, peak=0.580750 +PASS last-hat.wav: 7.392s, RMS=0.100000, peak=0.550079 +PASS last-umbrella.wav: 7.728s, RMS=0.100000, peak=0.597656 +PASS last-market.wav: 7.488s, RMS=0.100000, peak=0.600555 +PASS last-oil.wav: 7.032s, RMS=0.100000, peak=0.557068 +PASS last-sound.wav: 7.392s, RMS=0.100000, peak=0.564087 +PASS last-meat.wav: 7.224s, RMS=0.100000, peak=0.537201 +PASS last-furniture.wav: 7.440s, RMS=0.100000, peak=0.583588 +PASS first-apple.wav: 7.488s, RMS=0.100000, peak=0.579773 +PASS first-sea.wav: 7.272s, RMS=0.100000, peak=0.571777 +PASS first-tree.wav: 7.896s, RMS=0.100000, peak=0.583344 +PASS first-hat.wav: 7.440s, RMS=0.100000, peak=0.531250 +PASS first-umbrella.wav: 7.632s, RMS=0.100000, peak=0.570007 +PASS first-market.wav: 7.488s, RMS=0.100000, peak=0.639069 +PASS first-oil.wav: 6.984s, RMS=0.100000, peak=0.534729 +PASS first-sound.wav: 7.368s, RMS=0.100000, peak=0.518951 +PASS first-meat.wav: 7.200s, RMS=0.100000, peak=0.513123 +PASS first-furniture.wav: 7.584s, RMS=0.100000, peak=0.570038 +PASS repeat-apple.wav: 8.280s, RMS=0.100000, peak=0.641449 +PASS repeat-sea.wav: 7.968s, RMS=0.100000, peak=0.684326 +PASS repeat-tree.wav: 8.832s, RMS=0.100000, peak=0.618744 +PASS repeat-hat.wav: 8.256s, RMS=0.100000, peak=0.616730 +PASS repeat-umbrella.wav: 8.544s, RMS=0.100000, peak=0.674835 +PASS repeat-market.wav: 8.376s, RMS=0.100000, peak=0.682251 +PASS repeat-oil.wav: 7.704s, RMS=0.100000, peak=0.623962 +PASS repeat-sound.wav: 8.232s, RMS=0.100000, peak=0.611450 +PASS repeat-meat.wav: 7.920s, RMS=0.100000, peak=0.592102 +PASS repeat-furniture.wav: 8.280s, RMS=0.100000, peak=0.667023 +PASS correction-apple.wav: 8.376s, RMS=0.100000, peak=0.490326 +PASS correction-sea.wav: 8.304s, RMS=0.100000, peak=0.496155 +PASS correction-tree.wav: 8.352s, RMS=0.100000, peak=0.470612 +PASS correction-hat.wav: 8.304s, RMS=0.100000, peak=0.470978 +PASS correction-umbrella.wav: 8.472s, RMS=0.100000, peak=0.479034 +PASS correction-market.wav: 8.448s, RMS=0.100000, peak=0.491486 +PASS correction-oil.wav: 8.472s, RMS=0.100000, peak=0.470795 +PASS correction-sound.wav: 8.280s, RMS=0.100000, peak=0.469269 +PASS correction-meat.wav: 8.400s, RMS=0.100000, peak=0.481537 +PASS correction-furniture.wav: 8.304s, RMS=0.100000, peak=0.487396 +PASS middle-apple.wav: 7.512s, RMS=0.100000, peak=0.553558 +PASS middle-sea.wav: 7.224s, RMS=0.100000, peak=0.564941 +PASS middle-tree.wav: 7.968s, RMS=0.100000, peak=0.600006 +PASS middle-hat.wav: 7.368s, RMS=0.100000, peak=0.555206 +PASS middle-umbrella.wav: 7.680s, RMS=0.100000, peak=0.569550 +PASS middle-market.wav: 7.416s, RMS=0.100000, peak=0.594971 +PASS middle-oil.wav: 6.960s, RMS=0.100000, peak=0.539917 +PASS middle-sound.wav: 7.368s, RMS=0.100000, peak=0.534821 +PASS middle-meat.wav: 7.128s, RMS=0.100000, peak=0.510834 +PASS middle-furniture.wav: 7.392s, RMS=0.100000, peak=0.557617 +PASS message-apple.wav: 10.776s, RMS=0.100000, peak=0.525360 +PASS message-sea.wav: 10.776s, RMS=0.100000, peak=0.535217 +PASS message-tree.wav: 11.256s, RMS=0.100000, peak=0.526245 +PASS message-hat.wav: 10.800s, RMS=0.100000, peak=0.587006 +PASS message-umbrella.wav: 10.896s, RMS=0.100000, peak=0.530365 +PASS message-market.wav: 10.896s, RMS=0.100000, peak=0.534821 +PASS message-oil.wav: 10.608s, RMS=0.100000, peak=0.518127 +PASS message-sound.wav: 10.872s, RMS=0.100000, peak=0.519531 +PASS message-meat.wav: 10.680s, RMS=0.100000, peak=0.514771 +PASS message-furniture.wav: 10.872s, RMS=0.100000, peak=0.529327 +PASS mission-apple.wav: 5.136s, RMS=0.100000, peak=0.470154 +PASS mission-sea.wav: 5.064s, RMS=0.100000, peak=0.450134 +PASS mission-tree.wav: 5.040s, RMS=0.100000, peak=0.458527 +PASS mission-hat.wav: 5.112s, RMS=0.100000, peak=0.449829 +PASS mission-umbrella.wav: 5.160s, RMS=0.100000, peak=0.465332 +PASS mission-market.wav: 5.184s, RMS=0.100000, peak=0.485687 +PASS mission-oil.wav: 5.040s, RMS=0.100000, peak=0.443573 +PASS mission-sound.wav: 5.016s, RMS=0.100000, peak=0.454803 +PASS mission-meat.wav: 5.088s, RMS=0.100000, peak=0.441864 +PASS mission-furniture.wav: 5.040s, RMS=0.100000, peak=0.485229 +{ + "count": 101, + "newVsV01": 101, + "uniqueHashes": 101, + "bytes": 34898524, + "durationRange": [ + 3.816, + 11.256 + ], + "rmsRange": [ + 0.09999985338851038, + 0.10000004859108667 + ], + "peakRange": [ + 0.36920166015625, + 0.684326171875 + ], + "historicalUnchanged": 115 +} diff --git a/.agent/evidence-v0.1.1/13-build.log b/.agent/evidence-v0.1.1/13-build.log new file mode 100644 index 0000000..1b6ace5 --- /dev/null +++ b/.agent/evidence-v0.1.1/13-build.log @@ -0,0 +1,15 @@ + +> find-word@0.1.1 build +> tsc --noEmit && vite build && node scripts/check-build.mjs + +vite v8.3.4 building client environment for production... +transforming... +✓ 20 modules transformed. +rendering chunks... +computing gzip size... +dist/index.html 0.89 kB │ gzip: 0.63 kB +dist/assets/index-KT6VDfPn.css 15.25 kB │ gzip: 4.39 kB +dist/assets/index-CorsjeLi.js 245.34 kB │ gzip: 77.26 kB + +✓ built in 405ms +빌드 음성 자산 101개 및 런타임 notice 전문 확인 diff --git a/.agent/evidence-v0.1.1/14-browser.log b/.agent/evidence-v0.1.1/14-browser.log new file mode 100644 index 0000000..c3bb779 --- /dev/null +++ b/.agent/evidence-v0.1.1/14-browser.log @@ -0,0 +1,77 @@ + +> find-word@0.1.1 test:browser +> playwright test + +[WebServer] (node:789414) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +[WebServer] (Use `node --trace-warnings ...` to show where the warning was created) + +Running 12 tests using 2 workers + +(node:789465) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use `node --trace-warnings ...` to show where the warning was created) +(node:789464) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use `node --trace-warnings ...` to show where the warning was created) + pw:browser /usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-usRFeJ --remote-debugging-pipe --no-startup-window +0ms + pw:browser /usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-tgmIGX --remote-debugging-pipe --no-startup-window +0ms + pw:browser pid=789488 +15ms + pw:browser pid=789490 +14ms + pw:browser [pid=789488][err] [789574:789574:1014/065656.619803:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +253ms + pw:browser [pid=789490][err] [789572:789572:1014/065656.641388:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +270ms + pw:browser [pid=789488][err] [789657:7:1014/065657.003307:ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:287] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +394ms + pw:browser [pid=789490][err] [789697:789697:1014/065657.581581:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +937ms + pw:browser [pid=789488][err] [789721:789721:1014/065659.123843:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +2s + pw:browser [pid=789490][err] [789490:789531:1014/065701.312194:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +4s + pw:browser [pid=789488][err] [789488:789553:1014/065702.399765:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +3s + ✓ 2 [desktop] › tests/browser/game.spec.ts:114:1 › 오디오 HTTP 실패를 화면에 표시하고 재시도한다 (14.7s) + ✓ 3 [desktop] › tests/browser/release.spec.ts:6:1 › 릴리스는 사용자 크레딧과 런타임 MIT notice 전문을 제공한다 (309ms) + ✓ 4 [desktop] › tests/browser/ui.spec.ts:4:1 › 전체 음량은 설정 흐름 안에 들어가며 키보드로 조절된다 (1.6s) + ✓ 5 [desktop] › tests/browser/ui.spec.ts:52:1 › 난이도 설명은 모바일에서도 보인다 (1.0s) + pw:browser [pid=789490][err] [789490:789531:1014/065725.821501:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +25s + pw:browser [pid=789488][err] [789488:789553:1014/065728.796251:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: QUOTA_EXCEEDED +26s + ✓ 6 [desktop] › tests/browser/ui.spec.ts:68:1 › 한국어 제목은 단어 중간에서 줄바꿈하지 않는다 (16.7s) + pw:browser [pid=789490] +5s + pw:browser [pid=789490] +53ms + pw:browser [pid=789490] starting temporary directories cleanup +0ms + pw:browser [pid=789490] finished temporary directories cleanup +9ms + pw:browser [pid=789490] +0ms +(node:789845) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use `node --trace-warnings ...` to show where the warning was created) + pw:browser /usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-WzJ5B4 --remote-debugging-pipe --no-startup-window +0ms + pw:browser pid=789857 +13ms + pw:browser [pid=789857][err] [789900:789900:1014/065731.887141:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +138ms + pw:browser [pid=789857][err] [789972:789972:1014/065733.402085:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +2s + pw:browser [pid=789857][err] [789857:789885:1014/065736.698476:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +3s + pw:browser [pid=789857][err] [789857:789885:1014/065757.253598:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +21s + ✓ 1 [desktop] › tests/browser/game.spec.ts:7:1 › 실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 요청 없음 (1.6m) + pw:browser [pid=789488] +1m + pw:browser [pid=789488] +49ms + pw:browser [pid=789488] starting temporary directories cleanup +1ms + pw:browser [pid=789488] finished temporary directories cleanup +10ms + pw:browser [pid=789488] +0ms +(node:790149) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use `node --trace-warnings ...` to show where the warning was created) + pw:browser /usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-F4j9tO --remote-debugging-pipe --no-startup-window +0ms + pw:browser pid=790161 +5ms + pw:browser [pid=790161][err] [790203:790203:1014/065834.688015:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +137ms + pw:browser [pid=790161][err] [790242:7:1014/065834.832706:ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:287] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +143ms + pw:browser [pid=790161][err] [790262:790262:1014/065835.186112:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +352ms + pw:browser [pid=789857][err] [789857:789885:1014/065837.765817:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +41s + pw:browser [pid=790161][err] [790161:790189:1014/065838.519301:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: QUOTA_EXCEEDED +3s + ✓ 8 [mobile] › tests/browser/game.spec.ts:114:1 › 오디오 HTTP 실패를 화면에 표시하고 재시도한다 (16.9s) + ✓ 9 [mobile] › tests/browser/release.spec.ts:6:1 › 릴리스는 사용자 크레딧과 런타임 MIT notice 전문을 제공한다 (292ms) + ✓ 10 [mobile] › tests/browser/ui.spec.ts:4:1 › 전체 음량은 설정 흐름 안에 들어가며 키보드로 조절된다 (938ms) + ✓ 11 [mobile] › tests/browser/ui.spec.ts:52:1 › 난이도 설명은 모바일에서도 보인다 (921ms) + ✓ 12 [mobile] › tests/browser/ui.spec.ts:68:1 › 한국어 제목은 단어 중간에서 줄바꿈하지 않는다 (16.7s) + pw:browser [pid=790161] +32s + pw:browser [pid=790161] +51ms + pw:browser [pid=790161] starting temporary directories cleanup +0ms + pw:browser [pid=790161] finished temporary directories cleanup +9ms + pw:browser [pid=790161] +0ms + ✓ 7 [mobile] › tests/browser/game.spec.ts:7:1 › 실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 요청 없음 (1.7m) + pw:browser [pid=789857] +34s + pw:browser [pid=789857] +107ms + pw:browser [pid=789857] starting temporary directories cleanup +1ms + pw:browser [pid=789857] finished temporary directories cleanup +18ms + pw:browser [pid=789857] +1ms + + 12 passed (2.3m) diff --git a/.agent/evidence-v0.1.1/15-browser-audio.log b/.agent/evidence-v0.1.1/15-browser-audio.log new file mode 100644 index 0000000..c8c957f --- /dev/null +++ b/.agent/evidence-v0.1.1/15-browser-audio.log @@ -0,0 +1,21 @@ +2026-10-14T06:56:54.062Z pw:browser /usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-jPkOZ2 --remote-debugging-pipe --no-startup-window +2026-10-14T06:56:54.067Z pw:browser pid=789288 +2026-10-14T06:56:54.238Z pw:browser [pid=789288][err] [789341:789341:1014/065654.236997:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +2026-10-14T06:56:54.428Z pw:browser [pid=789288][err] [789379:7:1014/065654.427988:ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:287] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +SANDBOX STATUS +Sandbox Status +Layer 1 Sandbox Namespace +PID namespaces Yes +Network namespaces Yes +Seccomp-BPF sandbox Yes +Seccomp-BPF sandbox supports TSYNC Yes +Ptrace Protection with Yama LSM (Broker) Yes +Ptrace Protection with Yama LSM (Non-broker) No + +You are adequately sandboxed. +PASS: sandboxed Chrome decoded 101/101 WAVs at 24kHz mono; hashes, bytes, duration, RMS, peak verified; external requests=0, JS errors=0. +2026-10-14T06:56:57.008Z pw:browser [pid=789288] +2026-10-14T06:56:57.243Z pw:browser [pid=789288] +2026-10-14T06:56:57.243Z pw:browser [pid=789288] starting temporary directories cleanup +2026-10-14T06:56:57.285Z pw:browser [pid=789288] finished temporary directories cleanup +2026-10-14T06:56:57.286Z pw:browser [pid=789288] diff --git a/.agent/evidence-v0.1.1/16-final-lint.log b/.agent/evidence-v0.1.1/16-final-lint.log new file mode 100644 index 0000000..bc96f27 --- /dev/null +++ b/.agent/evidence-v0.1.1/16-final-lint.log @@ -0,0 +1,3 @@ + +> find-word@0.1.1 lint +> eslint . && tsc --noEmit diff --git a/.agent/evidence-v0.1.1/17-security-history.json b/.agent/evidence-v0.1.1/17-security-history.json new file mode 100644 index 0000000..2a85598 --- /dev/null +++ b/.agent/evidence-v0.1.1/17-security-history.json @@ -0,0 +1,9 @@ +{ + "historicalFilesUnchanged": 115, + "gitHeadUnchanged": "1c0069c367f6e108d3f67a2a0d044399dd0ef2bd", + "branch": "fix/v0.1.1-neural-voice", + "secretScanTextFiles": 40, + "secretMatches": 0, + "publicStageDirectories": 0, + "pythonVenvVersion": "7.3.1" +} diff --git a/.agent/evidence-v0.1.1/18-environment.log b/.agent/evidence-v0.1.1/18-environment.log new file mode 100644 index 0000000..38f72ea --- /dev/null +++ b/.agent/evidence-v0.1.1/18-environment.log @@ -0,0 +1,12 @@ +Linux-6.8.0-142-generic-x86_64-with-glibc2.39 +Generated at: 2026-10-14T06:55:41.751382+00:00 +$ node --version +v26.7.0 +$ npm --version +11.19.0 +$ .venv-audio/bin/python --version +Python 3.11.16 +$ .venv-audio/bin/python -m pip check +No broken requirements found. +$ google-chrome --version +Google Chrome 146.0.7680.80 diff --git a/.agent/evidence-v0.1.1/19-final-integrity.json b/.agent/evidence-v0.1.1/19-final-integrity.json new file mode 100644 index 0000000..f830465 --- /dev/null +++ b/.agent/evidence-v0.1.1/19-final-integrity.json @@ -0,0 +1,17 @@ +{ + "historicalFilesUnchanged": 115, + "gitHeadUnchanged": "1c0069c367f6e108d3f67a2a0d044399dd0ef2bd", + "branch": "fix/v0.1.1-neural-voice", + "chromeLaunchesWithSandbox": 5, + "browserTestsPassed": 12, + "browserRuntimeSandbox": [ + "Namespace", + "PID namespaces", + "Network namespaces", + "Seccomp-BPF" + ], + "documentLinksChecked": 56, + "gameMechanicsAndRuntimeAudioUnchanged": true, + "distContainsOnlyExpectedArtifacts": true, + "diffCheck": "PASS" +} diff --git a/.agent/evidence-v0.1.1/audio-integrity.json b/.agent/evidence-v0.1.1/audio-integrity.json new file mode 100644 index 0000000..3c680d1 --- /dev/null +++ b/.agent/evidence-v0.1.1/audio-integrity.json @@ -0,0 +1,1839 @@ +{ + "count": 101, + "newVsV01": 101, + "uniqueHashes": 101, + "bytes": 34898524, + "durationRange": [ + 3.816, + 11.256 + ], + "rmsRange": [ + 0.09999985338851038, + 0.10000004859108667 + ], + "peakRange": [ + 0.36920166015625, + 0.684326171875 + ], + "historicalUnchanged": 115, + "files": [ + { + "file": "intro.wav", + "sha256": "90d9f46e6e3f9c8a0c1a781096df54d7809ef4ad38b289508ef7b8c0411465ce", + "bytes": 338732, + "duration": 7.056, + "rms": 0.09999999766232884, + "peak": 0.499847412109375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.056000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-apple.wav", + "sha256": "7c8f413f4b7e164f090e508ba090448f1376c6012649bbb6a917d45851313883", + "bytes": 188972, + "duration": 3.936, + "rms": 0.10000000059246013, + "peak": 0.401641845703125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "3.936000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-sea.wav", + "sha256": "47aa31d757c9fb49c9b53a1c052d90a5ced4be57549d649850d9b48f8864045b", + "bytes": 191276, + "duration": 3.984, + "rms": 0.09999997583491328, + "peak": 0.388702392578125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "3.984000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-tree.wav", + "sha256": "122f496834fb16d6da47f801e50e46e272e4e934af959467d1db4e490f79be7a", + "bytes": 183212, + "duration": 3.816, + "rms": 0.1000000469252297, + "peak": 0.36920166015625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "3.816000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-hat.wav", + "sha256": "4ff43dd042f4b5451e9fdeb3edd59f1e389a7d1dd488094587a3a9fc7dfffa44", + "bytes": 191276, + "duration": 3.984, + "rms": 0.09999997899323479, + "peak": 0.417083740234375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "3.984000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-umbrella.wav", + "sha256": "8d3e8e8bc97e5fe3f3362184c4abb12f01a882a1f1174d2644f0f70aac677c0a", + "bytes": 192428, + "duration": 4.008, + "rms": 0.09999996793233332, + "peak": 0.385650634765625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.008000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-market.wav", + "sha256": "356af25332bf2000ed349671f3ff39767929e1f4599af622c021f907d11fb7bf", + "bytes": 194732, + "duration": 4.056, + "rms": 0.09999998683675566, + "peak": 0.4022216796875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.056000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-oil.wav", + "sha256": "3fa1e662a3af8f2726b17b2baa5bec2934406e6595ebc243c8380dd16e190977", + "bytes": 190124, + "duration": 3.96, + "rms": 0.09999999431198445, + "peak": 0.3817138671875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "3.960000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-sound.wav", + "sha256": "34e4d9758d2ed448646fda364610e34779c84794403d9051ab3d8c1d33e2c80c", + "bytes": 184364, + "duration": 3.84, + "rms": 0.10000003234489044, + "peak": 0.386322021484375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "3.840000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-meat.wav", + "sha256": "d36dfc395a5c53fbfa4abe9980db7bb59adfdac59e40318c4aca365365e3f2ab", + "bytes": 187820, + "duration": 3.912, + "rms": 0.0999999898733683, + "peak": 0.371124267578125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "3.912000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "luck-furniture.wav", + "sha256": "52cb92cdb8ee92b6f581c9d68dbc346f180c470564809ba57185429c9eb84127", + "bytes": 186668, + "duration": 3.888, + "rms": 0.10000004096194225, + "peak": 0.387054443359375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "3.888000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-apple.wav", + "sha256": "b6f5de3fe571fe3a7fdb722fe49250bcfeedef49475271e0ec76a7630907edd5", + "bytes": 235052, + "duration": 4.896, + "rms": 0.09999999690557956, + "peak": 0.483428955078125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.896000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-sea.wav", + "sha256": "03f45b8cb5b3dc72faf5c6e3d0e2a10cea6b60301ea400d0a4453596d2f0b337", + "bytes": 232748, + "duration": 4.848, + "rms": 0.09999999075635248, + "peak": 0.533905029296875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.848000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-tree.wav", + "sha256": "67da8147296e35134ec254bc82b58317630061b2c64d2d63c4be197ce8692d50", + "bytes": 233900, + "duration": 4.872, + "rms": 0.10000002225030123, + "peak": 0.490386962890625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.872000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-hat.wav", + "sha256": "e91bfc97df90a1675b6cd856880ac15c54de32cfc3b23603bdd1c2c5ac3ce6b8", + "bytes": 235052, + "duration": 4.896, + "rms": 0.10000000521661735, + "peak": 0.505889892578125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.896000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-umbrella.wav", + "sha256": "ab3ca6398c879971dc793eb25b472fcc0db72d76e386f11ef4d32c639f37c69d", + "bytes": 239660, + "duration": 4.992, + "rms": 0.10000000645104105, + "peak": 0.4776611328125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.992000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-market.wav", + "sha256": "02e325fb753ef7a3ee585a03d2ca2aa3f1fbc58523b062222ae1136eeed7a95c", + "bytes": 230444, + "duration": 4.8, + "rms": 0.10000002623625086, + "peak": 0.481689453125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.800000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-oil.wav", + "sha256": "b472957b27ff98ea7b8894fa1ff12653f7e9f66f77870faa9e52a1c1bf116c41", + "bytes": 240812, + "duration": 5.016, + "rms": 0.10000000204728039, + "peak": 0.4945068359375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.016000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-sound.wav", + "sha256": "044aafff08a19e39f21e58b963644715a987cd155896998769ef8e9ce6449628", + "bytes": 236204, + "duration": 4.92, + "rms": 0.10000000001020605, + "peak": 0.46209716796875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.920000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-meat.wav", + "sha256": "b009caadd56350d6a641ff17f0973e2b2724c183e1f45ae0bf3c483e237fa99e", + "bytes": 240812, + "duration": 5.016, + "rms": 0.10000000456206941, + "peak": 0.49786376953125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.016000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "password-furniture.wav", + "sha256": "fb2b631dd4e12120d08a84cce6450f9cd9cd68105d13d7001d808e7deaa1df4a", + "bytes": 233900, + "duration": 4.872, + "rms": 0.10000001899065697, + "peak": 0.483642578125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "4.872000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-apple.wav", + "sha256": "1bf2aef9a9f030af1a8fd7dee8f44aa420a397b3ba99b575feeb3dc7a63ec191", + "bytes": 395180, + "duration": 8.232, + "rms": 0.10000000668138131, + "peak": 0.563568115234375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.232000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-sea.wav", + "sha256": "5c91a36496e274d2be9c6fa8d9b506cd04f2a1b7ceb05ed1b7bb03fe7fca99d9", + "bytes": 399788, + "duration": 8.328, + "rms": 0.09999999180525658, + "peak": 0.546539306640625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.328000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-tree.wav", + "sha256": "bc8e54f5407e8866bf1ca1ddfd4f6c63d85fee29bd9e430c7dd33e87974fc5b8", + "bytes": 409004, + "duration": 8.52, + "rms": 0.09999998112848245, + "peak": 0.54864501953125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.520000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-hat.wav", + "sha256": "31a6a4270bf368cda419094d87a303ac3cd5c22539786cb165f8044b0b8e9d6e", + "bytes": 402092, + "duration": 8.376, + "rms": 0.09999997909968193, + "peak": 0.541778564453125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.376000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-umbrella.wav", + "sha256": "c8988e604ba05d52597e561389541e8f9cda86e052279f1862bbf8399ead314e", + "bytes": 403244, + "duration": 8.4, + "rms": 0.10000001748273872, + "peak": 0.557830810546875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.400000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-market.wav", + "sha256": "d89792da51c83ac49d27e55aa6d822a338899a5ee487b34b552748201e1522f6", + "bytes": 398636, + "duration": 8.304, + "rms": 0.09999999983200249, + "peak": 0.57281494140625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.304000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-oil.wav", + "sha256": "3ca98682ff06d1d6b8af02c0fd5b2124f42266ac989884223eff4a9bbde1287a", + "bytes": 399788, + "duration": 8.328, + "rms": 0.09999999241389293, + "peak": 0.524383544921875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.328000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-sound.wav", + "sha256": "98387ccc7aab566f6487d4c250b2128082cc527e013a6f7b7bc4d0266ac295df", + "bytes": 394028, + "duration": 8.208, + "rms": 0.10000002803973357, + "peak": 0.53778076171875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.208000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-meat.wav", + "sha256": "9183ef90b64f5ceaf3bb0178c689231ff3e9981d6ce889124a15f2a6e2b7a344", + "bytes": 395180, + "duration": 8.232, + "rms": 0.10000003134220614, + "peak": 0.519287109375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.232000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "remember-furniture.wav", + "sha256": "442bdcb956f120c270e1f321feae0415c897de0183e468980fcc119b898b2a67", + "bytes": 403244, + "duration": 8.4, + "rms": 0.10000001858461878, + "peak": 0.5618896484375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.400000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-apple.wav", + "sha256": "4cb2a8579fa3a7b91bb36363826ce8ca0d6d86e96e16a479e226a2802edd82f7", + "bytes": 356012, + "duration": 7.416, + "rms": 0.10000001764132661, + "peak": 0.622528076171875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.416000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-sea.wav", + "sha256": "f755452e1da44674aa1df7d12edffa988091b89a394e7eb6d4a00eb721f07cbf", + "bytes": 345644, + "duration": 7.2, + "rms": 0.09999998238433916, + "peak": 0.591033935546875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.200000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-tree.wav", + "sha256": "589e56d940a3c9e80ec02ae0b2dd93463fdc85ef019a753fcd0fded0752055f9", + "bytes": 382508, + "duration": 7.968, + "rms": 0.09999999432249608, + "peak": 0.58074951171875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.968000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-hat.wav", + "sha256": "bdaf8fbc54df8061e9e58aa8c14b3044a6e863dc54973ee3ee6ae2c5ae1a28f1", + "bytes": 354860, + "duration": 7.392, + "rms": 0.099999998595968, + "peak": 0.550079345703125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.392000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-umbrella.wav", + "sha256": "c801293a34013a8d93d3289a2cc87cd3eed83f97ba4647f5060efa00b117a9ad", + "bytes": 370988, + "duration": 7.728, + "rms": 0.09999998351512711, + "peak": 0.59765625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.728000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-market.wav", + "sha256": "7857e87e9dbfe8fe849cc0b18c1d45b60916cb337328099c7a87a42c82e2821e", + "bytes": 359468, + "duration": 7.488, + "rms": 0.09999999842960891, + "peak": 0.600555419921875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.488000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-oil.wav", + "sha256": "b5ce34950b8b31b1baf6ce55410797f0d8aa5478a670d6eace87cf76d134d6d3", + "bytes": 337580, + "duration": 7.032, + "rms": 0.10000002299391843, + "peak": 0.55706787109375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.032000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-sound.wav", + "sha256": "ec9c49802aebeb6a8b898a1c0d8ad1daa127d2d64c905c197ed49a8e0c4eb64a", + "bytes": 354860, + "duration": 7.392, + "rms": 0.10000004316064788, + "peak": 0.5640869140625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.392000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-meat.wav", + "sha256": "10f8ad4259ed8945c9da831e80b3902303c09caef96d400298599168fb1efed4", + "bytes": 346796, + "duration": 7.224, + "rms": 0.10000002519116054, + "peak": 0.537200927734375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.224000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "last-furniture.wav", + "sha256": "5ad4f776e35439d408abed0e66567d6b5cdc80fc54d579165472e474f1238776", + "bytes": 357164, + "duration": 7.44, + "rms": 0.10000003394798516, + "peak": 0.583587646484375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.440000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-apple.wav", + "sha256": "7c1dbcb3082055df2b8f087f7e906ec867c92f679de0894c34740c05e61a4bc1", + "bytes": 359468, + "duration": 7.488, + "rms": 0.1000000290826337, + "peak": 0.57977294921875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.488000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-sea.wav", + "sha256": "0f0b39314c5f7d7168d4854003c0c284e7bc3438cdc4d1d1c5adab7cb5ed30e5", + "bytes": 349100, + "duration": 7.272, + "rms": 0.10000001935816175, + "peak": 0.57177734375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.272000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-tree.wav", + "sha256": "3178230fd5850b6277a2153b438ec5289720e3db7ac929914cc213964f004bd5", + "bytes": 379052, + "duration": 7.896, + "rms": 0.09999998692384372, + "peak": 0.583343505859375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.896000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-hat.wav", + "sha256": "17c8cde90567e7574b0c0702663bd70713054089ee920241b5444d9b7869eef5", + "bytes": 357164, + "duration": 7.44, + "rms": 0.1000000080433648, + "peak": 0.53125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.440000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-umbrella.wav", + "sha256": "878e0bd2e6e4105f175aef51a028f6b141f3a2072be20f783193f3ba897a3ee0", + "bytes": 366380, + "duration": 7.632, + "rms": 0.10000000665263714, + "peak": 0.57000732421875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.632000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-market.wav", + "sha256": "1e00b8f70a796cdebb0feb00c605fece7488b06915e6e036f54206f05e201e0c", + "bytes": 359468, + "duration": 7.488, + "rms": 0.10000001450369593, + "peak": 0.639068603515625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.488000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-oil.wav", + "sha256": "f415195e0cfc7ee711d09de7519e88eab62cc8b58d70772c83c3f5c27c267819", + "bytes": 335276, + "duration": 6.984, + "rms": 0.10000000576279051, + "peak": 0.53472900390625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "6.984000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-sound.wav", + "sha256": "46f6adeed928a16af0700f1e8626cf8049f124632a04a5d10604426fb06f9596", + "bytes": 353708, + "duration": 7.368, + "rms": 0.10000001446744461, + "peak": 0.518951416015625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.368000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-meat.wav", + "sha256": "f51a37c1722fef581f7d642a4dcae7fcb9953b3c318dce971bf616cd1a81fecc", + "bytes": 345644, + "duration": 7.2, + "rms": 0.09999999288327781, + "peak": 0.51312255859375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.200000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "first-furniture.wav", + "sha256": "29f007b385b7182db1d8aeccad4fc3c260df5e66e14e52a46b0ef0fcd0ff3a0a", + "bytes": 364076, + "duration": 7.584, + "rms": 0.09999997909045008, + "peak": 0.570037841796875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.584000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-apple.wav", + "sha256": "cb7da4e9a1288cac64d168b241f27d7d2bc2dfecace58744f9bbf2792dfad04e", + "bytes": 397484, + "duration": 8.28, + "rms": 0.09999999222276726, + "peak": 0.641448974609375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.280000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-sea.wav", + "sha256": "db2f235bee26ac804b5362e8e013b8f40c8da24fd3a34e0fb3b43c6d264feb36", + "bytes": 382508, + "duration": 7.968, + "rms": 0.10000003134647438, + "peak": 0.684326171875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.968000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-tree.wav", + "sha256": "8735c3d9672561ca237fa87317beec2878492eb9063d6048d5b214423561acc1", + "bytes": 423980, + "duration": 8.832, + "rms": 0.10000000739222943, + "peak": 0.618743896484375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.832000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-hat.wav", + "sha256": "6e089a42fd7e65e82eec7186d369c926fb7fdac794365b2bd7658822fd92437c", + "bytes": 396332, + "duration": 8.256, + "rms": 0.09999999984675931, + "peak": 0.616729736328125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.256000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-umbrella.wav", + "sha256": "5e59ef39113c3d5e106862356d715f4cef2862ef8ee02654a97e532675c1ac6d", + "bytes": 410156, + "duration": 8.544, + "rms": 0.09999995863648034, + "peak": 0.674835205078125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.544000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-market.wav", + "sha256": "68a5b899115a66ab225d610ec47b4de56cc8793657362bc38f533c06f741e4cc", + "bytes": 402092, + "duration": 8.376, + "rms": 0.09999998813966116, + "peak": 0.6822509765625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.376000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-oil.wav", + "sha256": "55b2c7d6aa8bbce04edf8661882c9781da81c57f56217f99b80457a3bb895da0", + "bytes": 369836, + "duration": 7.704, + "rms": 0.0999999804882663, + "peak": 0.62396240234375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.704000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-sound.wav", + "sha256": "e9d636ff7f4777f38b1ff15bebdcec6fbde95b4b4b05625a7415b4cfa0025b59", + "bytes": 395180, + "duration": 8.232, + "rms": 0.1000000073748717, + "peak": 0.6114501953125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.232000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-meat.wav", + "sha256": "6599d2fac73ea616a793151b4ce6f51af673988d630bac6219a77e78ae6cb34e", + "bytes": 380204, + "duration": 7.92, + "rms": 0.10000001571010926, + "peak": 0.59210205078125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.920000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "repeat-furniture.wav", + "sha256": "bf84dc6cff1f227924a3a62158d2fc18769fb0ca40b91418fa19e534dbf92f00", + "bytes": 397484, + "duration": 8.28, + "rms": 0.10000003132755503, + "peak": 0.667022705078125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.280000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-apple.wav", + "sha256": "ccd7327a3fb54d3320d049ffdced3bc17595861cee69429c0ee97182d454195b", + "bytes": 402092, + "duration": 8.376, + "rms": 0.10000001654596266, + "peak": 0.490325927734375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.376000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-sea.wav", + "sha256": "9233431d78ab4b144e3c33ad0cf0d49fc0fb91b29514343230ba32987b794bb3", + "bytes": 398636, + "duration": 8.304, + "rms": 0.09999997225238083, + "peak": 0.49615478515625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.304000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-tree.wav", + "sha256": "b8e8bbe6b8b14727efd953fae9d4aed2caeb004055d52f611c6378322847edbe", + "bytes": 400940, + "duration": 8.352, + "rms": 0.10000003932190811, + "peak": 0.470611572265625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.352000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-hat.wav", + "sha256": "f9f98c91cfbcfcb106eef7d5d3795a9cf3c3aab7e19a60f13d7dffe284d19f86", + "bytes": 398636, + "duration": 8.304, + "rms": 0.10000000019629104, + "peak": 0.470977783203125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.304000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-umbrella.wav", + "sha256": "92b54f04884ab915783945077efd8f3bd443849a1720f72168b6348cae1f8e85", + "bytes": 406700, + "duration": 8.472, + "rms": 0.10000000137593418, + "peak": 0.479034423828125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.472000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-market.wav", + "sha256": "b8d25b867497adcd0fe7c8b6a8a8d2e7a52965956738a83e53f58735708773f9", + "bytes": 405548, + "duration": 8.448, + "rms": 0.10000000759335076, + "peak": 0.491485595703125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.448000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-oil.wav", + "sha256": "f6eff0cdddcad9790351491e60c53cb2a70d222033a583652f2de0b8cd6abcc7", + "bytes": 406700, + "duration": 8.472, + "rms": 0.09999998611167496, + "peak": 0.470794677734375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.472000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-sound.wav", + "sha256": "5a603bc69979603afddcaeb6edf500a7c540b72405daeb33f5335d746b327da1", + "bytes": 397484, + "duration": 8.28, + "rms": 0.10000004224092506, + "peak": 0.469268798828125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.280000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-meat.wav", + "sha256": "48e91b3932d3798e852041a854b3ee08c72127e092c62083a47af6b99adff241", + "bytes": 403244, + "duration": 8.4, + "rms": 0.09999999319342226, + "peak": 0.481536865234375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.400000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "correction-furniture.wav", + "sha256": "5c004a7b2f5a4a918c9f689f1a2dcb219a620ed14313f40a9655b36dbf64948d", + "bytes": 398636, + "duration": 8.304, + "rms": 0.10000003705538822, + "peak": 0.487396240234375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "8.304000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-apple.wav", + "sha256": "8832f1b793ab3eedbe663875ea8dbeabb122c329bd5879437c71230b0418c3c8", + "bytes": 360620, + "duration": 7.512, + "rms": 0.1000000102385111, + "peak": 0.553558349609375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.512000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-sea.wav", + "sha256": "b6dbe0af143ea775f05bd59a3d6af348bcc770ab378ac91ea75a56e06c326edf", + "bytes": 346796, + "duration": 7.224, + "rms": 0.1000000185368972, + "peak": 0.56494140625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.224000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-tree.wav", + "sha256": "341888c75275c13eb31058f53a1d4276be0daa0a1b5a3a45522036928b83b0c1", + "bytes": 382508, + "duration": 7.968, + "rms": 0.09999997157674806, + "peak": 0.600006103515625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.968000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-hat.wav", + "sha256": "b01ef9762ac84e3eb757243bc9286c62f7d45444a821ae35ac36a8b095e1598a", + "bytes": 353708, + "duration": 7.368, + "rms": 0.09999999400343942, + "peak": 0.555206298828125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.368000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-umbrella.wav", + "sha256": "399ea2f681f755918f1117c8baba42739ceca64b307b999342f457a9235050b6", + "bytes": 368684, + "duration": 7.68, + "rms": 0.10000004859108667, + "peak": 0.569549560546875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.680000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-market.wav", + "sha256": "c855b449e0d4ace74681bfe68e5e7f0b063568827ae872017234dbe8ae119e4c", + "bytes": 356012, + "duration": 7.416, + "rms": 0.10000000455585449, + "peak": 0.594970703125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.416000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-oil.wav", + "sha256": "7a6b122b11c07da86c95fb78e2db2a70681b5219c63d6441fe56da1979ac1dc3", + "bytes": 334124, + "duration": 6.96, + "rms": 0.10000001284837577, + "peak": 0.5399169921875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "6.960000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-sound.wav", + "sha256": "4d00aa6114327f450a00dacbc7e7b572ebf1de4ce9dc2fe4b5505766cca71ce9", + "bytes": 353708, + "duration": 7.368, + "rms": 0.09999999876572897, + "peak": 0.534820556640625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.368000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-meat.wav", + "sha256": "d416107cc05e7c5b92ca27100a10a8a9689de913346179945d5542de4a6bac0c", + "bytes": 342188, + "duration": 7.128, + "rms": 0.10000002241734321, + "peak": 0.510833740234375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.128000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "middle-furniture.wav", + "sha256": "b45b1afc8d9b534ff3e0a2a2ecb99efc1f4e40d41f89f55c4ed8ba94ad6d76d4", + "bytes": 354860, + "duration": 7.392, + "rms": 0.10000000075274348, + "peak": 0.5576171875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "7.392000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-apple.wav", + "sha256": "d85e6f47d871369f00de2274139e5ad6b4b5686b5e20e248df0ed6b9a320d942", + "bytes": 517292, + "duration": 10.776, + "rms": 0.10000001428889653, + "peak": 0.525360107421875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "10.776000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-sea.wav", + "sha256": "28a461fbe03bffe2560030dea8b60715f7c0fe262dcf85b5b7f7daf483836382", + "bytes": 517292, + "duration": 10.776, + "rms": 0.09999998804256775, + "peak": 0.53521728515625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "10.776000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-tree.wav", + "sha256": "5d684aa710a63d36ed8dcc2a8bfbdd5c71b0b1c63dad2a324adecb76591e85b8", + "bytes": 540332, + "duration": 11.256, + "rms": 0.09999996112285715, + "peak": 0.5262451171875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "11.256000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-hat.wav", + "sha256": "5a713a8522b231bb8276805c014b385b9150b79f58ff2618446607e2b442495e", + "bytes": 518444, + "duration": 10.8, + "rms": 0.10000001032745777, + "peak": 0.587005615234375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "10.800000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-umbrella.wav", + "sha256": "41d5866dae66600df4694a8312b531571d44cece6dba0e423e7c43d09d184819", + "bytes": 523052, + "duration": 10.896, + "rms": 0.09999985338851038, + "peak": 0.530364990234375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "10.896000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-market.wav", + "sha256": "9242d4cd6e0030e6b073e4d2360f84aa8a454d1749711cdb2d64c14f28a7ea20", + "bytes": 523052, + "duration": 10.896, + "rms": 0.10000000012409799, + "peak": 0.534820556640625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "10.896000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-oil.wav", + "sha256": "a2a71a7e5e07c81871a25c6cb7540c39aa0b32fc44f78cda66b5f34ace98d805", + "bytes": 509228, + "duration": 10.608, + "rms": 0.09999999520245731, + "peak": 0.51812744140625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "10.608000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-sound.wav", + "sha256": "1e682147dae8026ba78dc8f256c7f24acb3a6743bfd88a0fcc86096930eef078", + "bytes": 521900, + "duration": 10.872, + "rms": 0.09999998164749332, + "peak": 0.51953125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "10.872000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-meat.wav", + "sha256": "ce19735df079b54c171e775b6b365385a80160b9033442abab95b2b9c8505e1a", + "bytes": 512684, + "duration": 10.68, + "rms": 0.10000001752061703, + "peak": 0.5147705078125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "10.680000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "message-furniture.wav", + "sha256": "26c331b7e12311236f4bb48f88918dfbf53cccd2f50b55741ea6ff4473e71dd8", + "bytes": 521900, + "duration": 10.872, + "rms": 0.09999998693144198, + "peak": 0.529327392578125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "10.872000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-apple.wav", + "sha256": "3ef42e276ff3500c3e197d149f29f79d6a0c1d5516ed1d5a2c57210333fded9c", + "bytes": 246572, + "duration": 5.136, + "rms": 0.10000000281248776, + "peak": 0.47015380859375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.136000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-sea.wav", + "sha256": "8838d6a1ee8a790427dc5aa761e70b3d8872af62086efeed743297ff7f8b8a06", + "bytes": 243116, + "duration": 5.064, + "rms": 0.10000001881351578, + "peak": 0.45013427734375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.064000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-tree.wav", + "sha256": "00d8e77ec0371971d632724d91f8cf8d947bfb83c582a513b6421be4c457a1da", + "bytes": 241964, + "duration": 5.04, + "rms": 0.09999999375024479, + "peak": 0.458526611328125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.040000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-hat.wav", + "sha256": "308278ec2bd27d4224976a55632d1f7fa1edabb0792126d9d858a699c287aee4", + "bytes": 245420, + "duration": 5.112, + "rms": 0.10000000114877691, + "peak": 0.4498291015625, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.112000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-umbrella.wav", + "sha256": "4b4c858a25659540f9dbc524664496fc6de61ab8c1627f3cfee8c9c744e11d5b", + "bytes": 247724, + "duration": 5.16, + "rms": 0.1000000183428341, + "peak": 0.46533203125, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.160000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-market.wav", + "sha256": "cefa0e95649ea6bd1df4c8dfccd2b74b4f4c028714420ce1087b61db2f2a8d47", + "bytes": 248876, + "duration": 5.184, + "rms": 0.10000000503014783, + "peak": 0.485687255859375, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.184000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-oil.wav", + "sha256": "5fb63e04520c312aa6b7b48b70c2f7c140b5ab480e5c6992edbc2ba85b9a06e7", + "bytes": 241964, + "duration": 5.04, + "rms": 0.10000003066640888, + "peak": 0.443572998046875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.040000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-sound.wav", + "sha256": "659eb0e2b06e6b165b46744769743e032e2282519f8486c57bc45e0f7d07a069", + "bytes": 240812, + "duration": 5.016, + "rms": 0.10000000508879303, + "peak": 0.454803466796875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.016000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-meat.wav", + "sha256": "94f8a4f9e116a787e5ed052661d6de9ecf3ad8629db476661dd59ad03014b0c9", + "bytes": 244268, + "duration": 5.088, + "rms": 0.0999999931549985, + "peak": 0.441864013671875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.088000" + }, + "newVsV01": true, + "ffmpegDecoded": true + }, + { + "file": "mission-furniture.wav", + "sha256": "af82e03b686635c3356e706fe5b05c1231aeaaeab33f275aa5b8714988c65d43", + "bytes": 241964, + "duration": 5.04, + "rms": 0.09999996810995776, + "peak": 0.4852294921875, + "ffprobe": { + "codec_name": "pcm_s16le", + "sample_fmt": "s16", + "sample_rate": "24000", + "channels": 1, + "bits_per_sample": 16, + "duration": "5.040000" + }, + "newVsV01": true, + "ffmpegDecoded": true + } + ] +} diff --git a/.agent/evidence-v0.1.1/baseline.json b/.agent/evidence-v0.1.1/baseline.json new file mode 100644 index 0000000..d938817 --- /dev/null +++ b/.agent/evidence-v0.1.1/baseline.json @@ -0,0 +1,738 @@ +{ + "historical": { + ".agent/evidence/01-content-green.log": "5585acd257b49225218835acd69a1d240c7737d12dba62949f31a5b7c1c9ed2a", + ".agent/evidence/01-content-red.log": "0f28fe2ae2c4669a52ba2fc15bfa6285e4922474dc2b2571098b522679948e12", + ".agent/evidence/02-game-green.log": "5ca5895f8a09f4e9de7a846bf9ab37fb3715623e8e7654dab095505c097fc0c7", + ".agent/evidence/02-game-red.log": "78468ee5c25c1685f3613926f636b6fb4569227533feda3547afc6b8123d6452", + ".agent/evidence/03-dsp-green.log": "d3d08683a1426b845a2e987572861b82b3ac978b8c352991950c0f32a473d3de", + ".agent/evidence/03-dsp-intermediate.log": "084a52294f718718256eb1516048a672d045a5f7dcb5d11c646092d29efbab58", + ".agent/evidence/03-dsp-red.log": "bacf42e5c808d32702b9bcd30523d1fcf17d5ff75b281233d9e84f33b6e42589", + ".agent/evidence/04-audio-green.log": "d48c93fa7d32315ed4f719dee19222e7398bc4c4bceb8cf345e0e221a9ac89d8", + ".agent/evidence/04-audio-red.log": "58bbf8eae572f83e408dd101eac738a5f6963550e21c8265f42a7eeba18be7a7", + ".agent/evidence/05-assets-red.log": "04388acb098f7dabe9a35783b496075ee7c39757d3fde5361c9ac080188ac371", + ".agent/evidence/06-ui-05-assets-green.log": "ced2ad738cda673b24308dcb9d99d59e3ae918709eeb32de359d6ab1022133e2", + ".agent/evidence/06-ui-intermediate.log": "3295b3ae691dadb8feb7f0aaf604a1aa56cbfae27b5dbdd486cb5645f5edabe9", + ".agent/evidence/06-ui-red.log": "1f1ed6bd440af22f5abf59e4be194f969bee1d115d436a155d879d8cffddb747", + ".agent/evidence/07-build-green.log": "ec98087d20a68c5b81c6c23406316f1e5f19c6124498e1fc005d04bb78894a57", + ".agent/evidence/07-build-red.log": "138802de7bc67c1535668efe7a915424a70d5d7994acb7f366a139e89c1da77f", + ".agent/evidence/08-rule-green.log": "91b7ce3fda181fbe51cbe4e6debb61dd44dd55226dcfa81ed73559ba76811f3f", + ".agent/evidence/08-rule-red.log": "2cc1854cf6a4b9e8d53a7a36b1dde1fb6c6970b89f841b9e8f404e6e42bb929e", + ".agent/evidence/09-ime-green.log": "e5d5d1cfa2b6db724ded97d5f3309a517bcf637d9b2b417e9110dc9da0a48d72", + ".agent/evidence/09-ime-red.log": "10e5f9f99609044c768cd338df0d574a06a7442e0debe0e61ca16a6b5873adf3", + ".agent/evidence/10-ime-reset-green.log": "e12761b3a2814a79d1448e7452474a086bf371921287718eb72dd9b0f6b8fc82", + ".agent/evidence/10-ime-reset-red.log": "f6cb7c1724b8f9c410385e9970051577c03527fb7e719f43a77c967ca08ef18f", + ".agent/evidence/11-provenance-green.log": "54dcc417b97f82422c5c3e0ae11caa1f15a60412853ea1dc0152d15031bcd935", + ".agent/evidence/11-provenance-red.log": "e69fa7989223e659c16b8030f8668e562ad5b0daa060fbb1c9eff41a37fd56fd", + ".agent/evidence/12-ime-replay-green.log": "a93d9f16c7d81ed5e9a7c0b70ffee1e393d2943ddf3149a5bb115c5517327c44", + ".agent/evidence/12-ime-replay-red.log": "8cf5dab06d93cc544c21276ee633829f8b137b4b6d7c5fe47aadcad6fc2afb71", + ".agent/evidence/13-heading-build-after.log": "19829ac264f97aa023baf7d5e1fd429343272f3bd952e8b6dfb9eec0f594b453", + ".agent/evidence/13-heading-build-before.log": "646d197d2ef93c19fec4719e20297de1b22eff54f3e6155ba2125fd3dd372c46", + ".agent/evidence/13-heading-green.log": "d59fa8546083e9d371c7a552287f27bf3acd572bd0cce3da8eba860da5c05880", + ".agent/evidence/13-heading-red.log": "33acc2838786a87b8ecc170a1ac0a9d7521daf8e5b9aed2195e76e45ea327f76", + ".agent/evidence/14-snr-browser-green.log": "fcb82296e9af4b96112fae0bbb77c6ba41230c117021243e85916ae1130a462d", + ".agent/evidence/14-snr-browser-red.log": "36298e5bb1f45a69b0ca75e5cbce85ee1455c4485759670268fbbcf55bacd4d4", + ".agent/evidence/14-snr-build.log": "1e9ce36a48904a58195f9d1c2d9ca6cf2895fcb8bd0b5ab9b72f76e92c6b2585", + ".agent/evidence/14-snr-green.log": "ae74e57cea20841299a8c1ee5eaf60efad34bda697339f45527cdec83578e6df", + ".agent/evidence/14-snr-red.log": "b39bdbd8815d0d9cfcedc41c61124afece2011ea6c6c250b0ee0139eae7e9edd", + ".agent/evidence/15-volume-browser-green.log": "aeccd5f13eb9db4062cbf2c70eee332d0c39540d580e113d0a79a899b2ef02de", + ".agent/evidence/15-volume-browser-red.log": "84954280c1729703f9e9c2680c8933c3b0752b19fb9c3562e7dbf2ea1b4a548c", + ".agent/evidence/15-volume-build.log": "2767b794124804b3447f529eef025e2530975fcc506e1cbe8994acf8243e6ba8", + ".agent/evidence/15-volume-green.log": "0f9a90923f4fdc477c8f2436653eb2bf0e4b251bcb52e795701309ba14f23c7d", + ".agent/evidence/15-volume-red.log": "9beae6305080cd0d869bf3fd2093beb6e2e25e5424223aa7e4ad4db96954f37e", + ".agent/evidence/16-copy-green.log": "dacbdbab5d4af2b6e19eb4e0502a70720aac9fb6cf3b9d6dfdc73a5bf7edabfe", + ".agent/evidence/16-copy-red.log": "ebe3d7c86938d33d7f1a4818b18c7e94ab8feb18bee099fcb535a93d8083861b", + ".agent/evidence/17-spacing-green.log": "8ab85362ceadcdfecf4f7b1e95abfb4237e15018692e14f937be90265c351b11", + ".agent/evidence/17-spacing-red.log": "12db8083cd8627a81a52cd32358780a3c2257cb3bd702350b190b39226f6550f", + ".agent/evidence/18-literal-green.log": "878a8d788a8cedbf652fe0e819fe482099249a52ae7b5cc3d8826e64b32c6377", + ".agent/evidence/18-literal-red.log": "ad23ae7be3d9aab83907c12849012a01f9d1b12e5a174c3acede89817a74d2d2", + ".agent/evidence/19-notices-green.log": "efc51aca0738f3cb5264ec3c3ad0d17bb150d685f5fdb5e200bc1eedc0e3c02b", + ".agent/evidence/19-notices-red.log": "8f34c583cb8563803828d72fe56622d84bcf59a9541d983d6c0b705225955794", + ".agent/evidence/20-audio-generate.log": "f864fc266f7ca50c600457399f198a77949e9ee1601b0d2fc19b64942a2030cb", + ".agent/evidence/20-audio-green.log": "edb9b9f0aa1af11835960eee9bb6e8c013341aa528d2857bac0b89ac38457528", + ".agent/evidence/20-audio-red.log": "c1cbd5b585e8a70e23b88bfcee0c6479f85fdec0d1e06d13c81c689a140c0ac0", + ".agent/evidence/20-mechanics-green.log": "9776c68143f668e35df8b2e19884b25d2369e4800b4c264e635d0c260818c5f1", + ".agent/evidence/20-mechanics-red.log": "d25a1c844366e14fe1db9bb7c243fe0adb6857ee5b4548cf2865de96589af647", + ".agent/evidence/21-node-green.log": "3c36e9e45b6844ae2c184f943b61195482d9de53ceab06221093c8281d210d73", + ".agent/evidence/21-node-red.log": "ee25556037bdbd6b099dbb087a522284e8df00bd573dfcf0e42c734e31e09e84", + ".agent/evidence/22-sandbox-green.log": "e8a1a17e49c22dea81d7f85111c744473427466791ddeda75047cd0082e7faba", + ".agent/evidence/22-sandbox-red.log": "c8a97270f0f68367d6edb3e5ee10f34bbda1127f8dae2c5639afd482198e7bcd", + ".agent/evidence/23-delivery-browser-red.log": "180a61bbacf85db9e52f721b50da683103b719bc350454cc99d4c8954b2ae86e", + ".agent/evidence/24-retry-audio-budget.log": "b9241872b4ecd80265df4c6c2be90b31e9de4d0754772655e50b7fb4f05e2d3b", + ".agent/evidence/24-retry-final-browser.log": "68029aae89adf86fd60ee56a20a1e2f769cb4f894f27ae23963ca5b5b58c62c4", + ".agent/evidence/24-retry-final-build.log": "702c20554872e437137097ca59a80d911188477f018bc48c4b49a770b73d5a84", + ".agent/evidence/24-retry-final-lint.log": "8682ca1f55e3ec58f13321cc339c8647ef72c1934d74ea9b980aee717e528819", + ".agent/evidence/24-retry-final-test.log": "b71bb1375a039bcc3d7b3c5070da85ece3551f32ae1b8bb3a2abc5ef1edd57b8", + ".agent/evidence/24-retry-integrity.log": "a188fe54dce05f6caa868b3d9f2c22a852f6a8cdc462355673316578be623a08", + ".agent/evidence/24-retry-targeted-green.log": "4e53f2de988040540f5075b2e4a483ca53b671486c35e864fbc3c2c9c5361c61", + ".agent/evidence/audio-reproduce.log": "5f5726f3ed6845882c7a6d6f9a8d898c2b73bf61fa6328e8bbafd25f61ed4268", + ".agent/evidence/browser-final.log": "9273c91613160e239027452894b3735f9657ccad97a52412eb889290aebd5adf", + ".agent/evidence/browser-first.log": "f3087028d76aae36d61d7a14da488bd809079a9e121a2d8fb01fa034f99d4f05", + ".agent/evidence/browser-second.log": "2ff1d16f51b9384cf431d5923ad87d773510fe4b89bdf62f09cbda2efae20e3b", + ".agent/evidence/browser-third.log": "1b6c264930da8934e7f1c8da6c998543f35e68b1c3159dad722cfe23a8e5244c", + ".agent/evidence/changed-files.txt": "2b609231e5c7becd581f79413a4f7443312e71f0099ac6a51b2c5f0dfdee576f", + ".agent/evidence/desktop-game.png": "42702e89264357257db8bf54c2b3743e3212e62cfd3476a8c25bad83fb59ff81", + ".agent/evidence/desktop-home.png": "bf2f515fcd483cd5ca9a3b408f484d3556b75b1be0bb70360b7fd5f7c3404993", + ".agent/evidence/desktop-results.png": "003df3aeb08ce66618775377d9b2003809d696629f61cf54b7b86e7a0ed63593", + ".agent/evidence/final-audit.log": "6d8c5c8f3d7684adb070417bd608d01ae90aa3dc26a65af03ffda4955f38d9a3", + ".agent/evidence/final-browser.log": "5855691def462b23667a9d658338a0458a53d984b5b5f7e11b45f9a63092abcd", + ".agent/evidence/final-build.log": "565a95a51a10f78bc08f256755afce5ef183440dcdea5e75b3aefde8d7c5b837", + ".agent/evidence/final-lint.log": "d2354c241320f11f07890cbf32f99a5d88f1c7f4f236d860e2cd26db739badd8", + ".agent/evidence/final-security.log": "c585566624a4596e97327fb1864b1ac2a3189c0794b1e25a9d6ff1abb86046cb", + ".agent/evidence/final-test.log": "af03027cb22a090ecd107c10e3fc7e2f9ceb394122b0b73caa7d362310504170", + ".agent/evidence/mobile-game.png": "20a3f02f70a28b2faae1b33d0d4d6fea10b77caa426e4bf9faa5ef6d4474a846", + ".agent/evidence/mobile-home.png": "7f84bd54183fcdd03fe136b41145146ddfb323041c96a8b9e6b670c9a923033a", + ".agent/evidence/mobile-results.png": "ec2938c03ac03da8e33e25f043522b317b60c16be1c29f9dd325a81ae84513a8", + ".agent/evidence/neo-before-spacing-browser.log": "e5c214f14bd93ef1bb731f87ecfd3a02d299c9f9e5a788233f6eda5eb8946b4f", + ".agent/evidence/neo-desktop-game.png": "f5966688ba46f14fab5125452f935e55f6e6dc4355c876416729866129daea70", + ".agent/evidence/neo-desktop-heading.png": "db609f2f54af0e6b1bb96bce9bf839454403d6e2787e6520a2928fad31beb021", + ".agent/evidence/neo-desktop-home.png": "826c5da794c4ea3c15bd6c13e81d1c021fbeb8a902fc4908b78f9c457f091c0f", + ".agent/evidence/neo-desktop-results.png": "98a68ca050a4e66c17dcc3f1bd5f0e86bfc88c2ebe9a75284766d17e10426632", + ".agent/evidence/neo-final-browser.log": "582e7cbb643201e4b197b566b021322448abaae0b383967ad0e816d0c351e96b", + ".agent/evidence/neo-final-build.log": "f30138e53b8c12e70e80e2cd5f4bdfcf8d1f6a095216a4f9ade6687bca1f0488", + ".agent/evidence/neo-final-lint.log": "d2354c241320f11f07890cbf32f99a5d88f1c7f4f236d860e2cd26db739badd8", + ".agent/evidence/neo-final-test.log": "f718e2acda255fcba67b272c7fa625af450714b384bb67a72f09328bf6964f0b", + ".agent/evidence/neo-intermediate-build.log": "9d8a1f0586d58196e03e45330b6e8ff5522fdd3c27870aae2fb052199f8cf5cb", + ".agent/evidence/neo-intermediate-lint.log": "5e94ea847c3a092f4ff8e97ac5b61ebef805b3c859ad38461f4a437045d1b5b9", + ".agent/evidence/neo-mobile-game.png": "1a6733a3293204e4841ec8722a1ffe37033943097c691bcfadbcd907fde024ef", + ".agent/evidence/neo-mobile-heading.png": "ae07d0a1cb6f835525c7e0ef31655a543eaa9e10183596c2d409dead6faccf15", + ".agent/evidence/neo-mobile-home.png": "9b3ba8b0a9f7b321882429e31ac4a0472fcc13a998dfadfc930c273bc37558d4", + ".agent/evidence/neo-mobile-results.png": "d49fdf6ae9566d7a2003db3f252d84ed88ac4d73a0390ce404b47e7bab9c60df", + ".agent/evidence/release-desktop-game.png": "7300a06a507f651a587ed3deb36cc5bb728f4a36c30b7b1cf409a3b209ba564c", + ".agent/evidence/release-desktop-heading.png": "de454d77e29737e23c573646404489d5682004521deabfa01b439d607faa32ca", + ".agent/evidence/release-desktop-home.png": "f28f744e11b66373ee8db6eda2f7bdc384da4f7e763fbb467a4cda30090e869b", + ".agent/evidence/release-desktop-results.png": "2ebb4056d0ecdadea8a3469500801065ae158c17cd2db27817e813e53adc71f3", + ".agent/evidence/release-docs-final.log": "3529acce5befee536ec7d8e9d22cbbf27182297bb07408ab6e31551aaee76ed3", + ".agent/evidence/release-final-audit.log": "6d8c5c8f3d7684adb070417bd608d01ae90aa3dc26a65af03ffda4955f38d9a3", + ".agent/evidence/release-final-browser.log": "2a9881cef8c379f41abf5d69dd5ad21881988f78c0579c95e463121dc37f6e09", + ".agent/evidence/release-final-build.log": "3fba8356382341db9c562d985ca2229fd0b81969532746b0146bc489d54c321e", + ".agent/evidence/release-final-lint.log": "d2354c241320f11f07890cbf32f99a5d88f1c7f4f236d860e2cd26db739badd8", + ".agent/evidence/release-final-test.log": "46d5d0fc0180adc00a734b64db4acb4cbaa37d12567d0224836306312213870c", + ".agent/evidence/release-format.log": "57658c36cb3161b53cdf934017e4b7a733a273509ace184e0a2a6eaa046c742b", + ".agent/evidence/release-integrity.log": "cc77121b43a6b24a358999cf1a3bbf701d467fcee9730095f318303c2e80602b", + ".agent/evidence/release-intermediate-lint.log": "ee597e984ebdf11ea247c8cb1551cda446580cbd7a3dc708f85fa78bb72700ad", + ".agent/evidence/release-mobile-game.png": "83a918e5ff2142e540cd12b85806b3e69a71bc66df5d241f4b2f5abdaf493f82", + ".agent/evidence/release-mobile-heading.png": "d5c92a3b62f749d5f064af12efa5a1a2ad13b177270ec01dd10a549a8437899d", + ".agent/evidence/release-mobile-home.png": "c7fc37a247bbadba60de9dff3c8d87930a2e6ebb868f687a4135b3fd035ed1c3", + ".agent/evidence/release-mobile-results.png": "587956c9ad5eca054e4bb4b44748a2c637655e407427ba5b4c6ad7bf5a74539a", + ".agent/RESULT.md": "eef6d73a67a8a7e90cd7b4747debc5976cc51571bc268328634413f698d0e311" + }, + "audio": { + "generator": "eSpeak NG ko, speed 145, pitch 45, amplitude 100; FFmpeg PCM 16kHz mono s16", + "toolVersions": { + "espeak": "eSpeak NG text-to-speech: 1.51", + "ffmpeg": "ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers" + }, + "source": "https://github.com/espeak-ng/espeak-ng", + "method": "자체 작성 문장의 오프라인 포먼트 합성; 외부 녹음·네트워크 TTS 없음", + "files": [ + { + "file": "intro.wav", + "text": "으하하하~~ 나는 에스다. 널 위한 행운의 단어를 불러주마, 잘 찾아보거라.", + "sha256": "4c271551d99cd9dffc01f35c176e98b54b4879549543ff8e2c79c6af8d4dc53a", + "bytes": 259900 + }, + { + "file": "luck-apple.wav", + "text": "오늘의 행운을 담은 단어는 ‘사과’입니다.", + "sha256": "51e6cf35b219f13c0d72ec9633f0c108c3393bc5ef6ae6aca6aa5d00153b4177", + "bytes": 125658 + }, + { + "file": "luck-sea.wav", + "text": "오늘의 행운을 담은 단어는 ‘바다’입니다.", + "sha256": "6585cb8d50eea173395417fc5fb357f55901665fe45dd07d942f79e202729111", + "bytes": 123366 + }, + { + "file": "luck-tree.wav", + "text": "오늘의 행운을 담은 단어는 ‘나무’입니다.", + "sha256": "6d188188d1478d0298536e0e03a58883629459dee56c6ae8ab88b4d7170c1def", + "bytes": 121224 + }, + { + "file": "luck-hat.wav", + "text": "오늘의 행운을 담은 단어는 ‘모자’입니다.", + "sha256": "be8d4287ebce15e221a8908f6b2d7478e789ecc51ba95d65fdf34f130954e7e5", + "bytes": 121644 + }, + { + "file": "luck-umbrella.wav", + "text": "오늘의 행운을 담은 단어는 ‘우산’입니다.", + "sha256": "188e24932e5e68fb0de8270e5b48128d486da956ad4069afbbcd0ad9af5913ce", + "bytes": 122178 + }, + { + "file": "luck-market.wav", + "text": "오늘의 행운을 담은 단어는 ‘시장’입니다.", + "sha256": "e58cdfd5a43074179e65edaf5f223dd15ad1accdd2fd547a6d3311de57b78eca", + "bytes": 124352 + }, + { + "file": "luck-oil.wav", + "text": "오늘의 행운을 담은 단어는 ‘기름’입니다.", + "sha256": "8b1070414191ff87b9531caf0782b13458b246a1f07b51f7ddcff2cd613930f7", + "bytes": 120286 + }, + { + "file": "luck-sound.wav", + "text": "오늘의 행운을 담은 단어는 ‘소리’입니다.", + "sha256": "052e2c2724c2fcf9d4c68ba3b746a430af0cfc74b42ed6f8317ae2cc5af1c2ad", + "bytes": 122660 + }, + { + "file": "luck-meat.wav", + "text": "오늘의 행운을 담은 단어는 ‘고기’입니다.", + "sha256": "dcf3d1c84a9ee00ea8a1ea5552ac69b3dc46b9c4160ee3375a4afe38426c0b8d", + "bytes": 121754 + }, + { + "file": "luck-furniture.wav", + "text": "오늘의 행운을 담은 단어는 ‘가구’입니다.", + "sha256": "6265e3807ea10eb82e9bb15a7e72dcab5e148277bc49bacfdb2663817d151ddf", + "bytes": 122164 + }, + { + "file": "password-apple.wav", + "text": "연습 암호는 ‘다과’, 진짜 암호는 ‘사과’입니다.", + "sha256": "14b7c8c0253784f319a04b568da594d8e46077d03931a36a23e8a7689dd4b928", + "bytes": 161688 + }, + { + "file": "password-sea.wav", + "text": "연습 암호는 ‘사다’, 진짜 암호는 ‘바다’입니다.", + "sha256": "832178b1eee4fe29653ca8dfda5bfb356cab83f4f880a704ee9eab937443f872", + "bytes": 160256 + }, + { + "file": "password-tree.wav", + "text": "연습 암호는 ‘나루’, 진짜 암호는 ‘나무’입니다.", + "sha256": "ef25aa09f89acadcbb2c43a1f18d0c2da292fae4d87d052868942d08b3825a8d", + "bytes": 156642 + }, + { + "file": "password-hat.wav", + "text": "연습 암호는 ‘포자’, 진짜 암호는 ‘모자’입니다.", + "sha256": "aef3f595e1b2d36212d4611f9998d841303e96ee5385639094aebcc984ff8a94", + "bytes": 158616 + }, + { + "file": "password-umbrella.wav", + "text": "연습 암호는 ‘부산’, 진짜 암호는 ‘우산’입니다.", + "sha256": "6bc1bf13c3fd1739bc9e490525ecd5839b9a4032faecb9f101a2c68824d57b82", + "bytes": 162668 + }, + { + "file": "password-market.wav", + "text": "연습 암호는 ‘지장’, 진짜 암호는 ‘시장’입니다.", + "sha256": "33036e08a06dd33b6aa86e5b78352d2f90e35ac1c7f9f202af2a32e2ceffcfc1", + "bytes": 166360 + }, + { + "file": "password-oil.wav", + "text": "연습 암호는 ‘이름’, 진짜 암호는 ‘기름’입니다.", + "sha256": "d88914ab7302112beecb6b2edbe114c3c119ee375985eca9716bf239aea94038", + "bytes": 157396 + }, + { + "file": "password-sound.wav", + "text": "연습 암호는 ‘오리’, 진짜 암호는 ‘소리’입니다.", + "sha256": "d3df570902e1f1fb5b2a7c573d57857bf4da547fb97ebdf27f04fbbf29c1170f", + "bytes": 155298 + }, + { + "file": "password-meat.wav", + "text": "연습 암호는 ‘보기’, 진짜 암호는 ‘고기’입니다.", + "sha256": "59c422dbf3c2ed7a397e7bf3455a2bf59132f85477765c6ec13673633cdb1e8c", + "bytes": 157754 + }, + { + "file": "password-furniture.wav", + "text": "연습 암호는 ‘하구’, 진짜 암호는 ‘가구’입니다.", + "sha256": "c349bd57598eec8543ce488dd86a4c4ddd4ec661ac9e8d7f42d914d545f38f61", + "bytes": 158998 + }, + { + "file": "remember-apple.wav", + "text": "‘다과’. 이 단어는 잊으세요. 이제 ‘사과’, 이 단어를 기억하세요.", + "sha256": "3123d84c546ff4e630a8b93d59bd5e668a5de30ea651e56e134d47690aead28b", + "bytes": 205094 + }, + { + "file": "remember-sea.wav", + "text": "‘사다’. 이 단어는 잊으세요. 이제 ‘바다’, 이 단어를 기억하세요.", + "sha256": "d802055500865e38b9beffe0213b70314aa59ec181cb1af1897dd010bb5875e3", + "bytes": 204654 + }, + { + "file": "remember-tree.wav", + "text": "‘나루’. 이 단어는 잊으세요. 이제 ‘나무’, 이 단어를 기억하세요.", + "sha256": "1e3f340609eb9844b7fc727f0c17a7f9f60245d4e00df2330d734efa97d26a33", + "bytes": 201206 + }, + { + "file": "remember-hat.wav", + "text": "‘포자’. 이 단어는 잊으세요. 이제 ‘모자’, 이 단어를 기억하세요.", + "sha256": "8a3cd1ef975d8a6782c018bbe23d9e69b4876fe8e64dcb9971f5025bb7072a4c", + "bytes": 202530 + }, + { + "file": "remember-umbrella.wav", + "text": "‘부산’. 이 단어는 잊으세요. 이제 ‘우산’, 이 단어를 기억하세요.", + "sha256": "5c5ab3c30f02d3a3b8188f1d81f4d8f617a9e9783c18da38bf6d4bee9d03709e", + "bytes": 209448 + }, + { + "file": "remember-market.wav", + "text": "‘지장’. 이 단어는 잊으세요. 이제 ‘시장’, 이 단어를 기억하세요.", + "sha256": "eb582873b55043a61b80e34b25302f8d52ae9c7d641032ce65ddca8fb9bfb181", + "bytes": 212988 + }, + { + "file": "remember-oil.wav", + "text": "‘이름’. 이 단어는 잊으세요. 이제 ‘기름’, 이 단어를 기억하세요.", + "sha256": "d56d8639bf462b1c341c07252666975ff6f3428de74f0bef886019fe0085be57", + "bytes": 204388 + }, + { + "file": "remember-sound.wav", + "text": "‘오리’. 이 단어는 잊으세요. 이제 ‘소리’, 이 단어를 기억하세요.", + "sha256": "c3d8e3e75086fd52a38806ea2122542d4fe495e381d4aa4c36e72277449451bf", + "bytes": 200100 + }, + { + "file": "remember-meat.wav", + "text": "‘보기’. 이 단어는 잊으세요. 이제 ‘고기’, 이 단어를 기억하세요.", + "sha256": "be4cc7dc1af803a23eb4319d076ecf4c5ab37b56913287f49c3b3c96eb4a75ce", + "bytes": 201730 + }, + { + "file": "remember-furniture.wav", + "text": "‘하구’. 이 단어는 잊으세요. 이제 ‘가구’, 이 단어를 기억하세요.", + "sha256": "acc3efbba9ef6bf53a3992a669946b6c58e90c9821e2fb5ef8e8c4a738f8bdf7", + "bytes": 202972 + }, + { + "file": "last-apple.wav", + "text": "세 단어 중 마지막을 고르세요. ‘다과’, ‘사고’, ‘사과’.", + "sha256": "5f583e9af9213cd5dc78abb68e5fcba3dbb7c65d7d8131a53210d8326e5cbb87", + "bytes": 187732 + }, + { + "file": "last-sea.wav", + "text": "세 단어 중 마지막을 고르세요. ‘사다’, ‘보다’, ‘바다’.", + "sha256": "21e589a6f7c262cca5533a8599a9d4fb257028a6e0c42732f6147209ad35ecb6", + "bytes": 190468 + }, + { + "file": "last-tree.wav", + "text": "세 단어 중 마지막을 고르세요. ‘나루’, ‘너무’, ‘나무’.", + "sha256": "915249a9860aa29c36149fc57b04057b2fa6ab9253fdfa1e0837236f84cb7aab", + "bytes": 186820 + }, + { + "file": "last-hat.wav", + "text": "세 단어 중 마지막을 고르세요. ‘포자’, ‘모조’, ‘모자’.", + "sha256": "0ce70943449ec5fb18c57d4ba7ea09a6e995f4bc991db07c70c9a846e16679a1", + "bytes": 189216 + }, + { + "file": "last-umbrella.wav", + "text": "세 단어 중 마지막을 고르세요. ‘부산’, ‘오산’, ‘우산’.", + "sha256": "8e5a3a57fc55a7deae5df5be5425a970b0a4926b74844f0f6fba74d882d8ea73", + "bytes": 192394 + }, + { + "file": "last-market.wav", + "text": "세 단어 중 마지막을 고르세요. ‘지장’, ‘사장’, ‘시장’.", + "sha256": "2419dbb03e18d3528a615b4977405699ab5285ea4b392e45164b625ebb5ff0db", + "bytes": 201286 + }, + { + "file": "last-oil.wav", + "text": "세 단어 중 마지막을 고르세요. ‘이름’, ‘거름’, ‘기름’.", + "sha256": "8a7108a6b23dddb5f2ae1c72edf6c488335ccd322acdb6d96a371e7613d47eb2", + "bytes": 191000 + }, + { + "file": "last-sound.wav", + "text": "세 단어 중 마지막을 고르세요. ‘오리’, ‘수리’, ‘소리’.", + "sha256": "c2b77970b477fd05671c833daaf2fb48aace796e1da54a1f9286fca0a8b7f484", + "bytes": 185296 + }, + { + "file": "last-meat.wav", + "text": "세 단어 중 마지막을 고르세요. ‘보기’, ‘고개’, ‘고기’.", + "sha256": "f1bbcef87d1b2fda6d36ecc484c62c6f1a4a182c12e2e0ac8bb58f494809ddf8", + "bytes": 185540 + }, + { + "file": "last-furniture.wav", + "text": "세 단어 중 마지막을 고르세요. ‘하구’, ‘가교’, ‘가구’.", + "sha256": "5e625527e69a3f9a9c508cb9da26252ab621949d7ae9ff41bdd9336627234de0", + "bytes": 185132 + }, + { + "file": "first-apple.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘사과’, ‘다과’, ‘사고’.", + "sha256": "d539d03875121570650cac2a2bf800cc67e0a72effaf755e02c870f9569e5028", + "bytes": 203904 + }, + { + "file": "first-sea.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘바다’, ‘사다’, ‘보다’.", + "sha256": "2d789349b36dab9bbe784e2fcb8412704f2dd15b22ad63937bad9948720aa5fb", + "bytes": 207002 + }, + { + "file": "first-tree.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘나무’, ‘나루’, ‘너무’.", + "sha256": "af4ad056b18af0d376a1df7dda3d7b8419e76cda6ffe57dd71054692aa653fd9", + "bytes": 203354 + }, + { + "file": "first-hat.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘모자’, ‘포자’, ‘모조’.", + "sha256": "e34fd5edff5323e45f8621a2adb79ed871d1ce671bd3ac8c4a49a8c703878811", + "bytes": 205388 + }, + { + "file": "first-umbrella.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘우산’, ‘부산’, ‘오산’.", + "sha256": "4be32c725583f1579b15be4bbe110b4ab42705221a63621125aa1aa98f7f061c", + "bytes": 210768 + }, + { + "file": "first-market.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘시장’, ‘지장’, ‘사장’.", + "sha256": "37925d65ce12429905a1b185824722a34af8d77b15eb7bec3b52665b5c1fbc7c", + "bytes": 217794 + }, + { + "file": "first-oil.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘기름’, ‘이름’, ‘거름’.", + "sha256": "0991123aee956c26ca658382102c9b6d0a164fbf2dea45f07a978d2cc1fc6b80", + "bytes": 205784 + }, + { + "file": "first-sound.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘소리’, ‘오리’, ‘수리’.", + "sha256": "5b3063285cddd8f46a2c2cbbee1ecc9b64ecdbd287df76df65dec854679ec884", + "bytes": 201840 + }, + { + "file": "first-meat.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘고기’, ‘보기’, ‘고개’.", + "sha256": "acf07b6db586616e67e2841a6f122120a5b0b18543047271bd5b8c55e33786e3", + "bytes": 201712 + }, + { + "file": "first-furniture.wav", + "text": "세 단어 중 첫 번째를 고르세요. ‘가구’, ‘하구’, ‘가교’.", + "sha256": "acba37fc2641c7073210855d294eb7c6e5f3328652c606186b1b0e569e2d0bf6", + "bytes": 203872 + }, + { + "file": "repeat-apple.wav", + "text": "두 번 들린 단어를 고르세요. ‘사과’, ‘다과’, ‘사과’, ‘사고’.", + "sha256": "5fcef242c63dc7e86c02a1a5813e409e6392511aad5834510696b0d0499a5ef1", + "bytes": 215994 + }, + { + "file": "repeat-sea.wav", + "text": "두 번 들린 단어를 고르세요. ‘바다’, ‘사다’, ‘바다’, ‘보다’.", + "sha256": "ef675bfa38f4eb4abac04cc64aa2c3ec416d405f9c67646356c9a8f39fe17d21", + "bytes": 211180 + }, + { + "file": "repeat-tree.wav", + "text": "두 번 들린 단어를 고르세요. ‘나무’, ‘나루’, ‘나무’, ‘너무’.", + "sha256": "b6a1d2923d90f9dd35b324aea419daf3891b76cc8c5c9551fe5ec1140c2ab0bf", + "bytes": 206504 + }, + { + "file": "repeat-hat.wav", + "text": "두 번 들린 단어를 고르세요. ‘모자’, ‘포자’, ‘모자’, ‘모조’.", + "sha256": "32cbc9823f5af5f897416bec2c377a8559c82fd3fae66273152826dd000cdc4c", + "bytes": 208872 + }, + { + "file": "repeat-umbrella.wav", + "text": "두 번 들린 단어를 고르세요. ‘우산’, ‘부산’, ‘우산’, ‘오산’.", + "sha256": "4830662f39bc0c62ee9be89de80a44a837c50063fad8fb63129b865673de5525", + "bytes": 220408 + }, + { + "file": "repeat-market.wav", + "text": "두 번 들린 단어를 고르세요. ‘시장’, ‘지장’, ‘시장’, ‘사장’.", + "sha256": "0e146387377dc38fc10435e06e8eb8cd30a595092887bf8ee4c9d0e7b577650d", + "bytes": 232742 + }, + { + "file": "repeat-oil.wav", + "text": "두 번 들린 단어를 고르세요. ‘기름’, ‘이름’, ‘기름’, ‘거름’.", + "sha256": "94de2ef7f7d7f9132f3646c3dc09399de23b81909f3edf4bbd1c5b01fad4d8fe", + "bytes": 216502 + }, + { + "file": "repeat-sound.wav", + "text": "두 번 들린 단어를 고르세요. ‘소리’, ‘오리’, ‘소리’, ‘수리’.", + "sha256": "7f12b6f49a7b4f64af796aa4ad8cfffc85a6b39e2bc6698f74b420b74af6c7c8", + "bytes": 205404 + }, + { + "file": "repeat-meat.wav", + "text": "두 번 들린 단어를 고르세요. ‘고기’, ‘보기’, ‘고기’, ‘고개’.", + "sha256": "ba8beeb33bcbe4771aaea9b3832a4c25868f6e34e37b3e3d026e891c07b8e71a", + "bytes": 203544 + }, + { + "file": "repeat-furniture.wav", + "text": "두 번 들린 단어를 고르세요. ‘가구’, ‘하구’, ‘가구’, ‘가교’.", + "sha256": "581a94726b08b6cfd3c688f08838a05787ed902ab6d1d4a038e0ff6b34f63c3a", + "bytes": 208274 + }, + { + "file": "correction-apple.wav", + "text": "정답은 ‘다과’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘사과’입니다.", + "sha256": "53d1c1b207607e610964e68134a3d35ff521b569e228366ec4eb7c03691724bb", + "bytes": 256484 + }, + { + "file": "correction-sea.wav", + "text": "정답은 ‘사다’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘바다’입니다.", + "sha256": "df61f91374b9fbb5817249ae3b6cadfae914d3e97ee2c6d07366ef82080a11db", + "bytes": 255204 + }, + { + "file": "correction-tree.wav", + "text": "정답은 ‘나루’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘나무’입니다.", + "sha256": "837c24b18f63a73e135511b2edd48ba12593317de3be4d225622e6d11b1a57da", + "bytes": 251316 + }, + { + "file": "correction-hat.wav", + "text": "정답은 ‘포자’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘모자’입니다.", + "sha256": "00d7c21caccbbb6c80ec6218415be1b0a6683a2d509b60821d3556e43353b5dc", + "bytes": 253208 + }, + { + "file": "correction-umbrella.wav", + "text": "정답은 ‘부산’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘우산’입니다.", + "sha256": "9bb3ce5b00ccc1632c37638550f329cbb6cda01864433e068f85eb7ccab86370", + "bytes": 255400 + }, + { + "file": "correction-market.wav", + "text": "정답은 ‘지장’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘시장’입니다.", + "sha256": "c8152db42c956701cd5e21c588ef62aedaefa922658d4bc0fecf8243548d7054", + "bytes": 257262 + }, + { + "file": "correction-oil.wav", + "text": "정답은 ‘이름’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘기름’입니다.", + "sha256": "228759e87b1c80be72ea34ff5b77cb5e4557afca1b73434ca8f6f1c2fedaa836", + "bytes": 247864 + }, + { + "file": "correction-sound.wav", + "text": "정답은 ‘오리’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘소리’입니다.", + "sha256": "24f2d52bb865c71fc4735a4aebd69ce0e429a9f68646e73e77adababbe34af93", + "bytes": 250170 + }, + { + "file": "correction-meat.wav", + "text": "정답은 ‘보기’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘고기’입니다.", + "sha256": "821caf897ddab107df8435b00b686bf9223e043c91a311edcaa0c3c5eb8c6511", + "bytes": 253432 + }, + { + "file": "correction-furniture.wav", + "text": "정답은 ‘하구’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘가구’입니다.", + "sha256": "5d69426f3319fa435ee60b234540dd8d9a07f00900177f03da2770f44511ade8", + "bytes": 254342 + }, + { + "file": "middle-apple.wav", + "text": "세 단어 중 가운데를 고르세요. ‘다과’, ‘사과’, ‘사고’.", + "sha256": "0ecc7a7d27508f73c9064a35609c922bbdd6fce8ebfc155e74f7b78d4e897dae", + "bytes": 191056 + }, + { + "file": "middle-sea.wav", + "text": "세 단어 중 가운데를 고르세요. ‘사다’, ‘바다’, ‘보다’.", + "sha256": "c7828af4e64745d89a6f23fda6c86efb76febe379bab0901c2972a4569947604", + "bytes": 194176 + }, + { + "file": "middle-tree.wav", + "text": "세 단어 중 가운데를 고르세요. ‘나루’, ‘나무’, ‘너무’.", + "sha256": "756d13453c7e13dcfaa65c8cb3497ac8d3a5d12bedb56dcd74e7f1c022392416", + "bytes": 190516 + }, + { + "file": "middle-hat.wav", + "text": "세 단어 중 가운데를 고르세요. ‘포자’, ‘모자’, ‘모조’.", + "sha256": "6ad5ec1e2c2c0de568c55a0c1f0e8361885725a2515cf396c8fe5d2106ae22d0", + "bytes": 192548 + }, + { + "file": "middle-umbrella.wav", + "text": "세 단어 중 가운데를 고르세요. ‘부산’, ‘우산’, ‘오산’.", + "sha256": "a507896dd50f98909c6a9fc0a4bfddfde5c975836e494cd9b7f40b69bc8ec33b", + "bytes": 196102 + }, + { + "file": "middle-market.wav", + "text": "세 단어 중 가운데를 고르세요. ‘지장’, ‘시장’, ‘사장’.", + "sha256": "900024616e512ea6777d08f80e3e2c83815ea1f03b6eeb537d212995474e1048", + "bytes": 204956 + }, + { + "file": "middle-oil.wav", + "text": "세 단어 중 가운데를 고르세요. ‘이름’, ‘기름’, ‘거름’.", + "sha256": "c42389a5ccd3b39a5877a38e647a6abbec6ff6a7eabf3d4c95c0507fa65d6d2f", + "bytes": 194772 + }, + { + "file": "middle-sound.wav", + "text": "세 단어 중 가운데를 고르세요. ‘오리’, ‘소리’, ‘수리’.", + "sha256": "b05ae2042bc973c3d1873fe15b5bc7f2af7e803794315023486efa788048342c", + "bytes": 188984 + }, + { + "file": "middle-meat.wav", + "text": "세 단어 중 가운데를 고르세요. ‘보기’, ‘고기’, ‘고개’.", + "sha256": "d54d6f9b4bd90b840443b9e0b0390a93909eeb81c37af6a9b57ce545bede532d", + "bytes": 188870 + }, + { + "file": "middle-furniture.wav", + "text": "세 단어 중 가운데를 고르세요. ‘하구’, ‘가구’, ‘가교’.", + "sha256": "d9318ef33ae1c60630ddda5abeefcbe43626d8f8a9abd5d62efd132eeb77c8fc", + "bytes": 191024 + }, + { + "file": "message-apple.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘다과’, ‘사과’. 지금은 빨간 불입니다.", + "sha256": "3867563d5acd2530c72cbe6f98ca2b463ec0c92285069c99c8ba084c45aabfd6", + "bytes": 316834 + }, + { + "file": "message-sea.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘사다’, ‘바다’. 지금은 빨간 불입니다.", + "sha256": "b7d8d89d93247eca9aec124f80bf6328d6d88511d05b513ce6d9fa32b3211848", + "bytes": 317816 + }, + { + "file": "message-tree.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘나루’, ‘나무’. 지금은 빨간 불입니다.", + "sha256": "9539e5ee87068e241598db2e4fbacb240daa011a2238c9864da8ffde6b644918", + "bytes": 315930 + }, + { + "file": "message-hat.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘포자’, ‘모자’. 지금은 빨간 불입니다.", + "sha256": "ccbba6a22c9cbc1231f45fbd7420c6257ac22ac72de8e8c0c97f1cc31bb2b16e", + "bytes": 317258 + }, + { + "file": "message-umbrella.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘부산’, ‘우산’. 지금은 빨간 불입니다.", + "sha256": "e980f4476f7275cc5ae24e7ce0e6d9c1e6db027f7cbd1eb39cc5bc4bc07cbf6e", + "bytes": 320532 + }, + { + "file": "message-market.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘지장’, ‘시장’. 지금은 빨간 불입니다.", + "sha256": "35432a3ed8dd1e15894ace5aad6f5a66cf28509ba92b8ee47bcaae424cb25ba1", + "bytes": 325564 + }, + { + "file": "message-oil.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘이름’, ‘기름’. 지금은 빨간 불입니다.", + "sha256": "814fc749006ed040de58b5236a8d92f0d79683c8eea3b9d095386344f41a47bd", + "bytes": 317786 + }, + { + "file": "message-sound.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘오리’, ‘소리’. 지금은 빨간 불입니다.", + "sha256": "e25c867e6c18d906c010816475253d2b10aa3cc4192b5551ba78055d65c461c8", + "bytes": 313270 + }, + { + "file": "message-meat.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘보기’, ‘고기’. 지금은 빨간 불입니다.", + "sha256": "f1863e1527b7714ae64f5d752b542e8768b8ed89b5885012283f7ca20742ee00", + "bytes": 314464 + }, + { + "file": "message-furniture.wav", + "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘하구’, ‘가구’. 지금은 빨간 불입니다.", + "sha256": "43f87d0e3d92bd132f9ea46e024b9a940736b7399e899866ed42af367a550f78", + "bytes": 316152 + }, + { + "file": "mission-apple.wav", + "text": "오른쪽 표지는 ‘사과’, 왼쪽 표지는 ‘다과’입니다.", + "sha256": "36697151a1a57645f5acd9fcca2d13bffa21937e1bbcde08ab2d8d485730145a", + "bytes": 166540 + }, + { + "file": "mission-sea.wav", + "text": "오른쪽 표지는 ‘바다’, 왼쪽 표지는 ‘사다’입니다.", + "sha256": "2741f404e39ec64af7f73452f82c68b3ae1d377e9f40a586034f9b147fbfb114", + "bytes": 165122 + }, + { + "file": "mission-tree.wav", + "text": "오른쪽 표지는 ‘나무’, 왼쪽 표지는 ‘나루’입니다.", + "sha256": "8a7bb2c795d8877433be2621b69a48c2b6c5c1e192c094c01257fb93eb5d9449", + "bytes": 160834 + }, + { + "file": "mission-hat.wav", + "text": "오른쪽 표지는 ‘모자’, 왼쪽 표지는 ‘포자’입니다.", + "sha256": "26dfa26bd8dcb9d63c231dfc37bf25970f4a8dadcaa40347dd15974887e948b1", + "bytes": 163102 + }, + { + "file": "mission-umbrella.wav", + "text": "오른쪽 표지는 ‘우산’, 왼쪽 표지는 ‘부산’입니다.", + "sha256": "79be98918c3fb4e5e4e2e51b2bfa5e5aad6c600478ae1a34c055b16ce71d5264", + "bytes": 167480 + }, + { + "file": "mission-market.wav", + "text": "오른쪽 표지는 ‘시장’, 왼쪽 표지는 ‘지장’입니다.", + "sha256": "8f18144a91072be37dc31a161fe6e9bb1980ab36af0edee8d7a359397b8bb8ac", + "bytes": 170724 + }, + { + "file": "mission-oil.wav", + "text": "오른쪽 표지는 ‘기름’, 왼쪽 표지는 ‘이름’입니다.", + "sha256": "1ceabc58f445f7bd5609b2f9de80b309ad64d11c888c7261cf84315a7637a748", + "bytes": 161758 + }, + { + "file": "mission-sound.wav", + "text": "오른쪽 표지는 ‘소리’, 왼쪽 표지는 ‘오리’입니다.", + "sha256": "6651b14caf4e54069730f137e5d8c3c1e5a9c66aa72f15623cc442edb967cadb", + "bytes": 160162 + }, + { + "file": "mission-meat.wav", + "text": "오른쪽 표지는 ‘고기’, 왼쪽 표지는 ‘보기’입니다.", + "sha256": "5d2ab3071fd27e850d99c50ad37905096ef28267b660a35992559bdc126c4931", + "bytes": 162626 + }, + { + "file": "mission-furniture.wav", + "text": "오른쪽 표지는 ‘가구’, 왼쪽 표지는 ‘하구’입니다.", + "sha256": "080307bb46bb47378c0c03437d9ef44118a5d4ea17b2b6d70e5cbc710c71ba78", + "bytes": 163884 + } + ] + }, + "head": "1c0069c367f6e108d3f67a2a0d044399dd0ef2bd", + "branch": "fix/v0.1.1-neural-voice" +} diff --git a/.agent/evidence-v0.1.1/browser-audio-integrity.json b/.agent/evidence-v0.1.1/browser-audio-integrity.json new file mode 100644 index 0000000..91f8537 --- /dev/null +++ b/.agent/evidence-v0.1.1/browser-audio-integrity.json @@ -0,0 +1,1020 @@ +{ + "chrome": "146.0.7680.80", + "sandboxStatus": "Sandbox Status\nLayer 1 Sandbox\tNamespace\nPID namespaces\tYes\nNetwork namespaces\tYes\nSeccomp-BPF sandbox\tYes\nSeccomp-BPF sandbox supports TSYNC\tYes\nPtrace Protection with Yama LSM (Broker)\tYes\nPtrace Protection with Yama LSM (Non-broker)\tNo\n\nYou are adequately sandboxed.", + "external": [], + "errors": [], + "version": "0.1.1", + "voice": "ko-KR-HyunsuMultilingualNeural", + "files": [ + { + "file": "intro.wav", + "sha256": "90d9f46e6e3f9c8a0c1a781096df54d7809ef4ad38b289508ef7b8c0411465ce", + "bytes": 338732, + "sampleRate": 24000, + "channels": 1, + "duration": 7.056, + "rms": 0.10000144992850359, + "peak": 0.4998626708984375 + }, + { + "file": "luck-apple.wav", + "sha256": "7c8f413f4b7e164f090e508ba090448f1376c6012649bbb6a917d45851313883", + "bytes": 188972, + "sampleRate": 24000, + "channels": 1, + "duration": 3.936, + "rms": 0.10000130469939268, + "peak": 0.401641845703125 + }, + { + "file": "luck-sea.wav", + "sha256": "47aa31d757c9fb49c9b53a1c052d90a5ced4be57549d649850d9b48f8864045b", + "bytes": 191276, + "sampleRate": 24000, + "channels": 1, + "duration": 3.984, + "rms": 0.10000129182106483, + "peak": 0.388702392578125 + }, + { + "file": "luck-tree.wav", + "sha256": "122f496834fb16d6da47f801e50e46e272e4e934af959467d1db4e490f79be7a", + "bytes": 183212, + "sampleRate": 24000, + "channels": 1, + "duration": 3.816, + "rms": 0.10000140751524371, + "peak": 0.36920166015625 + }, + { + "file": "luck-hat.wav", + "sha256": "4ff43dd042f4b5451e9fdeb3edd59f1e389a7d1dd488094587a3a9fc7dfffa44", + "bytes": 191276, + "sampleRate": 24000, + "channels": 1, + "duration": 3.984, + "rms": 0.10000132502946775, + "peak": 0.417083740234375 + }, + { + "file": "luck-umbrella.wav", + "sha256": "8d3e8e8bc97e5fe3f3362184c4abb12f01a882a1f1174d2644f0f70aac677c0a", + "bytes": 192428, + "sampleRate": 24000, + "channels": 1, + "duration": 4.008, + "rms": 0.10000131475711933, + "peak": 0.385650634765625 + }, + { + "file": "luck-market.wav", + "sha256": "356af25332bf2000ed349671f3ff39767929e1f4599af622c021f907d11fb7bf", + "bytes": 194732, + "sampleRate": 24000, + "channels": 1, + "duration": 4.056, + "rms": 0.10000132629820717, + "peak": 0.4022216796875 + }, + { + "file": "luck-oil.wav", + "sha256": "3fa1e662a3af8f2726b17b2baa5bec2934406e6595ebc243c8380dd16e190977", + "bytes": 190124, + "sampleRate": 24000, + "channels": 1, + "duration": 3.96, + "rms": 0.10000134375667624, + "peak": 0.3817255198955536 + }, + { + "file": "luck-sound.wav", + "sha256": "34e4d9758d2ed448646fda364610e34779c84794403d9051ab3d8c1d33e2c80c", + "bytes": 184364, + "sampleRate": 24000, + "channels": 1, + "duration": 3.84, + "rms": 0.10000138709171018, + "peak": 0.386322021484375 + }, + { + "file": "luck-meat.wav", + "sha256": "d36dfc395a5c53fbfa4abe9980db7bb59adfdac59e40318c4aca365365e3f2ab", + "bytes": 187820, + "sampleRate": 24000, + "channels": 1, + "duration": 3.912, + "rms": 0.1000013329979738, + "peak": 0.371124267578125 + }, + { + "file": "luck-furniture.wav", + "sha256": "52cb92cdb8ee92b6f581c9d68dbc346f180c470564809ba57185429c9eb84127", + "bytes": 186668, + "sampleRate": 24000, + "channels": 1, + "duration": 3.888, + "rms": 0.1000013480244084, + "peak": 0.387054443359375 + }, + { + "file": "password-apple.wav", + "sha256": "b6f5de3fe571fe3a7fdb722fe49250bcfeedef49475271e0ec76a7630907edd5", + "bytes": 235052, + "sampleRate": 24000, + "channels": 1, + "duration": 4.896, + "rms": 0.10000148212193387, + "peak": 0.4834437072277069 + }, + { + "file": "password-sea.wav", + "sha256": "03f45b8cb5b3dc72faf5c6e3d0e2a10cea6b60301ea400d0a4453596d2f0b337", + "bytes": 232748, + "sampleRate": 24000, + "channels": 1, + "duration": 4.848, + "rms": 0.10000144821976133, + "peak": 0.5339213013648987 + }, + { + "file": "password-tree.wav", + "sha256": "67da8147296e35134ec254bc82b58317630061b2c64d2d63c4be197ce8692d50", + "bytes": 233900, + "sampleRate": 24000, + "channels": 1, + "duration": 4.872, + "rms": 0.10000156310809623, + "peak": 0.4904019236564636 + }, + { + "file": "password-hat.wav", + "sha256": "e91bfc97df90a1675b6cd856880ac15c54de32cfc3b23603bdd1c2c5ac3ce6b8", + "bytes": 235052, + "sampleRate": 24000, + "channels": 1, + "duration": 4.896, + "rms": 0.10000146155157119, + "peak": 0.5059053301811218 + }, + { + "file": "password-umbrella.wav", + "sha256": "ab3ca6398c879971dc793eb25b472fcc0db72d76e386f11ef4d32c639f37c69d", + "bytes": 239660, + "sampleRate": 24000, + "channels": 1, + "duration": 4.992, + "rms": 0.100001426973271, + "peak": 0.4776757061481476 + }, + { + "file": "password-market.wav", + "sha256": "02e325fb753ef7a3ee585a03d2ca2aa3f1fbc58523b062222ae1136eeed7a95c", + "bytes": 230444, + "sampleRate": 24000, + "channels": 1, + "duration": 4.8, + "rms": 0.10000151520861549, + "peak": 0.48170414566993713 + }, + { + "file": "password-oil.wav", + "sha256": "b472957b27ff98ea7b8894fa1ff12653f7e9f66f77870faa9e52a1c1bf116c41", + "bytes": 240812, + "sampleRate": 24000, + "channels": 1, + "duration": 5.016, + "rms": 0.10000146772662066, + "peak": 0.4945219159126282 + }, + { + "file": "password-sound.wav", + "sha256": "044aafff08a19e39f21e58b963644715a987cd155896998769ef8e9ce6449628", + "bytes": 236204, + "sampleRate": 24000, + "channels": 1, + "duration": 4.92, + "rms": 0.10000156641621728, + "peak": 0.4621112644672394 + }, + { + "file": "password-meat.wav", + "sha256": "b009caadd56350d6a641ff17f0973e2b2724c183e1f45ae0bf3c483e237fa99e", + "bytes": 240812, + "sampleRate": 24000, + "channels": 1, + "duration": 5.016, + "rms": 0.10000148386024335, + "peak": 0.4978789687156677 + }, + { + "file": "password-furniture.wav", + "sha256": "fb2b631dd4e12120d08a84cce6450f9cd9cd68105d13d7001d808e7deaa1df4a", + "bytes": 233900, + "sampleRate": 24000, + "channels": 1, + "duration": 4.872, + "rms": 0.10000146718546903, + "peak": 0.4836573302745819 + }, + { + "file": "remember-apple.wav", + "sha256": "1bf2aef9a9f030af1a8fd7dee8f44aa420a397b3ba99b575feeb3dc7a63ec191", + "bytes": 395180, + "sampleRate": 24000, + "channels": 1, + "duration": 8.232, + "rms": 0.10000148399213096, + "peak": 0.563568115234375 + }, + { + "file": "remember-sea.wav", + "sha256": "5c91a36496e274d2be9c6fa8d9b506cd04f2a1b7ceb05ed1b7bb03fe7fca99d9", + "bytes": 399788, + "sampleRate": 24000, + "channels": 1, + "duration": 8.328, + "rms": 0.10000144241513113, + "peak": 0.5465559959411621 + }, + { + "file": "remember-tree.wav", + "sha256": "bc8e54f5407e8866bf1ca1ddfd4f6c63d85fee29bd9e430c7dd33e87974fc5b8", + "bytes": 409004, + "sampleRate": 24000, + "channels": 1, + "duration": 8.52, + "rms": 0.10000146559822108, + "peak": 0.5486617684364319 + }, + { + "file": "remember-hat.wav", + "sha256": "31a6a4270bf368cda419094d87a303ac3cd5c22539786cb165f8044b0b8e9d6e", + "bytes": 402092, + "sampleRate": 24000, + "channels": 1, + "duration": 8.376, + "rms": 0.10000145804818952, + "peak": 0.541778564453125 + }, + { + "file": "remember-umbrella.wav", + "sha256": "c8988e604ba05d52597e561389541e8f9cda86e052279f1862bbf8399ead314e", + "bytes": 403244, + "sampleRate": 24000, + "channels": 1, + "duration": 8.4, + "rms": 0.10000148262660476, + "peak": 0.557830810546875 + }, + { + "file": "remember-market.wav", + "sha256": "d89792da51c83ac49d27e55aa6d822a338899a5ee487b34b552748201e1522f6", + "bytes": 398636, + "sampleRate": 24000, + "channels": 1, + "duration": 8.304, + "rms": 0.10000149710778367, + "peak": 0.5728324055671692 + }, + { + "file": "remember-oil.wav", + "sha256": "3ca98682ff06d1d6b8af02c0fd5b2124f42266ac989884223eff4a9bbde1287a", + "bytes": 399788, + "sampleRate": 24000, + "channels": 1, + "duration": 8.328, + "rms": 0.10000149133669024, + "peak": 0.5243995189666748 + }, + { + "file": "remember-sound.wav", + "sha256": "98387ccc7aab566f6487d4c250b2128082cc527e013a6f7b7bc4d0266ac295df", + "bytes": 394028, + "sampleRate": 24000, + "channels": 1, + "duration": 8.208, + "rms": 0.10000158053850364, + "peak": 0.5377971529960632 + }, + { + "file": "remember-meat.wav", + "sha256": "9183ef90b64f5ceaf3bb0178c689231ff3e9981d6ce889124a15f2a6e2b7a344", + "bytes": 395180, + "sampleRate": 24000, + "channels": 1, + "duration": 8.232, + "rms": 0.10000155087122137, + "peak": 0.5193029642105103 + }, + { + "file": "remember-furniture.wav", + "sha256": "442bdcb956f120c270e1f321feae0415c897de0183e468980fcc119b898b2a67", + "bytes": 403244, + "sampleRate": 24000, + "channels": 1, + "duration": 8.4, + "rms": 0.10000149119200713, + "peak": 0.5619068145751953 + }, + { + "file": "last-apple.wav", + "sha256": "4cb2a8579fa3a7b91bb36363826ce8ca0d6d86e96e16a479e226a2802edd82f7", + "bytes": 356012, + "sampleRate": 24000, + "channels": 1, + "duration": 7.416, + "rms": 0.10000144523423521, + "peak": 0.622528076171875 + }, + { + "file": "last-sea.wav", + "sha256": "f755452e1da44674aa1df7d12edffa988091b89a394e7eb6d4a00eb721f07cbf", + "bytes": 345644, + "sampleRate": 24000, + "channels": 1, + "duration": 7.2, + "rms": 0.10000140162255576, + "peak": 0.591033935546875 + }, + { + "file": "last-tree.wav", + "sha256": "589e56d940a3c9e80ec02ae0b2dd93463fdc85ef019a753fcd0fded0752055f9", + "bytes": 382508, + "sampleRate": 24000, + "channels": 1, + "duration": 7.968, + "rms": 0.10000149781706567, + "peak": 0.5807672142982483 + }, + { + "file": "last-hat.wav", + "sha256": "bdaf8fbc54df8061e9e58aa8c14b3044a6e863dc54973ee3ee6ae2c5ae1a28f1", + "bytes": 354860, + "sampleRate": 24000, + "channels": 1, + "duration": 7.392, + "rms": 0.10000152378078818, + "peak": 0.550079345703125 + }, + { + "file": "last-umbrella.wav", + "sha256": "c801293a34013a8d93d3289a2cc87cd3eed83f97ba4647f5060efa00b117a9ad", + "bytes": 370988, + "sampleRate": 24000, + "channels": 1, + "duration": 7.728, + "rms": 0.10000149360501835, + "peak": 0.59765625 + }, + { + "file": "last-market.wav", + "sha256": "7857e87e9dbfe8fe849cc0b18c1d45b60916cb337328099c7a87a42c82e2821e", + "bytes": 359468, + "sampleRate": 24000, + "channels": 1, + "duration": 7.488, + "rms": 0.10000148064140717, + "peak": 0.600555419921875 + }, + { + "file": "last-oil.wav", + "sha256": "b5ce34950b8b31b1baf6ce55410797f0d8aa5478a670d6eace87cf76d134d6d3", + "bytes": 337580, + "sampleRate": 24000, + "channels": 1, + "duration": 7.032, + "rms": 0.10000149980584365, + "peak": 0.55706787109375 + }, + { + "file": "last-sound.wav", + "sha256": "ec9c49802aebeb6a8b898a1c0d8ad1daa127d2d64c905c197ed49a8e0c4eb64a", + "bytes": 354860, + "sampleRate": 24000, + "channels": 1, + "duration": 7.392, + "rms": 0.10000165748716579, + "peak": 0.5641041398048401 + }, + { + "file": "last-meat.wav", + "sha256": "10f8ad4259ed8945c9da831e80b3902303c09caef96d400298599168fb1efed4", + "bytes": 346796, + "sampleRate": 24000, + "channels": 1, + "duration": 7.224, + "rms": 0.10000155834091913, + "peak": 0.537200927734375 + }, + { + "file": "last-furniture.wav", + "sha256": "5ad4f776e35439d408abed0e66567d6b5cdc80fc54d579165472e474f1238776", + "bytes": 357164, + "sampleRate": 24000, + "channels": 1, + "duration": 7.44, + "rms": 0.10000152394963031, + "peak": 0.583587646484375 + }, + { + "file": "first-apple.wav", + "sha256": "7c1dbcb3082055df2b8f087f7e906ec867c92f679de0894c34740c05e61a4bc1", + "bytes": 359468, + "sampleRate": 24000, + "channels": 1, + "duration": 7.488, + "rms": 0.10000143468253914, + "peak": 0.57977294921875 + }, + { + "file": "first-sea.wav", + "sha256": "0f0b39314c5f7d7168d4854003c0c284e7bc3438cdc4d1d1c5adab7cb5ed30e5", + "bytes": 349100, + "sampleRate": 24000, + "channels": 1, + "duration": 7.272, + "rms": 0.10000142148394393, + "peak": 0.5717948079109192 + }, + { + "file": "first-tree.wav", + "sha256": "3178230fd5850b6277a2153b438ec5289720e3db7ac929914cc213964f004bd5", + "bytes": 379052, + "sampleRate": 24000, + "channels": 1, + "duration": 7.896, + "rms": 0.10000144270360484, + "peak": 0.5833613276481628 + }, + { + "file": "first-hat.wav", + "sha256": "17c8cde90567e7574b0c0702663bd70713054089ee920241b5444d9b7869eef5", + "bytes": 357164, + "sampleRate": 24000, + "channels": 1, + "duration": 7.44, + "rms": 0.10000152904790521, + "peak": 0.5312662124633789 + }, + { + "file": "first-umbrella.wav", + "sha256": "878e0bd2e6e4105f175aef51a028f6b141f3a2072be20f783193f3ba897a3ee0", + "bytes": 366380, + "sampleRate": 24000, + "channels": 1, + "duration": 7.632, + "rms": 0.10000147647908036, + "peak": 0.5700247287750244 + }, + { + "file": "first-market.wav", + "sha256": "1e00b8f70a796cdebb0feb00c605fece7488b06915e6e036f54206f05e201e0c", + "bytes": 359468, + "sampleRate": 24000, + "channels": 1, + "duration": 7.488, + "rms": 0.10000152633991319, + "peak": 0.6390880942344666 + }, + { + "file": "first-oil.wav", + "sha256": "f415195e0cfc7ee711d09de7519e88eab62cc8b58d70772c83c3f5c27c267819", + "bytes": 335276, + "sampleRate": 24000, + "channels": 1, + "duration": 6.984, + "rms": 0.10000145886808136, + "peak": 0.5347453355789185 + }, + { + "file": "first-sound.wav", + "sha256": "46f6adeed928a16af0700f1e8626cf8049f124632a04a5d10604426fb06f9596", + "bytes": 353708, + "sampleRate": 24000, + "channels": 1, + "duration": 7.368, + "rms": 0.10000157588418972, + "peak": 0.5189672708511353 + }, + { + "file": "first-meat.wav", + "sha256": "f51a37c1722fef581f7d642a4dcae7fcb9953b3c318dce971bf616cd1a81fecc", + "bytes": 345644, + "sampleRate": 24000, + "channels": 1, + "duration": 7.2, + "rms": 0.10000152244411063, + "peak": 0.5131382346153259 + }, + { + "file": "first-furniture.wav", + "sha256": "29f007b385b7182db1d8aeccad4fc3c260df5e66e14e52a46b0ef0fcd0ff3a0a", + "bytes": 364076, + "sampleRate": 24000, + "channels": 1, + "duration": 7.584, + "rms": 0.1000014551820741, + "peak": 0.5700552463531494 + }, + { + "file": "repeat-apple.wav", + "sha256": "cb7da4e9a1288cac64d168b241f27d7d2bc2dfecace58744f9bbf2792dfad04e", + "bytes": 397484, + "sampleRate": 24000, + "channels": 1, + "duration": 8.28, + "rms": 0.10000142448574952, + "peak": 0.6414685249328613 + }, + { + "file": "repeat-sea.wav", + "sha256": "db2f235bee26ac804b5362e8e013b8f40c8da24fd3a34e0fb3b43c6d264feb36", + "bytes": 382508, + "sampleRate": 24000, + "channels": 1, + "duration": 7.968, + "rms": 0.10000145042993039, + "peak": 0.6843470335006714 + }, + { + "file": "repeat-tree.wav", + "sha256": "8735c3d9672561ca237fa87317beec2878492eb9063d6048d5b214423561acc1", + "bytes": 423980, + "sampleRate": 24000, + "channels": 1, + "duration": 8.832, + "rms": 0.10000150947389638, + "peak": 0.6187627911567688 + }, + { + "file": "repeat-hat.wav", + "sha256": "6e089a42fd7e65e82eec7186d369c926fb7fdac794365b2bd7658822fd92437c", + "bytes": 396332, + "sampleRate": 24000, + "channels": 1, + "duration": 8.256, + "rms": 0.1000015662548192, + "peak": 0.616748571395874 + }, + { + "file": "repeat-umbrella.wav", + "sha256": "5e59ef39113c3d5e106862356d715f4cef2862ef8ee02654a97e532675c1ac6d", + "bytes": 410156, + "sampleRate": 24000, + "channels": 1, + "duration": 8.544, + "rms": 0.10000147499132286, + "peak": 0.6748558282852173 + }, + { + "file": "repeat-market.wav", + "sha256": "68a5b899115a66ab225d610ec47b4de56cc8793657362bc38f533c06f741e4cc", + "bytes": 402092, + "sampleRate": 24000, + "channels": 1, + "duration": 8.376, + "rms": 0.10000160159844854, + "peak": 0.6822717785835266 + }, + { + "file": "repeat-oil.wav", + "sha256": "55b2c7d6aa8bbce04edf8661882c9781da81c57f56217f99b80457a3bb895da0", + "bytes": 369836, + "sampleRate": 24000, + "channels": 1, + "duration": 7.704, + "rms": 0.10000146897241707, + "peak": 0.6239814162254333 + }, + { + "file": "repeat-sound.wav", + "sha256": "e9d636ff7f4777f38b1ff15bebdcec6fbde95b4b4b05625a7415b4cfa0025b59", + "bytes": 395180, + "sampleRate": 24000, + "channels": 1, + "duration": 8.232, + "rms": 0.10000164955559301, + "peak": 0.6114688515663147 + }, + { + "file": "repeat-meat.wav", + "sha256": "6599d2fac73ea616a793151b4ce6f51af673988d630bac6219a77e78ae6cb34e", + "bytes": 380204, + "sampleRate": 24000, + "channels": 1, + "duration": 7.92, + "rms": 0.1000016061190976, + "peak": 0.5921201109886169 + }, + { + "file": "repeat-furniture.wav", + "sha256": "bf84dc6cff1f227924a3a62158d2fc18769fb0ca40b91418fa19e534dbf92f00", + "bytes": 397484, + "sampleRate": 24000, + "channels": 1, + "duration": 8.28, + "rms": 0.10000155255948041, + "peak": 0.6670430898666382 + }, + { + "file": "correction-apple.wav", + "sha256": "ccd7327a3fb54d3320d049ffdced3bc17595861cee69429c0ee97182d454195b", + "bytes": 402092, + "sampleRate": 24000, + "channels": 1, + "duration": 8.376, + "rms": 0.10000140667715379, + "peak": 0.490325927734375 + }, + { + "file": "correction-sea.wav", + "sha256": "9233431d78ab4b144e3c33ad0cf0d49fc0fb91b29514343230ba32987b794bb3", + "bytes": 398636, + "sampleRate": 24000, + "channels": 1, + "duration": 8.304, + "rms": 0.10000134688905045, + "peak": 0.49616992473602295 + }, + { + "file": "correction-tree.wav", + "sha256": "b8e8bbe6b8b14727efd953fae9d4aed2caeb004055d52f611c6378322847edbe", + "bytes": 400940, + "sampleRate": 24000, + "channels": 1, + "duration": 8.352, + "rms": 0.10000144970799159, + "peak": 0.470611572265625 + }, + { + "file": "correction-hat.wav", + "sha256": "f9f98c91cfbcfcb106eef7d5d3795a9cf3c3aab7e19a60f13d7dffe284d19f86", + "bytes": 398636, + "sampleRate": 24000, + "channels": 1, + "duration": 8.304, + "rms": 0.10000141071544869, + "peak": 0.470977783203125 + }, + { + "file": "correction-umbrella.wav", + "sha256": "92b54f04884ab915783945077efd8f3bd443849a1720f72168b6348cae1f8e85", + "bytes": 406700, + "sampleRate": 24000, + "channels": 1, + "duration": 8.472, + "rms": 0.10000140257790288, + "peak": 0.479034423828125 + }, + { + "file": "correction-market.wav", + "sha256": "b8d25b867497adcd0fe7c8b6a8a8d2e7a52965956738a83e53f58735708773f9", + "bytes": 405548, + "sampleRate": 24000, + "channels": 1, + "duration": 8.448, + "rms": 0.10000141405173993, + "peak": 0.491485595703125 + }, + { + "file": "correction-oil.wav", + "sha256": "f6eff0cdddcad9790351491e60c53cb2a70d222033a583652f2de0b8cd6abcc7", + "bytes": 406700, + "sampleRate": 24000, + "channels": 1, + "duration": 8.472, + "rms": 0.10000139122025363, + "peak": 0.470794677734375 + }, + { + "file": "correction-sound.wav", + "sha256": "5a603bc69979603afddcaeb6edf500a7c540b72405daeb33f5335d746b327da1", + "bytes": 397484, + "sampleRate": 24000, + "channels": 1, + "duration": 8.28, + "rms": 0.10000146491234768, + "peak": 0.469268798828125 + }, + { + "file": "correction-meat.wav", + "sha256": "48e91b3932d3798e852041a854b3ee08c72127e092c62083a47af6b99adff241", + "bytes": 403244, + "sampleRate": 24000, + "channels": 1, + "duration": 8.4, + "rms": 0.10000140336390047, + "peak": 0.481536865234375 + }, + { + "file": "correction-furniture.wav", + "sha256": "5c004a7b2f5a4a918c9f689f1a2dcb219a620ed14313f40a9655b36dbf64948d", + "bytes": 398636, + "sampleRate": 24000, + "channels": 1, + "duration": 8.304, + "rms": 0.10000141690978645, + "peak": 0.487396240234375 + }, + { + "file": "middle-apple.wav", + "sha256": "8832f1b793ab3eedbe663875ea8dbeabb122c329bd5879437c71230b0418c3c8", + "bytes": 360620, + "sampleRate": 24000, + "channels": 1, + "duration": 7.512, + "rms": 0.10000145024648589, + "peak": 0.553558349609375 + }, + { + "file": "middle-sea.wav", + "sha256": "b6dbe0af143ea775f05bd59a3d6af348bcc770ab378ac91ea75a56e06c326edf", + "bytes": 346796, + "sampleRate": 24000, + "channels": 1, + "duration": 7.224, + "rms": 0.10000144532868623, + "peak": 0.56494140625 + }, + { + "file": "middle-tree.wav", + "sha256": "341888c75275c13eb31058f53a1d4276be0daa0a1b5a3a45522036928b83b0c1", + "bytes": 382508, + "sampleRate": 24000, + "channels": 1, + "duration": 7.968, + "rms": 0.1000014873304774, + "peak": 0.600024402141571 + }, + { + "file": "middle-hat.wav", + "sha256": "b01ef9762ac84e3eb757243bc9286c62f7d45444a821ae35ac36a8b095e1598a", + "bytes": 353708, + "sampleRate": 24000, + "channels": 1, + "duration": 7.368, + "rms": 0.10000153089787694, + "peak": 0.555206298828125 + }, + { + "file": "middle-umbrella.wav", + "sha256": "399ea2f681f755918f1117c8baba42739ceca64b307b999342f457a9235050b6", + "bytes": 368684, + "sampleRate": 24000, + "channels": 1, + "duration": 7.68, + "rms": 0.10000155028684544, + "peak": 0.569549560546875 + }, + { + "file": "middle-market.wav", + "sha256": "c855b449e0d4ace74681bfe68e5e7f0b063568827ae872017234dbe8ae119e4c", + "bytes": 356012, + "sampleRate": 24000, + "channels": 1, + "duration": 7.416, + "rms": 0.10000151456676445, + "peak": 0.5949888825416565 + }, + { + "file": "middle-oil.wav", + "sha256": "7a6b122b11c07da86c95fb78e2db2a70681b5219c63d6441fe56da1979ac1dc3", + "bytes": 334124, + "sampleRate": 24000, + "channels": 1, + "duration": 6.96, + "rms": 0.1000014880654183, + "peak": 0.539933443069458 + }, + { + "file": "middle-sound.wav", + "sha256": "4d00aa6114327f450a00dacbc7e7b572ebf1de4ce9dc2fe4b5505766cca71ce9", + "bytes": 353708, + "sampleRate": 24000, + "channels": 1, + "duration": 7.368, + "rms": 0.10000162019345048, + "peak": 0.5348368883132935 + }, + { + "file": "middle-meat.wav", + "sha256": "d416107cc05e7c5b92ca27100a10a8a9689de913346179945d5542de4a6bac0c", + "bytes": 342188, + "sampleRate": 24000, + "channels": 1, + "duration": 7.128, + "rms": 0.10000157422522808, + "peak": 0.510833740234375 + }, + { + "file": "middle-furniture.wav", + "sha256": "b45b1afc8d9b534ff3e0a2a2ecb99efc1f4e40d41f89f55c4ed8ba94ad6d76d4", + "bytes": 354860, + "sampleRate": 24000, + "channels": 1, + "duration": 7.392, + "rms": 0.1000015072973225, + "peak": 0.5576171875 + }, + { + "file": "message-apple.wav", + "sha256": "d85e6f47d871369f00de2274139e5ad6b4b5686b5e20e248df0ed6b9a320d942", + "bytes": 517292, + "sampleRate": 24000, + "channels": 1, + "duration": 10.776, + "rms": 0.10000133916854889, + "peak": 0.525360107421875 + }, + { + "file": "message-sea.wav", + "sha256": "28a461fbe03bffe2560030dea8b60715f7c0fe262dcf85b5b7f7daf483836382", + "bytes": 517292, + "sampleRate": 24000, + "channels": 1, + "duration": 10.776, + "rms": 0.10000130584401576, + "peak": 0.53521728515625 + }, + { + "file": "message-tree.wav", + "sha256": "5d684aa710a63d36ed8dcc2a8bfbdd5c71b0b1c63dad2a324adecb76591e85b8", + "bytes": 540332, + "sampleRate": 24000, + "channels": 1, + "duration": 11.256, + "rms": 0.10000132424453942, + "peak": 0.5262451171875 + }, + { + "file": "message-hat.wav", + "sha256": "5a713a8522b231bb8276805c014b385b9150b79f58ff2618446607e2b442495e", + "bytes": 518444, + "sampleRate": 24000, + "channels": 1, + "duration": 10.8, + "rms": 0.10000136927555209, + "peak": 0.587005615234375 + }, + { + "file": "message-umbrella.wav", + "sha256": "41d5866dae66600df4694a8312b531571d44cece6dba0e423e7c43d09d184819", + "bytes": 523052, + "sampleRate": 24000, + "channels": 1, + "duration": 10.896, + "rms": 0.10000120668553683, + "peak": 0.530364990234375 + }, + { + "file": "message-market.wav", + "sha256": "9242d4cd6e0030e6b073e4d2360f84aa8a454d1749711cdb2d64c14f28a7ea20", + "bytes": 523052, + "sampleRate": 24000, + "channels": 1, + "duration": 10.896, + "rms": 0.10000133805787564, + "peak": 0.534820556640625 + }, + { + "file": "message-oil.wav", + "sha256": "a2a71a7e5e07c81871a25c6cb7540c39aa0b32fc44f78cda66b5f34ace98d805", + "bytes": 509228, + "sampleRate": 24000, + "channels": 1, + "duration": 10.608, + "rms": 0.10000135184106136, + "peak": 0.51812744140625 + }, + { + "file": "message-sound.wav", + "sha256": "1e682147dae8026ba78dc8f256c7f24acb3a6743bfd88a0fcc86096930eef078", + "bytes": 521900, + "sampleRate": 24000, + "channels": 1, + "duration": 10.872, + "rms": 0.10000138583471241, + "peak": 0.51953125 + }, + { + "file": "message-meat.wav", + "sha256": "ce19735df079b54c171e775b6b365385a80160b9033442abab95b2b9c8505e1a", + "bytes": 512684, + "sampleRate": 24000, + "channels": 1, + "duration": 10.68, + "rms": 0.10000140834686953, + "peak": 0.5147705078125 + }, + { + "file": "message-furniture.wav", + "sha256": "26c331b7e12311236f4bb48f88918dfbf53cccd2f50b55741ea6ff4473e71dd8", + "bytes": 521900, + "sampleRate": 24000, + "channels": 1, + "duration": 10.872, + "rms": 0.10000134596838128, + "peak": 0.529327392578125 + }, + { + "file": "mission-apple.wav", + "sha256": "3ef42e276ff3500c3e197d149f29f79d6a0c1d5516ed1d5a2c57210333fded9c", + "bytes": 246572, + "sampleRate": 24000, + "channels": 1, + "duration": 5.136, + "rms": 0.10000144279302126, + "peak": 0.4701681435108185 + }, + { + "file": "mission-sea.wav", + "sha256": "8838d6a1ee8a790427dc5aa761e70b3d8872af62086efeed743297ff7f8b8a06", + "bytes": 243116, + "sampleRate": 24000, + "channels": 1, + "duration": 5.064, + "rms": 0.10000138788281714, + "peak": 0.45013427734375 + }, + { + "file": "mission-tree.wav", + "sha256": "00d8e77ec0371971d632724d91f8cf8d947bfb83c582a513b6421be4c457a1da", + "bytes": 241964, + "sampleRate": 24000, + "channels": 1, + "duration": 5.04, + "rms": 0.10000146153372488, + "peak": 0.4585406184196472 + }, + { + "file": "mission-hat.wav", + "sha256": "308278ec2bd27d4224976a55632d1f7fa1edabb0792126d9d858a699c287aee4", + "bytes": 245420, + "sampleRate": 24000, + "channels": 1, + "duration": 5.112, + "rms": 0.10000140702099906, + "peak": 0.4498428404331207 + }, + { + "file": "mission-umbrella.wav", + "sha256": "4b4c858a25659540f9dbc524664496fc6de61ab8c1627f3cfee8c9c744e11d5b", + "bytes": 247724, + "sampleRate": 24000, + "channels": 1, + "duration": 5.16, + "rms": 0.10000144286672341, + "peak": 0.46534621715545654 + }, + { + "file": "mission-market.wav", + "sha256": "cefa0e95649ea6bd1df4c8dfccd2b74b4f4c028714420ce1087b61db2f2a8d47", + "bytes": 248876, + "sampleRate": 24000, + "channels": 1, + "duration": 5.184, + "rms": 0.10000142548167161, + "peak": 0.4857020676136017 + }, + { + "file": "mission-oil.wav", + "sha256": "5fb63e04520c312aa6b7b48b70c2f7c140b5ab480e5c6992edbc2ba85b9a06e7", + "bytes": 241964, + "sampleRate": 24000, + "channels": 1, + "duration": 5.04, + "rms": 0.10000143720418167, + "peak": 0.443586528301239 + }, + { + "file": "mission-sound.wav", + "sha256": "659eb0e2b06e6b165b46744769743e032e2282519f8486c57bc45e0f7d07a069", + "bytes": 240812, + "sampleRate": 24000, + "channels": 1, + "duration": 5.016, + "rms": 0.10000152909529536, + "peak": 0.45481735467910767 + }, + { + "file": "mission-meat.wav", + "sha256": "94f8a4f9e116a787e5ed052661d6de9ecf3ad8629db476661dd59ad03014b0c9", + "bytes": 244268, + "sampleRate": 24000, + "channels": 1, + "duration": 5.088, + "rms": 0.10000146176253134, + "peak": 0.44187748432159424 + }, + { + "file": "mission-furniture.wav", + "sha256": "af82e03b686635c3356e706fe5b05c1231aeaaeab33f275aa5b8714988c65d43", + "bytes": 241964, + "sampleRate": 24000, + "channels": 1, + "duration": 5.04, + "rms": 0.10000137346142077, + "peak": 0.4852443039417267 + } + ] +} diff --git a/.agent/evidence-v0.1.1/changed-files.txt b/.agent/evidence-v0.1.1/changed-files.txt new file mode 100644 index 0000000..2f43313 --- /dev/null +++ b/.agent/evidence-v0.1.1/changed-files.txt @@ -0,0 +1,161 @@ +ADDED .agent/RESULT-v0.1.1.md +ADDED .agent/evidence-v0.1.1/00-audio-venv.log +ADDED .agent/evidence-v0.1.1/00-npm-ci.log +ADDED .agent/evidence-v0.1.1/01-version-docs-red.log +ADDED .agent/evidence-v0.1.1/02-assets-red.log +ADDED .agent/evidence-v0.1.1/03-generator-red.log +ADDED .agent/evidence-v0.1.1/04-generator-additional-red.log +ADDED .agent/evidence-v0.1.1/05-generator-green.log +ADDED .agent/evidence-v0.1.1/06-audio-generate.log +ADDED .agent/evidence-v0.1.1/07-format.log +ADDED .agent/evidence-v0.1.1/08-version-docs-green.log +ADDED .agent/evidence-v0.1.1/09-lint.log +ADDED .agent/evidence-v0.1.1/10-audit.log +ADDED .agent/evidence-v0.1.1/11-full-test.log +ADDED .agent/evidence-v0.1.1/12-independent-audio.log +ADDED .agent/evidence-v0.1.1/13-build.log +ADDED .agent/evidence-v0.1.1/14-browser.log +ADDED .agent/evidence-v0.1.1/15-browser-audio.log +ADDED .agent/evidence-v0.1.1/16-final-lint.log +ADDED .agent/evidence-v0.1.1/17-security-history.json +ADDED .agent/evidence-v0.1.1/18-environment.log +ADDED .agent/evidence-v0.1.1/19-final-integrity.json +ADDED .agent/evidence-v0.1.1/audio-integrity.json +ADDED .agent/evidence-v0.1.1/baseline.json +ADDED .agent/evidence-v0.1.1/browser-audio-integrity.json +ADDED .agent/evidence-v0.1.1/changed-files.txt +ADDED .agent/evidence-v0.1.1/desktop-game.png +ADDED .agent/evidence-v0.1.1/desktop-heading.png +ADDED .agent/evidence-v0.1.1/desktop-home.png +ADDED .agent/evidence-v0.1.1/desktop-results.png +ADDED .agent/evidence-v0.1.1/mobile-game.png +ADDED .agent/evidence-v0.1.1/mobile-heading.png +ADDED .agent/evidence-v0.1.1/mobile-home.png +ADDED .agent/evidence-v0.1.1/mobile-results.png +ADDED .agent/evidence-v0.1.1/verify_audio.py +ADDED .agent/evidence-v0.1.1/verify_browser.mjs +ADDED requirements-audio.txt +ADDED scripts/neural_audio.py +ADDED tests/generator.test.ts +ADDED tests/neural-release.test.ts +ADDED tests/python/test_neural_audio.py +MODIFIED .gitignore +MODIFIED LICENSE +MODIFIED README.md +MODIFIED docs/ARCHITECTURE.md +MODIFIED docs/AUDIO_PROVENANCE.md +MODIFIED docs/DEPLOYMENT.md +MODIFIED eslint.config.js +MODIFIED index.html +MODIFIED package-lock.json +MODIFIED package.json +MODIFIED public/audio/correction-apple.wav +MODIFIED public/audio/correction-furniture.wav +MODIFIED public/audio/correction-hat.wav +MODIFIED public/audio/correction-market.wav +MODIFIED public/audio/correction-meat.wav +MODIFIED public/audio/correction-oil.wav +MODIFIED public/audio/correction-sea.wav +MODIFIED public/audio/correction-sound.wav +MODIFIED public/audio/correction-tree.wav +MODIFIED public/audio/correction-umbrella.wav +MODIFIED public/audio/first-apple.wav +MODIFIED public/audio/first-furniture.wav +MODIFIED public/audio/first-hat.wav +MODIFIED public/audio/first-market.wav +MODIFIED public/audio/first-meat.wav +MODIFIED public/audio/first-oil.wav +MODIFIED public/audio/first-sea.wav +MODIFIED public/audio/first-sound.wav +MODIFIED public/audio/first-tree.wav +MODIFIED public/audio/first-umbrella.wav +MODIFIED public/audio/intro.wav +MODIFIED public/audio/last-apple.wav +MODIFIED public/audio/last-furniture.wav +MODIFIED public/audio/last-hat.wav +MODIFIED public/audio/last-market.wav +MODIFIED public/audio/last-meat.wav +MODIFIED public/audio/last-oil.wav +MODIFIED public/audio/last-sea.wav +MODIFIED public/audio/last-sound.wav +MODIFIED public/audio/last-tree.wav +MODIFIED public/audio/last-umbrella.wav +MODIFIED public/audio/luck-apple.wav +MODIFIED public/audio/luck-furniture.wav +MODIFIED public/audio/luck-hat.wav +MODIFIED public/audio/luck-market.wav +MODIFIED public/audio/luck-meat.wav +MODIFIED public/audio/luck-oil.wav +MODIFIED public/audio/luck-sea.wav +MODIFIED public/audio/luck-sound.wav +MODIFIED public/audio/luck-tree.wav +MODIFIED public/audio/luck-umbrella.wav +MODIFIED public/audio/manifest.json +MODIFIED public/audio/message-apple.wav +MODIFIED public/audio/message-furniture.wav +MODIFIED public/audio/message-hat.wav +MODIFIED public/audio/message-market.wav +MODIFIED public/audio/message-meat.wav +MODIFIED public/audio/message-oil.wav +MODIFIED public/audio/message-sea.wav +MODIFIED public/audio/message-sound.wav +MODIFIED public/audio/message-tree.wav +MODIFIED public/audio/message-umbrella.wav +MODIFIED public/audio/middle-apple.wav +MODIFIED public/audio/middle-furniture.wav +MODIFIED public/audio/middle-hat.wav +MODIFIED public/audio/middle-market.wav +MODIFIED public/audio/middle-meat.wav +MODIFIED public/audio/middle-oil.wav +MODIFIED public/audio/middle-sea.wav +MODIFIED public/audio/middle-sound.wav +MODIFIED public/audio/middle-tree.wav +MODIFIED public/audio/middle-umbrella.wav +MODIFIED public/audio/mission-apple.wav +MODIFIED public/audio/mission-furniture.wav +MODIFIED public/audio/mission-hat.wav +MODIFIED public/audio/mission-market.wav +MODIFIED public/audio/mission-meat.wav +MODIFIED public/audio/mission-oil.wav +MODIFIED public/audio/mission-sea.wav +MODIFIED public/audio/mission-sound.wav +MODIFIED public/audio/mission-tree.wav +MODIFIED public/audio/mission-umbrella.wav +MODIFIED public/audio/password-apple.wav +MODIFIED public/audio/password-furniture.wav +MODIFIED public/audio/password-hat.wav +MODIFIED public/audio/password-market.wav +MODIFIED public/audio/password-meat.wav +MODIFIED public/audio/password-oil.wav +MODIFIED public/audio/password-sea.wav +MODIFIED public/audio/password-sound.wav +MODIFIED public/audio/password-tree.wav +MODIFIED public/audio/password-umbrella.wav +MODIFIED public/audio/remember-apple.wav +MODIFIED public/audio/remember-furniture.wav +MODIFIED public/audio/remember-hat.wav +MODIFIED public/audio/remember-market.wav +MODIFIED public/audio/remember-meat.wav +MODIFIED public/audio/remember-oil.wav +MODIFIED public/audio/remember-sea.wav +MODIFIED public/audio/remember-sound.wav +MODIFIED public/audio/remember-tree.wav +MODIFIED public/audio/remember-umbrella.wav +MODIFIED public/audio/repeat-apple.wav +MODIFIED public/audio/repeat-furniture.wav +MODIFIED public/audio/repeat-hat.wav +MODIFIED public/audio/repeat-market.wav +MODIFIED public/audio/repeat-meat.wav +MODIFIED public/audio/repeat-oil.wav +MODIFIED public/audio/repeat-sea.wav +MODIFIED public/audio/repeat-sound.wav +MODIFIED public/audio/repeat-tree.wav +MODIFIED public/audio/repeat-umbrella.wav +MODIFIED scripts/generate_audio.py +MODIFIED src/App.tsx +MODIFIED tests/App.test.tsx +MODIFIED tests/assets.test.ts +MODIFIED tests/browser/game.spec.ts +MODIFIED tests/browser/ui.spec.ts +USER INPUT .agent/ACCEPTANCE-v0.1.1.md +USER INPUT .agent/TASK-v0.1.1.md diff --git a/.agent/evidence-v0.1.1/desktop-game.png b/.agent/evidence-v0.1.1/desktop-game.png new file mode 100644 index 0000000..7c319c6 Binary files /dev/null and b/.agent/evidence-v0.1.1/desktop-game.png differ diff --git a/.agent/evidence-v0.1.1/desktop-heading.png b/.agent/evidence-v0.1.1/desktop-heading.png new file mode 100644 index 0000000..c60a60a Binary files /dev/null and b/.agent/evidence-v0.1.1/desktop-heading.png differ diff --git a/.agent/evidence-v0.1.1/desktop-home.png b/.agent/evidence-v0.1.1/desktop-home.png new file mode 100644 index 0000000..1b6d36d Binary files /dev/null and b/.agent/evidence-v0.1.1/desktop-home.png differ diff --git a/.agent/evidence-v0.1.1/desktop-results.png b/.agent/evidence-v0.1.1/desktop-results.png new file mode 100644 index 0000000..d91fe66 Binary files /dev/null and b/.agent/evidence-v0.1.1/desktop-results.png differ diff --git a/.agent/evidence-v0.1.1/mobile-game.png b/.agent/evidence-v0.1.1/mobile-game.png new file mode 100644 index 0000000..c033b1d Binary files /dev/null and b/.agent/evidence-v0.1.1/mobile-game.png differ diff --git a/.agent/evidence-v0.1.1/mobile-heading.png b/.agent/evidence-v0.1.1/mobile-heading.png new file mode 100644 index 0000000..423961b Binary files /dev/null and b/.agent/evidence-v0.1.1/mobile-heading.png differ diff --git a/.agent/evidence-v0.1.1/mobile-home.png b/.agent/evidence-v0.1.1/mobile-home.png new file mode 100644 index 0000000..1347e17 Binary files /dev/null and b/.agent/evidence-v0.1.1/mobile-home.png differ diff --git a/.agent/evidence-v0.1.1/mobile-results.png b/.agent/evidence-v0.1.1/mobile-results.png new file mode 100644 index 0000000..917dd91 Binary files /dev/null and b/.agent/evidence-v0.1.1/mobile-results.png differ diff --git a/.agent/evidence-v0.1.1/verify_audio.py b/.agent/evidence-v0.1.1/verify_audio.py new file mode 100644 index 0000000..135e620 --- /dev/null +++ b/.agent/evidence-v0.1.1/verify_audio.py @@ -0,0 +1,71 @@ +"""생성기 함수를 재사용하지 않는 v0.1.1 인수 검사: 원문·이전 해시·PCM·FFmpeg.""" +from array import array +import hashlib +import json +import math +from pathlib import Path +import subprocess +import sys +import wave + +root = Path(__file__).resolve().parents[2] +evidence = root / ".agent/evidence-v0.1.1" +manifest = json.loads((root / "public/audio/manifest.json").read_text()) +baseline = json.loads((evidence / "baseline.json").read_text()) +old = {entry["file"]: entry for entry in baseline["audio"]["files"]} +expected = json.loads(subprocess.check_output([ + "node", "--experimental-strip-types", "--input-type=module", "-e", + "import {INTRO,PATTERNS,WORDS,renderSentence} from './src/content.ts'; " + "console.log(JSON.stringify([{file:'intro.wav',text:INTRO}," + "...PATTERNS.flatMap(p=>WORDS.map(w=>({file:p.id+'-'+w.id+'.wav',text:renderSentence(p,w)})))]));" +], cwd=root, text=True)) +assert manifest["version"] == "0.1.1" +assert manifest["provider"] == "Microsoft Edge online TTS" +assert manifest["voice"] == "ko-KR-HyunsuMultilingualNeural" +assert manifest["rate"] == "-6%" and manifest["pitch"] == "-4Hz" +assert manifest["toolVersions"]["edge-tts"] == "7.3.1" +assert [{"file": e["file"], "text": e["text"]} for e in manifest["files"]] == expected +assert len(expected) == 101 +assert {p.name for p in (root / "public/audio").iterdir()} == {e["file"] for e in expected} | {"manifest.json"} +rows = [] +for entry in manifest["files"]: + path = root / "public/audio" / entry["file"] + data = path.read_bytes() + digest = hashlib.sha256(data).hexdigest() + assert digest == entry["sha256"] and digest != old[entry["file"]]["sha256"] + assert len(data) == entry["bytes"] + with wave.open(str(path), "rb") as stream: + assert (stream.getframerate(), stream.getnchannels(), stream.getsampwidth(), stream.getcomptype()) == (24000, 1, 2, "NONE") + duration = stream.getnframes()/24000 + assert 1 <= duration <= 20 and duration == entry["duration"] + samples = array("h", stream.readframes(stream.getnframes())) + if sys.byteorder != "little": + samples.byteswap() + rms = math.sqrt(sum(x*x for x in samples)/len(samples))/32768 + peak = max(abs(x) for x in samples)/32768 + assert rms > 100/32768 and peak < 32767/32768 + assert peak/rms*0.03*2 < 0.8 + assert abs(entry["rms"]-rms) < 1e-12 and abs(entry["peak"]-peak) < 1e-12 + probe = json.loads(subprocess.check_output([ + "ffprobe", "-v", "error", "-show_entries", + "stream=codec_name,sample_fmt,sample_rate,channels,bits_per_sample,duration", "-of", "json", str(path) + ], text=True))["streams"] + assert len(probe) == 1 + stream = probe[0] + assert (stream["codec_name"], stream["sample_fmt"], stream["sample_rate"], stream["channels"], stream["bits_per_sample"]) == ("pcm_s16le", "s16", "24000", 1, 16) + assert abs(float(stream["duration"])-duration) < 1e-5 + subprocess.run(["ffmpeg", "-v", "error", "-xerror", "-i", str(path), "-f", "null", "-"], check=True, capture_output=True) + rows.append({"file": path.name, "sha256": digest, "bytes": len(data), "duration": duration, + "rms": rms, "peak": peak, "ffprobe": stream, "newVsV01": True, "ffmpegDecoded": True}) + print(f"PASS {path.name}: {duration:.3f}s, RMS={rms:.6f}, peak={peak:.6f}", flush=True) +assert len({r["sha256"] for r in rows}) == 101 +assert sum(r["bytes"] for r in rows) < 48*1024*1024 +for path, digest in baseline["historical"].items(): + assert hashlib.sha256((root / path).read_bytes()).hexdigest() == digest, path +summary = {"count": len(rows), "newVsV01": 101, "uniqueHashes": 101, "bytes": sum(r["bytes"] for r in rows), + "durationRange": [min(r["duration"] for r in rows), max(r["duration"] for r in rows)], + "rmsRange": [min(r["rms"] for r in rows), max(r["rms"] for r in rows)], + "peakRange": [min(r["peak"] for r in rows), max(r["peak"] for r in rows)], + "historicalUnchanged": len(baseline["historical"]), "files": rows} +(evidence / "audio-integrity.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2)+"\n") +print(json.dumps({k:v for k,v in summary.items() if k != "files"}, indent=2)) diff --git a/.agent/evidence-v0.1.1/verify_browser.mjs b/.agent/evidence-v0.1.1/verify_browser.mjs new file mode 100644 index 0000000..1a020fc --- /dev/null +++ b/.agent/evidence-v0.1.1/verify_browser.mjs @@ -0,0 +1,64 @@ +/* global OfflineAudioContext */ +// 실제 Chrome의 WAV 디코더로 101개를 검증한다. 재생·UI 흐름은 전체 +// Playwright suite가 별도 담당하며 이 인수 검사는 모의 AudioContext를 쓰지 않는다. +import assert from 'node:assert/strict'; +import { writeFileSync } from 'node:fs'; +import { spawn } from 'node:child_process'; +import { chromium } from '@playwright/test'; + +const server = spawn(process.execPath, ['node_modules/vite/bin/vite.js', 'preview', '--host', '127.0.0.1', '--port', '4174', '--strictPort'], { stdio: 'ignore' }); +const base = 'http://127.0.0.1:4174'; +let browser; +try { + for (let i = 0; ; i++) { + try { if ((await fetch(base)).ok) break; } catch { /* localhost 준비만 기다린다. */ } + assert(i < 100, 'preview startup failed'); + await new Promise(resolve => setTimeout(resolve, 100)); + } + browser = await chromium.launch({ executablePath: '/usr/bin/google-chrome', chromiumSandbox: true }); + const sandbox = await browser.newPage(); + await sandbox.goto('chrome://sandbox'); + const sandboxStatus = await sandbox.locator('body').innerText(); + console.log('SANDBOX STATUS\n' + sandboxStatus); + assert.match(sandboxStatus, /Seccomp-BPF sandbox\s+Yes/); + await sandbox.close(); + const page = await browser.newPage(); + const external = [], errors = []; + page.on('request', r => { if (!r.url().startsWith(base + '/')) external.push(r.url()); }); + page.on('pageerror', e => errors.push(e.message)); + await page.goto(base); + assert.equal(await page.title(), '단어를 맞춰라 · Find-Word v0.1.1'); + assert.equal(await page.locator('.version').innerText(), 'v0.1.1'); + const result = await page.evaluate(async () => { + const manifest = await (await fetch('/audio/manifest.json')).json(); + const context = new OfflineAudioContext(1, 1, 24000); + const rows = []; + for (const entry of manifest.files) { + const response = await fetch('/audio/' + entry.file); + if (!response.ok) throw Error(entry.file + ': HTTP failure'); + const bytes = await response.arrayBuffer(); + const digest = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), b => b.toString(16).padStart(2, '0')).join(''); + if (digest !== entry.sha256 || bytes.byteLength !== entry.bytes) throw Error(entry.file + ': integrity failure'); + const buffer = await context.decodeAudioData(bytes); + let power = 0, peak = 0; + for (const x of buffer.getChannelData(0)) { power += x*x; peak = Math.max(peak, Math.abs(x)); } + if (buffer.sampleRate !== 24000 || buffer.numberOfChannels !== 1 || Math.abs(buffer.duration-entry.duration)>1e-9) + throw Error(entry.file + ': decode format/duration mismatch'); + const rms = Math.sqrt(power/buffer.length); + if (rms <= 100/32768 || peak >= 32767/32768) throw Error(entry.file + ': signal failure'); + rows.push({ file: entry.file, sha256: digest, bytes: entry.bytes, sampleRate: buffer.sampleRate, + channels: buffer.numberOfChannels, duration: buffer.duration, rms, peak }); + } + return { version: manifest.version, voice: manifest.voice, files: rows }; + }); + assert.equal(result.version, '0.1.1'); + assert.equal(result.voice, 'ko-KR-HyunsuMultilingualNeural'); + assert.equal(result.files.length, 101); + assert.deepEqual(external, []); + assert.deepEqual(errors, []); + writeFileSync('.agent/evidence-v0.1.1/browser-audio-integrity.json', JSON.stringify({ chrome: browser.version(), sandboxStatus, external, errors, ...result }, null, 2)+'\n'); + console.log('PASS: sandboxed Chrome decoded 101/101 WAVs at 24kHz mono; hashes, bytes, duration, RMS, peak verified; external requests=0, JS errors=0.'); +} finally { + await browser?.close(); + server.kill('SIGTERM'); +} diff --git a/.gitignore b/.gitignore index 9e71100..6a0fdbe 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ dist/ test-results/ playwright-report/ *.local +.venv-audio/ +__pycache__/ diff --git a/LICENSE b/LICENSE index 15e763e..835d2c7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,14 @@ -Find-Word v0.1 +Find-Word v0.1.1 Private / proprietary. All rights reserved. No license is granted to use, copy, modify, publish, distribute, sublicense, -or sell the original project code, documentation, or assets without prior +or sell the original project code or documentation without prior written permission from the respective rights holders. Third-party components remain subject to their own licenses. Their notices are reproduced in THIRD_PARTY_NOTICES.md and accompany the distribution. + +This project does not claim exclusive rights to generated speech. +Provider terms and any third-party rights remain applicable to generated audio; +the project notice does not grant or warrant rights in that output. +See docs/AUDIO_PROVENANCE.md. diff --git a/README.md b/README.md index 52367a3..c85395a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 단어를 맞춰라 · Find-Word v0.1 +# 단어를 맞춰라 · Find-Word v0.1.1 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. @@ -79,17 +79,35 @@ Chromium sandbox는 기본으로 켭니다. sandbox를 지원하지 않는 격 브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요. 실제 음성 재생이 끝나기를 기다려 약 2–3분이 걸립니다. 로컬 4173 포트를 -테스트 중에만 열고 종료합니다. 스크린샷은 `.agent/evidence/`에 남습니다. +테스트 중에만 열고 종료합니다. v0.1.1 스크린샷은 `.agent/evidence-v0.1.1/`에 남습니다. ## 음성과 문서 -한국어 WAV는 직접 쓴 문장을 로컬 eSpeak NG로 합성했습니다. 환경음은 -브라우저에서 고정 seed로 합성합니다. 외부 음원이나 신경망 음성 모델을 사용하지 않습니다. +한국어 WAV 101개는 직접 쓴 문장을 Microsoft Edge online TTS의 +`ko-KR-HyunsuMultilingualNeural`로 새로 생성했습니다. edge-tts **7.3.1**, +rate `-6%`, pitch `-4Hz`이며 FFmpeg로 **24kHz mono PCM16**으로 변환합니다. +생성 시 네트워크가 필요하며 앱은 저장된 WAV만 재생합니다. 환경음은 브라우저에서 +고정 seed로 합성합니다. 생성 음성의 독점 권리를 주장하지 않습니다. + +Linux에서 Python 3.11 이상과 FFmpeg를 준비한 뒤 다음 명령을 사용합니다. +클라이언트 버전을 고정해도 원격 모델 변경으로 동일 바이트 재생성은 보장하지 않습니다. + +```bash +python3 -m venv .venv-audio +.venv-audio/bin/python -m pip install -r requirements-audio.txt +.venv-audio/bin/python -m pip check +npm run audio:generate +``` + +고정 venv의 Python이 101개 모두 임시 생성·검증하고 디렉터리를 원자 교체합니다. +요청별 60초 제한과 최대 3회 시도를 적용하며, 실패 시 기존 자산을 보존합니다. +청취 샘플: [안내](public/audio/intro.wav), [행운의 사과](public/audio/luck-apple.wav). 생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를 참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다. - [설계와 상태 전이](docs/ARCHITECTURE.md) - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) -- [RED/GREEN 및 실제 검증 결과](.agent/RESULT.md) +- [v0.1.1 RED/GREEN 및 실제 검증 결과](.agent/RESULT-v0.1.1.md) +- [v0.1 역사 기록](.agent/RESULT.md) 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7fcc5aa..bfd1e11 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# 설계 +# Find-Word v0.1.1 설계 ## 데이터와 출제 @@ -67,6 +67,11 @@ Promise를 무시한다. 질문과 보기는 재생 완료 후 DOM에 생성되 ## 오디오 +101개 저장 WAV는 edge-tts 7.3.1의 `ko-KR-HyunsuMultilingualNeural`로 +빌드 전에 생성한다. 24kHz mono PCM16이며 원문·도구·해시는 manifest에 남긴다. +생성기는 임시 디렉터리에서 전체 검증 후 원자 교체한다. 생성 절차는 +[AUDIO_PROVENANCE](AUDIO_PROVENANCE.md)에 있으며 앱은 provider에 연결하지 않는다. + `src/audio/dsp.ts`는 순수 수치 함수이며 장치가 없어도 테스트할 수 있다. 정수 seed 잡음에 저역 통과 필터, 사인파, 진폭 변조를 조합하고 양 끝을 페이드한다. 8개 프리셋은 실제 현장 녹음이 아니라 환경을 추상적으로 표현한 합성 신호다. diff --git a/docs/AUDIO_PROVENANCE.md b/docs/AUDIO_PROVENANCE.md index 583cc94..cb87347 100644 --- a/docs/AUDIO_PROVENANCE.md +++ b/docs/AUDIO_PROVENANCE.md @@ -1,83 +1,103 @@ -# 오디오 출처와 재현 +# Find-Word v0.1.1 오디오 출처와 재현 -## 포함 자산 +## 포함 자산과 생성 설정 | 대상 | 생성 방식 | 원문/설정 | 배포 방식 | | --- | --- | --- | --- | -| `public/audio/intro.wav` | 로컬 eSpeak NG 한국어 포먼트 합성 | `src/content.ts`의 INTRO | PCM WAV | -| 목표 음성 100개 | 같은 로컬 엔진 | 10 패턴 × 10 단어 | PCM WAV | -| 길거리·쇼핑몰·극장·백화점·시장·지하철·카페·비 오는 거리 | 직접 작성한 결정론적 DSP | `ENVIRONMENTS`, 고정 seed | 브라우저 메모리에서 합성 | -| 화면 파형·S 캐릭터 | 직접 작성한 CSS | `App.tsx`, `styles.css` | CSS, 외부 이미지 없음 | +| `public/audio/intro.wav` | Microsoft Edge online TTS neural 음성 | `src/content.ts`의 INTRO | 24kHz mono PCM16 WAV | +| 목표 음성 100개 | 같은 provider·voice | 10 패턴 × 10 단어, `renderSentence` | 같은 WAV | +| 8개 배경 환경 | 직접 작성한 결정론적 DSP | `ENVIRONMENTS`, 고정 seed | 브라우저 메모리에서 합성 | +| 화면 파형·S 캐릭터 | 직접 작성한 CSS | `App.tsx`, `styles.css` | CSS | -모든 한국어 문장은 이번 앱을 위해 직접 작성했다. 다른 사람의 녹음, 노래, -샘플 라이브러리, 음성 복제, 신경망 모델을 포함하지 않는다. 웃음도 INTRO의 -‘으하하하~~’ 문자열을 동일 엔진으로 합성한 것이다. 효과를 위해 타인 음원을 -다운로드하지 않았다. 런타임에 speechSynthesis 또는 네트워크 TTS도 호출하지 않는다. +voice는 **`ko-KR-HyunsuMultilingualNeural`**, rate는 **`-6%`**, pitch는 +**`-4Hz`**, 생성 클라이언트는 **edge-tts 7.3.1**이다. 직접 작성한 한국어 +문장을 온라인 provider에 보내 MP3 응답을 받은 뒤 FFmpeg로 PCM을 만든다. +음성 복제나 사람의 녹음 샘플을 사용하지 않는다. 웃음은 INTRO의 ‘으하하하~~’를 +같은 voice로 합성한다. 앱은 저장한 WAV만 같은 출처에서 가져오며 런타임 외부 +TTS/API, speechSynthesis, 인증정보가 필요하지 않다. -WAV는 16kHz, mono, signed 16-bit little-endian PCM이다. 101개 합계는 -**20,497,222바이트**다. 파일별 원문·바이트 수·SHA-256은 -`public/audio/manifest.json`에 있다. 생성 시 실행 도구에서 읽은 eSpeak NG 1.51, -FFmpeg 6.1.1-3ubuntu5의 실제 버전도 기록했다. `tests/assets.test.ts`는 원문 대응, PCM -포맷, 비무음 신호, 20MiB 이내 크기, gain 적용 후 피크 여유를 검사한다. -빌드 마지막에는 `dist/audio` 파일의 해시도 확인한다. -청취 연산 변경에 맞춰 101개 WAV와 manifest를 모두 재생성했다. 정답과 경쟁 -단어는 `renderSentence`로 채우며, 순서·반복·정정·조건·방향 단서가 실제 음성에 들어간다. +청취용 샘플은 [intro.wav](../public/audio/intro.wav)와 +[luck-apple.wav](../public/audio/luck-apple.wav)이다. 101개 전체 생성 결과의 +크기·길이 범위와 독립 검증은 [v0.1.1 RESULT](../.agent/RESULT-v0.1.1.md)에 기록한다. +`public/audio/manifest.json`은 version, provider, voice, rate, pitch, sampleRate, +생성 시각, 실측 edge-tts/FFmpeg 버전, 파일별 원문·SHA-256·bytes·duration·RMS·peak를 담는다. -## 사용 도구와 라이선스 +## 설치와 생성 -- [eSpeak NG 공식 저장소](https://github.com/espeak-ng/espeak-ng): GPL-3.0-or-later. - 이번에는 Ubuntu 24.04 패키지 `1.51+dfsg-12build1`, `ko` 포먼트 음성을 사용했다. - [공식 언어 목록](https://github.com/espeak-ng/espeak-ng/blob/master/docs/languages.md)에 한국어가 있다. -- [FFmpeg 공식 라이선스 안내](https://ffmpeg.org/legal.html): 빌드 구성에 따라 - LGPL/GPL. 생성 과정에서 PCM 리샘플링만 사용했으며 실행 파일을 배포하지 않는다. -- 합성 엔진의 프로그램 라이선스와 그 출력물은 구분한다. - [GPLv3 §2](https://www.gnu.org/licenses/gpl-3.0.html#section2) 및 - [GNU 출력물 설명](https://www.gnu.org/licenses/gpl-faq.html#GPLOutput)에 따르면 - 출력이 그 프로그램을 바탕으로 한 저작물인 경우에만 프로그램 라이선스가 - 출력에 적용된다. 이 작업의 산출물은 직접 작성한 텍스트의 포먼트 합성 파형이며, - 프로그램 코드·외부 녹음·모델을 포함하지 않는 자체 생성 오디오로 취급한다. -- 엔진·라이브러리·다운로드한 Debian 패키지는 `.cache/`에서만 생성 도구로 - 사용하며 Git 대상과 `dist/`에 들어가지 않는다. 별도 외부 음원 라이선스는 없다. - 프로젝트 자체의 소스·문서·자체 생성 자산은 private / all-rights-reserved다. - [루트 LICENSE](../LICENSE)를 따른다. 별도로 번들에 포함되는 React, ReactDOM, - scheduler의 MIT 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 - 있으며 `dist/THIRD_PARTY_NOTICES.md`로 함께 배포한다. - -## 생성 절차 - -일반적인 Ubuntu 개발 환경에는 `espeak-ng`, `ffmpeg`, Python 3, Node.js가 -필요하다. **이미 포함된 WAV로 빌드·실행할 때는 이 도구가 필요 없다.** +생성 환경은 Linux (기존 디렉터리 교체에 `renameat2(RENAME_EXCHANGE)` 필요), +Python 3.11 이상, FFmpeg, Node.js 22.22.2 이상이다. **이미 포함된 WAV를 +빌드·실행할 때는 Python/FFmpeg/provider 연결이 필요 없다.** 생성기 계약 테스트는 +Python과 FFmpeg를 사용하되 네트워크 없이 동작한다. ```bash +python3 -m venv .venv-audio +.venv-audio/bin/python -m pip install -r requirements-audio.txt +.venv-audio/bin/python -m pip check npm run audio:generate -npm test -- tests/assets.test.ts +npm test npm run build ``` -현재 작업에서는 시스템 설치 없이 다음 명령으로 worktree 내부에 도구를 풀었다. -이 명령은 문서화용이며 서버에서 실행한 이력이 없다. +`audio:generate`는 `.venv-audio/bin/python scripts/generate_audio.py`로 고정되어 +전역 Python으로 대체되지 않는다. `requirements-audio.txt`는 edge-tts==7.3.1과 +전이 의존성 버전을 고정한다. 생성기는 실제 설치 버전을 조회하고 7.3.1이 아니면 +외부 요청 전에 실패한다. `.venv-audio/`는 Git과 배포물에서 제외한다. -```bash -mkdir -p .cache/espeak -cd .cache/espeak -apt download espeak-ng espeak-ng-data libespeak-ng1 libpcaudio0 libsonic0 -for pkg in *.deb; do dpkg-deb -x "$pkg" root; done -cd ../.. -npm run audio:generate -``` +원문을 Node type stripping으로 앱 데이터에서 읽고, 101개 모두 같은 파일시스템의 +`public/.audio-stage-*/`에 순차 생성한다. 각 provider 요청은 최대 60초, 최대 +3회 시도이며 실패 사이 1초/2초를 기다린다. FFmpeg도 60초 제한이 있다. +재시도 전 불완전 MP3를 삭제하고 변환·검증 실패는 즉시 중단한다. 로그에는 +파일명·시도 횟수만 남기고 provider stderr/응답·쿠키·인증 헤더를 저장하지 않는다. -`scripts/generate_audio.py`가 시스템 엔진 또는 `.cache/espeak/root/usr`를 -자동 선택한다. `ko`, 속도 145, 피치 45, 진폭 100을 고정하고 FFmpeg로 16kHz -PCM을 만든 뒤 Python wave 모듈로 메타데이터 없는 WAV를 쓴다. 런타임 데이터는 -Node의 type stripping으로 읽어 텍스트 중복을 피한다. 같은 엔진·버전·플랫폼에서는 -해시로 재현 여부를 비교할 수 있다. 다른 버전은 새 출처와 해시를 검토해야 한다. +전체 WAV와 manifest를 검증한 뒤 기존 `public/audio`와 완성 디렉터리를 단일 +원자 교환으로 바꾼다. 실패·Ctrl-C는 기존 디렉터리의 바이트를 그대로 보존한다. +원자 교환을 지원하지 않는 파일시스템에서는 실패하며 비원자 방식으로 우회하지 않는다. +성공 후 이전 자산과 임시 MP3를 정리한다. 강제 종료(SIGKILL)·전원 손실 때는 +숨김 staging 폴더가 남을 수 있으나 공개 디렉터리는 교환 전 또는 후의 전체 집합이다. +이는 전원 손실 시 저장장치 내구성 보장을 뜻하지 않는다. -## 음량·품질 한계 +고정 클라이언트와 설정으로 **생성 절차**를 재현할 수 있다. 원격 모델·서비스 버전은 +고정할 수 없어 재생성 WAV의 바이트 동일성은 보장하지 않는다. 새 실행의 manifest를 +검토해야 한다. 빌드는 이미 생성한 WAV의 해시를 보존하고 101개 모두 포함됐는지 확인한다. -보이스와 배경은 같은 디지털 RMS로 정규화되고 음성 경로만 dB gain을 적용한다. -안내의 gain은 1.8에서 0.12로 감소한다. 기본 전체 음량은 35%이다. -물리적 음압을 측정하거나 장치 출력을 보정하지 않는다. +## 레벨 처리와 검증 -포먼트 합성 특성상 한국어 발음·억양과 웃음이 기계적으로 들린다. 배경도 실제 -장소 녹음처럼 세밀하지 않다. 객관식의 자모 차이와 단어 친숙도, 발음의 자연스러움은 -검수자가 직접 들어 평가해야 한다. 현재 단계 수치는 임상 검증이나 청력 판정용이 아니다. +MP3를 24kHz mono signed 16-bit little-endian PCM으로 디코드한다. 파일 전체에 +동일한 선형 gain을 적용해 RMS 0.10을 목표로 하며 증폭은 최대 2배, peak는 +0.89로 제한한다. 동적 압축·노이즈 게이트·침묵 제거·시간 변형은 하지 않는다. +원래 신호의 자음과 억양을 보존하면서 파일 간 레벨 차이와 피크를 완화한다. + +생성기와 독립 자산 테스트는 101개 파일의 원문·집합·SHA-256·bytes·duration, +24kHz mono PCM16, 1–20초 길이, RMS > 100/32768, full-scale 미도달, +총 48MiB 미만을 검사한다. 런타임 RMS 0.03과 최대 +6dB에서도 peak < 0.8을 +요구한다. 기존 앱은 Web Audio에서 배경·음성을 RMS 정규화한 뒤 상대 SNR을 적용한다. +물리적 음압이나 주파수별 청감을 보정한 값은 아니다. + +## 도구 라이선스와 서비스 조건 + +- [edge-tts 7.3.1 LICENSE](https://github.com/rany2/edge-tts/blob/7.3.1/LICENSE): + 대부분의 코드에 LGPLv3, `srt_composer.py`에만 MIT가 적용된다. + [공식 프로젝트](https://github.com/rany2/edge-tts)는 Microsoft Edge의 온라인 + TTS를 이용하는 클라이언트다. 클라이언트 라이선스가 provider 사용·음성 출력의 + 재배포 권한을 대신 부여하지 않는다. 이 프로젝트는 클라이언트/모델을 번들하지 않는다. +- [FFmpeg 공식 라이선스](https://ffmpeg.org/legal.html): 빌드 옵션에 따라 LGPL/GPL이 + 적용된다. 실제 사용 버전은 manifest에 남기며 실행 파일은 배포하지 않는다. +- [Microsoft Services Agreement](https://www.microsoft.com/en-us/servicesagreement)의 + AI Services 등 해당 서비스 조건은 도구의 오픈소스 라이선스와 별개다. + 위 문서 §13(s)(vi)는 출력의 권리·사용 가능성을 이용자가 판단하도록 설명한다. + API 키 없이 호출된다는 사실이 상업 사용·재배포의 포괄적 허락이나 SLA를 뜻하지 않는다. + 배포자는 자신의 이용 상황에 적용되는 최신 조건을 확인해야 한다. +- 프로젝트는 생성 음성 자체에 **독점 권리를 주장하지 않는다**. [LICENSE](../LICENSE)의 + 소스·문서 권리 유보와 provider 및 제3자의 권리는 구분한다. 런타임 React, + ReactDOM, scheduler의 MIT 전문은 [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md)에 있다. + +문서 확인일: 2026-10-14 UTC. provider의 음성 목록, rate/pitch 지원, 가용성, +접근 제한, 조건은 바뀔 수 있다. 온라인 생성이 중단되어도 포함된 WAV의 빌드·재생에는 +영향이 없다. 실패 시 다른 voice로 자동 대체하지 않는다. + +## 청감과 평가의 한계 + +자동 검증은 PCM 신호·무결성·재생 흐름을 확인한다. 한국어 발음, 유사 자모의 +구분, 웃음의 자연스러움과 실제 사용자의 난이도는 청취 평가가 필요하다. +대표 샘플과 게임 내 소음 혼합 상태를 직접 들어 인수 판단한다. 배경은 장소를 +추상화한 합성 신호다. 이 게임의 수치는 임상 검증·치료·청력 판정용이 아니다. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 72a7c9b..e21bba7 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,4 +1,4 @@ -# 정적 배포 인계 +# Find-Word v0.1.1 정적 배포 인계 이 문서는 **Neo가 검토 후 수행할 절차**다. Codex는 서버, DNS, 인증정보에 접근하지 않았고 배포·서비스 생성·Git commit/push도 하지 않았다. @@ -7,7 +7,7 @@ Node.js 최소 **22.22.2** (`>=22.22.2`)의 지원 LTS 환경에서 `npm ci`, `npm run lint`, `npm test`, `npm run build`를 실행한다. `dist/` 전체를 하나의 릴리스로 전달한다. -`dist/audio/`의 WAV 101개와 manifest가 반드시 포함되어야 한다. 런타임은 +`dist/audio/`의 24kHz neural WAV 101개와 version 0.1.1 manifest가 반드시 포함되어야 한다. 런타임은 Nginx 정적 파일만 필요하며 Node 서버·TTS 엔진·DB·환경변수·API 키는 필요 없다. 잠긴 jsdom 계열 의존성 때문에 24.x는 24.15.0 이상, 26.x는 26.0.0 이상이어야 한다. 브라우저 검증의 Chromium sandbox는 기본 활성화다. sandbox를 지원하지 않는 @@ -15,6 +15,8 @@ Nginx 정적 파일만 필요하며 Node 서버·TTS 엔진·DB·환경변수·A Canonical repository는 [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word)다. 프로젝트 자체는 private / all-rights-reserved이며 [LICENSE](../LICENSE)를 따른다. +생성 음성은 프로젝트의 독점 권리 주장 대상이 아니며 provider 조건은 +[AUDIO_PROVENANCE](AUDIO_PROVENANCE.md)에 기록한다. React, ReactDOM, scheduler의 MIT 전문을 담은 `dist/THIRD_PARTY_NOTICES.md`도 반드시 배포한다. `build` 검증기는 고지 파일이 없거나 축약·변조되면 실패한다. diff --git a/eslint.config.js b/eslint.config.js index b4eba63..7411f1c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -11,6 +11,7 @@ export default tseslint.config( "dist/**", "node_modules/**", ".cache/**", + ".venv-audio/**", "test-results/**", "playwright-report/**", ], diff --git a/index.html b/index.html index 5e2527c..5a79403 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ name="description" content="소음 속 작은 한국어 단어를 찾는 청각적 주의·식별 게임. 에스와 함께 나만의 속도로 소리를 탐험하세요." /> - 단어를 맞춰라 · Find-Word v0.1 + 단어를 맞춰라 · Find-Word v0.1.1
diff --git a/package-lock.json b/package-lock.json index 89002c9..c9806c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "find-word", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "find-word", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "react": "^19.3.0", "react-dom": "^19.3.0" diff --git a/package.json b/package.json index fb397e4..ab1f7ef 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "find-word", "private": true, - "version": "0.1.0", + "version": "0.1.1", "type": "module", "engines": { "node": ">=22.22.2" @@ -13,7 +13,7 @@ "test:watch": "vitest", "build": "tsc --noEmit && vite build && node scripts/check-build.mjs", "preview": "vite preview --host 127.0.0.1", - "audio:generate": "python3 scripts/generate_audio.py", + "audio:generate": ".venv-audio/bin/python scripts/generate_audio.py", "test:browser": "playwright test" }, "dependencies": { diff --git a/public/audio/correction-apple.wav b/public/audio/correction-apple.wav index 40aff58..f0dc8bc 100644 Binary files a/public/audio/correction-apple.wav and b/public/audio/correction-apple.wav differ diff --git a/public/audio/correction-furniture.wav b/public/audio/correction-furniture.wav index 3808a79..81af603 100644 Binary files a/public/audio/correction-furniture.wav and b/public/audio/correction-furniture.wav differ diff --git a/public/audio/correction-hat.wav b/public/audio/correction-hat.wav index 50fa7d2..67d6703 100644 Binary files a/public/audio/correction-hat.wav and b/public/audio/correction-hat.wav differ diff --git a/public/audio/correction-market.wav b/public/audio/correction-market.wav index 4c9fb00..3730d70 100644 Binary files a/public/audio/correction-market.wav and b/public/audio/correction-market.wav differ diff --git a/public/audio/correction-meat.wav b/public/audio/correction-meat.wav index e64f5bc..5fbe194 100644 Binary files a/public/audio/correction-meat.wav and b/public/audio/correction-meat.wav differ diff --git a/public/audio/correction-oil.wav b/public/audio/correction-oil.wav index 236556d..10da118 100644 Binary files a/public/audio/correction-oil.wav and b/public/audio/correction-oil.wav differ diff --git a/public/audio/correction-sea.wav b/public/audio/correction-sea.wav index 23eedd8..7a47008 100644 Binary files a/public/audio/correction-sea.wav and b/public/audio/correction-sea.wav differ diff --git a/public/audio/correction-sound.wav b/public/audio/correction-sound.wav index 323dad7..db9ffae 100644 Binary files a/public/audio/correction-sound.wav and b/public/audio/correction-sound.wav differ diff --git a/public/audio/correction-tree.wav b/public/audio/correction-tree.wav index 305bd0b..b07f959 100644 Binary files a/public/audio/correction-tree.wav and b/public/audio/correction-tree.wav differ diff --git a/public/audio/correction-umbrella.wav b/public/audio/correction-umbrella.wav index 067343e..1012b75 100644 Binary files a/public/audio/correction-umbrella.wav and b/public/audio/correction-umbrella.wav differ diff --git a/public/audio/first-apple.wav b/public/audio/first-apple.wav index 3de4412..835fd06 100644 Binary files a/public/audio/first-apple.wav and b/public/audio/first-apple.wav differ diff --git a/public/audio/first-furniture.wav b/public/audio/first-furniture.wav index 8f17f6c..8e6549a 100644 Binary files a/public/audio/first-furniture.wav and b/public/audio/first-furniture.wav differ diff --git a/public/audio/first-hat.wav b/public/audio/first-hat.wav index ca2be6b..eac90b4 100644 Binary files a/public/audio/first-hat.wav and b/public/audio/first-hat.wav differ diff --git a/public/audio/first-market.wav b/public/audio/first-market.wav index 2bfb415..0408da3 100644 Binary files a/public/audio/first-market.wav and b/public/audio/first-market.wav differ diff --git a/public/audio/first-meat.wav b/public/audio/first-meat.wav index 1c90920..731594f 100644 Binary files a/public/audio/first-meat.wav and b/public/audio/first-meat.wav differ diff --git a/public/audio/first-oil.wav b/public/audio/first-oil.wav index 9feb23d..b7a85d1 100644 Binary files a/public/audio/first-oil.wav and b/public/audio/first-oil.wav differ diff --git a/public/audio/first-sea.wav b/public/audio/first-sea.wav index 8406e18..ae520f4 100644 Binary files a/public/audio/first-sea.wav and b/public/audio/first-sea.wav differ diff --git a/public/audio/first-sound.wav b/public/audio/first-sound.wav index 6932dbe..4458b0d 100644 Binary files a/public/audio/first-sound.wav and b/public/audio/first-sound.wav differ diff --git a/public/audio/first-tree.wav b/public/audio/first-tree.wav index e6dc9e6..f2c2173 100644 Binary files a/public/audio/first-tree.wav and b/public/audio/first-tree.wav differ diff --git a/public/audio/first-umbrella.wav b/public/audio/first-umbrella.wav index 81846ca..f9d6d76 100644 Binary files a/public/audio/first-umbrella.wav and b/public/audio/first-umbrella.wav differ diff --git a/public/audio/intro.wav b/public/audio/intro.wav index 45ee424..25b4c40 100644 Binary files a/public/audio/intro.wav and b/public/audio/intro.wav differ diff --git a/public/audio/last-apple.wav b/public/audio/last-apple.wav index 63e58a5..827d272 100644 Binary files a/public/audio/last-apple.wav and b/public/audio/last-apple.wav differ diff --git a/public/audio/last-furniture.wav b/public/audio/last-furniture.wav index 171099f..b57678a 100644 Binary files a/public/audio/last-furniture.wav and b/public/audio/last-furniture.wav differ diff --git a/public/audio/last-hat.wav b/public/audio/last-hat.wav index 8a94791..bd6878d 100644 Binary files a/public/audio/last-hat.wav and b/public/audio/last-hat.wav differ diff --git a/public/audio/last-market.wav b/public/audio/last-market.wav index 256b2df..9b83788 100644 Binary files a/public/audio/last-market.wav and b/public/audio/last-market.wav differ diff --git a/public/audio/last-meat.wav b/public/audio/last-meat.wav index 1ed65af..8c21b55 100644 Binary files a/public/audio/last-meat.wav and b/public/audio/last-meat.wav differ diff --git a/public/audio/last-oil.wav b/public/audio/last-oil.wav index 82e6b32..62ad63c 100644 Binary files a/public/audio/last-oil.wav and b/public/audio/last-oil.wav differ diff --git a/public/audio/last-sea.wav b/public/audio/last-sea.wav index 921dc6e..2afe9e5 100644 Binary files a/public/audio/last-sea.wav and b/public/audio/last-sea.wav differ diff --git a/public/audio/last-sound.wav b/public/audio/last-sound.wav index 07a6280..71993c5 100644 Binary files a/public/audio/last-sound.wav and b/public/audio/last-sound.wav differ diff --git a/public/audio/last-tree.wav b/public/audio/last-tree.wav index 981f876..d98c974 100644 Binary files a/public/audio/last-tree.wav and b/public/audio/last-tree.wav differ diff --git a/public/audio/last-umbrella.wav b/public/audio/last-umbrella.wav index 518a2ee..26a731e 100644 Binary files a/public/audio/last-umbrella.wav and b/public/audio/last-umbrella.wav differ diff --git a/public/audio/luck-apple.wav b/public/audio/luck-apple.wav index 733afe7..9d16883 100644 Binary files a/public/audio/luck-apple.wav and b/public/audio/luck-apple.wav differ diff --git a/public/audio/luck-furniture.wav b/public/audio/luck-furniture.wav index add6c1d..ccd4c31 100644 Binary files a/public/audio/luck-furniture.wav and b/public/audio/luck-furniture.wav differ diff --git a/public/audio/luck-hat.wav b/public/audio/luck-hat.wav index ef0ce7a..e0b1396 100644 Binary files a/public/audio/luck-hat.wav and b/public/audio/luck-hat.wav differ diff --git a/public/audio/luck-market.wav b/public/audio/luck-market.wav index 8081cbe..bece7bd 100644 Binary files a/public/audio/luck-market.wav and b/public/audio/luck-market.wav differ diff --git a/public/audio/luck-meat.wav b/public/audio/luck-meat.wav index f53ccd0..3d497f4 100644 Binary files a/public/audio/luck-meat.wav and b/public/audio/luck-meat.wav differ diff --git a/public/audio/luck-oil.wav b/public/audio/luck-oil.wav index c7c98bd..5a5a867 100644 Binary files a/public/audio/luck-oil.wav and b/public/audio/luck-oil.wav differ diff --git a/public/audio/luck-sea.wav b/public/audio/luck-sea.wav index c12185a..3dd18e0 100644 Binary files a/public/audio/luck-sea.wav and b/public/audio/luck-sea.wav differ diff --git a/public/audio/luck-sound.wav b/public/audio/luck-sound.wav index d66528c..52278f4 100644 Binary files a/public/audio/luck-sound.wav and b/public/audio/luck-sound.wav differ diff --git a/public/audio/luck-tree.wav b/public/audio/luck-tree.wav index aa05fcb..e8a54d4 100644 Binary files a/public/audio/luck-tree.wav and b/public/audio/luck-tree.wav differ diff --git a/public/audio/luck-umbrella.wav b/public/audio/luck-umbrella.wav index 54a8200..5966c6f 100644 Binary files a/public/audio/luck-umbrella.wav and b/public/audio/luck-umbrella.wav differ diff --git a/public/audio/manifest.json b/public/audio/manifest.json index f2449f1..d53cb79 100644 --- a/public/audio/manifest.json +++ b/public/audio/manifest.json @@ -1,617 +1,933 @@ { - "generator": "eSpeak NG ko, speed 145, pitch 45, amplitude 100; FFmpeg PCM 16kHz mono s16", + "version": "0.1.1", + "provider": "Microsoft Edge online TTS", + "voice": "ko-KR-HyunsuMultilingualNeural", + "rate": "-6%", + "pitch": "-4Hz", + "sampleRate": 24000, + "channels": 1, + "sampleFormat": "PCM s16le", "toolVersions": { - "espeak": "eSpeak NG text-to-speech: 1.51", + "edge-tts": "7.3.1", "ffmpeg": "ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers" }, - "source": "https://github.com/espeak-ng/espeak-ng", - "method": "자체 작성 문장의 오프라인 포먼트 합성; 외부 녹음·네트워크 TTS 없음", + "generatedAtUtc": "2026-10-14T06:55:41.751382+00:00", + "source": "https://github.com/rany2/edge-tts", + "method": "온라인 neural 생성 MP3 → FFmpeg decode → 파일별 선형 gain → 로컬 PCM WAV", + "normalization": { + "targetRms": 0.1, + "peakCeiling": 0.89, + "maxGain": 2.0 + }, "files": [ { "file": "intro.wav", "text": "으하하하~~ 나는 에스다. 널 위한 행운의 단어를 불러주마, 잘 찾아보거라.", - "sha256": "4c271551d99cd9dffc01f35c176e98b54b4879549543ff8e2c79c6af8d4dc53a", - "bytes": 259900 + "sha256": "90d9f46e6e3f9c8a0c1a781096df54d7809ef4ad38b289508ef7b8c0411465ce", + "bytes": 338732, + "duration": 7.056, + "rms": 0.09999999766232884, + "peak": 0.499847412109375 }, { "file": "luck-apple.wav", "text": "오늘의 행운을 담은 단어는 ‘사과’입니다.", - "sha256": "51e6cf35b219f13c0d72ec9633f0c108c3393bc5ef6ae6aca6aa5d00153b4177", - "bytes": 125658 + "sha256": "7c8f413f4b7e164f090e508ba090448f1376c6012649bbb6a917d45851313883", + "bytes": 188972, + "duration": 3.936, + "rms": 0.10000000059246013, + "peak": 0.401641845703125 }, { "file": "luck-sea.wav", "text": "오늘의 행운을 담은 단어는 ‘바다’입니다.", - "sha256": "6585cb8d50eea173395417fc5fb357f55901665fe45dd07d942f79e202729111", - "bytes": 123366 + "sha256": "47aa31d757c9fb49c9b53a1c052d90a5ced4be57549d649850d9b48f8864045b", + "bytes": 191276, + "duration": 3.984, + "rms": 0.09999997583491328, + "peak": 0.388702392578125 }, { "file": "luck-tree.wav", "text": "오늘의 행운을 담은 단어는 ‘나무’입니다.", - "sha256": "6d188188d1478d0298536e0e03a58883629459dee56c6ae8ab88b4d7170c1def", - "bytes": 121224 + "sha256": "122f496834fb16d6da47f801e50e46e272e4e934af959467d1db4e490f79be7a", + "bytes": 183212, + "duration": 3.816, + "rms": 0.1000000469252297, + "peak": 0.36920166015625 }, { "file": "luck-hat.wav", "text": "오늘의 행운을 담은 단어는 ‘모자’입니다.", - "sha256": "be8d4287ebce15e221a8908f6b2d7478e789ecc51ba95d65fdf34f130954e7e5", - "bytes": 121644 + "sha256": "4ff43dd042f4b5451e9fdeb3edd59f1e389a7d1dd488094587a3a9fc7dfffa44", + "bytes": 191276, + "duration": 3.984, + "rms": 0.09999997899323479, + "peak": 0.417083740234375 }, { "file": "luck-umbrella.wav", "text": "오늘의 행운을 담은 단어는 ‘우산’입니다.", - "sha256": "188e24932e5e68fb0de8270e5b48128d486da956ad4069afbbcd0ad9af5913ce", - "bytes": 122178 + "sha256": "8d3e8e8bc97e5fe3f3362184c4abb12f01a882a1f1174d2644f0f70aac677c0a", + "bytes": 192428, + "duration": 4.008, + "rms": 0.09999996793233332, + "peak": 0.385650634765625 }, { "file": "luck-market.wav", "text": "오늘의 행운을 담은 단어는 ‘시장’입니다.", - "sha256": "e58cdfd5a43074179e65edaf5f223dd15ad1accdd2fd547a6d3311de57b78eca", - "bytes": 124352 + "sha256": "356af25332bf2000ed349671f3ff39767929e1f4599af622c021f907d11fb7bf", + "bytes": 194732, + "duration": 4.056, + "rms": 0.09999998683675566, + "peak": 0.4022216796875 }, { "file": "luck-oil.wav", "text": "오늘의 행운을 담은 단어는 ‘기름’입니다.", - "sha256": "8b1070414191ff87b9531caf0782b13458b246a1f07b51f7ddcff2cd613930f7", - "bytes": 120286 + "sha256": "3fa1e662a3af8f2726b17b2baa5bec2934406e6595ebc243c8380dd16e190977", + "bytes": 190124, + "duration": 3.96, + "rms": 0.09999999431198445, + "peak": 0.3817138671875 }, { "file": "luck-sound.wav", "text": "오늘의 행운을 담은 단어는 ‘소리’입니다.", - "sha256": "052e2c2724c2fcf9d4c68ba3b746a430af0cfc74b42ed6f8317ae2cc5af1c2ad", - "bytes": 122660 + "sha256": "34e4d9758d2ed448646fda364610e34779c84794403d9051ab3d8c1d33e2c80c", + "bytes": 184364, + "duration": 3.84, + "rms": 0.10000003234489044, + "peak": 0.386322021484375 }, { "file": "luck-meat.wav", "text": "오늘의 행운을 담은 단어는 ‘고기’입니다.", - "sha256": "dcf3d1c84a9ee00ea8a1ea5552ac69b3dc46b9c4160ee3375a4afe38426c0b8d", - "bytes": 121754 + "sha256": "d36dfc395a5c53fbfa4abe9980db7bb59adfdac59e40318c4aca365365e3f2ab", + "bytes": 187820, + "duration": 3.912, + "rms": 0.0999999898733683, + "peak": 0.371124267578125 }, { "file": "luck-furniture.wav", "text": "오늘의 행운을 담은 단어는 ‘가구’입니다.", - "sha256": "6265e3807ea10eb82e9bb15a7e72dcab5e148277bc49bacfdb2663817d151ddf", - "bytes": 122164 + "sha256": "52cb92cdb8ee92b6f581c9d68dbc346f180c470564809ba57185429c9eb84127", + "bytes": 186668, + "duration": 3.888, + "rms": 0.10000004096194225, + "peak": 0.387054443359375 }, { "file": "password-apple.wav", "text": "연습 암호는 ‘다과’, 진짜 암호는 ‘사과’입니다.", - "sha256": "14b7c8c0253784f319a04b568da594d8e46077d03931a36a23e8a7689dd4b928", - "bytes": 161688 + "sha256": "b6f5de3fe571fe3a7fdb722fe49250bcfeedef49475271e0ec76a7630907edd5", + "bytes": 235052, + "duration": 4.896, + "rms": 0.09999999690557956, + "peak": 0.483428955078125 }, { "file": "password-sea.wav", "text": "연습 암호는 ‘사다’, 진짜 암호는 ‘바다’입니다.", - "sha256": "832178b1eee4fe29653ca8dfda5bfb356cab83f4f880a704ee9eab937443f872", - "bytes": 160256 + "sha256": "03f45b8cb5b3dc72faf5c6e3d0e2a10cea6b60301ea400d0a4453596d2f0b337", + "bytes": 232748, + "duration": 4.848, + "rms": 0.09999999075635248, + "peak": 0.533905029296875 }, { "file": "password-tree.wav", "text": "연습 암호는 ‘나루’, 진짜 암호는 ‘나무’입니다.", - "sha256": "ef25aa09f89acadcbb2c43a1f18d0c2da292fae4d87d052868942d08b3825a8d", - "bytes": 156642 + "sha256": "67da8147296e35134ec254bc82b58317630061b2c64d2d63c4be197ce8692d50", + "bytes": 233900, + "duration": 4.872, + "rms": 0.10000002225030123, + "peak": 0.490386962890625 }, { "file": "password-hat.wav", "text": "연습 암호는 ‘포자’, 진짜 암호는 ‘모자’입니다.", - "sha256": "aef3f595e1b2d36212d4611f9998d841303e96ee5385639094aebcc984ff8a94", - "bytes": 158616 + "sha256": "e91bfc97df90a1675b6cd856880ac15c54de32cfc3b23603bdd1c2c5ac3ce6b8", + "bytes": 235052, + "duration": 4.896, + "rms": 0.10000000521661735, + "peak": 0.505889892578125 }, { "file": "password-umbrella.wav", "text": "연습 암호는 ‘부산’, 진짜 암호는 ‘우산’입니다.", - "sha256": "6bc1bf13c3fd1739bc9e490525ecd5839b9a4032faecb9f101a2c68824d57b82", - "bytes": 162668 + "sha256": "ab3ca6398c879971dc793eb25b472fcc0db72d76e386f11ef4d32c639f37c69d", + "bytes": 239660, + "duration": 4.992, + "rms": 0.10000000645104105, + "peak": 0.4776611328125 }, { "file": "password-market.wav", "text": "연습 암호는 ‘지장’, 진짜 암호는 ‘시장’입니다.", - "sha256": "33036e08a06dd33b6aa86e5b78352d2f90e35ac1c7f9f202af2a32e2ceffcfc1", - "bytes": 166360 + "sha256": "02e325fb753ef7a3ee585a03d2ca2aa3f1fbc58523b062222ae1136eeed7a95c", + "bytes": 230444, + "duration": 4.8, + "rms": 0.10000002623625086, + "peak": 0.481689453125 }, { "file": "password-oil.wav", "text": "연습 암호는 ‘이름’, 진짜 암호는 ‘기름’입니다.", - "sha256": "d88914ab7302112beecb6b2edbe114c3c119ee375985eca9716bf239aea94038", - "bytes": 157396 + "sha256": "b472957b27ff98ea7b8894fa1ff12653f7e9f66f77870faa9e52a1c1bf116c41", + "bytes": 240812, + "duration": 5.016, + "rms": 0.10000000204728039, + "peak": 0.4945068359375 }, { "file": "password-sound.wav", "text": "연습 암호는 ‘오리’, 진짜 암호는 ‘소리’입니다.", - "sha256": "d3df570902e1f1fb5b2a7c573d57857bf4da547fb97ebdf27f04fbbf29c1170f", - "bytes": 155298 + "sha256": "044aafff08a19e39f21e58b963644715a987cd155896998769ef8e9ce6449628", + "bytes": 236204, + "duration": 4.92, + "rms": 0.10000000001020605, + "peak": 0.46209716796875 }, { "file": "password-meat.wav", "text": "연습 암호는 ‘보기’, 진짜 암호는 ‘고기’입니다.", - "sha256": "59c422dbf3c2ed7a397e7bf3455a2bf59132f85477765c6ec13673633cdb1e8c", - "bytes": 157754 + "sha256": "b009caadd56350d6a641ff17f0973e2b2724c183e1f45ae0bf3c483e237fa99e", + "bytes": 240812, + "duration": 5.016, + "rms": 0.10000000456206941, + "peak": 0.49786376953125 }, { "file": "password-furniture.wav", "text": "연습 암호는 ‘하구’, 진짜 암호는 ‘가구’입니다.", - "sha256": "c349bd57598eec8543ce488dd86a4c4ddd4ec661ac9e8d7f42d914d545f38f61", - "bytes": 158998 + "sha256": "fb2b631dd4e12120d08a84cce6450f9cd9cd68105d13d7001d808e7deaa1df4a", + "bytes": 233900, + "duration": 4.872, + "rms": 0.10000001899065697, + "peak": 0.483642578125 }, { "file": "remember-apple.wav", "text": "‘다과’. 이 단어는 잊으세요. 이제 ‘사과’, 이 단어를 기억하세요.", - "sha256": "3123d84c546ff4e630a8b93d59bd5e668a5de30ea651e56e134d47690aead28b", - "bytes": 205094 + "sha256": "1bf2aef9a9f030af1a8fd7dee8f44aa420a397b3ba99b575feeb3dc7a63ec191", + "bytes": 395180, + "duration": 8.232, + "rms": 0.10000000668138131, + "peak": 0.563568115234375 }, { "file": "remember-sea.wav", "text": "‘사다’. 이 단어는 잊으세요. 이제 ‘바다’, 이 단어를 기억하세요.", - "sha256": "d802055500865e38b9beffe0213b70314aa59ec181cb1af1897dd010bb5875e3", - "bytes": 204654 + "sha256": "5c91a36496e274d2be9c6fa8d9b506cd04f2a1b7ceb05ed1b7bb03fe7fca99d9", + "bytes": 399788, + "duration": 8.328, + "rms": 0.09999999180525658, + "peak": 0.546539306640625 }, { "file": "remember-tree.wav", "text": "‘나루’. 이 단어는 잊으세요. 이제 ‘나무’, 이 단어를 기억하세요.", - "sha256": "1e3f340609eb9844b7fc727f0c17a7f9f60245d4e00df2330d734efa97d26a33", - "bytes": 201206 + "sha256": "bc8e54f5407e8866bf1ca1ddfd4f6c63d85fee29bd9e430c7dd33e87974fc5b8", + "bytes": 409004, + "duration": 8.52, + "rms": 0.09999998112848245, + "peak": 0.54864501953125 }, { "file": "remember-hat.wav", "text": "‘포자’. 이 단어는 잊으세요. 이제 ‘모자’, 이 단어를 기억하세요.", - "sha256": "8a3cd1ef975d8a6782c018bbe23d9e69b4876fe8e64dcb9971f5025bb7072a4c", - "bytes": 202530 + "sha256": "31a6a4270bf368cda419094d87a303ac3cd5c22539786cb165f8044b0b8e9d6e", + "bytes": 402092, + "duration": 8.376, + "rms": 0.09999997909968193, + "peak": 0.541778564453125 }, { "file": "remember-umbrella.wav", "text": "‘부산’. 이 단어는 잊으세요. 이제 ‘우산’, 이 단어를 기억하세요.", - "sha256": "5c5ab3c30f02d3a3b8188f1d81f4d8f617a9e9783c18da38bf6d4bee9d03709e", - "bytes": 209448 + "sha256": "c8988e604ba05d52597e561389541e8f9cda86e052279f1862bbf8399ead314e", + "bytes": 403244, + "duration": 8.4, + "rms": 0.10000001748273872, + "peak": 0.557830810546875 }, { "file": "remember-market.wav", "text": "‘지장’. 이 단어는 잊으세요. 이제 ‘시장’, 이 단어를 기억하세요.", - "sha256": "eb582873b55043a61b80e34b25302f8d52ae9c7d641032ce65ddca8fb9bfb181", - "bytes": 212988 + "sha256": "d89792da51c83ac49d27e55aa6d822a338899a5ee487b34b552748201e1522f6", + "bytes": 398636, + "duration": 8.304, + "rms": 0.09999999983200249, + "peak": 0.57281494140625 }, { "file": "remember-oil.wav", "text": "‘이름’. 이 단어는 잊으세요. 이제 ‘기름’, 이 단어를 기억하세요.", - "sha256": "d56d8639bf462b1c341c07252666975ff6f3428de74f0bef886019fe0085be57", - "bytes": 204388 + "sha256": "3ca98682ff06d1d6b8af02c0fd5b2124f42266ac989884223eff4a9bbde1287a", + "bytes": 399788, + "duration": 8.328, + "rms": 0.09999999241389293, + "peak": 0.524383544921875 }, { "file": "remember-sound.wav", "text": "‘오리’. 이 단어는 잊으세요. 이제 ‘소리’, 이 단어를 기억하세요.", - "sha256": "c3d8e3e75086fd52a38806ea2122542d4fe495e381d4aa4c36e72277449451bf", - "bytes": 200100 + "sha256": "98387ccc7aab566f6487d4c250b2128082cc527e013a6f7b7bc4d0266ac295df", + "bytes": 394028, + "duration": 8.208, + "rms": 0.10000002803973357, + "peak": 0.53778076171875 }, { "file": "remember-meat.wav", "text": "‘보기’. 이 단어는 잊으세요. 이제 ‘고기’, 이 단어를 기억하세요.", - "sha256": "be4cc7dc1af803a23eb4319d076ecf4c5ab37b56913287f49c3b3c96eb4a75ce", - "bytes": 201730 + "sha256": "9183ef90b64f5ceaf3bb0178c689231ff3e9981d6ce889124a15f2a6e2b7a344", + "bytes": 395180, + "duration": 8.232, + "rms": 0.10000003134220614, + "peak": 0.519287109375 }, { "file": "remember-furniture.wav", "text": "‘하구’. 이 단어는 잊으세요. 이제 ‘가구’, 이 단어를 기억하세요.", - "sha256": "acc3efbba9ef6bf53a3992a669946b6c58e90c9821e2fb5ef8e8c4a738f8bdf7", - "bytes": 202972 + "sha256": "442bdcb956f120c270e1f321feae0415c897de0183e468980fcc119b898b2a67", + "bytes": 403244, + "duration": 8.4, + "rms": 0.10000001858461878, + "peak": 0.5618896484375 }, { "file": "last-apple.wav", "text": "세 단어 중 마지막을 고르세요. ‘다과’, ‘사고’, ‘사과’.", - "sha256": "5f583e9af9213cd5dc78abb68e5fcba3dbb7c65d7d8131a53210d8326e5cbb87", - "bytes": 187732 + "sha256": "4cb2a8579fa3a7b91bb36363826ce8ca0d6d86e96e16a479e226a2802edd82f7", + "bytes": 356012, + "duration": 7.416, + "rms": 0.10000001764132661, + "peak": 0.622528076171875 }, { "file": "last-sea.wav", "text": "세 단어 중 마지막을 고르세요. ‘사다’, ‘보다’, ‘바다’.", - "sha256": "21e589a6f7c262cca5533a8599a9d4fb257028a6e0c42732f6147209ad35ecb6", - "bytes": 190468 + "sha256": "f755452e1da44674aa1df7d12edffa988091b89a394e7eb6d4a00eb721f07cbf", + "bytes": 345644, + "duration": 7.2, + "rms": 0.09999998238433916, + "peak": 0.591033935546875 }, { "file": "last-tree.wav", "text": "세 단어 중 마지막을 고르세요. ‘나루’, ‘너무’, ‘나무’.", - "sha256": "915249a9860aa29c36149fc57b04057b2fa6ab9253fdfa1e0837236f84cb7aab", - "bytes": 186820 + "sha256": "589e56d940a3c9e80ec02ae0b2dd93463fdc85ef019a753fcd0fded0752055f9", + "bytes": 382508, + "duration": 7.968, + "rms": 0.09999999432249608, + "peak": 0.58074951171875 }, { "file": "last-hat.wav", "text": "세 단어 중 마지막을 고르세요. ‘포자’, ‘모조’, ‘모자’.", - "sha256": "0ce70943449ec5fb18c57d4ba7ea09a6e995f4bc991db07c70c9a846e16679a1", - "bytes": 189216 + "sha256": "bdaf8fbc54df8061e9e58aa8c14b3044a6e863dc54973ee3ee6ae2c5ae1a28f1", + "bytes": 354860, + "duration": 7.392, + "rms": 0.099999998595968, + "peak": 0.550079345703125 }, { "file": "last-umbrella.wav", "text": "세 단어 중 마지막을 고르세요. ‘부산’, ‘오산’, ‘우산’.", - "sha256": "8e5a3a57fc55a7deae5df5be5425a970b0a4926b74844f0f6fba74d882d8ea73", - "bytes": 192394 + "sha256": "c801293a34013a8d93d3289a2cc87cd3eed83f97ba4647f5060efa00b117a9ad", + "bytes": 370988, + "duration": 7.728, + "rms": 0.09999998351512711, + "peak": 0.59765625 }, { "file": "last-market.wav", "text": "세 단어 중 마지막을 고르세요. ‘지장’, ‘사장’, ‘시장’.", - "sha256": "2419dbb03e18d3528a615b4977405699ab5285ea4b392e45164b625ebb5ff0db", - "bytes": 201286 + "sha256": "7857e87e9dbfe8fe849cc0b18c1d45b60916cb337328099c7a87a42c82e2821e", + "bytes": 359468, + "duration": 7.488, + "rms": 0.09999999842960891, + "peak": 0.600555419921875 }, { "file": "last-oil.wav", "text": "세 단어 중 마지막을 고르세요. ‘이름’, ‘거름’, ‘기름’.", - "sha256": "8a7108a6b23dddb5f2ae1c72edf6c488335ccd322acdb6d96a371e7613d47eb2", - "bytes": 191000 + "sha256": "b5ce34950b8b31b1baf6ce55410797f0d8aa5478a670d6eace87cf76d134d6d3", + "bytes": 337580, + "duration": 7.032, + "rms": 0.10000002299391843, + "peak": 0.55706787109375 }, { "file": "last-sound.wav", "text": "세 단어 중 마지막을 고르세요. ‘오리’, ‘수리’, ‘소리’.", - "sha256": "c2b77970b477fd05671c833daaf2fb48aace796e1da54a1f9286fca0a8b7f484", - "bytes": 185296 + "sha256": "ec9c49802aebeb6a8b898a1c0d8ad1daa127d2d64c905c197ed49a8e0c4eb64a", + "bytes": 354860, + "duration": 7.392, + "rms": 0.10000004316064788, + "peak": 0.5640869140625 }, { "file": "last-meat.wav", "text": "세 단어 중 마지막을 고르세요. ‘보기’, ‘고개’, ‘고기’.", - "sha256": "f1bbcef87d1b2fda6d36ecc484c62c6f1a4a182c12e2e0ac8bb58f494809ddf8", - "bytes": 185540 + "sha256": "10f8ad4259ed8945c9da831e80b3902303c09caef96d400298599168fb1efed4", + "bytes": 346796, + "duration": 7.224, + "rms": 0.10000002519116054, + "peak": 0.537200927734375 }, { "file": "last-furniture.wav", "text": "세 단어 중 마지막을 고르세요. ‘하구’, ‘가교’, ‘가구’.", - "sha256": "5e625527e69a3f9a9c508cb9da26252ab621949d7ae9ff41bdd9336627234de0", - "bytes": 185132 + "sha256": "5ad4f776e35439d408abed0e66567d6b5cdc80fc54d579165472e474f1238776", + "bytes": 357164, + "duration": 7.44, + "rms": 0.10000003394798516, + "peak": 0.583587646484375 }, { "file": "first-apple.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘사과’, ‘다과’, ‘사고’.", - "sha256": "d539d03875121570650cac2a2bf800cc67e0a72effaf755e02c870f9569e5028", - "bytes": 203904 + "sha256": "7c1dbcb3082055df2b8f087f7e906ec867c92f679de0894c34740c05e61a4bc1", + "bytes": 359468, + "duration": 7.488, + "rms": 0.1000000290826337, + "peak": 0.57977294921875 }, { "file": "first-sea.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘바다’, ‘사다’, ‘보다’.", - "sha256": "2d789349b36dab9bbe784e2fcb8412704f2dd15b22ad63937bad9948720aa5fb", - "bytes": 207002 + "sha256": "0f0b39314c5f7d7168d4854003c0c284e7bc3438cdc4d1d1c5adab7cb5ed30e5", + "bytes": 349100, + "duration": 7.272, + "rms": 0.10000001935816175, + "peak": 0.57177734375 }, { "file": "first-tree.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘나무’, ‘나루’, ‘너무’.", - "sha256": "af4ad056b18af0d376a1df7dda3d7b8419e76cda6ffe57dd71054692aa653fd9", - "bytes": 203354 + "sha256": "3178230fd5850b6277a2153b438ec5289720e3db7ac929914cc213964f004bd5", + "bytes": 379052, + "duration": 7.896, + "rms": 0.09999998692384372, + "peak": 0.583343505859375 }, { "file": "first-hat.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘모자’, ‘포자’, ‘모조’.", - "sha256": "e34fd5edff5323e45f8621a2adb79ed871d1ce671bd3ac8c4a49a8c703878811", - "bytes": 205388 + "sha256": "17c8cde90567e7574b0c0702663bd70713054089ee920241b5444d9b7869eef5", + "bytes": 357164, + "duration": 7.44, + "rms": 0.1000000080433648, + "peak": 0.53125 }, { "file": "first-umbrella.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘우산’, ‘부산’, ‘오산’.", - "sha256": "4be32c725583f1579b15be4bbe110b4ab42705221a63621125aa1aa98f7f061c", - "bytes": 210768 + "sha256": "878e0bd2e6e4105f175aef51a028f6b141f3a2072be20f783193f3ba897a3ee0", + "bytes": 366380, + "duration": 7.632, + "rms": 0.10000000665263714, + "peak": 0.57000732421875 }, { "file": "first-market.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘시장’, ‘지장’, ‘사장’.", - "sha256": "37925d65ce12429905a1b185824722a34af8d77b15eb7bec3b52665b5c1fbc7c", - "bytes": 217794 + "sha256": "1e00b8f70a796cdebb0feb00c605fece7488b06915e6e036f54206f05e201e0c", + "bytes": 359468, + "duration": 7.488, + "rms": 0.10000001450369593, + "peak": 0.639068603515625 }, { "file": "first-oil.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘기름’, ‘이름’, ‘거름’.", - "sha256": "0991123aee956c26ca658382102c9b6d0a164fbf2dea45f07a978d2cc1fc6b80", - "bytes": 205784 + "sha256": "f415195e0cfc7ee711d09de7519e88eab62cc8b58d70772c83c3f5c27c267819", + "bytes": 335276, + "duration": 6.984, + "rms": 0.10000000576279051, + "peak": 0.53472900390625 }, { "file": "first-sound.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘소리’, ‘오리’, ‘수리’.", - "sha256": "5b3063285cddd8f46a2c2cbbee1ecc9b64ecdbd287df76df65dec854679ec884", - "bytes": 201840 + "sha256": "46f6adeed928a16af0700f1e8626cf8049f124632a04a5d10604426fb06f9596", + "bytes": 353708, + "duration": 7.368, + "rms": 0.10000001446744461, + "peak": 0.518951416015625 }, { "file": "first-meat.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘고기’, ‘보기’, ‘고개’.", - "sha256": "acf07b6db586616e67e2841a6f122120a5b0b18543047271bd5b8c55e33786e3", - "bytes": 201712 + "sha256": "f51a37c1722fef581f7d642a4dcae7fcb9953b3c318dce971bf616cd1a81fecc", + "bytes": 345644, + "duration": 7.2, + "rms": 0.09999999288327781, + "peak": 0.51312255859375 }, { "file": "first-furniture.wav", "text": "세 단어 중 첫 번째를 고르세요. ‘가구’, ‘하구’, ‘가교’.", - "sha256": "acba37fc2641c7073210855d294eb7c6e5f3328652c606186b1b0e569e2d0bf6", - "bytes": 203872 + "sha256": "29f007b385b7182db1d8aeccad4fc3c260df5e66e14e52a46b0ef0fcd0ff3a0a", + "bytes": 364076, + "duration": 7.584, + "rms": 0.09999997909045008, + "peak": 0.570037841796875 }, { "file": "repeat-apple.wav", "text": "두 번 들린 단어를 고르세요. ‘사과’, ‘다과’, ‘사과’, ‘사고’.", - "sha256": "5fcef242c63dc7e86c02a1a5813e409e6392511aad5834510696b0d0499a5ef1", - "bytes": 215994 + "sha256": "cb7da4e9a1288cac64d168b241f27d7d2bc2dfecace58744f9bbf2792dfad04e", + "bytes": 397484, + "duration": 8.28, + "rms": 0.09999999222276726, + "peak": 0.641448974609375 }, { "file": "repeat-sea.wav", "text": "두 번 들린 단어를 고르세요. ‘바다’, ‘사다’, ‘바다’, ‘보다’.", - "sha256": "ef675bfa38f4eb4abac04cc64aa2c3ec416d405f9c67646356c9a8f39fe17d21", - "bytes": 211180 + "sha256": "db2f235bee26ac804b5362e8e013b8f40c8da24fd3a34e0fb3b43c6d264feb36", + "bytes": 382508, + "duration": 7.968, + "rms": 0.10000003134647438, + "peak": 0.684326171875 }, { "file": "repeat-tree.wav", "text": "두 번 들린 단어를 고르세요. ‘나무’, ‘나루’, ‘나무’, ‘너무’.", - "sha256": "b6a1d2923d90f9dd35b324aea419daf3891b76cc8c5c9551fe5ec1140c2ab0bf", - "bytes": 206504 + "sha256": "8735c3d9672561ca237fa87317beec2878492eb9063d6048d5b214423561acc1", + "bytes": 423980, + "duration": 8.832, + "rms": 0.10000000739222943, + "peak": 0.618743896484375 }, { "file": "repeat-hat.wav", "text": "두 번 들린 단어를 고르세요. ‘모자’, ‘포자’, ‘모자’, ‘모조’.", - "sha256": "32cbc9823f5af5f897416bec2c377a8559c82fd3fae66273152826dd000cdc4c", - "bytes": 208872 + "sha256": "6e089a42fd7e65e82eec7186d369c926fb7fdac794365b2bd7658822fd92437c", + "bytes": 396332, + "duration": 8.256, + "rms": 0.09999999984675931, + "peak": 0.616729736328125 }, { "file": "repeat-umbrella.wav", "text": "두 번 들린 단어를 고르세요. ‘우산’, ‘부산’, ‘우산’, ‘오산’.", - "sha256": "4830662f39bc0c62ee9be89de80a44a837c50063fad8fb63129b865673de5525", - "bytes": 220408 + "sha256": "5e59ef39113c3d5e106862356d715f4cef2862ef8ee02654a97e532675c1ac6d", + "bytes": 410156, + "duration": 8.544, + "rms": 0.09999995863648034, + "peak": 0.674835205078125 }, { "file": "repeat-market.wav", "text": "두 번 들린 단어를 고르세요. ‘시장’, ‘지장’, ‘시장’, ‘사장’.", - "sha256": "0e146387377dc38fc10435e06e8eb8cd30a595092887bf8ee4c9d0e7b577650d", - "bytes": 232742 + "sha256": "68a5b899115a66ab225d610ec47b4de56cc8793657362bc38f533c06f741e4cc", + "bytes": 402092, + "duration": 8.376, + "rms": 0.09999998813966116, + "peak": 0.6822509765625 }, { "file": "repeat-oil.wav", "text": "두 번 들린 단어를 고르세요. ‘기름’, ‘이름’, ‘기름’, ‘거름’.", - "sha256": "94de2ef7f7d7f9132f3646c3dc09399de23b81909f3edf4bbd1c5b01fad4d8fe", - "bytes": 216502 + "sha256": "55b2c7d6aa8bbce04edf8661882c9781da81c57f56217f99b80457a3bb895da0", + "bytes": 369836, + "duration": 7.704, + "rms": 0.0999999804882663, + "peak": 0.62396240234375 }, { "file": "repeat-sound.wav", "text": "두 번 들린 단어를 고르세요. ‘소리’, ‘오리’, ‘소리’, ‘수리’.", - "sha256": "7f12b6f49a7b4f64af796aa4ad8cfffc85a6b39e2bc6698f74b420b74af6c7c8", - "bytes": 205404 + "sha256": "e9d636ff7f4777f38b1ff15bebdcec6fbde95b4b4b05625a7415b4cfa0025b59", + "bytes": 395180, + "duration": 8.232, + "rms": 0.1000000073748717, + "peak": 0.6114501953125 }, { "file": "repeat-meat.wav", "text": "두 번 들린 단어를 고르세요. ‘고기’, ‘보기’, ‘고기’, ‘고개’.", - "sha256": "ba8beeb33bcbe4771aaea9b3832a4c25868f6e34e37b3e3d026e891c07b8e71a", - "bytes": 203544 + "sha256": "6599d2fac73ea616a793151b4ce6f51af673988d630bac6219a77e78ae6cb34e", + "bytes": 380204, + "duration": 7.92, + "rms": 0.10000001571010926, + "peak": 0.59210205078125 }, { "file": "repeat-furniture.wav", "text": "두 번 들린 단어를 고르세요. ‘가구’, ‘하구’, ‘가구’, ‘가교’.", - "sha256": "581a94726b08b6cfd3c688f08838a05787ed902ab6d1d4a038e0ff6b34f63c3a", - "bytes": 208274 + "sha256": "bf84dc6cff1f227924a3a62158d2fc18769fb0ca40b91418fa19e534dbf92f00", + "bytes": 397484, + "duration": 8.28, + "rms": 0.10000003132755503, + "peak": 0.667022705078125 }, { "file": "correction-apple.wav", "text": "정답은 ‘다과’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘사과’입니다.", - "sha256": "53d1c1b207607e610964e68134a3d35ff521b569e228366ec4eb7c03691724bb", - "bytes": 256484 + "sha256": "ccd7327a3fb54d3320d049ffdced3bc17595861cee69429c0ee97182d454195b", + "bytes": 402092, + "duration": 8.376, + "rms": 0.10000001654596266, + "peak": 0.490325927734375 }, { "file": "correction-sea.wav", "text": "정답은 ‘사다’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘바다’입니다.", - "sha256": "df61f91374b9fbb5817249ae3b6cadfae914d3e97ee2c6d07366ef82080a11db", - "bytes": 255204 + "sha256": "9233431d78ab4b144e3c33ad0cf0d49fc0fb91b29514343230ba32987b794bb3", + "bytes": 398636, + "duration": 8.304, + "rms": 0.09999997225238083, + "peak": 0.49615478515625 }, { "file": "correction-tree.wav", "text": "정답은 ‘나루’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘나무’입니다.", - "sha256": "837c24b18f63a73e135511b2edd48ba12593317de3be4d225622e6d11b1a57da", - "bytes": 251316 + "sha256": "b8e8bbe6b8b14727efd953fae9d4aed2caeb004055d52f611c6378322847edbe", + "bytes": 400940, + "duration": 8.352, + "rms": 0.10000003932190811, + "peak": 0.470611572265625 }, { "file": "correction-hat.wav", "text": "정답은 ‘포자’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘모자’입니다.", - "sha256": "00d7c21caccbbb6c80ec6218415be1b0a6683a2d509b60821d3556e43353b5dc", - "bytes": 253208 + "sha256": "f9f98c91cfbcfcb106eef7d5d3795a9cf3c3aab7e19a60f13d7dffe284d19f86", + "bytes": 398636, + "duration": 8.304, + "rms": 0.10000000019629104, + "peak": 0.470977783203125 }, { "file": "correction-umbrella.wav", "text": "정답은 ‘부산’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘우산’입니다.", - "sha256": "9bb3ce5b00ccc1632c37638550f329cbb6cda01864433e068f85eb7ccab86370", - "bytes": 255400 + "sha256": "92b54f04884ab915783945077efd8f3bd443849a1720f72168b6348cae1f8e85", + "bytes": 406700, + "duration": 8.472, + "rms": 0.10000000137593418, + "peak": 0.479034423828125 }, { "file": "correction-market.wav", "text": "정답은 ‘지장’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘시장’입니다.", - "sha256": "c8152db42c956701cd5e21c588ef62aedaefa922658d4bc0fecf8243548d7054", - "bytes": 257262 + "sha256": "b8d25b867497adcd0fe7c8b6a8a8d2e7a52965956738a83e53f58735708773f9", + "bytes": 405548, + "duration": 8.448, + "rms": 0.10000000759335076, + "peak": 0.491485595703125 }, { "file": "correction-oil.wav", "text": "정답은 ‘이름’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘기름’입니다.", - "sha256": "228759e87b1c80be72ea34ff5b77cb5e4557afca1b73434ca8f6f1c2fedaa836", - "bytes": 247864 + "sha256": "f6eff0cdddcad9790351491e60c53cb2a70d222033a583652f2de0b8cd6abcc7", + "bytes": 406700, + "duration": 8.472, + "rms": 0.09999998611167496, + "peak": 0.470794677734375 }, { "file": "correction-sound.wav", "text": "정답은 ‘오리’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘소리’입니다.", - "sha256": "24f2d52bb865c71fc4735a4aebd69ce0e429a9f68646e73e77adababbe34af93", - "bytes": 250170 + "sha256": "5a603bc69979603afddcaeb6edf500a7c540b72405daeb33f5335d746b327da1", + "bytes": 397484, + "duration": 8.28, + "rms": 0.10000004224092506, + "peak": 0.469268798828125 }, { "file": "correction-meat.wav", "text": "정답은 ‘보기’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘고기’입니다.", - "sha256": "821caf897ddab107df8435b00b686bf9223e043c91a311edcaa0c3c5eb8c6511", - "bytes": 253432 + "sha256": "48e91b3932d3798e852041a854b3ee08c72127e092c62083a47af6b99adff241", + "bytes": 403244, + "duration": 8.4, + "rms": 0.09999999319342226, + "peak": 0.481536865234375 }, { "file": "correction-furniture.wav", "text": "정답은 ‘하구’입니다. 아니, 잘못 말했어요. 정정한 답은 ‘가구’입니다.", - "sha256": "5d69426f3319fa435ee60b234540dd8d9a07f00900177f03da2770f44511ade8", - "bytes": 254342 + "sha256": "5c004a7b2f5a4a918c9f689f1a2dcb219a620ed14313f40a9655b36dbf64948d", + "bytes": 398636, + "duration": 8.304, + "rms": 0.10000003705538822, + "peak": 0.487396240234375 }, { "file": "middle-apple.wav", "text": "세 단어 중 가운데를 고르세요. ‘다과’, ‘사과’, ‘사고’.", - "sha256": "0ecc7a7d27508f73c9064a35609c922bbdd6fce8ebfc155e74f7b78d4e897dae", - "bytes": 191056 + "sha256": "8832f1b793ab3eedbe663875ea8dbeabb122c329bd5879437c71230b0418c3c8", + "bytes": 360620, + "duration": 7.512, + "rms": 0.1000000102385111, + "peak": 0.553558349609375 }, { "file": "middle-sea.wav", "text": "세 단어 중 가운데를 고르세요. ‘사다’, ‘바다’, ‘보다’.", - "sha256": "c7828af4e64745d89a6f23fda6c86efb76febe379bab0901c2972a4569947604", - "bytes": 194176 + "sha256": "b6dbe0af143ea775f05bd59a3d6af348bcc770ab378ac91ea75a56e06c326edf", + "bytes": 346796, + "duration": 7.224, + "rms": 0.1000000185368972, + "peak": 0.56494140625 }, { "file": "middle-tree.wav", "text": "세 단어 중 가운데를 고르세요. ‘나루’, ‘나무’, ‘너무’.", - "sha256": "756d13453c7e13dcfaa65c8cb3497ac8d3a5d12bedb56dcd74e7f1c022392416", - "bytes": 190516 + "sha256": "341888c75275c13eb31058f53a1d4276be0daa0a1b5a3a45522036928b83b0c1", + "bytes": 382508, + "duration": 7.968, + "rms": 0.09999997157674806, + "peak": 0.600006103515625 }, { "file": "middle-hat.wav", "text": "세 단어 중 가운데를 고르세요. ‘포자’, ‘모자’, ‘모조’.", - "sha256": "6ad5ec1e2c2c0de568c55a0c1f0e8361885725a2515cf396c8fe5d2106ae22d0", - "bytes": 192548 + "sha256": "b01ef9762ac84e3eb757243bc9286c62f7d45444a821ae35ac36a8b095e1598a", + "bytes": 353708, + "duration": 7.368, + "rms": 0.09999999400343942, + "peak": 0.555206298828125 }, { "file": "middle-umbrella.wav", "text": "세 단어 중 가운데를 고르세요. ‘부산’, ‘우산’, ‘오산’.", - "sha256": "a507896dd50f98909c6a9fc0a4bfddfde5c975836e494cd9b7f40b69bc8ec33b", - "bytes": 196102 + "sha256": "399ea2f681f755918f1117c8baba42739ceca64b307b999342f457a9235050b6", + "bytes": 368684, + "duration": 7.68, + "rms": 0.10000004859108667, + "peak": 0.569549560546875 }, { "file": "middle-market.wav", "text": "세 단어 중 가운데를 고르세요. ‘지장’, ‘시장’, ‘사장’.", - "sha256": "900024616e512ea6777d08f80e3e2c83815ea1f03b6eeb537d212995474e1048", - "bytes": 204956 + "sha256": "c855b449e0d4ace74681bfe68e5e7f0b063568827ae872017234dbe8ae119e4c", + "bytes": 356012, + "duration": 7.416, + "rms": 0.10000000455585449, + "peak": 0.594970703125 }, { "file": "middle-oil.wav", "text": "세 단어 중 가운데를 고르세요. ‘이름’, ‘기름’, ‘거름’.", - "sha256": "c42389a5ccd3b39a5877a38e647a6abbec6ff6a7eabf3d4c95c0507fa65d6d2f", - "bytes": 194772 + "sha256": "7a6b122b11c07da86c95fb78e2db2a70681b5219c63d6441fe56da1979ac1dc3", + "bytes": 334124, + "duration": 6.96, + "rms": 0.10000001284837577, + "peak": 0.5399169921875 }, { "file": "middle-sound.wav", "text": "세 단어 중 가운데를 고르세요. ‘오리’, ‘소리’, ‘수리’.", - "sha256": "b05ae2042bc973c3d1873fe15b5bc7f2af7e803794315023486efa788048342c", - "bytes": 188984 + "sha256": "4d00aa6114327f450a00dacbc7e7b572ebf1de4ce9dc2fe4b5505766cca71ce9", + "bytes": 353708, + "duration": 7.368, + "rms": 0.09999999876572897, + "peak": 0.534820556640625 }, { "file": "middle-meat.wav", "text": "세 단어 중 가운데를 고르세요. ‘보기’, ‘고기’, ‘고개’.", - "sha256": "d54d6f9b4bd90b840443b9e0b0390a93909eeb81c37af6a9b57ce545bede532d", - "bytes": 188870 + "sha256": "d416107cc05e7c5b92ca27100a10a8a9689de913346179945d5542de4a6bac0c", + "bytes": 342188, + "duration": 7.128, + "rms": 0.10000002241734321, + "peak": 0.510833740234375 }, { "file": "middle-furniture.wav", "text": "세 단어 중 가운데를 고르세요. ‘하구’, ‘가구’, ‘가교’.", - "sha256": "d9318ef33ae1c60630ddda5abeefcbe43626d8f8a9abd5d62efd132eeb77c8fc", - "bytes": 191024 + "sha256": "b45b1afc8d9b534ff3e0a2a2ecb99efc1f4e40d41f89f55c4ed8ba94ad6d76d4", + "bytes": 354860, + "duration": 7.392, + "rms": 0.10000000075274348, + "peak": 0.5576171875 }, { "file": "message-apple.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘다과’, ‘사과’. 지금은 빨간 불입니다.", - "sha256": "3867563d5acd2530c72cbe6f98ca2b463ec0c92285069c99c8ba084c45aabfd6", - "bytes": 316834 + "sha256": "d85e6f47d871369f00de2274139e5ad6b4b5686b5e20e248df0ed6b9a320d942", + "bytes": 517292, + "duration": 10.776, + "rms": 0.10000001428889653, + "peak": 0.525360107421875 }, { "file": "message-sea.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘사다’, ‘바다’. 지금은 빨간 불입니다.", - "sha256": "b7d8d89d93247eca9aec124f80bf6328d6d88511d05b513ce6d9fa32b3211848", - "bytes": 317816 + "sha256": "28a461fbe03bffe2560030dea8b60715f7c0fe262dcf85b5b7f7daf483836382", + "bytes": 517292, + "duration": 10.776, + "rms": 0.09999998804256775, + "peak": 0.53521728515625 }, { "file": "message-tree.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘나루’, ‘나무’. 지금은 빨간 불입니다.", - "sha256": "9539e5ee87068e241598db2e4fbacb240daa011a2238c9864da8ffde6b644918", - "bytes": 315930 + "sha256": "5d684aa710a63d36ed8dcc2a8bfbdd5c71b0b1c63dad2a324adecb76591e85b8", + "bytes": 540332, + "duration": 11.256, + "rms": 0.09999996112285715, + "peak": 0.5262451171875 }, { "file": "message-hat.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘포자’, ‘모자’. 지금은 빨간 불입니다.", - "sha256": "ccbba6a22c9cbc1231f45fbd7420c6257ac22ac72de8e8c0c97f1cc31bb2b16e", - "bytes": 317258 + "sha256": "5a713a8522b231bb8276805c014b385b9150b79f58ff2618446607e2b442495e", + "bytes": 518444, + "duration": 10.8, + "rms": 0.10000001032745777, + "peak": 0.587005615234375 }, { "file": "message-umbrella.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘부산’, ‘우산’. 지금은 빨간 불입니다.", - "sha256": "e980f4476f7275cc5ae24e7ce0e6d9c1e6db027f7cbd1eb39cc5bc4bc07cbf6e", - "bytes": 320532 + "sha256": "41d5866dae66600df4694a8312b531571d44cece6dba0e423e7c43d09d184819", + "bytes": 523052, + "duration": 10.896, + "rms": 0.09999985338851038, + "peak": 0.530364990234375 }, { "file": "message-market.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘지장’, ‘시장’. 지금은 빨간 불입니다.", - "sha256": "35432a3ed8dd1e15894ace5aad6f5a66cf28509ba92b8ee47bcaae424cb25ba1", - "bytes": 325564 + "sha256": "9242d4cd6e0030e6b073e4d2360f84aa8a454d1749711cdb2d64c14f28a7ea20", + "bytes": 523052, + "duration": 10.896, + "rms": 0.10000000012409799, + "peak": 0.534820556640625 }, { "file": "message-oil.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘이름’, ‘기름’. 지금은 빨간 불입니다.", - "sha256": "814fc749006ed040de58b5236a8d92f0d79683c8eea3b9d095386344f41a47bd", - "bytes": 317786 + "sha256": "a2a71a7e5e07c81871a25c6cb7540c39aa0b32fc44f78cda66b5f34ace98d805", + "bytes": 509228, + "duration": 10.608, + "rms": 0.09999999520245731, + "peak": 0.51812744140625 }, { "file": "message-sound.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘오리’, ‘소리’. 지금은 빨간 불입니다.", - "sha256": "e25c867e6c18d906c010816475253d2b10aa3cc4192b5551ba78055d65c461c8", - "bytes": 313270 + "sha256": "1e682147dae8026ba78dc8f256c7f24acb3a6743bfd88a0fcc86096930eef078", + "bytes": 521900, + "duration": 10.872, + "rms": 0.09999998164749332, + "peak": 0.51953125 }, { "file": "message-meat.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘보기’, ‘고기’. 지금은 빨간 불입니다.", - "sha256": "f1863e1527b7714ae64f5d752b542e8768b8ed89b5885012283f7ca20742ee00", - "bytes": 314464 + "sha256": "ce19735df079b54c171e775b6b365385a80160b9033442abab95b2b9c8505e1a", + "bytes": 512684, + "duration": 10.68, + "rms": 0.10000001752061703, + "peak": 0.5147705078125 }, { "file": "message-furniture.wav", "text": "초록 불이면 앞 단어, 빨간 불이면 뒤 단어입니다. ‘하구’, ‘가구’. 지금은 빨간 불입니다.", - "sha256": "43f87d0e3d92bd132f9ea46e024b9a940736b7399e899866ed42af367a550f78", - "bytes": 316152 + "sha256": "26c331b7e12311236f4bb48f88918dfbf53cccd2f50b55741ea6ff4473e71dd8", + "bytes": 521900, + "duration": 10.872, + "rms": 0.09999998693144198, + "peak": 0.529327392578125 }, { "file": "mission-apple.wav", "text": "오른쪽 표지는 ‘사과’, 왼쪽 표지는 ‘다과’입니다.", - "sha256": "36697151a1a57645f5acd9fcca2d13bffa21937e1bbcde08ab2d8d485730145a", - "bytes": 166540 + "sha256": "3ef42e276ff3500c3e197d149f29f79d6a0c1d5516ed1d5a2c57210333fded9c", + "bytes": 246572, + "duration": 5.136, + "rms": 0.10000000281248776, + "peak": 0.47015380859375 }, { "file": "mission-sea.wav", "text": "오른쪽 표지는 ‘바다’, 왼쪽 표지는 ‘사다’입니다.", - "sha256": "2741f404e39ec64af7f73452f82c68b3ae1d377e9f40a586034f9b147fbfb114", - "bytes": 165122 + "sha256": "8838d6a1ee8a790427dc5aa761e70b3d8872af62086efeed743297ff7f8b8a06", + "bytes": 243116, + "duration": 5.064, + "rms": 0.10000001881351578, + "peak": 0.45013427734375 }, { "file": "mission-tree.wav", "text": "오른쪽 표지는 ‘나무’, 왼쪽 표지는 ‘나루’입니다.", - "sha256": "8a7bb2c795d8877433be2621b69a48c2b6c5c1e192c094c01257fb93eb5d9449", - "bytes": 160834 + "sha256": "00d8e77ec0371971d632724d91f8cf8d947bfb83c582a513b6421be4c457a1da", + "bytes": 241964, + "duration": 5.04, + "rms": 0.09999999375024479, + "peak": 0.458526611328125 }, { "file": "mission-hat.wav", "text": "오른쪽 표지는 ‘모자’, 왼쪽 표지는 ‘포자’입니다.", - "sha256": "26dfa26bd8dcb9d63c231dfc37bf25970f4a8dadcaa40347dd15974887e948b1", - "bytes": 163102 + "sha256": "308278ec2bd27d4224976a55632d1f7fa1edabb0792126d9d858a699c287aee4", + "bytes": 245420, + "duration": 5.112, + "rms": 0.10000000114877691, + "peak": 0.4498291015625 }, { "file": "mission-umbrella.wav", "text": "오른쪽 표지는 ‘우산’, 왼쪽 표지는 ‘부산’입니다.", - "sha256": "79be98918c3fb4e5e4e2e51b2bfa5e5aad6c600478ae1a34c055b16ce71d5264", - "bytes": 167480 + "sha256": "4b4c858a25659540f9dbc524664496fc6de61ab8c1627f3cfee8c9c744e11d5b", + "bytes": 247724, + "duration": 5.16, + "rms": 0.1000000183428341, + "peak": 0.46533203125 }, { "file": "mission-market.wav", "text": "오른쪽 표지는 ‘시장’, 왼쪽 표지는 ‘지장’입니다.", - "sha256": "8f18144a91072be37dc31a161fe6e9bb1980ab36af0edee8d7a359397b8bb8ac", - "bytes": 170724 + "sha256": "cefa0e95649ea6bd1df4c8dfccd2b74b4f4c028714420ce1087b61db2f2a8d47", + "bytes": 248876, + "duration": 5.184, + "rms": 0.10000000503014783, + "peak": 0.485687255859375 }, { "file": "mission-oil.wav", "text": "오른쪽 표지는 ‘기름’, 왼쪽 표지는 ‘이름’입니다.", - "sha256": "1ceabc58f445f7bd5609b2f9de80b309ad64d11c888c7261cf84315a7637a748", - "bytes": 161758 + "sha256": "5fb63e04520c312aa6b7b48b70c2f7c140b5ab480e5c6992edbc2ba85b9a06e7", + "bytes": 241964, + "duration": 5.04, + "rms": 0.10000003066640888, + "peak": 0.443572998046875 }, { "file": "mission-sound.wav", "text": "오른쪽 표지는 ‘소리’, 왼쪽 표지는 ‘오리’입니다.", - "sha256": "6651b14caf4e54069730f137e5d8c3c1e5a9c66aa72f15623cc442edb967cadb", - "bytes": 160162 + "sha256": "659eb0e2b06e6b165b46744769743e032e2282519f8486c57bc45e0f7d07a069", + "bytes": 240812, + "duration": 5.016, + "rms": 0.10000000508879303, + "peak": 0.454803466796875 }, { "file": "mission-meat.wav", "text": "오른쪽 표지는 ‘고기’, 왼쪽 표지는 ‘보기’입니다.", - "sha256": "5d2ab3071fd27e850d99c50ad37905096ef28267b660a35992559bdc126c4931", - "bytes": 162626 + "sha256": "94f8a4f9e116a787e5ed052661d6de9ecf3ad8629db476661dd59ad03014b0c9", + "bytes": 244268, + "duration": 5.088, + "rms": 0.0999999931549985, + "peak": 0.441864013671875 }, { "file": "mission-furniture.wav", "text": "오른쪽 표지는 ‘가구’, 왼쪽 표지는 ‘하구’입니다.", - "sha256": "080307bb46bb47378c0c03437d9ef44118a5d4ea17b2b6d70e5cbc710c71ba78", - "bytes": 163884 + "sha256": "af82e03b686635c3356e706fe5b05c1231aeaaeab33f275aa5b8714988c65d43", + "bytes": 241964, + "duration": 5.04, + "rms": 0.09999996810995776, + "peak": 0.4852294921875 } ] } diff --git a/public/audio/message-apple.wav b/public/audio/message-apple.wav index 33c07b6..0d44224 100644 Binary files a/public/audio/message-apple.wav and b/public/audio/message-apple.wav differ diff --git a/public/audio/message-furniture.wav b/public/audio/message-furniture.wav index a315407..5cae0cb 100644 Binary files a/public/audio/message-furniture.wav and b/public/audio/message-furniture.wav differ diff --git a/public/audio/message-hat.wav b/public/audio/message-hat.wav index 3567e24..c3421a9 100644 Binary files a/public/audio/message-hat.wav and b/public/audio/message-hat.wav differ diff --git a/public/audio/message-market.wav b/public/audio/message-market.wav index 94e8c60..dcb36fb 100644 Binary files a/public/audio/message-market.wav and b/public/audio/message-market.wav differ diff --git a/public/audio/message-meat.wav b/public/audio/message-meat.wav index 607a993..1f40458 100644 Binary files a/public/audio/message-meat.wav and b/public/audio/message-meat.wav differ diff --git a/public/audio/message-oil.wav b/public/audio/message-oil.wav index 8e311d1..9915a1d 100644 Binary files a/public/audio/message-oil.wav and b/public/audio/message-oil.wav differ diff --git a/public/audio/message-sea.wav b/public/audio/message-sea.wav index f215908..2b44958 100644 Binary files a/public/audio/message-sea.wav and b/public/audio/message-sea.wav differ diff --git a/public/audio/message-sound.wav b/public/audio/message-sound.wav index 306e3c9..7a15dc3 100644 Binary files a/public/audio/message-sound.wav and b/public/audio/message-sound.wav differ diff --git a/public/audio/message-tree.wav b/public/audio/message-tree.wav index b0fc06f..2d80203 100644 Binary files a/public/audio/message-tree.wav and b/public/audio/message-tree.wav differ diff --git a/public/audio/message-umbrella.wav b/public/audio/message-umbrella.wav index bc29eab..eda3f4c 100644 Binary files a/public/audio/message-umbrella.wav and b/public/audio/message-umbrella.wav differ diff --git a/public/audio/middle-apple.wav b/public/audio/middle-apple.wav index fc5573a..050daf8 100644 Binary files a/public/audio/middle-apple.wav and b/public/audio/middle-apple.wav differ diff --git a/public/audio/middle-furniture.wav b/public/audio/middle-furniture.wav index 53fcca9..202b0f5 100644 Binary files a/public/audio/middle-furniture.wav and b/public/audio/middle-furniture.wav differ diff --git a/public/audio/middle-hat.wav b/public/audio/middle-hat.wav index fa511b4..38fee81 100644 Binary files a/public/audio/middle-hat.wav and b/public/audio/middle-hat.wav differ diff --git a/public/audio/middle-market.wav b/public/audio/middle-market.wav index ed769b1..264df8b 100644 Binary files a/public/audio/middle-market.wav and b/public/audio/middle-market.wav differ diff --git a/public/audio/middle-meat.wav b/public/audio/middle-meat.wav index 7ab8dc4..9dcd036 100644 Binary files a/public/audio/middle-meat.wav and b/public/audio/middle-meat.wav differ diff --git a/public/audio/middle-oil.wav b/public/audio/middle-oil.wav index 1f32982..794df0f 100644 Binary files a/public/audio/middle-oil.wav and b/public/audio/middle-oil.wav differ diff --git a/public/audio/middle-sea.wav b/public/audio/middle-sea.wav index 1a50c18..e7cd54e 100644 Binary files a/public/audio/middle-sea.wav and b/public/audio/middle-sea.wav differ diff --git a/public/audio/middle-sound.wav b/public/audio/middle-sound.wav index 0eda14c..db79803 100644 Binary files a/public/audio/middle-sound.wav and b/public/audio/middle-sound.wav differ diff --git a/public/audio/middle-tree.wav b/public/audio/middle-tree.wav index d565b05..a32de20 100644 Binary files a/public/audio/middle-tree.wav and b/public/audio/middle-tree.wav differ diff --git a/public/audio/middle-umbrella.wav b/public/audio/middle-umbrella.wav index 6d7106c..2757fa3 100644 Binary files a/public/audio/middle-umbrella.wav and b/public/audio/middle-umbrella.wav differ diff --git a/public/audio/mission-apple.wav b/public/audio/mission-apple.wav index ff18515..614951c 100644 Binary files a/public/audio/mission-apple.wav and b/public/audio/mission-apple.wav differ diff --git a/public/audio/mission-furniture.wav b/public/audio/mission-furniture.wav index b3b6a77..fbbaad0 100644 Binary files a/public/audio/mission-furniture.wav and b/public/audio/mission-furniture.wav differ diff --git a/public/audio/mission-hat.wav b/public/audio/mission-hat.wav index 89b0f39..47d018e 100644 Binary files a/public/audio/mission-hat.wav and b/public/audio/mission-hat.wav differ diff --git a/public/audio/mission-market.wav b/public/audio/mission-market.wav index a78bed0..1651ed5 100644 Binary files a/public/audio/mission-market.wav and b/public/audio/mission-market.wav differ diff --git a/public/audio/mission-meat.wav b/public/audio/mission-meat.wav index a5bf74c..f65a996 100644 Binary files a/public/audio/mission-meat.wav and b/public/audio/mission-meat.wav differ diff --git a/public/audio/mission-oil.wav b/public/audio/mission-oil.wav index 0497e0e..1561937 100644 Binary files a/public/audio/mission-oil.wav and b/public/audio/mission-oil.wav differ diff --git a/public/audio/mission-sea.wav b/public/audio/mission-sea.wav index 919941d..f62f493 100644 Binary files a/public/audio/mission-sea.wav and b/public/audio/mission-sea.wav differ diff --git a/public/audio/mission-sound.wav b/public/audio/mission-sound.wav index d7ceab1..08e40e6 100644 Binary files a/public/audio/mission-sound.wav and b/public/audio/mission-sound.wav differ diff --git a/public/audio/mission-tree.wav b/public/audio/mission-tree.wav index 2caf41d..8176bda 100644 Binary files a/public/audio/mission-tree.wav and b/public/audio/mission-tree.wav differ diff --git a/public/audio/mission-umbrella.wav b/public/audio/mission-umbrella.wav index 39f35f3..fdb6048 100644 Binary files a/public/audio/mission-umbrella.wav and b/public/audio/mission-umbrella.wav differ diff --git a/public/audio/password-apple.wav b/public/audio/password-apple.wav index cd5510a..1c99a55 100644 Binary files a/public/audio/password-apple.wav and b/public/audio/password-apple.wav differ diff --git a/public/audio/password-furniture.wav b/public/audio/password-furniture.wav index 91cdc97..bfe8651 100644 Binary files a/public/audio/password-furniture.wav and b/public/audio/password-furniture.wav differ diff --git a/public/audio/password-hat.wav b/public/audio/password-hat.wav index e47af21..ccfa9a2 100644 Binary files a/public/audio/password-hat.wav and b/public/audio/password-hat.wav differ diff --git a/public/audio/password-market.wav b/public/audio/password-market.wav index 6344a6d..9ad2127 100644 Binary files a/public/audio/password-market.wav and b/public/audio/password-market.wav differ diff --git a/public/audio/password-meat.wav b/public/audio/password-meat.wav index 172ea2f..7f16e3b 100644 Binary files a/public/audio/password-meat.wav and b/public/audio/password-meat.wav differ diff --git a/public/audio/password-oil.wav b/public/audio/password-oil.wav index 2304a39..4babc0a 100644 Binary files a/public/audio/password-oil.wav and b/public/audio/password-oil.wav differ diff --git a/public/audio/password-sea.wav b/public/audio/password-sea.wav index 764d899..1222d77 100644 Binary files a/public/audio/password-sea.wav and b/public/audio/password-sea.wav differ diff --git a/public/audio/password-sound.wav b/public/audio/password-sound.wav index 50f476e..6de34f3 100644 Binary files a/public/audio/password-sound.wav and b/public/audio/password-sound.wav differ diff --git a/public/audio/password-tree.wav b/public/audio/password-tree.wav index ce4e587..d851dc4 100644 Binary files a/public/audio/password-tree.wav and b/public/audio/password-tree.wav differ diff --git a/public/audio/password-umbrella.wav b/public/audio/password-umbrella.wav index bf97452..142e6d2 100644 Binary files a/public/audio/password-umbrella.wav and b/public/audio/password-umbrella.wav differ diff --git a/public/audio/remember-apple.wav b/public/audio/remember-apple.wav index e3c19d5..f47c9ff 100644 Binary files a/public/audio/remember-apple.wav and b/public/audio/remember-apple.wav differ diff --git a/public/audio/remember-furniture.wav b/public/audio/remember-furniture.wav index c969da9..84b7b6d 100644 Binary files a/public/audio/remember-furniture.wav and b/public/audio/remember-furniture.wav differ diff --git a/public/audio/remember-hat.wav b/public/audio/remember-hat.wav index 02fa29a..e316c95 100644 Binary files a/public/audio/remember-hat.wav and b/public/audio/remember-hat.wav differ diff --git a/public/audio/remember-market.wav b/public/audio/remember-market.wav index 71a4ed6..9873294 100644 Binary files a/public/audio/remember-market.wav and b/public/audio/remember-market.wav differ diff --git a/public/audio/remember-meat.wav b/public/audio/remember-meat.wav index 2aac715..560e072 100644 Binary files a/public/audio/remember-meat.wav and b/public/audio/remember-meat.wav differ diff --git a/public/audio/remember-oil.wav b/public/audio/remember-oil.wav index 9e49583..9128ced 100644 Binary files a/public/audio/remember-oil.wav and b/public/audio/remember-oil.wav differ diff --git a/public/audio/remember-sea.wav b/public/audio/remember-sea.wav index d5076ed..0c0da83 100644 Binary files a/public/audio/remember-sea.wav and b/public/audio/remember-sea.wav differ diff --git a/public/audio/remember-sound.wav b/public/audio/remember-sound.wav index e58c076..4f34562 100644 Binary files a/public/audio/remember-sound.wav and b/public/audio/remember-sound.wav differ diff --git a/public/audio/remember-tree.wav b/public/audio/remember-tree.wav index 306c9a1..8ae4c87 100644 Binary files a/public/audio/remember-tree.wav and b/public/audio/remember-tree.wav differ diff --git a/public/audio/remember-umbrella.wav b/public/audio/remember-umbrella.wav index 7db2b2e..3f95ce8 100644 Binary files a/public/audio/remember-umbrella.wav and b/public/audio/remember-umbrella.wav differ diff --git a/public/audio/repeat-apple.wav b/public/audio/repeat-apple.wav index 87d6284..a6c5e95 100644 Binary files a/public/audio/repeat-apple.wav and b/public/audio/repeat-apple.wav differ diff --git a/public/audio/repeat-furniture.wav b/public/audio/repeat-furniture.wav index 9fec355..07a3dbf 100644 Binary files a/public/audio/repeat-furniture.wav and b/public/audio/repeat-furniture.wav differ diff --git a/public/audio/repeat-hat.wav b/public/audio/repeat-hat.wav index 43fd603..bcb1113 100644 Binary files a/public/audio/repeat-hat.wav and b/public/audio/repeat-hat.wav differ diff --git a/public/audio/repeat-market.wav b/public/audio/repeat-market.wav index 1aaf4be..3ed8ea0 100644 Binary files a/public/audio/repeat-market.wav and b/public/audio/repeat-market.wav differ diff --git a/public/audio/repeat-meat.wav b/public/audio/repeat-meat.wav index 54c89a1..76b959b 100644 Binary files a/public/audio/repeat-meat.wav and b/public/audio/repeat-meat.wav differ diff --git a/public/audio/repeat-oil.wav b/public/audio/repeat-oil.wav index a0af96f..dd426b2 100644 Binary files a/public/audio/repeat-oil.wav and b/public/audio/repeat-oil.wav differ diff --git a/public/audio/repeat-sea.wav b/public/audio/repeat-sea.wav index a6bd217..001cedb 100644 Binary files a/public/audio/repeat-sea.wav and b/public/audio/repeat-sea.wav differ diff --git a/public/audio/repeat-sound.wav b/public/audio/repeat-sound.wav index 692a8f2..71a3bc6 100644 Binary files a/public/audio/repeat-sound.wav and b/public/audio/repeat-sound.wav differ diff --git a/public/audio/repeat-tree.wav b/public/audio/repeat-tree.wav index c5e4cc1..61a6349 100644 Binary files a/public/audio/repeat-tree.wav and b/public/audio/repeat-tree.wav differ diff --git a/public/audio/repeat-umbrella.wav b/public/audio/repeat-umbrella.wav index ae7d7b7..a958319 100644 Binary files a/public/audio/repeat-umbrella.wav and b/public/audio/repeat-umbrella.wav differ diff --git a/requirements-audio.txt b/requirements-audio.txt new file mode 100644 index 0000000..c7744eb --- /dev/null +++ b/requirements-audio.txt @@ -0,0 +1,13 @@ +aiohappyeyeballs==2.7.1 +aiohttp==3.14.5 +aiosignal==1.4.0 +attrs==26.1.0 +certifi==2026.7.22 +edge-tts==7.3.1 +frozenlist==1.8.0 +idna==3.20 +multidict==7.1.0 +propcache==0.5.4 +tabulate==0.10.0 +typing_extensions==4.16.0 +yarl==1.25.1 diff --git a/scripts/generate_audio.py b/scripts/generate_audio.py index 1b7d019..d0cfcea 100644 --- a/scripts/generate_audio.py +++ b/scripts/generate_audio.py @@ -1,71 +1,35 @@ -"""직접 작성한 한국어 문장을 로컬 포먼트 합성기로 WAV로 만든다. +"""앱 원문 101개를 pinned edge-tts로 새로 생성하는 명령 진입점. -네트워크 TTS·녹음 샘플·API 키를 사용하지 않는다. 원문은 TypeScript 데이터에서 -읽어 중복 관리를 없애고, 바이트 해시를 남겨 빌드에서 자산 누락·변조를 찾는다. +import에는 부작용이 없다. 원문은 앱의 renderSentence를 그대로 호출하며 +네트워크 생성·검증·원자 교체는 neural_audio가 담당한다. 빌드는 생성기를 호출하지 않는다. """ - -import hashlib -import io import json -import os -from pathlib import Path -import shutil import subprocess -import wave +import sys -ROOT = Path(__file__).resolve().parent.parent -OUTPUT = ROOT / "public/audio" -LOCAL = ROOT / ".cache/espeak/root/usr" -ENV = os.environ.copy() -ENGINE = shutil.which("espeak-ng") -if ENGINE is None: - # 시스템 설치 권한 없이 worktree 안에 압축 해제한 Ubuntu 패키지도 지원한다. - ENGINE = str(LOCAL / "bin/espeak-ng") - ENV["LD_LIBRARY_PATH"] = str(LOCAL / "lib/x86_64-linux-gnu") - ENV["ESPEAK_DATA_PATH"] = str(LOCAL / "lib/x86_64-linux-gnu/espeak-ng-data") +from neural_audio import ROOT, generate -source = subprocess.check_output([ - "node", "--experimental-strip-types", "--input-type=module", "-e", - "import {INTRO,PATTERNS,WORDS,renderSentence} from './src/content.ts'; " - "console.log(JSON.stringify([{file:'intro.wav',text:INTRO}," - "...PATTERNS.flatMap(p=>WORDS.map(w=>({file:p.id+'-'+w.id+'.wav'," - "text:renderSentence(p,w)})))]));", -], cwd=ROOT, text=True) -entries = json.loads(source) -OUTPUT.mkdir(parents=True, exist_ok=True) -# 버전을 상수로 쓰면 다른 PC에서 재생성할 때 출처가 거짓이 된다. 실행 중인 -# 도구에서 직접 읽되 eSpeak의 로컬 데이터 경로는 재현성 비교에서 제외한다. -versions = { - "espeak": subprocess.check_output([ENGINE, "--version"], env=ENV, text=True).split("Data at:")[0].strip(), - "ffmpeg": subprocess.check_output(["ffmpeg", "-version"], text=True).splitlines()[0], -} -for entry in entries: - # 명령 인자로 텍스트를 넘겨 쉘 보간을 피한다. 합성 속도·피치·리샘플링 - # 설정을 고정하고 메타데이터 없는 표준 PCM 헤더를 직접 작성한다. - speech = subprocess.check_output([ - ENGINE, "-v", "ko", "-s", "145", "-p", "45", "-a", "100", "--stdout", entry["text"], - ], env=ENV) - pcm = subprocess.check_output([ - "ffmpeg", "-hide_banner", "-loglevel", "error", "-i", "pipe:0", - "-ar", "16000", "-ac", "1", "-f", "s16le", "pipe:1", - ], input=speech) - stream = io.BytesIO() - with wave.open(stream, "wb") as output: - output.setnchannels(1) - output.setsampwidth(2) - output.setframerate(16000) - output.writeframes(pcm) - data = stream.getvalue() - (OUTPUT / entry["file"]).write_bytes(data) - entry["sha256"] = hashlib.sha256(data).hexdigest() - entry["bytes"] = len(data) -manifest = { - "generator": "eSpeak NG ko, speed 145, pitch 45, amplitude 100; FFmpeg PCM 16kHz mono s16", - "toolVersions": versions, - "source": "https://github.com/espeak-ng/espeak-ng", - "method": "자체 작성 문장의 오프라인 포먼트 합성; 외부 녹음·네트워크 TTS 없음", - "files": entries, -} -(OUTPUT / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n") -print(f"{len(entries)}개 WAV 생성, 합계 {sum(e['bytes'] for e in entries):,} 바이트") +def main(): + source = subprocess.check_output([ + "node", "--experimental-strip-types", "--input-type=module", "-e", + "import {INTRO,PATTERNS,WORDS,renderSentence} from './src/content.ts'; " + "console.log(JSON.stringify([{file:'intro.wav',text:INTRO}," + "...PATTERNS.flatMap(p=>WORDS.map(w=>({file:p.id+'-'+w.id+'.wav'," + "text:renderSentence(p,w)})))]));", + ], cwd=ROOT, text=True, timeout=10) + manifest = generate(json.loads(source), ROOT / "public/audio") + print(f"{len(manifest['files'])}개 신규 WAV 원자 교체 완료, " + f"합계 {sum(e['bytes'] for e in manifest['files']):,} 바이트") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("음성 생성 중단; 완성 전 공개 자산 변경 없음", file=sys.stderr) + sys.exit(130) + except Exception as error: + # 인증 응답이 섞일 수 있는 외부 예외는 adapter에서 안전한 메시지로 바뀐다. + print(f"음성 생성 실패: {error}", file=sys.stderr) + sys.exit(1) diff --git a/scripts/neural_audio.py b/scripts/neural_audio.py new file mode 100644 index 0000000..e919197 --- /dev/null +++ b/scripts/neural_audio.py @@ -0,0 +1,204 @@ +"""한국어 neural 음성을 임시 공간에서 완성하고 검증 후 한 번에 공개한다. + +외부 서비스는 생성할 때만 접근한다. public에는 PCM과 허용된 출처 정보만 +남긴다. 테스트는 provider/변환 경계에 fake를 넣어 네트워크 없이 실패를 재현한다. +""" + +from array import array +import ctypes +from datetime import datetime, timezone +import hashlib +import importlib.metadata +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import time +import wave + +ROOT = Path(__file__).resolve().parent.parent +VOICE = "ko-KR-HyunsuMultilingualNeural" +RATE = "-6%" +PITCH = "-4Hz" +SAMPLE_RATE = 24000 +EDGE_VERSION = "7.3.1" +ATTEMPTS = 3 +TIMEOUT = 60 + + +def tool_versions(): + """실행 중인 도구를 조회한다. 다른 edge-tts를 몰래 사용하는 fallback은 없다.""" + try: + edge = importlib.metadata.version("edge-tts") + except importlib.metadata.PackageNotFoundError: + edge = "missing" + if edge != EDGE_VERSION: + raise RuntimeError("edge-tts==7.3.1이 필요합니다. requirements-audio.txt로 venv를 설치하세요.") + result = subprocess.run(["ffmpeg", "-version"], check=True, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True, timeout=10) + return {"edge-tts": edge, "ffmpeg": result.stdout.splitlines()[0]} + + +def synthesize_edge(text, path): + """별도 프로세스의 전체 시간을 제한하고 provider 응답·인증 헤더를 기록하지 않는다.""" + subprocess.run([ + sys.executable, "-m", "edge_tts", "--voice", VOICE, + f"--rate={RATE}", f"--pitch={PITCH}", "--text", text, + "--write-media", str(path), + ], check=True, timeout=TIMEOUT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def pcm_values(data): + samples = array("h", data) + if sys.byteorder != "little": + samples.byteswap() + return samples + + +def levels(samples): + if not samples: + raise ValueError("빈 PCM입니다.") + rms = math.sqrt(sum(x*x for x in samples) / len(samples)) / 32768 + peak = max(abs(x) for x in samples) / 32768 + return rms, peak + + +def convert_mp3(source, destination): + """24kHz mono로 디코드한 뒤 파일 전체에 동일 gain만 적용한다. + + RMS 0.10을 목표로 하되 최대 증폭 2배, peak 0.89로 제한한다. 압축·게이트· + 잘라내기 없이 자음의 시간·주파수 정보를 보존하고 작은 입력도 과증폭하지 않는다. + """ + decoded = subprocess.run([ + "ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(source), + "-ar", str(SAMPLE_RATE), "-ac", "1", "-f", "s16le", "pipe:1", + ], check=True, timeout=TIMEOUT, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout + samples = pcm_values(decoded) + rms, peak = levels(samples) + if rms <= 100 / 32768: + raise ValueError("무음 또는 RMS가 너무 낮습니다.") + gain = min(0.1 / rms, 0.89 / peak, 2.0) + normalized = array("h", (round(x * gain) for x in samples)) + if sys.byteorder != "little": + normalized.byteswap() + # wave 모듈의 표준 44-byte PCM 헤더로 도구별 부가 메타데이터 차이를 피한다. + with wave.open(str(destination), "wb") as stream: + stream.setparams((1, 2, SAMPLE_RATE, 0, "NONE", "not compressed")) + stream.writeframes(normalized.tobytes()) + + +def inspect_wav(path): + """헤더뿐 아니라 전체 PCM을 측정한다. 기준 밖 파일은 공개 대상에서 제외한다.""" + data = path.read_bytes() + with wave.open(str(path), "rb") as stream: + if (stream.getframerate(), stream.getnchannels(), stream.getsampwidth(), stream.getcomptype()) != (SAMPLE_RATE, 1, 2, "NONE"): + raise ValueError(f"{path.name}: 24kHz mono PCM16이 아닙니다.") + duration = stream.getnframes() / SAMPLE_RATE + if not 1 <= duration <= 20: + raise ValueError(f"{path.name}: 길이 {duration:.3f}초가 1–20초 범위를 벗어났습니다.") + raw = stream.readframes(stream.getnframes()) + if len(raw) != stream.getnframes() * 2 or len(data) != len(raw) + 44: + raise ValueError(f"{path.name}: 잘리거나 비표준 PCM 파일입니다.") + rms, peak = levels(pcm_values(raw)) + if rms <= 100 / 32768: + raise ValueError(f"{path.name}: 무음 또는 RMS가 너무 낮습니다.") + if peak >= 32767 / 32768: + raise ValueError(f"{path.name}: full-scale peak입니다.") + if peak / rms * 0.03 * 2 >= 0.8: + raise ValueError(f"{path.name}: 런타임 gain의 peak 여유가 부족합니다.") + return {"sha256": hashlib.sha256(data).hexdigest(), "bytes": len(data), + "duration": duration, "rms": rms, "peak": peak} + + +def atomic_publish(staged, output): + """같은 파일시스템의 완성 디렉터리를 한 syscall로 교체한다 (Linux). + + rename 두 번을 사용하면 public/audio가 잠깐 사라진다. RENAME_EXCHANGE는 + 기존 트리를 staged로 돌려놓아 실패 시 기존 이름을 유지하고 성공 시에만 + 임시 폴더 정리로 이전 파일이 제거되게 한다. 미지원 환경은 안전하게 실패한다. + """ + if not output.exists(): + os.rename(staged, output) + return + libc = ctypes.CDLL(None, use_errno=True) + exchange = getattr(libc, "renameat2", None) + if exchange is None: + raise OSError("이 환경은 원자 디렉터리 교환(renameat2)을 지원하지 않습니다.") + exchange.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint] + exchange.restype = ctypes.c_int + if exchange(-100, os.fsencode(staged), -100, os.fsencode(output), 2) != 0: + error = ctypes.get_errno() + raise OSError(error, "원자 디렉터리 교환 실패; 기존 음성을 보존했습니다.") + + +def generate(entries, output, *, synthesize=None, convert=None, sleep=None): + """모든 입력·도구를 확인한 뒤 생성한다. 어떤 실패도 기존 public을 수정하지 않는다.""" + names = [e["file"] for e in entries] + if (len(entries) != 101 or len(set(names)) != 101 or names[0] != "intro.wav" + or any(not re.fullmatch(r"[a-z0-9-]+\.wav", name) for name in names) + or any(not isinstance(e["text"], str) or not e["text"].strip() for e in entries)): + raise ValueError("안내 1개와 고유한 안전 경로의 목표 문장 100개가 필요합니다.") + versions = tool_versions() + synthesize = synthesize or synthesize_edge + convert = convert or convert_mp3 + sleep = sleep or time.sleep + output = Path(output) + if output.is_symlink(): + raise ValueError("음성 출력 디렉터리에 symlink를 사용할 수 없습니다.") + output.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix=".audio-stage-", dir=output.parent) as temporary: + working = Path(temporary) + ready = working / "ready" + ready.mkdir() + files = [] + for index, entry in enumerate(entries, 1): + mp3 = working / "speech.mp3" + for attempt in range(1, ATTEMPTS + 1): + mp3.unlink(missing_ok=True) + try: + synthesize(entry["text"], mp3) + if not mp3.is_file() or mp3.stat().st_size == 0: + raise OSError("provider가 빈 음성을 반환했습니다.") + break + except (subprocess.SubprocessError, OSError): + # 원래 예외의 URL·응답·명령 인자는 기록하지 않는다. traceback의 + # cause도 숨겨 service 내부 인증값이 로그에 섞이지 않도록 한다. + print(f"{entry['file']}: provider 시도 {attempt}/{ATTEMPTS} 실패", flush=True) + if attempt == ATTEMPTS: + raise RuntimeError(f"{entry['file']}: provider 생성 {ATTEMPTS}회 실패; 기존 자산 보존") from None + sleep(2 ** (attempt - 1)) + destination = ready / entry["file"] + try: + convert(mp3, destination) + except (subprocess.SubprocessError, OSError): + raise RuntimeError(f"{entry['file']}: PCM 변환 실패; 기존 자산 보존") from None + finally: + mp3.unlink(missing_ok=True) + files.append({**entry, **inspect_wav(destination)}) + print(f"[{index:03}/101] {entry['file']} {files[-1]['duration']:.3f}s", flush=True) + # 앞서 생성한 파일도 마지막에 다시 읽는다. manifest와 실제 집합·바이트를 + # 모두 대조해야 생성 중 손상된 파일이나 임시 파일의 유출을 막을 수 있다. + if sorted(p.name for p in ready.iterdir()) != sorted(names): + raise ValueError("생성된 파일 집합이 원문 목록과 다릅니다.") + for entry in files: + if {**entry, **inspect_wav(ready / entry["file"])} != entry: + raise ValueError(f"{entry['file']}: 최종 검증에서 파일이 변경되었습니다.") + if sum(e["bytes"] for e in files) >= 48 * 1024 * 1024: + raise ValueError("음성 자산이 48MiB 예산을 초과했습니다.") + manifest = { + "version": json.loads((ROOT / "package.json").read_text())["version"], + "provider": "Microsoft Edge online TTS", "voice": VOICE, "rate": RATE, "pitch": PITCH, + "sampleRate": SAMPLE_RATE, "channels": 1, "sampleFormat": "PCM s16le", + "toolVersions": versions, "generatedAtUtc": datetime.now(timezone.utc).isoformat(), + "source": "https://github.com/rany2/edge-tts", + "method": "온라인 neural 생성 MP3 → FFmpeg decode → 파일별 선형 gain → 로컬 PCM WAV", + "normalization": {"targetRms": 0.1, "peakCeiling": 0.89, "maxGain": 2.0}, + "files": files, + } + (ready / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + atomic_publish(ready, output) + return manifest diff --git a/src/App.tsx b/src/App.tsx index 3c2be36..3f6d473 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -200,7 +200,7 @@ export default function App({ audio, seed }: AppProps) {

단어를 맞춰라

- v0.1 + v0.1.1
{game ? ( diff --git a/tests/App.test.tsx b/tests/App.test.tsx index d631e63..a1c56cb 100644 --- a/tests/App.test.tsx +++ b/tests/App.test.tsx @@ -201,7 +201,7 @@ describe("시작부터 결과까지 사용자 흐름", () => { exact: true, }), ).toBeInTheDocument(); - expect(screen.getByText("v0.1")).toBeInTheDocument(); + expect(screen.getByText("v0.1.1")).toBeInTheDocument(); expect(f.audio.play).not.toHaveBeenCalled(); expect( screen.getByRole("button", { name: "게임 시작 · 소리 듣기" }), diff --git a/tests/assets.test.ts b/tests/assets.test.ts index 2090a98..31442ad 100644 --- a/tests/assets.test.ts +++ b/tests/assets.test.ts @@ -2,44 +2,61 @@ import { createHash } from "node:crypto"; import { readFileSync, readdirSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { INTRO, PATTERNS, WORDS } from "../src/content"; +import { INTRO, PATTERNS, WORDS, renderSentence } from "../src/content"; -// 생성 바이너리를 눈으로 검토할 수 없으므로 문장·해시·PCM 포맷·실제 신호를 -// 함께 확인한다. manifest는 생성기가 남기며 100개 조합과 안내를 빠짐없이 묶는다. -describe("저장한 한국어 음성 자산", () => { - it("실제 생성 도구 버전을 기록하여 재생성의 출처를 추적한다", () => { - const manifest = JSON.parse( - readFileSync("public/audio/manifest.json", "utf8"), - ) as { toolVersions?: { espeak: string; ffmpeg: string } }; - expect(manifest.toolVersions?.espeak).toMatch(/eSpeak NG.*\d+\.\d+/); - expect(manifest.toolVersions?.ffmpeg).toMatch(/ffmpeg version \d+\.\d+/); +const manifest = JSON.parse( + readFileSync("public/audio/manifest.json", "utf8"), +) as { + version: string; + provider: string; + voice: string; + rate: string; + pitch: string; + sampleRate: number; + toolVersions: { "edge-tts"?: string; ffmpeg?: string }; + files: { + file: string; + text: string; + sha256: string; + bytes: number; + duration: number; + }[]; +}; +const expected = [ + { file: "intro.wav", text: INTRO }, + ...PATTERNS.flatMap((p) => + WORDS.map((w) => ({ + file: `${p.id}-${w.id}.wav`, + text: renderSentence(p, w), + })), + ), +]; + +// 생성기의 검증 함수를 재사용하지 않고 실제 WAV 바이트를 직접 읽는다. +// 문장·해시·헤더·신호를 묶어 manifest만 갱신하는 거짓 업그레이드를 막는다. +describe("저장한 한국어 neural 음성 자산", () => { + it("0.1.1 neural provider·voice·prosody와 실측 도구 버전을 기록한다", () => { + expect.soft(manifest.version).toBe("0.1.1"); + expect.soft(manifest.provider).toBe("Microsoft Edge online TTS"); + expect.soft(manifest.voice).toBe("ko-KR-HyunsuMultilingualNeural"); + expect.soft(manifest.rate).toBe("-6%"); + expect.soft(manifest.pitch).toBe("-4Hz"); + expect.soft(manifest.sampleRate).toBe(24000); + expect.soft(manifest.toolVersions["edge-tts"]).toBe("7.3.1"); + expect + .soft(manifest.toolVersions.ffmpeg) + .toMatch(/^ffmpeg version \d+\.\d+/); + expect(JSON.stringify(manifest)).not.toMatch( + /espeak|cookie|authorization|trustedclienttoken/i, + ); }); - it("모든 문장과 안내에 대응하는 로컬 WAV·출처·해시를 제공한다", () => { - const manifest = JSON.parse( - readFileSync("public/audio/manifest.json", "utf8"), - ) as { - generator: string; - files: { file: string; text: string; sha256: string }[]; - }; - expect(manifest.generator).toContain("eSpeak NG"); - const expected = [ - { file: "intro.wav", text: INTRO }, - ...PATTERNS.flatMap((p) => - WORDS.map((w) => ({ - file: `${p.id}-${w.id}.wav`, - text: p.sentence - .replaceAll("{word}", w.answer) - .replaceAll("{distractor1}", w.distractors[0]!.word) - .replaceAll("{distractor2}", w.distractors[1]!.word), - })), - ), - ]; + it("101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다", () => { expect(manifest.files.map(({ file, text }) => ({ file, text }))).toEqual( expected, ); - expect( - readdirSync("public/audio").filter((f) => f.endsWith(".wav")), - ).toHaveLength(101); + expect(readdirSync("public/audio").sort()).toEqual( + [...expected.map((e) => e.file), "manifest.json"].sort(), + ); let size = 0; for (const entry of manifest.files) { const file = readFileSync(`public/audio/${entry.file}`); @@ -47,26 +64,34 @@ describe("저장한 한국어 음성 자산", () => { expect(createHash("sha256").update(file).digest("hex")).toBe( entry.sha256, ); + expect(file.length).toBe(entry.bytes); expect(file.toString("ascii", 0, 4)).toBe("RIFF"); expect(file.toString("ascii", 8, 12)).toBe("WAVE"); + expect(file.toString("ascii", 12, 16)).toBe("fmt "); + expect(file.readUInt32LE(16)).toBe(16); + expect(file.readUInt16LE(20)).toBe(1); expect(file.readUInt16LE(22)).toBe(1); - expect(file.readUInt32LE(24)).toBe(16000); + expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); expect(file.readUInt16LE(34)).toBe(16); - expect(file.length).toBeGreaterThan(16000); - const values = Array.from({ length: (file.length - 44) / 2 }, (_, i) => - file.readInt16LE(44 + i * 2), - ); - const rms = Math.sqrt( - values.reduce((sum, x) => sum + x * x, 0) / values.length, - ); + expect(file.toString("ascii", 36, 40)).toBe("data"); + expect(file.readUInt32LE(40)).toBe(file.length - 44); + const duration = (file.length - 44) / 2 / file.readUInt32LE(24); + expect.soft(entry.duration, entry.file).toBe(duration); + expect(duration).toBeGreaterThanOrEqual(1); + expect(duration).toBeLessThanOrEqual(20); + let sum = 0, + peak = 0; + for (let offset = 44; offset < file.length; offset += 2) { + const value = file.readInt16LE(offset); + sum += value * value; + peak = Math.max(peak, Math.abs(value)); + } + const rms = Math.sqrt(sum / ((file.length - 44) / 2)); expect(rms).toBeGreaterThan(100); - // RMS 정규화 후 최고 난이도 아닌 가장 큰 +6dB에서도 클리핑 여유를 확인한다. - expect( - (values.reduce((peak, x) => Math.max(peak, Math.abs(x)), 0) / rms) * - 0.03 * - 2, - ).toBeLessThan(0.8); + expect(peak).toBeLessThan(32767); + // 런타임 최대 +6dB와 RMS 정규화에서도 기존 클리핑 여유를 보존한다. + expect((peak / rms) * 0.03 * 2).toBeLessThan(0.8); } - expect(size).toBeLessThan(20 * 1024 * 1024); + expect(size).toBeLessThan(48 * 1024 * 1024); }); }); diff --git a/tests/browser/game.spec.ts b/tests/browser/game.spec.ts index c6cb907..aaa6200 100644 --- a/tests/browser/game.spec.ts +++ b/tests/browser/game.spec.ts @@ -13,10 +13,7 @@ test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 page.on("request", (request) => { if (!request.url().startsWith("http://127.0.0.1:4173")) external.push(request.url()); - if ( - request.url().endsWith(".wav") && - !request.url().endsWith("intro.wav") - ) + if (request.url().endsWith(".wav") && !request.url().endsWith("intro.wav")) audioPath = new URL(request.url()).pathname; }); page.on("pageerror", (error) => errors.push(error.message)); @@ -32,19 +29,13 @@ test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 ).toBe(true); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ - path: `.agent/evidence/release-${info.project.name}-home.png`, + path: `.agent/evidence-v0.1.1/${info.project.name}-home.png`, fullPage: true, }); - expect((await new AxeBuilder({ page }).analyze()).violations).toEqual( - [], - ); + expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); await page.emulateMedia({ reducedMotion: "reduce" }); - await page - .getByRole("button", { name: "게임 시작 · 소리 듣기" }) - .click(); - await expect( - page.getByRole("button", { name: /듣는 중/ }), - ).toBeDisabled(); + await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click(); + await expect(page.getByRole("button", { name: /듣는 중/ })).toBeDisabled(); await expect(page.locator(".sound-wave.active i").first()).toHaveCSS( "animation-name", "none", @@ -76,12 +67,10 @@ test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 ).toBe(true); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ - path: `.agent/evidence/release-${info.project.name}-game.png`, + path: `.agent/evidence-v0.1.1/${info.project.name}-game.png`, fullPage: true, }); - expect( - (await new AxeBuilder({ page }).analyze()).violations, - ).toEqual([]); + expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); await page.getByRole("button", { name: /다시 듣기/ }).click(); await expect(target).toBeVisible(); } @@ -110,12 +99,10 @@ test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 ).toBe(true); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ - path: `.agent/evidence/release-${info.project.name}-results.png`, + path: `.agent/evidence-v0.1.1/${info.project.name}-results.png`, fullPage: true, }); - expect((await new AxeBuilder({ page }).analyze()).violations).toEqual( - [], - ); + expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); await page.getByRole("button", { name: "다시 시작" }).click(); await expect( page.getByRole("button", { name: "게임 시작 · 소리 듣기" }), @@ -129,15 +116,13 @@ test("오디오 HTTP 실패를 화면에 표시하고 재시도한다", async ({ route.fulfill({ status: 404, body: "missing" }), ); await page.goto("/"); - await page - .getByRole("button", { name: "게임 시작 · 소리 듣기" }) - .click(); + await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click(); await expect(page.getByRole("alert")).toContainText( "소리를 재생하지 못했어요", ); await page.unroute("**/audio/*.wav"); await page.getByRole("button", { name: "다시 시도" }).click(); - // 재시도는 안내부터 재생한다. 최대 8.1205 + 10.1725 + 시작·전환 0.4초에 + // 재시도는 안내부터 재생한다. v0.1.1 최대 7.056 + 11.256 + 시작·전환 0.4초에 // 로드·디코드·병렬 브라우저 스케줄링 여유를 더해 이 assertion만 30초로 제한한다. await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({ timeout: 30_000, diff --git a/tests/browser/ui.spec.ts b/tests/browser/ui.spec.ts index 4c5c321..26f3bbf 100644 --- a/tests/browser/ui.spec.ts +++ b/tests/browser/ui.spec.ts @@ -19,9 +19,7 @@ test("전체 음량은 설정 흐름 안에 들어가며 키보드로 조절된 .soft(control.y + control.height) .toBeLessThanOrEqual(bounds.y + bounds.height); }; - await expectInside( - page.getByRole("region", { name: "나의 소리 탐험 준비" }), - ); + await expectInside(page.getByRole("region", { name: "나의 소리 탐험 준비" })); const start = page.getByRole("button", { name: "게임 시작 · 소리 듣기", }); @@ -53,14 +51,10 @@ test("전체 음량은 설정 흐름 안에 들어가며 키보드로 조절된 test("난이도 설명은 모바일에서도 보인다", async ({ page }) => { await page.goto("/"); - await page - .getByRole("button", { name: "게임 시작 · 소리 듣기" }) - .click(); + await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click(); // DOM에만 설명을 남기고 모바일 CSS로 숨기면 컴포넌트 검증은 통과하므로 // 실제 화면에서 난이도 배지와 쉬운 설명을 모두 읽을 수 있는지 확인한다. - await expect - .soft(page.locator(".db-note")) - .toBeVisible({ timeout: 1000 }); + await expect.soft(page.locator(".db-note")).toBeVisible({ timeout: 1000 }); await expect .soft(page.getByText("1단계 · SNR +6 dB")) .toBeVisible({ timeout: 1000 }); @@ -86,17 +80,13 @@ test("한국어 제목은 단어 중간에서 줄바꿈하지 않는다", async })), ); const homeHeadings = await wordBreaks(); - expect - .soft(homeHeadings) - .toEqual( - homeHeadings.map((heading) => ({ - ...heading, - wordBreak: "keep-all", - })), - ); - await page - .getByRole("button", { name: "게임 시작 · 소리 듣기" }) - .click(); + expect.soft(homeHeadings).toEqual( + homeHeadings.map((heading) => ({ + ...heading, + wordBreak: "keep-all", + })), + ); + await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click(); const question = page.getByRole("heading", { name: createQuestions(42, "mixed")[0]!.pattern.question, }); @@ -105,18 +95,16 @@ test("한국어 제목은 단어 중간에서 줄바꿈하지 않는다", async // 이미지 안으로 들어오는 Chrome 캡처 아티팩트를 방지한다. 초점은 유지한다. await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ - path: `.agent/evidence/release-${info.project.name}-heading.png`, + path: `.agent/evidence-v0.1.1/${info.project.name}-heading.png`, fullPage: true, }); const gameHeadings = await wordBreaks(); - expect - .soft(gameHeadings) - .toEqual( - gameHeadings.map((heading) => ({ - ...heading, - wordBreak: "keep-all", - })), - ); + expect.soft(gameHeadings).toEqual( + gameHeadings.map((heading) => ({ + ...heading, + wordBreak: "keep-all", + })), + ); expect( await question.evaluate( (heading) => heading.scrollWidth <= heading.clientWidth, diff --git a/tests/generator.test.ts b/tests/generator.test.ts new file mode 100644 index 0000000..3e8c672 --- /dev/null +++ b/tests/generator.test.ts @@ -0,0 +1,14 @@ +// @vitest-environment node +import { spawnSync } from "node:child_process"; +import { expect, it } from "vitest"; + +// Python 경계 테스트도 npm test에 포함한다. provider 호출은 fake이며 ffmpeg +// 변환과 Linux 원자 디렉터리 교체는 실제로 실행한다. 외부 네트워크는 쓰지 않는다. +it("neural 생성기의 실패 보존·retry·PCM·비밀값 부재 계약", () => { + const result = spawnSync( + "python3", + ["-m", "unittest", "discover", "-s", "tests/python", "-v"], + { encoding: "utf8" }, + ); + expect(result.status, result.stdout + result.stderr).toBe(0); +}, 60_000); diff --git a/tests/neural-release.test.ts b/tests/neural-release.test.ts new file mode 100644 index 0000000..a48dc5b --- /dev/null +++ b/tests/neural-release.test.ts @@ -0,0 +1,44 @@ +// @vitest-environment node +import { readFileSync } from "node:fs"; +import { expect, it } from "vitest"; + +// 배포 표기와 재현 명령을 함께 고정한다. 과거 인수 기록은 검사·수정하지 않는다. +it("0.1.1 버전을 package·lock·UI·index·문서에 일치시킨다", () => { + const pkg = JSON.parse(readFileSync("package.json", "utf8")); + const lock = JSON.parse(readFileSync("package-lock.json", "utf8")); + expect.soft(pkg.version).toBe("0.1.1"); + expect.soft(lock.version).toBe("0.1.1"); + expect.soft(lock.packages[""].version).toBe("0.1.1"); + for (const path of [ + "src/App.tsx", + "index.html", + "README.md", + "LICENSE", + "docs/AUDIO_PROVENANCE.md", + ]) { + expect.soft(readFileSync(path, "utf8"), path).toContain("0.1.1"); + } +}); + +it("고정 venv 생성 명령과 provider·도구·출력물 권리의 경계를 문서화한다", () => { + const pkg = JSON.parse(readFileSync("package.json", "utf8")); + expect + .soft(pkg.scripts["audio:generate"]) + .toBe(".venv-audio/bin/python scripts/generate_audio.py"); + for (const path of ["README.md", "docs/AUDIO_PROVENANCE.md"]) { + const doc = readFileSync(path, "utf8"); + expect.soft(doc, path).toContain("requirements-audio.txt"); + expect.soft(doc, path).toContain("ko-KR-HyunsuMultilingualNeural"); + expect.soft(doc, path).toContain("7.3.1"); + expect.soft(doc, path).not.toMatch(/espeak|포먼트/i); + } + const provenance = readFileSync("docs/AUDIO_PROVENANCE.md", "utf8"); + expect + .soft(provenance) + .toContain("https://www.microsoft.com/en-us/servicesagreement"); + expect.soft(provenance).toContain("LGPL"); + expect.soft(provenance).toContain("독점 권리를 주장하지 않는다"); + expect + .soft(readFileSync("LICENSE", "utf8")) + .toContain("does not claim exclusive rights to generated speech"); +}); diff --git a/tests/python/test_neural_audio.py b/tests/python/test_neural_audio.py new file mode 100644 index 0000000..8d634e4 --- /dev/null +++ b/tests/python/test_neural_audio.py @@ -0,0 +1,256 @@ +"""외부 provider 대신 작은 PCM fixture로 실패·복구·원자 공개 계약을 검증한다. + +실제 네트워크 생성은 별도 인수 단계에서 101개 전부 실행한다. 이 테스트의 +가짜 MP3는 adapter 경계용이며 public 자산이나 생성 evidence로 쓰지 않는다. +""" +import contextlib +import hashlib +import importlib.util +import io +import json +import math +from pathlib import Path +import struct +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch +import wave + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "scripts")) +import neural_audio as audio + +ENTRIES = [{"file": "intro.wav", "text": "안내 문장"}] + [ + {"file": f"target-{i}.wav", "text": f"목표 문장 {i}"} for i in range(100) +] +VERSIONS = {"edge-tts": "7.3.1", "ffmpeg": "ffmpeg version fixture"} + + +def wav(path, *, rate=24000, channels=1, width=2, duration=1, amplitude=4000): + values = [int(amplitude * math.sin(2 * math.pi * 220 * i / rate)) for i in range(int(rate * duration))] + with wave.open(str(path), "wb") as stream: + stream.setparams((channels, width, rate, 0, "NONE", "not compressed")) + stream.writeframes(b"".join(struct.pack("