vignette/docs/guides/local-development.md
Yun Chan 2624d49984 설치형 로컬 TTS를 MeloTTS 한국어(MIT)로 채택하고 엔드포인트로 연결
Higgs Audio v3 는 연구/비상업 라이선스라 config.py 가 environment != dev 에서
차단하고 있었다. 그 가드를 푸는 건 법적 판단이라 코드로 결정할 수 없어서,
상업 사용이 허용된 설치형을 다시 찾아 MeloTTS Korean 으로 바꿨다. 결과적으로
가드를 건드릴 필요 자체가 사라졌다 — Higgs 가드는 그대로 두고 provider 만
melotts 로 두면 운영에서도 동작한다.

검토 결과:
- MeloTTS   MIT       한국어 지원  -> 채택. CPU 실시간, 사전학습 다화자
- Kokoro-82M Apache2.0 한국어 없음  -> 탈락. 공식 VOICES.md 언어 목록에 부재
- Piper      GPL                   -> 탈락
- XTTS-v2 / Fish Speech 비상업      -> 탈락. Higgs 와 같은 문제

사전학습 다화자 모델이라 실존 인물 reference 를 쓰지 않는다. Higgs 경로가
P1 프리셋 한정이던 이유가 없으므로 모든 페르소나 프리셋에 적용된다.

구현:
- scripts/melotts-server.py  loopback HTTP 사이드카(/health, POST /tts -> WAV)
- voice_tts_provider=melotts 경로와 VIGNETTE_MELOTTS_TTS_* 설정
- scripts/start-melotts.ps1  런처(설치 순서 안내 포함)

실측:
- CPU 정상 상태 RTF 0.27~0.28(실시간 3.6배). 첫 실행 13.25 는 모델 다운로드
- POST /tts 200, WAV 350,566 bytes, 3.61s, 헤더 provider/model/license
- 빈 텍스트 422, 미지 경로 404 로 fail-closed
- 왕복 검증: MeloTTS 합성음을 로컬 faster-whisper 가 완전 일치 전사
  "그렇게 느끼셨군요. 조금 더 이야기해 주실 수 있을까요?" (word timestamp 8개)

설치 함정 3가지를 decisions/local-voice-stack.md 에 남겼다.
librosa 0.9.1 의 pkg_resources(setuptools<81), MeloTTS 가 언어와 무관하게
임포트하는 일본어 unidic 사전, Windows 한국어 g2p 의 eunjeon.

G7 게이트의 TTS 허용목록에 melotts 를 추가했다. 선언/실제 불일치 차단과
배치 STT 배제는 그대로다.

검증: API 914 passed, 사이드카 melotts 16/16 + whisper 37/37, SSOT FAIL 0, ruff clean.
2026-08-08 09:29:57 +09:00

38 KiB

로컬 개발 실행 가이드

Vignette(AI 심리상담 시뮬레이션 훈련 플랫폼)를 로컬에서 띄워서 테스트하기 위한 단계별 가이드다. 주 환경은 Windows 11 + PowerShell을 기준으로 하되, 셸 차이가 중요한 곳은 별도로 표시한다.

  • 작업 디렉터리(저장소 루트): D:\workspace\vignette
  • 모노레포 구성
    • apps/api — FastAPI 백엔드 (Python)
    • apps/api/engine_gateway — AI 턴 생성용 엔진 게이트웨이(별도 프로세스, 포트 9099)
    • apps/web — React 19 + Vite 프런트엔드
    • infra — Docker Compose 스택(pgvector pg16 + api + web + proxy)
    • scripts — 운영/점검 스크립트

핵심 구성요소: 학습자(상담수련생)가 AI 내담자 페르소나와 회기를 진행하고, 종료 후 회기 리뷰 피드백을 받는다. 백엔드는 페르소나 엔진 · 오케스트레이터 · 저항엔진 · 마스킹 게이트 · 음성 캐스케이드 · 회기 리뷰를 소유하고, 실제 AI 발화 생성은 engine_gateway(ENGINE_MODE 라우팅)가 담당한다. 단일 SoR은 Postgres16+pgvector이며, DB가 없으면 인메모리 degraded 폴백으로 기동한다.


0. 가장 빠른 경로 — 한 커맨드 (권장)

게이트웨이(9099)+API(8000)+웹(5173)을 한 번에 깔끔히 (재)기동한다. Docker가 사용 가능하면 vignette-dev-db Postgres(pgvector) 컨테이너도 보장하고 health/role을 점검한다. 기존/고아 프로세스는 커맨드라인 기준으로 정리한 뒤 결정론적으로 띄운다(--reload 워처 불안정 회피).

powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-up.ps1
# 로컬 Higgs Audio v3 P1 음성까지 함께 연결
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-up.ps1 -UseHiggsVoice

# 노트북 상주 MeloTTS 한국어 TTS 사이드카 (MIT, 운영에서도 사용 가능)
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-melotts.ps1
# 그 뒤 API 에 VIGNETTE_VOICE_TTS_PROVIDER=melotts 를 준다.

# 노트북 상주 faster-whisper STT 사이드카 (외부 STT 키 불필요)
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-local-whisper-stt.ps1 -Model small
# 그 뒤 API 에 VIGNETTE_VOICE_STT_PROVIDER=local_whisper 를 준다.
# 이 노트북은 cuDNN 이 없어 GPU 추론이 네이티브 크래시로 죽는다. 사이드카가 자식 프로세스로
# 디바이스를 먼저 확인하고 CPU(int8)로 자동 폴백한다. cuDNN 9 를 설치하면 GPU(float16)로 올라간다.
# 종료: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1
# DB 컨테이너까지 멈출 때만: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1 -Db
  • 진입점 http://localhost:5173 → 로그인 페이지에서 dev-login(아무 @hs.ac.kr, role learner/teacher/admin).
  • Docker가 있으면 기본적으로 127.0.0.1:55432 DB 컨테이너를 사용한다. 정지된 기존 vignette-dev-db는 제거·재생성하지 않고 그대로 시작하며, 새 컨테이너만 고정 named volume vignette-dev-db-pgdata를 사용한다. 시작이나 readiness가 실패하면 container/volume을 보존한 채 fail closed한다. 새 컨테이너 생성 시 POSTGRES_USER=vignette_owner, API용 앱 role은 DATABASE_URL 사용자로 분리해 RLS 검증 기반을 보존한다. Docker가 없거나 -NoDb를 쓰면 in-memory degraded로 뜬다.
  • 로그는 .devlogs/(gitignore). 코드 수정 후에는 dev-up을 다시 실행해 재기동(reload 미사용).
  • 옵션: -NoGateway(기존 gateway 보존), -NoWeb(기존 web 보존, API만 재기동), -NoDb(DB 컨테이너 보장 건너뜀), -UseHiggsVoice(설치된 higgs-audio-v3-tts-4b를 127.0.0.1:9881에 상주시켜 P1 TTS로 연결).
  • -NoGateway/-NoWeb를 쓰면 해당 컴포넌트의 stale 정리도 건너뛰고, API 정리는 지정한 -ApiPort listener만 대상으로 한다. public 8001, Tailnet 8010, local 8000을 나눠 띄운 상태에서 API-only 재기동할 때 다른 포트를 건드리지 않는다.
  • dev-down.ps1은 기본적으로 DB 컨테이너를 보존한다. 컨테이너도 멈추려면 -Db를 명시한다.

DB나 배포 경로를 바꾸기 전에는 custom-format dump와 SHA-256 manifest를 먼저 만든다. 스크립트는 DB 내용을 stdout에 흘리지 않고 container 내부 임시 파일을 pg_restore --list로 검증한 뒤에만 로컬 백업을 원자 게시한다. 기존 container나 volume을 제거하는 경로는 없다.

powershell -NoProfile -ExecutionPolicy Bypass -File scripts\backup-vignette-db.ps1
# 기본 위치: D:\workspace\vignette-backups\*.dump + 같은 이름의 .json manifest

스크립트는 uvicorn이 설치된 python을 자동 해석한다(시스템에 복수 python 공존 시 'python' 별칭이 uvicorn 없는 인터프리터를 가리킬 수 있음 — 이 함정 때문에 명시 해석함).

0.1 Tailscale PC/모바일 접속

같은 Tailnet에 로그인한 장비에서는 아래 명령으로 Tailnet 전용 HTTPS 접속점을 켠다.

powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-tailscale-runtime.ps1
  • 현재 머신의 접속 주소: https://alpaca-home.taile93291.ts.net
  • 이 경로는 tailscale servehttps://alpaca-home.taile93291.ts.net → 로컬 web 127.0.0.1:5173을 연결한다.
  • alpaca-home.taile93291.ts.net는 Vite 기본 allowedHosts에도 포함되어 있어, 수동 Vite 기동에서도 Tailnet host-header 403이 나지 않아야 한다. 추가 Tailnet host는 VITE_ALLOWED_HOSTS에 쉼표로 더한다.
  • API는 Tailnet 전용으로 127.0.0.1:8010에 뜨고, Vite proxy는 VITE_API_PROXY_TARGET으로 이 포트를 본다. 기존 8000 reloader 잔류와 충돌하지 않기 위해 분리했다.
  • dev-login은 AUTH_DEV_LOGIN_EXTRA_ORIGINS에 Tailnet origin이 들어간 경우에만 dev 환경에서 열린다. prod에서는 열리지 않는다.
  • Tailnet/로컬 dev에서는 Google OAuth를 사용하지 않는다. OAuth redirect URI가 공개 API callback으로 고정된 동안에는 콜백이 로컬/Tailnet 세션이 아니라 공개 API 세션으로 돌아가므로, 로그인 화면은 Google 버튼을 비활성화하고 직접 /api/auth/login?provider=google을 열어도 local_oauth_unavailable 안내로 되돌린다.

0.2 Public runtime watchdog

공개 API 복구 스크립트는 docs/ops/public-runtime-watchdog.md가 runbook이다. 로컬 개발 서버와 별개로 prod API 8001, web preview 5174, engine gateway 9099, cloudflared tunnel을 검사한다.

powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 -CheckOnly
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 -RunNow
  • Scheduled Task는 현재 Windows 사용자 기준 AtLogOn + 반복 watchdog이다. 사용자 로그인 전 headless boot service가 아니다.
  • secret은 task 인자에 넣지 않는다. API secret은 apps/api/.env, cloudflared/Claude CLI credential은 사용자 profile에 둔다.
  • 아직 DNS가 없는 future host는 기본 검사에 넣지 않는다. api-vnet.18ka.net처럼 실제로 열린 뒤에만 -AdditionalPublicHealthUrls로 명시 추가한다.
  • 완료 판정은 parser/check-only가 아니라 실제 재부팅 또는 로그오프/로그온 뒤 Get-ScheduledTaskInfo, watchdog 로그의 restart verified, public health, 인증된 public /turn smoke까지 한 세트로 남겨야 한다.

수동 기동(대안)

DB·엔진 없이도 UI/로그인/페르소나/세션 생성까지는 동작한다(턴 생성만 불가).

  1. APIapps/api에서 python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
  2. apps/web에서 npm run devhttp://localhost:5173
  3. (선택) 엔진 게이트웨이apps/api에서 python -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099 (실제 AI 턴 생성을 하려면 필요. claude CLI 설치+로그인 전제)

아래에서 각 단계를 자세히 설명한다.


1. 사전 준비

  • Python 3.11 (운영 스크립트가 Python 3.11 기준). 가상환경 권장.
  • Node.js 22 + npm — CI와 같은 기준. newer LTS는 별도 재검증 전까지 기준선으로 쓰지 않는다.
  • (선택) Docker Desktopinfra/docker-compose.yml 전체 스택을 띄울 때만.
  • (선택) claude CLIclaude_cli 실행. 설치 후 로그인되어 있어야 한다.
  • (선택) Codex CLIcodex_cli 실행과 model/list 탐색. 기본 선택은 gpt-5.6-terra / Medium.
  • (선택) Agy CLIagy_cli 실행과 agy models 탐색. 기본 선택은 gemini-3.6-flash-high / High.
  • (선택) ANTHROPIC_API_KEYclaude_api의 모델 목록과 Messages API 실행.

Python 의존성 설치 (apps/api)

# 저장소 루트에서
python -m venv .venv
.\.venv\Scripts\Activate.ps1          # PowerShell 활성화
python -m pip install -r apps\api\requirements.txt

apps/api/requirements.txtfastapi, uvicorn[standard], asyncpg, pydantic, pydantic-settings, python-multipart, httpx, sse-starlette를 exact version으로 고정한다. 엔진 게이트웨이도 fastapi+uvicorn만 쓰므로 위 설치로 함께 충족된다. (Presidio PII 마스킹은 선택 의존성이라 기본 제외 — 없으면 정규식 폴백으로 자동 degrade.)

웹 의존성 설치 (apps/web)

cd apps\web
npm install

2. API 서버 실행 (FastAPI)

2.1 환경설정(.env 자동 로드)

설정은 전부 env 주입이며 pydantic-settings작업 디렉터리의 .env를 자동 로드한다 (apps/api/app/config.pySettingsConfigDict(env_file=".env", env_file_encoding="utf-8")). 즉 apps/api 디렉터리에서 uvicorn을 실행하면 apps/api/.env가 자동 반영된다.

로컬 dev에서 중요한 키(이미 apps/api/.env에 셋업되어 있거나, 없으면 .env.example 참고):

로컬 dev 값 의미
ENVIRONMENT dev dev여야 DB 폴백·dev-login·seed 폴백이 허용됨
DATABASE_URL postgresql://...@127.0.0.1:55432/vignette 미연결 시 degraded 폴백(dev 한정)
ENGINE_URL http://127.0.0.1:9099 엔진 게이트웨이 베이스 URL
ENGINE_MODE claude_cli claude_cli / claude_api / codex_cli / agy_cli 공급자 라우팅. 실제 운영 변경은 관리자 드롭다운이 DB에 저장
ENGINE_GATEWAY_SHARED_SECRET 빈 값 선택 인증. NAS/원격 preview에서는 API와 gateway에 동일한 32자 이상 비-placeholder 값을 설정. 빈 값은 기존 로컬 9099 호환
VIGNETTE_LIVE_CLIENT_PROVIDER claude_cli 실시간 내담자 AI 전용 lane. 관리자에서 선택한 evaluator/review 공급자와 분리해 회기별 Claude 상주 세션을 재사용
AUTH_DEV_LOGIN_ENABLED true dev-login 엔드포인트 활성화
AUTH_ALLOWED_EMAIL_DOMAINS ["hs.ac.kr","twentyoz.kr"] 기본 로그인 허용 이메일 도메인(dev-login 포함 검증). /admin/users에 미리 등록된 정확한 이메일은 도메인 밖이어도 예외로 로그인 가능
AUTH_SUPER_ADMIN_EMAILS ["yunchan@twentyoz.kr","hoonjungkoo@hs.ac.kr"] 학습자·교수자·관리자 공간 접근과 승인 상태를 부여할 슈퍼 관리자 이메일
AUTH_APPROVED_EMAILS [] 신규 외부 로그인 시 pending 없이 바로 승인할 이메일 allowlist
AUTH_NEW_USER_DEFAULT_STATUS pending Google/SAML 신규 사용자의 기본 승인 상태. dev: 로그인은 로컬/E2E 편의를 위해 자동 승인
AUTH_EMAIL_COHORT_MAP {} 특정 이메일을 cohort id로 매핑한다. 값은 comma-separated 문자열도 허용
AUTH_DOMAIN_COHORT_MAP {} 이메일/Google hosted domain을 cohort id로 매핑한다. Google/SAML/dev-login 세션 cohort_ids에 반영
VIGNETTE_MELOTTS_TTS_URL http://127.0.0.1:9883 로컬 MeloTTS 한국어 사이드카. scripts/start-melotts.ps1로 띄운다
VIGNETTE_VOICE_STT_PROVIDER openai 또는 deepgram STT 공급자. deepgram은 streaming interim/final 경로, openai는 batch 경로
DEEPGRAM_API_KEY 비밀값 Deepgram streaming credential. key 없이 deepgram을 고르면 OPENAI_API_KEY가 있을 때만 openai-batch-fallback; live Deepgram 증거가 아님
DEEPGRAM_STT_URL / DEEPGRAM_STT_MODEL / DEEPGRAM_STT_LANGUAGE wss://api.deepgram.com/v1/listen / nova-3 / ko Deepgram endpoint와 provider/model metadata 계약
DEEPGRAM_ENDPOINTING_MS / DEEPGRAM_UTTERANCE_END_MS 300 / 1200 streaming EOT 경계. utterance end 최솟값은 1000ms
DEEPGRAM_KEEPALIVE_SECONDS / DEEPGRAM_FINALIZE_TIMEOUT_SECONDS 4 / 15 streaming keepalive와 final 대기 상한
DEEPGRAM_MIP_OPT_OUT true Deepgram model improvement program opt-out query 기본값
VIGNETTE_VOICE_POC_SAMPLE_TTS false P1 무참조 샘플 음성을 /voice/ws TTS에 연결하는 개발 전용 플래그. 마이크/STT는 선택한 STT provider credential 필요, 프로덕션 금지
VIGNETTE_VOICE_TTS_PROVIDER openai·higgs·melotts TTS 공급자. higgs는 dev + P1에서만 허용하며 다른 환경은 설정 검증에서 차단. 동작 자체는 운영 상업 이용권 증거를 대신하지 않음
VIGNETTE_HIGGS_TTS_URL http://127.0.0.1:9881 로컬 Higgs 상주 서버. 저장소의 무참조 synthetic seed만 화자 참조로 사용
VIGNETTE_VOICE_STT_PROVIDER openai·deepgram·local_whisper STT 공급자. local_whisper는 노트북 상주 faster-whisper 사이드카를 쓴다(외부 키 불필요, 오디오가 호스트를 벗어나지 않음). openai는 배치라 interim 이 없다
VIGNETTE_LOCAL_WHISPER_STT_URL ws://127.0.0.1:9882/v1/listen 로컬 whisper 사이드카. scripts/start-local-whisper-stt.ps1로 띄운다
VIGNETTE_LOCAL_WHISPER_STT_MODEL large-v3 large-v3-turbo·medium·small·base 허용. CPU 폴백 시에는 small 이 현실적이다
EVALUATOR_SEMANTIC_CACHE_ENABLED true fast/deep evaluator structured 결과 인메모리 캐시 활성화. 원문 prompt/completion은 저장하지 않음
EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS 900 evaluator cache TTL(초). 0 이하면 비활성
EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES 256 evaluator cache LRU 최대 엔트리 수. 0 이하면 비활성
NOTIFICATION_EMAIL_PROVIDER disabled 운영 메일 provider. 실제 발송은 smtpSMTP_* 설정이 있을 때만 수행
SMTP_HOST / SMTP_FROM_EMAIL 빈 값 NOTIFICATION_EMAIL_PROVIDER=smtp일 때 필요한 SMTP 호스트와 발신 주소

참고: 프로세스 환경변수($env:KEY)는 .env보다 우선한다. 일회성 오버라이드에 쓸 수 있다.

2.2 실행

cd apps\api
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload

기동 로그에 Application startup complete.가 뜨면 정상이다. 프록시(아래 웹)는 /api 프리픽스를 제거해 이 8000 포트로 붙는다.

2.3 seed 페르소나로 띄우기 (선택)

기본값은 seed 미적재(AUTO_SEED_PERSONAS=false)다. 시스템 페르소나(P1P3)와 저장소 페르소나(P4P7)를 DB 카탈로그에 올려서 바로 회기를 만들고 싶으면 실행 전에 두 플래그를 켠다.

cd apps\api
$env:AUTO_SEED_PERSONAS = "true"
$env:ALLOW_SEED_PERSONA_FALLBACK = "true"
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload

main.py lifespan은 settings.auto_seed_personas가 켜져 있을 때만 materialize_seed_personas()를 호출한다. 이 호출은 (code, version) 누락분만 insert하며, 교수자가 수정한 DB 저작본이나 archived 보관본을 덮어쓰거나 재노출하지 않는다. (또는 apps/api/.env에 두 키를 true로 적어도 된다.)

seed manifest만 확인할 때는 API 서버 없이 저장소 루트에서 dry-run runner를 실행한다. Windows에서 python 별칭이 다른 인터프리터를 가리키면 API 의존성이 없을 수 있으므로, 이 저장소에서는 Python 3.11 런처를 우선 쓴다. --apply는 DB pool을 초기화한 뒤 기존 idempotent DB materializer를 호출하므로 로컬 DB 대상이 맞는지 확인한 뒤에만 쓴다.

cd D:\workspace\vignette
py -3.11 scripts\materialize-persona-seeds.py --json

2.4 M2 session digest worker 실행

세션 종료 시 저장되는 fallback digest를 LLM 후보로 압축해 볼 때는 명시 session id runner를 쓴다. 기본은 dry-run이며, DB row를 바꾸려면 --apply를 반드시 붙인다. runner는 DB에서 작업을 읽은 뒤 connection을 놓고 engine을 호출하고, accepted 결과만 짧은 DB acquire로 적용한다.

cd D:\workspace\vignette
py -3.11 scripts\run-session-digest-worker.py --session-id <session_uuid> --json

# accepted 후보를 실제 session_summary/case_profile에 반영할 때만
py -3.11 scripts\run-session-digest-worker.py --session-id <session_uuid> --apply --json
  • 출력은 기본적으로 metadata-only다. digest 본문은 민감할 수 있으므로 --show-digest를 명시할 때만 출력한다.
  • 입력은 client-visible text_masked transcript와 open thread만 사용한다. raw text, evaluator-only turn, CCD, end_state는 압축 prompt에 넣지 않는다.
  • API 서버는 SESSION_DIGEST_WORKER_ENABLED=true일 때만 세션 종료 뒤 같은 worker를 background task로 실행한다. 기본값은 false다.
  • scheduler도 DB load/apply 구간만 connection을 잡고, engine 호출은 DB transaction 밖에서 수행한다.
  • 장시간 provider 운영, 임상 골든셋 품질평가, 재압축 정책은 별도 gate다.

2.5 G8 scheduled agentic producer

G8 scheduler는 한 job에 generator·독립 reviewer·variant judge 모델 호출이 여러 번 발생하므로 기본값이 false다. infra/db/init/14_continuous_improvement.sql의 runtime contract가 준비된 경우에만 API lifespan이 producer를 시작한다. 활성화하면 저장소의 승인된 비식별 synthetic source pack을 immutable app.ci_agentic_job으로 멱등 등록하고, lease·FOR UPDATE SKIP LOCKED로 제한된 batch를 처리한다.

$env:VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENABLED = "true"
$env:VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_INTERVAL_SECONDS = "3600"
$env:VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_STARTUP_DELAY_SECONDS = "30"
$env:VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_RETRY_DELAY_SECONDS = "300"
$env:VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_LEASE_TIMEOUT_SECONDS = "1800"
$env:VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENGINE_TIMEOUT_SECONDS = "300"
$env:VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_BATCH_SIZE = "1"
$env:VIGNETTE_CONTINUOUS_IMPROVEMENT_DRIFT_TRIGGER_ENABLED = "true"
  • source usage가 approved가 아니거나 classification·본문 SHA-256·PII 검사가 실패하면 engine을 호출하지 않는다.
  • engine/structured-output 실패는 candidate를 저장하지 않고 retry_wait로 남긴다. 한 job 실패는 다음 job을 막지 않는다.
  • 안전 gate 실패는 rejected, 전 gate 통과 결과는 completed job과 pending_human_approval candidate로만 저장한다.
  • producer는 사람 승인 event나 catalog entry를 만들지 않는다. 동일 submission recovery는 model call 0으로 재생한다.
  • drift trigger는 producer와 별도 opt-in이다. 켜면 G6 합성 drift 원장의 canonical 임계값·subgroup 근거를 재검증해 metadata-only incident·4-node DAG·scheduled_incident job만 원자적으로 enqueue한다. 두 설정 중 하나라도 false면 운영 drift 자동 기동은 없다.

실제 configured engine과 dev PostgreSQL을 일회 검증할 때는 로그인 없이 아래 runner를 사용한다.

py -3.11 -X utf8 scripts\smoke-continuous-improvement-agentic-producer.py `
  --out docs\ops\evidence\continuous-improvement-agentic-producer-live-2026-08-07.json

2.6 DB 없이 degraded 기동 (정상 동작)

DB 연결이 안 되어도 dev에서는 그대로 기동한다. main.py lifespan이 풀 초기화 예외를 잡고 store 인메모리 폴백으로 degraded 기동하며, 다음 경고를 남긴다.

DB 풀 초기화 실패 — store 인메모리 폴백으로 degraded 기동: ...

이때 /health는 다음과 같이 응답한다(DB·엔진 미가용이면 status: degraded).

{ "status": "degraded", "db": false, "engine": false, "environment": "dev", "engine_mode": "claude_cli", ... }

중요: dev에서 db: false버그가 아니라 의도된 폴백이다. 실제 DB가 필요하면 Postgres16+pgvector를 DATABASE_URL이 가리키는 곳(로컬 dev는 127.0.0.1:55432)에 띄우면 된다. ENVIRONMENT가 dev가 아니면 폴백하지 않고 그대로 실패한다.

헬스 체크:

Invoke-RestMethod http://127.0.0.1:8000/health
# 또는
curl.exe http://127.0.0.1:8000/health

운영 콘솔 헬스 샘플을 브라우저 방문 없이 DB에 1회 적재하려면 저장소 루트에서 아래를 실행한다. 이 스크립트는 apps/api/.env를 읽고 API lifespan과 같은 DB/engine/voice 초기화 경로를 사용해 app.admin_health_event에 서비스별 샘플을 append한다. SLA 수치가 아니라 관측 샘플 이력이다.

C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\record-admin-health-sample.py --json

# 예약 작업 명령 확인만
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-health-sampler-task.ps1 -PrintOnly

# 실제 등록 + 즉시 1회 실행
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-health-sampler-task.ps1 -IntervalMinutes 5 -RunNow

쌓인 원시 헬스 샘플은 명시 기간을 준 maintenance 스크립트로 일별 rollup에 먼저 집계한 뒤 정리한다. 기본은 dry-run이며, 실제 삭제는 --apply를 붙였을 때만 수행한다. non-dev 환경에서는 --allow-non-dev-apply가 없으면 apply가 차단된다.

# 영향 범위 확인
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\maintain-admin-health-events.py --rollup-days 2 --retention-days 30 --json

# dev DB에서만 실제 적용
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\maintain-admin-health-events.py --rollup-days 2 --retention-days 30 --apply --json

3. dev 로그인 (로컬/E2E용 서버 로그인)

프로덕션 경로는 Google OIDC지만, 로컬에서는 dev-login으로 쿠키 세션을 바로 발급받을 수 있다. 조건: ENVIRONMENT=dev 그리고 AUTH_DEV_LOGIN_ENABLED=true, 그리고 요청 Origin/Host가 로컬이어야 한다 (routes/auth.py_dev_login_available). 충족 못 하면 404 dev login is disabled.

  • 엔드포인트: POST /auth/dev-login
  • 본문: { "email": ..., "role": "learner|teacher|admin", "display_name": ... }
  • role 기본값은 learner.
  • 성공 시 __Host-vignette_sid 쿠키(및 dev 전용 vignette_sid 쿠키)를 세팅한다.
  • dev-login 사용자는 로컬/E2E 흐름 유지를 위해 account_status=approved로 생성된다.
  • Google/SAML 신규 사용자는 기본적으로 account_status=pending이며, 승인 전에는 /pending 화면만 볼 수 있다. 관리자 콘솔 /admin/users의 가입 승인 탭에서 approved로 바꾸면 역할 홈에 접근한다. DB가 연결되어 있으면 pending 생성 시 app.notification_event/app.notification_delivery에 가입 승인 메일 큐가 생긴다. NOTIFICATION_EMAIL_PROVIDER=disabled인 로컬 기본값에서는 실제 메일은 발송하지 않는다.
  • 신규 사용자 또는 온보딩 미완료 사용자는 로그인 직후 /onboarding에서 닉네임, 자기소개, 선택 아바타 이미지, 이름, 소속, 학과, 학년/직위, 전화번호, 주소/수령지와 약관·개인정보 동의를 저장해야 역할 홈으로 이동한다. 학습자 회기 시작은 온보딩 완료와 동의가 모두 있어야 한다.
  • 프로필 아바타 업로드는 API 작업 디렉터리 기준 USER_UPLOAD_DIR(기본 uploads) 아래 profile-avatars/에 저장되고, /uploads/profile-avatars/... URL로 서빙된다.

주의(이메일 도메인): dev-login도 기본적으로 validate_google_identity_domain을 거치므로 이메일 도메인이 AUTH_ALLOWED_EMAIL_DOMAINS에 있어야 한다. 예: learner@hs.ac.kr. 단, 관리자가 /admin/users에 미리 등록한 정확한 이메일은 도메인 밖이어도 로그인할 수 있다. 미등록 외부 도메인은 계속 403 email domain is not allowed.

3.1 메일 알림 큐 확인/처리

가입 승인과 교수자 회기 검토 알림은 상태 원본이 아니라 보조 알림이다. 원본 상태는 각각 app.app_user.account_status, app.session_review_status이고, 메일 전송 상태만 app.notification_delivery에 남는다.

cd D:\workspace\vignette
# SMTP 설정이 준비된 환경에서 큐를 한 번 처리
python scripts\run-notification-worker.py --limit 25

관리자 API에서도 GET /admin/notifications로 최근 delivery를 보고, POST /admin/notifications/process로 한 번 처리할 수 있다. POST /admin/notifications/testAUTH_SUPER_ADMIN_EMAILS 대상에게 테스트 메일 이벤트를 만들고 같은 큐로 즉시 처리한다. 메일 본문에는 축어록이나 평가 전문을 넣지 않고, /admin/users, /teach/session/:sessionId/review, /admin 링크만 제공한다.

3.2 PowerShell(권장) — Invoke-RestMethod + 세션 쿠키

PowerShell의 curlInvoke-WebRequest 별칭이라 JSON 본문·쿠키 다루기가 번거롭다. PowerShell에서는 Invoke-RestMethod가 가장 깔끔하다.

$body = '{"email":"learner@hs.ac.kr","role":"learner","display_name":"테스트 학습자"}'
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8000/auth/dev-login `
  -ContentType 'application/json' -Body $body -SessionVariable s

# 발급된 쿠키로 현재 사용자 확인
Invoke-RestMethod -Uri http://127.0.0.1:8000/auth/me -WebSession $s

3.3 curl.exe(셸 무관) — JSON 본문은 파일로

Windows에서 따옴표 이스케이프 사고를 피하려면 본문을 파일에 넣고 --data-binary @file로 보낸다. (PowerShell에서는 반드시 curl.exe라고 적어 별칭이 아닌 실제 curl을 호출한다.)

# body.json 작성
'{"email":"learner@hs.ac.kr","role":"learner","display_name":"테스트 학습자"}' |
  Out-File -Encoding ascii body.json

# 쿠키 저장(-c) + 본문은 파일(@)로
curl.exe -i -X POST http://127.0.0.1:8000/auth/dev-login `
  -H "Content-Type: application/json" --data-binary "@body.json" -c cookies.txt

# 저장한 쿠키(-b)로 재호출
curl.exe http://127.0.0.1:8000/auth/me -b cookies.txt

3.4 웹 UI

apps/web의 로그인 화면(Login.tsx)에 dev-login 경로가 있다. 웹을 띄운 상태(5173)에서 프록시를 통해 /api/auth/dev-login으로 동일하게 동작한다.

로컬/Tailnet 테스트는 dev-login을 사용한다. Google OAuth는 공개 도메인 https://vignette.chanpaca.net에서만 실제 계정 흐름으로 검증한다.

3.5 라이브 코칭 source pack RAG 색인

data/kb/live_coaching_workbook_0615.jsondata/kb/live_coaching_sources/*.json는 라이브 코칭의 기본 근거 source pack이다. API가 DB와 연결된 상태라면 관리자 dev-login 쿠키로 같은 자료를 RAG KB에도 증분 색인할 수 있다.

이 디렉터리는 Docker 이미지가 COPY data ./data로 포함하는 런타임 입력이다. 다른 머신에 심기 전에는 source pack 파일이 워크트리에 존재하는지 반드시 확인한다. commit하지 않는 배포 방식이면 같은 경로로 별도 provision해야 하며, 아래 preflight가 없으면 실패하게 둔다.

python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets
$body = '{"email":"admin@hs.ac.kr","role":"admin","display_name":"로컬 관리자"}'
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8000/auth/dev-login `
  -ContentType 'application/json' -Body $body -SessionVariable adminSession

Invoke-RestMethod -Method Post `
  -Uri http://127.0.0.1:8000/kb/live-coach/source-packs/sync `
  -WebSession $adminSession

응답의 skipped_unchanged가 0보다 크면 content_hash가 동일해 재색인을 건너뛴 source pack이 있다는 뜻이다. 현재 sync 경로는 active kb.document의 hash/version을 먼저 읽고, hash가 바뀐 pack만 다음 document version으로 색인한다. 임베딩 모델이 없으면 degraded=true로 BM25-only 색인이 되지만, 라이브 코칭 기본 로컬 근거는 계속 동작한다.

API 서버 없이 DB에 직접 비교/적용하려면 저장소 루트에서 runner를 쓴다. 기본은 DB-backed dry-run이라 write하지 않고, --apply에서만 kb.source upsert와 kb.document/kb.chunk 색인을 수행한다.

cd D:\workspace\vignette
py -3.11 scripts\sync-persona-sources.py --json
py -3.11 scripts\sync-persona-sources.py --apply --json

DB 없이 CLI shape만 확인하려면:

py -3.11 scripts\sync-persona-sources.py --help

4. 웹(프런트엔드) 실행

cd apps\web
npm run dev
  • 접속: http://localhost:5173
  • 프록시: apps/web/vite.config.ts/apihttp://127.0.0.1:8000로 보내며 /api 프리픽스를 제거한다. (changeOrigin: true, ws: true로 쿠키·WebSocket 전달.) 따라서 브라우저의 /api/auth/me는 백엔드의 /auth/me에 도달한다.
  • API가 8000에서 떠 있어야 프록시가 의미가 있다(2장 먼저 실행).

기타 웹 스크립트(apps/web):

npm run typecheck   # tsc -b
npm run build       # tsc -b && vite build
npm run e2e         # Playwright (web + api + DB 스택 필요)

5. 엔진 게이트웨이 (AI 턴 생성)

실제 발화 생성은 ENGINE_MODE와 무관하게 engine_gateway가 담당한다. 게이트웨이는 컨테이너 밖(호스트) 9099 포트에서 Claude CLI 상주 풀, Anthropic Messages API, Codex CLI, Agy CLI를 공통 계약으로 라우팅한다. 관리자는 /admin/ai 또는 /settings의 AI 운영 섹션에서 공급자를 고르고, 게이트웨이가 반환한 모델·추론 강도만 드롭다운으로 저장할 수 있다.

실시간 내담자 발화는 VIGNETTE_LIVE_CLIENT_PROVIDER=claude_cli일 때 관리자 기본 공급자와 별도의 회기별 상주 Claude lane을 쓴다. Agy를 evaluator/review 기본값으로 유지해도 내담자 답변이 Agy CLI의 매 호출 도구 스키마 부팅 비용을 기다리지 않는다. 스트림의 learner/client turn과 상태는 먼저 저장하고 done을 보낸 뒤 fast-loop 평가는 백그라운드에서 같은 learner turn에 durable하게 붙인다.

5.1 실행

cd apps\api
python -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099

관련 환경변수(gateway.py):

  • ENGINE_GATEWAY_SHARED_SECRET — 선택 shared secret. 설정하면 /health를 제외한 모든 경로가 X-Vignette-Engine-Token을 constant-time으로 검증한다. API의 EngineClient에도 같은 값을 설정한다. 값 자체는 로그나 OpenAPI 스키마에 노출하지 않는다. 빈 값은 기존 로컬 9099 호출을 그대로 허용한다.
  • CLAUDE_BIN — claude 실행 파일 경로(기본 claude). PATH에 없으면 절대경로 지정.
  • CODEX_BIN, AGY_BIN — 각 CLI 경로. Windows Codex는 npm shim 아래 native exe를 자동 탐색한다.
  • ANTHROPIC_API_KEY, ANTHROPIC_API_BASE — Anthropic 모델 목록·Messages API.
  • ENGINE_CLI_CWD — Codex/Agy를 저장소 밖에서 실행할 격리 cwd.
  • ENGINE_CAPABILITY_CACHE_TTL_SECONDS(기본 60), ENGINE_CLI_TIMEOUT_SECONDS(기본 300).
  • ENGINE_MODEL — 비우면 CLI 기본(Opus 4.8), ENGINE_FALLBACK_MODEL, SESSION_BUDGET_USD 등.

5.2 헬스/레디 확인

게이트웨이는 세 가지 점검 엔드포인트를 제공한다.

  • GET /health — 얕은 게이트웨이 프로세스 liveness. {"ok": true, "engine": "claude_cli", ...}
  • GET /ready?provider=&model=&reasoning_effort= — 선택 조합으로 실제 1회 생성을 돌려 인증·동작까지 증명.
  • GET /v1/capabilities?provider=&force= — 현재 계정에서 선택 가능한 모델·추론 강도를 반환.
Invoke-RestMethod http://127.0.0.1:9099/health
$headers = @{ 'X-Vignette-Engine-Token' = $env:ENGINE_GATEWAY_SHARED_SECRET }
Invoke-RestMethod 'http://127.0.0.1:9099/ready?provider=codex_cli&model=gpt-5.6-terra&reasoning_effort=medium' -Headers $headers
Invoke-RestMethod 'http://127.0.0.1:9099/v1/capabilities?provider=codex_cli&force=true' -Headers $headers

API의 /health는 현재 DB 설정의 provider/model/reasoning_effort를 게이트웨이 /ready에 전달하고 (구형 게이트웨이는 /health로 폴백) engine 필드를 채운다. 따라서 프로세스만 떠 있고 선택한 CLI/API가 미인증이거나 모델 조합을 실행할 수 없으면 API /healthengine: false가 된다.

5.3 프로빙 스크립트 (선택)

이미 떠 있는 게이트웨이의 스트리밍 TTFT/지연/세션 재사용을 측정한다.

python scripts\probe-engine-gateway.py --base-url http://127.0.0.1:9099
python scripts\probe-engine-gateway.py --json     # 기계 판독용

기본 base-url은 ENGINE_URL 또는 http://127.0.0.1:9099. 이 스크립트는 실제 세션을 만들어 생성 호출을 하므로(예산 소모) 자격증명은 넘기지 않고 이미 떠 있는 게이트웨이에만 붙는다.

게이트웨이가 없을 때: API /healthengine: false이고 턴 생성은 실패한다. 다만 UI/네비게이션/로그인/페르소나 조회/세션 생성(in-memory)은 정상 동작한다.


6. (선택) Docker Compose 전체 스택

DB까지 포함한 통합 실행은 infra/docker-compose.yml(db pgvector pg16 + api + web + proxy)을 쓴다. Docker Desktop이 필요하고, infra/.env(템플릿: infra/.env.example)에 POSTGRES_PASSWORD, APP_DB_PASSWORD, SESSION_SECRET, OAuth/OpenAI 키 등을 채워야 한다.

cd infra
docker compose up -d db        # DB만
docker compose up -d           # 전체

배포지에 심기 전에는 저장소 루트에서 preflight를 먼저 실행한다. DB 검증을 붙일 때는 owner 계정이 아니라 API가 쓸 app-role DATABASE_URL을 넘긴다. 실패하면 owner-run init/migration을 먼저 처리하고 API를 띄운다.

python scripts\check-deploy-preflight.py --env-file infra\.env
python scripts\check-deploy-preflight.py --env-file infra\.env --database-url $env:DATABASE_URL --require-app-role

현재 Windows public runtime처럼 apps/api/.envDATABASE_URL로 직접 Uvicorn을 띄우는 경로는 Compose 패키징 비밀번호를 요구하지 않는 전용 profile을 쓴다. G3~G8 내부 토큰 값은 명령행이나 로그에 출력하지 않는다.

python scripts\provision-outcome-os-runtime-secrets.py --env-file apps\api\.env
python scripts\check-deploy-preflight.py --env-file apps\api\.env --deployment-profile direct-runtime --require-app-role

운영·스테이징 env는 G3G8 내부 ingestion 경계별로 서로 다른 32자 이상 토큰 6개를 가져야 한다. 프리플라이트는 누락·짧은 값·예시 값·재사용 값을 배포 전에 거부하며, Compose도 같은 키를 필수로 전달한다. DB 검사를 켜면 G0G8 migration별 대표 원장 테이블도 전부 확인하므로 하나라도 빠진 스키마에는 배포하지 않는다.

엔진 게이트웨이(9099)는 컴포즈 밖 호스트에서 돌리고, api 컨테이너는 ENGINE_URL=http://host.docker.internal:9099로 호출한다(compose 기본값). RAG 임베딩/리랭커 의존성은 기본 이미지에 설치하지 않는다. 모델까지 포함한 API 이미지를 만들 때만 infra/.envINSTALL_RAG=true를 설정한다.


7. 테스트

# 백엔드 (apps/api)
cd apps\api
python -m pytest app/ -q              # 백엔드 기준선 432 pass
python -m pytest engine_gateway\ -q   # 현재 44 pass

# 웹 (apps/web)
cd apps\web
npm run typecheck
npm run build
npm run e2e                           # Playwright — web+api+DB 스택 필요

8. 트러블슈팅

  • 포트 충돌(8000/5173/9099): 점유 프로세스 확인 후 종료.

    Get-NetTCPConnection -LocalPort 8000 | Select-Object OwningProcess
    Stop-Process -Id <PID> -Force
    

    또는 uvicorn --port를 바꾼다(웹 프록시는 8000 가정이므로 바꾸면 vite.config.ts target도 함께 조정).

  • /healthdb: false: dev에서는 정상 degraded 폴백(인메모리 store). 실제 DB가 필요하면 DATABASE_URL 대상(로컬 dev 127.0.0.1:55432)에 Postgres16+pgvector를 띄운다. ENVIRONMENT가 dev가 아니면 폴백하지 않고 기동이 실패한다.

  • /healthengine: false / 턴 생성 실패: 게이트웨이(9099)가 안 떠 있거나 claude CLI가 인증 안 됨. 게이트웨이 GET /readydetail을 보고 원인을 확인한다. 게이트웨이를 먼저 실행하라.

  • dev-login이 404 (dev login is disabled): ENVIRONMENT=dev + AUTH_DEV_LOGIN_ENABLED=true인지, 요청이 로컬 Origin/Host인지 확인. apps/api에서 uvicorn을 실행해 .env가 로드됐는지도 확인.

  • dev-login이 403 (email domain is not allowed): 이메일 도메인이 AUTH_ALLOWED_EMAIL_DOMAINS에 없고, /admin/users에 정확히 등록된 관리 사용자도 아니다. learner@hs.ac.kr 같은 허용 도메인을 쓰거나 관리자가 해당 이메일을 먼저 등록한다.

  • /auth/me가 401: 쿠키가 전달되지 않음. curl은 -c/-b로 쿠키를 저장·재사용하고, PowerShell은 -SessionVariable/-WebSession을 쓴다. 브라우저는 프록시(5173) 경유로 호출해야 __Host- 쿠키가 동일 출처로 붙는다.

  • PowerShell에서 curl이 이상하게 동작: PowerShell curlInvoke-WebRequest 별칭이다. 실제 curl을 쓰려면 curl.exe로 호출하고, JSON 본문은 --data-binary "@body.json"처럼 파일로 넘긴다.

  • 한글/공백 경로: 경로에 공백·한글이 있으면 큰따옴표로 감싼다. .env/JSON 파일은 UTF-8(또는 ascii)로 저장.