feat: initial commit of AI Comic Generator

This commit is contained in:
p
2026-01-17 15:26:14 +08:00
commit 06d76e0e69
278 changed files with 8612 additions and 0 deletions
+181
View File
@@ -0,0 +1,181 @@
import re
import json
import copy
from typing import List, Dict, Any
from sqlmodel import Session, select
from app.models.models import Project, Character, StoryboardItem, GlobalConfig
class ConsistencyService:
def __init__(self, session: Session):
self.session = session
def normalize_project(self, project_id: str):
"""
Normalizes the project's data (characters, storyboard) based on the global config.
This mirrors the logic in comic_generator.py.
"""
project = self.session.get(Project, project_id)
if not project:
return
# Fetch all related data
# Note: relationships are loaded if accessed, but let's be explicit if needed.
# ProjectRead should handle loading, but here we work with ORM objects.
global_config = project.global_config
characters = project.characters
storyboard_items = sorted(project.storyboard_items, key=lambda x: x.sequence)
master_style = None
master_meta = {}
# Priority 0: Comic Config
if global_config and global_config.data:
config_data = global_config.data
master_style = config_data.get("style")
# Explicitly map all fields we want to sync
master_meta = {
"style": master_style,
"bubble_style": config_data.get("bubble_style"),
"narration_style": config_data.get("narration_style"),
"border_style": config_data.get("border_style"),
"gutter_style": config_data.get("gutter_style"),
"layout_settings": config_data.get("layout_settings"),
"aspect_ratio": config_data.get("aspect_ratio", "16:9"),
"language": config_data.get("language", "English")
}
# Priority 1: First Character Sheet (if no config style)
if not master_style and characters:
# Try to find one with style
for char in characters:
if char.data.get("style"):
master_style = char.data.get("style")
break
# Priority 2: First Story Block
if storyboard_items:
first_item = storyboard_items[0]
first_meta = first_item.data.get("meta_info", {})
if not master_style:
master_style = first_meta.get("style")
if not master_meta:
master_meta = first_meta.copy()
# Build Character Registry
known_characters = {}
for char in characters:
full_name = char.name
if not full_name: continue
keywords = [full_name]
simplified = re.split(r'[\(]', full_name)[0].strip()
if simplified and simplified != full_name:
keywords.append(simplified)
for kw in keywords:
if kw:
known_characters[kw] = full_name
# Apply Normalization
# Even if master_style is None, we might still have layout_settings to sync
# So we check if we have ANY master_meta to apply
if master_meta:
import logging
logger = logging.getLogger(__name__)
logger.info(f"Normalizing project {project_id} with master config: {json.dumps(master_meta, ensure_ascii=False)}")
# 1. Normalize Characters
for char in characters:
char_data = copy.deepcopy(char.data)
if not isinstance(char_data, dict):
char_data = dict(char_data)
meta = char_data.get("meta_info", {})
# Sync Core Fields
if master_meta.get("style"): meta["style"] = master_meta.get("style")
if master_meta.get("language"): meta["language"] = master_meta.get("language")
if master_meta.get("aspect_ratio"): meta["aspect_ratio"] = master_meta.get("aspect_ratio")
char_data["meta_info"] = meta
# Cleanup top-level legacy fields
char_data.pop("style", None)
char_data.pop("language", None)
char_data.pop("Language", None)
char.data = char_data
self.session.add(char)
# 2. Normalize Storyboard Items
total_volumes = len(storyboard_items)
for i, item in enumerate(storyboard_items):
# Use deepcopy to ensure we don't mutate the original object in place before assignment
# and to ensure SQLAlchemy detects the change when we reassign.
item_data = copy.deepcopy(item.data)
if not isinstance(item_data, dict):
item_data = dict(item_data)
# Check for missing characters
plot_text = json.dumps(item_data.get("plot_breakdown", []), ensure_ascii=False)
current_chars = item_data.get("characters", [])
if isinstance(current_chars, str):
current_chars = [current_chars]
if not isinstance(current_chars, list):
current_chars = []
found_missing = []
for kw, full_name in known_characters.items():
if kw in plot_text:
is_present = False
for char_name in current_chars:
if kw in char_name or char_name in full_name:
is_present = True
break
if not is_present:
if full_name not in current_chars and full_name not in found_missing:
found_missing.append(full_name)
if found_missing:
current_chars.extend(found_missing)
item_data["characters"] = current_chars
# Sync Meta Info
meta = item_data.get("meta_info", {})
# We want to preserve existing fields in meta (like volume) but overwrite style configs
# master_meta has style, bubble_style, etc.
# Force update fields from master_meta even if they exist in meta
# But careful with 'None' values in master_meta (though we constructed it from config)
for key, value in master_meta.items():
if key == "volume": continue
# Special handling for boolean values (like False in show_panel_numbers)
# "if value is not None" is correct for booleans.
# For nested dictionaries (like layout_settings), we should merge to preserve other keys.
if value is not None:
if isinstance(value, dict) and isinstance(meta.get(key), dict):
meta[key].update(value)
else:
meta[key] = value
# If value is None but key exists in master_meta keys (explicitly set to null), we might want to unset it?
# But our master_meta construction uses .get() defaults, so None usually means "not in config".
# Ensure Volume format
meta["volume"] = f"{i+1}/{total_volumes}"
item_data["meta_info"] = meta
logger.info(f"Updated Item {i+1} meta: {json.dumps(meta, ensure_ascii=False)}")
item.data = item_data
self.session.add(item)
self.session.commit()