Audio Wave: fixed 721-frame subsegment grid + per-segment select/crop + notes box auto-fill

Pivot to the user's model: segments are a fixed grid of subsegment_frames frames
(default 721 @ 24fps = one LTX clip), not arbitrary clicks. New inputs:
subsegment_frames (grid size) and segment_select (0=all, N=output ONLY chunk N —
crops waveform_image, AUDIO, and summary so you can generate/skip one beat at a time).
_render gains window-crop + frame markers + per-segment time/frame labels. JS rewritten:
draws the fixed grid, auto-fills the notes box with one segN: line per chunk (type or
dblclick to note), click-to-seek, playhead + time readout. Workflows updated for the
new widgets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 23:12:44 +02:00
co-authored by Claude Opus 4.8
parent e63f6e6058
commit 75b20f9656
6 changed files with 198 additions and 107 deletions
+30 -13
View File
@@ -130,27 +130,44 @@ def _segments(rms_n, times, duration, fps, max_segments, beats):
return segs, bounds
def _render(rms_n, times, duration, beats, bounds, segs):
"""Render the envelope + beats + segment boundaries to a ComfyUI IMAGE tensor."""
def _render(rms_n, times, duration, beats, bounds, segs,
fps=None, frames_marker=0, window=None):
"""Render the envelope + beats + segment boundaries + time labels to a ComfyUI IMAGE.
window=(t0,t1) crops to that time span (for a selected segment). frames_marker draws
a green line every N frames (N/fps seconds) — LTX clip-length grid."""
W, H = 1024, 256
img = Image.new("RGB", (W, H), (18, 18, 22))
d = ImageDraw.Draw(img)
dur = max(duration, 1e-6)
t0, t1 = window if window else (0.0, duration)
span = max(t1 - t0, 1e-6)
def X(t):
return int(max(0, min(W - 1, t / dur * (W - 1))))
return int(max(0, min(W - 1, (t - t0) / span * (W - 1))))
pts = [(0, H)] + [(X(times[i]), H - int(rms_n[i] * (H - 26))) for i in range(len(rms_n))] + [(W - 1, H)]
d.polygon(pts, fill=(60, 140, 220))
pts = [(0, H)] + [(X(times[i]), H - int(rms_n[i] * (H - 30)))
for i in range(len(rms_n)) if t0 <= times[i] <= t1] + [(W - 1, H)]
if len(pts) > 2:
d.polygon(pts, fill=(60, 140, 220))
if fps and frames_marker: # frame grid (green)
step = frames_marker / fps
k = 1
while k * step < duration + 1e-6:
mt = k * step
if t0 <= mt <= t1:
d.line([(X(mt), 0), (X(mt), H)], fill=(70, 200, 120), width=1)
d.text((X(mt) + 2, H - 13), f"{frames_marker * k}f", fill=(70, 200, 120))
k += 1
for b in beats: # beat markers (orange)
d.line([(X(b), 0), (X(b), H)], fill=(230, 110, 60), width=1)
for bd in bounds: # segment boundaries (white)
d.line([(X(bd), 0), (X(bd), H)], fill=(240, 240, 240), width=1)
for s in segs:
x = X(s["start_s"]) + 4
d.text((x, 4), f"S{s['segment']} {s['energy']}", fill=(255, 255, 255))
if t0 <= b <= t1:
d.line([(X(b), 0), (X(b), H)], fill=(230, 110, 60), width=1)
for s in segs: # boundaries + label + time + note
if not (t0 <= s["start_s"] <= t1):
continue
x = X(s["start_s"])
d.line([(x, 0), (x, H)], fill=(240, 240, 240), width=1)
d.text((x + 4, 4), f"S{s['segment']} {s['energy']} {s['start_s']}s/{s['frames']}f", fill=(255, 255, 255))
if s.get("note"): # per-segment note (amber)
d.text((x, 18), s["note"][:30], fill=(255, 210, 110))
d.text((x + 4, 18), s["note"][:30], fill=(255, 210, 110))
arr = np.asarray(img, dtype=np.float32) / 255.0
return torch.from_numpy(arr)[None, ...] # [1, H, W, 3]