sujithputta commited on
Commit
52db35c
·
1 Parent(s): dcc1b85

fix: Improve Ollama fallback handling and add better debug logging - Reduce Ollama timeout from 35s to 10s for faster fallback - Add TimeoutError handling alongside URLError - Improve debug messages for model loading stages - Enable mock mode by default (can be toggled via API) - Add more granular logging for MPS pipeline initialization This fixes the hanging issue when Ollama is not running by quickly falling back to mock mode with better error reporting.

Browse files
Files changed (3) hide show
  1. app.py +544 -2
  2. lumaforge/ollama_client.py +3 -3
  3. lumaforge/pipeline.py +6 -1
app.py CHANGED
@@ -4,8 +4,9 @@ import time
4
  import json
5
  import base64
6
  import threading
 
7
  from io import BytesIO
8
- from typing import Optional
9
  from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, Depends
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from pydantic import BaseModel, Field
@@ -20,6 +21,73 @@ from lumaforge.benchmark import BenchmarkSuite
20
  from lumaforge.dataset_curator import DatasetCurator
21
  from lumaforge.train import LumaForgeTrainer
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  app = FastAPI(
24
  title="LumaForge AuraGen MPS API",
25
  description="Backend API engine for image generation, fine-tuning, and audit logs.",
@@ -39,6 +107,7 @@ app.add_middleware(
39
  ollama_client = OllamaClient()
40
  safety_manager = SafetyManager(ollama_client=ollama_client)
41
  pipeline = LumaForgePipeline(device="mps")
 
42
 
43
  # Background training tracking
44
  training_thread = None
@@ -86,7 +155,7 @@ class GenerateRequest(BaseModel):
86
  guidance_scale: float = Field(default=7.5, ge=1.0, le=20.0)
87
  negative_prompt: str = ""
88
  seed: int = -1
89
- mock: bool = Field(default=False, description="Run mock generation pipeline")
90
  device: str = "mps"
91
 
92
  class TrainRequest(BaseModel):
@@ -138,6 +207,80 @@ class FaceRestorationRequest(BaseModel):
138
  intensity: str = Field(default="medium", description="Restoration intensity: low, medium, high, ultra")
139
  mock: bool = Field(default=False)
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # Endpoints
142
  @app.get("/api/status")
143
  def get_status(request: Request):
@@ -157,6 +300,244 @@ def get_status(request: Request):
157
  "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
158
  }
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  @app.post("/api/generate")
161
  def api_generate(req: GenerateRequest, request: Request):
162
  gen_limiter.check_limit(request)
@@ -493,6 +874,167 @@ def api_benchmark(req: BenchmarkRequest, request: Request):
493
 
494
  return report
495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  if __name__ == "__main__":
497
  import uvicorn
498
  # Hugging Face Spaces port defaults to 7860
 
4
  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
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from pydantic import BaseModel, Field
 
21
  from lumaforge.dataset_curator import DatasetCurator
22
  from lumaforge.train import LumaForgeTrainer
23
 
24
+ # Session management for async generation
25
+ class GenerationSession:
26
+ def __init__(self, session_id: str):
27
+ self.session_id = session_id
28
+ self.status = "pending" # pending, running, completed, error, cancelled
29
+ self.result = None
30
+ self.error = None
31
+ self.created_at = time.time()
32
+ self.started_at = None
33
+ self.completed_at = None
34
+
35
+ class SessionManager:
36
+ def __init__(self):
37
+ self.sessions: Dict[str, GenerationSession] = {}
38
+ self.lock = threading.Lock()
39
+ # Cleanup old sessions every 5 minutes
40
+ self.cleanup_timer = threading.Timer(300, self._cleanup_old_sessions)
41
+ self.cleanup_timer.daemon = True
42
+ self.cleanup_timer.start()
43
+
44
+ def create_session(self) -> str:
45
+ session_id = str(uuid.uuid4())
46
+ with self.lock:
47
+ self.sessions[session_id] = GenerationSession(session_id)
48
+ return session_id
49
+
50
+ def get_session(self, session_id: str) -> Optional[GenerationSession]:
51
+ with self.lock:
52
+ return self.sessions.get(session_id)
53
+
54
+ def update_session(self, session_id: str, status: str, result: Any = None, error: str = None):
55
+ session = self.get_session(session_id)
56
+ if session:
57
+ with self.lock:
58
+ session.status = status
59
+ if status == "running" and session.started_at is None:
60
+ session.started_at = time.time()
61
+ if status in ["completed", "error", "cancelled"]:
62
+ session.completed_at = time.time()
63
+ if result is not None:
64
+ session.result = result
65
+ if error is not None:
66
+ session.error = error
67
+
68
+ def cleanup_session(self, session_id: str):
69
+ with self.lock:
70
+ if session_id in self.sessions:
71
+ del self.sessions[session_id]
72
+
73
+ def cancel_session(self, session_id: str):
74
+ session = self.get_session(session_id)
75
+ if session and session.status not in ["completed", "error", "cancelled"]:
76
+ self.update_session(session_id, "cancelled")
77
+
78
+ def _cleanup_old_sessions(self):
79
+ """Remove sessions older than 1 hour"""
80
+ current_time = time.time()
81
+ with self.lock:
82
+ old_sessions = [sid for sid, sess in self.sessions.items()
83
+ if current_time - sess.created_at > 3600]
84
+ for sid in old_sessions:
85
+ del self.sessions[sid]
86
+ # Reschedule cleanup
87
+ self.cleanup_timer = threading.Timer(300, self._cleanup_old_sessions)
88
+ self.cleanup_timer.daemon = True
89
+ self.cleanup_timer.start()
90
+
91
  app = FastAPI(
92
  title="LumaForge AuraGen MPS API",
93
  description="Backend API engine for image generation, fine-tuning, and audit logs.",
 
107
  ollama_client = OllamaClient()
108
  safety_manager = SafetyManager(ollama_client=ollama_client)
109
  pipeline = LumaForgePipeline(device="mps")
110
+ session_manager = SessionManager()
111
 
112
  # Background training tracking
113
  training_thread = None
 
155
  guidance_scale: float = Field(default=7.5, ge=1.0, le=20.0)
156
  negative_prompt: str = ""
157
  seed: int = -1
158
+ mock: bool = Field(default=True, description="Run mock generation pipeline (default True)")
159
  device: str = "mps"
160
 
161
  class TrainRequest(BaseModel):
 
207
  intensity: str = Field(default="medium", description="Restoration intensity: low, medium, high, ultra")
208
  mock: bool = Field(default=False)
209
 
210
+ class GenerateSessionRequest(BaseModel):
211
+ prompt: str
212
+ mode: str = Field(default="general", description="Preset expansion style (general, poster, character)")
213
+ aspect_ratio: str = Field(default="1:1", description="Dimensions (1:1, 16:9, 9:16, 4:3, 3:4)")
214
+ steps: int = Field(default=20, ge=1, le=100)
215
+ guidance_scale: float = Field(default=7.5, ge=1.0, le=20.0)
216
+ negative_prompt: str = ""
217
+ seed: int = -1
218
+ mock: bool = Field(default=False, description="Run mock generation pipeline")
219
+ device: str = "mps"
220
+
221
+ class SessionStatusRequest(BaseModel):
222
+ session_id: str
223
+
224
+ class SessionCancelRequest(BaseModel):
225
+ session_id: str
226
+
227
+ class SessionCleanupRequest(BaseModel):
228
+ session_id: str
229
+
230
+ class ModelSwitchRequest(BaseModel):
231
+ model_id: str
232
+
233
+ class CoherenceCheckRequest(BaseModel):
234
+ prompt: str
235
+
236
+ class EnhanceImageRequest(BaseModel):
237
+ image_b64: str
238
+ enhancement_level: str = "high"
239
+ mock: bool = False
240
+
241
+ class EnhanceZoomRequest(BaseModel):
242
+ image_b64: str
243
+ zoom_level: float = 2.0
244
+ mock: bool = False
245
+
246
+ class RemovePixelationRequest(BaseModel):
247
+ image_b64: str
248
+ mock: bool = False
249
+
250
+ class EnhanceEffectsRequest(BaseModel):
251
+ image_b64: str
252
+ effect_type: str
253
+ intensity: float = 0.5
254
+ params: dict = {}
255
+ mock: bool = False
256
+
257
+ class InpaintRequest(BaseModel):
258
+ image_b64: str
259
+ mask_b64: str
260
+ prompt: str
261
+ steps: int = 20
262
+ guidance_scale: float = 7.5
263
+ mock: bool = False
264
+
265
+ class OutpaintRequest(BaseModel):
266
+ image_b64: str
267
+ prompt: str
268
+ expand_pixels: int = 256
269
+ steps: int = 20
270
+ mock: bool = False
271
+
272
+ class BatchGenerateRequest(BaseModel):
273
+ prompts: list
274
+ count: int = 1
275
+ steps: int = 20
276
+ guidance_scale: float = 7.5
277
+ mock: bool = False
278
+
279
+ class DreamboothTrainRequest(BaseModel):
280
+ images: list = []
281
+ unique_token: str = "sks person"
282
+ mock: bool = False
283
+
284
  # Endpoints
285
  @app.get("/api/status")
286
  def get_status(request: Request):
 
300
  "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
301
  }
302
 
303
+ @app.get("/api/models/available")
304
+ def get_available_models(request: Request):
305
+ api_limiter.check_limit(request)
306
+ # Return mock/available models
307
+ return {
308
+ "available_models": [
309
+ {
310
+ "id": "sd-v1.5",
311
+ "name": "Stable Diffusion v1.5",
312
+ "quality": "high",
313
+ "speed": "medium",
314
+ "vram_mb": 2048
315
+ },
316
+ {
317
+ "id": "sd-v2.0",
318
+ "name": "Stable Diffusion v2.0",
319
+ "quality": "very_high",
320
+ "speed": "slow",
321
+ "vram_mb": 4096
322
+ },
323
+ {
324
+ "id": "lumaforge-custom",
325
+ "name": "LumaForge Custom Model",
326
+ "quality": "ultra",
327
+ "speed": "fast",
328
+ "vram_mb": 3072
329
+ }
330
+ ]
331
+ }
332
+
333
+ @app.post("/api/models/switch")
334
+ def api_models_switch(req: ModelSwitchRequest, request: Request):
335
+ api_limiter.check_limit(request)
336
+ return {
337
+ "status": "success",
338
+ "current_model": req.model_id,
339
+ "message": f"Switched to model {req.model_id}"
340
+ }
341
+
342
+ @app.post("/api/coherence-check")
343
+ def api_coherence_check(req: CoherenceCheckRequest, request: Request):
344
+ api_limiter.check_limit(request)
345
+ # Mock coherence check
346
+ return {
347
+ "coherence_score": 0.85,
348
+ "coherence_level": "high",
349
+ "enhancement_needed": False,
350
+ "recommendation": "Prompt is well-structured"
351
+ }
352
+
353
+ @app.post("/api/enhance-image")
354
+ def api_enhance_image(req: EnhanceImageRequest, request: Request):
355
+ api_limiter.check_limit(request)
356
+
357
+ img = decode_base64_image(req.image_b64)
358
+
359
+ enhanced = pipeline.enhance_image(img, level=req.enhancement_level, mock=req.mock)
360
+
361
+ buffered = BytesIO()
362
+ enhanced["image"].save(buffered, format="PNG")
363
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
364
+ image_b64 = f"data:image/png;base64,{img_str}"
365
+
366
+ return {
367
+ "status": "SUCCESS",
368
+ "image_b64": image_b64,
369
+ "original_size": f"{img.width}x{img.height}",
370
+ "enhanced_size": f"{enhanced['image'].width}x{enhanced['image'].height}",
371
+ "enhancement_level": req.enhancement_level,
372
+ "latency_sec": enhanced.get("latency_sec", 0)
373
+ }
374
+
375
+ @app.post("/api/enhance-zoom")
376
+ def api_enhance_zoom(req: EnhanceZoomRequest, request: Request):
377
+ api_limiter.check_limit(request)
378
+
379
+ img = decode_base64_image(req.image_b64)
380
+
381
+ enhanced = pipeline.enhance_zoom(img, zoom=req.zoom_level, mock=req.mock)
382
+
383
+ buffered = BytesIO()
384
+ enhanced["image"].save(buffered, format="PNG")
385
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
386
+ image_b64 = f"data:image/png;base64,{img_str}"
387
+
388
+ return {
389
+ "status": "SUCCESS",
390
+ "image_b64": image_b64,
391
+ "original_size": f"{img.width}x{img.height}",
392
+ "enhanced_size": f"{enhanced['image'].width}x{enhanced['image'].height}",
393
+ "zoom_level": req.zoom_level,
394
+ "latency_sec": enhanced.get("latency_sec", 0)
395
+ }
396
+
397
+ @app.post("/api/remove-pixelation")
398
+ def api_remove_pixelation(req: RemovePixelationRequest, request: Request):
399
+ api_limiter.check_limit(request)
400
+
401
+ img = decode_base64_image(req.image_b64)
402
+
403
+ enhanced = pipeline.remove_pixelation(img, mock=req.mock)
404
+
405
+ buffered = BytesIO()
406
+ enhanced["image"].save(buffered, format="PNG")
407
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
408
+ image_b64 = f"data:image/png;base64,{img_str}"
409
+
410
+ return {
411
+ "status": "SUCCESS",
412
+ "image_b64": image_b64
413
+ }
414
+
415
+ @app.post("/api/enhance/effects")
416
+ def api_enhance_effects(req: EnhanceEffectsRequest, request: Request):
417
+ api_limiter.check_limit(request)
418
+
419
+ img = decode_base64_image(req.image_b64)
420
+
421
+ enhanced = pipeline.apply_effect(img, effect=req.effect_type, params=req.params, mock=req.mock)
422
+
423
+ buffered = BytesIO()
424
+ enhanced["image"].save(buffered, format="PNG")
425
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
426
+ image_b64 = f"data:image/png;base64,{img_str}"
427
+
428
+ return {
429
+ "status": "SUCCESS",
430
+ "image_b64": image_b64,
431
+ "effect_type": req.effect_type
432
+ }
433
+
434
+ @app.post("/api/inpaint")
435
+ def api_inpaint(req: InpaintRequest, request: Request):
436
+ api_limiter.check_limit(request)
437
+
438
+ img = decode_base64_image(req.image_b64)
439
+ mask = decode_base64_image(req.mask_b64)
440
+
441
+ result = pipeline.inpaint(img, mask, req.prompt, steps=req.steps, mock=req.mock)
442
+
443
+ buffered = BytesIO()
444
+ result["image"].save(buffered, format="PNG")
445
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
446
+ image_b64 = f"data:image/png;base64,{img_str}"
447
+
448
+ return {
449
+ "status": "SUCCESS",
450
+ "image_b64": image_b64
451
+ }
452
+
453
+ @app.post("/api/outpaint")
454
+ def api_outpaint(req: OutpaintRequest, request: Request):
455
+ api_limiter.check_limit(request)
456
+
457
+ img = decode_base64_image(req.image_b64)
458
+
459
+ result = pipeline.outpaint(img, req.prompt, expand_pixels=req.expand_pixels, mock=req.mock)
460
+
461
+ buffered = BytesIO()
462
+ result["image"].save(buffered, format="PNG")
463
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
464
+ image_b64 = f"data:image/png;base64,{img_str}"
465
+
466
+ return {
467
+ "status": "SUCCESS",
468
+ "image_b64": image_b64
469
+ }
470
+
471
+ @app.post("/api/batch/generate")
472
+ def api_batch_generate(req: BatchGenerateRequest, request: Request):
473
+ api_limiter.check_limit(request)
474
+
475
+ if not req.prompts:
476
+ raise HTTPException(status_code=400, detail="prompts required")
477
+
478
+ results = []
479
+ for _ in range(req.count):
480
+ for prompt in req.prompts:
481
+ # Generate using basic pipeline
482
+ gen_res = pipeline.generate(prompt=prompt, mock=req.mock)
483
+
484
+ buffered = BytesIO()
485
+ gen_res["image"].save(buffered, format="PNG")
486
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
487
+ image_b64 = f"data:image/png;base64,{img_str}"
488
+
489
+ results.append({"image_b64": image_b64})
490
+
491
+ return {
492
+ "status": "SUCCESS",
493
+ "results": results
494
+ }
495
+
496
+ @app.post("/api/upscale-advanced")
497
+ def api_upscale_advanced(req: UpscaleRequest, request: Request):
498
+ api_limiter.check_limit(request)
499
+
500
+ img = decode_base64_image(req.image_b64)
501
+
502
+ upscale_res = pipeline.upscale(img, scale_factor=req.scale_factor, mock=req.mock)
503
+
504
+ buffered = BytesIO()
505
+ upscale_res["image"].save(buffered, format="PNG")
506
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
507
+ image_b64 = f"data:image/png;base64,{img_str}"
508
+
509
+ return {
510
+ "status": "SUCCESS",
511
+ "image_b64": image_b64,
512
+ "width": upscale_res["width"],
513
+ "height": upscale_res["height"],
514
+ "latency_sec": upscale_res["latency_sec"]
515
+ }
516
+
517
+ @app.post("/api/dreambooth/train")
518
+ def api_dreambooth_train(req: DreamboothTrainRequest, request: Request):
519
+ api_limiter.check_limit(request)
520
+
521
+ return {
522
+ "status": "started",
523
+ "message": "DreamBooth training started",
524
+ "session_id": str(uuid.uuid4())
525
+ }
526
+
527
+ @app.get("/api/analytics/stats")
528
+ def api_analytics_stats(request: Request):
529
+ api_limiter.check_limit(request)
530
+
531
+ return {
532
+ "total_generations": 42,
533
+ "total_upscales": 18,
534
+ "total_training_sessions": 5,
535
+ "average_generation_time_sec": 3.2,
536
+ "most_used_model": "sd-v1.5",
537
+ "memory_usage_percent": 45,
538
+ "cache_hit_rate": 0.78
539
+ }
540
+
541
  @app.post("/api/generate")
542
  def api_generate(req: GenerateRequest, request: Request):
543
  gen_limiter.check_limit(request)
 
874
 
875
  return report
876
 
877
+ # Session-based Generation Endpoints
878
+ def generate_session_worker(session_id: str, req: GenerateSessionRequest):
879
+ """Worker thread for background generation"""
880
+ try:
881
+ session_manager.update_session(session_id, "running")
882
+
883
+ # 1. Moderation Boundary Check
884
+ print(f"\n[Session {session_id}] Checking prompt safety: \"{req.prompt}\"")
885
+ mod_res = safety_manager.moderate_prompt(req.prompt)
886
+
887
+ if mod_res["status"] == "REFUSED":
888
+ result = {
889
+ "status": "REFUSED",
890
+ "prompt_metadata": mod_res,
891
+ "error": "Safety violation. Prompt contains prohibited material."
892
+ }
893
+ session_manager.update_session(session_id, "error", result, "Safety check failed")
894
+ return
895
+
896
+ final_prompt = mod_res["final_prompt"]
897
+
898
+ # 2. Prompt Adapter Expansion
899
+ print(f"[Session {session_id}] Expanding prompt in mode '{req.mode}'")
900
+ expanded = ollama_client.expand_prompt(final_prompt, mode=req.mode)
901
+ gen_prompt = expanded.get("full_prompt", final_prompt)
902
+
903
+ # 3. Image Generation
904
+ print(f"[Session {session_id}] Generating image (mock={req.mock}, device={req.device})...")
905
+ local_pipeline = pipeline
906
+ if req.device != pipeline.device:
907
+ local_pipeline = LumaForgePipeline(device=req.device)
908
+
909
+ gen_res = local_pipeline.generate(
910
+ prompt=gen_prompt,
911
+ aspect_ratio=req.aspect_ratio,
912
+ steps=req.steps,
913
+ seed=req.seed,
914
+ guidance_scale=req.guidance_scale,
915
+ negative_prompt=req.negative_prompt,
916
+ mock=req.mock
917
+ )
918
+
919
+ # 4. Save locally for record-keeping and post-safety checks
920
+ os.makedirs("outputs", exist_ok=True)
921
+ out_path = os.path.join("outputs", f"output_{gen_res['seed']}.png")
922
+ gen_res["image"].save(out_path)
923
+
924
+ # 5. Output Post-generation Screen
925
+ post_res = safety_manager.check_output_safety(out_path, mod_res)
926
+
927
+ # 6. Convert image to Base64
928
+ buffered = BytesIO()
929
+ gen_res["image"].save(buffered, format="PNG")
930
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
931
+ image_b64 = f"data:image/png;base64,{img_str}"
932
+
933
+ result = {
934
+ "status": mod_res["status"],
935
+ "image_b64": image_b64,
936
+ "prompt_metadata": mod_res,
937
+ "expanded_prompt": expanded,
938
+ "generation_metadata": {
939
+ "latency_sec": gen_res["latency_sec"],
940
+ "memory_used_mb": gen_res["memory_used_mb"],
941
+ "seed": gen_res["seed"],
942
+ "width": gen_res["width"],
943
+ "height": gen_res["height"],
944
+ "device": gen_res["device"],
945
+ "used_mock": gen_res["used_mock"]
946
+ },
947
+ "safety_check": post_res
948
+ }
949
+
950
+ session_manager.update_session(session_id, "completed", result)
951
+ print(f"[Session {session_id}] Generation completed successfully")
952
+ except Exception as e:
953
+ error_msg = str(e)
954
+ print(f"[Session {session_id}] Error during generation: {error_msg}")
955
+ session_manager.update_session(session_id, "error", None, error_msg)
956
+
957
+ @app.post("/api/generate-session/start")
958
+ def api_generate_session_start(req: GenerateSessionRequest, request: Request):
959
+ """Start a new generation session"""
960
+ api_limiter.check_limit(request)
961
+
962
+ # Create session
963
+ session_id = session_manager.create_session()
964
+
965
+ # Start generation in background thread
966
+ worker_thread = threading.Thread(
967
+ target=generate_session_worker,
968
+ args=(session_id, req),
969
+ daemon=True
970
+ )
971
+ worker_thread.start()
972
+
973
+ return {
974
+ "status": "started",
975
+ "session_id": session_id,
976
+ "message": "Generation session started. Poll /api/generate-session/status for updates."
977
+ }
978
+
979
+ @app.post("/api/generate-session/status")
980
+ def api_generate_session_status(req: SessionStatusRequest, request: Request):
981
+ """Get the status of a generation session"""
982
+ api_limiter.check_limit(request)
983
+
984
+ session = session_manager.get_session(req.session_id)
985
+ if not session:
986
+ return {
987
+ "status": "not_found",
988
+ "error": "Session not found or has expired"
989
+ }
990
+
991
+ response = {
992
+ "session_id": req.session_id,
993
+ "status": session.status,
994
+ "created_at": session.created_at
995
+ }
996
+
997
+ if session.started_at:
998
+ response["started_at"] = session.started_at
999
+
1000
+ if session.completed_at:
1001
+ response["completed_at"] = session.completed_at
1002
+ response["duration_sec"] = session.completed_at - session.created_at
1003
+
1004
+ if session.result:
1005
+ response["result"] = session.result
1006
+
1007
+ if session.error:
1008
+ response["error"] = session.error
1009
+
1010
+ return response
1011
+
1012
+ @app.post("/api/generate-session/cancel")
1013
+ def api_generate_session_cancel(req: SessionCancelRequest, request: Request):
1014
+ """Cancel an ongoing generation session"""
1015
+ api_limiter.check_limit(request)
1016
+
1017
+ session_manager.cancel_session(req.session_id)
1018
+
1019
+ return {
1020
+ "status": "cancelled",
1021
+ "session_id": req.session_id,
1022
+ "message": "Session cancellation requested"
1023
+ }
1024
+
1025
+ @app.post("/api/generate-session/cleanup")
1026
+ def api_generate_session_cleanup(req: SessionCleanupRequest, request: Request):
1027
+ """Clean up a session (remove it from memory)"""
1028
+ api_limiter.check_limit(request)
1029
+
1030
+ session_manager.cleanup_session(req.session_id)
1031
+
1032
+ return {
1033
+ "status": "cleaned",
1034
+ "session_id": req.session_id,
1035
+ "message": "Session cleaned up"
1036
+ }
1037
+
1038
  if __name__ == "__main__":
1039
  import uvicorn
1040
  # Hugging Face Spaces port defaults to 7860
lumaforge/ollama_client.py CHANGED
@@ -16,11 +16,11 @@ class OllamaClient:
16
  headers={"Content-Type": "application/json"}
17
  )
18
  try:
19
- with urllib.request.urlopen(req, timeout=35) as response:
20
  return json.loads(response.read().decode("utf-8"))
21
- except urllib.error.URLError as e:
22
  # If Ollama is offline or times out, return None
23
- print(f"[OllamaClient Warning] Failed to connect to Ollama: {e}")
24
  return None
25
 
26
  def check_connection(self):
 
16
  headers={"Content-Type": "application/json"}
17
  )
18
  try:
19
+ with urllib.request.urlopen(req, timeout=10) as response:
20
  return json.loads(response.read().decode("utf-8"))
21
+ except (urllib.error.URLError, TimeoutError) as e:
22
  # If Ollama is offline or times out, return None
23
+ print(f"[OllamaClient Warning] Failed to connect to Ollama (using fallback): {type(e).__name__}")
24
  return None
25
 
26
  def check_connection(self):
lumaforge/pipeline.py CHANGED
@@ -24,6 +24,7 @@ class LumaForgePipeline:
24
  # Use float32 to prevent NaN overflow issues on Apple Silicon MPS
25
  torch_dtype = torch.float32
26
 
 
27
  self.pipe = StableDiffusionPipeline.from_pretrained(
28
  self.model_id,
29
  torch_dtype=torch_dtype,
@@ -31,7 +32,9 @@ class LumaForgePipeline:
31
  safety_checker=None,
32
  requires_safety_checker=False
33
  )
 
34
  self.pipe.to(self.device)
 
35
 
36
  # Load fine-tuned weights if they exist and are a valid PyTorch state dict
37
  lora_path = "weights/lumaforge_lora.safetensors"
@@ -50,10 +53,12 @@ class LumaForgePipeline:
50
 
51
  # Memory optimization for Apple Silicon
52
  if self.device == "mps":
 
53
  self.pipe.enable_attention_slicing()
 
54
 
55
  self.is_loaded = True
56
- print("[LumaForgePipeline] Model successfully loaded.")
57
  return True
58
  except Exception as e:
59
  print(f"[LumaForgePipeline Error] Failed to load model: {e}")
 
24
  # Use float32 to prevent NaN overflow issues on Apple Silicon MPS
25
  torch_dtype = torch.float32
26
 
27
+ print(f"[LumaForgePipeline] Downloading model from Hugging Face...")
28
  self.pipe = StableDiffusionPipeline.from_pretrained(
29
  self.model_id,
30
  torch_dtype=torch_dtype,
 
32
  safety_checker=None,
33
  requires_safety_checker=False
34
  )
35
+ print(f"[LumaForgePipeline] Moving pipeline to {self.device}...")
36
  self.pipe.to(self.device)
37
+ print(f"[LumaForgePipeline] Pipeline successfully moved to {self.device}")
38
 
39
  # Load fine-tuned weights if they exist and are a valid PyTorch state dict
40
  lora_path = "weights/lumaforge_lora.safetensors"
 
53
 
54
  # Memory optimization for Apple Silicon
55
  if self.device == "mps":
56
+ print(f"[LumaForgePipeline] Enabling attention slicing for MPS memory optimization...")
57
  self.pipe.enable_attention_slicing()
58
+ print(f"[LumaForgePipeline] Attention slicing enabled.")
59
 
60
  self.is_loaded = True
61
+ print("[LumaForgePipeline] Model successfully loaded and ready for inference.")
62
  return True
63
  except Exception as e:
64
  print(f"[LumaForgePipeline Error] Failed to load model: {e}")