Takosaga commited on
Commit
113ea78
Β·
1 Parent(s): 9e96fae

test: verify image generation changes with smoke test

Browse files
docs/superpowers/plans/2026-06-12-image-generation.md ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Image Generation Pipeline Integration β€” Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Wire FLUX.2-klein image generation into EuropaLex's Phase 2 pipeline, switch model source from GGUF to diffusers, and make both Audio and Images toggles default ON after Phase 1.
6
+
7
+ **Architecture:** Image generation is added as a batch step between translation (Phase 2) and TTS audio in `app.py:generate_media_async()`. The existing `ImageGenEngine` already loads the correct model via diffusers β€” no engine changes needed. Model download entry and config are updated to point to the diffusers-compatible safetensors repo. Card dimensions are updated to accommodate landscape images.
8
+
9
+ **Tech Stack:** Python 3.12+, diffusers (Flux2KleinPipeline), Gradio 6, Pydantic
10
+
11
+ ---
12
+
13
+ ### Task 1: Update Model Download Script
14
+
15
+ **Files:**
16
+ - Modify: `models/download_models.py`
17
+
18
+ Change the `flux` model entry from GGUF (ComfyUI) to diffusers-compatible safetensors. The new repo `black-forest-labs/FLUX.2-klein-4B` contains many files (~10–12 GB). Use `allow_patterns` to download all safetensors weights plus scheduler/tokenizer configs.
19
+
20
+ - [ ] **Step 1: Replace the flux entry in MODELS dict**
21
+
22
+ Change lines 38-42 from:
23
+ ```python
24
+ "flux": {
25
+ "repo": "unsloth/FLUX.2-klein-4B-GGUF",
26
+ "files": ["flux-2-klein-4b-Q4_K_M.gguf"],
27
+ "description": "FLUX.2-klein 4B Q4_K_M image gen (ComfyUI-GGUF)",
28
+ },
29
+ ```
30
+ to:
31
+ ```python
32
+ "flux": {
33
+ "repo": "black-forest-labs/FLUX.2-klein-4B",
34
+ "files": None, # Download all files β€” safetensors weights + configs (~10–12 GB)
35
+ "description": "FLUX.2-klein 4B image gen (diffusers)",
36
+ },
37
+ ```
38
+
39
+ Also update the module docstring line that says `flux β€” FLUX.2-klein 4B Q4_K_M image gen (ComfyUI-GGUF)` to:
40
+ ```
41
+ flux β€” FLUX.2-klein 4B image gen (diffusers)
42
+ ```
43
+
44
+ - [ ] **Step 2: Update download_model() to handle None files**
45
+
46
+ In the `download_model` function, when `info["files"]` is `None`, skip the file listing and use `allow_patterns=None` (which downloads everything):
47
+
48
+ ```python
49
+ def download_model(name: str, target_dir: Path) -> None:
50
+ """Download a single model from HF Hub using Python API."""
51
+ info = MODELS[name]
52
+ output_dir = target_dir / name
53
+
54
+ print(f"Downloading {info['description']} ({info['repo']})...")
55
+ print(f" Target: {output_dir}")
56
+ if info["files"]:
57
+ for f in info["files"]:
58
+ print(f" πŸ“¦ {f}")
59
+ else:
60
+ print(f" πŸ“¦ All files ({info['description']} is ~10–12 GB)")
61
+ print()
62
+
63
+ from huggingface_hub import snapshot_download
64
+
65
+ snapshot_download(
66
+ repo_id=info["repo"],
67
+ allow_patterns=info["files"] or ["*"], # None β†’ download all
68
+ local_dir=str(output_dir),
69
+ resume_download=True,
70
+ )
71
+ print(f" βœ“ Done β€” {output_dir}\n")
72
+ ```
73
+
74
+ - [ ] **Step 3: Commit**
75
+
76
+ ```bash
77
+ git add models/download_models.py
78
+ git commit -m "refactor: switch flux model from GGUF to diffusers safetensors"
79
+ ```
80
+
81
+ ---
82
+
83
+ ### Task 2: Update Settings Configuration
84
+
85
+ **Files:**
86
+ - Modify: `configs/settings.yaml`
87
+
88
+ Update the `flux` section to reflect the new diffusers-compatible model path and runtime.
89
+
90
+ - [ ] **Step 1: Replace the flux config block**
91
+
92
+ Change lines 19-23 from:
93
+ ```yaml
94
+ flux:
95
+ repo: unsloth/FLUX.2-klein-4B-GGUF
96
+ file: flux-2-klein-4b-Q4_K_M.gguf
97
+ runtime: ComfyUI-GGUF
98
+ quant: Q4_K_M
99
+ ```
100
+ to:
101
+ ```yaml
102
+ flux:
103
+ repo: black-forest-labs/FLUX.2-klein-4B
104
+ file: null # diffuses model β€” no single GGUF file
105
+ runtime: diffusers
106
+ quant: null
107
+ ```
108
+
109
+ Note: The `EngineConfig.from_settings_yaml()` method does not currently read the `flux` section, so these fields are informational only. Setting them to `null` keeps the YAML valid without breaking existing code.
110
+
111
+ - [ ] **Step 2: Commit**
112
+
113
+ ```bash
114
+ git add configs/settings.yaml
115
+ git commit -m "docs: update flux config for diffusers runtime"
116
+ ```
117
+
118
+ ---
119
+
120
+ ### Task 3: Update Toggle Defaults in app.py
121
+
122
+ **Files:**
123
+ - Modify: `app.py`
124
+
125
+ Both toggles start OFF during Phase 1 (disabled via CSS), then turn ON when Phase 2 is enabled. Change the default values from `False` to `True`.
126
+
127
+ - [ ] **Step 1: Change toggle creation defaults**
128
+
129
+ Find the toggle creation lines (~line 240 in app.py) and change both from `value=False` to `value=True`:
130
+
131
+ ```python
132
+ # Before:
133
+ audio_toggle = create_toggle("πŸ”Š Audio", value=False, elem_id="toggle-audio")
134
+ images_toggle = create_toggle("πŸ–ΌοΈ Images", value=False, elem_id="toggle-images")
135
+
136
+ # After:
137
+ audio_toggle = create_toggle("πŸ”Š Audio", value=True, elem_id="toggle-audio")
138
+ images_toggle = create_toggle("πŸ–ΌοΈ Images", value=True, elem_id="toggle-images")
139
+ ```
140
+
141
+ - [ ] **Step 2: Update _enable_phase2() return values**
142
+
143
+ The `_enable_phase2()` function returns Gradio component states. Change both Checkbox values from `False` to `True`:
144
+
145
+ ```python
146
+ # Before:
147
+ def _enable_phase2():
148
+ return (
149
+ gr.Checkbox(interactive=True, value=False),
150
+ gr.Checkbox(interactive=True, value=False),
151
+ gr.Button(interactive=True),
152
+ gr.Dropdown(interactive=True),
153
+ "",
154
+ )
155
+
156
+ # After:
157
+ def _enable_phase2():
158
+ return (
159
+ gr.Checkbox(interactive=True, value=True),
160
+ gr.Checkbox(interactive=True, value=True),
161
+ gr.Button(interactive=True),
162
+ gr.Dropdown(interactive=True),
163
+ "",
164
+ )
165
+ ```
166
+
167
+ The `_reset_to_idle()` function should keep `value=False` β€” when the user changes parameters and the UI resets, toggles should be OFF (disabled). Only after Phase 1 completes should they become ON.
168
+
169
+ - [ ] **Step 3: Commit**
170
+
171
+ ```bash
172
+ git add app.py
173
+ git commit -m "style: set Audio and Images toggle defaults to ON"
174
+ ```
175
+
176
+ ---
177
+
178
+ ### Task 4: Add Image Generation to generate_media_async()
179
+
180
+ **Files:**
181
+ - Modify: `app.py` (in `generate_media_async()` function)
182
+
183
+ Add image generation as a batch step between translation completion and TTS audio generation. Mirrors the existing TTS pattern exactly.
184
+
185
+ - [ ] **Step 1: Update _progress_pct to use spec progress ranges**
186
+
187
+ The translation phase should occupy 15%β†’70%, images 70%β†’85%, audio 85%β†’100%. Modify `_progress_pct` to accept a configurable range (defaulting to the new ranges):
188
+
189
+ ```python
190
+ # Before (existing function):
191
+ def _progress_pct(translated_idx: int, total: int) -> tuple[float, str]:
192
+ if total <= 1:
193
+ return 100.0, "Translation complete!"
194
+ pct = ((translated_idx + 1) / total) * 100
195
+ remaining = total - (translated_idx + 1)
196
+ if pct >= 100:
197
+ return 100.0, "Translation complete!"
198
+ return round(pct, 1), f"Translated {translated_idx + 1}/{total} β€” {remaining} remaining..."
199
+
200
+ # After:
201
+ def _progress_pct(
202
+ translated_idx: int,
203
+ total: int,
204
+ start_pct: float = 15.0,
205
+ end_pct: float = 70.0,
206
+ ) -> tuple[float, str]:
207
+ """Calculate progress percentage for translation within a given range."""
208
+ if total <= 1:
209
+ return end_pct, "Translation complete!"
210
+ pct = start_pct + ((translated_idx + 1) / total) * (end_pct - start_pct)
211
+ remaining = total - (translated_idx + 1)
212
+ if pct >= end_pct:
213
+ return end_pct, "Translation complete!"
214
+ return round(pct, 1), f"Translated {translated_idx + 1}/{total} β€” {remaining} remaining..."
215
+ ```
216
+
217
+ - [ ] **Step 2: Update the translation loop to use new progress range**
218
+
219
+ In `generate_media_async()`, find the call to `_progress_pct` inside the translation loop and add the explicit range parameters:
220
+
221
+ ```python
222
+ # Change from:
223
+ pct, label = _progress_pct(i, total)
224
+
225
+ # To:
226
+ pct, label = _progress_pct(i, total, start_pct=15.0, end_pct=70.0)
227
+ ```
228
+
229
+ - [ ] **Step 3: Add image generation between translation and audio**
230
+
231
+ After the translation loop completes (after `for i, english_text in enumerate(_current_texts):`), add the image generation block before the existing TTS block. Insert this code right after the translation loop ends and before the `tts_generated = False` line:
232
+
233
+ ```python
234
+ # Generate images for all translations if requested
235
+ image_paths: list[str | None] = [None] * len(cards)
236
+ if include_images and cards:
237
+ yield generate_progress_html(70, "Generating images..."), generate_cards_html(
238
+ cards, include_image=True, include_audio=tts_generated, placeholder_back=False
239
+ )
240
+ try:
241
+ img_engine = pool.get_image_engine()
242
+ output_dir = Path(config.models_dir) / "output" / "images"
243
+ # Build prompts from English text + CEFR level
244
+ prompts = []
245
+ for card in cards:
246
+ prompt = (
247
+ f"Simple educational illustration for language learning: {card['text']}. "
248
+ f"Level: {cefr.value}. No text in image."
249
+ )
250
+ prompts.append(prompt)
251
+ image_result = img_engine.generate(prompts, output_dir)
252
+ image_paths = image_result.image_paths
253
+ # Attach image paths to cards
254
+ for i, path in enumerate(image_paths):
255
+ if path is not None:
256
+ cards[i]["image_path"] = path
257
+ except Exception as e:
258
+ logger.error("Image generation failed: %s", e, exc_info=True)
259
+ # Cards remain without images β€” user can retry
260
+ ```
261
+
262
+ - [ ] **Step 4: Update the TTS progress start point**
263
+
264
+ Change the TTS audio progress from 70% to 85% (since image generation now occupies 70-85):
265
+
266
+ ```python
267
+ # Before:
268
+ yield generate_progress_html(70, "Generating audio..."), generate_cards_html(
269
+ cards, include_image=include_images, include_audio=True, placeholder_back=False
270
+ )
271
+
272
+ # After:
273
+ yield generate_progress_html(85, "Generating audio..."), generate_cards_html(
274
+ cards, include_image=include_images, include_audio=tts_generated, placeholder_back=False
275
+ )
276
+ ```
277
+
278
+ - [ ] **Step 5: Update the final yield to reflect image generation**
279
+
280
+ The final yield already passes `include_image=include_images` β€” this is correct. No change needed for the final yield line itself. However, update the completion label to mention images when they were generated:
281
+
282
+ ```python
283
+ # Before:
284
+ final_label = "Translation and audio complete!" if tts_generated else "Translation complete!"
285
+
286
+ # After:
287
+ if include_images:
288
+ if tts_generated:
289
+ final_label = "Translation, images, and audio complete!"
290
+ else:
291
+ final_label = "Translation and images complete!"
292
+ else:
293
+ final_label = "Translation and audio complete!" if tts_generated else "Translation complete!"
294
+ ```
295
+
296
+ - [ ] **Step 6: Commit**
297
+
298
+ ```bash
299
+ git add app.py
300
+ git commit -m "feat: add image generation to Phase 2 pipeline"
301
+ ```
302
+
303
+ ---
304
+
305
+ ### Task 5: Update Card Dimensions for Images
306
+
307
+ **Files:**
308
+ - Modify: `frontend/ui/cards.py` (in `render_card_html()` function)
309
+
310
+ Update the adaptive card dimensions to accommodate 600Γ—400 landscape images. The image box renders at ~120px tall in a ~180px wide container.
311
+
312
+ - [ ] **Step 1: Update min_height values**
313
+
314
+ Find the dimension block in `render_card_html()` (~lines 79-87) and update:
315
+
316
+ ```python
317
+ # Before:
318
+ if include_image and include_audio:
319
+ width = 190
320
+ min_height = 200
321
+ elif include_image:
322
+ width = 180
323
+ min_height = 170
324
+ elif include_audio:
325
+ width = 180
326
+ min_height = 160
327
+ else:
328
+ width = 160
329
+ min_height = 90
330
+
331
+ # After (matching spec table):
332
+ if include_image and include_audio:
333
+ width = 190
334
+ min_height = 350
335
+ elif include_image:
336
+ width = 180
337
+ min_height = 310
338
+ elif include_audio:
339
+ width = 180
340
+ min_height = 270
341
+ else:
342
+ width = 160
343
+ min_height = 90
344
+ ```
345
+
346
+ - [ ] **Step 2: Commit**
347
+
348
+ ```bash
349
+ git add frontend/ui/cards.py
350
+ git commit -m "style: update card dimensions for landscape image layout"
351
+ ```
352
+
353
+ ---
354
+
355
+ ### Task 6: Verify with Smoke Test
356
+
357
+ **Files:**
358
+ - Run: `python scripts/smoke_test.py`
359
+
360
+ - [ ] **Step 1: Run smoke test**
361
+
362
+ ```bash
363
+ python scripts/smoke_test.py
364
+ ```
365
+
366
+ Expected output: clean exit with all checks passing (no traceback). Specifically verify:
367
+ - `βœ“ core.types imports OK`
368
+ - `βœ“ core.engine imports OK`
369
+ - `βœ“ CardData validation OK`
370
+ - `βœ“ TextResult validation OK`
371
+ - `βœ“ AudioResult validation OK`
372
+ - `βœ“ ImageResult validation OK`
373
+ - `βœ“ TextResult.validate_and_parse gate OK`
374
+ - `βœ“ frontend.ui imports OK`
375
+ - `βœ“ app module loads OK`
376
+
377
+ - [ ] **Step 2: If smoke test passes, commit**
378
+
379
+ ```bash
380
+ git add -A
381
+ git commit -m "test: verify image generation changes with smoke test"
382
+ ```
383
+
384
+ - [ ] **Step 3: Manual verification (optional but recommended)**
385
+
386
+ Start the Gradio app and verify:
387
+ ```bash
388
+ python app.py
389
+ ```
390
+
391
+ Check in browser at `http://localhost:7860`:
392
+ 1. Both Audio and Images toggles are checked after Phase 1 text generation
393
+ 2. Toggles are disabled (dimmed) before Phase 1
394
+ 3. Clicking "Generate Cards" with Images ON shows image placeholders on cards during translation, then actual images after generation
395
+ 4. Progress bar flows: preparing β†’ translating β†’ generating images β†’ generating audio β†’ complete
396
+ 5. Cards display at the correct height for each media combination
397
+
398
+ ---
399
+
400
+ ## Self-Review Checklist
401
+
402
+ **1. Spec coverage:**
403
+ - βœ… Model source change (Task 1 + Task 2): flux entry updated from GGUF to diffusers safetensors repo
404
+ - βœ… Toggle defaults ON (Task 3): both toggles set to `value=True` in creation and `_enable_phase2()`
405
+ - βœ… Image generation in pipeline (Task 4): batch generation after translation, before audio; prompts built from English text + CEFR level; image paths attached to cards; progress 70%β†’85%; error handling tracks None per card
406
+ - βœ… Card layout dimensions (Task 5): updated min_height values matching spec table
407
+ - βœ… Error handling: failed images tracked as None in list, don't block audio or rendering
408
+
409
+ **2. Placeholder scan:** No placeholders found. All code is concrete.
410
+
411
+ **3. Type consistency:**
412
+ - `include_images` parameter flows from Gradio handler β†’ `_handle_media_generation_v2` β†’ `generate_media_async()` β€” all use the same name and boolean type
413
+ - `image_path` dict key matches what `render_card_html()` reads via `card_data.get("image_path")`
414
+ - `cefr.value` used for prompt string (CEFRLevel enum β†’ "B1" etc.)
415
+ - `ImageResult.image_paths` pattern mirrors `AudioResult.audio_paths` (already exists in types.py)
416
+
417
+ **4. No engine.py changes needed:** The existing `ImageGenEngine` already loads from `black-forest-labs/FLUX.2-klein-4B` via diffusers with bfloat16 + CPU offload. Resolution parameter not added β€” FLUX pipeline handles it internally.
418
+
419
+ ---
420
+
421
+ ## Files Changed Summary
422
+
423
+ | File | Change |
424
+ |---|---|
425
+ | `models/download_models.py` | Replace flux entry: GGUF β†’ diffusers safetensors repo; handle None files in download |
426
+ | `configs/settings.yaml` | Update flux section: new repo, null file/runtime/quant (informational) |
427
+ | `app.py` | Toggle defaults β†’ True; progress ranges 15-70/70-85/85-100; image generation batch step |
428
+ | `frontend/ui/cards.py` | Update min_height: 200β†’350, 170β†’310, 160β†’270 for media combos |