From 2b901739cbe6225c76b817a577fc29064b7eabf4 Mon Sep 17 00:00:00 2001 From: "luke.yang" Date: Fri, 24 Jul 2026 13:58:55 +0900 Subject: [PATCH] fix(voice): move preset audio to Azure Blob (#392, P01-P05) --- .agents/context/process-status.json | 17 +++++++---- .users/context/process-status.md | 14 +++++++-- docs/requirements.md | 1 + docs/user-scenarios.md | 2 +- packages/shell/src-tauri/tauri.conf.json | 2 +- packages/shell/src/components/ChatArea.tsx | 9 +++--- .../shell/src/components/RefAudioSection.tsx | 5 ++-- .../shell/src/lib/__tests__/config.test.ts | 8 +++++ .../avatar/__tests__/cascade-renderer.test.ts | 4 +-- .../shell/src/lib/avatar/cascade-renderer.ts | 13 ++++---- packages/shell/src/lib/config.ts | 7 +++-- .../__tests__/naia-omni-ref-audio.test.ts | 2 +- .../lib/voice/__tests__/ref-audio-api.test.ts | 6 ++-- packages/shell/src/lib/voice/ref-audio-api.ts | 4 +-- packages/shell/src/lib/voice/types.ts | 8 +++-- src/test/azure-ref-audio-csp.contract.test.ts | 30 +++++++++++++++++++ 16 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 src/test/azure-ref-audio-csp.contract.test.ts diff --git a/.agents/context/process-status.json b/.agents/context/process-status.json index 960447d31..89f2dcc9a 100644 --- a/.agents/context/process-status.json +++ b/.agents/context/process-status.json @@ -15,7 +15,7 @@ "issue_doc": "docs/progress/ (F0-baseline, F0-contract, F{1,2,3}-baseline-contract, F0-graft) + 00-PHASES.md", "github_issue": null, "started": "2026-06-08T00:00:00Z", - "last_updated": "2026-07-17T00:00:00+09:00", + "last_updated": "2026-07-24T13:42:00+09:00", "status": "in_progress", "installer_crossplatform_377": "2026-07-17 #377 (S-INSTALL, FR-INSTALL.1~6 — IDD, 루크 승인 계획): 크로스플랫폼 설치 파일 — 매트릭스 SoT(`src-tauri/platform-matrix.json`)→단일 스크립트(stage-runtime.mjs)가 리소스 프로비저닝+`tauri.conf.generated.json` 생성. 배경 = 설치 파일 0건 + clean checkout 재현 불가(node.exe·MSVC 3종 = 선언만 있고 생성 주체 부재, windows 빌드가 stage-cascade-loader 누락한 채 딥머지 잔재로 우연 통과). 확정 = 3 OS(win/linux/mac) Node 22 번들·WSL 불요 불변(NFR-noWSL)·CI 3 OS 매트릭스(ubuntu 는 deb 설치+xvfb 기동 스모크 — 판정 = 마커 AND 실사용 node 경로가 resource_dir 하위 2조건 + mutation probe. R5 에서 'PATH node 제거' 폐기)·mac 완료선=arm64 CI 빌드 성공(미서명·arm64 전용 정직 표기). P01~P03 산출물 = user-scenarios.md S-INSTALL + requirements.md FR-INSTALL 섹션 + glossary 배포·설치 용어(2026-07-17, 적대 리뷰 R1 16건 + R2 12건 반영 — R1 BLOCKER createUpdaterArtifacts, R2 BLOCKER vosk dll 4종(libvosk+MinGW 3종) 이주·--config 머지 시맨틱스(base 훅=pnpm build 축소)·node arch 맵·ubuntu 스모크 판정 기준, R3 BLOCKER vosk 하드검사 순서모순 제거(build.rs 가 번들 전 생성=안전)·--config 리터럴 dangling 회피(stage-runtime 이 tauri build 직접 spawn)·agent 리소스 매핑 소유 결정, R4 BLOCKER 로 그 결정 반증·개정 — tauri-build copy_resources 는 dev/cargo check 포함 무조건 실행이라 gitignored 스테이징 산출물(agent 4종)은 base 금지 → 매트릭스 3 OS 공통 그룹→생성 conf 소유, 분담 규칙 = 커밋 자산=base/스테이징 산출·델타·조건부=생성 conf. R4 MINOR 6건 중 5건 반영(NAIA_MINIMAL 스모크 전제·nsis-hooks 귀속 교정·S-INSTALL 검증열 FR-INSTALL.6 추가·glossary 6용어·매트릭스 초기값 출처), 잔여 1건 = sdlc_gates 구조화 필드에 #377 게이트 병기 → 2026-07-17 해소(각 P01~P05 의 tracks.installer_crossplatform_377)). 단계별 게이트: 원 규칙 = 각 phase 종료 시 적대 리뷰 2연속 클린(루크 명시). **P0 는 2026-07-17 루크 승인으로 '구현이 검증' 기준으로 조정하고 종료** — R5~R8 이 각각 직전 라운드의 개정에서 MAJOR 를 찾는 진자운동에 들어갔고(발견은 전부 실측 근거로 진짜였으나 성격이 '명세를 코드 수준으로 정밀화하다 생기는 실수'로 이동), 잔여 계열(로그 정규식·줄 개수·glob 형태)은 P1 구현 + vitest golden + 실제 CI 가 더 싸고 확실하게 잡는다는 판단. P1~P5 게이트는 원 규칙 유지 여부를 각 phase 종료 시 재확인. 리뷰 이력 — R5(3렌즈 병렬: tauri 실소스 대조 · 설계정합/검증축 · 실현가능성/정직성) NOT CLEAN 7건(MAJOR 2 + MINOR 5) 반영 완료. R5 MAJOR ① ubuntu 스모크의 'PATH 에서 node 제거' 폐기 — unix 폴백이 PATH 무관하게 홈 아래 .nvm/versions/node 를 직접 스캔(실측 lib.rs)하므로 번들 node 를 증명 못 하고, 현 러너가 통과하는 건 이미지가 우연히 비어서일 뿐(판정력의 외부 위탁) → 판정을 '마커 AND 실사용 node 경로가 resource_dir 하위' 2조건으로 교체 + FR-INSTALL.3 에 node 경로 로그 1줄 신설 + mutation probe. ② flatpak targets 이관 철회 — 매트릭스 linux 행과 같은 사실이 수기 파일 2곳이 되고 검증축 밖(repo 내 소비자 0 실측) → linux.json 과 동일한 외부 소비자 확인 규율 적용 후 무변경 원칙. MINOR 5 = icon 배열 = RFC 7386 통째 대체(mac 행은 전체 배열 emit, 겹침금지의 명시적 예외) · vosk 순서 근거를 'links 키'로 교정하고 links 실존을 게이트로 승격 · 테스트 경로 확정(scripts/__tests__/ — src-tauri/** 는 vite.config test.exclude 로 영구 미수집, 프로브 실측) · 3 OS 모두 OS 기본 tar 추출로 문구 정리 · mac = arm64 전용 명시(macos-latest=arm64 러너, Intel 몫 없음). 두 리뷰어가 충돌한 nvm 마스킹 쟁점은 lib.rs 폴백 체인 실측으로 판정(관측을 '떴는가'→'무엇으로 떴는가'로 전환). R6(2렌즈: R5 개정분 회귀 적대 · 구현 착수 가능성) NOT CLEAN 5건(MAJOR 3 + MINOR 2) 반영 완료. R6 MAJOR ① R5 가 폐기한 'PATH node 제거'가 같은 커밋이 신규 작성한 process-status 트랙 필드 4곳에 잔존(본문과 반대 = 헌장/Mandatory Read 가 폐기된 게이트를 지시) → 4곳 전부 2조건 판정으로 치환. ② FR-INSTALL.3 이 번들 분기 실행 증명을 FR-INSTALL.4 에 위임했으나 .4 는 '기동+핸드셰이크 한정'이라 수임 안 함 — 게다가 win 실측 머신은 정의상 빌드 머신이라 시스템 node 가 있어 PATH 폴백으로 green 이 됨(node.exe 동봉이 #377 출발점인데 win 번들 분기만 검증축 밖) → .4 에 .5 와 동형 2조건 상속. ③ FR-INSTALL.6 의 '기대 산출물'에 지시 대상 부재 — 매트릭스에 산출물 필드가 없고 minBytes 값도 미지정(항상 통과 가능) → 매트릭스에 artifacts[{glob,minBytes}] 신설(glob 이유 = 파일명 버전 출처가 src-tauri/Cargo.toml 이라 리터럴은 두 출처를 얽음), minBytes = P3/P4 첫 실측의 50% 로 핀하되 그전엔 null + 스크립트가 null 이면 명확 에러 중단. MINOR 2 = 클린 카운트 재개 지점이 한 파일 안에서 R5/R6 로 갈림(카운터가 두 값) → 통일 · node 로그가 부팅당 2줄(agent+BGM 둘 다 무조건 스폰)인데 판정이 1줄 전제 → '모든 줄이 resource_dir 하위' 로 확정(BGM 만 새어도 red) + env 오버라이드 포함 최종값·폴백 클로저 바깥 기록 명시. R7(2렌즈: R6 개정분 회귀 · fresh 전체 통독) NOT CLEAN 4건(MAJOR 3 + MINOR 1) 반영 완료. R7 MAJOR ① 판정 근거 naia.log 가 append 전용 머신단위 누적 파일(truncate 코드 0건 실측)인데 '모든 줄' 판정에 세션 스코프 부재 → 정상 설치본도 영구 red(빌드 머신엔 dev/e2e 가 남긴 시스템 node 줄 존재) + 같은 job 의 mutation probe 가 먼저 돌면 순서에 판정이 좌우 → '마지막 [Naia] === Session started === 이후 줄만'(lib.rs:4442, spawn 4506/4529 보다 선행 실측). 두 렌즈가 독립 발견 = 강신호. ② mac 'macos/*.app' 는 파일 아닌 번들 디렉토리라 크기가 아이노드(수십B)로 잡히고 minBytes 초기값도 그 50% 로 굳어 빈 앱도 통과(mac 은 실기기 실측 부재라 이 축이 유일) → glob = macos/*.app/Contents/MacOS/* 로 교체 + '모든 glob 은 단일 파일' 규칙. ③ R6 MAJOR②(win 2조건)가 user-scenarios.md·process-status 2곳에 미전파 — R6 자신이 지적한 실패 양식의 재발이자 P02 deliverable 이 가리키는 파일이라 다음 세션이 구멍을 재개방할 경로 → 전파. MINOR = 신설 artifacts 필드가 glossary·user-scenarios 의 매트릭스 정의 열거에 누락 → 추가. R8(2렌즈: R7 개정분 회귀 · 게이트 반증가능성 전수) NOT CLEAN 4건(MAJOR 2 + MINOR 2) 반영 완료. (여기서 P0 종료 — 위 게이트 조정 참조) R8 MAJOR ① 판정 술어가 공허참 — 파일 줄은 log_to_file 이 유닉스초를 붙여 '[unix_secs] [Naia] node = ...' 인데(실측: 앵커 매칭 0건 vs 포함 67건) 명세가 '[Naia] node = 로 시작하는' 이라 앵커로 읽으면 매치 0 → 전칭이 공허참 → ② 판정력 0, mutation probe 도 red 안 됨. 세션 구분자도 같은 오독 → 술어를 포함 매칭/정규식으로 확정 + FR-INSTALL.3 에 파일 줄 형식 명시. R7 의 '정상 설치본도 영구 red' 논거 자체가 이 오독 위에 있었음(앵커 독법에선 영구 green) — 세션 스코프 처방은 유지(술어 교정 시 누적 문제가 실제로 살아남). ② 전칭은 0줄이면 참이라 로그 접두 드리프트·폴백 클로저 안쪽 기록으로 줄이 사라지면 green — Linux 는 probe 가 잡지만 Windows 엔 probe 가 없어 유일 축이 무력 → '최소 2줄 AND 전부 하위' 개수 하한을 양쪽 대칭 추가(공허참 원리적 차단. '정확히' 는 자체 교정 — restart_agent 재호출 시 3번째 줄이 나와 상한은 거짓 red 유발, 실측). MINOR 2 = S-INSTALL Linux 절에만 세션 스코프 누락(비대칭) → 병기 · FR-INSTALL.6 부정 테스트가 로직 재구현 시 영원히 green(FR-INSTALL.2 엔 있는 순수함수 제약이 .6 엔 부재, check-build-contract 의 import.meta.url 자기고정 관례가 유인) → verifyArtifacts({bundleDir,artifacts}) 주입 가능 순수함수 분리 + import 구동·재구현 금지 명시. 두 렌즈가 공허참을 독립 발견 = 강신호(R7 의 누적로그도 동일 패턴). 진행(2026-07-17): P0 종료(루크 승인) → P1 착수. P1·P2·P5 는 리눅스 머신, P3 Windows 실측만 Windows PC 필요. 트랙 = alpha-adk .agents/progress/naia-shell-crossplatform-installer-2026-07-17.md.", "config_sot_clobber_fix": "2026-07-16 FR-CONFIG-SOT.5 (시연장 실측 클로버 — persona 21,187자→5,953자 4회 재발): App.tsx AdkSetup 분기가 하이드레이션 없이 configHydratedRef=true 선마킹 → mount-time syncConfigToFile 이 스테일 캐시를 config.json 에 되씀. 픽스 = 게이트 닫힘 유지(설정 완료 → effect 재실행 → 하이드레이션 후 개방). 검증 = 신규 e2e/config-sot-boot.spec.ts(Playwright 실 UI 3계약: 하이드레이션 파일우선·부팅 후 무클로버·읽기지연 경쟁) 3/3 + config vitest 63/63 + tsc 0. 운영 수칙(코드 밖) = config.json 외부 수정은 셸 종료 후(떠 있는 셸 캐시가 되씀 — unload flush 가 새 부팅 읽기보다 선행하는 자기영속). 잔여(후속) = 에이전트 재스폰 시 패널 스킬 재등록 누락(자격증명만 재푸시), ollamaNumCtx 셸→에이전트 배선. 트랙 = alpha-adk .agents/progress/naia-demo-knowledge-persona-clobber-2026-07-16.md.", @@ -38,27 +38,32 @@ "_note": "각 게이트의 최상위 status/deliverable/ref = 기본 트랙(current_work.issue = naia-shell-transplant, F0~F3) 소유. 병행 트랙은 같은 게이트의 tracks 에만 기재한다(한 사실 두 곳 금지 — 기본 트랙 상태를 tracks 에 복사하지 않는다).", "P01": { "status": "done", "name": "user_scenarios", "deliverable": "docs/user-scenarios.md (UC1-14, S01~S71)", "ref": "완전성 13R 3연속 NONE", "tracks": { - "installer_crossplatform_377": { "status": "done", "deliverable": "docs/user-scenarios.md — 셸 feature 시나리오 표의 S-INSTALL 행 (#377, 2026-07-17)", "ref": "DONE 2026-07-17 — 적대 리뷰 R1~R8 (8라운드, 실측 근거 결함 전부 반영). P0 종료 조건이 '리뷰 2연속 클린' 에서 **'구현이 검증'** 으로 조정됨(루크 승인 2026-07-17, AI 임의 변경 아님): R5~R8 이 각각 직전 라운드의 개정에서 MAJOR 를 찾는 진자운동에 들어갔고 잔여 결함 계열(로그 정규식·줄 개수·glob 형태)은 P1 구현 + vitest golden + 실제 CI 가 더 싸고 확실하게 잡는다는 판단. 상세 = current_work.installer_crossplatform_377" } + "installer_crossplatform_377": { "status": "done", "deliverable": "docs/user-scenarios.md — 셸 feature 시나리오 표의 S-INSTALL 행 (#377, 2026-07-17)", "ref": "DONE 2026-07-17 — 적대 리뷰 R1~R8 (8라운드, 실측 근거 결함 전부 반영). P0 종료 조건이 '리뷰 2연속 클린' 에서 **'구현이 검증'** 으로 조정됨(루크 승인 2026-07-17, AI 임의 변경 아님): R5~R8 이 각각 직전 라운드의 개정에서 MAJOR 를 찾는 진자운동에 들어갔고 잔여 결함 계열(로그 정규식·줄 개수·glob 형태)은 P1 구현 + vitest golden + 실제 CI 가 더 싸고 확실하게 잡는다는 판단. 상세 = current_work.installer_crossplatform_377" }, + "azure_ref_audio_392": { "status": "done", "deliverable": "docs/user-scenarios.md S66 GCS 비의존·Azure Blob 전환 시나리오", "ref": "GitHub #392, 2026-07-24" } } }, "P02": { "status": "done", "name": "test_scenarios", "deliverable": "docs/user-scenarios.md Test Coverage Map + src/test/*.contract.test.ts (계약 67/67)", "ref": "13R", "tracks": { - "installer_crossplatform_377": { "status": "done", "deliverable": "docs/user-scenarios.md — S-INSTALL 행의 검증 열 (scripts/__tests__/platform-matrix.test.ts 단위 + check-build-contract 계약 + Windows 설치 실측(2조건: 핸드셰이크 AND node 줄 최소 2줄 AND 전부 $INSTDIR 하위) + ubuntu deb 설치·xvfb 기동 스모크(마커 AND node 줄 최소 2줄 AND 전부 resource_dir 하위 + mutation probe) — 양쪽 모두 판정 범위 = 마지막 '=== Session started ===' 포함 줄 이후(naia.log 누적), 술어 = 앵커 아닌 포함 매칭(파일 줄 = '[unix_secs] [Naia] ...') + 산출물 검증 스크립트/부정 케이스)", "ref": "DONE 2026-07-17 — P01 과 동일(게이트 기준 조정, 루크 승인)" } + "installer_crossplatform_377": { "status": "done", "deliverable": "docs/user-scenarios.md — S-INSTALL 행의 검증 열 (scripts/__tests__/platform-matrix.test.ts 단위 + check-build-contract 계약 + Windows 설치 실측(2조건: 핸드셰이크 AND node 줄 최소 2줄 AND 전부 $INSTDIR 하위) + ubuntu deb 설치·xvfb 기동 스모크(마커 AND node 줄 최소 2줄 AND 전부 resource_dir 하위 + mutation probe) — 양쪽 모두 판정 범위 = 마지막 '=== Session started ===' 포함 줄 이후(naia.log 누적), 술어 = 앵커 아닌 포함 매칭(파일 줄 = '[unix_secs] [Naia] ...') + 산출물 검증 스크립트/부정 케이스)", "ref": "DONE 2026-07-17 — P01 과 동일(게이트 기준 조정, 루크 승인)" }, + "azure_ref_audio_392": { "status": "done", "deliverable": "config.test.ts + azure-ref-audio-csp.contract.test.ts + cascade-renderer.test.ts + Azure Blob HTTP/WAV probe", "ref": "2026-07-24" } } }, "P03": { "status": "done", "name": "requirements", "deliverable": "docs/requirements.md (FR-F0~F3, NFR)", "ref": "8R", "tracks": { - "installer_crossplatform_377": { "status": "done", "deliverable": "docs/requirements.md — FR-INSTALL.1~6 + NFR-noWSL(불변)·NFR-honesty", "ref": "DONE 2026-07-17 — P01 과 동일(게이트 기준 조정, 루크 승인). R4 개정 반영: agent 리소스 4종 = 매트릭스 3 OS 공통 그룹→생성 conf 소유(base 금지 — tauri-build copy_resources 가 dev/cargo check 포함 무조건 실행)" } + "installer_crossplatform_377": { "status": "done", "deliverable": "docs/requirements.md — FR-INSTALL.1~6 + NFR-noWSL(불변)·NFR-honesty", "ref": "DONE 2026-07-17 — P01 과 동일(게이트 기준 조정, 루크 승인). R4 개정 반영: agent 리소스 4종 = 매트릭스 3 OS 공통 그룹→생성 conf 소유(base 금지 — tauri-build copy_resources 가 dev/cargo check 포함 무조건 실행)" }, + "azure_ref_audio_392": { "status": "done", "deliverable": "docs/requirements.md FR-VOICE.8", "ref": "GitHub #392, 2026-07-24" } } }, "P04": { "status": "in_progress", "name": "integration_test", "deliverable": "src/test/integration-reafference.test.ts (인지흐름 관통+negative+contamination) + scripts/builds/f0-graft-smoke.sh (Old-Baseline drift-gate harness)", "ref": "통합 67/67 통과; 라이브 trace(루크 머신 graft) 대기", "tracks": { - "installer_crossplatform_377": { "status": "done", "deliverable": "platform-matrix.test.ts + verify-artifacts.mjs + verify-installed-smoke.mjs + build-installers.yml 3 OS", "ref": "DONE 2026-07-19 — PR #379 merge 7ba15dc9. Exact 검증 head de0e9e25, installer CI 29658918546 Windows/Linux/macOS GREEN, Windows NSIS 실제 설치·실행·제거 후 Steam depot 독립 기동, Linux deb 설치·mutation/tamper probe, 적대 리뷰 2회 연속 CLEAN" } + "installer_crossplatform_377": { "status": "done", "deliverable": "platform-matrix.test.ts + verify-artifacts.mjs + verify-installed-smoke.mjs + build-installers.yml 3 OS", "ref": "DONE 2026-07-19 — PR #379 merge 7ba15dc9. Exact 검증 head de0e9e25, installer CI 29658918546 Windows/Linux/macOS GREEN, Windows NSIS 실제 설치·실행·제거 후 Steam depot 독립 기동, Linux deb 설치·mutation/tamper probe, 적대 리뷰 2회 연속 CLEAN" }, + "azure_ref_audio_392": { "status": "done", "deliverable": "azure-ref-audio-csp.contract.test.ts + focused shell vitest + root build + shell production build + Azure Blob HTTP 200/RIFF", "ref": "2026-07-24; full root test has 4 unrelated pre-existing paired-agent pin failures" } } }, "P05": { "status": "pending", "name": "requirements_complete", "deliverable": null, "ref": "라이브 trace 등가 확인 + 후속 슬라이스 후", "tracks": { - "installer_crossplatform_377": { "status": "done", "deliverable": "README + platform matrix/manifest + artifact SHA report + GitHub #377 final report", "ref": "DONE 2026-07-19 — FR-INSTALL.1~6, FR-STEAM.1~3, NFR-noWSL 충족. #377 closed; Steamworks 운영 단계는 #314에 이관" } + "installer_crossplatform_377": { "status": "done", "deliverable": "README + platform matrix/manifest + artifact SHA report + GitHub #377 final report", "ref": "DONE 2026-07-19 — FR-INSTALL.1~6, FR-STEAM.1~3, NFR-noWSL 충족. #377 closed; Steamworks 운영 단계는 #314에 이관" }, + "azure_ref_audio_392": { "status": "done", "deliverable": "docs/requirements.md FR-VOICE.8 status Done; implemented and verified", "ref": "GitHub #392, 2026-07-24" } } } }, diff --git a/.users/context/process-status.md b/.users/context/process-status.md index b31728901..8506fb8e4 100644 --- a/.users/context/process-status.md +++ b/.users/context/process-status.md @@ -1,4 +1,4 @@ - + # 프로세스 현황 (Process Status) SoT @@ -34,7 +34,7 @@ **시작**: 2026-06-08 -**마지막 업데이트**: 2026-07-17 00:00 KST +**마지막 업데이트**: 2026-07-24 13:42 KST **상태**: 진행 중 (in_progress) @@ -173,6 +173,16 @@ | **P04** | 완료 | `platform-matrix.test.ts` + `verify-artifacts.mjs` + `verify-installed-smoke.mjs` + 3 OS `build-installers.yml` | **완료 2026-07-19** — PR #379 merge `7ba15dc9`; exact 검증 head `de0e9e25`; installer CI 29658918546 Windows/Linux/macOS GREEN; Windows NSIS 실제 설치·실행·제거 후 Steam depot 독립 기동; Linux deb 설치·mutation/tamper probe; 적대 리뷰 2회 연속 CLEAN | | **P05** | 완료 | README + platform matrix/manifest + artifact SHA report + GitHub #377 final report | **완료 2026-07-19** — FR-INSTALL.1~6, FR-STEAM.1~3, NFR-noWSL 충족. #377 종료; Steamworks 운영 단계는 #314에 이관 | +### 병행 트랙: #392 Azure 참조 음성 전환 (azure_ref_audio_392) + +| 게이트 | 상태 | 산출물 (Deliverable) | 참조 | +|--------|------|---------------------|------| +| **P01** | 완료 | `docs/user-scenarios.md` S66 GCS 비의존·Azure Blob 전환 시나리오 | GitHub #392, 2026-07-24 | +| **P02** | 완료 | `config.test.ts` + `azure-ref-audio-csp.contract.test.ts` + `cascade-renderer.test.ts` + Azure Blob HTTP/WAV probe | 2026-07-24 | +| **P03** | 완료 | `docs/requirements.md` FR-VOICE.8 | GitHub #392, 2026-07-24 | +| **P04** | 완료 | 패키지 CSP 계약 테스트 + 집중 셸 vitest + 루트 build + 셸 production build + Azure Blob HTTP 200/RIFF | 전체 루트 테스트의 paired-agent pin 실패 4건은 변경 전부터 존재하는 별도 이슈 | +| **P05** | 완료 | `docs/requirements.md` FR-VOICE.8 상태 Done, 구현·검증 | GitHub #392, 2026-07-24 | + --- ## 리소스 레지스트리 (Resource Registry) diff --git a/docs/requirements.md b/docs/requirements.md index a3f3984ae..a912009dc 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -140,6 +140,7 @@ localStorage `naia-config` 는 파일에서 하이드레이트되는 **순수 | **FR-VRAM.5** | **검증-티어 전용 프로파일 + 자동설정** (FR-VRAM.4 개정). ① `hidden` 티어는 피커 비노출 **+ `selectVramTier`(auto) 제외** — 미검증 티어 자동선택이 NVA 아바타를 몰래 심던 사고 차단. 현재 검증 티어 = `local-llm-voice-16g`(3080 Ti 16G 실측). **트레이드오프(명시)**: <16GB VRAM 은 auto 로 로컬 프로파일 미수령(클라우드) — 프리릴리스 허용, 검증 시 hidden 해제로 편입. ② 프로파일 선택 = **자동설정**(stageLocalSlots): 두뇌(로컬 LLM capability → provider=ollama + compact 기본 `DNA3.0-4B`), 음성(tts capability → naia-local-voice + host), 아바타(avatar capability 없으면 → VRM 복원). ③ 저장값·구 id 하위호환(normalizeTierId) 유지 | S-VRAM-AUTO | `vram-tiers.test.ts`(hidden auto 제외·데이터 계약) · `SettingsTab.test.tsx`(프로파일 클릭 자동설정) · `slots-manifest.contract.test.ts` · e2e `settings-slots.spec.ts`(16G 프로파일 → 두뇌·음성·호스트·아바타 전환) | | **FR-VOICE.6** | **로컬 음성 정본 호스트 = :8910 façade**. `DEFAULT_LOCAL_VOICE_HOST` = `http://localhost:8910`(OpenAI 표면 `/v1/audio/speech` 서빙 cascade façade). 구 `:22600`(raw `/tts`)은 이 표면이 없어 기본이 될 수 없다 — placeholder·힌트·주석 모두 :8910 으로 정정. 프로파일 자동설정은 **빈 값·localhost/127.0.0.1 변형만** 이 기본으로 교체(원격 GPU Tailscale 호스트는 보존 — 문서화된 원격 cascade 워크플로 파괴 금지) | S-VOICE-AUTO | `synthesize.test.ts`(:8910 기본 호스트) · `SettingsTab` 자동설정(원격 호스트 보존) | | **FR-VOICE.7** | **프리셋 음색 façade 팔레트 id 전달**. naia-local-voice 의 `voice` = 사용자 음성 참조(`voiceRefUrl`)의 basename(쿼리/프래그먼트 제거 후 `.wav` 파일명 → façade `/ref/voices` 팔레트 id). 팔레트 밖 값(녹음/업로드·비-wav)은 `naia-default` 폴백(서버가 모르는 id 를 200+랜덤 음색으로 받으므로). **vllm provider 는 제외** — 범용 OpenAI 서버라 팔레트 id 를 모름, `"default"` 유지. 두 합성 경로(파이프라인·Live)가 단일 `resolveTtsVoiceId(config)` 공유 → 분기 드리프트 방지 | S-VOICE-PRESET | `ChatArea` 음색 해석(프리셋→id·쿼리스트링·vllm 분리) | +| **FR-VOICE.8** | **Done — 기본 CC0 참조 음성의 GCP 의존 제거**. `DEFAULT_VOICE_REF_URL`은 공개 전용 Azure Storage 계정 `stnaiapub83b29893`의 `ref-audio/cc0/cc0-ko-female-01.wav`를 사용한다. Tauri `media-src`는 해당 Azure Blob origin을 허용하고 이전 `storage.googleapis.com` 허용은 제거한다. 사용자 업로드는 기존 private gateway 경로를 유지한다 | S66 | `config.test.ts`(정확한 Azure URL·GCS 비의존) · `azure-ref-audio-csp.contract.test.ts`(패키지 CSP의 Azure 허용·GCS 거부) · `cascade-renderer.test.ts`(Azure URL basename 변환) · 실제 Blob HTTP 200/Content-Length/RIFF probe | | **FR-ECHO.1** | **자기발화(에코) 방어 2단**. ① 재생 중 마이크(STT 세션) 정지 + 종료 0.8초 후 재개 — 재개 대기 타이머는 다음 문장 재생 시작 시 취소(문장 간 큐 드레인으로 마이크가 발화 중 재개통되던 누수 차단). ② 최근 TTS 문장과 유사도(문자 bigram Dice ≥ 0.6 또는 ≥8자 부분일치)면 STT 결과 스킵 — **짧은 정상 답변("좋아/네/그래")은 절대 스킵 금지**(bigram 폴백 정확일치, 부분일치 길이-게이트) | S-ECHO | `echo-text-filter.test.ts`(동일·부분·짧은답변·정상질문 8건) | > NFR: FR-VRAM.5 <16GB 트레이드오프는 프리릴리스 한정. FR-ECHO.1 은 web-speech 지연배달 특성 대응(1차 마이크정지가 주 방어, 2차 텍스트필터는 누수 폴백). ⚠️ **후속(비블로킹)**: 부팅 병합(mergeBootConfig)이 localStorage-only 키(naiaKey 시크릿·discord 커서·세션 플래그)를 보존하지 않는 회귀 — 데모 실사용 정상 확인이나 재로그인/중복응답 가능성, 부팅 흐름 변경은 별도 안전작업으로 분리. diff --git a/docs/user-scenarios.md b/docs/user-scenarios.md index 7557cb721..77b8ded66 100644 --- a/docs/user-scenarios.md +++ b/docs/user-scenarios.md @@ -122,7 +122,7 @@ UC15 제품 수용 확장(#84): | S63 | 워크스페이스 **GitHub Issues 패널**(IssuesPanel, `gh issue list`) | UC5/UC7 | workspace group·skill(github) | 측정 (완전성R2) | | S64 | **AppBar 브라우저 바로가기 관리**(URL shortcut 추가/삭제/재정렬/아이콘) | UC6 | browser group·UI | 측정 (완전성R2) | | S65 | **botmadang 커뮤니티 연동**(botmadang.org: register·post_article·comment) — 기본 스킬·skill.json 매니페스트 | UC10/UC5 | skill·channels | **rejected(루크 결정 2026-06-09: 이식 제외)** — voice-server류, 카탈로그엔 rejected로 명시 | -| S66 | **참조 오디오 / voice clone**(RefAudioSection: 미리듣기·녹음/업로드·preset·삭제, `/v1/ref-audio`, mid-session 반영) = naia 음색 | UC2 | voice·ExpressionPort(timbre) | 측정 (완전성R4) | +| S66 | **참조 오디오 / voice clone**(RefAudioSection: 미리듣기·녹음/업로드·preset·삭제, `/v1/ref-audio`, mid-session 반영) = naia 음색. 기본 CC0 프리셋은 공개 Azure Blob에서 로드하며 GCS에 의존하지 않는다 | UC2 | voice·ExpressionPort(timbre) | `config.test.ts`(기본 URL) · `azure-ref-audio-csp.contract.test.ts`(정확한 Azure origin만 허용) · `cascade-renderer.test.ts`(외부 URL→팔레트 id) · Azure Blob HTTP/WAV probe (완전성R4) | | S67 | **Naia Lab 설정 동기화**(lab-sync: pull/push + 충돌 선택 다이얼로그, 로컬변경 자동 push) — 계정/비용과 별개 | UC12 | control-plane(settings sync) | 측정 (완전성R5) | | S70 | 채팅 **절대경로 파일 deeplink**(chat-file-deeplink 버튼 → workspace 패널 openFile + 전환) | UC1/UC7 | ChatPort·workspace | 측정 (완전성R9) | | **S71 번들 default-skills 컬렉션 (~60+, OpenClaw 출처)** = command-group (preload + SkillsTab 노출 + tool-bridge) | UC5 | skill·gateway | 측정 (완전성R10, **개별 스킬 per-skill 검증**) | diff --git a/packages/shell/src-tauri/tauri.conf.json b/packages/shell/src-tauri/tauri.conf.json index a226946de..59475ad0d 100644 --- a/packages/shell/src-tauri/tauri.conf.json +++ b/packages/shell/src-tauri/tauri.conf.json @@ -40,7 +40,7 @@ "requireLiteralLeadingDot": true } }, - "csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' http: https: ws: wss: http://localhost:18789 ws://localhost:18789 http://localhost:18791 http://127.0.0.1:18792 http://localhost:18792 https://naia.nextain.io wss://naia.nextain.io https://www.naia.land wss://www.naia.land https://api.nextain.io wss://api.nextain.io https://generativelanguage.googleapis.com wss://generativelanguage.googleapis.com https://speech.googleapis.com https://api.x.ai https://api.anthropic.com https://api.openai.com wss://api.openai.com http://asset.localhost https://asset.localhost tauri://localhost blob: data:; img-src 'self' asset: http://asset.localhost https://asset.localhost tauri://localhost blob: data: https://i.ytimg.com https://*.ytimg.com; media-src 'self' asset: http://asset.localhost https://asset.localhost https://storage.googleapis.com blob: data:; font-src 'self' data:; worker-src 'self' blob:; frame-src http://asset.localhost https://asset.localhost https://www.youtube.com https://www.youtube-nocookie.com" + "csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' http: https: ws: wss: http://localhost:18789 ws://localhost:18789 http://localhost:18791 http://127.0.0.1:18792 http://localhost:18792 https://naia.nextain.io wss://naia.nextain.io https://www.naia.land wss://www.naia.land https://api.nextain.io wss://api.nextain.io https://generativelanguage.googleapis.com wss://generativelanguage.googleapis.com https://speech.googleapis.com https://api.x.ai https://api.anthropic.com https://api.openai.com wss://api.openai.com http://asset.localhost https://asset.localhost tauri://localhost blob: data:; img-src 'self' asset: http://asset.localhost https://asset.localhost tauri://localhost blob: data: https://i.ytimg.com https://*.ytimg.com; media-src 'self' asset: http://asset.localhost https://asset.localhost https://stnaiapub83b29893.blob.core.windows.net blob: data:; font-src 'self' data:; worker-src 'self' blob:; frame-src http://asset.localhost https://asset.localhost https://www.youtube.com https://www.youtube-nocookie.com" } }, "bundle": { diff --git a/packages/shell/src/components/ChatArea.tsx b/packages/shell/src/components/ChatArea.tsx index 5d1793cf3..c029f7ac5 100644 --- a/packages/shell/src/components/ChatArea.tsx +++ b/packages/shell/src/components/ChatArea.tsx @@ -291,7 +291,7 @@ const mdComponents: Components = { * 비팔레트 형식(녹음/업로드 data·로컬경로)은 façade 가 400 fail-closed 라 기본 음색 폴백. */ function naiaLocalVoiceId(voiceRefUrl?: string): string { if (!voiceRefUrl) return "naia-default"; - // 쿼리/프래그먼트 제거 후 basename — GCS 서명 URL(...wav?X-Goog-...) 이나 프리셋 + // 쿼리/프래그먼트 제거 후 basename — 서명된 객체 URL이나 프리셋 // sampleUrl 의 쿼리스트링 때문에 정규식이 빗나가 프리셋이 무시되던 것 방지(2026-07-15 리뷰). const noQuery = voiceRefUrl.split(/[?#]/)[0]; const base = noQuery.split(/[/\\]/).pop()?.trim() ?? ""; @@ -2829,12 +2829,13 @@ export function ChatArea({ // uses (no unreliable GET /v1/ref-audio status round-trip). // Sent for BOTH cloud gateway AND Naia Local (own container, direct // mode): both run the same omni cascade and accept ref_audio_url in - // session.update. For Naia Local there is NO gateway GCS injection in + // session.update. For Naia Local there is NO gateway-side URL injection in // the path, so the client sending the URL is the ONLY way the cloned // voice reaches the container — gating this behind gateway mode left // local-container voice with a random per-turn voice. The sample_url - // is a public storage.googleapis.com URL (no secret), so it is safe - // on the direct socket. Empty for uploads (injected server-side). + // is a public Azure Blob URL (no secret), so it is safe on the direct + // socket. The cloud gateway independently injects any stored private + // upload; this pre-existing fallback behavior is unchanged here. // Naia Local recorded/uploaded voice is kept as a base64 WAV locally // (no gateway upload → no credit charge) and sent embedded. It wins // over a preset URL when present. diff --git a/packages/shell/src/components/RefAudioSection.tsx b/packages/shell/src/components/RefAudioSection.tsx index 9649237dd..1627e20b1 100644 --- a/packages/shell/src/components/RefAudioSection.tsx +++ b/packages/shell/src/components/RefAudioSection.tsx @@ -48,7 +48,8 @@ const MAX_DURATION_S = 30; * Persist the active voice-reference preset URL into AppConfig so the realtime * voice session (ChatArea) sends it directly as `ref_audio_url` — the * deterministic source the web demo uses. Pass null on upload/remove so an - * uploaded voice (injected server-side from GCS) is not shadowed by a preset. + * uploaded voice (injected server-side from private object storage) is not + * shadowed by a preset. */ function setConfigVoiceRefUrl(url: string | null): void { const c = loadConfig(); @@ -387,7 +388,7 @@ export function RefAudioSection() { setError(""); try { if (active.kind === "preset") { - // Presets store no GCS blob — the content endpoint 404s for them. + // Presets have no private upload blob — the content endpoint 404s for them. // Preview via the preset's public sampleUrl instead. const list = presets ?? (await getRefAudioPresets()); if (presets === null) setPresets(list); diff --git a/packages/shell/src/lib/__tests__/config.test.ts b/packages/shell/src/lib/__tests__/config.test.ts index 223364bc6..6bcddcada 100644 --- a/packages/shell/src/lib/__tests__/config.test.ts +++ b/packages/shell/src/lib/__tests__/config.test.ts @@ -1,6 +1,7 @@ // @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { + DEFAULT_VOICE_REF_URL, addAllowedTool, clearAllowedTools, hasApiKey, @@ -23,6 +24,13 @@ describe("config", () => { localStorage.clear(); }); + it("uses the public Azure Blob CC0 preset without a GCS dependency", () => { + expect(DEFAULT_VOICE_REF_URL).toBe( + "https://stnaiapub83b29893.blob.core.windows.net/ref-audio/cc0/cc0-ko-female-01.wav", + ); + expect(DEFAULT_VOICE_REF_URL).not.toContain("storage.googleapis.com"); + }); + it("loadConfig returns null when not set", () => { expect(loadConfig()).toBeNull(); }); diff --git a/packages/shell/src/lib/avatar/__tests__/cascade-renderer.test.ts b/packages/shell/src/lib/avatar/__tests__/cascade-renderer.test.ts index a29a9c141..197280b8c 100644 --- a/packages/shell/src/lib/avatar/__tests__/cascade-renderer.test.ts +++ b/packages/shell/src/lib/avatar/__tests__/cascade-renderer.test.ts @@ -45,7 +45,7 @@ describe("CascadeAvatarRenderer.streamUrl", () => { }); describe("CascadeAvatarRenderer.setVoice — PUT /voice 계약 (NVA 전환과 독립된 활성 음성)", () => { - it("외부(GCS) 프리셋 URL 을 **그대로 보내지 않고** 파일명으로 팔레트 URL 로 변환해 민다", async () => { + it("외부(Azure Blob) 프리셋 URL 을 **그대로 보내지 않고** 파일명으로 팔레트 URL 로 변환해 민다", async () => { // 외부 URL 을 서버가 다운로드해 쓰면 샘플레이트 불일치로 합성이 깨진다 // (2026-07-16 새벽 시연 서버 무음 사고 실증) — 팔레트(/ref/audio) 미러가 정본. const fetchMock = vi.fn().mockResolvedValue({ ok: true }); @@ -53,7 +53,7 @@ describe("CascadeAvatarRenderer.setVoice — PUT /voice 계약 (NVA 전환과 try { const r = new CascadeAvatarRenderer({ runtimeUrl: "http://gpu:9449" }); const ok = await r.setVoice( - "https://storage.googleapis.com/naia-ref-audio-presets/cc0/cc0-ko-female-01.wav?x=1", + "https://stnaiapub83b29893.blob.core.windows.net/ref-audio/cc0/cc0-ko-female-01.wav?x=1", ); expect(ok).toBe(true); expect(fetchMock).toHaveBeenCalledTimes(1); diff --git a/packages/shell/src/lib/avatar/cascade-renderer.ts b/packages/shell/src/lib/avatar/cascade-renderer.ts index 29217b80f..a95f21a11 100644 --- a/packages/shell/src/lib/avatar/cascade-renderer.ts +++ b/packages/shell/src/lib/avatar/cascade-renderer.ts @@ -350,13 +350,12 @@ export class CascadeAvatarRenderer { t?.(); } - /** ?쒖꽦 ?덊띁?곗뒪 ?뚯깋 ?ㅼ젙 ??cascade `PUT /voice` 怨꾩빟(2026-07-16 3???⑹쓽: NVA/罹먮┃???꾪솚怨? - * ?낅┰???고????뚯꽦). ?좑툘 ?몃?(GCS ?? URL ??**洹몃?濡?蹂대궡吏€ ?딅뒗??* ???쒕쾭媛€ ?몃? ?뚯씪?? - * ?ㅼ슫濡쒕뱶???덊띁?곗뒪濡??곕㈃ ?섑뵆?덉씠??遺덉씪移섎줈 ?⑹꽦??源⑥쭊 ?ㅼ쬆(2026-07-16 ?덈꼍, ?쒖뿰 ?쒕쾭 - * 臾댁쓬 ?ш퀬). cascade ??媛숈? ?꾨━?뗫뱾??48kHz 濡쒖뺄 誘몃윭 ?붾젅??`GET /ref/voices` ?? - * `/ref/audio/`)瑜?媛€吏€誘€濡? **?뚯씪紐낅쭔 戮묒븘 ?붾젅??URL 濡?蹂€??*??蹂대궦?? - * ?붾젅?몄뿉 ?녿뒗 ?대쫫 = ?쒕쾭 400 fail-closed(湲곗〈 ?쒖꽦 ?뚯꽦 ?좎?) ??false. - * 誘몄??뺤씠硫??꾨Т寃껊룄 蹂대궡吏€ ?딆븘 ?쒕쾭 湲곕낯(naia ?붾젅??default)???좎??쒕떎. */ + /** + * Activate the selected reference voice through cascade `PUT /voice`. + * External preset URLs (including Azure Blob) are never sent to cascade: + * only the filename is mapped to the runtime's `/ref/audio/` mirror. + * Unknown names fail closed on the server; an empty value keeps its default. + */ async setVoice(refUrl: string | null | undefined): Promise { const raw = refUrl?.trim(); if (!raw) return false; diff --git a/packages/shell/src/lib/config.ts b/packages/shell/src/lib/config.ts index 4528b96d8..3eedac455 100644 --- a/packages/shell/src/lib/config.ts +++ b/packages/shell/src/lib/config.ts @@ -23,11 +23,11 @@ export const DEFAULT_NAIA_LOCAL_URL = "ws://127.0.0.1:8892"; /** * Default reference voice ("여성 음색 1" / cc0-ko-female-01) used when the user * has no custom ref and no preset selected. Public CC0 sample (Mozilla Common - * Voice) on the gateway's public bucket — sent as `ref_audio_url` so the omni + * Voice) on the public Azure Blob container — sent as `ref_audio_url` so the omni * voice is always a stable human voice, never the unconditioned/random default. */ export const DEFAULT_VOICE_REF_URL = - "https://storage.googleapis.com/naia-ref-audio-presets/cc0/cc0-ko-female-01.wav"; + "https://stnaiapub83b29893.blob.core.windows.net/ref-audio/cc0/cc0-ko-female-01.wav"; export type ThemeId = | "system" @@ -136,7 +136,8 @@ export interface AppConfig { * when the user applies a preset in Settings; sent verbatim as * `session.update.ref_audio_url` at connect (the deterministic source the * web demo uses — no dependence on the GET /v1/ref-audio status round-trip). - * Empty for uploads/recordings (those are injected server-side from GCS). + * Applying an upload clears this stored preset. The connection fallback and + * the cloud gateway's separate private-upload injection are independent. */ voiceRefUrl?: string; persona?: string; diff --git a/packages/shell/src/lib/voice/__tests__/naia-omni-ref-audio.test.ts b/packages/shell/src/lib/voice/__tests__/naia-omni-ref-audio.test.ts index 09ba76ff3..60f9b5be2 100644 --- a/packages/shell/src/lib/voice/__tests__/naia-omni-ref-audio.test.ts +++ b/packages/shell/src/lib/voice/__tests__/naia-omni-ref-audio.test.ts @@ -162,7 +162,7 @@ describe("MiniCPM-o ref_audio wire plumbing", () => { it("forwards refAudioUrl as session.update.session.ref_audio_url (#15)", async () => { const url = - "https://storage.googleapis.com/naia-ref-audio-presets/aihub-10/female-30s-01.wav"; + "https://stnaiapub83b29893.blob.core.windows.net/ref-audio/cc0/cc0-ko-female-01.wav"; const { promise } = connect({ provider: "naia-omni", serverUrl: "http://localhost:8000", diff --git a/packages/shell/src/lib/voice/__tests__/ref-audio-api.test.ts b/packages/shell/src/lib/voice/__tests__/ref-audio-api.test.ts index 58aa32dd0..a73907a3e 100644 --- a/packages/shell/src/lib/voice/__tests__/ref-audio-api.test.ts +++ b/packages/shell/src/lib/voice/__tests__/ref-audio-api.test.ts @@ -119,7 +119,7 @@ describe("ref-audio preset (CONTRACT P1-P7)", () => { gender: "female", age_range: "30s", duration_seconds: 8.2, - sample_url: "https://storage.googleapis.com/x/female-30s-01.wav", + sample_url: "https://stnaiapub83b29893.blob.core.windows.net/ref-audio/cc0/female-30s-01.wav", sample_format: "wav", source: "aihub-10", license: "research-internal", @@ -136,7 +136,7 @@ describe("ref-audio preset (CONTRACT P1-P7)", () => { expect(list[0]).toMatchObject({ id: "aihub-10-female-30s-01", ageRange: "30s", - sampleUrl: "https://storage.googleapis.com/x/female-30s-01.wav", + sampleUrl: "https://stnaiapub83b29893.blob.core.windows.net/ref-audio/cc0/female-30s-01.wav", durationSeconds: 8.2, }); }); @@ -274,7 +274,7 @@ describe("getRefAudioContent (upload preview)", () => { ); }); - it("404 no-active-ref → 'no-active-ref' code (preset has no GCS blob)", async () => { + it("404 no-active-ref → 'no-active-ref' code (preset has no private blob)", async () => { mockFetch.mockResolvedValue( new Response(JSON.stringify({ error: "no-active-ref" }), { status: 404 }), ); diff --git a/packages/shell/src/lib/voice/ref-audio-api.ts b/packages/shell/src/lib/voice/ref-audio-api.ts index a579ac518..bbdfbec62 100644 --- a/packages/shell/src/lib/voice/ref-audio-api.ts +++ b/packages/shell/src/lib/voice/ref-audio-api.ts @@ -22,7 +22,7 @@ const TAG = "RefAudioApi"; // ── Naia Local recorded/uploaded reference voice (no gateway, no credits) ── // For Naia Local the voice WS goes direct to the user's own container, so the -// gateway GCS upload+inject path can't reach it (and would charge $0.01). We +// gateway upload+inject path can't reach it (and would charge $0.01). We // instead keep the recorded clip as a base64 WAV in localStorage and send it // straight to the container as `session.update.session.ref_audio`. Stored // outside AppConfig so the (large) blob never bloats the frequently-saved @@ -442,7 +442,7 @@ export async function deleteRefAudio( * Stream the user's active *uploaded* ref-audio back as a WAV blob for * in-app preview (GET /v1/ref-audio/content). Free (no charge). * - * Only valid when the active slot is an upload — presets store no GCS blob, + * Only valid when the active slot is an upload — presets have no private blob, * so the gateway returns 404 `no-active-ref` for them; callers must preview * presets via their `sampleUrl` instead. Throws `RefAudioApiError` with code * `no-active-ref` (404), `unauthenticated` (401), or `network`/`unknown`. diff --git a/packages/shell/src/lib/voice/types.ts b/packages/shell/src/lib/voice/types.ts index d7e86901d..d7e87a6fa 100644 --- a/packages/shell/src/lib/voice/types.ts +++ b/packages/shell/src/lib/voice/types.ts @@ -134,10 +134,12 @@ export interface NaiaOmniConfig extends LiveProviderConfigBase { /** * Optional voice-clone reference as an https URL (#15). Sent on the * initial `session.update` as `ref_audio_url`; the backend downloads it - * once (allowlist: storage.googleapis.com / naia.nextain.io) and clones + * once (allowlist: the public Nextain Azure Blob origin / naia.nextain.io) + * and clones * the timbre. Takes priority over `refAudio` (base64) server-side, so - * presets (sample_url) and uploads (storage URL) avoid shipping a heavy - * blob each session. + * public presets (sample_url) avoid shipping a heavy blob each session. + * The cloud gateway may separately inject a stored private upload; this + * client field does not represent the gateway's active-upload state. */ refAudioUrl?: string; /** BCP-47-ish hint to the speech tokenizer. Server defaults to "en". */ diff --git a/src/test/azure-ref-audio-csp.contract.test.ts b/src/test/azure-ref-audio-csp.contract.test.ts new file mode 100644 index 000000000..3e520aa9a --- /dev/null +++ b/src/test/azure-ref-audio-csp.contract.test.ts @@ -0,0 +1,30 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +const repoRoot = resolve(import.meta.dirname, "../.."); +const tauriConfig = JSON.parse( + readFileSync( + resolve(repoRoot, "packages/shell/src-tauri/tauri.conf.json"), + "utf8", + ), +) as { app: { security: { csp: string } } }; + +describe("FR-VOICE.8 packaged Tauri CSP", () => { + it("allows only the Azure preset origin and removes the former GCS origin", () => { + const csp = tauriConfig.app.security.csp; + const mediaSrc = csp.match(/(?:^|;)\s*media-src\s+([^;]+)/)?.[1]; + + expect(mediaSrc).toBeDefined(); + expect(mediaSrc).toContain( + "https://stnaiapub83b29893.blob.core.windows.net", + ); + expect(mediaSrc).not.toContain("https://storage.googleapis.com"); + const sources = mediaSrc?.trim().split(/\s+/) ?? []; + expect( + sources.filter((source) => source.includes("blob.core.windows.net")), + ).toEqual(["https://stnaiapub83b29893.blob.core.windows.net"]); + expect(sources).not.toContain("https:"); + expect(sources).not.toContain("*"); + }); +});