Files

182 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()