$ npx vitest run tests/App.test.tsx tests/neural-release.test.ts RUN v5.0.4 /home/hermes/worktrees/find-word-v0.2.3 ❯ tests/neural-release.test.ts (4 tests | 1 failed) 54ms × README는 현재 리뷰 증거 경로와 정확한 라운드 보너스를 안내한다 44ms ❯ tests/App.test.tsx (23 tests | 6 failed) 6299ms × 오디오 started=false 중 종료는 즉시 정지하고 결과를 거쳐 홈으로 간다 227ms × 오디오 started=true 중 종료는 즉시 정지하고 결과를 거쳐 홈으로 간다 208ms × 5단계 완료는 6단계 대신 같은 단계의 새 세 문항을 시작한다 601ms × 세 문항 라운드에서 가능한 연속 정답 보너스 상한을 설명한다 100ms × 현재 문항 진행률은 1/3→2/3→3/3이고 1단계 만점은 330점이다 221ms × 무응답 종료는 정답률 대신 미응답 상태와 단계별 미진행을 보여 준다 215ms ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 7 ⎯⎯⎯⎯⎯⎯⎯ FAIL tests/App.test.tsx > 오디오 started=false 중 종료는 즉시 정지하고 결과를 거쳐 홈으로 간다 FAIL tests/App.test.tsx > 오디오 started=true 중 종료는 즉시 정지하고 결과를 거쳐 홈으로 간다 TestingLibraryElementError: Unable to find an accessible element with the role "heading" and name "문제 풀이를 중간에 종료했어요." Here are the accessible roles: link: Name "본문으로 건너뛰기":  Name "환경음 출처·라이선스":  -------------------------------------------------- banner: Name "":  -------------------------------------------------- heading: Name "단어를 맞춰라": 

 Name "문제 풀이를 종료했어요.":  -------------------------------------------------- main: Name "":  -------------------------------------------------- paragraph: Name "":  Name "": 

 Name "":  Name "": 

 Name "": 

 -------------------------------------------------- strong: Name "":  Name "":  Name "":  -------------------------------------------------- table: Name "난이도별 결과":  -------------------------------------------------- rowgroup: Name "":  Name "":  -------------------------------------------------- row: Name "난이도 SNR 맞힌 문제":  Name "1단계 SNR +6 dB 0 / 0":  Name "2단계 SNR +3 dB 0 / 0":  Name "3단계 SNR 0 dB 0 / 0":  Name "4단계 SNR -3 dB 0 / 0":  Name "5단계 SNR -6 dB 0 / 0":  -------------------------------------------------- columnheader: Name "난이도":  Name "SNR":  Name "맞힌 문제":  -------------------------------------------------- rowheader: Name "1단계":  Name "2단계":  Name "3단계":  Name "4단계":  Name "5단계":  -------------------------------------------------- cell: Name "SNR +6 dB":  Name "0 / 0":  Name "SNR +3 dB":  Name "0 / 0":  Name "SNR 0 dB":  Name "0 / 0":  Name "SNR -3 dB":  Name "0 / 0":  Name "SNR -6 dB":  Name "0 / 0":  -------------------------------------------------- button: Name "처음 화면으로":  -------------------------------------------------- slider: Name "전체 음량":  -------------------------------------------------- status: Name "":  -------------------------------------------------- contentinfo: Name "":  -------------------------------------------------- Ignored nodes: comments, script, style  

  본문으로 건너뛰기          

 단어를 맞춰라 

  v0.2.3  
   소리에 집중하는 시간      지금까지의 소리 탐험          S     문제 풀이를 종료했어요. 

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

 

 응답  0 문항 · 정답  0 문항 

  
  0   총 점수  
 
  0 %   정답률 ·  0 / 0  
 
  0  회    최고 연속 정답  
      난이도   SNR   맞힌 문제       1 단계   SNR +6 dB   0  /  0     2 단계   SNR +3 dB   0  /  0     3 단계   SNR 0 dB   0  /  0     4 단계   SNR -3 dB   0  /  0     5 단계   SNR -6 dB   0  /  0      단계 × 100점 + 연속 정답 보너스 (라운드 내 최대   20 점) 

  처음 화면으로      ◖))   전체 음량    35 %     

 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:415:14 413| expect(f.audio.stop).toHaveBeenCalledOnce(); 414| expect( 415| screen.getByRole("heading", { name: "문제 풀이를 중간에 종료했어요." }), | ^ 416| ).toHaveFocus(); 417| expect(screen.getByText("응답 0문항 · 정답 0문항")).toBeInTheDocument(); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/8]⎯ FAIL tests/App.test.tsx > 5단계 완료는 6단계 대신 같은 단계의 새 세 문항을 시작한다 Error: expect(element).toHaveAttribute("value", "1") // element.getAttribute("value") === "1" Expected the element to have attribute: value="1" Received: value="0" ❯ tests/App.test.tsx:477:43 475| ); 476| expect(screen.getByRole("progressbar")).toHaveAttribute("max", "3"); 477| expect(screen.getByRole("progressbar")).toHaveAttribute("value", "1"… | ^ 478| await f.finish(); 479| }); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/8]⎯ FAIL tests/App.test.tsx > 세 문항 라운드에서 가능한 연속 정답 보너스 상한을 설명한다 TestingLibraryElementError: Unable to find an element with the text: 정답마다 단계 × 100점 + 연속 정답 보너스 (문항별 최대 20점, 라운드 합계 최대 30점). 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.2.3  
   소리에 집중하는 시간      지금까지의 소리 탐험          S     문제 풀이를 종료했어요.   작은 소리에 집중한 당신, 다음 탐험에서도 만나요. 

 

 응답  0 문항 · 정답  0 문항 

  
  0   총 점수  
 
  0 %   정답률 ·  0 / 0  
 
  0  회    최고 연속 정답  
      난이도   SNR   맞힌 문제       1 단계   SNR +6 dB   0  /  0     2 단계   SNR +3 dB   0  /  0     3 단계   SNR 0 dB   0  /  0     4 단계   SNR -3 dB   0  /  0     5 단계   SNR -6 dB   0  /  0      단계 × 100점 + 연속 정답 보너스 (라운드 내 최대   20 점) 

  처음 화면으로      ◖))   전체 음량    35 %     

 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:507:12 505| await f.user.click(screen.getByRole("button", { name: "문제 풀이 종료" })); 506| expect( 507| screen.getByText("정답마다 단계 × 100점 + 연속 정답 보너스 (문항별 최대 20점, 라운드 합계 최… | ^ 508| ).toBeInTheDocument(); 509| await f.finish(); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/8]⎯ FAIL tests/App.test.tsx > 현재 문항 진행률은 1/3→2/3→3/3이고 1단계 만점은 330점이다 TestingLibraryElementError: Unable to find an accessible element with the role "progressbar" and name "현재 문항 진행률" Here are the accessible roles: link: Name "본문으로 건너뛰기":  Name "환경음 출처·라이선스":  -------------------------------------------------- banner: Name "":  -------------------------------------------------- heading: Name "단어를 맞춰라": 

 Name "카페": 

 Name "소리 사이에 집중해 보세요.":  -------------------------------------------------- button: Name "문제 풀이 종료":  Name "듣는 중…":  Name "듣기 중단":  -------------------------------------------------- main: Name "":  -------------------------------------------------- region: Name "단어 찾기 게임":  -------------------------------------------------- progressbar: Name "전체 진행률":  -------------------------------------------------- complementary: Name "":  -------------------------------------------------- paragraph: Name "":  Name "": 

 Name "":  Name "":  Name "": 

 Name "": 

 Name "": 

 -------------------------------------------------- slider: Name "목표 음성 SNR":  Name "전체 음량":  -------------------------------------------------- status: Name "":  Name "":  Name "":  -------------------------------------------------- contentinfo: Name "":  -------------------------------------------------- Ignored nodes: comments, script, style  

  본문으로 건너뛰기          

 단어를 맞춰라 

  v0.2.3  
   문제 풀이 종료   소리에 집중하는 시간       나의 소리 탐험   문제   01   /   3       지금 듣고 있는 공간 

 

 카페 

 

 부드러운 웅성임과 잔의 울림 

        S                                         1 단계 ·  SNR +6 dB   소리에 익숙해지기 

   목표 음성 SNR    SNR +6 dB   음성이 배경보다 6 dB 커요.  범위 −12~+12 dB · 1 dB 간격. 양수는 음성이 더 크게, 0은 같게, 음수는 더 작게 들려요. 변경은 다음 재생·재청취부터 적용돼요. 

     ◖))   전체 음량    35 %     점수   0    연속 정답   0        두 번의 힌트   객관식     ♬   소리 사이에 집중해 보세요. 

 

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

  에스의 목소리를 듣고 있어요… 

    듣는 중…   듣기 중단   다시 들어도 점수는 줄어들지 않아요.        

 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:518:24 516| await f.user.click(screen.getByRole("button", { name: "게임 시작 · 소리 듣기… 517| for (let index = 0; index < 3; index++) { 518| const bar = screen.getByRole("progressbar", { name: "현재 문항 진행률" }); | ^ 519| expect(bar).toHaveAttribute("max", "3"); 520| expect(bar).toHaveAttribute("value", String(index + 1)); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/8]⎯ FAIL tests/App.test.tsx > 무응답 종료는 정답률 대신 미응답 상태와 단계별 미진행을 보여 준다 TestingLibraryElementError: Unable to find an accessible element with the role "heading" and name "문제 풀이를 중간에 종료했어요." Here are the accessible roles: link: Name "본문으로 건너뛰기":  Name "환경음 출처·라이선스":  -------------------------------------------------- banner: Name "":  -------------------------------------------------- heading: Name "단어를 맞춰라": 

 Name "문제 풀이를 종료했어요.":  -------------------------------------------------- main: Name "":  -------------------------------------------------- paragraph: Name "":  Name "": 

 Name "":  Name "": 

 Name "": 

 -------------------------------------------------- strong: Name "":  Name "":  Name "":  -------------------------------------------------- table: Name "난이도별 결과":  -------------------------------------------------- rowgroup: Name "":  Name "":  -------------------------------------------------- row: Name "난이도 SNR 맞힌 문제":  Name "1단계 SNR +6 dB 0 / 0":  Name "2단계 SNR +3 dB 0 / 0":  Name "3단계 SNR 0 dB 0 / 0":  Name "4단계 SNR -3 dB 0 / 0":  Name "5단계 SNR -6 dB 0 / 0":  -------------------------------------------------- columnheader: Name "난이도":  Name "SNR":  Name "맞힌 문제":  -------------------------------------------------- rowheader: Name "1단계":  Name "2단계":  Name "3단계":  Name "4단계":  Name "5단계":  -------------------------------------------------- cell: Name "SNR +6 dB":  Name "0 / 0":  Name "SNR +3 dB":  Name "0 / 0":  Name "SNR 0 dB":  Name "0 / 0":  Name "SNR -3 dB":  Name "0 / 0":  Name "SNR -6 dB":  Name "0 / 0":  -------------------------------------------------- button: Name "처음 화면으로":  -------------------------------------------------- slider: Name "전체 음량":  -------------------------------------------------- status: Name "":  -------------------------------------------------- contentinfo: Name "":  -------------------------------------------------- Ignored nodes: comments, script, style  

  본문으로 건너뛰기          

 단어를 맞춰라 

  v0.2.3  
   소리에 집중하는 시간      지금까지의 소리 탐험          S     문제 풀이를 종료했어요. 

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

 

 응답  0 문항 · 정답  0 문항 

  
  0   총 점수  
 
  0 %   정답률 ·  0 / 0  
 
  0  회    최고 연속 정답  
      난이도   SNR   맞힌 문제       1 단계   SNR +6 dB   0  /  0     2 단계   SNR +3 dB   0  /  0     3 단계   SNR 0 dB   0  /  0     4 단계   SNR -3 dB   0  /  0     5 단계   SNR -6 dB   0  /  0      단계 × 100점 + 연속 정답 보너스 (라운드 내 최대   20 점) 

  처음 화면으로      ◖))   전체 음량    35 %     

 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:538:17 536| await f.user.click(screen.getByRole("button", { name: "게임 시작 · 소리 듣기… 537| await f.user.click(screen.getByRole("button", { name: "문제 풀이 종료" })); 538| expect(screen.getByRole("heading", { name: "문제 풀이를 중간에 종료했어요." })).t… | ^ 539| expect(screen.getByText("—")).toBeInTheDocument(); 540| expect(screen.getByText("정답률 · 응답 없음")).toBeInTheDocument(); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/8]⎯ FAIL tests/neural-release.test.ts > README는 현재 리뷰 증거 경로와 정확한 라운드 보너스를 안내한다 AssertionError: expected '# 단어를 맞춰라 · Find-Word v0.2.3\n\n소음 속에…' to contain '.agent/evidence-v0.2.3/review-fixes/' - Expected + Received - .agent/evidence-v0.2.3/review-fixes/ + # 단어를 맞춰라 · Find-Word v0.2.3 + + 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. + + **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개 실제 현장 녹음 환경 중 하나를 지정하거나 라운드마다 섞어 세 곳을 방문합니다. + - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다. + - 시작 난이도 1~5를 고르고 정확히 **3문항**을 같은 단계에서 풉니다. 단계 기본값은 **SNR +6, +3, 0, −3, −6 dB**입니다. + - 목표 음성 SNR을 **−12~+12 dB, 1 dB 간격**으로 직접 조절할 수 있습니다. 재생 중 변경은 다음 재생·재청취와 남은 문제부터 적용합니다. + 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다. + - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 라운드 내 최대 20점의 보너스를 받습니다. + - 정상 결과에서 다음 단계의 새 3문항을 바로 시작합니다. 5단계에서는 같은 단계 재도전과 홈 복귀를 제공합니다. 수동 SNR은 다음 라운드에도 유지되고, 홈 복귀 시 선택한 시작 단계의 기본값으로 돌아갑니다. + - 언제든 **문제 풀이 종료**를 누르면 즉시 오디오를 취소하고 **응답 수·정답 수**를 보여 줍니다. 무응답은 오답으로 세지 않으며 결과에서 **처음 화면으로** 돌아갑니다. + - 결과의 SNR은 실제 마지막 청취값을 기록합니다. 점수·정답률·최고 연속 정답·단계별 결과를 확인합니다. + - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다. + + 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과 + 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은 + [WORDS.md](docs/WORDS.md)에 설명했습니다. + 청취 과제는 직접 목표 식별, 연습 암호와 진짜 암호 구별, 앞 단어를 잊고 다음 + 단어 기억, 첫째·마지막·가운데 선택, 반복 찾기, 정정 반영, 신호 조건 적용, + 오른쪽 표지 선택의 10가지입니다. 문장에 실제 보기의 오답도 포함됩니다. + + ## 검증과 정적 빌드 + + ```bash + npm run lint + npm test + python3 -m unittest discover -s tests/python -v + npm run build + npm audit + npm run preview + ``` + + `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기 + 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개와 현장음 8개가 manifest 해시 그대로 + 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다. + 런타임 MIT notice와 현장음 attribution/license 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/`를 사용하므로 소스 변경 후 먼저 빌드하세요. + 실제 음성 재생이 끝나기를 기다려 몇 분이 걸립니다. 로컬 4173 포트를 + 테스트 중에만 열고 종료합니다. 스크린샷과 109개 WAV의 query URL·해시·디코드 + 결과는 `test-results/`에 저장합니다. 인수 기록은 실행별 새 evidence 경로에 + 복사하여 `.agent/evidence-v0.1.1/`의 과거 결과를 보존합니다. + + ## 음성과 문서 + + 한국어 WAV 101개는 직접 쓴 문장을 Microsoft Edge online TTS의 + `ko-KR-HyunsuMultilingualNeural`로 v0.1.1에서 생성했으며 WAV 바이트는 그대로 보존했습니다. edge-tts **7.3.1**, + rate `-6%`, pitch `-4Hz`이며 FFmpeg로 **24kHz mono PCM16**으로 변환합니다. + 생성 시 네트워크가 필요하며 앱은 저장된 WAV만 재생합니다. 환경음은 공개 라이선스의 실제 현장 녹음 8개를 각 12초로 변환해 번들합니다. 생성 음성의 독점 권리를 주장하지 않습니다. + + intro와 목표 WAV는 모두 `/audio/파일명.wav?v=0.2.3`로 요청합니다. + `src/audio/assets.ts`의 자산 버전 한 곳을 사용하며, 같은 파일명으로 음성을 + 교체할 때 이 값을 갱신해 기존 7일 캐시와 구분합니다. 배경은 `/ambience/환경.wav?v=0.2.3`입니다. 제품 버전은 0.2.3입니다. + + 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는 디지털 상대값입니다. + + 현장음 원본은 `/tmp/findword-ambience-sources`에서만 보관하고 배포하지 않습니다. + `python3 scripts/ambience_audio.py`로 검증된 원본의 파생본과 manifest를 재현합니다. + PD·CC0·CC BY 4.0·CC BY-SA 4.0의 정확한 출처·저작자·변환·해시는 + [현장음 manifest](public/ambience/manifest.json), [배포 고지](public/AMBIENCE_NOTICES.md)에 있습니다. + 시장 파생본은 CC BY-SA 4.0을 유지합니다. 외부 링크는 출처 확인용이며 앱은 이를 요청하지 않습니다. + + - [변경 이력](CHANGELOG.md) + - [v0.2.3 구현·검증 결과](.agent/RESULT-v0.2.3.md) + - [설계와 상태 전이](docs/ARCHITECTURE.md) + - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) + - [v0.1.1 RED/GREEN 및 실제 검증 결과](.agent/RESULT-v0.1.1.md) + - [v0.1 역사 기록](.agent/RESULT.md) + + 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. + ❯ tests/neural-release.test.ts:72:20 70| it("README는 현재 리뷰 증거 경로와 정확한 라운드 보너스를 안내한다", () => { 71| const doc = readFileSync("README.md", "utf8"); 72| expect.soft(doc).toContain(".agent/evidence-v0.2.3/review-fixes/"); | ^ 73| expect.soft(doc).toContain("문항별 최대 20점, 라운드 합계 최대 30점"); 74| }); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/8]⎯ FAIL tests/neural-release.test.ts > README는 현재 리뷰 증거 경로와 정확한 라운드 보너스를 안내한다 AssertionError: expected '# 단어를 맞춰라 · Find-Word v0.2.3\n\n소음 속에…' to contain '문항별 최대 20점, 라운드 합계 최대 30점' - Expected + Received - 문항별 최대 20점, 라운드 합계 최대 30점 + # 단어를 맞춰라 · Find-Word v0.2.3 + + 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다. + + **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개 실제 현장 녹음 환경 중 하나를 지정하거나 라운드마다 섞어 세 곳을 방문합니다. + - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다. + - 시작 난이도 1~5를 고르고 정확히 **3문항**을 같은 단계에서 풉니다. 단계 기본값은 **SNR +6, +3, 0, −3, −6 dB**입니다. + - 목표 음성 SNR을 **−12~+12 dB, 1 dB 간격**으로 직접 조절할 수 있습니다. 재생 중 변경은 다음 재생·재청취와 남은 문제부터 적용합니다. + 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다. + - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 라운드 내 최대 20점의 보너스를 받습니다. + - 정상 결과에서 다음 단계의 새 3문항을 바로 시작합니다. 5단계에서는 같은 단계 재도전과 홈 복귀를 제공합니다. 수동 SNR은 다음 라운드에도 유지되고, 홈 복귀 시 선택한 시작 단계의 기본값으로 돌아갑니다. + - 언제든 **문제 풀이 종료**를 누르면 즉시 오디오를 취소하고 **응답 수·정답 수**를 보여 줍니다. 무응답은 오답으로 세지 않으며 결과에서 **처음 화면으로** 돌아갑니다. + - 결과의 SNR은 실제 마지막 청취값을 기록합니다. 점수·정답률·최고 연속 정답·단계별 결과를 확인합니다. + - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다. + + 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과 + 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은 + [WORDS.md](docs/WORDS.md)에 설명했습니다. + 청취 과제는 직접 목표 식별, 연습 암호와 진짜 암호 구별, 앞 단어를 잊고 다음 + 단어 기억, 첫째·마지막·가운데 선택, 반복 찾기, 정정 반영, 신호 조건 적용, + 오른쪽 표지 선택의 10가지입니다. 문장에 실제 보기의 오답도 포함됩니다. + + ## 검증과 정적 빌드 + + ```bash + npm run lint + npm test + python3 -m unittest discover -s tests/python -v + npm run build + npm audit + npm run preview + ``` + + `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기 + 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개와 현장음 8개가 manifest 해시 그대로 + 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다. + 런타임 MIT notice와 현장음 attribution/license 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/`를 사용하므로 소스 변경 후 먼저 빌드하세요. + 실제 음성 재생이 끝나기를 기다려 몇 분이 걸립니다. 로컬 4173 포트를 + 테스트 중에만 열고 종료합니다. 스크린샷과 109개 WAV의 query URL·해시·디코드 + 결과는 `test-results/`에 저장합니다. 인수 기록은 실행별 새 evidence 경로에 + 복사하여 `.agent/evidence-v0.1.1/`의 과거 결과를 보존합니다. + + ## 음성과 문서 + + 한국어 WAV 101개는 직접 쓴 문장을 Microsoft Edge online TTS의 + `ko-KR-HyunsuMultilingualNeural`로 v0.1.1에서 생성했으며 WAV 바이트는 그대로 보존했습니다. edge-tts **7.3.1**, + rate `-6%`, pitch `-4Hz`이며 FFmpeg로 **24kHz mono PCM16**으로 변환합니다. + 생성 시 네트워크가 필요하며 앱은 저장된 WAV만 재생합니다. 환경음은 공개 라이선스의 실제 현장 녹음 8개를 각 12초로 변환해 번들합니다. 생성 음성의 독점 권리를 주장하지 않습니다. + + intro와 목표 WAV는 모두 `/audio/파일명.wav?v=0.2.3`로 요청합니다. + `src/audio/assets.ts`의 자산 버전 한 곳을 사용하며, 같은 파일명으로 음성을 + 교체할 때 이 값을 갱신해 기존 7일 캐시와 구분합니다. 배경은 `/ambience/환경.wav?v=0.2.3`입니다. 제품 버전은 0.2.3입니다. + + 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는 디지털 상대값입니다. + + 현장음 원본은 `/tmp/findword-ambience-sources`에서만 보관하고 배포하지 않습니다. + `python3 scripts/ambience_audio.py`로 검증된 원본의 파생본과 manifest를 재현합니다. + PD·CC0·CC BY 4.0·CC BY-SA 4.0의 정확한 출처·저작자·변환·해시는 + [현장음 manifest](public/ambience/manifest.json), [배포 고지](public/AMBIENCE_NOTICES.md)에 있습니다. + 시장 파생본은 CC BY-SA 4.0을 유지합니다. 외부 링크는 출처 확인용이며 앱은 이를 요청하지 않습니다. + + - [변경 이력](CHANGELOG.md) + - [v0.2.3 구현·검증 결과](.agent/RESULT-v0.2.3.md) + - [설계와 상태 전이](docs/ARCHITECTURE.md) + - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md) + - [v0.1.1 RED/GREEN 및 실제 검증 결과](.agent/RESULT-v0.1.1.md) + - [v0.1 역사 기록](.agent/RESULT.md) + + 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다. + ❯ tests/neural-release.test.ts:73:20 71| const doc = readFileSync("README.md", "utf8"); 72| expect.soft(doc).toContain(".agent/evidence-v0.2.3/review-fixes/"); 73| expect.soft(doc).toContain("문항별 최대 20점, 라운드 합계 최대 30점"); | ^ 74| }); 75| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/8]⎯ Test Files 2 failed (2) Tests 7 failed | 20 passed (27) Start at 08:46:34 Duration 7.76s (tests 81%, environment 12%, setup 4%, transform 2%, import 1%) EXIT_CODE=1