Initial commit
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
def group_into_pages(scenes, mode="normal"):
|
||||
if mode == "tiny":
|
||||
return [[s] for s in scenes]
|
||||
pages = []
|
||||
temp = []
|
||||
for s in scenes:
|
||||
temp.append(s)
|
||||
if len(temp) == 3:
|
||||
pages.append(temp)
|
||||
temp = []
|
||||
if temp:
|
||||
pages.append(temp)
|
||||
return pages
|
||||
@@ -0,0 +1,137 @@
|
||||
import re
|
||||
from ai.character_memory import build_consistency_tokens
|
||||
|
||||
STYLES = {
|
||||
"lineart": {
|
||||
"positive": (
|
||||
"anime illustration, manga style, black and white, monochrome, "
|
||||
"clean ink linework, expressive characters, dynamic composition, "
|
||||
"professional manga art, single scene, full image"
|
||||
),
|
||||
"negative": (
|
||||
"color, colorful, coloured, vibrant, saturated, "
|
||||
"multiple panels, panel borders, panel grid, split panels, "
|
||||
"collage, triptych, diptych, "
|
||||
"realistic, photorealistic, photograph, 3d render, "
|
||||
"ugly, blurry, watermark, text, signature, lowres, "
|
||||
"bad anatomy, deformed, extra limbs"
|
||||
),
|
||||
"cfg": 7.0,
|
||||
},
|
||||
"manga": {
|
||||
"positive": (
|
||||
"single full-page manga illustration, full-bleed scene, "
|
||||
"black and white ink, screentone shading, "
|
||||
"expressive faces, detailed backgrounds, "
|
||||
"professional manga art, one continuous scene"
|
||||
),
|
||||
"negative": (
|
||||
"multiple panels, panel borders, panel grid, comic layout, split panels, "
|
||||
"panel dividers, gutters, multi-panel page, page layout, comic book grid, "
|
||||
"collage, triptych, diptych, "
|
||||
"lowres, bad anatomy, blurry, watermark, text, ugly, deformed, "
|
||||
"color, coloured, western comic style"
|
||||
),
|
||||
"cfg": 7.5,
|
||||
},
|
||||
}
|
||||
|
||||
MOOD_MAP = {
|
||||
"tense": "tense dramatic scene, characters look worried or scared",
|
||||
"sad": "sad melancholy scene, characters look downcast",
|
||||
"happy": "happy cheerful scene, characters smiling",
|
||||
"angry": "angry confrontational scene, characters look furious",
|
||||
"mysterious": "mysterious eerie scene, shadowy atmosphere",
|
||||
"neutral": "calm everyday scene",
|
||||
"romantic": "romantic gentle scene, soft atmosphere",
|
||||
"action": "action dynamic scene, characters in motion",
|
||||
"excited": "excited energetic scene, characters enthusiastic",
|
||||
"fearful": "fearful tense scene, characters look afraid",
|
||||
}
|
||||
|
||||
JUNK_NAMES = {
|
||||
"he", "she", "they", "him", "her", "them", "his", "hers", "their",
|
||||
"i", "me", "we", "us", "it", "you", "location", "unknown", "none",
|
||||
"character", "person", "man", "woman", "boy", "girl",
|
||||
"the man", "the woman", "the boy", "the girl", "the person",
|
||||
"a man", "a woman", "old man", "young man", "young woman",
|
||||
"his secretary", "his secretarys", "her secretary",
|
||||
"narrator", "voice", "someone", "anyone", "everyone",
|
||||
}
|
||||
|
||||
PLACEHOLDER_SETTINGS = {"location", "unknown", "none", "'location'", '"location"', ""}
|
||||
|
||||
POSSESSIVE_RE = re.compile(r"'s$|s'$", re.IGNORECASE)
|
||||
|
||||
def clean_text(s):
|
||||
s = s.replace("\\u0022", "").replace("\\u0027", "")
|
||||
s = s.replace('\\"', "").replace("\\'", "")
|
||||
s = re.sub(r'[\"\'`]', "", s)
|
||||
s = re.sub(r"\s+", " ", s).strip()
|
||||
return s
|
||||
|
||||
def is_valid_name(name):
|
||||
n = name.strip().lower()
|
||||
if not n or len(n) < 2:
|
||||
return False
|
||||
if n in JUNK_NAMES:
|
||||
return False
|
||||
if POSSESSIVE_RE.search(n):
|
||||
return False
|
||||
return True
|
||||
|
||||
def build_page_prompt(parsed_scenes, style="lineart"):
|
||||
style_def = STYLES.get(style, STYLES["lineart"])
|
||||
|
||||
if not parsed_scenes:
|
||||
return style_def["positive"], style_def["negative"]
|
||||
|
||||
all_characters = []
|
||||
visual_parts = []
|
||||
settings = []
|
||||
moods = []
|
||||
|
||||
for scene in parsed_scenes:
|
||||
if isinstance(scene, str):
|
||||
visual_parts.append(scene)
|
||||
continue
|
||||
for char in scene.get("characters", []):
|
||||
char = clean_text(char)
|
||||
if is_valid_name(char):
|
||||
all_characters.append(char)
|
||||
visual = clean_text(scene.get("visual_scene", ""))
|
||||
if visual:
|
||||
visual_parts.append(visual)
|
||||
setting = clean_text(scene.get("setting", ""))
|
||||
if setting.lower() not in PLACEHOLDER_SETTINGS:
|
||||
settings.append(setting)
|
||||
mood = scene.get("mood", "neutral").strip().lower()
|
||||
if mood:
|
||||
moods.append(mood)
|
||||
|
||||
unique_chars = list(dict.fromkeys(all_characters))
|
||||
char_tokens = build_consistency_tokens(unique_chars)
|
||||
|
||||
dominant_mood = moods[0] if moods else "neutral"
|
||||
mood_desc = MOOD_MAP.get(dominant_mood, MOOD_MAP["neutral"])
|
||||
|
||||
setting_str = settings[0][:60] if settings else ""
|
||||
scene_str = visual_parts[0][:80] if visual_parts else ""
|
||||
|
||||
style_prefix = (
|
||||
"(anime style:1.4), (manga illustration:1.3), "
|
||||
"(black and white:1.3), (monochrome:1.2), "
|
||||
) if style == "lineart" else ""
|
||||
|
||||
parts = [style_prefix + style_def["positive"], mood_desc]
|
||||
if setting_str:
|
||||
parts.append(setting_str)
|
||||
if char_tokens:
|
||||
parts.append(char_tokens)
|
||||
if scene_str:
|
||||
parts.append(scene_str)
|
||||
|
||||
return ", ".join(parts), style_def["negative"]
|
||||
|
||||
def get_cfg(style="lineart"):
|
||||
return STYLES.get(style, STYLES["lineart"])["cfg"]
|
||||
@@ -0,0 +1,115 @@
|
||||
import textwrap
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
|
||||
MAX_BUBBLES = 3
|
||||
MAX_LINE_WIDTH = 22
|
||||
BUBBLE_PADDING = 14
|
||||
FONT_SIZE = 22
|
||||
TAIL_SIZE = 14
|
||||
|
||||
def get_font(size=FONT_SIZE):
|
||||
candidates = [
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
"/System/Library/Fonts/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
return ImageFont.truetype(path, size)
|
||||
except Exception:
|
||||
pass
|
||||
return ImageFont.load_default()
|
||||
|
||||
def wrap_text(text, max_width=MAX_LINE_WIDTH):
|
||||
return textwrap.fill(text, max_width)
|
||||
|
||||
def draw_bubble(draw, x, y, w, h, tail_side="bottom"):
|
||||
r = 16
|
||||
draw.rounded_rectangle([x, y, x+w, y+h], radius=r, fill="white", outline="black", width=3)
|
||||
|
||||
if tail_side == "bottom":
|
||||
tx = x + w // 2
|
||||
ty = y + h
|
||||
draw.polygon([
|
||||
(tx - TAIL_SIZE, ty - 4),
|
||||
(tx + TAIL_SIZE, ty - 4),
|
||||
(tx, ty + TAIL_SIZE),
|
||||
], fill="white")
|
||||
draw.line([(tx - TAIL_SIZE, ty - 4), (tx, ty + TAIL_SIZE)], fill="black", width=3)
|
||||
draw.line([(tx + TAIL_SIZE, ty - 4), (tx, ty + TAIL_SIZE)], fill="black", width=3)
|
||||
elif tail_side == "top":
|
||||
tx = x + w // 2
|
||||
ty = y
|
||||
draw.polygon([
|
||||
(tx - TAIL_SIZE, ty + 4),
|
||||
(tx + TAIL_SIZE, ty + 4),
|
||||
(tx, ty - TAIL_SIZE),
|
||||
], fill="white")
|
||||
draw.line([(tx - TAIL_SIZE, ty + 4), (tx, ty - TAIL_SIZE)], fill="black", width=3)
|
||||
draw.line([(tx + TAIL_SIZE, ty + 4), (tx, ty - TAIL_SIZE)], fill="black", width=3)
|
||||
|
||||
def add_speech_bubbles(img_path, dialogue):
|
||||
if not dialogue:
|
||||
return
|
||||
|
||||
entries = [d for d in dialogue if d.get("line", "").strip()][:MAX_BUBBLES]
|
||||
if not entries:
|
||||
return
|
||||
|
||||
img = Image.open(img_path).convert("RGB")
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = get_font(FONT_SIZE)
|
||||
small_font = get_font(FONT_SIZE - 6)
|
||||
|
||||
iw, ih = img.size
|
||||
margin = 18
|
||||
|
||||
zones = [
|
||||
ih // 8,
|
||||
ih * 5 // 8,
|
||||
ih // 4,
|
||||
]
|
||||
|
||||
for i, entry in enumerate(entries):
|
||||
speaker = entry.get("speaker", "").strip()
|
||||
line = entry.get("line", "").strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
wrapped = wrap_text(line)
|
||||
|
||||
bbox = draw.textbbox((0, 0), wrapped, font=font)
|
||||
text_w = bbox[2] - bbox[0]
|
||||
text_h = bbox[3] - bbox[1]
|
||||
|
||||
if speaker and speaker != "?":
|
||||
spk_bbox = draw.textbbox((0, 0), speaker, font=small_font)
|
||||
spk_h = spk_bbox[3] - spk_bbox[1] + 4
|
||||
else:
|
||||
spk_h = 0
|
||||
|
||||
bw = min(text_w + BUBBLE_PADDING * 2, iw - margin * 2)
|
||||
bh = text_h + spk_h + BUBBLE_PADDING * 2
|
||||
|
||||
x_offset = margin if i % 2 == 0 else iw - bw - margin
|
||||
by = zones[i % len(zones)]
|
||||
|
||||
by = max(margin, min(by, ih - bh - margin))
|
||||
|
||||
tail = "bottom" if by < ih // 2 else "top"
|
||||
draw_bubble(draw, x_offset, by, bw, bh, tail_side=tail)
|
||||
|
||||
if speaker and speaker != "?":
|
||||
draw.text((x_offset + BUBBLE_PADDING, by + BUBBLE_PADDING), speaker, font=small_font, fill="#444444")
|
||||
|
||||
draw.text(
|
||||
(x_offset + BUBBLE_PADDING, by + BUBBLE_PADDING + spk_h),
|
||||
wrapped,
|
||||
font=font,
|
||||
fill="black",
|
||||
)
|
||||
|
||||
img.save(img_path)
|
||||
Reference in New Issue
Block a user