diff --git a/nodes/audio_guide.py b/nodes/audio_guide.py index eefa608..d4254bc 100644 --- a/nodes/audio_guide.py +++ b/nodes/audio_guide.py @@ -180,6 +180,9 @@ def _render(rms_n, times, duration, beats, bounds, segs, def _summary(duration, sr, bpm, beats, segs, global_notes): + if not segs: + return (f"AUDIO GUIDE — {duration:.2f}s clip. No segments defined yet: double-click " + f"the waveform to add split points (each split starts a new beat).") lines = ["AUDIO GUIDE — align the video beats to this audio:", f"- duration: {duration:.2f}s | sample_rate: {sr} Hz"] if bpm: diff --git a/nodes/audio_wave_segments.py b/nodes/audio_wave_segments.py index 629cc5e..d970ed1 100644 --- a/nodes/audio_wave_segments.py +++ b/nodes/audio_wave_segments.py @@ -108,19 +108,19 @@ def _parse_range(sel, n): def _segments_grouped(rms_n, times, duration, fps, group_frames, user_starts, beats): - """Segments = user split points UNION the fixed group boundaries (every group_frames - frames). The group lines are hard splits, so a segment never crosses one — the last - segment in a group ends exactly on the group boundary (e.g. frame 721).""" + """Segments come ONLY from the user's split points (the 721 group lines are just clip + markers, NOT segments). Points = [0] + splits; a segment runs between consecutive + points, so N splits -> N segments (the tail after the last split is not a segment). + Global numbering. `group` = which 721 group each segment starts in (for reference).""" gstep = max(group_frames, 1) / max(fps, 1) gbounds, k = [], 1 - while k * gstep < duration - 1e-6: + while k * gstep < duration - 1e-6: # for drawing + the group number only gbounds.append(round(k * gstep, 3)); k += 1 - allb = sorted({round(b, 3) for b in list(user_starts) + gbounds if 0 < b < duration - 1e-6}) - starts = [0.0] + allb + pts = [0.0] + sorted({round(s, 3) for s in user_starts if 0 < s < duration - 1e-6}) stages = ["establish", "build", "peak", "settle"] segs, bounds = [], [0.0] - for i, t0 in enumerate(starts): - t1 = starts[i + 1] if i + 1 < len(starts) else duration + for i in range(len(pts) - 1): # exclude the tail (last point -> end) + t0, t1 = pts[i], pts[i + 1] bounds.append(round(t1, 3)) mask = (times >= t0) & (times < t1) e = float(rms_n[mask].mean()) if mask.any() else 0.0 diff --git a/web/audio_wave.js b/web/audio_wave.js index 941317e..1c97123 100644 --- a/web/audio_wave.js +++ b/web/audio_wave.js @@ -91,12 +91,12 @@ function setupWave(node) { for (let t = step; t < st.duration - 1e-6; t += step) arr.push(t); return arr; } - function segStarts() { // union of 0 + user splits + group lines + function segPoints() { // [0] + user splits (NOT the group lines) const set = new Set([0]); for (const s of st.splits) if (s > 0 && s < st.duration) set.add(Math.round(s * 100) / 100); - for (const g of groupBounds()) set.add(Math.round(g * 100) / 100); return Array.from(set).sort((a, b) => a - b); } + const nSegs = () => Math.max(0, segPoints().length - 1); // N splits -> N segments (tail excluded) function serialize() { // write ONLY user splits (small + stable) const w = getW(node, "segments_json"); if (!w) return; @@ -111,7 +111,7 @@ function setupWave(node) { function syncNotesBox() { // one segN: line per segment, preserve notes const nw = getW(node, "notes"); if (!nw) return; const { seg, glob } = parseNotes(nw.value); - const n = segStarts().length; const lines = []; + const n = nSegs(); const lines = []; for (let i = 1; i <= n; i++) lines.push(`seg${i}: ${seg[i] !== undefined ? seg[i] : ""}`); const val = lines.concat(glob).join("\n"); if (nw.value !== val) { nw.value = val; nw.callback?.(val); } @@ -127,10 +127,10 @@ function setupWave(node) { function draw() { ctx.fillStyle = "#141418"; ctx.fillRect(0, 0, canvas.width, canvas.height); const H = canvas.height, mid = H / 2; - const starts = segStarts(); + const pts = segPoints(); // [0, split1, split2, ...]; seg k = pts[k-1]..pts[k] const sel = selRange(); - if (sel) { // shade selected range - const a = starts[sel[0] - 1], b = sel[1] < starts.length ? starts[sel[1]] : st.duration; + if (sel) { // shade selected segment range + const a = pts[sel[0] - 1], b = sel[1] < pts.length ? pts[sel[1]] : st.duration; if (a !== undefined) { ctx.fillStyle = "#26364f"; ctx.fillRect(t2x(a), 0, t2x(b) - t2x(a), H); } } if (st.peaks) { // mirrored waveform around centre @@ -144,11 +144,14 @@ function setupWave(node) { ctx.lineWidth = 1; const { seg } = parseNotes(getW(node, "notes")?.value); ctx.font = "10px monospace"; - starts.forEach((s, i) => { // segment line + label top + note bottom + const nseg = nSegs(); + pts.forEach((s, i) => { // split line; label seg i+1 (skip tail) const x = t2x(s); ctx.strokeStyle = "#7ec8a0"; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); - ctx.fillStyle = "#fff"; ctx.fillText(`S${i + 1} ${s.toFixed(1)}s`, x + 3, 11); - if (seg[i + 1]) { ctx.fillStyle = "#ffd27a"; ctx.fillText(seg[i + 1].slice(0, 22), x + 3, H - 4); } + if (i < nseg) { + ctx.fillStyle = "#fff"; ctx.fillText(`S${i + 1} ${s.toFixed(1)}s`, x + 3, 11); + if (seg[i + 1]) { ctx.fillStyle = "#ffd27a"; ctx.fillText(seg[i + 1].slice(0, 22), x + 3, H - 4); } + } }); ctx.strokeStyle = "#ff5a3c"; const px = t2x(st.audio.currentTime || 0); // playhead ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke();