Text-to-Image
Diffusers
English
sdxl
sdxl-turbo
stable-diffusion
image-to-image
image-generation
image-editing
fastapi
mps
Instructions to use sujithputta/Lumaforge with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use sujithputta/Lumaforge with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("sujithputta/Lumaforge", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
- DiffusionBee
Commit Β·
9748021
1
Parent(s): 68fae0c
Deploy architectural upgrades: VRAM CPU offloading, sequential task queue, pipeline registry, and SQLite database manager
Browse files- .gitignore +2 -0
- app.py +258 -108
- lumaforge/database.py +103 -0
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 565 |
-
local_pipeline = pipeline
|
| 566 |
-
if req.device != pipeline.device:
|
| 567 |
-
local_pipeline = LumaForgePipeline(device=req.device)
|
| 568 |
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 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 =
|
| 648 |
-
if req.device != pipeline.device:
|
| 649 |
-
local_pipeline = LumaForgePipeline(device=req.device)
|
| 650 |
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
"
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 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 |
-
|
| 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 =
|
| 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 |
-
#
|
| 968 |
-
|
| 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
|
| 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 |
-
|
| 48 |
-
self.
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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)
|