from __future__ import annotations import re from dataclasses import dataclass from pathlib import Path from typing import Any EXPRESSIONS = ["idle", "listening", "thinking", "worried", "smile", "happy", "talk", "focus"] CANONICAL_STAGE_SIZE = (900, 1200) WARM_FOUR_IMAGE_LIMIT_SECONDS = 60.0 EIGHT_ASSET_LIMIT_SECONDS = 180.0 MIN_USABLE_ASSET_COUNT = 6 @dataclass(frozen=True) class ModelCandidate: id: str label: str family: str model_id: str mode: str default_steps: int default_gpu: str implemented: bool notes: str MODEL_CANDIDATES: tuple[ModelCandidate, ...] = ( ModelCandidate( id="flux_schnell", label="FLUX.1-schnell", family="flux", model_id="black-forest-labs/FLUX.1-schnell", mode="text_to_image_speed_baseline", default_steps=4, default_gpu="H100", implemented=True, notes="Speed baseline for main visual candidates.", ), ModelCandidate( id="qwen_image", label="Qwen-Image", family="qwen_image", model_id="Qwen/Qwen-Image", mode="text_to_image_quality_candidate", default_steps=50, default_gpu="H100", implemented=True, notes="Quality and Chinese prompt candidate; likely slower than FLUX.", ), ModelCandidate( id="qwen_image_edit", label="Qwen-Image-Edit", family="qwen_image_edit", model_id="Qwen/Qwen-Image-Edit", mode="instruction_image_edit", default_steps=50, default_gpu="H100", implemented=True, notes="Expression and local edit candidate based on a reference image.", ), ModelCandidate( id="qwen_controlnet_union", label="Qwen-Image-ControlNet-Union", family="qwen_controlnet", model_id="InstantX/Qwen-Image-ControlNet-Union", mode="pose_canny_depth_control", default_steps=30, default_gpu="H100", implemented=True, notes="Structure control candidate for action poses.", ), ModelCandidate( id="instantid_sdxl", label="InstantID SDXL", family="instantid", model_id="InstantX/InstantID", mode="identity_preserving_candidate", default_steps=30, default_gpu="H100", implemented=False, notes="Tracked as identity-preserving candidate; remote runner is intentionally not enabled until antelopev2/model download path is decided.", ), ) def candidate_by_id(candidate_id: str) -> ModelCandidate: for candidate in MODEL_CANDIDATES: if candidate.id == candidate_id: return candidate known = ", ".join(candidate.id for candidate in MODEL_CANDIDATES) raise ValueError(f"unknown model candidate: {candidate_id}; expected one of {known}") def slugify_identifier(value: str, fallback: str = "character") -> str: normalized = value.strip().lower() normalized = re.sub(r"[^a-z0-9_\-\u4e00-\u9fff]+", "_", normalized) normalized = re.sub(r"_+", "_", normalized).strip("_-") return normalized or fallback def project_root() -> Path: return Path(__file__).resolve().parents[2] def default_character_package(character_id: str, display_name: str) -> dict[str, Any]: return { "id": character_id, "name": display_name, "display_name": display_name, "summary": "自动化角色生成风险验证用原创角色草案。", "description": f"{display_name} 是用于验证多表情虚拟角色生成流水线的原创角色。", "personality": "冷静、温柔、有清晰边界。", "scenario": "用户正在通过虚拟角色实验台与角色进行对话和视觉资产测试。", "first_mes": "我在。现在可以开始验证角色生成流程。", "alternate_greetings": ["测试频道已接入。", "角色资产验证准备完成。"], "mes_example": "", "creator_notes": "由自动化角色生成 spike 创建;用于技术验证,不代表最终角色设定。", "tags": ["生成测试", "原创角色", "技术验证"], "profile": { "identity": "自动化角色生成风险验证用原创虚拟角色", "core_traits": ["冷静", "温柔", "边界清晰"], "relationship_to_user": "把用户当成共同验证系统的协作者", "boundaries": ["不声称自己是商业 IP 角色", "不复述商业 IP 官方设定"], }, "dialogue_style": { "tone": "自然、简短、清晰", "sentence_shape": "中短句", "catchphrases": ["我在。"], }, "skills": ["daily_chat", "style_guard"], "voice": {"voice_id": "default", "pace": "normal", "energy": 0.5}, "visual": { "accent": "#67e8f9", "background": "#111827", "background_image": f"{character_id}_spike_background", "avatar": character_id, }, "metadata": {"source": "character_generation_spike"}, }