대시보드 폴드아웃/드릴다운 정리 + 페르소나 역린·misconduct 반응 + 게이트웨이 격리·RAG 비차단 수정

SSOT 대시보드:
- 한신대 기술분석 PDF(19쪽) 정합성 분석 + 이번 세션 발견 섹션 추가
- 섹션 폴드아웃(접기)·상단 목차(드릴다운)·모두 펼치기/접기 — 내용 보존, 레이아웃만 정리

페르소나 반응 강화('저항·반응 조절' 핵심 차별):
- PersonaCard.triggers(역린) 필드 + CCD 핵심상처 파생 역린 블록
- L0에 무례·모욕·조롱 시 현실적 동맹 균열 반응 지침

버그·성능 수정(라이브/E2E로 포착):
- 게이트웨이 페르소나 격리: --append-system-prompt를 --system-prompt(교체)로 + --exclude-dynamic-system-prompt-sections (내담자 캐릭터 붕괴·개발맥락 누출 차단)
- RAG: 임베더 동기 로드(약 7-13초)를 _warm_rag_caches 백그라운드 warm으로(세션 생성 블로킹 회귀 수정)
- voice TTS RMS 데드힌트 제거, init_state OpennessParams 파라미터객체화
- 한국어 PII(날짜·금액·주소) 마스킹 보강
- 레이아웃 시각 게이트: 폼 컨트롤 값 스크롤 오탐 제외(7/7)

검증: 백엔드 84/84, E2E 42(데스크톱 27·모바일 11·아바타 4), 시각 게이트 7/7
This commit is contained in:
Yun Chan 2026-06-27 02:30:46 +09:00
parent cb2aebd76c
commit 085460b5e0
327 changed files with 31226 additions and 1829 deletions

View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""character-sheet-v1.png의 완성 흉상을 안전한 public neutral.png로 게시한다.
깨진 실험 파츠가 사용자 화면에 바로 노출되지 않도록, 렌더 기본값은
완성 흉상 컷아웃을 사용한다. 세부 파츠 리깅은 별도 parts 실험 파일로 유지한다.
"""
from pathlib import Path
from PIL import Image, ImageFilter
import numpy as np
ROOT = Path("D:/workspace/vignette")
SHEET = ROOT / "docs/avatar-art/seoyeon/character-sheet-v1.png"
PUB = ROOT / "apps/web/public/avatar/seoyeon"
OUT = PUB / "neutral.png"
CANVAS = (900, 1125)
def remove_white_bg(im: Image.Image) -> Image.Image:
arr = np.array(im.convert("RGBA")).astype(np.float32)
rgb = arr[:, :, :3]
border = np.concatenate(
[
rgb[:6, :, :].reshape(-1, 3),
rgb[-6:, :, :].reshape(-1, 3),
rgb[:, :6, :].reshape(-1, 3),
rgb[:, -6:, :].reshape(-1, 3),
],
axis=0,
)
bg = np.median(border, axis=0)
dist_bg = np.sqrt(((rgb - bg.reshape(1, 1, 3)) ** 2).sum(axis=2))
dist_white = np.sqrt(((255.0 - rgb) ** 2).sum(axis=2))
alpha_bg = np.clip((dist_bg - 15.0) * 255.0 / (58.0 - 15.0), 0, 255)
alpha_white = np.clip((dist_white - 18.0) * 255.0 / (58.0 - 18.0), 0, 255)
alpha = np.minimum(alpha_bg, alpha_white)
# Keep linework/hair/clothes solid once away from the paper background.
chroma = rgb.max(axis=2) - rgb.min(axis=2)
dark = rgb.max(axis=2) < 205
alpha = np.where((dark | (chroma > 18)) & (alpha > 60), np.maximum(alpha, 235), alpha)
alpha = np.minimum(alpha, arr[:, :, 3])
arr[:, :, 3] = alpha
arr[alpha < 1, :3] = 0
return Image.fromarray(arr.astype(np.uint8), "RGBA").filter(ImageFilter.GaussianBlur(0.12))
def main() -> int:
sheet = Image.open(SHEET).convert("RGBA")
# Left assembled bust from the generated parts sheet.
crop = sheet.crop((0, 60, 570, 835))
cut = remove_white_bg(crop)
cut.thumbnail((820, 1040), Image.Resampling.LANCZOS)
canvas = Image.new("RGBA", CANVAS, (0, 0, 0, 0))
x = (CANVAS[0] - cut.width) // 2
y = 52
canvas.alpha_composite(cut, (x, y))
OUT.parent.mkdir(parents=True, exist_ok=True)
canvas.save(OUT, optimize=True)
for name in ["sad", "tired", "anxious", "warm", "startled", "eyes-closed", "speaking"]:
canvas.save(PUB / f"{name}.png", optimize=True)
print(f"published {OUT}")
return 0
if __name__ == "__main__":
raise SystemExit(main())