Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions .agents/context/process-status.json

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions .users/context/process-status.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- src-sha: 40fa012a363d8ddd -->
<!-- src-sha: 25331ad8c94d5c57 -->
<!-- 자동 번역 미러 (M13-mirror). 원본: .agents/context/process-status.json -->

# 프로세스 현황 (Process Status) SoT
Expand Down Expand Up @@ -34,7 +34,7 @@

**시작**: 2026-06-08

**마지막 업데이트**: 2026-07-17 00:00 KST
**마지막 업데이트**: 2026-07-24 13:42 KST

**상태**: 진행 중 (in_progress)

Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 커서·세션 플래그)를 보존하지 않는 회귀 — 데모 실사용 정상 확인이나 재로그인/중복응답 가능성, 부팅 흐름 변경은 별도 안전작업으로 분리.
Expand Down
2 changes: 1 addition & 1 deletion docs/user-scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 검증**) |
Expand Down
2 changes: 1 addition & 1 deletion packages/shell/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
9 changes: 5 additions & 4 deletions packages/shell/src/components/ChatArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() ?? "";
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions packages/shell/src/components/RefAudioSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions packages/shell/src/lib/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
DEFAULT_VOICE_REF_URL,
addAllowedTool,
clearAllowedTools,
hasApiKey,
Expand All @@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ 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 });
vi.stubGlobal("fetch", fetchMock);
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);
Expand Down
13 changes: 6 additions & 7 deletions packages/shell/src/lib/avatar/cascade-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>`)瑜?媛€吏€誘€濡? **?뚯씪紐낅쭔 戮묒븘 ?붾젅??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/<name>` mirror.
* Unknown names fail closed on the server; an empty value keeps its default.
*/
async setVoice(refUrl: string | null | undefined): Promise<boolean> {
const raw = refUrl?.trim();
if (!raw) return false;
Expand Down
7 changes: 4 additions & 3 deletions packages/shell/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions packages/shell/src/lib/voice/__tests__/ref-audio-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
});
});
Expand Down Expand Up @@ -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 }),
);
Expand Down
Loading
Loading