Audio Wave: correct model — 721 groups are hard splits, segments inside; range select; mirrored render; fix reload explosion

Rebuilt around the real model: group_frames (721) are HARD splits; segments = user
splits UNION the group lines, so a segment never crosses a group boundary (last segment
in a group ends exactly on frame 721). segment_select is now a RANGE string ('A-B' /
'N' / '' = all) that crops waveform_image + audio + summary to segments A..B. Render
rewritten: mirrored waveform (uses top+bottom), bold group grid, thin segment lines,
labels along the top and notes along the bottom, selected range shaded. JS: guards
frame values (0 -> default, fixing the 400+-segments-on-reload explosion), writes only
USER splits to segments_json, dblclick=add split / shift-click=remove / click=seek.
group number fixed (boundary rounding). Workflows + README updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 23:35:48 +02:00
co-authored by Claude Opus 4.8
parent 29c48a9115
commit 488afa0284
6 changed files with 205 additions and 181 deletions
+29 -22
View File
@@ -131,43 +131,50 @@ def _segments(rms_n, times, duration, fps, max_segments, beats):
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."""
fps=None, group_frames=0, frames_marker=0, window=None, sel=None):
"""Render a mirrored waveform + the 721-frame group grid (bold) + segment lines (thin),
labels along the top and per-segment notes along the bottom. window=(t0,t1) crops to a
range; sel=(a,b) shades segments a..b."""
W, H = 1024, 256
img = Image.new("RGB", (W, H), (18, 18, 22))
d = ImageDraw.Draw(img)
t0, t1 = window if window else (0.0, duration)
span = max(t1 - t0, 1e-6)
mid = H // 2
def X(t):
return int(max(0, min(W - 1, (t - t0) / span * (W - 1))))
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
if sel and not window: # shade the selected segment range
chosen = [s for s in segs if sel[0] <= s["segment"] <= sel[1]]
if chosen:
xa = X(chosen[0]["start_s"])
xb = X(chosen[-1]["start_s"] + chosen[-1]["duration_s"])
d.rectangle([xa, 0, xb, H], fill=(38, 54, 82))
amp = H * 0.44 # mirrored waveform around the centre line
for i in range(len(rms_n)):
if t0 <= times[i] <= t1:
x = X(times[i]); h = int(rms_n[i] * amp)
d.line([(x, mid - h), (x, mid + h)], fill=(60, 140, 220))
if fps and group_frames: # bold 721-frame group grid
gstep = group_frames / 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))
while k * gstep < duration + 1e-6:
gt = k * gstep
if t0 <= gt <= t1:
d.line([(X(gt), 0), (X(gt), H)], fill=(235, 235, 242), width=2)
k += 1
for b in beats: # beat markers (orange)
for b in beats: # beat ticks (faint, centre band)
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
d.line([(X(b), mid - 4), (X(b), mid + 4)], fill=(150, 90, 60), width=1)
for s in segs: # segment line + label top + note bottom
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 + 4, 18), s["note"][:30], fill=(255, 210, 110))
d.line([(x, 0), (x, H)], fill=(120, 190, 150), width=1)
d.text((x + 3, 3), f"S{s['segment']} {s['start_s']}s/{s['frames']}f", fill=(240, 240, 240))
if s.get("note"):
d.text((x + 3, H - 13), s["note"][:28], fill=(255, 210, 110))
arr = np.asarray(img, dtype=np.float32) / 255.0
return torch.from_numpy(arr)[None, ...] # [1, H, W, 3]