virtual-characters / tests /test_character_workshop.py
ShadowInk's picture
Deploy Virtual Characters for Build Small Hackathon
005e075 verified
Raw
History Blame Contribute Delete
10.1 kB
from __future__ import annotations
import io
import json
import os
import unittest
import uuid
from pathlib import Path
from PIL import Image, ImageDraw
import src.character_workshop as workshop
from src.character_registry import load_user_character_packages
from src.model_status import ModelStatus, statuses_markdown
PROJECT_ROOT = Path(__file__).resolve().parents[1]
TMP_ROOT = PROJECT_ROOT / ".tmp" / "workshop_tests"
class CharacterWorkshopTests(unittest.TestCase):
def setUp(self) -> None:
self.test_root = TMP_ROOT / f"{self._testMethodName}_{uuid.uuid4().hex}"
self.test_root.mkdir(parents=True, exist_ok=True)
self.old_roots = (
workshop.WORKSHOP_ROOT,
workshop.INSTALLED_CHARACTER_ROOT,
workshop.INSTALLED_BACKGROUND_ROOT,
workshop.INSTALLED_CHARACTER_JSON_ROOT,
)
self.old_data_root = os.environ.get("VC_DATA_ROOT")
workshop.WORKSHOP_ROOT = self.test_root / "generated"
workshop.INSTALLED_CHARACTER_ROOT = self.test_root / "installed" / "assets" / "characters"
workshop.INSTALLED_BACKGROUND_ROOT = self.test_root / "installed" / "assets" / "backgrounds"
workshop.INSTALLED_CHARACTER_JSON_ROOT = self.test_root / "installed" / "characters"
os.environ["VC_DATA_ROOT"] = str(self.test_root / "data")
workshop.set_remote_probe_for_tests(self._fake_probe)
self.probe_calls = []
def tearDown(self) -> None:
(
workshop.WORKSHOP_ROOT,
workshop.INSTALLED_CHARACTER_ROOT,
workshop.INSTALLED_BACKGROUND_ROOT,
workshop.INSTALLED_CHARACTER_JSON_ROOT,
) = self.old_roots
if self.old_data_root is None:
os.environ.pop("VC_DATA_ROOT", None)
else:
os.environ["VC_DATA_ROOT"] = self.old_data_root
workshop.set_remote_probe_for_tests(None)
def test_form_to_installable_character_package(self) -> None:
draft = workshop.create_draft_from_form(
display_name="工坊角色",
description="银白短发的原创角色。",
personality="冷静、温柔",
scenario="通讯端测试。",
first_mes="我在。",
tags="测试,工坊",
)
state = workshop.create_initial_state(draft)
state = workshop.generate_main_candidates(state)
self.assertEqual(len(state["main_candidates"]), 4)
state = workshop.select_main_candidate(state, 2)
self.assertEqual(state["selected_candidate_index"], 2)
state = workshop.generate_expression_pack(state)
self.assertEqual(set(state["expression_assets"]), set(workshop.EXPRESSIONS))
state = workshop.generate_background(state)
self.assertTrue(Path(state["background_asset"]).exists())
state = workshop.matte_and_package_assets(state, mode="fallback")
package_dir = Path(state["package_dir"])
self.assertTrue((package_dir / "generated" / "asset_grid.png").exists())
self.assertTrue((package_dir / "generated" / "stage_smoke.html").exists())
state = workshop.install_character_package(state)
character_id = state["installed_character_id"]
self.assertTrue((workshop.INSTALLED_CHARACTER_JSON_ROOT / f"{character_id}.json").exists())
self.assertTrue((workshop.INSTALLED_BACKGROUND_ROOT / f"{character_id}_background.png").exists())
for expression in workshop.EXPRESSIONS:
self.assertTrue((workshop.INSTALLED_CHARACTER_ROOT / character_id / f"{expression}.png").exists())
def test_user_character_loader_reads_installed_json(self) -> None:
root = TMP_ROOT / "registry"
root.mkdir(parents=True, exist_ok=True)
path = root / "loaded_character.json"
path.write_text(json.dumps({"id": "loaded_character", "display_name": "已加载角色"}), encoding="utf-8")
packages = load_user_character_packages(root)
self.assertEqual(packages["loaded_character"]["display_name"], "已加载角色")
def test_image_generation_status_markdown(self) -> None:
html = statuses_markdown(
[
ModelStatus(
"image_generation",
"sleeping",
"已休眠",
url="modal_apps/modal_character_spike.py",
message="Modal 图像生成服务可能已休眠或正在冷启动,请等待容器启动和模型载入后重试。",
)
]
)
self.assertIn("Image Generation", html)
self.assertIn("等待容器启动", html)
def test_logged_in_runs_are_user_scoped_and_loadable(self) -> None:
alice = workshop.get_current_user(_Profile("alice", "Alice"))
bob = workshop.get_current_user(_Profile("bob", "Bob"))
alice_state = workshop.create_initial_state(
workshop.create_draft_from_form(
display_name="Alice 角色",
description="测试。",
personality="冷静",
scenario="测试。",
first_mes="你好。",
),
user=alice,
)
bob_state = workshop.create_initial_state(
workshop.create_draft_from_form(
display_name="Bob 角色",
description="测试。",
personality="冷静",
scenario="测试。",
first_mes="你好。",
),
user=bob,
)
alice_runs = workshop.list_user_workshop_runs(alice)
bob_runs = workshop.list_user_workshop_runs(bob)
self.assertEqual(len(alice_runs), 1)
self.assertEqual(len(bob_runs), 1)
self.assertIn("Alice 角色", alice_runs[0][0])
self.assertIn("Bob 角色", bob_runs[0][0])
self.assertNotEqual(alice_state["run_dir"], bob_state["run_dir"])
loaded = workshop.load_workshop_run(alice_runs[0][1], user=alice)
self.assertEqual(loaded["character_id"], alice_state["character_id"])
with self.assertRaises(ValueError):
workshop.load_workshop_run(alice_runs[0][1], user=bob)
def test_resume_main_candidates_from_manifest(self) -> None:
user = workshop.get_current_user(_Profile("alice", "Alice"))
state = workshop.create_initial_state(
workshop.create_draft_from_form(
display_name="恢复角色",
description="测试。",
personality="冷静",
scenario="测试。",
first_mes="你好。",
),
user=user,
)
state = workshop.generate_main_candidates(state)
loaded = workshop.load_workshop_run(state["run_dir"], user=user)
self.assertEqual(len(loaded["main_candidates"]), 4)
self.assertEqual(loaded["selected_candidate_index"], 0)
def test_partial_expression_pack_only_generates_missing_slots(self) -> None:
user = workshop.get_current_user(_Profile("alice", "Alice"))
state = workshop.create_initial_state(
workshop.create_draft_from_form(
display_name="续跑角色",
description="测试。",
personality="冷静",
scenario="测试。",
first_mes="你好。",
),
user=user,
)
run_dir = Path(state["run_dir"])
idle_dir = run_dir / "expressions_raw" / "idle"
idle_dir.mkdir(parents=True, exist_ok=True)
idle_path = idle_dir / "00.png"
idle_path.write_bytes(_png_bytes(0))
manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8"))
manifest["expression_assets_raw"] = {"idle": str(idle_path.relative_to(run_dir))}
(run_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False), encoding="utf-8")
self.probe_calls.clear()
state = workshop.load_workshop_run(run_dir, user=user)
state = workshop.generate_expression_pack(state)
self.assertEqual(set(state["expression_assets"]), set(workshop.EXPRESSIONS))
self.assertEqual(len(self.probe_calls), len(workshop.EXPRESSIONS) - 1)
def test_stats_event_summary(self) -> None:
user = workshop.get_current_user(_Profile("alice", "Alice"))
workshop.record_workshop_event(user, "generate_main_candidates", {"stage": "main_candidates", "success": True, "duration_seconds": 1.2, "character_id": "x"})
workshop.record_workshop_event(user, "generate_expression_pack", {"stage": "assets_ready", "success": False, "failure_reason": "boom", "character_id": "x"})
summary = workshop.summarize_workshop_stats()
self.assertEqual(summary["events"], 2)
self.assertEqual(summary["users"], 1)
self.assertEqual(summary["failures_by_stage"]["assets_ready"], 1)
def _fake_probe(self, **kwargs):
self.probe_calls.append(dict(kwargs))
batch_size = int(kwargs.get("batch_size", 1))
images = [_png_bytes(index) for index in range(batch_size)]
return {
"candidate_id": kwargs.get("candidate_id", "qwen_image"),
"status": "ok",
"image_count": batch_size,
"duration_seconds": 0.01,
"seed": kwargs.get("seed"),
"images": images,
}
def _png_bytes(index: int) -> bytes:
image = Image.new("RGB", (256, 384), (238, 246, 248))
draw = ImageDraw.Draw(image)
accent = [(45, 212, 191), (96, 165, 250), (244, 114, 182), (250, 204, 21)][index % 4]
draw.ellipse((76, 36, 180, 150), fill=(245, 222, 214))
draw.rectangle((84, 150, 172, 330), fill=(35, 45, 60))
draw.rectangle((112, 166, 144, 310), fill=accent)
buffer = io.BytesIO()
image.save(buffer, format="PNG")
return buffer.getvalue()
class _Profile:
def __init__(self, username: str, name: str):
self.username = username
self.name = name
if __name__ == "__main__":
unittest.main()