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
65 lines
2.5 KiB
Python
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())
|