kaupane commited on
Commit
64e3a4e
·
verified ·
1 Parent(s): 9722e34

Clarify generation progress and carousel playback; allow deleting results

Browse files
Files changed (4) hide show
  1. README.md +2 -0
  2. demo/app.py +87 -7
  3. demo/gallery.js +9 -6
  4. demo/style.css +19 -2
README.md CHANGED
@@ -31,6 +31,8 @@ per axis, in multiples of 16.
31
  The result viewer keeps the latest 20 successful generations in the current session.
32
  Selecting a thumbnail restores its recipe. Downloaded PNGs include the actual prompt,
33
  dimensions, seed, steps, CFG, sampler, and model identifier in the `inko` metadata field.
 
 
34
  History clears with a new session; download images you want to keep.
35
 
36
  The carousel contains the 18 release-model images from
 
31
  The result viewer keeps the latest 20 successful generations in the current session.
32
  Selecting a thumbnail restores its recipe. Downloaded PNGs include the actual prompt,
33
  dimensions, seed, steps, CFG, sampler, and model identifier in the `inko` metadata field.
34
+ **Delete image** removes the selected generation and selects a neighboring image.
35
+ Deleting the last image clears the viewer and keeps your input for another generation.
36
  History clears with a new session; download images you want to keep.
37
 
38
  The carousel contains the 18 release-model images from
demo/app.py CHANGED
@@ -47,6 +47,10 @@ SHOWCASE = json.loads((SHOWCASE_DIR / "prompts.json").read_text())
47
  RESULT_DIR = Path(tempfile.mkdtemp(prefix="inko-results-"))
48
  gr.set_static_paths(paths=[SHOWCASE_DIR, RESULT_DIR])
49
  PROGRESS = gr.Progress()
 
 
 
 
50
 
51
 
52
  def image_url(path):
@@ -69,7 +73,9 @@ def carousel_html():
69
  f'<div class="showcase-track"><div class="showcase-group">{cards}</div>'
70
  f'<div class="showcase-group" aria-hidden="true">{duplicate}</div></div></div>'
71
  '<div class="showcase-caption"><span>Choose a painting to load its prompt and settings.</span>'
72
- '<button type="button" class="motion-toggle" aria-label="Pause carousel">Pause</button></div>'
 
 
73
  )
74
 
75
 
@@ -118,7 +124,12 @@ def canvas_ui(aspect, resolution, width, height, enhance):
118
 
119
 
120
  def interaction_updates(is_busy):
121
- return {control: gr.update(interactive=not is_busy) for control in input_controls}
 
 
 
 
 
122
 
123
 
124
  def prepare_request(
@@ -137,8 +148,13 @@ def prepare_request(
137
  yield {
138
  busy: True,
139
  status: "Enhancing prompt…" if enhance else "Preparing your painting…",
 
140
  reuse_button: gr.update(interactive=False),
 
141
  **interaction_updates(True),
 
 
 
142
  }
143
  try:
144
  sampling = sampling_settings(seed, randomize_seed, steps, guidance)
@@ -147,7 +163,11 @@ def prepare_request(
147
  )
148
  except ValueError as error:
149
  raise gr.Error(str(error)) from error
150
- yield {pending: prepared | sampling, status: "Generating image…"}
 
 
 
 
151
 
152
 
153
  def gpu_duration(request, progress=PROGRESS):
@@ -202,6 +222,9 @@ def display_updates(record, records):
202
  ),
203
  history_gallery: history_html(records, record["image"]),
204
  reuse_button: gr.update(interactive=True),
 
 
 
205
  }
206
 
207
 
@@ -213,6 +236,7 @@ def commit_result(path, records):
213
  history: updated,
214
  busy: False,
215
  status: "",
 
216
  **interaction_updates(False),
217
  seed: record["seed"],
218
  width: record["width"],
@@ -265,6 +289,38 @@ def select_history(is_busy, records, event: gr.SelectData):
265
  return restore_record(records[index], records)
266
 
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  def reuse_prompt(record, is_busy):
269
  if is_busy or record is None:
270
  return {prompt: gr.skip()}
@@ -285,7 +341,9 @@ def failed_request(record):
285
  return {
286
  busy: False,
287
  status: "The request failed. You can try again with the same settings.",
 
288
  reuse_button: gr.update(interactive=record is not None),
 
289
  **interaction_updates(False),
290
  }
291
 
@@ -353,6 +411,9 @@ with gr.Blocks(title="Inko", delete_cache=(3600, 3600)) as demo:
353
  "Generate", variant="primary", elem_id="generate-button"
354
  )
355
  status = gr.Markdown("", elem_id="generation-status")
 
 
 
356
  with gr.Accordion("Generation settings", open=False):
357
  randomize_seed = gr.Checkbox(value=True, label="Randomize seed")
358
  seed = gr.Number(
@@ -390,6 +451,9 @@ with gr.Blocks(title="Inko", delete_cache=(3600, 3600)) as demo:
390
  elem_id="result-history",
391
  apply_default_css=False,
392
  )
 
 
 
393
  used_prompt = gr.Textbox(
394
  label="Prompt used",
395
  lines=4,
@@ -432,6 +496,7 @@ with gr.Blocks(title="Inko", delete_cache=(3600, 3600)) as demo:
432
  result_notice,
433
  history_gallery,
434
  reuse_button,
 
435
  ]
436
  restore_outputs = [
437
  *result_outputs,
@@ -440,7 +505,14 @@ with gr.Blocks(title="Inko", delete_cache=(3600, 3600)) as demo:
440
  canvas_label,
441
  status,
442
  ]
443
- busy_outputs = [busy, status, reuse_button, *input_controls]
 
 
 
 
 
 
 
444
  gr.on(
445
  triggers=[
446
  aspect.input,
@@ -469,6 +541,13 @@ with gr.Blocks(title="Inko", delete_cache=(3600, 3600)) as demo:
469
  queue=False,
470
  api_visibility="private",
471
  )
 
 
 
 
 
 
 
472
  reuse_button.click(
473
  reuse_prompt,
474
  inputs=[selected, busy],
@@ -492,15 +571,16 @@ with gr.Blocks(title="Inko", delete_cache=(3600, 3600)) as demo:
492
  outputs=[pending, *busy_outputs],
493
  api_name="prepare_prompt",
494
  trigger_mode="once",
495
- show_progress="hidden",
 
496
  )
497
  generation = preparation.success(
498
  generate,
499
  inputs=pending,
500
  outputs=completed,
501
  api_name="generate",
502
- show_progress="minimal",
503
- show_progress_on=status,
504
  )
505
  committed = generation.success(
506
  commit_result,
 
47
  RESULT_DIR = Path(tempfile.mkdtemp(prefix="inko-results-"))
48
  gr.set_static_paths(paths=[SHOWCASE_DIR, RESULT_DIR])
49
  PROGRESS = gr.Progress()
50
+ ACTIVITY_INDICATOR = (
51
+ '<div class="activity-indicator" role="progressbar" '
52
+ 'aria-label="Request in progress"><span></span></div>'
53
+ )
54
 
55
 
56
  def image_url(path):
 
73
  f'<div class="showcase-track"><div class="showcase-group">{cards}</div>'
74
  f'<div class="showcase-group" aria-hidden="true">{duplicate}</div></div></div>'
75
  '<div class="showcase-caption"><span>Choose a painting to load its prompt and settings.</span>'
76
+ '<button type="button" class="motion-toggle" aria-label="Pause carousel">'
77
+ '<span class="motion-icon" aria-hidden="true">Ⅱ</span>'
78
+ '<span class="motion-label">Pause</span></button></div>'
79
  )
80
 
81
 
 
124
 
125
 
126
  def interaction_updates(is_busy):
127
+ updates = {
128
+ control: gr.update(interactive=not is_busy) for control in input_controls
129
+ }
130
+ if not is_busy:
131
+ updates[generate_button] = gr.update(value="Generate", interactive=True)
132
+ return updates
133
 
134
 
135
  def prepare_request(
 
148
  yield {
149
  busy: True,
150
  status: "Enhancing prompt…" if enhance else "Preparing your painting…",
151
+ progress_display: ACTIVITY_INDICATOR,
152
  reuse_button: gr.update(interactive=False),
153
+ delete_button: gr.update(interactive=False),
154
  **interaction_updates(True),
155
+ generate_button: gr.update(
156
+ value="Enhancing…" if enhance else "Preparing…", interactive=False
157
+ ),
158
  }
159
  try:
160
  sampling = sampling_settings(seed, randomize_seed, steps, guidance)
 
163
  )
164
  except ValueError as error:
165
  raise gr.Error(str(error)) from error
166
+ yield {
167
+ pending: prepared | sampling,
168
+ status: "Generating image…",
169
+ generate_button: gr.update(value="Generating…", interactive=False),
170
+ }
171
 
172
 
173
  def gpu_duration(request, progress=PROGRESS):
 
222
  ),
223
  history_gallery: history_html(records, record["image"]),
224
  reuse_button: gr.update(interactive=True),
225
+ delete_button: gr.update(
226
+ visible=record["kind"] == "Generated", interactive=True
227
+ ),
228
  }
229
 
230
 
 
236
  history: updated,
237
  busy: False,
238
  status: "",
239
+ progress_display: "",
240
  **interaction_updates(False),
241
  seed: record["seed"],
242
  width: record["width"],
 
289
  return restore_record(records[index], records)
290
 
291
 
292
+ def delete_selected(record, is_busy, records):
293
+ if is_busy:
294
+ raise gr.Error("Wait for the current generation before deleting an image.")
295
+ index = next(
296
+ (
297
+ index
298
+ for index, item in enumerate(records)
299
+ if record is not None and item["image"] == record["image"]
300
+ ),
301
+ None,
302
+ )
303
+ if index is None:
304
+ raise gr.Error("Select an image from your generation history to delete.")
305
+ updated = records[:index] + records[index + 1 :]
306
+ if updated:
307
+ changes = restore_record(updated[min(index, len(updated) - 1)], updated)
308
+ else:
309
+ changes = {
310
+ selected: None,
311
+ output: None,
312
+ used_prompt: "",
313
+ recipe: gr.update(value="", visible=False),
314
+ result_notice: gr.update(value="", visible=False),
315
+ history_gallery: history_html([]),
316
+ reuse_button: gr.update(interactive=False),
317
+ delete_button: gr.update(visible=False, interactive=False),
318
+ status: "",
319
+ }
320
+ delete_results([records[index]], RESULT_DIR)
321
+ return {**changes, history: updated}
322
+
323
+
324
  def reuse_prompt(record, is_busy):
325
  if is_busy or record is None:
326
  return {prompt: gr.skip()}
 
341
  return {
342
  busy: False,
343
  status: "The request failed. You can try again with the same settings.",
344
+ progress_display: "",
345
  reuse_button: gr.update(interactive=record is not None),
346
+ delete_button: gr.update(interactive=record is not None),
347
  **interaction_updates(False),
348
  }
349
 
 
411
  "Generate", variant="primary", elem_id="generate-button"
412
  )
413
  status = gr.Markdown("", elem_id="generation-status")
414
+ progress_display = gr.HTML(
415
+ "", elem_id="generation-progress", apply_default_css=False
416
+ )
417
  with gr.Accordion("Generation settings", open=False):
418
  randomize_seed = gr.Checkbox(value=True, label="Randomize seed")
419
  seed = gr.Number(
 
451
  elem_id="result-history",
452
  apply_default_css=False,
453
  )
454
+ delete_button = gr.Button(
455
+ "Delete image", size="sm", visible=False, elem_id="delete-image"
456
+ )
457
  used_prompt = gr.Textbox(
458
  label="Prompt used",
459
  lines=4,
 
496
  result_notice,
497
  history_gallery,
498
  reuse_button,
499
+ delete_button,
500
  ]
501
  restore_outputs = [
502
  *result_outputs,
 
505
  canvas_label,
506
  status,
507
  ]
508
+ busy_outputs = [
509
+ busy,
510
+ status,
511
+ progress_display,
512
+ reuse_button,
513
+ delete_button,
514
+ *input_controls,
515
+ ]
516
  gr.on(
517
  triggers=[
518
  aspect.input,
 
541
  queue=False,
542
  api_visibility="private",
543
  )
544
+ delete_button.click(
545
+ delete_selected,
546
+ inputs=[selected, busy, history],
547
+ outputs=[*restore_outputs, history],
548
+ queue=False,
549
+ api_visibility="private",
550
+ )
551
  reuse_button.click(
552
  reuse_prompt,
553
  inputs=[selected, busy],
 
571
  outputs=[pending, *busy_outputs],
572
  api_name="prepare_prompt",
573
  trigger_mode="once",
574
+ show_progress="full",
575
+ show_progress_on=progress_display,
576
  )
577
  generation = preparation.success(
578
  generate,
579
  inputs=pending,
580
  outputs=completed,
581
  api_name="generate",
582
+ show_progress="full",
583
+ show_progress_on=progress_display,
584
  )
585
  committed = generation.success(
586
  commit_result,
demo/gallery.js CHANGED
@@ -13,24 +13,27 @@ if (strip) {
13
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
14
  let paused = reducedMotion.matches;
15
  let hovered = false;
16
- let focused = false;
17
  let touching = false;
18
  let previousTime = null;
19
  let position = strip.scrollLeft;
20
 
21
  const updateToggle = () => {
22
- toggle.textContent = paused ? "Play" : "Pause";
 
23
  toggle.setAttribute("aria-label", paused ? "Play carousel" : "Pause carousel");
24
  };
 
25
  toggle.addEventListener("click", () => { paused = !paused; updateToggle(); });
26
  reducedMotion.addEventListener("change", () => { paused = reducedMotion.matches; updateToggle(); });
27
- strip.addEventListener("pointerenter", () => { hovered = true; });
28
  strip.addEventListener("pointerleave", () => { hovered = false; touching = false; });
29
  strip.addEventListener("pointerdown", () => { touching = true; });
30
  strip.addEventListener("pointerup", () => { touching = false; });
31
  strip.addEventListener("pointercancel", () => { touching = false; });
32
- strip.addEventListener("focusin", () => { focused = true; });
33
- strip.addEventListener("focusout", () => { focused = false; });
 
 
34
  strip.addEventListener("scroll", () => { position = strip.scrollLeft; }, { passive: true });
35
  strip.addEventListener("keydown", (event) => {
36
  if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
@@ -45,7 +48,7 @@ if (strip) {
45
  const elapsed = previousTime === null ? 0 : Math.min(time - previousTime, 64);
46
  previousTime = time;
47
  const distance = group.getBoundingClientRect().width + 16;
48
- if (!paused && !hovered && !focused && !touching && !document.hidden && distance > 16) {
49
  position += elapsed * 0.022;
50
  if (position >= distance) position -= distance;
51
  strip.scrollLeft = position;
 
13
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
14
  let paused = reducedMotion.matches;
15
  let hovered = false;
 
16
  let touching = false;
17
  let previousTime = null;
18
  let position = strip.scrollLeft;
19
 
20
  const updateToggle = () => {
21
+ toggle.querySelector(".motion-icon").textContent = paused ? "▶" : "Ⅱ";
22
+ toggle.querySelector(".motion-label").textContent = paused ? "Play" : "Pause";
23
  toggle.setAttribute("aria-label", paused ? "Play carousel" : "Pause carousel");
24
  };
25
+ const pauseForSelection = () => { paused = true; updateToggle(); };
26
  toggle.addEventListener("click", () => { paused = !paused; updateToggle(); });
27
  reducedMotion.addEventListener("change", () => { paused = reducedMotion.matches; updateToggle(); });
28
+ strip.addEventListener("pointerenter", (event) => { hovered = event.pointerType === "mouse"; });
29
  strip.addEventListener("pointerleave", () => { hovered = false; touching = false; });
30
  strip.addEventListener("pointerdown", () => { touching = true; });
31
  strip.addEventListener("pointerup", () => { touching = false; });
32
  strip.addEventListener("pointercancel", () => { touching = false; });
33
+ strip.addEventListener("focusin", pauseForSelection);
34
+ strip.addEventListener("click", (event) => {
35
+ if (event.target.closest(".painting-card")) pauseForSelection();
36
+ });
37
  strip.addEventListener("scroll", () => { position = strip.scrollLeft; }, { passive: true });
38
  strip.addEventListener("keydown", (event) => {
39
  if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
 
48
  const elapsed = previousTime === null ? 0 : Math.min(time - previousTime, 64);
49
  previousTime = time;
50
  const distance = group.getBoundingClientRect().width + 16;
51
+ if (!paused && !hovered && !touching && !document.hidden && distance > 16) {
52
  position += elapsed * 0.022;
53
  if (position >= distance) position -= distance;
54
  strip.scrollLeft = position;
demo/style.css CHANGED
@@ -24,15 +24,30 @@ body { background: #f4f0e8; }
24
  .painting-card:hover, .painting-card:focus-visible { transform: translateY(-3px); box-shadow: 0 5px 14px #4c39212b; outline: 2px solid #a94f3c; outline-offset: 2px; }
25
  .painting-card:hover span, .painting-card:focus-visible span { opacity: 1; }
26
  .showcase-caption { display: flex; justify-content: space-between; align-items: center; gap: 16px; color: #85786b; font-size: 12px; padding: 3px 0 16px; }
27
- .motion-toggle { border: 0; background: transparent; color: #766052; cursor: pointer; padding: 4px 8px; font-size: 12px; }
 
 
28
  .motion-toggle:focus-visible { outline: 2px solid #a94f3c; outline-offset: 2px; }
29
  #workspace { gap: 30px; align-items: flex-start; }
30
  #prompt-panel, #result-panel { gap: 13px; }
31
  #prompt-panel { padding: 20px; background: #fbf8f1; border: 1px solid #e2d9cb; border-radius: 12px; }
32
  #prompt-panel .block { box-shadow: none; }
33
  #generate-button { min-height: 48px; border-radius: 8px; box-shadow: none; font-weight: 600; letter-spacing: .02em; }
34
- .canvas-note, #generation-status { font-size: 12px; color: #85786b; min-height: 0 !important; }
 
35
  .canvas-note p, #generation-status p { margin: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
36
  #result-viewer { height: min(58vh, 640px) !important; min-height: 0 !important; background: #ebe5da !important; border: 1px solid #ded6c9; border-radius: 12px; overflow: hidden; }
37
  #result-viewer .image-container { padding: 24px; box-sizing: border-box; height: 100% !important; min-height: 0 !important; }
38
  #result-viewer img { object-fit: contain !important; max-height: calc(min(58vh, 640px) - 48px) !important; max-width: 100% !important; }
@@ -48,6 +63,7 @@ body { background: #f4f0e8; }
48
  .history-card[aria-pressed="true"], .history-card:focus-visible { border-color: #a94f3c; outline: none; }
49
  .history-card:hover { background: #e3d8c9; }
50
  .history-empty { margin: 2px 0; color: #968879; font-size: 12px; }
 
51
  @media (max-width: 760px) {
52
  .gradio-container { padding: 20px 16px 28px !important; }
53
  .gradio-container > .main { padding: 0 !important; }
@@ -64,4 +80,5 @@ body { background: #f4f0e8; }
64
  }
65
  @media (prefers-reduced-motion: reduce) {
66
  .painting-card, .painting-card span { transition: none; }
 
67
  }
 
24
  .painting-card:hover, .painting-card:focus-visible { transform: translateY(-3px); box-shadow: 0 5px 14px #4c39212b; outline: 2px solid #a94f3c; outline-offset: 2px; }
25
  .painting-card:hover span, .painting-card:focus-visible span { opacity: 1; }
26
  .showcase-caption { display: flex; justify-content: space-between; align-items: center; gap: 16px; color: #85786b; font-size: 12px; padding: 3px 0 16px; }
27
+ .motion-toggle { display: inline-flex; flex: none; align-items: center; justify-content: center; gap: 7px; min-width: 88px; min-height: 38px; border: 1px solid #ba7e68; border-radius: 8px; background: #eee0d3; color: #793d2d; cursor: pointer; padding: 8px 12px; font-size: 13px; font-weight: 600; }
28
+ .motion-toggle:hover { background: #e3cfbd; border-color: #a94f3c; }
29
+ .motion-icon { width: 14px; font-size: 12px; text-align: center; }
30
  .motion-toggle:focus-visible { outline: 2px solid #a94f3c; outline-offset: 2px; }
31
  #workspace { gap: 30px; align-items: flex-start; }
32
  #prompt-panel, #result-panel { gap: 13px; }
33
  #prompt-panel { padding: 20px; background: #fbf8f1; border: 1px solid #e2d9cb; border-radius: 12px; }
34
  #prompt-panel .block { box-shadow: none; }
35
  #generate-button { min-height: 48px; border-radius: 8px; box-shadow: none; font-weight: 600; letter-spacing: .02em; }
36
+ .canvas-note { font-size: 12px; color: #85786b; min-height: 0 !important; }
37
+ #generation-status { font-size: 15px; font-weight: 600; color: #91412f; min-height: 0 !important; }
38
  .canvas-note p, #generation-status p { margin: 0; }
39
+ #generation-progress { padding: 0 !important; min-height: 0 !important; }
40
+ #generation-progress .html-container { padding: 0; }
41
+ #generation-progress:has(.activity-indicator), #generation-progress:has([data-testid="status-tracker"]:not(.hide)) { min-height: 104px !important; background: #f3e9dc; border: 1px solid #d5ae95; border-radius: 9px; }
42
+ #generation-progress [data-testid="status-tracker"] { height: 100%; box-sizing: border-box; padding: 16px !important; background: #f3e9dc; }
43
+ #generation-progress [data-testid="status-tracker"].generating { display: none; }
44
+ #generation-progress .progress-bar-wrap { width: 100%; height: 12px; border: 0; border-radius: 6px; background: #decab6; overflow: hidden; }
45
+ #generation-progress .progress-bar { background: #a94f3c; }
46
+ #generation-progress .progress-level-inner { font-family: inherit; font-size: 14px; font-weight: 600; color: #793d2d; }
47
+ #generation-progress .progress-text { top: 8px; right: 12px; transform: none; color: #766052; font-size: 12px; }
48
+ .activity-indicator { position: relative; height: 12px; margin: 45px 16px; overflow: hidden; border-radius: 6px; background: #decab6; }
49
+ .activity-indicator span { display: block; width: 35%; height: 100%; border-radius: inherit; background: #a94f3c; animation: inko-progress 1.4s ease-in-out infinite alternate; }
50
+ @keyframes inko-progress { from { transform: translateX(0); } to { transform: translateX(185%); } }
51
  #result-viewer { height: min(58vh, 640px) !important; min-height: 0 !important; background: #ebe5da !important; border: 1px solid #ded6c9; border-radius: 12px; overflow: hidden; }
52
  #result-viewer .image-container { padding: 24px; box-sizing: border-box; height: 100% !important; min-height: 0 !important; }
53
  #result-viewer img { object-fit: contain !important; max-height: calc(min(58vh, 640px) - 48px) !important; max-width: 100% !important; }
 
63
  .history-card[aria-pressed="true"], .history-card:focus-visible { border-color: #a94f3c; outline: none; }
64
  .history-card:hover { background: #e3d8c9; }
65
  .history-empty { margin: 2px 0; color: #968879; font-size: 12px; }
66
+ #delete-image { align-self: flex-end; width: auto; min-width: 116px; padding: 7px 12px; color: #91412f; border: 1px solid #d5ae95; }
67
  @media (max-width: 760px) {
68
  .gradio-container { padding: 20px 16px 28px !important; }
69
  .gradio-container > .main { padding: 0 !important; }
 
80
  }
81
  @media (prefers-reduced-motion: reduce) {
82
  .painting-card, .painting-card span { transition: none; }
83
+ .activity-indicator span { animation: none; width: 100%; opacity: .65; }
84
  }