etchisone commited on
Commit
5206227
·
verified ·
1 Parent(s): a51bc64

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -547
app.py DELETED
@@ -1,547 +0,0 @@
1
- """
2
- Text-to-Image Generator + Evaluation Metrics
3
- Dataset : rhli/genarena | Model: runwayml/stable-diffusion-v1-5
4
- Deploy on: Hugging Face Spaces (Gradio SDK)
5
-
6
- Evaluation metrics
7
- ──────────────────
8
- • CLIP Score – prompt-image alignment (higher = better; 0-100)
9
- Analogue of recall: did the image capture the prompt?
10
- • FID – Fréchet Inception Distance vs. a reference batch
11
- (lower = better; 0 = identical distributions)
12
- Analogue of precision: are generated images realistic?
13
- • Aesthetic Score – LAION aesthetic predictor (higher = better; 1-10)
14
- """
15
-
16
- import torch
17
- import torch.nn as nn
18
- import gradio as gr
19
- import numpy as np
20
- import random
21
- from PIL import Image
22
- from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
23
- from datasets import load_dataset
24
-
25
- # ─────────────────────────────────────────────────────────────────────────────
26
- # 1. Device / dtype
27
- # ─────────────────────────────────────────────────────────────────────────────
28
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
29
- DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
30
-
31
- # ─────────────────────────────────────────────────────────────────────────────
32
- # 2. Generation pipeline
33
- # ─────────────────────────────────────────────────────────────────────────────
34
- MODEL_ID = "runwayml/stable-diffusion-v1-5"
35
- print(f"Loading generation model on {DEVICE} ...")
36
- pipe = StableDiffusionPipeline.from_pretrained(
37
- MODEL_ID,
38
- torch_dtype=DTYPE,
39
- safety_checker=None,
40
- requires_safety_checker=False,
41
- )
42
- pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
43
- pipe = pipe.to(DEVICE)
44
- if DEVICE == "cuda":
45
- pipe.enable_attention_slicing()
46
- print("Generation model ready")
47
-
48
- # ─────────────────────────────────────────────────────────────────────────────
49
- # 3. Evaluation models (lazy-loaded on first use to save startup time)
50
- # ─────────────────────────────────────────────────────────────────────────────
51
- _clip_model = None
52
- _clip_processor = None
53
- _aesthetic_model = None
54
- _inception_model = None
55
-
56
-
57
- def _load_clip():
58
- global _clip_model, _clip_processor
59
- if _clip_model is None:
60
- from transformers import CLIPModel, CLIPProcessor
61
- print("Loading CLIP ViT-B/32 ...")
62
- _clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(DEVICE)
63
- _clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
64
- _clip_model.eval()
65
- print("CLIP ready")
66
- return _clip_model, _clip_processor
67
-
68
-
69
- class _AestheticPredictor(nn.Module):
70
- """Small MLP trained on LAION human ratings — predicts aesthetic score from CLIP embeddings."""
71
- def __init__(self, input_size: int = 768):
72
- super().__init__()
73
- self.layers = nn.Sequential(
74
- nn.Linear(input_size, 1024), nn.Dropout(0.2),
75
- nn.Linear(1024, 128), nn.Dropout(0.2),
76
- nn.Linear(128, 64), nn.Dropout(0.1),
77
- nn.Linear(64, 16),
78
- nn.Linear(16, 1),
79
- )
80
- def forward(self, x):
81
- return self.layers(x)
82
-
83
-
84
- def _load_aesthetic():
85
- global _aesthetic_model
86
- if _aesthetic_model is None:
87
- from huggingface_hub import hf_hub_download
88
- print("Loading aesthetic predictor ...")
89
- weights_path = hf_hub_download(
90
- "camenduru/improved-aesthetic-predictor",
91
- filename="sac+logos+ava1-l14-linearMSE.pth",
92
- )
93
- _aesthetic_model = _AestheticPredictor(input_size=768)
94
- # weights_only=False required for legacy .pth files (PyTorch 2.x changed the default)
95
- state = torch.load(weights_path, map_location="cpu", weights_only=False)
96
- _aesthetic_model.load_state_dict(state)
97
- _aesthetic_model.eval().to(DEVICE)
98
- print("Aesthetic predictor ready")
99
- return _aesthetic_model
100
-
101
-
102
- def _load_inception():
103
- global _inception_model
104
- if _inception_model is None:
105
- from torchvision.models import inception_v3, Inception_V3_Weights
106
- print("Loading Inception-v3 for FID ...")
107
- # weights= API replaces deprecated pretrained=True (torchvision >= 0.13)
108
- _inception_model = inception_v3(
109
- weights=Inception_V3_Weights.DEFAULT,
110
- transform_input=False,
111
- )
112
- _inception_model.fc = nn.Identity() # strip classifier → 2048-d features
113
- _inception_model.aux_logits = False # disable aux output in eval mode
114
- _inception_model.eval().to(DEVICE)
115
- print("Inception-v3 ready")
116
- return _inception_model
117
-
118
-
119
- # ─────────────────────────────────────────────────────────────────────────────
120
- # 4. Metric helpers
121
- # ─────────────────────────────────────────────────────────────────────────────
122
-
123
- def compute_clip_score(image: Image.Image, prompt: str) -> float:
124
- """
125
- CLIP Score in [0, 100].
126
- Cosine similarity between CLIP image & text embeddings, scaled to 0-100.
127
- Higher = better prompt alignment — recall analogue.
128
- truncation=True + max_length=77 prevents the hard 77-token limit overflow.
129
- """
130
- model, processor = _load_clip()
131
- inputs = processor(
132
- text=[prompt], images=image,
133
- return_tensors="pt", padding=True,
134
- truncation=True, max_length=77,
135
- )
136
- # Move each tensor individually — BatchEncoding.to() is unreliable across versions
137
- inputs = {k: v.to(DEVICE) for k, v in inputs.items()}
138
- with torch.no_grad():
139
- out = model(**inputs)
140
- img_emb = out.image_embeds
141
- txt_emb = out.text_embeds
142
- img_emb = img_emb / img_emb.norm(dim=-1, keepdim=True)
143
- txt_emb = txt_emb / txt_emb.norm(dim=-1, keepdim=True)
144
- sim = (img_emb * txt_emb).sum().item()
145
- return round(float(np.clip(sim * 100, 0, 100)), 2)
146
-
147
-
148
- def compute_aesthetic_score(image: Image.Image) -> float:
149
- """
150
- LAION aesthetic score in [1, 10]. Higher = more visually pleasing.
151
- Uses CLIPVisionModelWithProjection (not CLIPModel) so .forward() always
152
- returns image_embeds as a plain tensor, not a BaseModelOutputWithPooling.
153
- """
154
- try:
155
- from transformers import CLIPVisionModelWithProjection, CLIPProcessor
156
- clip_v = CLIPVisionModelWithProjection.from_pretrained(
157
- "openai/clip-vit-large-patch14"
158
- ).to(DEVICE)
159
- clip_v.eval()
160
- proc_v = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")
161
- aes = _load_aesthetic()
162
-
163
- pixel_values = proc_v(images=image, return_tensors="pt")["pixel_values"].to(DEVICE)
164
- with torch.no_grad():
165
- out = clip_v(pixel_values=pixel_values) # CLIPVisionModelOutput
166
- emb = out.image_embeds # plain tensor (1, 768)
167
- emb = emb / emb.norm(dim=-1, keepdim=True)
168
- score = aes(emb).item()
169
- return round(float(np.clip(score, 1, 10)), 2)
170
- except Exception as e:
171
- print(f"Aesthetic score skipped: {e}")
172
- return -1.0
173
-
174
-
175
- def _inception_features(images: list, batch_size: int = 8) -> np.ndarray:
176
- """Extract 2048-d Inception-v3 features from a list of PIL images."""
177
- import torchvision.transforms as T
178
- model = _load_inception()
179
- tfm = T.Compose([
180
- T.Resize((299, 299)),
181
- T.ToTensor(),
182
- T.Normalize([0.5] * 3, [0.5] * 3),
183
- ])
184
- feats = []
185
- for i in range(0, len(images), batch_size):
186
- batch = torch.stack(
187
- [tfm(img.convert("RGB")) for img in images[i : i + batch_size]]
188
- ).to(DEVICE)
189
- with torch.no_grad():
190
- out = model(batch)
191
- # aux_logits=False + fc=Identity → plain tensor; guard just in case
192
- feat = out if isinstance(out, torch.Tensor) else out.logits
193
- feats.append(feat.cpu().numpy())
194
- return np.concatenate(feats, axis=0)
195
-
196
-
197
- def compute_fid(generated_images: list, reference_images: list) -> float:
198
- """
199
- Fréchet Inception Distance — lower is better, 0 = identical distributions.
200
- Precision analogue: are generated images statistically similar to real ones?
201
- """
202
- if len(generated_images) < 2 or len(reference_images) < 2:
203
- return -1.0
204
-
205
- gen_f = _inception_features(generated_images)
206
- ref_f = _inception_features(reference_images)
207
-
208
- mu_g, sigma_g = gen_f.mean(0), np.cov(gen_f, rowvar=False)
209
- mu_r, sigma_r = ref_f.mean(0), np.cov(ref_f, rowvar=False)
210
-
211
- diff = mu_g - mu_r
212
- # Matrix square root via eigendecomposition (no scipy needed)
213
- eigvals, eigvecs = np.linalg.eigh(sigma_g @ sigma_r)
214
- eigvals = np.maximum(eigvals, 0)
215
- sqrt_product = eigvecs @ np.diag(np.sqrt(eigvals)) @ eigvecs.T
216
-
217
- fid = float(diff @ diff + np.trace(sigma_g + sigma_r - 2 * sqrt_product))
218
- return round(fid, 2)
219
-
220
-
221
- # ─────────────────────────────────────────────────────────────────────────────
222
- # 5. Dataset prompts
223
- # ─────────────────────────────────────────────────────────────────────────────
224
- PROMPT_COLUMN = "prompt"
225
- try:
226
- _ds = load_dataset("rhli/genarena", split="train")
227
- DATASET_PROMPTS = [_ds[i][PROMPT_COLUMN] for i in range(min(200, len(_ds)))]
228
- print(f"Loaded {len(DATASET_PROMPTS)} prompts from rhli/genarena")
229
- except Exception as e:
230
- print(f"Dataset load failed: {e}")
231
- DATASET_PROMPTS = [
232
- "a futuristic city at sunset",
233
- "a cozy cottage in a misty forest",
234
- "a robot painting a watercolor",
235
- "an astronaut on a purple alien planet",
236
- ]
237
-
238
-
239
- # ─────────────────────────────────────────────────────────────────────────────
240
- # 6. Core inference helpers wired to Gradio callbacks
241
- # ─────────────────────────────────────────────────────────────────────────────
242
-
243
- def _run_pipe(prompt, negative_prompt, num_steps, guidance_scale, seed):
244
- generator = torch.Generator(DEVICE).manual_seed(int(seed))
245
- # torch.amp.autocast is the stable API across PyTorch versions
246
- if DEVICE == "cuda":
247
- ctx = torch.amp.autocast(device_type="cuda")
248
- else:
249
- ctx = torch.no_grad()
250
- with ctx:
251
- result = pipe(
252
- prompt,
253
- negative_prompt=negative_prompt or None,
254
- num_inference_steps=int(num_steps),
255
- guidance_scale=float(guidance_scale),
256
- generator=generator,
257
- height=512, width=512,
258
- )
259
- return result.images[0]
260
-
261
-
262
- def generate_image(prompt, negative_prompt, num_steps, guidance_scale, seed):
263
- if not prompt.strip():
264
- return None, "Please enter a prompt."
265
- try:
266
- image = _run_pipe(prompt, negative_prompt, num_steps, guidance_scale, seed)
267
- return image, f"Generated with seed {int(seed)}"
268
- except Exception as e:
269
- return None, f"Error: {e}"
270
-
271
-
272
- def evaluate_single(prompt, negative_prompt, num_steps, guidance_scale, seed, run_aesthetic):
273
- """Generate one image and compute CLIP score + optionally aesthetic score."""
274
- if not prompt.strip():
275
- return None, 0.0, 0.0, "Please enter a prompt."
276
- try:
277
- image = _run_pipe(prompt, negative_prompt, num_steps, guidance_scale, seed)
278
- clip = compute_clip_score(image, prompt)
279
- aes = compute_aesthetic_score(image) if run_aesthetic else -1.0
280
-
281
- clip_status = "Good" if clip >= 25 else "Moderate" if clip >= 15 else "Low"
282
- rows = [
283
- "### Evaluation Results",
284
- "",
285
- "| Metric | Value | Status |",
286
- "|--------|-------|--------|",
287
- f"| **CLIP Score** (0-100, recall analogue) | `{clip:.1f}` | {clip_status} |",
288
- ]
289
- if aes > 0:
290
- aes_status = "Good" if aes >= 5 else "Moderate" if aes >= 3 else "Low"
291
- rows.append(f"| **Aesthetic Score** (1-10) | `{aes:.2f}` | {aes_status} |")
292
- else:
293
- rows.append("| **Aesthetic Score** | `skipped` | enable checkbox to compute |")
294
-
295
- rows += [
296
- "",
297
- "**CLIP Score** — how well the image matches the prompt (recall analogue).",
298
- "**Aesthetic Score** — perceived visual quality via LAION predictor.",
299
- ]
300
- return image, clip, aes if aes > 0 else 0.0, "\n".join(rows)
301
-
302
- except Exception as e:
303
- return None, 0.0, 0.0, f"Error: {e}"
304
-
305
-
306
- def evaluate_batch_fid(prompts_text, negative_prompt, num_steps, guidance_scale, n_ref):
307
- """Generate one image per prompt, compute CLIP scores, then compute FID vs dataset reference."""
308
- prompts = [p.strip() for p in prompts_text.strip().splitlines() if p.strip()]
309
- if len(prompts) < 2:
310
- return "Enter at least 2 prompts (one per line) for a meaningful FID score."
311
-
312
- gen_images, clip_scores, rows = [], [], []
313
-
314
- for i, prompt in enumerate(prompts):
315
- try:
316
- seed = random.randint(0, 2**31 - 1)
317
- image = _run_pipe(prompt, negative_prompt, num_steps, guidance_scale, seed)
318
- clip = compute_clip_score(image, prompt)
319
- gen_images.append(image)
320
- clip_scores.append(clip)
321
- rows.append(f"| {i+1} | {prompt[:60]} | `{clip:.1f}` |")
322
- except Exception as e:
323
- rows.append(f"| {i+1} | {prompt[:60]} | Error: {e} |")
324
-
325
- # Reference batch from dataset
326
- ref_prompts = random.sample(DATASET_PROMPTS, min(int(n_ref), len(DATASET_PROMPTS)))
327
- ref_images = []
328
- for rp in ref_prompts:
329
- try:
330
- ref_images.append(
331
- _run_pipe(rp, negative_prompt, num_steps, guidance_scale,
332
- random.randint(0, 2**31 - 1))
333
- )
334
- except Exception:
335
- pass
336
-
337
- fid_val = compute_fid(gen_images, ref_images) if len(ref_images) >= 2 else -1.0
338
- avg_clip = float(np.mean(clip_scores)) if clip_scores else 0.0
339
-
340
- fid_str = (
341
- f"`{fid_val:.1f}` ({'Good' if fid_val < 50 else 'Moderate' if fid_val < 150 else 'High'})"
342
- if fid_val >= 0 else "`N/A` (need ≥ 2 reference images)"
343
- )
344
-
345
- output = [
346
- "### Batch Evaluation Results",
347
- "",
348
- f"**Average CLIP Score:** `{avg_clip:.1f} / 100`",
349
- f"**FID** vs {len(ref_images)} dataset reference images: {fid_str}",
350
- "",
351
- "| # | Prompt | CLIP Score |",
352
- "|---|--------|------------|",
353
- ] + rows + [
354
- "",
355
- "---",
356
- "**FID** (precision analogue) — distribution similarity to real dataset images. Lower = better.",
357
- "**CLIP Score** (recall analogue) — per-image prompt alignment. Higher = better.",
358
- ]
359
- return "\n".join(output)
360
-
361
-
362
- def random_prompt():
363
- return random.choice(DATASET_PROMPTS)
364
-
365
- def random_seed():
366
- return random.randint(0, 2**31 - 1)
367
-
368
-
369
- # ─────────────────────────────────────────────────────────────────────────────
370
- # 7. Gradio UI
371
- # ─────────────────────────────────────────────────────────────────────────────
372
- with gr.Blocks(title="Text-to-Image Generator", theme=gr.themes.Soft()) as demo:
373
-
374
- gr.Markdown(
375
- "# Text-to-Image Generator\n"
376
- "Stable Diffusion v1.5 · Dataset: "
377
- "[rhli/genarena](https://huggingface.co/datasets/rhli/genarena)"
378
- )
379
-
380
- with gr.Tabs():
381
-
382
- # ── Tab 1: Generate ──────────────────────────────────────────────────
383
- with gr.TabItem("Generate"):
384
- with gr.Row():
385
- with gr.Column(scale=1):
386
- prompt_box = gr.Textbox(label="Prompt", lines=3,
387
- placeholder="Describe the image you want...")
388
- surprise_btn = gr.Button("Surprise me (dataset prompt)",
389
- variant="secondary", size="sm")
390
- neg_box = gr.Textbox(
391
- label="Negative prompt (optional)",
392
- value="blurry, low quality, ugly, distorted",
393
- lines=2,
394
- )
395
- with gr.Accordion("Advanced settings", open=False):
396
- steps_sl = gr.Slider(10, 50, 20, step=1, label="Inference steps")
397
- guide_sl = gr.Slider(1.0, 20.0, 7.5, step=0.5, label="Guidance scale")
398
- with gr.Row():
399
- seed_box = gr.Number(label="Seed", value=42, precision=0)
400
- rand_seed_btn = gr.Button("Random seed", size="sm")
401
- gen_btn = gr.Button("Generate", variant="primary")
402
-
403
- with gr.Column(scale=1):
404
- gen_image = gr.Image(label="Generated image", type="pil")
405
- gen_status = gr.Markdown("")
406
-
407
- gen_btn.click(
408
- generate_image,
409
- inputs=[prompt_box, neg_box, steps_sl, guide_sl, seed_box],
410
- outputs=[gen_image, gen_status],
411
- )
412
- surprise_btn.click(random_prompt, outputs=prompt_box)
413
- rand_seed_btn.click(random_seed, outputs=seed_box)
414
-
415
- gr.Examples(
416
- examples=[
417
- ["a golden sunset over a calm ocean, photorealistic", "blurry, low quality", 20, 7.5, 42],
418
- ["a watercolor painting of a Japanese cherry blossom garden", "", 25, 8.0, 7],
419
- ["a futuristic robot chef in a neon-lit kitchen", "low quality", 20, 7.5, 99],
420
- ["an ancient library filled with glowing magical books", "", 20, 9.0, 12],
421
- ],
422
- inputs=[prompt_box, neg_box, steps_sl, guide_sl, seed_box],
423
- outputs=[gen_image, gen_status],
424
- fn=generate_image,
425
- cache_examples=False,
426
- )
427
-
428
- # ── Tab 2: Single-image evaluation ───────────────────────────────────
429
- with gr.TabItem("Evaluate Single Image"):
430
- gr.Markdown(
431
- "Generate one image and measure:\n"
432
- "- **CLIP Score** (0-100) — prompt alignment. *Recall analogue.*\n"
433
- "- **Aesthetic Score** (1-10) — visual quality. *(adds ~30 s, loads an extra model)*"
434
- )
435
- with gr.Row():
436
- with gr.Column(scale=1):
437
- eval_prompt = gr.Textbox(label="Prompt", lines=3,
438
- placeholder="Enter your prompt...")
439
- eval_neg = gr.Textbox(
440
- label="Negative prompt",
441
- value="blurry, low quality, ugly, distorted",
442
- lines=2,
443
- )
444
- with gr.Accordion("Settings", open=False):
445
- eval_steps = gr.Slider(10, 50, 20, step=1, label="Inference steps")
446
- eval_guide = gr.Slider(1.0, 20.0, 7.5, step=0.5, label="Guidance scale")
447
- with gr.Row():
448
- eval_seed = gr.Number(label="Seed", value=42, precision=0)
449
- eval_rand_btn = gr.Button("Random seed", size="sm")
450
- eval_aes_chk = gr.Checkbox(label="Compute Aesthetic Score (slower)", value=False)
451
- eval_btn = gr.Button("Generate + Evaluate", variant="primary")
452
-
453
- with gr.Column(scale=1):
454
- eval_image = gr.Image(label="Generated image", type="pil")
455
- clip_num = gr.Number(label="CLIP Score (0-100)", precision=2)
456
- aes_num = gr.Number(label="Aesthetic Score (1-10)", precision=2)
457
- eval_md = gr.Markdown("")
458
-
459
- eval_btn.click(
460
- evaluate_single,
461
- inputs=[eval_prompt, eval_neg, eval_steps, eval_guide, eval_seed, eval_aes_chk],
462
- outputs=[eval_image, clip_num, aes_num, eval_md],
463
- )
464
- eval_rand_btn.click(random_seed, outputs=eval_seed)
465
-
466
- # ── Tab 3: Batch FID evaluation ───────────────────────────────────────
467
- with gr.TabItem("Batch Evaluate (FID)"):
468
- gr.Markdown(
469
- "Enter multiple prompts (one per line). The app will:\n"
470
- "1. Generate one image per prompt and compute **CLIP scores**.\n"
471
- "2. Generate a reference batch from `rhli/genarena`.\n"
472
- "3. Compute **FID** between your images and the reference batch.\n\n"
473
- "> This tab is slow — keep prompts to 3-10 and inference steps at 15-20."
474
- )
475
- with gr.Row():
476
- with gr.Column(scale=1):
477
- batch_prompts = gr.Textbox(
478
- label="Prompts (one per line)", lines=6,
479
- placeholder="a sunset over the ocean\na robot in a library\na cat in a hat",
480
- )
481
- batch_neg = gr.Textbox(label="Negative prompt",
482
- value="blurry, low quality", lines=1)
483
- with gr.Accordion("Settings", open=False):
484
- batch_steps = gr.Slider(10, 30, 15, step=1,
485
- label="Inference steps (keep low for speed)")
486
- batch_guide = gr.Slider(1.0, 15.0, 7.5, step=0.5, label="Guidance scale")
487
- n_ref_sl = gr.Slider(2, 20, 5, step=1,
488
- label="# reference images from dataset")
489
- batch_btn = gr.Button("Run Batch Evaluation", variant="primary")
490
-
491
- with gr.Column(scale=1):
492
- batch_output = gr.Markdown("")
493
-
494
- batch_btn.click(
495
- evaluate_batch_fid,
496
- inputs=[batch_prompts, batch_neg, batch_steps, batch_guide, n_ref_sl],
497
- outputs=[batch_output],
498
- )
499
-
500
- # ── Tab 4: Metric guide ───────────────────────────────────────────────
501
- with gr.TabItem("Metric Guide"):
502
- gr.Markdown(
503
- """
504
- ## How the metrics map to Precision & Recall
505
-
506
- | Metric | Range | Better when | Classic analogue | Method |
507
- |--------|-------|-------------|-----------------|--------|
508
- | CLIP Score | 0 – 100 | Higher | **Recall** | Cosine sim of CLIP image & text embeddings |
509
- | FID | 0 – ∞ | Lower | **Precision** | Fréchet distance of Inception-v3 feature distributions |
510
- | Aesthetic Score | 1 – 10 | Higher | Quality | LAION linear head on CLIP ViT-L/14 features |
511
-
512
- ---
513
-
514
- ### CLIP Score — Recall analogue
515
- - **What it measures:** Did the image capture the content described in the prompt?
516
- - **How:** CLIP encodes the image and text into a shared embedding space; cosine similarity is computed and scaled to 0-100.
517
- - **Threshold:** ≥ 25 is generally good alignment for SD v1.5.
518
- - **Limit:** CLIP can miss subtle semantic errors and spatial relationships. Prompts are truncated to 77 tokens.
519
-
520
- ### FID — Precision analogue
521
- - **What it measures:** Are the generated images statistically similar to real images?
522
- - **How:** Inception-v3 extracts 2048-d activations; the Fréchet distance between the Gaussian fitted to generated vs. reference features is computed.
523
- - **Threshold:** < 50 is good; < 30 is strong for general SD models.
524
- - **Limit:** Needs dozens of images for reliable estimates — results from 3-5 are directional only.
525
-
526
- ### Aesthetic Score
527
- - **What it measures:** Perceived visual quality, independent of the prompt.
528
- - **How:** A small MLP trained on human LAION ratings predicts a score from CLIP ViT-L/14 embeddings.
529
- - **Threshold:** ≥ 5.0 is considered aesthetically pleasing.
530
-
531
- ---
532
-
533
- ### Why not standard precision/recall?
534
- Traditional precision and recall count true positives against a ground-truth label set.
535
- Generated images have no ground-truth labels, so the community uses:
536
- - **CLIP Score** as the recall proxy (did we capture the prompt?)
537
- - **FID** as the precision proxy (are the images realistic?)
538
- """
539
- )
540
-
541
- # ─────────────────────────────────────────────────────────────────────────────
542
- # 8. Launch
543
- # ─────────────────────────────────────────────────────────────────────────────
544
- if __name__ == "__main__":
545
- # queue() is required for long-running functions (batch eval, FID)
546
- # Without it Gradio times out silently when a function takes > a few seconds
547
- demo.queue().launch()