LogicGoInfotechSpaces commited on
Commit
779884f
·
1 Parent(s): 115b125

Add FastAPI version using SDXL + ControlNet for text-guided colorization (from fffiloni)

Browse files
Files changed (3) hide show
  1. Dockerfile +2 -1
  2. app/main_sdxl.py +499 -0
  3. requirements.txt +6 -1
Dockerfile CHANGED
@@ -63,4 +63,5 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
63
  ENTRYPOINT ["/entrypoint.sh"]
64
 
65
  # Run the application (port will be set via environment variable)
66
- CMD ["sh", "-c", "uvicorn app.main_fastai:app --host 0.0.0.0 --port ${PORT:-7860}"]
 
 
63
  ENTRYPOINT ["/entrypoint.sh"]
64
 
65
  # Run the application (port will be set via environment variable)
66
+ # Use SDXL version for text-guided colorization
67
+ CMD ["sh", "-c", "uvicorn app.main_sdxl:app --host 0.0.0.0 --port ${PORT:-7860}"]
app/main_sdxl.py ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI application for Text-Guided Image Colorization using SDXL + ControlNet
3
+ Based on fffiloni/text-guided-image-colorization
4
+ """
5
+ import os
6
+ import io
7
+ import uuid
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Optional, Tuple
11
+
12
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Depends, Request
13
+ from fastapi.responses import FileResponse, JSONResponse
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from fastapi.staticfiles import StaticFiles
16
+ import firebase_admin
17
+ from firebase_admin import credentials, app_check, auth as firebase_auth
18
+ from PIL import Image
19
+ import torch
20
+ import uvicorn
21
+ import gradio as gr
22
+
23
+ # SDXL + ControlNet imports
24
+ from accelerate import Accelerator
25
+ from diffusers import (
26
+ AutoencoderKL,
27
+ StableDiffusionXLControlNetPipeline,
28
+ ControlNetModel,
29
+ UNet2DConditionModel,
30
+ )
31
+ from transformers import (
32
+ BlipProcessor, BlipForConditionalGeneration,
33
+ )
34
+ from safetensors.torch import load_file
35
+ from huggingface_hub import hf_hub_download, snapshot_download
36
+
37
+ from app.config import settings
38
+
39
+ # Configure logging
40
+ logging.basicConfig(
41
+ level=logging.INFO,
42
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
43
+ )
44
+ logger = logging.getLogger(__name__)
45
+
46
+ # Create writable directories
47
+ Path("/tmp/hf_cache").mkdir(parents=True, exist_ok=True)
48
+ Path("/tmp/matplotlib_config").mkdir(parents=True, exist_ok=True)
49
+ Path("/tmp/colorize_uploads").mkdir(parents=True, exist_ok=True)
50
+ Path("/tmp/colorize_results").mkdir(parents=True, exist_ok=True)
51
+
52
+ # Initialize FastAPI app
53
+ app = FastAPI(
54
+ title="Text-Guided Image Colorization API",
55
+ description="Image colorization using SDXL + ControlNet with automatic captioning",
56
+ version="1.0.0"
57
+ )
58
+
59
+ # CORS middleware
60
+ app.add_middleware(
61
+ CORSMiddleware,
62
+ allow_origins=["*"],
63
+ allow_credentials=True,
64
+ allow_methods=["*"],
65
+ allow_headers=["*"],
66
+ )
67
+
68
+ # Initialize Firebase Admin SDK
69
+ firebase_cred_path = os.getenv("FIREBASE_CREDENTIALS_PATH", "/tmp/firebase-adminsdk.json")
70
+ if os.path.exists(firebase_cred_path):
71
+ try:
72
+ cred = credentials.Certificate(firebase_cred_path)
73
+ firebase_admin.initialize_app(cred)
74
+ logger.info("Firebase Admin SDK initialized")
75
+ except Exception as e:
76
+ logger.warning("Failed to initialize Firebase: %s", str(e))
77
+ try:
78
+ firebase_admin.initialize_app()
79
+ except:
80
+ pass
81
+ else:
82
+ logger.warning("Firebase credentials file not found. App Check will be disabled.")
83
+ try:
84
+ firebase_admin.initialize_app()
85
+ except:
86
+ pass
87
+
88
+ # Storage directories
89
+ UPLOAD_DIR = Path("/tmp/colorize_uploads")
90
+ RESULT_DIR = Path("/tmp/colorize_results")
91
+
92
+ # Mount static files
93
+ app.mount("/results", StaticFiles(directory=str(RESULT_DIR)), name="results")
94
+ app.mount("/uploads", StaticFiles(directory=str(UPLOAD_DIR)), name="uploads")
95
+
96
+ # Global model variables
97
+ pipe = None
98
+ caption_model = None
99
+ processor = None
100
+ device = None
101
+ weight_dtype = None
102
+ model_load_error: Optional[str] = None
103
+
104
+ # ========== Utility Functions ==========
105
+
106
+ def apply_color(image: Image.Image, color_map: Image.Image) -> Image.Image:
107
+ """Apply color from color_map to image using LAB color space."""
108
+ # Convert to LAB color space
109
+ image_lab = image.convert('LAB')
110
+ color_map_lab = color_map.convert('LAB')
111
+
112
+ # Extract and merge LAB channels
113
+ l, _, _ = image_lab.split()
114
+ _, a_map, b_map = color_map_lab.split()
115
+ merged_lab = Image.merge('LAB', (l, a_map, b_map))
116
+
117
+ return merged_lab.convert('RGB')
118
+
119
+
120
+ def remove_unlikely_words(prompt: str) -> str:
121
+ """Removes predefined unlikely phrases from prompt text."""
122
+ unlikely_words = []
123
+
124
+ a1 = [f'{i}s' for i in range(1900, 2000)]
125
+ a2 = [f'{i}' for i in range(1900, 2000)]
126
+ a3 = [f'year {i}' for i in range(1900, 2000)]
127
+ a4 = [f'circa {i}' for i in range(1900, 2000)]
128
+
129
+ b1 = [f"{y[0]} {y[1]} {y[2]} {y[3]} s" for y in a1]
130
+ b2 = [f"{y[0]} {y[1]} {y[2]} {y[3]}" for y in a1]
131
+ b3 = [f"year {y[0]} {y[1]} {y[2]} {y[3]}" for y in a1]
132
+ b4 = [f"circa {y[0]} {y[1]} {y[2]} {y[3]}" for y in a1]
133
+
134
+ manual = [
135
+ "black and white,", "black and white", "black & white,", "black & white", "circa",
136
+ "balck and white,", "monochrome,", "black-and-white,", "black-and-white photography,",
137
+ "black - and - white photography,", "monochrome bw,", "black white,", "black an white,",
138
+ "grainy footage,", "grainy footage", "grainy photo,", "grainy photo", "b&w photo",
139
+ "back and white", "back and white,", "monochrome contrast", "monochrome", "grainy",
140
+ "grainy photograph,", "grainy photograph", "low contrast,", "low contrast", "b & w",
141
+ "grainy black-and-white photo,", "bw", "bw,", "grainy black-and-white photo",
142
+ "b & w,", "b&w,", "b&w!,", "b&w", "black - and - white,", "bw photo,", "grainy photo,",
143
+ "black-and-white photo,", "black-and-white photo", "black - and - white photography",
144
+ "b&w photo,", "monochromatic photo,", "grainy monochrome photo,", "monochromatic",
145
+ "blurry photo,", "blurry,", "blurry photography,", "monochromatic photo",
146
+ "black - and - white photograph,", "black - and - white photograph", "black on white,",
147
+ "black on white", "black-and-white", "historical image,", "historical picture,",
148
+ "historical photo,", "historical photograph,", "archival photo,", "taken in the early",
149
+ "taken in the late", "taken in the", "historic photograph,", "restored,", "restored",
150
+ "historical photo", "historical setting,",
151
+ "historic photo,", "historic", "desaturated!!,", "desaturated!,", "desaturated,", "desaturated",
152
+ "taken in", "shot on leica", "shot on leica sl2", "sl2",
153
+ "taken with a leica camera", "leica sl2", "leica", "setting",
154
+ "overcast day", "overcast weather", "slight overcast", "overcast",
155
+ "picture taken in", "photo taken in",
156
+ ", photo", ", photo", ", photo", ", photo", ", photograph",
157
+ ",,", ",,,", ",,,,", " ,", " ,", " ,", " ,",
158
+ ]
159
+
160
+ unlikely_words.extend(a1 + a2 + a3 + a4 + b1 + b2 + b3 + b4 + manual)
161
+
162
+ for word in unlikely_words:
163
+ prompt = prompt.replace(word, "")
164
+ return prompt
165
+
166
+
167
+ # ========== Model Loading ==========
168
+
169
+ @app.on_event("startup")
170
+ async def startup_event():
171
+ """Load SDXL + ControlNet models on startup"""
172
+ global pipe, caption_model, processor, device, weight_dtype, model_load_error
173
+
174
+ try:
175
+ logger.info("🔄 Loading SDXL + ControlNet colorization models...")
176
+
177
+ # Ensure required directories exist
178
+ os.makedirs("sdxl_light_caption_output", exist_ok=True)
179
+
180
+ # Download controlnet model snapshot
181
+ try:
182
+ snapshot_download(
183
+ repo_id='nickpai/sdxl_light_caption_output',
184
+ local_dir='sdxl_light_caption_output'
185
+ )
186
+ except Exception as e:
187
+ logger.warning(f"Could not download controlnet snapshot: {e}")
188
+
189
+ # Device and precision setup
190
+ accelerator = Accelerator(mixed_precision="fp16")
191
+ weight_dtype = torch.float16 if accelerator.mixed_precision == "fp16" else torch.float32
192
+ device = accelerator.device
193
+
194
+ logger.info(f"Using device: {device}, dtype: {weight_dtype}")
195
+
196
+ # Pretrained paths
197
+ base_model_path = settings.BASE_MODEL_ID
198
+ safetensors_ckpt = settings.LIGHTNING_WEIGHTS
199
+ controlnet_path = "sdxl_light_caption_output/checkpoint-30000/controlnet"
200
+
201
+ # Load diffusion components
202
+ logger.info("Loading VAE...")
203
+ vae = AutoencoderKL.from_pretrained(base_model_path, subfolder="vae")
204
+
205
+ logger.info("Loading UNet...")
206
+ unet = UNet2DConditionModel.from_config(base_model_path, subfolder="unet")
207
+ unet.load_state_dict(load_file(hf_hub_download("ByteDance/SDXL-Lightning", safetensors_ckpt)))
208
+
209
+ logger.info("Loading ControlNet...")
210
+ controlnet = ControlNetModel.from_pretrained(controlnet_path, torch_dtype=weight_dtype)
211
+
212
+ logger.info("Creating pipeline...")
213
+ pipe = StableDiffusionXLControlNetPipeline.from_pretrained(
214
+ base_model_path, vae=vae, unet=unet, controlnet=controlnet
215
+ )
216
+ pipe.to(device, dtype=weight_dtype)
217
+ pipe.safety_checker = None
218
+
219
+ # Load BLIP captioning model
220
+ logger.info("Loading BLIP captioning model...")
221
+ # Try large first, fallback to base
222
+ caption_model_name = "blip-image-captioning-large"
223
+ try:
224
+ processor = BlipProcessor.from_pretrained(f"Salesforce/{caption_model_name}")
225
+ caption_model = BlipForConditionalGeneration.from_pretrained(
226
+ f"Salesforce/{caption_model_name}", torch_dtype=weight_dtype
227
+ ).to(device)
228
+ except Exception as e:
229
+ logger.warning(f"Failed to load large model, trying base: {e}")
230
+ caption_model_name = "blip-image-captioning-base"
231
+ processor = BlipProcessor.from_pretrained(f"Salesforce/{caption_model_name}")
232
+ caption_model = BlipForConditionalGeneration.from_pretrained(
233
+ f"Salesforce/{caption_model_name}", torch_dtype=weight_dtype
234
+ ).to(device)
235
+
236
+ logger.info("✅ All models loaded successfully!")
237
+ model_load_error = None
238
+
239
+ except Exception as e:
240
+ error_msg = str(e)
241
+ logger.error(f"❌ Failed to load models: {error_msg}")
242
+ model_load_error = error_msg
243
+ # Don't raise - allow health check to work
244
+
245
+
246
+ @app.on_event("shutdown")
247
+ async def shutdown_event():
248
+ """Cleanup on shutdown"""
249
+ global pipe, caption_model
250
+ if pipe:
251
+ del pipe
252
+ if caption_model:
253
+ del caption_model
254
+ logger.info("Application shutdown")
255
+
256
+
257
+ # ========== Authentication ==========
258
+
259
+ def _extract_bearer_token(authorization_header: str | None) -> str | None:
260
+ if not authorization_header:
261
+ return None
262
+ parts = authorization_header.split(" ", 1)
263
+ if len(parts) == 2 and parts[0].lower() == "bearer":
264
+ return parts[1].strip()
265
+ return None
266
+
267
+
268
+ async def verify_request(request: Request):
269
+ """Verify Firebase authentication"""
270
+ if not firebase_admin._apps or os.getenv("DISABLE_AUTH", "false").lower() == "true":
271
+ return True
272
+
273
+ bearer = _extract_bearer_token(request.headers.get("Authorization"))
274
+ if bearer:
275
+ try:
276
+ decoded = firebase_auth.verify_id_token(bearer)
277
+ request.state.user = decoded
278
+ logger.info("Firebase Auth id_token verified for uid: %s", decoded.get("uid"))
279
+ return True
280
+ except Exception as e:
281
+ logger.warning("Auth token verification failed: %s", str(e))
282
+
283
+ if settings.ENABLE_APP_CHECK:
284
+ app_check_token = request.headers.get("X-Firebase-AppCheck")
285
+ if not app_check_token:
286
+ raise HTTPException(status_code=401, detail="Missing App Check token")
287
+ try:
288
+ app_check_claims = app_check.verify_token(app_check_token)
289
+ logger.info("App Check token verified for: %s", app_check_claims.get("app_id"))
290
+ return True
291
+ except Exception as e:
292
+ logger.warning("App Check token verification failed: %s", str(e))
293
+ raise HTTPException(status_code=401, detail="Invalid App Check token")
294
+
295
+ return True
296
+
297
+
298
+ # ========== API Endpoints ==========
299
+
300
+ @app.get("/api")
301
+ async def api_info():
302
+ """API info endpoint"""
303
+ return {
304
+ "app": "Text-Guided Image Colorization API",
305
+ "version": "1.0.0",
306
+ "health": "/health",
307
+ "colorize": "/colorize",
308
+ "gradio": "/"
309
+ }
310
+
311
+
312
+ @app.get("/health")
313
+ async def health_check():
314
+ """Health check endpoint"""
315
+ response = {
316
+ "status": "healthy",
317
+ "model_loaded": pipe is not None and caption_model is not None,
318
+ "model_type": "sdxl_controlnet",
319
+ "device": str(device) if device else None
320
+ }
321
+ if model_load_error:
322
+ response["model_error"] = model_load_error
323
+ return response
324
+
325
+
326
+ def colorize_image_sdxl(
327
+ image: Image.Image,
328
+ positive_prompt: Optional[str] = None,
329
+ negative_prompt: Optional[str] = None,
330
+ seed: int = 123,
331
+ num_inference_steps: int = 8
332
+ ) -> Tuple[Image.Image, str]:
333
+ """
334
+ Colorize a grayscale or low-color image using SDXL + ControlNet.
335
+
336
+ Args:
337
+ image: PIL Image to colorize
338
+ positive_prompt: Additional descriptive text to enhance the caption
339
+ negative_prompt: Words or phrases to avoid during generation
340
+ seed: Random seed for reproducible generation
341
+ num_inference_steps: Number of inference steps
342
+
343
+ Returns:
344
+ Tuple of (colorized PIL Image, caption string)
345
+ """
346
+ if pipe is None or caption_model is None:
347
+ raise RuntimeError("Models not loaded")
348
+
349
+ torch.manual_seed(seed)
350
+ original_size = image.size
351
+ control_image = image.convert("L").convert("RGB").resize((512, 512))
352
+
353
+ # Image captioning
354
+ input_text = settings.CAPTION_PREFIX
355
+ inputs = processor(control_image, input_text, return_tensors="pt").to(device, dtype=weight_dtype)
356
+ caption_ids = caption_model.generate(**inputs)
357
+ caption = processor.decode(caption_ids[0], skip_special_tokens=True)
358
+ caption = remove_unlikely_words(caption)
359
+
360
+ # Construct final prompt
361
+ if positive_prompt:
362
+ final_prompt = f"{positive_prompt}, {caption}"
363
+ else:
364
+ final_prompt = caption
365
+
366
+ # Inference
367
+ result = pipe(
368
+ prompt=final_prompt,
369
+ negative_prompt=negative_prompt or settings.NEGATIVE_PROMPT,
370
+ num_inference_steps=num_inference_steps,
371
+ generator=torch.manual_seed(seed),
372
+ image=control_image
373
+ )
374
+
375
+ colorized = apply_color(control_image, result.images[0]).resize(original_size)
376
+ return colorized, caption
377
+
378
+
379
+ @app.post("/colorize")
380
+ async def colorize_api(
381
+ file: UploadFile = File(...),
382
+ positive_prompt: Optional[str] = None,
383
+ negative_prompt: Optional[str] = None,
384
+ seed: int = 123,
385
+ num_inference_steps: int = 8,
386
+ verified: bool = Depends(verify_request)
387
+ ):
388
+ """
389
+ Upload a grayscale image -> returns colorized image.
390
+ Uses SDXL + ControlNet with automatic captioning.
391
+ """
392
+ if pipe is None or caption_model is None:
393
+ raise HTTPException(status_code=503, detail="Colorization models not loaded")
394
+
395
+ if not file.content_type or not file.content_type.startswith("image/"):
396
+ raise HTTPException(status_code=400, detail="File must be an image")
397
+
398
+ try:
399
+ img_bytes = await file.read()
400
+ image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
401
+
402
+ logger.info("Colorizing image with SDXL + ControlNet...")
403
+ colorized, caption = colorize_image_sdxl(
404
+ image,
405
+ positive_prompt=positive_prompt,
406
+ negative_prompt=negative_prompt,
407
+ seed=seed,
408
+ num_inference_steps=num_inference_steps
409
+ )
410
+
411
+ output_filename = f"{uuid.uuid4()}.png"
412
+ output_path = RESULT_DIR / output_filename
413
+ colorized.save(output_path, "PNG")
414
+
415
+ logger.info("Colorized image saved: %s", output_filename)
416
+
417
+ return JSONResponse({
418
+ "success": True,
419
+ "result_id": output_filename.replace(".png", ""),
420
+ "caption": caption,
421
+ "download_url": f"/results/{output_filename}",
422
+ "api_download": f"/download/{output_filename.replace('.png', '')}"
423
+ })
424
+ except Exception as e:
425
+ logger.error("Error colorizing image: %s", str(e))
426
+ raise HTTPException(status_code=500, detail=f"Error colorizing image: {str(e)}")
427
+
428
+
429
+ @app.get("/download/{file_id}")
430
+ def download_result(file_id: str, verified: bool = Depends(verify_request)):
431
+ """Download colorized image by file ID"""
432
+ filename = f"{file_id}.png"
433
+ path = RESULT_DIR / filename
434
+
435
+ if not path.exists():
436
+ raise HTTPException(status_code=404, detail="Result not found")
437
+
438
+ return FileResponse(path, media_type="image/png")
439
+
440
+
441
+ @app.get("/results/{filename}")
442
+ def get_result(filename: str):
443
+ """Public endpoint to access colorized images"""
444
+ path = RESULT_DIR / filename
445
+ if not path.exists():
446
+ raise HTTPException(status_code=404, detail="Result not found")
447
+ return FileResponse(path, media_type="image/png")
448
+
449
+
450
+ # ========== Gradio Interface (Optional) ==========
451
+
452
+ def gradio_colorize(image, positive_prompt=None, negative_prompt=None, seed=123):
453
+ """Gradio colorization function"""
454
+ if image is None:
455
+ return None, ""
456
+ try:
457
+ if pipe is None or caption_model is None:
458
+ return None, "Models not loaded"
459
+ colorized, caption = colorize_image_sdxl(
460
+ image,
461
+ positive_prompt=positive_prompt,
462
+ negative_prompt=negative_prompt,
463
+ seed=seed
464
+ )
465
+ return colorized, caption
466
+ except Exception as e:
467
+ logger.error("Gradio colorization error: %s", str(e))
468
+ return None, str(e)
469
+
470
+
471
+ title = "🎨 Text-Guided Image Colorization"
472
+ description = "Upload a grayscale image and generate a color version guided by automatic captioning using SDXL + ControlNet."
473
+
474
+ iface = gr.Interface(
475
+ fn=gradio_colorize,
476
+ inputs=[
477
+ gr.Image(type="pil", label="Upload Image"),
478
+ gr.Textbox(label="Positive Prompt", placeholder="Enter details to enhance the caption"),
479
+ gr.Textbox(label="Negative Prompt", value=settings.NEGATIVE_PROMPT),
480
+ gr.Slider(0, 1000, 123, label="Seed")
481
+ ],
482
+ outputs=[
483
+ gr.Image(type="pil", label="Colorized Image"),
484
+ gr.Textbox(label="Caption", show_copy_button=True)
485
+ ],
486
+ title=title,
487
+ description=description,
488
+ )
489
+
490
+ # Mount Gradio app at root
491
+ app = gr.mount_gradio_app(app, iface, path="/")
492
+
493
+
494
+ # ========== Run Server ==========
495
+
496
+ if __name__ == "__main__":
497
+ port = int(os.getenv("PORT", "7860"))
498
+ uvicorn.run(app, host="0.0.0.0", port=port)
499
+
requirements.txt CHANGED
@@ -9,4 +9,9 @@ fastai
9
  huggingface_hub
10
  pydantic-settings
11
  opencv-python
12
- numpy
 
 
 
 
 
 
9
  huggingface_hub
10
  pydantic-settings
11
  opencv-python
12
+ numpy
13
+ accelerate
14
+ transformers
15
+ diffusers
16
+ safetensors
17
+ ftfy