sujithputta commited on
Commit
9748021
Β·
1 Parent(s): 68fae0c

Deploy architectural upgrades: VRAM CPU offloading, sequential task queue, pipeline registry, and SQLite database manager

Browse files
Files changed (4) hide show
  1. .gitignore +2 -0
  2. app.py +258 -108
  3. lumaforge/database.py +103 -0
  4. lumaforge/pipeline.py +21 -17
.gitignore CHANGED
@@ -6,3 +6,5 @@ weights/
6
  audit_log.jsonl
7
  train_log.json
8
  .DS_Store
 
 
 
6
  audit_log.jsonl
7
  train_log.json
8
  .DS_Store
9
+ *.db
10
+
app.py CHANGED
@@ -5,6 +5,7 @@ import json
5
  import base64
6
  import threading
7
  import uuid
 
8
  from io import BytesIO
9
  from typing import Optional, Dict, Any
10
  from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, Depends
@@ -20,6 +21,7 @@ from lumaforge.safety import SafetyManager
20
  from lumaforge.benchmark import BenchmarkSuite
21
  from lumaforge.dataset_curator import DatasetCurator
22
  from lumaforge.train import LumaForgeTrainer
 
23
 
24
  # Session management for async generation
25
  class GenerationSession:
@@ -106,9 +108,30 @@ app.add_middleware(
106
  # Singletons for backend resources
107
  ollama_client = OllamaClient()
108
  safety_manager = SafetyManager(ollama_client=ollama_client)
109
- pipeline = LumaForgePipeline(device="mps", ollama_client=ollama_client)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  session_manager = SessionManager()
111
 
 
 
 
112
  # Background training tracking
113
  training_thread = None
114
 
@@ -546,6 +569,18 @@ def api_generate(req: GenerateRequest, request: Request):
546
  mod_res = safety_manager.moderate_prompt(req.prompt)
547
 
548
  if mod_res["status"] == "REFUSED":
 
 
 
 
 
 
 
 
 
 
 
 
549
  return {
550
  "status": "REFUSED",
551
  "prompt_metadata": mod_res,
@@ -561,51 +596,82 @@ def api_generate(req: GenerateRequest, request: Request):
561
 
562
  # 3. Image Generation
563
  print(f"[API Generate] Generating image (mock={req.mock}, device={req.device})...")
564
- # If device matches our pipeline device, use existing pipeline, otherwise initialize
565
- local_pipeline = pipeline
566
- if req.device != pipeline.device:
567
- local_pipeline = LumaForgePipeline(device=req.device)
568
 
569
- gen_res = local_pipeline.generate(
570
- prompt=gen_prompt,
571
- aspect_ratio=req.aspect_ratio,
572
- steps=req.steps,
573
- seed=req.seed,
574
- guidance_scale=req.guidance_scale,
575
- negative_prompt=req.negative_prompt,
576
- mock=req.mock
577
- )
578
-
579
- # 4. Save locally for record-keeping and post-safety checks
580
- os.makedirs("outputs", exist_ok=True)
581
- out_path = os.path.join("outputs", f"output_{gen_res['seed']}.png")
582
- gen_res["image"].save(out_path, pnginfo=gen_res.get("pnginfo"))
583
-
584
- # 5. Output Post-generation Screen
585
- post_res = safety_manager.check_output_safety(out_path, mod_res)
586
-
587
- # 6. Convert image to Base64 to return in JSON payload
588
- buffered = BytesIO()
589
- gen_res["image"].save(buffered, format="PNG", pnginfo=gen_res.get("pnginfo"))
590
- img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
591
- image_b64 = f"data:image/png;base64,{img_str}"
592
-
593
- return {
594
- "status": mod_res["status"],
595
- "image_b64": image_b64,
596
- "prompt_metadata": mod_res,
597
- "expanded_prompt": expanded,
598
- "generation_metadata": {
599
- "latency_sec": gen_res["latency_sec"],
600
- "memory_used_mb": gen_res["memory_used_mb"],
601
- "seed": gen_res["seed"],
602
- "width": gen_res["width"],
603
- "height": gen_res["height"],
604
- "device": gen_res["device"],
605
- "used_mock": gen_res["used_mock"]
606
- },
607
- "safety_check": post_res
608
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
 
610
  def decode_base64_image(image_b64: str) -> Image.Image:
611
  try:
@@ -626,6 +692,18 @@ def api_generate_img2img(req: Img2ImgRequest, request: Request):
626
  mod_res = safety_manager.moderate_prompt(req.prompt)
627
 
628
  if mod_res["status"] == "REFUSED":
 
 
 
 
 
 
 
 
 
 
 
 
629
  return {
630
  "status": "REFUSED",
631
  "prompt_metadata": mod_res,
@@ -644,54 +722,86 @@ def api_generate_img2img(req: Img2ImgRequest, request: Request):
644
 
645
  # 4. Image Generation
646
  print(f"[API Generate Img2Img] Generating image (mock={req.mock}, device={req.device}, strength={req.strength})...")
647
- local_pipeline = pipeline
648
- if req.device != pipeline.device:
649
- local_pipeline = LumaForgePipeline(device=req.device)
650
 
651
- gen_res = local_pipeline.generate_img2img(
652
- image=img,
653
- prompt=gen_prompt,
654
- strength=req.strength,
655
- steps=req.steps,
656
- seed=req.seed,
657
- guidance_scale=req.guidance_scale,
658
- negative_prompt=req.negative_prompt,
659
- mock=req.mock
660
- )
661
-
662
- # 5. Save locally for record-keeping and post-safety checks
663
- os.makedirs("outputs", exist_ok=True)
664
- out_path = os.path.join("outputs", f"output_{gen_res['seed']}.png")
665
- gen_res["image"].save(out_path, pnginfo=gen_res.get("pnginfo"))
666
-
667
- # 6. Output Post-generation Screen
668
- post_res = safety_manager.check_output_safety(out_path, mod_res)
669
-
670
- # 7. Convert image to Base64 to return in JSON payload
671
- buffered = BytesIO()
672
- gen_res["image"].save(buffered, format="PNG", pnginfo=gen_res.get("pnginfo"))
673
- img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
674
- image_b64 = f"data:image/png;base64,{img_str}"
675
-
676
- return {
677
- "status": mod_res["status"],
678
- "image_b64": image_b64,
679
- "prompt_metadata": mod_res,
680
- "expanded_prompt": expanded,
681
- "generation_metadata": {
682
- "latency_sec": gen_res["latency_sec"],
683
- "memory_used_mb": gen_res["memory_used_mb"],
684
- "seed": gen_res["seed"],
685
- "width": gen_res["width"],
686
- "height": gen_res["height"],
687
- "steps": gen_res["steps"],
688
- "guidance_scale": gen_res["guidance_scale"],
689
- "strength": gen_res["strength"],
690
- "device": gen_res["device"],
691
- "used_mock": gen_res["used_mock"]
692
- },
693
- "safety_check": post_res
694
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
 
696
  @app.post("/api/upscale")
697
  def api_upscale(req: UpscaleRequest, request: Request):
@@ -789,6 +899,12 @@ def api_audit_log(request: Request, limit: int = 20):
789
  logs = safety_manager.get_audit_logs(limit=limit)
790
  return {"logs": logs}
791
 
 
 
 
 
 
 
792
  def run_train_worker(req: TrainRequest):
793
  trainer = LumaForgeTrainer(device="mps" if req.demo else "cpu")
794
  trainer.run_training(
@@ -863,10 +979,7 @@ def api_curate(req: CurateRequest, request: Request):
863
  def api_benchmark(req: BenchmarkRequest, request: Request):
864
  api_limiter.check_limit(request)
865
 
866
- # Run in a simple separate execution or directly
867
- local_pipeline = pipeline
868
- if req.device != pipeline.device:
869
- local_pipeline = LumaForgePipeline(device=req.device)
870
 
871
  suite = BenchmarkSuite(local_pipeline, safety_manager)
872
  report = suite.run(mock=req.mock)
@@ -890,6 +1003,19 @@ def generate_session_worker(session_id: str, req: GenerateSessionRequest):
890
  "error": "Safety violation. Prompt contains prohibited material."
891
  }
892
  session_manager.update_session(session_id, "error", result, "Safety check failed")
 
 
 
 
 
 
 
 
 
 
 
 
 
893
  return
894
 
895
  final_prompt = mod_res["final_prompt"]
@@ -904,9 +1030,7 @@ def generate_session_worker(session_id: str, req: GenerateSessionRequest):
904
 
905
  # 3. Image Generation
906
  print(f"[Session {session_id}] Generating image (mock={req.mock}, device={req.device})...")
907
- local_pipeline = pipeline
908
- if req.device != pipeline.device:
909
- local_pipeline = LumaForgePipeline(device=req.device)
910
 
911
  gen_res = local_pipeline.generate(
912
  prompt=gen_prompt,
@@ -951,31 +1075,57 @@ def generate_session_worker(session_id: str, req: GenerateSessionRequest):
951
 
952
  session_manager.update_session(session_id, "completed", result)
953
  print(f"[Session {session_id}] Generation completed successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
954
  except Exception as e:
955
  error_msg = str(e)
956
  print(f"[Session {session_id}] Error during generation: {error_msg}")
957
  session_manager.update_session(session_id, "error", None, error_msg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
958
 
959
  @app.post("/api/generate-session/start")
960
  def api_generate_session_start(req: GenerateSessionRequest, request: Request):
961
- """Start a new generation session"""
962
  api_limiter.check_limit(request)
963
 
964
  # Create session
965
  session_id = session_manager.create_session()
966
 
967
- # Start generation in background thread
968
- worker_thread = threading.Thread(
969
- target=generate_session_worker,
970
- args=(session_id, req),
971
- daemon=True
972
- )
973
- worker_thread.start()
974
 
975
  return {
976
  "status": "started",
977
  "session_id": session_id,
978
- "message": "Generation session started. Poll /api/generate-session/status for updates."
979
  }
980
 
981
  @app.post("/api/generate-session/status")
 
5
  import base64
6
  import threading
7
  import uuid
8
+ import concurrent.futures
9
  from io import BytesIO
10
  from typing import Optional, Dict, Any
11
  from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, Depends
 
21
  from lumaforge.benchmark import BenchmarkSuite
22
  from lumaforge.dataset_curator import DatasetCurator
23
  from lumaforge.train import LumaForgeTrainer
24
+ from lumaforge.database import DatabaseManager
25
 
26
  # Session management for async generation
27
  class GenerationSession:
 
108
  # Singletons for backend resources
109
  ollama_client = OllamaClient()
110
  safety_manager = SafetyManager(ollama_client=ollama_client)
111
+ db_manager = DatabaseManager()
112
+
113
+ class PipelineRegistry:
114
+ def __init__(self, ollama_client):
115
+ self.ollama_client = ollama_client
116
+ self._cache = {}
117
+ self._lock = threading.Lock()
118
+
119
+ def get_pipeline(self, device: str) -> LumaForgePipeline:
120
+ with self._lock:
121
+ if device not in self._cache:
122
+ print(f"[PipelineRegistry] Creating and caching pipeline instance for device: {device}")
123
+ self._cache[device] = LumaForgePipeline(device=device, ollama_client=self.ollama_client)
124
+ return self._cache[device]
125
+
126
+ pipeline_registry = PipelineRegistry(ollama_client=ollama_client)
127
+ # Keep global reference to default 'mps' pipeline for backwards compatibility/direct usage
128
+ pipeline = pipeline_registry.get_pipeline("mps")
129
+
130
  session_manager = SessionManager()
131
 
132
+ # Sequential generation queue executor to prevent VRAM / hardware OOM
133
+ generation_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
134
+
135
  # Background training tracking
136
  training_thread = None
137
 
 
569
  mod_res = safety_manager.moderate_prompt(req.prompt)
570
 
571
  if mod_res["status"] == "REFUSED":
572
+ # Log refusal to database
573
+ db_manager.log_generation(
574
+ session_id=f"direct-refused-{uuid.uuid4()}",
575
+ prompt=req.prompt,
576
+ status="refused",
577
+ negative_prompt=req.negative_prompt,
578
+ steps=req.steps,
579
+ guidance_scale=req.guidance_scale,
580
+ seed=req.seed,
581
+ aspect_ratio=req.aspect_ratio,
582
+ device=req.device
583
+ )
584
  return {
585
  "status": "REFUSED",
586
  "prompt_metadata": mod_res,
 
596
 
597
  # 3. Image Generation
598
  print(f"[API Generate] Generating image (mock={req.mock}, device={req.device})...")
599
+ local_pipeline = pipeline_registry.get_pipeline(req.device)
 
 
 
600
 
601
+ try:
602
+ gen_res = local_pipeline.generate(
603
+ prompt=gen_prompt,
604
+ aspect_ratio=req.aspect_ratio,
605
+ steps=req.steps,
606
+ seed=req.seed,
607
+ guidance_scale=req.guidance_scale,
608
+ negative_prompt=req.negative_prompt,
609
+ mock=req.mock
610
+ )
611
+
612
+ # 4. Save locally for record-keeping and post-safety checks
613
+ os.makedirs("outputs", exist_ok=True)
614
+ out_path = os.path.join("outputs", f"output_{gen_res['seed']}.png")
615
+ gen_res["image"].save(out_path, pnginfo=gen_res.get("pnginfo"))
616
+
617
+ # 5. Output Post-generation Screen
618
+ post_res = safety_manager.check_output_safety(out_path, mod_res)
619
+
620
+ # 6. Convert image to Base64 to return in JSON payload
621
+ buffered = BytesIO()
622
+ gen_res["image"].save(buffered, format="PNG", pnginfo=gen_res.get("pnginfo"))
623
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
624
+ image_b64 = f"data:image/png;base64,{img_str}"
625
+
626
+ # Log successful generation
627
+ db_manager.log_generation(
628
+ session_id=f"direct-{uuid.uuid4()}",
629
+ prompt=req.prompt,
630
+ status="completed",
631
+ expanded_prompt=expanded,
632
+ negative_prompt=req.negative_prompt,
633
+ steps=req.steps,
634
+ guidance_scale=req.guidance_scale,
635
+ seed=gen_res["seed"],
636
+ aspect_ratio=req.aspect_ratio,
637
+ device=gen_res["device"],
638
+ latency_sec=gen_res["latency_sec"],
639
+ memory_used_mb=gen_res["memory_used_mb"],
640
+ image_path=out_path
641
+ )
642
+
643
+ return {
644
+ "status": mod_res["status"],
645
+ "image_b64": image_b64,
646
+ "prompt_metadata": mod_res,
647
+ "expanded_prompt": expanded,
648
+ "generation_metadata": {
649
+ "latency_sec": gen_res["latency_sec"],
650
+ "memory_used_mb": gen_res["memory_used_mb"],
651
+ "seed": gen_res["seed"],
652
+ "width": gen_res["width"],
653
+ "height": gen_res["height"],
654
+ "device": gen_res["device"],
655
+ "used_mock": gen_res["used_mock"]
656
+ },
657
+ "safety_check": post_res
658
+ }
659
+ except Exception as e:
660
+ error_msg = str(e)
661
+ print(f"[API Generate Error] Inference failed: {error_msg}")
662
+ db_manager.log_generation(
663
+ session_id=f"direct-failed-{uuid.uuid4()}",
664
+ prompt=req.prompt,
665
+ status="error",
666
+ negative_prompt=req.negative_prompt,
667
+ steps=req.steps,
668
+ guidance_scale=req.guidance_scale,
669
+ seed=req.seed,
670
+ aspect_ratio=req.aspect_ratio,
671
+ device=req.device,
672
+ error_message=error_msg
673
+ )
674
+ raise HTTPException(status_code=500, detail=f"Generation failed: {error_msg}")
675
 
676
  def decode_base64_image(image_b64: str) -> Image.Image:
677
  try:
 
692
  mod_res = safety_manager.moderate_prompt(req.prompt)
693
 
694
  if mod_res["status"] == "REFUSED":
695
+ # Log refusal to database
696
+ db_manager.log_generation(
697
+ session_id=f"direct-img2img-refused-{uuid.uuid4()}",
698
+ prompt=req.prompt,
699
+ status="refused",
700
+ negative_prompt=req.negative_prompt,
701
+ steps=req.steps,
702
+ guidance_scale=req.guidance_scale,
703
+ seed=req.seed,
704
+ aspect_ratio="1:1",
705
+ device=req.device
706
+ )
707
  return {
708
  "status": "REFUSED",
709
  "prompt_metadata": mod_res,
 
722
 
723
  # 4. Image Generation
724
  print(f"[API Generate Img2Img] Generating image (mock={req.mock}, device={req.device}, strength={req.strength})...")
725
+ local_pipeline = pipeline_registry.get_pipeline(req.device)
 
 
726
 
727
+ try:
728
+ gen_res = local_pipeline.generate_img2img(
729
+ image=img,
730
+ prompt=gen_prompt,
731
+ strength=req.strength,
732
+ steps=req.steps,
733
+ seed=req.seed,
734
+ guidance_scale=req.guidance_scale,
735
+ negative_prompt=req.negative_prompt,
736
+ mock=req.mock
737
+ )
738
+
739
+ # 5. Save locally for record-keeping and post-safety checks
740
+ os.makedirs("outputs", exist_ok=True)
741
+ out_path = os.path.join("outputs", f"output_{gen_res['seed']}.png")
742
+ gen_res["image"].save(out_path, pnginfo=gen_res.get("pnginfo"))
743
+
744
+ # 6. Output Post-generation Screen
745
+ post_res = safety_manager.check_output_safety(out_path, mod_res)
746
+
747
+ # 7. Convert image to Base64 to return in JSON payload
748
+ buffered = BytesIO()
749
+ gen_res["image"].save(buffered, format="PNG", pnginfo=gen_res.get("pnginfo"))
750
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
751
+ image_b64 = f"data:image/png;base64,{img_str}"
752
+
753
+ # Log successful generation to SQLite
754
+ db_manager.log_generation(
755
+ session_id=f"direct-img2img-{uuid.uuid4()}",
756
+ prompt=req.prompt,
757
+ status="completed",
758
+ expanded_prompt=expanded,
759
+ negative_prompt=req.negative_prompt,
760
+ steps=req.steps,
761
+ guidance_scale=req.guidance_scale,
762
+ seed=gen_res["seed"],
763
+ aspect_ratio="1:1",
764
+ device=gen_res["device"],
765
+ latency_sec=gen_res["latency_sec"],
766
+ memory_used_mb=gen_res["memory_used_mb"],
767
+ image_path=out_path
768
+ )
769
+
770
+ return {
771
+ "status": mod_res["status"],
772
+ "image_b64": image_b64,
773
+ "prompt_metadata": mod_res,
774
+ "expanded_prompt": expanded,
775
+ "generation_metadata": {
776
+ "latency_sec": gen_res["latency_sec"],
777
+ "memory_used_mb": gen_res["memory_used_mb"],
778
+ "seed": gen_res["seed"],
779
+ "width": gen_res["width"],
780
+ "height": gen_res["height"],
781
+ "steps": gen_res["steps"],
782
+ "guidance_scale": gen_res["guidance_scale"],
783
+ "strength": gen_res["strength"],
784
+ "device": gen_res["device"],
785
+ "used_mock": gen_res["used_mock"]
786
+ },
787
+ "safety_check": post_res
788
+ }
789
+ except Exception as e:
790
+ error_msg = str(e)
791
+ print(f"[API Generate Img2Img Error] Inference failed: {error_msg}")
792
+ db_manager.log_generation(
793
+ session_id=f"direct-img2img-failed-{uuid.uuid4()}",
794
+ prompt=req.prompt,
795
+ status="error",
796
+ negative_prompt=req.negative_prompt,
797
+ steps=req.steps,
798
+ guidance_scale=req.guidance_scale,
799
+ seed=req.seed,
800
+ aspect_ratio="1:1",
801
+ device=req.device,
802
+ error_message=error_msg
803
+ )
804
+ raise HTTPException(status_code=500, detail=f"Img2Img generation failed: {error_msg}")
805
 
806
  @app.post("/api/upscale")
807
  def api_upscale(req: UpscaleRequest, request: Request):
 
899
  logs = safety_manager.get_audit_logs(limit=limit)
900
  return {"logs": logs}
901
 
902
+ @app.get("/api/history")
903
+ def api_history(request: Request, limit: int = 50):
904
+ api_limiter.check_limit(request)
905
+ history = db_manager.get_history(limit=limit)
906
+ return {"history": history}
907
+
908
  def run_train_worker(req: TrainRequest):
909
  trainer = LumaForgeTrainer(device="mps" if req.demo else "cpu")
910
  trainer.run_training(
 
979
  def api_benchmark(req: BenchmarkRequest, request: Request):
980
  api_limiter.check_limit(request)
981
 
982
+ local_pipeline = pipeline_registry.get_pipeline(req.device)
 
 
 
983
 
984
  suite = BenchmarkSuite(local_pipeline, safety_manager)
985
  report = suite.run(mock=req.mock)
 
1003
  "error": "Safety violation. Prompt contains prohibited material."
1004
  }
1005
  session_manager.update_session(session_id, "error", result, "Safety check failed")
1006
+
1007
+ # Log refusal to SQLite
1008
+ db_manager.log_generation(
1009
+ session_id=session_id,
1010
+ prompt=req.prompt,
1011
+ status="refused",
1012
+ negative_prompt=req.negative_prompt,
1013
+ steps=req.steps,
1014
+ guidance_scale=req.guidance_scale,
1015
+ seed=req.seed,
1016
+ aspect_ratio=req.aspect_ratio,
1017
+ device=req.device
1018
+ )
1019
  return
1020
 
1021
  final_prompt = mod_res["final_prompt"]
 
1030
 
1031
  # 3. Image Generation
1032
  print(f"[Session {session_id}] Generating image (mock={req.mock}, device={req.device})...")
1033
+ local_pipeline = pipeline_registry.get_pipeline(req.device)
 
 
1034
 
1035
  gen_res = local_pipeline.generate(
1036
  prompt=gen_prompt,
 
1075
 
1076
  session_manager.update_session(session_id, "completed", result)
1077
  print(f"[Session {session_id}] Generation completed successfully")
1078
+
1079
+ # Log successful generation to SQLite
1080
+ db_manager.log_generation(
1081
+ session_id=session_id,
1082
+ prompt=req.prompt,
1083
+ status="completed",
1084
+ expanded_prompt=expanded,
1085
+ negative_prompt=req.negative_prompt,
1086
+ steps=req.steps,
1087
+ guidance_scale=req.guidance_scale,
1088
+ seed=gen_res["seed"],
1089
+ aspect_ratio=req.aspect_ratio,
1090
+ device=gen_res["device"],
1091
+ latency_sec=gen_res["latency_sec"],
1092
+ memory_used_mb=gen_res["memory_used_mb"],
1093
+ image_path=out_path
1094
+ )
1095
  except Exception as e:
1096
  error_msg = str(e)
1097
  print(f"[Session {session_id}] Error during generation: {error_msg}")
1098
  session_manager.update_session(session_id, "error", None, error_msg)
1099
+
1100
+ # Log error to SQLite
1101
+ db_manager.log_generation(
1102
+ session_id=session_id,
1103
+ prompt=req.prompt,
1104
+ status="error",
1105
+ negative_prompt=req.negative_prompt,
1106
+ steps=req.steps,
1107
+ guidance_scale=req.guidance_scale,
1108
+ seed=req.seed,
1109
+ aspect_ratio=req.aspect_ratio,
1110
+ device=req.device,
1111
+ error_message=error_msg
1112
+ )
1113
 
1114
  @app.post("/api/generate-session/start")
1115
  def api_generate_session_start(req: GenerateSessionRequest, request: Request):
1116
+ """Start a new generation session in the sequential task queue"""
1117
  api_limiter.check_limit(request)
1118
 
1119
  # Create session
1120
  session_id = session_manager.create_session()
1121
 
1122
+ # Queue generation session in thread pool executor (sequential queue)
1123
+ generation_executor.submit(generate_session_worker, session_id, req)
 
 
 
 
 
1124
 
1125
  return {
1126
  "status": "started",
1127
  "session_id": session_id,
1128
+ "message": "Generation session queued. Poll /api/generate-session/status for updates."
1129
  }
1130
 
1131
  @app.post("/api/generate-session/status")
lumaforge/database.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import json
3
+ import os
4
+ from datetime import datetime
5
+
6
+ class DatabaseManager:
7
+ def __init__(self, db_path="lumaforge.db"):
8
+ self.db_path = db_path
9
+ self.init_db()
10
+
11
+ def _get_connection(self):
12
+ """Creates a new SQLite database connection."""
13
+ conn = sqlite3.connect(self.db_path, check_same_thread=False)
14
+ conn.row_factory = sqlite3.Row
15
+ return conn
16
+
17
+ def init_db(self):
18
+ """Initializes the database and creates the generations table if it doesn't exist."""
19
+ query = """
20
+ CREATE TABLE IF NOT EXISTS generations (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ session_id TEXT UNIQUE,
23
+ prompt TEXT NOT NULL,
24
+ expanded_prompt TEXT, -- JSON string
25
+ negative_prompt TEXT,
26
+ steps INTEGER,
27
+ guidance_scale REAL,
28
+ seed INTEGER,
29
+ aspect_ratio TEXT,
30
+ device TEXT,
31
+ latency_sec REAL,
32
+ memory_used_mb REAL,
33
+ status TEXT, -- 'completed', 'refused', 'error'
34
+ image_path TEXT, -- Path to the stored file on disk
35
+ error_message TEXT, -- Holds error logs if failed
36
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
37
+ );
38
+ """
39
+ conn = self._get_connection()
40
+ try:
41
+ with conn:
42
+ conn.execute(query)
43
+ print(f"[DatabaseManager] Database initialized at {os.path.abspath(self.db_path)}")
44
+ finally:
45
+ conn.close()
46
+
47
+ def log_generation(self, session_id: str, prompt: str, status: str,
48
+ expanded_prompt: dict = None, negative_prompt: str = None,
49
+ steps: int = None, guidance_scale: float = None,
50
+ seed: int = -1, aspect_ratio: str = "1:1", device: str = "mps",
51
+ latency_sec: float = 0.0, memory_used_mb: float = 0.0,
52
+ image_path: str = None, error_message: str = None):
53
+ """Logs a generation run (success or failure) to the SQLite database."""
54
+ query = """
55
+ INSERT OR REPLACE INTO generations (
56
+ session_id, prompt, expanded_prompt, negative_prompt, steps,
57
+ guidance_scale, seed, aspect_ratio, device, latency_sec,
58
+ memory_used_mb, status, image_path, error_message
59
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
60
+ """
61
+
62
+ # Serialize dict to JSON string
63
+ expanded_prompt_json = json.dumps(expanded_prompt) if expanded_prompt else None
64
+
65
+ conn = self._get_connection()
66
+ try:
67
+ with conn:
68
+ conn.execute(query, (
69
+ session_id, prompt, expanded_prompt_json, negative_prompt, steps,
70
+ guidance_scale, seed, aspect_ratio, device, latency_sec,
71
+ memory_used_mb, status, image_path, error_message
72
+ ))
73
+ print(f"[DatabaseManager] Logged session {session_id} to database (status: {status})")
74
+ except Exception as e:
75
+ print(f"[DatabaseManager Error] Failed to log session {session_id}: {e}")
76
+ finally:
77
+ conn.close()
78
+
79
+ def get_history(self, limit: int = 50) -> list:
80
+ """Retrieves historical generation logs, returning a list of dicts."""
81
+ query = "SELECT * FROM generations ORDER BY created_at DESC LIMIT ?;"
82
+ conn = self._get_connection()
83
+ try:
84
+ cursor = conn.cursor()
85
+ cursor.execute(query, (limit,))
86
+ rows = cursor.fetchall()
87
+
88
+ history = []
89
+ for row in rows:
90
+ item = dict(row)
91
+ # Deserialize expanded prompt back to dict
92
+ if item["expanded_prompt"]:
93
+ try:
94
+ item["expanded_prompt"] = json.loads(item["expanded_prompt"])
95
+ except Exception:
96
+ pass
97
+ history.append(item)
98
+ return history
99
+ except Exception as e:
100
+ print(f"[DatabaseManager Error] Failed to retrieve history: {e}")
101
+ return []
102
+ finally:
103
+ conn.close()
lumaforge/pipeline.py CHANGED
@@ -44,21 +44,25 @@ class LumaForgePipeline:
44
  )
45
 
46
  print(f"[LumaForgePipeline] βœ… SD 3.5 Medium loaded successfully")
47
- print(f"[LumaForgePipeline] Moving pipeline to {self.device}...")
48
- self.pipe.to(self.device)
49
- # Keep VAE in float16 to match input latents on MPS (prevent c10::Half / float mismatch)
50
- # if self.device == "mps":
51
- # print("[LumaForgePipeline] Upcasting VAE decoder to float32 precision for MPS...")
52
- # self.pipe.vae.to(dtype=torch.float32)
53
- # print("[LumaForgePipeline] βœ… VAE upcasted successfully.")
54
-
55
- print(f"[LumaForgePipeline] βœ… Pipeline successfully moved to {self.device}")
56
-
57
- # Memory optimization
58
- if self.device == "mps":
59
- print(f"[LumaForgePipeline] Enabling attention slicing for MPS memory optimization...")
60
- self.pipe.enable_attention_slicing()
61
- print(f"[LumaForgePipeline] βœ… Attention slicing enabled.")
 
 
 
 
62
 
63
  self.is_loaded = True
64
  print("[LumaForgePipeline] βœ… SD 3.5 Medium ready for inference!")
@@ -1206,7 +1210,7 @@ class LumaForgePipeline:
1206
  draw_gradient_text(
1207
  overlay, (tx, ty), title_text, t_font, spacing=t_spacing,
1208
  top_color=(255, 255, 255), bottom_color=(235, 235, 240),
1209
- shadow_fill=(0, 0, 0, 100), shadow_offset=(1, 1)
1210
  )
1211
 
1212
  # Gold separator line under title
@@ -1230,7 +1234,7 @@ class LumaForgePipeline:
1230
  s_w = len(sub_text) * 10
1231
  sx = (width - s_w) // 2
1232
  sy = ty - int(height * 0.05)
1233
- draw_spaced_text(draw_overlay, (sx, sy), sub_text, s_font, fill=(212, 175, 55, 220), spacing=4)
1234
 
1235
  else:
1236
  # 3. Cinematic Action Theme (Default)
 
44
  )
45
 
46
  print(f"[LumaForgePipeline] βœ… SD 3.5 Medium loaded successfully")
47
+ # Memory optimization & Device placement
48
+ if self.device in ["mps", "cuda"]:
49
+ try:
50
+ print(f"[LumaForgePipeline] Enabling model CPU offloading for {self.device} memory optimization...")
51
+ self.pipe.enable_model_cpu_offload(device=self.device)
52
+ print(f"[LumaForgePipeline] βœ… Model CPU offloading enabled.")
53
+ except Exception as e:
54
+ print(f"[LumaForgePipeline Warning] Failed to enable CPU offloading: {e}. Falling back to full device load.")
55
+ print(f"[LumaForgePipeline] Moving pipeline to {self.device}...")
56
+ self.pipe.to(self.device)
57
+ print(f"[LumaForgePipeline] βœ… Pipeline successfully moved to {self.device}")
58
+ if self.device == "mps":
59
+ print(f"[LumaForgePipeline] Enabling attention slicing for MPS memory optimization...")
60
+ self.pipe.enable_attention_slicing()
61
+ print(f"[LumaForgePipeline] βœ… Attention slicing enabled.")
62
+ else:
63
+ print(f"[LumaForgePipeline] Moving pipeline to {self.device}...")
64
+ self.pipe.to(self.device)
65
+ print(f"[LumaForgePipeline] βœ… Pipeline successfully moved to {self.device}")
66
 
67
  self.is_loaded = True
68
  print("[LumaForgePipeline] βœ… SD 3.5 Medium ready for inference!")
 
1210
  draw_gradient_text(
1211
  overlay, (tx, ty), title_text, t_font, spacing=t_spacing,
1212
  top_color=(255, 255, 255), bottom_color=(235, 235, 240),
1213
+ shadow_fill=(0, 0, 0, 180), shadow_offset=(2, 2)
1214
  )
1215
 
1216
  # Gold separator line under title
 
1234
  s_w = len(sub_text) * 10
1235
  sx = (width - s_w) // 2
1236
  sy = ty - int(height * 0.05)
1237
+ draw_spaced_text(draw_overlay, (sx, sy), sub_text, s_font, fill=(212, 175, 55, 220), spacing=4, shadow_fill=(0, 0, 0, 160), shadow_offset=(1, 1))
1238
 
1239
  else:
1240
  # 3. Cinematic Action Theme (Default)