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
+205
View File
@@ -0,0 +1,205 @@
from fastapi import APIRouter, Depends, HTTPException, Body
from sqlmodel import Session
from typing import List, Dict
from pydantic import BaseModel
from app.core.database import get_session
from app.models.models import Project, GlobalConfig, Character, StoryboardItem
from app.schemas.schemas import ProjectCreate, ProjectUpdate, ProjectRead
from app.cruds import crud_project
from app.services.consistency_service import ConsistencyService
router = APIRouter()
@router.post("/", response_model=Project)
def create_project(project_in: ProjectCreate, session: Session = Depends(get_session)):
return crud_project.create_project(session, project_in)
@router.get("/", response_model=List[Project])
def read_projects(skip: int = 0, limit: int = 100, session: Session = Depends(get_session)):
return crud_project.get_projects(session, skip, limit)
@router.get("/{project_id}", response_model=ProjectRead)
def read_project(project_id: str, session: Session = Depends(get_session)):
project = crud_project.get_project(session, project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
@router.put("/{project_id}", response_model=Project)
def update_project(project_id: str, project_in: ProjectUpdate, session: Session = Depends(get_session)):
project = crud_project.get_project(session, project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return crud_project.update_project(session, project, project_in)
@router.delete("/{project_id}")
def delete_project(project_id: str, session: Session = Depends(get_session)):
project = crud_project.get_project(session, project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
crud_project.delete_project(session, project)
return {"ok": True}
# --- Data Management Endpoints ---
@router.put("/{project_id}/global_config")
def update_global_config(project_id: str, data: Dict = Body(...), session: Session = Depends(get_session)):
project = crud_project.get_project(session, project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
# 1. Update Project level fields first
# This ensures consistency between project.theme/language and global_config
if "language" in data:
project.language = data["language"]
if "style" in data:
project.theme = data["style"]
if "aspect_ratio" in data:
project.aspect_ratio = data["aspect_ratio"]
session.add(project)
session.commit()
session.refresh(project)
# 2. Update GlobalConfig in DB
config = crud_project.create_global_config(session, project_id, data)
# 3. Trigger consistency check (Propagate to all items)
consistency = ConsistencyService(session)
consistency.normalize_project(project_id)
return config
@router.put("/{project_id}/characters/{char_id}")
def update_character(project_id: str, char_id: int, data: Dict = Body(...), session: Session = Depends(get_session)):
char = session.get(Character, char_id)
if not char:
raise HTTPException(status_code=404, detail="Character not found")
char.data = data
session.add(char)
session.commit()
session.refresh(char)
# We might want to trigger consistency here too if character style changes,
# but primarily it's driven by global config.
return char
@router.put("/{project_id}/storyboard/{item_id}")
def update_storyboard_item(project_id: str, item_id: int, data: Dict = Body(...), session: Session = Depends(get_session)):
item = session.get(StoryboardItem, item_id)
if not item:
raise HTTPException(status_code=404, detail="Storyboard Item not found")
item.data = data
session.add(item)
session.commit()
session.refresh(item)
return item
@router.delete("/{project_id}/characters/{char_id}")
def delete_character(project_id: str, char_id: int, session: Session = Depends(get_session)):
char = session.get(Character, char_id)
if not char:
raise HTTPException(status_code=404, detail="Character not found")
session.delete(char)
session.commit()
return {"ok": True}
class MergeCharacterRequest(BaseModel):
target_char_id: int
source_char_ids: List[int]
@router.post("/{project_id}/characters/merge")
def merge_characters(
project_id: str,
request: MergeCharacterRequest,
session: Session = Depends(get_session)
):
target_char = session.get(Character, request.target_char_id)
if not target_char:
raise HTTPException(status_code=404, detail="Target character not found")
source_chars = []
for cid in request.source_char_ids:
c = session.get(Character, cid)
if c:
source_chars.append(c)
if not source_chars:
raise HTTPException(status_code=400, detail="No valid source characters found")
target_name = target_char.name
source_names = [c.name for c in source_chars]
# 1. Update Storyboard Items
# We need to scan all items and replace source names with target name
project = session.get(Project, project_id)
if project.storyboard_items:
for item in project.storyboard_items:
data = item.data
# 'characters' field in storyboard item data
# It can be a list of strings, or list of dicts with 'name' key, or a single string
chars = data.get("characters", [])
new_chars = []
modified = False
# Helper to normalize input to list
char_list = []
if isinstance(chars, str): char_list = [chars]
elif isinstance(chars, list): char_list = chars
for c_entry in char_list:
c_name = ""
if isinstance(c_entry, str): c_name = c_entry
elif isinstance(c_entry, dict): c_name = c_entry.get("name", "")
# Check if this name matches any source name
# Fuzzy match or exact? Let's do exact or containment for safety
# User said "Ma Laoguanjia" vs "Ma Guanjia".
# Ideally we replace if it matches one of the source characters' name EXACTLY or close enough?
# Since we selected source characters by ID, we know their names.
# Let's replace if the name in storyboard matches a source character name.
is_source = False
for src_name in source_names:
if src_name == c_name:
is_source = True
break
if is_source:
# Replace with target name
# If entry was dict, update name field? Or just use string?
# Let's keep format.
if isinstance(c_entry, str):
new_chars.append(target_name)
elif isinstance(c_entry, dict):
c_entry['name'] = target_name
new_chars.append(c_entry)
modified = True
else:
new_chars.append(c_entry)
if modified:
# Deduplicate if target name already existed?
# Simple dedup for strings
final_chars = []
seen = set()
for c in new_chars:
n = c if isinstance(c, str) else c.get("name", "")
if n not in seen:
final_chars.append(c)
seen.add(n)
data['characters'] = final_chars
item.data = data
session.add(item)
# 2. Delete Source Characters
for c in source_chars:
session.delete(c)
session.commit()
return {"ok": True, "merged_count": len(source_chars)}