Skip to content

Commit 2ab4701

Browse files
cursoragentjmjava
andcommitted
Fail closed when image-generate returns empty bytes
A 0-byte PNG was written as generated, including --force over a committed asset. Raise ImageGenerationError before write, matching TTS empty-audio (#99). Co-authored-by: jmjava <jmjava@gmail.com>
1 parent d8e65fe commit 2ab4701

5 files changed

Lines changed: 75 additions & 7 deletions

File tree

‎milestones/README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ repositories that install `docgen` and maintain their own demo bundle. The
55
library no longer ships an in-repo dogfood; consumers are the integration test
66
of record.
77

8-
**Active:** **[hint-front-matter-yaml.md](hint-front-matter-yaml.md)** —
9-
Invalid `hints/*.md` YAML front matter must fail `yaml-generate`, not skip
10-
hint `visual_map` wiring.
8+
**Active:** **[image-empty-bytes.md](image-empty-bytes.md)** —
9+
`image-generate` must not write empty PNG bytes as success.
1110

1211
**Shipped:**
12+
- **[hint-front-matter-yaml.md](hint-front-matter-yaml.md)** —
13+
Invalid `hints/*.md` YAML front matter must fail `yaml-generate`, not skip
14+
hint `visual_map` wiring (#102).
1315
- **[av-sync-anchor-keywords.md](av-sync-anchor-keywords.md)** —
1416
`validation.av_sync.anchor_keywords` must be a mapping of keyword rows
1517
(#101).

‎milestones/hint-front-matter-yaml.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Milestone: invalid hint front matter must not skip wiring
22

3-
**Status:** Active
4-
**PR:** pending
3+
**Status:** Shipped
4+
**PR:** #102
55
**Depends on:** `milestones/av-sync-anchor-keywords.md` (PR #101),
66
`milestones/yaml-generate-mappings.md` (PR #91)
77

‎milestones/image-empty-bytes.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Milestone: image-generate must not write empty assets
2+
3+
**Status:** Active
4+
**PR:** pending
5+
**Depends on:** `milestones/hint-front-matter-yaml.md` (PR #102),
6+
`milestones/tts-empty-audio.md` (PR #99)
7+
8+
## Problem
9+
10+
`generate_images_for_spec` wrote whatever `image_fn` / the Images API
11+
returned, including **0-byte** files, and reported `generated`. Pipeline
12+
Manim then loaded an empty PNG. TTS already fails closed on empty audio
13+
(#99); images did not.
14+
15+
`generate_image_bytes` also returned empty `b64_json` / URL bodies.
16+
17+
## Goal
18+
19+
Empty provider bytes raise `ImageGenerationError` **before** writing.
20+
`--force` must not clobber a committed asset with empty bytes.
21+
22+
## Done when
23+
24+
- [x] Empty `image_fn` / API bytes raise and do not write a new file.
25+
- [x] `--force` with empty bytes leaves an existing asset unchanged.
26+
- [x] Empty b64 / URL download raises in `generate_image_bytes`.
27+
- [x] Tests for the spec write path.
28+
- [ ] `ruff check src/ tests/`
29+
- [ ] `pytest tests/`
30+
- [ ] `docgen benchmark` (no clock change)
31+
32+
## Out of scope
33+
34+
- `image_generation.model` / `size` string typing is separate.
35+
- Existing non-empty assets are still skipped unless `--force`.

‎src/docgen/image_generate.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,25 @@ def generate_image_bytes(
103103
data = response.data[0] if response.data else None
104104
b64 = getattr(data, "b64_json", None) if data is not None else None
105105
if b64:
106-
return base64.b64decode(b64)
106+
raw = base64.b64decode(b64)
107+
if not raw:
108+
raise ImageGenerationError(
109+
f"Image model {resolved!r} returned empty b64_json bytes"
110+
)
111+
return raw
107112
url = getattr(data, "url", None) if data is not None else None
108113
if url:
109114
try:
110-
return fetch_url_bytes(str(url))
115+
raw = fetch_url_bytes(str(url))
111116
except Exception as exc:
112117
raise ImageGenerationError(
113118
f"Image model {resolved!r} returned a URL but download failed: {exc}."
114119
) from exc
120+
if not raw:
121+
raise ImageGenerationError(
122+
f"Image model {resolved!r} URL download was empty"
123+
)
124+
return raw
115125
raise ImageGenerationError(
116126
f"Image response for model {resolved!r} had neither b64_json nor url; "
117127
"cannot write the asset."
@@ -178,6 +188,10 @@ def generate_images_for_spec(
178188
)
179189
)
180190
data = fn(prompt)
191+
if not data:
192+
raise ImageGenerationError(
193+
f"{spec_path}: image element {rel!r} — provider returned empty bytes"
194+
)
181195
out.parent.mkdir(parents=True, exist_ok=True)
182196
out.write_bytes(data)
183197
results.append(ImageAssetResult(rel, out, "generated", prompt))

‎tests/test_image_generate.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,23 @@ def test_bundle_scan_generates_only_missing(cfg: Config) -> None:
103103
assert existing.read_bytes() == b"committed"
104104

105105

106+
def test_empty_provider_bytes_fails_without_writing(cfg: Config) -> None:
107+
spec = _write_spec(cfg.animations_dir / "specs" / "01-x.scene.yaml")
108+
with pytest.raises(ImageGenerationError, match="empty bytes"):
109+
generate_images_for_spec(cfg, spec, image_fn=lambda p: b"")
110+
assert not (cfg.base_dir / "images" / "arch.png").exists()
111+
112+
113+
def test_empty_provider_bytes_does_not_clobber_existing(cfg: Config) -> None:
114+
spec = _write_spec(cfg.animations_dir / "specs" / "01-x.scene.yaml")
115+
out = cfg.base_dir / "images" / "arch.png"
116+
out.parent.mkdir(parents=True)
117+
out.write_bytes(b"committed")
118+
with pytest.raises(ImageGenerationError, match="empty bytes"):
119+
generate_images_for_spec(cfg, spec, force=True, image_fn=lambda p: b"")
120+
assert out.read_bytes() == b"committed"
121+
122+
106123
def test_no_specs_dir_is_noop(cfg: Config) -> None:
107124
assert spec_files_for_bundle(cfg) == []
108125
assert generate_missing_images_for_bundle(cfg, image_fn=lambda p: _PNG_BYTES) == []

0 commit comments

Comments
 (0)