vignette/docs/avatar-art/seoyeon/extract-safe-bust.py
Yun Chan 085460b5e0 대시보드 폴드아웃/드릴다운 정리 + 페르소나 역린·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
2026-06-27 02:30:46 +09:00

65 lines
2.5 KiB
Python

#!/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())