yukee1992 commited on
Commit
8e7984f
Β·
verified Β·
1 Parent(s): 5237974

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +212 -428
app.py CHANGED
@@ -20,7 +20,7 @@ import hashlib
20
  from enum import Enum
21
  import random
22
 
23
- # External OCI API URL
24
  OCI_API_BASE_URL = "https://yukee1992-oci-story-book.hf.space"
25
 
26
  # Initialize FastAPI app
@@ -85,11 +85,6 @@ current_model_name = None
85
  current_pipe = None
86
  model_lock = threading.Lock()
87
 
88
- # Create persistent directory for test images
89
- PERSISTENT_IMAGE_DIR = "generated_test_images"
90
- os.makedirs(PERSISTENT_IMAGE_DIR, exist_ok=True)
91
- print(f"πŸ“ Created persistent image directory: {PERSISTENT_IMAGE_DIR}")
92
-
93
  def load_model(model_name="dreamshaper-8"):
94
  """Thread-safe model loading with HIGH-QUALITY settings"""
95
  global model_cache, current_model_name, current_pipe
@@ -131,9 +126,9 @@ def load_model(model_name="dreamshaper-8"):
131
  ).to("cpu")
132
 
133
  # Initialize default model
134
- print("πŸš€ Initializing HIGH-QUALITY Storybook Generator...")
135
  load_model("dreamshaper-8")
136
- print("βœ… HIGH-QUALITY Model loaded and ready!")
137
 
138
  # PROFESSIONAL PROMPT ENGINEERING
139
  def enhance_prompt(prompt, style="childrens_book"):
@@ -207,114 +202,45 @@ def generate_high_quality_image(prompt, model_choice="dreamshaper-8", style="chi
207
  print(f"❌ HQ Generation failed: {str(e)}")
208
  raise
209
 
210
- def save_image_to_persistent_storage(image, prompt, subfolder=""):
211
- """Save image to persistent storage that appears in Files tab"""
212
- try:
213
- # Create filename
214
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
215
- safe_prompt = "".join(c for c in prompt[:50] if c.isalnum() or c in (' ', '-', '_')).rstrip()
216
- filename = f"image_{safe_prompt}_{timestamp}.png"
217
-
218
- # Create subfolder if specified
219
- if subfolder:
220
- save_dir = os.path.join(PERSISTENT_IMAGE_DIR, subfolder)
221
- os.makedirs(save_dir, exist_ok=True)
222
- filepath = os.path.join(save_dir, filename)
223
- else:
224
- filepath = os.path.join(PERSISTENT_IMAGE_DIR, filename)
225
-
226
- # Save the image
227
- image.save(filepath)
228
- print(f"πŸ’Ύ Image saved to persistent storage: {filepath}")
229
-
230
- # Return relative path for display
231
- return filepath
232
-
233
- except Exception as e:
234
- print(f"❌ Failed to save to persistent storage: {e}")
235
- return None
236
-
237
- def delete_image_file(filepath):
238
- """Delete an image file from persistent storage"""
239
  try:
240
- if os.path.exists(filepath):
241
- os.remove(filepath)
242
- print(f"πŸ—‘οΈ Deleted image: {filepath}")
243
- return True, f"βœ… Successfully deleted: {os.path.basename(filepath)}"
244
- else:
245
- return False, f"❌ File not found: {filepath}"
246
- except Exception as e:
247
- return False, f"❌ Error deleting file: {str(e)}"
248
-
249
- def get_storage_info():
250
- """Get information about storage usage"""
251
- try:
252
- total_size = 0
253
- file_count = 0
254
-
255
- for root, dirs, files in os.walk(PERSISTENT_IMAGE_DIR):
256
- for file in files:
257
- filepath = os.path.join(root, file)
258
- if os.path.exists(filepath):
259
- total_size += os.path.getsize(filepath)
260
- file_count += 1
261
-
262
- return {
263
- "total_files": file_count,
264
- "total_size_mb": round(total_size / (1024 * 1024), 2),
265
- "directory": PERSISTENT_IMAGE_DIR
266
  }
267
- except Exception as e:
268
- return {"error": str(e)}
269
-
270
- def generate_test_image(prompt, model_choice, style_choice):
271
- """Generate a single image for testing and save to persistent storage"""
272
- try:
273
- if not prompt.strip():
274
- return None, "❌ Please enter a prompt", None
275
-
276
- print(f"🎨 Generating test image with prompt: {prompt}")
277
 
278
- # Enhance the prompt
279
- enhanced_prompt, negative_prompt = enhance_prompt(prompt, style_choice)
280
-
281
- # Generate the image
282
- image = generate_high_quality_image(
283
- enhanced_prompt,
284
- model_choice,
285
- style_choice,
286
- negative_prompt
287
- )
288
 
289
- # Save to persistent storage with style-based subfolder
290
- saved_path = save_image_to_persistent_storage(image, prompt, style_choice)
291
 
292
- if saved_path:
293
- # Get just the filename for display
294
- filename = os.path.basename(saved_path)
295
- save_info = f"πŸ’Ύ **Auto-saved to Files tab:** `{PERSISTENT_IMAGE_DIR}/{style_choice}/{filename}`"
 
 
296
  else:
297
- save_info = "⚠️ Could not auto-save to persistent storage"
298
-
299
- status_msg = f"""βœ… Success! Generated: {prompt}
300
-
301
- {save_info}
302
-
303
- 🎨 Enhanced prompt: {enhanced_prompt}
304
-
305
- πŸ“ **Location in Files tab:**
306
- - Navigate to: `{PERSISTENT_IMAGE_DIR}/`
307
- - Then go to: `{style_choice}/` folder
308
- - Find your image: `{os.path.basename(saved_path) if saved_path else 'filename'}`
309
-
310
- πŸ’‘ You can also use the download button below the image!"""
311
-
312
- return image, status_msg, saved_path
313
-
314
  except Exception as e:
315
- error_msg = f"❌ Generation failed: {str(e)}"
316
- print(error_msg)
317
- return None, error_msg, None
318
 
319
  # JOB MANAGEMENT FUNCTIONS
320
  def create_job(story_request: StorybookRequest) -> str:
@@ -348,6 +274,7 @@ def update_job_status(job_id: str, status: JobStatus, progress: int, message: st
348
  if result:
349
  job_storage[job_id]["result"] = result
350
 
 
351
  job_data = job_storage[job_id]
352
  request_data = job_data["request"]
353
 
@@ -370,30 +297,168 @@ def update_job_status(job_id: str, status: JobStatus, progress: int, message: st
370
  json=callback_data,
371
  timeout=10
372
  )
373
- print(f"πŸ“’ Callback sent: Status {response.status_code}")
374
  except Exception as e:
375
  print(f"⚠️ Callback failed: {str(e)}")
376
 
377
  return True
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  # FASTAPI ENDPOINTS
380
- @app.post("/api/generate-storybook", response_model=dict)
381
- async def generate_storybook(request: StorybookRequest, background_tasks: BackgroundTasks):
 
382
  try:
383
- job_id = create_job(request)
384
- return {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  "status": "success",
386
- "message": "Storybook generation started",
387
  "job_id": job_id,
388
- "story_title": request.story_title,
389
- "total_scenes": len(request.scenes),
390
- "callback_url": request.callback_url
 
 
391
  }
 
 
 
 
 
392
  except Exception as e:
393
- raise HTTPException(status_code=500, detail=f"Failed to start generation: {str(e)}")
 
 
394
 
395
- @app.get("/api/job-status/{job_id}", response_model=JobStatusResponse)
396
  async def get_job_status_endpoint(job_id: str):
 
397
  job_data = job_storage.get(job_id)
398
  if not job_data:
399
  raise HTTPException(status_code=404, detail="Job not found")
@@ -408,320 +473,39 @@ async def get_job_status_endpoint(job_id: str):
408
  updated_at=job_data["updated_at"]
409
  )
410
 
411
- @app.get("/api/list-test-images")
412
- async def list_test_images():
413
- """API endpoint to list all generated test images"""
414
- try:
415
- images_list = []
416
- for root, dirs, files in os.walk(PERSISTENT_IMAGE_DIR):
417
- for file in files:
418
- if file.endswith(('.png', '.jpg', '.jpeg')):
419
- full_path = os.path.join(root, file)
420
- rel_path = os.path.relpath(full_path, PERSISTENT_IMAGE_DIR)
421
- images_list.append({
422
- "filename": file,
423
- "path": rel_path,
424
- "full_path": full_path,
425
- "size": os.path.getsize(full_path) if os.path.exists(full_path) else 0,
426
- "created": os.path.getctime(full_path) if os.path.exists(full_path) else 0
427
- })
428
-
429
- storage_info = get_storage_info()
430
-
431
- return {
432
- "status": "success",
433
- "image_count": len(images_list),
434
- "image_directory": PERSISTENT_IMAGE_DIR,
435
- "storage_info": storage_info,
436
- "images": sorted(images_list, key=lambda x: x["created"], reverse=True)
437
- }
438
- except Exception as e:
439
- return {"status": "error", "message": str(e)}
440
-
441
- @app.delete("/api/delete-image/{image_path:path}")
442
- async def delete_image(image_path: str):
443
- """API endpoint to delete an image"""
444
- try:
445
- # Security check: ensure the path is within our image directory
446
- full_path = os.path.join(PERSISTENT_IMAGE_DIR, image_path)
447
- if not full_path.startswith(os.path.abspath(PERSISTENT_IMAGE_DIR)):
448
- return {"status": "error", "message": "Invalid path"}
449
-
450
- success, message = delete_image_file(full_path)
451
- return {"status": "success" if success else "error", "message": message}
452
- except Exception as e:
453
- return {"status": "error", "message": str(e)}
454
-
455
- @app.delete("/api/clear-all-images")
456
- async def clear_all_images():
457
- """API endpoint to delete all generated images"""
458
- try:
459
- deleted_count = 0
460
- error_count = 0
461
-
462
- for root, dirs, files in os.walk(PERSISTENT_IMAGE_DIR):
463
- for file in files:
464
- if file.endswith(('.png', '.jpg', '.jpeg')):
465
- filepath = os.path.join(root, file)
466
- success, _ = delete_image_file(filepath)
467
- if success:
468
- deleted_count += 1
469
- else:
470
- error_count += 1
471
-
472
- return {
473
- "status": "success",
474
- "message": f"Deleted {deleted_count} images, {error_count} errors",
475
- "deleted_count": deleted_count,
476
- "error_count": error_count
477
- }
478
- except Exception as e:
479
- return {"status": "error", "message": str(e)}
480
-
481
- # GRADIO INTERFACE
482
- def create_test_interface():
483
- with gr.Blocks(title="High-Quality Image Generator", theme="soft") as demo:
484
- gr.Markdown("# 🎨 High-Quality Image Generator")
485
- gr.Markdown("**Generate studio-quality images that auto-save to your Files tab!**")
486
-
487
- # Storage info display
488
- storage_info = gr.Textbox(
489
- label="πŸ“Š Storage Information",
490
- interactive=False,
491
- lines=2
492
- )
493
-
494
- def update_storage_info():
495
- info = get_storage_info()
496
- if "error" not in info:
497
- return f"πŸ“ Storage: {info['total_files']} images, {info['total_size_mb']} MB used"
498
- return "πŸ“ Storage: Unable to calculate"
499
-
500
- with gr.Row():
501
- with gr.Column(scale=1):
502
- model_dropdown = gr.Dropdown(
503
- label="AI Model",
504
- choices=list(MODEL_CHOICES.keys()),
505
- value="dreamshaper-8"
506
- )
507
-
508
- style_dropdown = gr.Dropdown(
509
- label="Art Style",
510
- choices=["childrens_book", "realistic", "fantasy", "anime"],
511
- value="childrens_book"
512
- )
513
-
514
- prompt_input = gr.Textbox(
515
- label="Prompt",
516
- placeholder="Describe what you want to generate...",
517
- lines=3
518
- )
519
-
520
- generate_btn = gr.Button("✨ Generate Image", variant="primary", size="lg")
521
-
522
- gr.Markdown("### πŸ’‘ Auto-Save Feature")
523
- gr.Markdown(f"""
524
- **All images are automatically saved to:**
525
- - `{PERSISTENT_IMAGE_DIR}/[style]/` in your Files tab
526
- - Organized by art style for easy browsing
527
- - Also available via download button below
528
- """)
529
-
530
- with gr.Column(scale=2):
531
- image_output = gr.Image(
532
- label="Generated Image",
533
- height=400,
534
- show_download_button=True,
535
- show_share_button=True
536
- )
537
-
538
- # Delete button for current image
539
- current_file_path = gr.State() # Hidden state to store file path
540
- delete_btn = gr.Button("πŸ—‘οΈ Delete This Image", variant="stop", size="sm")
541
- delete_status = gr.Textbox(label="Delete Status", interactive=False, lines=2)
542
-
543
- status_output = gr.Textbox(
544
- label="Status & File Location",
545
- lines=4,
546
- show_copy_button=True
547
- )
548
-
549
- # Quick examples
550
- with gr.Row():
551
- gr.Button("πŸ‰ Dragon Example").click(
552
- lambda: "A friendly dragon reading a giant book under a magical tree with glowing fairies",
553
- outputs=prompt_input
554
- )
555
- gr.Button("🐱 Kitten Example").click(
556
- lambda: "Cute kitten playing with yarn ball in a sunny living room",
557
- outputs=prompt_input
558
- )
559
- gr.Button("πŸš€ Space Example").click(
560
- lambda: "Space astronaut exploring a colorful alien planet with strange creatures",
561
- outputs=prompt_input
562
- )
563
-
564
- # Image browser section with delete functionality
565
- with gr.Accordion("πŸ“ Manage Generated Images", open=False):
566
- gr.Markdown(f"### Images saved in `{PERSISTENT_IMAGE_DIR}/`")
567
-
568
- with gr.Row():
569
- refresh_btn = gr.Button("πŸ”„ Refresh List")
570
- clear_all_btn = gr.Button("πŸ—‘οΈ Clear All Images", variant="stop")
571
-
572
- file_list = gr.Gallery(
573
- label="Generated Images",
574
- show_label=True,
575
- elem_id="gallery",
576
- columns=3,
577
- height="auto"
578
- )
579
-
580
- selected_file_info = gr.Textbox(label="Selected Image Info", interactive=False)
581
- delete_selected_btn = gr.Button("πŸ—‘οΈ Delete Selected Image", variant="stop")
582
- delete_selected_status = gr.Textbox(label="Delete Status", interactive=False)
583
-
584
- def refresh_file_list():
585
- """Refresh the list of generated images"""
586
- try:
587
- image_files = []
588
- image_info = []
589
-
590
- for root, dirs, files in os.walk(PERSISTENT_IMAGE_DIR):
591
- for file in files:
592
- if file.endswith(('.png', '.jpg', '.jpeg')):
593
- full_path = os.path.join(root, file)
594
- if os.path.exists(full_path):
595
- image_files.append(full_path)
596
- image_info.append(f"πŸ“„ {file}\nπŸ“ {os.path.relpath(root, PERSISTENT_IMAGE_DIR)}")
597
-
598
- return image_files, update_storage_info()
599
- except Exception as e:
600
- print(f"Error refreshing file list: {e}")
601
- return [], "❌ Error loading images"
602
-
603
- def get_file_info(evt: gr.SelectData):
604
- """Get info about selected file"""
605
- if evt.value:
606
- filepath = evt.value
607
- filename = os.path.basename(filepath)
608
- size_kb = os.path.getsize(filepath) / 1024 if os.path.exists(filepath) else 0
609
- return f"Selected: {filename}\nSize: {size_kb:.1f} KB\nPath: {filepath}"
610
- return "No image selected"
611
-
612
- def delete_selected_file(selected_files):
613
- """Delete the selected file"""
614
- if not selected_files:
615
- return "❌ No image selected", None, update_storage_info()
616
-
617
- filepath = selected_files[0] if isinstance(selected_files, list) else selected_files
618
- success, message = delete_image_file(filepath)
619
-
620
- # Refresh the file list
621
- new_files, storage_info = refresh_file_list()
622
- return message, new_files, storage_info
623
-
624
- def clear_all_images_func():
625
- """Clear all images"""
626
- try:
627
- deleted_count = 0
628
- for root, dirs, files in os.walk(PERSISTENT_IMAGE_DIR):
629
- for file in files:
630
- if file.endswith(('.png', '.jpg', '.jpeg')):
631
- filepath = os.path.join(root, file)
632
- success, _ = delete_image_file(filepath)
633
- if success:
634
- deleted_count += 1
635
-
636
- new_files, storage_info = refresh_file_list()
637
- return f"βœ… Deleted {deleted_count} images", new_files, storage_info
638
- except Exception as e:
639
- return f"❌ Error: {str(e)}", None, update_storage_info()
640
-
641
- # Connect events
642
- refresh_btn.click(fn=refresh_file_list, outputs=[file_list, storage_info])
643
- clear_all_btn.click(fn=clear_all_images_func, outputs=[delete_selected_status, file_list, storage_info])
644
- file_list.select(fn=get_file_info, outputs=selected_file_info)
645
- delete_selected_btn.click(
646
- fn=delete_selected_file,
647
- inputs=file_list,
648
- outputs=[delete_selected_status, file_list, storage_info]
649
- )
650
-
651
- # Delete current image functionality
652
- def delete_current_image(filepath):
653
- """Delete the currently displayed image"""
654
- if not filepath:
655
- return "❌ No image to delete", None, None, update_storage_info()
656
-
657
- success, message = delete_image_file(filepath)
658
- new_files, storage_info = refresh_file_list()
659
-
660
- if success:
661
- return message, None, "βœ… Image deleted successfully!", storage_info
662
- else:
663
- return message, None, "❌ Delete failed", storage_info
664
-
665
- # Connect main generate button
666
- generate_btn.click(
667
- fn=generate_test_image,
668
- inputs=[prompt_input, model_dropdown, style_dropdown],
669
- outputs=[image_output, status_output, current_file_path]
670
- ).then(
671
- fn=refresh_file_list,
672
- outputs=[file_list, storage_info]
673
- )
674
-
675
- # Connect delete button
676
- delete_btn.click(
677
- fn=delete_current_image,
678
- inputs=current_file_path,
679
- outputs=[delete_status, image_output, status_output, storage_info]
680
- ).then(
681
- fn=refresh_file_list,
682
- outputs=[file_list, storage_info]
683
- )
684
-
685
- # Initialize on load
686
- demo.load(fn=refresh_file_list, outputs=[file_list, storage_info])
687
-
688
- return demo
689
-
690
- # Create and launch Gradio app
691
- gradio_app = create_test_interface()
692
 
693
  @app.get("/")
694
  async def root():
695
- return {"message": "Storybook Generator API + UI is running!"}
696
-
697
- @app.get("/files")
698
- async def files_info():
699
- """Endpoint to check file structure"""
700
- try:
701
- file_structure = {}
702
- for root, dirs, files in os.walk(PERSISTENT_IMAGE_DIR):
703
- rel_root = os.path.relpath(root, PERSISTENT_IMAGE_DIR)
704
- file_structure[rel_root] = {
705
- "files": files,
706
- "file_count": len(files)
707
- }
708
-
709
- return {
710
- "persistent_directory": PERSISTENT_IMAGE_DIR,
711
- "exists": os.path.exists(PERSISTENT_IMAGE_DIR),
712
- "file_structure": file_structure,
713
- "storage_info": get_storage_info()
714
- }
715
- except Exception as e:
716
- return {"error": str(e)}
717
 
718
  # For Hugging Face Spaces
719
  def get_app():
720
  return app
721
 
722
  if __name__ == "__main__":
723
- print("πŸš€ Starting High-Quality Storybook Generator...")
724
- print(f"πŸ“ Persistent image directory: {PERSISTENT_IMAGE_DIR}")
725
- print("βœ… Images will auto-save to your Files tab")
726
- print("πŸ—‘οΈ Delete functionality enabled for storage management")
727
- gradio_app.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
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"
25
 
26
  # Initialize FastAPI app
 
85
  current_pipe = None
86
  model_lock = threading.Lock()
87
 
 
 
 
 
 
88
  def load_model(model_name="dreamshaper-8"):
89
  """Thread-safe model loading with HIGH-QUALITY settings"""
90
  global model_cache, current_model_name, current_pipe
 
126
  ).to("cpu")
127
 
128
  # Initialize default model
129
+ print("πŸš€ Initializing Storybook Generator API...")
130
  load_model("dreamshaper-8")
131
+ print("βœ… Model loaded and ready!")
132
 
133
  # PROFESSIONAL PROMPT ENGINEERING
134
  def enhance_prompt(prompt, style="childrens_book"):
 
202
  print(f"❌ HQ Generation failed: {str(e)}")
203
  raise
204
 
205
+ def save_to_oci_bucket(image, text_content, story_title, page_number, file_type="image"):
206
+ """Save both images and text to OCI bucket via your OCI API"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  try:
208
+ if file_type == "image":
209
+ # Convert image to bytes
210
+ img_bytes = io.BytesIO()
211
+ image.save(img_bytes, format='PNG')
212
+ file_data = img_bytes.getvalue()
213
+ filename = f"page_{page_number:03d}.png"
214
+ mime_type = "image/png"
215
+ else: # text
216
+ file_data = text_content.encode('utf-8')
217
+ filename = f"page_{page_number:03d}.txt"
218
+ mime_type = "text/plain"
219
+
220
+ # Use your OCI API to save the file
221
+ api_url = f"{OCI_API_BASE_URL}/api/upload"
222
+
223
+ files = {'file': (filename, file_data, mime_type)}
224
+ data = {
225
+ 'project_id': 'storybook-library',
226
+ 'subfolder': f'stories/{story_title}'
 
 
 
 
 
 
 
227
  }
 
 
 
 
 
 
 
 
 
 
228
 
229
+ response = requests.post(api_url, files=files, data=data, timeout=30)
 
 
 
 
 
 
 
 
 
230
 
231
+ print(f"πŸ“¨ OCI API Response: {response.status_code}")
 
232
 
233
+ if response.status_code == 200:
234
+ result = response.json()
235
+ if result['status'] == 'success':
236
+ return result.get('file_url', 'Unknown URL')
237
+ else:
238
+ raise Exception(f"OCI API Error: {result.get('message', 'Unknown error')}")
239
  else:
240
+ raise Exception(f"HTTP Error: {response.status_code}")
241
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  except Exception as e:
243
+ raise Exception(f"OCI upload failed: {str(e)}")
 
 
244
 
245
  # JOB MANAGEMENT FUNCTIONS
246
  def create_job(story_request: StorybookRequest) -> str:
 
274
  if result:
275
  job_storage[job_id]["result"] = result
276
 
277
+ # Send webhook notification if callback URL exists
278
  job_data = job_storage[job_id]
279
  request_data = job_data["request"]
280
 
 
297
  json=callback_data,
298
  timeout=10
299
  )
300
+ print(f"πŸ“’ Callback sent to n8n: Status {response.status_code}")
301
  except Exception as e:
302
  print(f"⚠️ Callback failed: {str(e)}")
303
 
304
  return True
305
 
306
+ # BACKGROUND TASK FOR COMPLETE STORYBOOK GENERATION
307
+ def generate_storybook_background(job_id: str):
308
+ """Background task to generate complete storybook with images and text"""
309
+ try:
310
+ job_data = job_storage[job_id]
311
+ story_request_data = job_data["request"]
312
+ story_request = StorybookRequest(**story_request_data)
313
+
314
+ print(f"🎬 Starting COMPLETE storybook generation for job {job_id}")
315
+ print(f"πŸ“– Story: {story_request.story_title}")
316
+ print(f"πŸ“„ Pages: {len(story_request.scenes)} scenes")
317
+
318
+ update_job_status(job_id, JobStatus.PROCESSING, 5, "Starting storybook generation...")
319
+
320
+ total_scenes = len(story_request.scenes)
321
+ generated_pages = []
322
+ start_time = time.time()
323
+
324
+ for i, scene in enumerate(story_request.scenes):
325
+ progress = 5 + int((i / total_scenes) * 90)
326
+ update_job_status(
327
+ job_id,
328
+ JobStatus.PROCESSING,
329
+ progress,
330
+ f"Generating page {i+1}/{total_scenes}: {scene.visual[:50]}..."
331
+ )
332
+
333
+ try:
334
+ print(f"πŸ–ΌοΈ Generating page {i+1}: {scene.visual}")
335
+
336
+ # Create enhanced prompt for high-quality image
337
+ enhanced_prompt, negative_prompt = enhance_prompt(scene.visual, story_request.style)
338
+
339
+ # Generate high-quality image
340
+ image = generate_high_quality_image(
341
+ enhanced_prompt,
342
+ story_request.model_choice,
343
+ story_request.style,
344
+ negative_prompt
345
+ )
346
+
347
+ # Save IMAGE to OCI bucket
348
+ image_url = save_to_oci_bucket(
349
+ image,
350
+ "", # No text for image
351
+ story_request.story_title,
352
+ i + 1,
353
+ "image"
354
+ )
355
+
356
+ # Save TEXT to OCI bucket
357
+ text_url = save_to_oci_bucket(
358
+ None, # No image for text
359
+ scene.text,
360
+ story_request.story_title,
361
+ i + 1,
362
+ "text"
363
+ )
364
+
365
+ # Store page data
366
+ page_data = {
367
+ "page_number": i + 1,
368
+ "image_url": image_url,
369
+ "text_url": text_url,
370
+ "text_content": scene.text,
371
+ "prompt_used": enhanced_prompt,
372
+ "visual_description": scene.visual
373
+ }
374
+ generated_pages.append(page_data)
375
+
376
+ print(f"βœ… Page {i+1} completed - Image: {image_url}, Text: {text_url}")
377
+
378
+ except Exception as e:
379
+ error_msg = f"Failed to generate page {i+1}: {str(e)}"
380
+ print(f"❌ {error_msg}")
381
+ update_job_status(job_id, JobStatus.FAILED, 0, error_msg)
382
+ return
383
+
384
+ # Complete the job
385
+ generation_time = time.time() - start_time
386
+
387
+ result = {
388
+ "story_title": story_request.story_title,
389
+ "total_pages": total_scenes,
390
+ "characters_used": len(story_request.characters),
391
+ "generated_pages": len(generated_pages),
392
+ "generation_time": round(generation_time, 2),
393
+ "folder_path": f"stories/{story_request.story_title}",
394
+ "oci_bucket_url": f"https://oci.com/stories/{story_request.story_title}",
395
+ "pages": generated_pages,
396
+ "file_structure": {
397
+ "images": [f"page_{i+1:03d}.png" for i in range(total_scenes)],
398
+ "texts": [f"page_{i+1:03d}.txt" for i in range(total_scenes)]
399
+ }
400
+ }
401
+
402
+ update_job_status(
403
+ job_id,
404
+ JobStatus.COMPLETED,
405
+ 100,
406
+ f"πŸŽ‰ Storybook completed! {len(generated_pages)} pages created in {generation_time:.2f}s. Saved to OCI bucket.",
407
+ result
408
+ )
409
+
410
+ print(f"πŸŽ‰ COMPLETE Storybook generation finished for job {job_id}")
411
+ print(f"πŸ“ Saved to: stories/{story_request.story_title} in OCI bucket")
412
+
413
+ except Exception as e:
414
+ error_msg = f"Story generation failed: {str(e)}"
415
+ print(f"❌ {error_msg}")
416
+ update_job_status(job_id, JobStatus.FAILED, 0, error_msg)
417
+
418
  # FASTAPI ENDPOINTS
419
+ @app.post("/api/generate-storybook")
420
+ async def generate_storybook(request: dict, background_tasks: BackgroundTasks):
421
+ """Main endpoint for n8n integration - generates complete storybook"""
422
  try:
423
+ print(f"πŸ“₯ Received n8n request for story: {request.get('story_title', 'Unknown')}")
424
+
425
+ # Convert to Pydantic model
426
+ story_request = StorybookRequest(**request)
427
+
428
+ # Validate required fields
429
+ if not story_request.story_title or not story_request.scenes:
430
+ raise HTTPException(status_code=400, detail="story_title and scenes are required")
431
+
432
+ # Create job immediately
433
+ job_id = create_job(story_request)
434
+
435
+ # Start background processing (runs independently of HF idle)
436
+ background_tasks.add_task(generate_storybook_background, job_id)
437
+
438
+ # Immediate response for n8n
439
+ response_data = {
440
  "status": "success",
441
+ "message": "Storybook generation started successfully",
442
  "job_id": job_id,
443
+ "story_title": story_request.story_title,
444
+ "total_scenes": len(story_request.scenes),
445
+ "callback_url": story_request.callback_url,
446
+ "estimated_time_seconds": len(story_request.scenes) * 30, # ~30s per image
447
+ "timestamp": datetime.now().isoformat()
448
  }
449
+
450
+ print(f"βœ… Job {job_id} started for: {story_request.story_title}")
451
+
452
+ return response_data
453
+
454
  except Exception as e:
455
+ error_msg = f"API Error: {str(e)}"
456
+ print(f"❌ {error_msg}")
457
+ raise HTTPException(status_code=500, detail=error_msg)
458
 
459
+ @app.get("/api/job-status/{job_id}")
460
  async def get_job_status_endpoint(job_id: str):
461
+ """Check job status"""
462
  job_data = job_storage.get(job_id)
463
  if not job_data:
464
  raise HTTPException(status_code=404, detail="Job not found")
 
473
  updated_at=job_data["updated_at"]
474
  )
475
 
476
+ @app.get("/api/health")
477
+ async def api_health():
478
+ """Health check endpoint for n8n"""
479
+ return {
480
+ "status": "healthy",
481
+ "service": "storybook-generator",
482
+ "timestamp": datetime.now().isoformat(),
483
+ "active_jobs": len(job_storage),
484
+ "models_loaded": list(model_cache.keys()),
485
+ "oci_api_connected": OCI_API_BASE_URL
486
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
 
488
  @app.get("/")
489
  async def root():
490
+ return {
491
+ "message": "Storybook Generator API is running!",
492
+ "endpoints": {
493
+ "generate_storybook": "POST /api/generate-storybook",
494
+ "check_status": "GET /api/job-status/{job_id}",
495
+ "health": "GET /api/health"
496
+ },
497
+ "oci_bucket_api": OCI_API_BASE_URL
498
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
  # For Hugging Face Spaces
501
  def get_app():
502
  return app
503
 
504
  if __name__ == "__main__":
505
+ import uvicorn
506
+ print("πŸš€ Starting Storybook Generator API...")
507
+ print("πŸ“š Features: Multi-image generation + Text saving + OCI bucket storage")
508
+ print("πŸ”— OCI API:", OCI_API_BASE_URL)
509
+ print("🌐 API ready at: http://localhost:7860")
510
+
511
+ uvicorn.run(app, host="0.0.0.0", port=7860)