yukee1992 commited on
Commit
5c75b32
·
verified ·
1 Parent(s): a5c960b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -517
app.py CHANGED
@@ -1,7 +1,7 @@
1
  import gradio as gr
2
  import torch
3
- from diffusers import StableDiffusionPipeline, EulerAncestralDiscreteScheduler, StableDiffusionInpaintPipeline
4
- from PIL import Image, ImageDraw
5
  import io
6
  import requests
7
  import os
@@ -9,7 +9,7 @@ from datetime import datetime
9
  import re
10
  import time
11
  import json
12
- from typing import List, Optional, Dict, Tuple
13
  from fastapi import FastAPI, HTTPException, BackgroundTasks
14
  from pydantic import BaseModel
15
  import gc
@@ -19,7 +19,6 @@ import uuid
19
  import hashlib
20
  from enum import Enum
21
  import random
22
- import numpy as np
23
 
24
  # External OCI API URL - YOUR BUCKET SAVING API
25
  OCI_API_BASE_URL = "https://yukee1992-oci-story-book.hf.space"
@@ -30,7 +29,7 @@ os.makedirs(PERSISTENT_IMAGE_DIR, exist_ok=True)
30
  print(f"📁 Created local image directory: {PERSISTENT_IMAGE_DIR}")
31
 
32
  # Initialize FastAPI app
33
- app = FastAPI(title="Dual-Pipeline Storybook Generator API")
34
 
35
  # Add CORS middleware
36
  from fastapi.middleware.cors import CORSMiddleware
@@ -45,9 +44,6 @@ app.add_middleware(
45
  # Job Status Enum
46
  class JobStatus(str, Enum):
47
  PENDING = "pending"
48
- GENERATING_CHARACTERS = "generating_characters"
49
- GENERATING_BACKGROUNDS = "generating_backgrounds"
50
- COMPOSING_SCENES = "composing_scenes"
51
  PROCESSING = "processing"
52
  COMPLETED = "completed"
53
  FAILED = "failed"
@@ -56,26 +52,23 @@ class JobStatus(str, Enum):
56
  class StoryScene(BaseModel):
57
  visual: str
58
  text: str
59
- characters_present: List[str] = []
60
- scene_type: str = "general"
61
- background_context: str = ""
62
 
63
  class CharacterDescription(BaseModel):
64
  name: str
65
  description: str
66
- visual_prompt: str = ""
67
- key_features: List[str] = []
68
- pose_reference: str = "standing naturally"
69
 
70
  class StorybookRequest(BaseModel):
71
  story_title: str
72
  scenes: List[StoryScene]
73
  characters: List[CharacterDescription] = []
74
- model_choice: str = "sd-1.5" # CHANGED: Default to working model
75
  style: str = "childrens_book"
76
  callback_url: Optional[str] = None
77
- consistency_seed: Optional[int] = None
78
- pipeline_type: str = "standard"
79
 
80
  class JobStatusResponse(BaseModel):
81
  job_id: str
@@ -86,14 +79,16 @@ class JobStatusResponse(BaseModel):
86
  created_at: float
87
  updated_at: float
88
 
89
- # UPDATED MODEL CHOICES - Only use working models
90
  MODEL_CHOICES = {
91
- "sd-1.5": "runwayml/stable-diffusion-v1-5", # Most reliable
92
- "openjourney": "prompthero/openjourney", # Public & free
93
- "sd-2.1": "stabilityai/stable-diffusion-2-1", # Public alternative
 
 
94
  }
95
 
96
- # FALLBACK CHARACTER TEMPLATES
97
  FALLBACK_CHARACTER_TEMPLATES = {
98
  "Sparkle the Star Cat": {
99
  "visual_prompt": "small white kitten with distinctive silver star-shaped spots on fur, big golden eyes, shiny blue collar with star charm, playful expression",
@@ -102,20 +97,22 @@ FALLBACK_CHARACTER_TEMPLATES = {
102
  "Benny the Bunny": {
103
  "visual_prompt": "fluffy brown rabbit with long ears, bright green eyes, red scarf around neck, cheerful expression",
104
  "key_features": ["red scarf", "long ears", "green eyes", "brown fur"],
 
 
 
 
105
  }
106
  }
107
 
108
  # GLOBAL STORAGE
109
  job_storage = {}
110
  model_cache = {}
111
- inpaint_pipe = None
112
  current_model_name = None
113
  current_pipe = None
114
  model_lock = threading.Lock()
115
 
116
- # FIXED MODEL LOADING - With fallback like old script
117
- def load_model(model_name="sd-1.5"):
118
- """Thread-safe model loading with FALLBACK like old working script"""
119
  global model_cache, current_model_name, current_pipe
120
 
121
  with model_lock:
@@ -124,9 +121,9 @@ def load_model(model_name="sd-1.5"):
124
  current_model_name = model_name
125
  return current_pipe
126
 
127
- print(f"🔄 Loading model: {model_name}")
128
  try:
129
- model_id = MODEL_CHOICES.get(model_name, "runwayml/stable-diffusion-v1-5")
130
 
131
  pipe = StableDiffusionPipeline.from_pretrained(
132
  model_id,
@@ -142,57 +139,24 @@ def load_model(model_name="sd-1.5"):
142
  current_pipe = pipe
143
  current_model_name = model_name
144
 
145
- print(f"✅ Model loaded: {model_name}")
146
  return pipe
147
 
148
  except Exception as e:
149
  print(f"❌ Model loading failed: {e}")
150
- # FALLBACK TO SD 1.5 LIKE OLD SCRIPT
151
- print("🔄 Falling back to stable-diffusion-v1-5")
152
- try:
153
- fallback_pipe = StableDiffusionPipeline.from_pretrained(
154
- "runwayml/stable-diffusion-v1-5",
155
- torch_dtype=torch.float32,
156
- safety_checker=None,
157
- requires_safety_checker=False
158
- ).to("cpu")
159
- model_cache["sd-1.5"] = fallback_pipe
160
- return fallback_pipe
161
- except Exception as fallback_error:
162
- print(f"❌ Fallback model also failed: {fallback_error}")
163
- return None
164
-
165
- def load_inpaint_model():
166
- """Load inpainting model for composition"""
167
- global inpaint_pipe
168
-
169
- if inpaint_pipe is not None:
170
- return inpaint_pipe
171
-
172
- print("🔄 Loading inpainting model...")
173
- try:
174
- inpaint_pipe = StableDiffusionInpaintPipeline.from_pretrained(
175
- "runwayml/stable-diffusion-inpainting",
176
- torch_dtype=torch.float32,
177
- safety_checker=None,
178
- requires_safety_checker=False
179
- )
180
- inpaint_pipe = inpaint_pipe.to("cpu")
181
- print("✅ Inpainting model loaded")
182
- return inpaint_pipe
183
- except Exception as e:
184
- print(f"❌ Inpainting model failed: {e}")
185
- return None
186
-
187
- # Initialize models
188
- print("🚀 Initializing Dual-Pipeline Storybook Generator API...")
189
- load_model("sd-1.5") # CHANGED: Initialize with working model
190
- print("✅ Models loaded and ready!")
191
 
192
- # ============================================================================
193
- # CHARACTER PROCESSING FUNCTIONS (from old script - working)
194
- # ============================================================================
 
195
 
 
196
  def process_character_descriptions(characters_from_request):
197
  """Process character descriptions from n8n and create consistency templates"""
198
  character_templates = {}
@@ -200,11 +164,14 @@ def process_character_descriptions(characters_from_request):
200
  for character in characters_from_request:
201
  char_name = character.name
202
 
 
203
  if character.visual_prompt:
204
  visual_prompt = character.visual_prompt
205
  else:
 
206
  visual_prompt = generate_visual_prompt_from_description(character.description, char_name)
207
 
 
208
  if character.key_features:
209
  key_features = character.key_features
210
  else:
@@ -214,7 +181,7 @@ def process_character_descriptions(characters_from_request):
214
  "visual_prompt": visual_prompt,
215
  "key_features": key_features,
216
  "consistency_keywords": f"consistent character, same {char_name.split()[-1].lower()}, maintaining appearance",
217
- "source": "n8n_request"
218
  }
219
 
220
  print(f"✅ Processed {len(character_templates)} characters from n8n request")
@@ -222,8 +189,10 @@ def process_character_descriptions(characters_from_request):
222
 
223
  def generate_visual_prompt_from_description(description, character_name):
224
  """Generate a visual prompt from character description"""
 
225
  description_lower = description.lower()
226
 
 
227
  species_keywords = ["kitten", "cat", "rabbit", "bunny", "turtle", "dog", "bird", "dragon", "bear", "fox"]
228
  species = "character"
229
  for keyword in species_keywords:
@@ -231,18 +200,21 @@ def generate_visual_prompt_from_description(description, character_name):
231
  species = keyword
232
  break
233
 
 
234
  color_keywords = ["white", "black", "brown", "red", "blue", "green", "yellow", "golden", "silver", "orange"]
235
  colors = []
236
  for color in color_keywords:
237
  if color in description_lower:
238
  colors.append(color)
239
 
 
240
  feature_keywords = ["spots", "stripes", "collar", "scarf", "shell", "wings", "horn", "tail", "ears", "eyes"]
241
  features = []
242
  for feature in feature_keywords:
243
  if feature in description_lower:
244
  features.append(feature)
245
 
 
246
  visual_prompt_parts = []
247
  if colors:
248
  visual_prompt_parts.append(f"{' '.join(colors)} {species}")
@@ -254,6 +226,7 @@ def generate_visual_prompt_from_description(description, character_name):
254
  if features:
255
  visual_prompt_parts.append(f"with {', '.join(features)}")
256
 
 
257
  trait_keywords = ["playful", "brave", "curious", "kind", "cheerful", "wise", "calm", "friendly"]
258
  traits = [trait for trait in trait_keywords if trait in description_lower]
259
  if traits:
@@ -269,6 +242,7 @@ def extract_key_features_from_description(description):
269
  description_lower = description.lower()
270
  key_features = []
271
 
 
272
  feature_patterns = [
273
  r"(\w+)\s+(?:spots|stripes|marks)",
274
  r"(\w+)\s+(?:collar|scarf|ribbon)",
@@ -280,8 +254,10 @@ def extract_key_features_from_description(description):
280
  matches = re.findall(pattern, description_lower)
281
  key_features.extend(matches)
282
 
 
283
  key_features = list(set(key_features))[:3]
284
 
 
285
  if not key_features:
286
  if any(word in description_lower for word in ["kitten", "cat"]):
287
  key_features = ["whiskers", "tail", "paws"]
@@ -295,25 +271,11 @@ def extract_key_features_from_description(description):
295
  print(f"🔧 Extracted key features: {key_features}")
296
  return key_features
297
 
298
- def extract_characters_from_visual(visual_description, available_characters):
299
- """Extract character names from visual description using available characters"""
300
- characters = []
301
- visual_lower = visual_description.lower()
302
-
303
- for char_name in available_characters:
304
- char_identifier = char_name.split()[0].lower()
305
- if char_identifier in visual_lower or char_name.lower() in visual_lower:
306
- characters.append(char_name)
307
-
308
- return characters
309
-
310
- # ============================================================================
311
- # STANDARD PIPELINE FUNCTIONS (from old script - working)
312
- # ============================================================================
313
-
314
  def enhance_prompt_with_characters(scene_visual, characters_present, character_templates, style="childrens_book", scene_number=1):
315
  """Create prompts that maintain character consistency using dynamic templates"""
316
 
 
317
  character_descriptions = []
318
  consistency_keywords = []
319
 
@@ -323,9 +285,11 @@ def enhance_prompt_with_characters(scene_visual, characters_present, character_t
323
  character_descriptions.append(f"{char_name}: {char_data['visual_prompt']}")
324
  consistency_keywords.append(char_data['consistency_keywords'])
325
  else:
 
326
  character_descriptions.append(f"{char_name}: distinctive character")
327
  consistency_keywords.append(f"consistent {char_name}")
328
 
 
329
  style_templates = {
330
  "childrens_book": "children's book illustration, watercolor style, soft colors, whimsical, magical, storybook art, professional illustration",
331
  "realistic": "photorealistic, detailed, natural lighting, professional photography",
@@ -335,6 +299,7 @@ def enhance_prompt_with_characters(scene_visual, characters_present, character_t
335
 
336
  style_prompt = style_templates.get(style, style_templates["childrens_book"])
337
 
 
338
  character_context = ". ".join(character_descriptions)
339
  consistency_context = ", ".join(consistency_keywords)
340
 
@@ -345,6 +310,7 @@ def enhance_prompt_with_characters(scene_visual, characters_present, character_t
345
  f"Scene {scene_number} of storybook series. "
346
  )
347
 
 
348
  quality_boosters = [
349
  "consistent character design", "maintain identical features",
350
  "same characters throughout", "continuous visual narrative",
@@ -354,6 +320,7 @@ def enhance_prompt_with_characters(scene_visual, characters_present, character_t
354
 
355
  enhanced_prompt += ", ".join(quality_boosters)
356
 
 
357
  negative_prompt = (
358
  "inconsistent characters, different appearances, changing features, "
359
  "multiple versions of same character, inconsistent art style, "
@@ -363,30 +330,60 @@ def enhance_prompt_with_characters(scene_visual, characters_present, character_t
363
 
364
  return enhanced_prompt, negative_prompt
365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  def generate_consistent_image(prompt, model_choice, style, characters_present, character_templates, scene_number, consistency_seed=None):
367
  """Generate image with character consistency measures using dynamic templates"""
368
 
 
369
  enhanced_prompt, negative_prompt = enhance_prompt_with_characters(
370
  prompt, characters_present, character_templates, style, scene_number
371
  )
372
 
 
373
  if consistency_seed:
374
  base_seed = consistency_seed
375
  else:
376
  base_seed = hash("".join(characters_present)) % 1000000 if characters_present else random.randint(1000, 9999)
377
 
 
378
  scene_seed = base_seed + scene_number
379
 
380
  try:
381
  pipe = load_model(model_choice)
382
- if pipe is None:
383
- raise Exception("Model not available")
384
 
385
  image = pipe(
386
  prompt=enhanced_prompt,
387
  negative_prompt=negative_prompt,
388
- num_inference_steps=35,
389
- guidance_scale=7.5,
390
  width=768,
391
  height=768,
392
  generator=torch.Generator(device="cpu").manual_seed(scene_seed)
@@ -402,447 +399,122 @@ def generate_consistent_image(prompt, model_choice, style, characters_present, c
402
  print(f"❌ Consistent generation failed: {str(e)}")
403
  raise
404
 
405
- # ============================================================================
406
- # SIMPLIFIED ENHANCED PIPELINE (Basic composition without complex models)
407
- # ============================================================================
 
408
 
409
- def generate_character_image(character: CharacterDescription, model_choice: str, style: str, seed: int = None) -> Image.Image:
410
- """Generate a single character with simple background"""
411
-
412
- character_prompt = f"{character.visual_prompt or character.description}, {character.pose_reference}, full body character, children's book character design"
413
-
414
- character_prompt = re.sub(r'\s+', ' ', character_prompt).strip()
415
-
416
- negative_prompt = "background, scenery, environment, other characters, blurry, low quality"
417
-
418
- pipe = load_model(model_choice)
419
- if pipe is None:
420
- raise Exception("Model not available")
421
-
422
- if seed is None:
423
- seed = hash(character.name) % 1000000
424
-
425
- generator = torch.Generator(device="cpu").manual_seed(seed)
426
-
427
- image = pipe(
428
- prompt=character_prompt,
429
- negative_prompt=negative_prompt,
430
- num_inference_steps=25, # Reduced for speed
431
- guidance_scale=7.0,
432
- width=512,
433
- height=768,
434
- generator=generator
435
- ).images[0]
436
-
437
- print(f"✅ Generated character: {character.name}")
438
- return image
439
 
440
- def generate_scene_background(scene: StoryScene, model_choice: str, style: str, seed: int = None) -> Image.Image:
441
- """Generate scene background without characters"""
442
-
443
- background_prompt = f"{scene.visual} {scene.background_context}, empty scene, no characters, background environment, children's book background"
444
-
445
- background_prompt = re.sub(r'\s+', ' ', background_prompt).strip()
446
-
447
- negative_prompt = "characters, people, animals, person, human, animal, blurry, low quality"
448
-
449
- pipe = load_model(model_choice)
450
- if pipe is None:
451
- raise Exception("Model not available")
452
-
453
- if seed is None:
454
- seed = random.randint(1000, 9999)
455
-
456
- generator = torch.Generator(device="cpu").manual_seed(seed)
457
-
458
- image = pipe(
459
- prompt=background_prompt,
460
- negative_prompt=negative_prompt,
461
- num_inference_steps=25, # Reduced for speed
462
- guidance_scale=7.0,
463
- width=768,
464
- height=768,
465
- generator=generator
466
- ).images[0]
467
-
468
- print(f"✅ Generated background for scene")
469
- return image
470
-
471
- def compose_scene_with_characters(background: Image.Image, character_images: Dict[str, Image.Image],
472
- characters_present: List[str], scene_context: str) -> Image.Image:
473
- """Simple composition by placing characters on background"""
474
-
475
- final_image = background.copy()
476
-
477
- # Simple positioning
478
- positions = []
479
- num_chars = len(characters_present)
480
-
481
- if num_chars == 1:
482
- positions.append((284, 300, 200, 300)) # Center
483
- elif num_chars == 2:
484
- positions.extend([(184, 300, 200, 300), (484, 300, 200, 300)]) # Left & right
485
- else:
486
- for i in range(num_chars):
487
- x = 150 + (i % 3) * 200
488
- y = 250 + (i // 3) * 200
489
- positions.append((x, y, 180, 270))
490
-
491
- for i, char_name in enumerate(characters_present):
492
- if i >= len(positions) or char_name not in character_images:
493
- continue
494
-
495
- char_image = character_images[char_name]
496
- x, y, width, height = positions[i]
497
-
498
- char_resized = char_image.resize((width, height))
499
- final_image.paste(char_resized, (x, y), char_resized)
500
-
501
- return final_image
502
-
503
- # ============================================================================
504
- # OCI BUCKET FUNCTIONS (from old script)
505
- # ============================================================================
506
-
507
- def save_to_oci_bucket(file_data, filename, story_title, file_type="image", subfolder=""):
508
- """Save files to OCI bucket"""
509
  try:
510
- api_url = f"{OCI_API_BASE_URL}/api/upload"
 
 
511
 
512
- if subfolder:
513
- full_subfolder = f'stories/{story_title}/{subfolder}'
514
- else:
515
- full_subfolder = f'stories/{story_title}'
516
-
517
- mime_type = "image/png" if file_type == "image" else "text/plain"
518
- files = {'file': (filename, file_data, mime_type)}
519
- data = {
520
- 'project_id': 'storybook-library',
521
- 'subfolder': full_subfolder
522
- }
523
 
524
- response = requests.post(api_url, files=files, data=data, timeout=30)
 
 
525
 
526
- print(f"📨 OCI API Response: {response.status_code}")
527
 
528
- if response.status_code == 200:
529
- result = response.json()
530
- if result['status'] == 'success':
531
- return result.get('file_url', 'Unknown URL')
532
- else:
533
- raise Exception(f"OCI API Error: {result.get('message', 'Unknown error')}")
534
- else:
535
- raise Exception(f"HTTP Error: {response.status_code}")
536
-
537
  except Exception as e:
538
- raise Exception(f"OCI upload failed: {str(e)}")
539
-
540
- # ============================================================================
541
- # JOB MANAGEMENT (from old script with enhancements)
542
- # ============================================================================
543
-
544
- def create_job(story_request: StorybookRequest) -> str:
545
- job_id = str(uuid.uuid4())
546
-
547
- character_templates = process_character_descriptions(story_request.characters)
548
-
549
- job_storage[job_id] = {
550
- "status": JobStatus.PENDING,
551
- "progress": 0,
552
- "message": "Job created and queued",
553
- "request": story_request.dict(),
554
- "result": None,
555
- "created_at": time.time(),
556
- "updated_at": time.time(),
557
- "pages": [],
558
- "character_templates": character_templates,
559
- }
560
-
561
- print(f"📝 Created job {job_id} for story: {story_request.story_title}")
562
- print(f"🚀 Pipeline type: {story_request.pipeline_type}")
563
-
564
- return job_id
565
-
566
- def update_job_status(job_id: str, status: JobStatus, progress: int, message: str, result=None):
567
- if job_id not in job_storage:
568
- return False
569
-
570
- job_storage[job_id].update({
571
- "status": status,
572
- "progress": progress,
573
- "message": message,
574
- "updated_at": time.time()
575
- })
576
-
577
- if result:
578
- job_storage[job_id]["result"] = result
579
-
580
- job_data = job_storage[job_id]
581
- request_data = job_data["request"]
582
-
583
- if request_data.get("callback_url"):
584
- try:
585
- callback_url = request_data["callback_url"]
586
-
587
- callback_data = {
588
- "job_id": job_id,
589
- "status": status.value,
590
- "progress": progress,
591
- "message": message,
592
- "story_title": request_data["story_title"],
593
- "total_scenes": len(request_data["scenes"]),
594
- "total_characters": len(request_data["characters"]),
595
- "pipeline_type": request_data.get("pipeline_type", "standard"),
596
- "timestamp": time.time(),
597
- }
598
-
599
- headers = {'Content-Type': 'application/json'}
600
- response = requests.post(callback_url, json=callback_data, headers=headers, timeout=30)
601
- print(f"📢 Callback sent: Status {response.status_code}")
602
-
603
- except Exception as e:
604
- print(f"⚠️ Callback failed: {str(e)}")
605
-
606
- return True
607
 
608
- # ============================================================================
609
- # BACKGROUND TASKS - SIMPLIFIED
610
- # ============================================================================
 
 
 
 
 
 
 
 
611
 
612
- def generate_storybook_standard(job_id: str):
613
- """Standard pipeline background task"""
614
  try:
615
- job_data = job_storage[job_id]
616
- story_request_data = job_data["request"]
617
- story_request = StorybookRequest(**story_request_data)
618
- character_templates = job_data["character_templates"]
619
-
620
- print(f"🎬 Starting STANDARD pipeline for job {job_id}")
621
-
622
- update_job_status(job_id, JobStatus.PROCESSING, 5, "Starting storybook generation...")
623
-
624
- total_scenes = len(story_request.scenes)
625
- generated_pages = []
626
- start_time = time.time()
627
-
628
- for i, scene in enumerate(story_request.scenes):
629
- progress = 5 + int((i / total_scenes) * 90)
630
-
631
- characters_present = []
632
- if hasattr(scene, 'characters_present') and scene.characters_present:
633
- characters_present = scene.characters_present
634
- else:
635
- available_chars = [char.name for char in story_request.characters]
636
- characters_present = extract_characters_from_visual(scene.visual, available_chars)
637
-
638
- update_job_status(
639
- job_id,
640
- JobStatus.PROCESSING,
641
- progress,
642
- f"Generating page {i+1}/{total_scenes}..."
643
- )
644
-
645
- try:
646
- print(f"🖼️ Generating page {i+1}")
647
-
648
- image = generate_consistent_image(
649
- scene.visual,
650
- story_request.model_choice,
651
- story_request.style,
652
- characters_present,
653
- character_templates,
654
- i + 1,
655
- story_request.consistency_seed
656
- )
657
-
658
- img_bytes = io.BytesIO()
659
- image.save(img_bytes, format='PNG')
660
- image_url = save_to_oci_bucket(
661
- img_bytes.getvalue(),
662
- f"page_{i+1:03d}.png",
663
- story_request.story_title,
664
- "image"
665
- )
666
-
667
- text_url = save_to_oci_bucket(
668
- scene.text.encode('utf-8'),
669
- f"page_{i+1:03d}.txt",
670
- story_request.story_title,
671
- "text"
672
- )
673
-
674
- page_data = {
675
- "page_number": i + 1,
676
- "image_url": image_url,
677
- "text_url": text_url,
678
- "text_content": scene.text,
679
- }
680
- generated_pages.append(page_data)
681
-
682
- print(f"✅ Page {i+1} completed")
683
-
684
- except Exception as e:
685
- error_msg = f"Failed to generate page {i+1}: {str(e)}"
686
- print(f"❌ {error_msg}")
687
- update_job_status(job_id, JobStatus.FAILED, 0, error_msg)
688
- return
689
-
690
- generation_time = time.time() - start_time
691
-
692
- result = {
693
- "story_title": story_request.story_title,
694
- "total_pages": total_scenes,
695
- "generated_pages": len(generated_pages),
696
- "generation_time": round(generation_time, 2),
697
- "pipeline_used": "standard",
698
- "pages": generated_pages
699
  }
700
-
701
- update_job_status(
702
- job_id,
703
- JobStatus.COMPLETED,
704
- 100,
705
- f"🎉 Standard pipeline completed! {len(generated_pages)} pages in {generation_time:.2f}s.",
706
- result
707
- )
708
-
709
- print(f"🎉 STANDARD pipeline finished for job {job_id}")
710
-
711
  except Exception as e:
712
- error_msg = f"Standard pipeline failed: {str(e)}"
713
- print(f"❌ {error_msg}")
714
- update_job_status(job_id, JobStatus.FAILED, 0, error_msg)
715
 
716
- def generate_storybook_dispatcher(job_id: str):
717
- """Choose between pipelines"""
718
- job_data = job_storage[job_id]
719
- story_request_data = job_data["request"]
720
-
721
- pipeline_type = story_request_data.get("pipeline_type", "standard")
722
-
723
- # For now, only use standard pipeline until models are stable
724
- generate_storybook_standard(job_id)
725
-
726
- # ============================================================================
727
- # FASTAPI ENDPOINTS (simplified)
728
- # ============================================================================
 
729
 
730
- @app.post("/api/generate-storybook")
731
- async def generate_storybook_unified(request: dict, background_tasks: BackgroundTasks):
732
- """Unified endpoint that handles both pipelines"""
733
  try:
734
- print(f"📥 Received storybook request: {request.get('story_title', 'Unknown')}")
735
-
736
- if 'consistency_seed' not in request or not request['consistency_seed']:
737
- request['consistency_seed'] = random.randint(1000, 9999)
738
-
739
- # Ensure model_choice is valid
740
- if request.get('model_choice') not in MODEL_CHOICES:
741
- request['model_choice'] = "sd-1.5" # Force to working model
742
-
743
- story_request = StorybookRequest(**request)
744
-
745
- if not story_request.story_title or not story_request.scenes:
746
- raise HTTPException(status_code=400, detail="story_title and scenes are required")
747
-
748
- job_id = create_job(story_request)
749
- background_tasks.add_task(generate_storybook_dispatcher, job_id)
750
 
751
- response_data = {
752
- "status": "success",
753
- "message": f"Storybook generation started with {story_request.pipeline_type} pipeline",
754
- "job_id": job_id,
755
- "story_title": story_request.story_title,
756
- "total_scenes": len(story_request.scenes),
757
- "model_choice": story_request.model_choice,
758
- "pipeline_type": story_request.pipeline_type,
759
- "timestamp": datetime.now().isoformat()
760
  }
761
 
762
- print(f"✅ Job {job_id} started")
763
-
764
- return response_data
765
-
766
- except Exception as e:
767
- error_msg = f"API Error: {str(e)}"
768
- print(f"❌ {error_msg}")
769
- raise HTTPException(status_code=500, detail=error_msg)
770
-
771
- @app.get("/api/job-status/{job_id}")
772
- async def get_job_status_endpoint(job_id: str):
773
- job_data = job_storage.get(job_id)
774
- if not job_data:
775
- raise HTTPException(status_code=404, detail="Job not found")
776
-
777
- return JobStatusResponse(
778
- job_id=job_id,
779
- status=job_data["status"],
780
- progress=job_data["progress"],
781
- message=job_data["message"],
782
- result=job_data["result"],
783
- created_at=job_data["created_at"],
784
- updated_at=job_data["updated_at"]
785
- )
786
-
787
- @app.get("/api/health")
788
- async def api_health():
789
- return {
790
- "status": "healthy",
791
- "service": "storybook-generator",
792
- "timestamp": datetime.now().isoformat(),
793
- "active_jobs": len(job_storage),
794
- "models_loaded": list(model_cache.keys()),
795
- "available_models": list(MODEL_CHOICES.keys()),
796
- "oci_api_connected": OCI_API_BASE_URL
797
- }
798
-
799
- # Simple Gradio interface
800
- def create_simple_interface():
801
- with gr.Blocks(title="Storybook Generator") as demo:
802
- gr.Markdown("# Storybook Generator")
803
-
804
- with gr.Row():
805
- with gr.Column():
806
- prompt = gr.Textbox(label="Prompt")
807
- generate_btn = gr.Button("Generate")
808
- with gr.Column():
809
- output = gr.Image(label="Output")
810
 
811
- def generate_image(prompt_text):
812
- pipe = load_model("sd-1.5")
813
- if pipe:
814
- image = pipe(prompt_text, num_inference_steps=20).images[0]
815
- return image
816
- return None
817
 
818
- generate_btn.click(generate_image, inputs=prompt, outputs=output)
819
-
820
- return demo
821
-
822
- demo = create_simple_interface()
823
-
824
- @app.get("/")
825
- async def root():
826
- return {
827
- "message": "Storybook Generator API is running!",
828
- "available_models": list(MODEL_CHOICES.keys()),
829
- "default_model": "sd-1.5"
830
- }
831
-
832
- # Mount Gradio for Hugging Face Spaces
833
- def get_app():
834
- return app
835
-
836
- if __name__ == "__main__":
837
- import uvicorn
838
- import os
839
-
840
- HF_SPACE = os.environ.get('SPACE_ID') is not None
841
-
842
- if HF_SPACE:
843
- print("🚀 Running on Hugging Face Spaces")
844
- gr.mount_gradio_app(app, demo, path="/ui")
845
- uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")
846
- else:
847
- print("🚀 Running locally")
848
- uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
 
1
  import gradio as gr
2
  import torch
3
+ from diffusers import StableDiffusionPipeline, EulerAncestralDiscreteScheduler
4
+ from PIL import Image
5
  import io
6
  import requests
7
  import os
 
9
  import re
10
  import time
11
  import json
12
+ from typing import List, Optional, Dict
13
  from fastapi import FastAPI, HTTPException, BackgroundTasks
14
  from pydantic import BaseModel
15
  import gc
 
19
  import hashlib
20
  from enum import Enum
21
  import random
 
22
 
23
  # External OCI API URL - YOUR BUCKET SAVING API
24
  OCI_API_BASE_URL = "https://yukee1992-oci-story-book.hf.space"
 
29
  print(f"📁 Created local image directory: {PERSISTENT_IMAGE_DIR}")
30
 
31
  # Initialize FastAPI app
32
+ app = FastAPI(title="Storybook Generator API")
33
 
34
  # Add CORS middleware
35
  from fastapi.middleware.cors import CORSMiddleware
 
44
  # Job Status Enum
45
  class JobStatus(str, Enum):
46
  PENDING = "pending"
 
 
 
47
  PROCESSING = "processing"
48
  COMPLETED = "completed"
49
  FAILED = "failed"
 
52
  class StoryScene(BaseModel):
53
  visual: str
54
  text: str
55
+ characters_present: List[str] = [] # Which characters are in this scene
56
+ scene_type: str = "general" # "action", "dialogue", "establishing", etc.
 
57
 
58
  class CharacterDescription(BaseModel):
59
  name: str
60
  description: str
61
+ visual_prompt: str = "" # Detailed visual description for AI
62
+ key_features: List[str] = [] # Critical features that must stay consistent
 
63
 
64
  class StorybookRequest(BaseModel):
65
  story_title: str
66
  scenes: List[StoryScene]
67
  characters: List[CharacterDescription] = []
68
+ model_choice: str = "dreamshaper-8"
69
  style: str = "childrens_book"
70
  callback_url: Optional[str] = None
71
+ consistency_seed: Optional[int] = None # For consistent character generation
 
72
 
73
  class JobStatusResponse(BaseModel):
74
  job_id: str
 
79
  created_at: float
80
  updated_at: float
81
 
82
+ # HIGH-QUALITY MODEL SELECTION
83
  MODEL_CHOICES = {
84
+ "dreamshaper-8": "lykon/dreamshaper-8",
85
+ "realistic-vision": "SG161222/Realistic_Vision_V5.1",
86
+ "anything-v5": "andite/anything-v5.0",
87
+ "openjourney": "prompthero/openjourney",
88
+ "sd-2.1": "stabilityai/stable-diffusion-2-1",
89
  }
90
 
91
+ # FALLBACK CHARACTER TEMPLATES (used only if n8n doesn't provide character details)
92
  FALLBACK_CHARACTER_TEMPLATES = {
93
  "Sparkle the Star Cat": {
94
  "visual_prompt": "small white kitten with distinctive silver star-shaped spots on fur, big golden eyes, shiny blue collar with star charm, playful expression",
 
97
  "Benny the Bunny": {
98
  "visual_prompt": "fluffy brown rabbit with long ears, bright green eyes, red scarf around neck, cheerful expression",
99
  "key_features": ["red scarf", "long ears", "green eyes", "brown fur"],
100
+ },
101
+ "Tilly the Turtle": {
102
+ "visual_prompt": "gentle green turtle with shiny turquoise shell decorated with swirl patterns, wise expression, slow-moving",
103
+ "key_features": ["turquoise shell", "swirl patterns", "green skin", "wise expression"],
104
  }
105
  }
106
 
107
  # GLOBAL STORAGE
108
  job_storage = {}
109
  model_cache = {}
 
110
  current_model_name = None
111
  current_pipe = None
112
  model_lock = threading.Lock()
113
 
114
+ def load_model(model_name="dreamshaper-8"):
115
+ """Thread-safe model loading with HIGH-QUALITY settings"""
 
116
  global model_cache, current_model_name, current_pipe
117
 
118
  with model_lock:
 
121
  current_model_name = model_name
122
  return current_pipe
123
 
124
+ print(f"🔄 Loading HIGH-QUALITY model: {model_name}")
125
  try:
126
+ model_id = MODEL_CHOICES.get(model_name, "lykon/dreamshaper-8")
127
 
128
  pipe = StableDiffusionPipeline.from_pretrained(
129
  model_id,
 
139
  current_pipe = pipe
140
  current_model_name = model_name
141
 
142
+ print(f"✅ HIGH-QUALITY Model loaded: {model_name}")
143
  return pipe
144
 
145
  except Exception as e:
146
  print(f"❌ Model loading failed: {e}")
147
+ return StableDiffusionPipeline.from_pretrained(
148
+ "runwayml/stable-diffusion-v1-5",
149
+ torch_dtype=torch.float32,
150
+ safety_checker=None,
151
+ requires_safety_checker=False
152
+ ).to("cpu")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
+ # Initialize default model
155
+ print("🚀 Initializing Storybook Generator API...")
156
+ load_model("dreamshaper-8")
157
+ print("✅ Model loaded and ready!")
158
 
159
+ # DYNAMIC CHARACTER PROCESSING FUNCTIONS
160
  def process_character_descriptions(characters_from_request):
161
  """Process character descriptions from n8n and create consistency templates"""
162
  character_templates = {}
 
164
  for character in characters_from_request:
165
  char_name = character.name
166
 
167
+ # Use provided visual_prompt or generate from description
168
  if character.visual_prompt:
169
  visual_prompt = character.visual_prompt
170
  else:
171
+ # Generate visual prompt from description
172
  visual_prompt = generate_visual_prompt_from_description(character.description, char_name)
173
 
174
+ # Use provided key_features or extract from description
175
  if character.key_features:
176
  key_features = character.key_features
177
  else:
 
181
  "visual_prompt": visual_prompt,
182
  "key_features": key_features,
183
  "consistency_keywords": f"consistent character, same {char_name.split()[-1].lower()}, maintaining appearance",
184
+ "source": "n8n_request" # Track where this template came from
185
  }
186
 
187
  print(f"✅ Processed {len(character_templates)} characters from n8n request")
 
189
 
190
  def generate_visual_prompt_from_description(description, character_name):
191
  """Generate a visual prompt from character description"""
192
+ # Basic extraction of visual elements
193
  description_lower = description.lower()
194
 
195
+ # Extract species/type
196
  species_keywords = ["kitten", "cat", "rabbit", "bunny", "turtle", "dog", "bird", "dragon", "bear", "fox"]
197
  species = "character"
198
  for keyword in species_keywords:
 
200
  species = keyword
201
  break
202
 
203
+ # Extract colors
204
  color_keywords = ["white", "black", "brown", "red", "blue", "green", "yellow", "golden", "silver", "orange"]
205
  colors = []
206
  for color in color_keywords:
207
  if color in description_lower:
208
  colors.append(color)
209
 
210
+ # Extract distinctive features
211
  feature_keywords = ["spots", "stripes", "collar", "scarf", "shell", "wings", "horn", "tail", "ears", "eyes"]
212
  features = []
213
  for feature in feature_keywords:
214
  if feature in description_lower:
215
  features.append(feature)
216
 
217
+ # Build visual prompt
218
  visual_prompt_parts = []
219
  if colors:
220
  visual_prompt_parts.append(f"{' '.join(colors)} {species}")
 
226
  if features:
227
  visual_prompt_parts.append(f"with {', '.join(features)}")
228
 
229
+ # Add emotional/character traits
230
  trait_keywords = ["playful", "brave", "curious", "kind", "cheerful", "wise", "calm", "friendly"]
231
  traits = [trait for trait in trait_keywords if trait in description_lower]
232
  if traits:
 
242
  description_lower = description.lower()
243
  key_features = []
244
 
245
+ # Look for distinctive physical features
246
  feature_patterns = [
247
  r"(\w+)\s+(?:spots|stripes|marks)",
248
  r"(\w+)\s+(?:collar|scarf|ribbon)",
 
254
  matches = re.findall(pattern, description_lower)
255
  key_features.extend(matches)
256
 
257
+ # Remove duplicates and limit to 3 most important features
258
  key_features = list(set(key_features))[:3]
259
 
260
+ # If no features found, use some defaults based on character type
261
  if not key_features:
262
  if any(word in description_lower for word in ["kitten", "cat"]):
263
  key_features = ["whiskers", "tail", "paws"]
 
271
  print(f"🔧 Extracted key features: {key_features}")
272
  return key_features
273
 
274
+ # ENHANCED PROMPT ENGINEERING WITH DYNAMIC CHARACTER CONSISTENCY
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  def enhance_prompt_with_characters(scene_visual, characters_present, character_templates, style="childrens_book", scene_number=1):
276
  """Create prompts that maintain character consistency using dynamic templates"""
277
 
278
+ # Get character descriptions for this scene
279
  character_descriptions = []
280
  consistency_keywords = []
281
 
 
285
  character_descriptions.append(f"{char_name}: {char_data['visual_prompt']}")
286
  consistency_keywords.append(char_data['consistency_keywords'])
287
  else:
288
+ # Fallback if character not in templates
289
  character_descriptions.append(f"{char_name}: distinctive character")
290
  consistency_keywords.append(f"consistent {char_name}")
291
 
292
+ # Style templates
293
  style_templates = {
294
  "childrens_book": "children's book illustration, watercolor style, soft colors, whimsical, magical, storybook art, professional illustration",
295
  "realistic": "photorealistic, detailed, natural lighting, professional photography",
 
299
 
300
  style_prompt = style_templates.get(style, style_templates["childrens_book"])
301
 
302
+ # Build the enhanced prompt
303
  character_context = ". ".join(character_descriptions)
304
  consistency_context = ", ".join(consistency_keywords)
305
 
 
310
  f"Scene {scene_number} of storybook series. "
311
  )
312
 
313
+ # Quality boosters for consistency
314
  quality_boosters = [
315
  "consistent character design", "maintain identical features",
316
  "same characters throughout", "continuous visual narrative",
 
320
 
321
  enhanced_prompt += ", ".join(quality_boosters)
322
 
323
+ # Enhanced negative prompt to avoid inconsistencies
324
  negative_prompt = (
325
  "inconsistent characters, different appearances, changing features, "
326
  "multiple versions of same character, inconsistent art style, "
 
330
 
331
  return enhanced_prompt, negative_prompt
332
 
333
+ def extract_characters_from_visual(visual_description, available_characters):
334
+ """Extract character names from visual description using available characters"""
335
+ characters = []
336
+ visual_lower = visual_description.lower()
337
+
338
+ # Check for each available character name in the visual description
339
+ for char_name in available_characters:
340
+ # Use the first word or main identifier from character name
341
+ char_identifier = char_name.split()[0].lower()
342
+ if char_identifier in visual_lower or char_name.lower() in visual_lower:
343
+ characters.append(char_name)
344
+
345
+ return characters
346
+
347
+ def generate_character_reference_sheet(characters):
348
+ """Generate reference descriptions for consistent character generation"""
349
+ reference_sheet = {}
350
+
351
+ for character in characters:
352
+ char_name = character.name
353
+ reference_sheet[char_name] = {
354
+ "name": char_name,
355
+ "base_prompt": character.visual_prompt if character.visual_prompt else generate_visual_prompt_from_description(character.description, char_name),
356
+ "key_features": character.key_features if character.key_features else extract_key_features_from_description(character.description),
357
+ "must_include": character.key_features[:2] if character.key_features else []
358
+ }
359
+
360
+ return reference_sheet
361
+
362
  def generate_consistent_image(prompt, model_choice, style, characters_present, character_templates, scene_number, consistency_seed=None):
363
  """Generate image with character consistency measures using dynamic templates"""
364
 
365
+ # Enhance prompt with character consistency
366
  enhanced_prompt, negative_prompt = enhance_prompt_with_characters(
367
  prompt, characters_present, character_templates, style, scene_number
368
  )
369
 
370
+ # Use a consistent seed for character generation
371
  if consistency_seed:
372
  base_seed = consistency_seed
373
  else:
374
  base_seed = hash("".join(characters_present)) % 1000000 if characters_present else random.randint(1000, 9999)
375
 
376
+ # Adjust seed slightly per scene but maintain character consistency
377
  scene_seed = base_seed + scene_number
378
 
379
  try:
380
  pipe = load_model(model_choice)
 
 
381
 
382
  image = pipe(
383
  prompt=enhanced_prompt,
384
  negative_prompt=negative_prompt,
385
+ num_inference_steps=35, # Increased for better quality
386
+ guidance_scale=7.5, # Slightly lower for more consistency
387
  width=768,
388
  height=768,
389
  generator=torch.Generator(device="cpu").manual_seed(scene_seed)
 
399
  print(f"❌ Consistent generation failed: {str(e)}")
400
  raise
401
 
402
+ # Backward compatibility functions
403
+ def enhance_prompt(prompt, style="childrens_book"):
404
+ """Legacy function for backward compatibility"""
405
+ return enhance_prompt_with_characters(prompt, [], {}, style, 1)
406
 
407
+ def generate_high_quality_image(prompt, model_choice="dreamshaper-8", style="childrens_book", negative_prompt=""):
408
+ """Legacy function for backward compatibility"""
409
+ return generate_consistent_image(prompt, model_choice, style, [], {}, 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
+ # LOCAL FILE MANAGEMENT FUNCTIONS
412
+ def save_image_to_local(image, prompt, style="test"):
413
+ """Save image to local persistent storage"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  try:
415
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
416
+ safe_prompt = "".join(c for c in prompt[:50] if c.isalnum() or c in (' ', '-', '_')).rstrip()
417
+ filename = f"image_{safe_prompt}_{timestamp}.png"
418
 
419
+ # Create style subfolder
420
+ style_dir = os.path.join(PERSISTENT_IMAGE_DIR, style)
421
+ os.makedirs(style_dir, exist_ok=True)
422
+ filepath = os.path.join(style_dir, filename)
 
 
 
 
 
 
 
423
 
424
+ # Save the image
425
+ image.save(filepath)
426
+ print(f"💾 Image saved locally: {filepath}")
427
 
428
+ return filepath, filename
429
 
 
 
 
 
 
 
 
 
 
430
  except Exception as e:
431
+ print(f" Failed to save locally: {e}")
432
+ return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
+ def delete_local_image(filepath):
435
+ """Delete an image from local storage"""
436
+ try:
437
+ if os.path.exists(filepath):
438
+ os.remove(filepath)
439
+ print(f"🗑️ Deleted local image: {filepath}")
440
+ return True, f"✅ Deleted: {os.path.basename(filepath)}"
441
+ else:
442
+ return False, f"❌ File not found: {filepath}"
443
+ except Exception as e:
444
+ return False, f"❌ Error deleting: {str(e)}"
445
 
446
+ def get_local_storage_info():
447
+ """Get information about local storage usage"""
448
  try:
449
+ total_size = 0
450
+ file_count = 0
451
+ images_list = []
452
+
453
+ for root, dirs, files in os.walk(PERSISTENT_IMAGE_DIR):
454
+ for file in files:
455
+ if file.endswith(('.png', '.jpg', '.jpeg')):
456
+ filepath = os.path.join(root, file)
457
+ if os.path.exists(filepath):
458
+ file_size = os.path.getsize(filepath)
459
+ total_size += file_size
460
+ file_count += 1
461
+ images_list.append({
462
+ 'path': filepath,
463
+ 'filename': file,
464
+ 'size_kb': round(file_size / 1024, 1),
465
+ 'created': os.path.getctime(filepath)
466
+ })
467
+
468
+ return {
469
+ "total_files": file_count,
470
+ "total_size_mb": round(total_size / (1024 * 1024), 2),
471
+ "images": sorted(images_list, key=lambda x: x['created'], reverse=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  }
 
 
 
 
 
 
 
 
 
 
 
473
  except Exception as e:
474
+ return {"error": str(e)}
 
 
475
 
476
+ def refresh_local_images():
477
+ """Get list of all locally saved images"""
478
+ try:
479
+ image_files = []
480
+ for root, dirs, files in os.walk(PERSISTENT_IMAGE_DIR):
481
+ for file in files:
482
+ if file.endswith(('.png', '.jpg', '.jpeg')):
483
+ filepath = os.path.join(root, file)
484
+ if os.path.exists(filepath):
485
+ image_files.append(filepath)
486
+ return image_files
487
+ except Exception as e:
488
+ print(f"Error refreshing local images: {e}")
489
+ return []
490
 
491
+ # OCI BUCKET FUNCTIONS
492
+ def save_to_oci_bucket(image, text_content, story_title, page_number, file_type="image"):
493
+ """Save both images and text to OCI bucket via your OCI API"""
494
  try:
495
+ if file_type == "image":
496
+ # Convert image to bytes
497
+ img_bytes = io.BytesIO()
498
+ image.save(img_bytes, format='PNG')
499
+ file_data = img_bytes.getvalue()
500
+ filename = f"page_{page_number:03d}.png"
501
+ mime_type = "image/png"
502
+ else: # text
503
+ file_data = text_content.encode('utf-8')
504
+ filename = f"page_{page_number:03d}.txt"
505
+ mime_type = "text/plain"
506
+
507
+ # Use your OCI API to save the file
508
+ api_url = f"{OCI_API_BASE_URL}/api/upload"
 
 
509
 
510
+ files = {'file': (filename, file_data, mime_type)}
511
+ data = {
512
+ 'project_id': 'storybook-library',
513
+ 'subfolder': f'stories/{story_title}'
 
 
 
 
 
514
  }
515
 
516
+ response = requests.post(api_url, files=files, data=data, timeout=30)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
 
518
+ print(f"📨 OCI API Response: {response.status_code}")
 
 
 
 
 
519
 
520
+ if response.status_