Skip to content

Commit 97588ec

Browse files
authored
Merge branch 'mainline' into ci-static-validation
2 parents d25db9e + 47189c5 commit 97588ec

13 files changed

Lines changed: 3219 additions & 0 deletions

File tree

job_bundles/text_to_image_batch/README.md

Lines changed: 378 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{"prompt":"a young pizza delivery chef with a red apron and curly hair tying her apron strings in a small pizzeria kitchen, looking out the window at a strange purple sky","id":"scene_01","caption":"Tuesday started weird."}
2+
{"prompt":"wide city street view with a massive alien spaceship hovering above downtown skyscrapers, civilians running in panic, smoke and debris falling","id":"scene_02","caption":"Then it got weirder."}
3+
{"prompt":"the young pizza chef with a red apron on a vintage scooter weaving through smoking street debris, holding a pizza box high in one hand, dynamic motion lines","id":"scene_03","caption":"But pizza waits for no one."}
4+
{"prompt":"the young pizza chef with a red apron sliding sideways across a rooftop, pizza box held aloft, a glowing alien energy beam streaking through the sky behind her","id":"scene_04","caption":"EXPRESS DELIVERY!"}
5+
{"prompt":"the young pizza chef knocking on an apartment door with one hand and balancing a pizza box on the other, a wide-eyed customer peering through the door crack","id":"scene_05","caption":"ORDER UP!"}
6+
{"prompt":"a startled customer in pajamas handing a crumpled dollar bill to the pizza chef in a doorway, a green alien tentacle smashing through the apartment window in the background","id":"scene_06","caption":"Keep the change."}
7+
{"prompt":"the pizza chef giving a confident thumbs up while jumping onto her scooter, the customer fleeing the apartment building behind her","id":"scene_07","caption":"Five stars, please."}
8+
{"prompt":"the pizza chef back inside her small pizzeria with the sun rising over a calm city in the window, a sign on the wall reads WE DELIVER ANYWHERE, she sips coffee with a tired smile","id":"scene_08","caption":"Same time tomorrow?"}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{"id": "bakery_001", "prompt": "Write a catchy slogan for artisan sourdough bread targeting health-conscious millennials for an Instagram ad campaign.", "generated_text": "Real grain. Real fermentation. Real you.", "style": "modern Instagram poster, soft natural lighting, rustic wood and linen, overhead flat lay of golden sourdough loaf, warm bakery tones"}
2+
{"id": "bakery_002", "prompt": "Write a playful slogan for custom birthday cakes targeting parents of kids ages 5-10 for a local flyer campaign.", "generated_text": "Make their day a slice of magic.", "style": "cheerful flyer illustration, bright pastel palette, confetti and balloons, cartoon birthday cake with sparkler"}
3+
{"id": "bakery_003", "prompt": "Write an elegant slogan for tiered wedding cakes targeting engaged couples ages 25-35 for a bridal magazine ad.", "generated_text": "Where every layer tells your love story.", "style": "elegant bridal magazine photography, soft white and ivory palette, three-tier wedding cake with delicate floral detail, romantic backlit window light"}
4+
{"id": "bakery_004", "prompt": "Write a fun and colorful slogan for cupcake party packs targeting college students for a campus poster campaign.", "generated_text": "Twelve little ways to make today better.", "style": "vibrant campus poster illustration, neon colors, bold geometric shapes, dozen cupcakes arranged in a grid, playful typography aesthetic"}
5+
{"id": "bakery_005", "prompt": "Write a warm nostalgic slogan for chocolate chip cookies targeting grandparents buying for grandchildren for a holiday email campaign.", "generated_text": "From your kitchen, with love — even when you're far apart.", "style": "warm nostalgic holiday photography, golden hour kitchen lighting, chocolate chip cookies on a vintage cooling rack, knit sweater texture in background"}
6+
{"id": "bakery_006", "prompt": "Write a luxurious slogan for French macarons targeting corporate clients for a B2B gifting brochure.", "generated_text": "The gift that says: I noticed.", "style": "minimalist luxury B2B brochure photography, marble surface, neat row of pastel French macarons, soft directional studio light"}
7+
{"id": "bakery_007", "prompt": "Write a wholesome slogan for gluten-free brownies targeting people with dietary restrictions for a Google Search ad.", "generated_text": "Fudgy. Indulgent. Yes, really gluten-free.", "style": "clean wholesome product photography, neutral background, single rich gluten-free brownie square cut in half showing fudgy interior, natural daylight"}
8+
{"id": "bakery_008", "prompt": "Write an indulgent slogan for croissants targeting young professionals for a morning commute billboard.", "generated_text": "Buttery layers. Bigger ideas.", "style": "moody high-contrast billboard photography, dramatic morning side light, single golden flaky croissant on dark slate, espresso shadow in foreground"}
9+
{"id": "bakery_009", "prompt": "Write a trendy slogan for cake pops targeting Gen Z for a TikTok video ad.", "generated_text": "Tiny. Iconic. Obsessed.", "style": "trendy TikTok-style flat lay, hot pink and lime green palette, glittery sprinkle-covered cake pops on wavy patterned background, bold high-saturation"}
10+
{"id": "bakery_010", "prompt": "Write a heartfelt slogan for custom anniversary cakes targeting couples celebrating 25+ years for a Facebook ad.", "generated_text": "Twenty-five years, and still the sweetest chapter.", "style": "heartfelt anniversary photography, warm candlelit dining room, two-tier silver-themed cake with calligraphy 25, soft bokeh of fairy lights"}
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/env python3
2+
"""Aggregate per-task image metadata into output.jsonl + gallery.html.
3+
4+
Reads OutputDir/output/metadata/image_NNNN.json sidecars produced by
5+
run_task.py and builds:
6+
- OutputDir/output/output.jsonl — one line per generated image (metadata)
7+
- OutputDir/output/gallery.html — self-contained gallery viewer that
8+
references images/image_NNNN.png by
9+
relative path
10+
11+
The gallery.html is a single static HTML file; open it from the downloaded
12+
OutputDir/output/ directory in any browser, no server needed.
13+
"""
14+
import argparse
15+
import json
16+
import os
17+
import re
18+
import sys
19+
20+
21+
HTML_TEMPLATE = r"""<!DOCTYPE html>
22+
<html lang="en">
23+
<head>
24+
<meta charset="UTF-8">
25+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
26+
<title>text_to_image_batch — Gallery</title>
27+
<style>
28+
* { box-sizing: border-box; margin: 0; padding: 0; }
29+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #1a1a2e; padding: 2rem; }
30+
.container { max-width: 1200px; margin: 0 auto; }
31+
h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
32+
.subtitle { color: #666; margin-bottom: 1.5rem; font-size: 0.9rem; }
33+
.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
34+
.stat { background: #fff; border-radius: 8px; padding: 1rem 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
35+
.stat-value { font-size: 1.4rem; font-weight: 600; color: #4a90d9; }
36+
.stat-label { font-size: 0.75rem; color: #888; margin-top: 0.2rem; }
37+
.controls { margin-bottom: 1rem; display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; }
38+
.search { padding: 0.5rem 0.75rem; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9rem; width: 280px; }
39+
.search:focus { outline: none; border-color: #4a90d9; }
40+
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.85rem; cursor: pointer; font-weight: 500; background: #e8ecf0; color: #333; }
41+
.btn:hover { background: #dde2e8; }
42+
.count { font-size: 0.85rem; color: #666; }
43+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
44+
.card { background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer; transition: transform 0.15s, box-shadow 0.15s; }
45+
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
46+
.card img { width: 100%; height: auto; display: block; background: #f0f0f0; }
47+
.card-info { padding: 0.75rem; }
48+
.card-id { font-size: 0.7rem; color: #999; font-family: monospace; margin-bottom: 0.25rem; }
49+
.card-prompt { font-size: 0.8rem; color: #333; line-height: 1.4; max-height: 4.2em; overflow: hidden; }
50+
.modal { position: fixed; inset: 0; background: rgba(0,0,0,0.85); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 2rem; }
51+
.modal.hidden { display: none; }
52+
.modal-content { max-width: 90vw; max-height: 95vh; display: flex; flex-direction: column; gap: 0.75rem; align-items: center; }
53+
.modal-content img { max-width: 100%; max-height: 75vh; border-radius: 4px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
54+
.modal-info { background: rgba(255,255,255,0.95); border-radius: 6px; padding: 0.75rem 1rem; font-size: 0.85rem; max-width: 800px; }
55+
.modal-info-row { margin-bottom: 0.4rem; }
56+
.modal-info-row:last-child { margin-bottom: 0; }
57+
.modal-info-label { font-size: 0.7rem; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-right: 0.5rem; }
58+
.modal-info-value { white-space: pre-wrap; word-break: break-word; }
59+
.modal-meta { display: flex; gap: 1rem; font-size: 0.75rem; color: #666; }
60+
.close-hint { color: rgba(255,255,255,0.6); font-size: 0.8rem; }
61+
</style>
62+
</head>
63+
<body>
64+
<div class="container">
65+
<h1>text_to_image_batch — Gallery</h1>
66+
<p class="subtitle">Generated with AWS Deadline Cloud</p>
67+
68+
<div class="stats">
69+
<div class="stat"><div class="stat-value" id="totalCount">0</div><div class="stat-label">Total Images</div></div>
70+
<div class="stat"><div class="stat-value" id="totalTime">0s</div><div class="stat-label">Total Generation Time</div></div>
71+
<div class="stat"><div class="stat-value" id="avgTime">0s</div><div class="stat-label">Avg per Image</div></div>
72+
<div class="stat"><div class="stat-value" id="resolution">—</div><div class="stat-label">Resolution</div></div>
73+
</div>
74+
75+
<div class="controls">
76+
<input type="text" class="search" id="search" placeholder="Search prompts...">
77+
<button class="btn" onclick="exportCsv()">Export CSV</button>
78+
<span class="count" id="showing"></span>
79+
</div>
80+
81+
<div class="grid" id="grid"></div>
82+
</div>
83+
84+
<div class="modal hidden" id="modal" onclick="closeModal(event)">
85+
<div class="modal-content" onclick="event.stopPropagation()">
86+
<img id="modalImg" src="" alt="">
87+
<div class="modal-info">
88+
<div class="modal-info-row">
89+
<span class="modal-info-label">ID</span>
90+
<span class="modal-info-value" id="modalId"></span>
91+
</div>
92+
<div class="modal-info-row">
93+
<span class="modal-info-label">Final prompt</span>
94+
<span class="modal-info-value" id="modalPrompt"></span>
95+
</div>
96+
<div class="modal-info-row modal-meta">
97+
<span><strong id="modalSize"></strong> px</span>
98+
<span><strong id="modalSteps"></strong> steps</span>
99+
<span>guidance <strong id="modalGuidance"></strong></span>
100+
<span>seed <strong id="modalSeed"></strong></span>
101+
<span><strong id="modalTime"></strong></span>
102+
</div>
103+
</div>
104+
<div class="close-hint">click anywhere outside to close</div>
105+
</div>
106+
</div>
107+
108+
<script>
109+
const DATA = __DATA_PLACEHOLDER__;
110+
111+
function esc(s) {
112+
const el = document.createElement('span');
113+
el.textContent = s == null ? '' : String(s);
114+
return el.innerHTML.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
115+
}
116+
117+
function render(filter) {
118+
const lower = (filter || '').toLowerCase();
119+
const filtered = lower
120+
? DATA.filter(d =>
121+
(d.final_prompt || '').toLowerCase().includes(lower) ||
122+
(d.prompt || '').toLowerCase().includes(lower) ||
123+
(d.generated_text || '').toLowerCase().includes(lower) ||
124+
(d.id || '').toLowerCase().includes(lower))
125+
: DATA;
126+
document.getElementById('showing').textContent = lower ? `Showing ${filtered.length} of ${DATA.length}` : '';
127+
document.getElementById('grid').innerHTML = filtered.map(d => {
128+
const i = DATA.indexOf(d);
129+
const caption = d.final_prompt || d.prompt || d.generated_text || '';
130+
return `
131+
<div class="card" onclick="openModal(${i})">
132+
<img src="images/${esc(d.image)}" alt="${esc(caption.slice(0, 80))}" loading="lazy">
133+
<div class="card-info">
134+
<div class="card-id">${esc(d.id || '#' + (d.index || i + 1))}</div>
135+
<div class="card-prompt">${esc(caption)}</div>
136+
</div>
137+
</div>`;
138+
}).join('');
139+
}
140+
141+
function openModal(i) {
142+
const d = DATA[i];
143+
document.getElementById('modalImg').src = 'images/' + d.image;
144+
document.getElementById('modalId').textContent = d.id || '#' + (d.index || i + 1);
145+
document.getElementById('modalPrompt').textContent = d.final_prompt || d.prompt || d.generated_text || '(no prompt)';
146+
document.getElementById('modalSize').textContent = `${d.width || '?'}×${d.height || '?'}`;
147+
document.getElementById('modalSteps').textContent = d.inference_steps || '?';
148+
document.getElementById('modalGuidance').textContent = d.guidance_scale != null ? d.guidance_scale : '?';
149+
document.getElementById('modalSeed').textContent = d.seed != null ? d.seed : '?';
150+
document.getElementById('modalTime').textContent = d.elapsed_seconds != null ? d.elapsed_seconds.toFixed(1) + 's' : '';
151+
document.getElementById('modal').classList.remove('hidden');
152+
}
153+
154+
function closeModal(e) {
155+
// Only close on background click; clicks on the inner content stop propagation.
156+
document.getElementById('modal').classList.add('hidden');
157+
}
158+
159+
function csvSafe(c) { const s = String(c); return /^[=+\-@]/.test(s) ? "\t" + s : s; }
160+
function exportCsv() {
161+
const rows = [['id', 'image', 'final_prompt', 'width', 'height', 'inference_steps', 'guidance_scale', 'seed', 'elapsed_seconds']];
162+
DATA.forEach(d => rows.push([
163+
d.id || '', d.image || '', d.final_prompt || d.prompt || '',
164+
d.width || '', d.height || '', d.inference_steps || '',
165+
d.guidance_scale == null ? '' : d.guidance_scale,
166+
d.seed == null ? '' : d.seed,
167+
d.elapsed_seconds == null ? '' : d.elapsed_seconds,
168+
]));
169+
const csv = rows.map(r => r.map(c => '"' + csvSafe(c).replace(/"/g, '""') + '"').join(',')).join('\n');
170+
const blob = new Blob([csv], { type: 'text/csv' });
171+
const a = document.createElement('a');
172+
a.href = URL.createObjectURL(blob);
173+
a.download = 'gallery.csv';
174+
a.click();
175+
}
176+
177+
// Initial stats
178+
document.getElementById('totalCount').textContent = DATA.length;
179+
const totalSeconds = DATA.reduce((s, d) => s + (d.elapsed_seconds || 0), 0);
180+
document.getElementById('totalTime').textContent = totalSeconds.toFixed(1) + 's';
181+
document.getElementById('avgTime').textContent = (DATA.length ? (totalSeconds / DATA.length).toFixed(1) : '0') + 's';
182+
const sizes = new Set(DATA.map(d => `${d.width}×${d.height}`));
183+
document.getElementById('resolution').textContent = sizes.size === 1 ? [...sizes][0] : (sizes.size + ' sizes');
184+
185+
document.getElementById('search').addEventListener('input', e => render(e.target.value));
186+
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
187+
render('');
188+
</script>
189+
</body>
190+
</html>
191+
"""
192+
193+
194+
def _index_from_filename(name):
195+
m = re.search(r"\d+", name)
196+
return int(m.group()) if m else 0
197+
198+
199+
def main():
200+
parser = argparse.ArgumentParser()
201+
parser.add_argument(
202+
"--output-dir",
203+
required=True,
204+
help="Job's OutputDir; aggregate reads from OutputDir/output/metadata/ "
205+
"and writes OutputDir/output/{output.jsonl,gallery.html}",
206+
)
207+
args = parser.parse_args()
208+
209+
# All artifacts live under OutputDir/output/ — keeps each job's results
210+
# grouped so multiple runs can share an OutputDir without clobbering.
211+
output_root = os.path.join(args.output_dir, "output")
212+
metadata_dir = os.path.join(output_root, "metadata")
213+
images_dir = os.path.join(output_root, "images")
214+
215+
if not os.path.isdir(metadata_dir):
216+
print(f"ERROR: metadata dir does not exist: {metadata_dir}", file=sys.stderr)
217+
sys.exit(1)
218+
219+
items = []
220+
for filename in sorted(os.listdir(metadata_dir), key=_index_from_filename):
221+
if not filename.endswith(".json"):
222+
continue
223+
path = os.path.join(metadata_dir, filename)
224+
with open(path) as f:
225+
try:
226+
items.append(json.load(f))
227+
except json.JSONDecodeError as e:
228+
print(f"WARN: skipping malformed {filename}: {e}", file=sys.stderr)
229+
230+
if not items:
231+
print(
232+
f"ERROR: no metadata files found in {metadata_dir} — did the Generate step succeed?",
233+
file=sys.stderr,
234+
)
235+
sys.exit(1)
236+
237+
# output.jsonl — one line per image, easy to grep / pipe into other tools.
238+
output_jsonl = os.path.join(output_root, "output.jsonl")
239+
with open(output_jsonl, "w") as f:
240+
for item in items:
241+
f.write(json.dumps(item) + "\n")
242+
print(f"Aggregated {len(items)} items into {output_jsonl}")
243+
244+
# gallery.html — the human-friendly viewer. References images/ relatively
245+
# (sibling directory inside output/).
246+
gallery_path = os.path.join(output_root, "gallery.html")
247+
data_json = json.dumps(items, ensure_ascii=False)
248+
html = HTML_TEMPLATE.replace("__DATA_PLACEHOLDER__", data_json)
249+
with open(gallery_path, "w") as f:
250+
f.write(html)
251+
print(f"Generated gallery: {gallery_path}")
252+
253+
# Sanity check that referenced images exist (warn, don't fail).
254+
missing = [i["image"] for i in items if not os.path.exists(os.path.join(images_dir, i.get("image", "")))]
255+
if missing:
256+
print(
257+
f"WARN: {len(missing)} referenced image(s) missing from {images_dir}: "
258+
f"{missing[:5]}{'...' if len(missing) > 5 else ''}",
259+
file=sys.stderr,
260+
)
261+
262+
263+
if __name__ == "__main__":
264+
main()

0 commit comments

Comments
 (0)