Audio Wave: segments = user splits only (721 groups are markers, not segments)

Per clarification: the 721 group lines are clip markers, NOT segments. Segments come
ONLY from user split points, numbered globally from 1 — so the first split is seg1, not
seg23. 0 splits = 0 segments; N splits = N segments (seg k = point k-1 .. split k, tail
after the last split excluded). group lines still drawn (bold) + used for the group#.
JS matches (segPoints/nSegs). Empty summary now prompts to add splits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 23:49:46 +02:00
co-authored by Claude Opus 4.8
parent 7f256977b5
commit ea099ccac4
3 changed files with 23 additions and 17 deletions
+3
View File
@@ -180,6 +180,9 @@ def _render(rms_n, times, duration, beats, bounds, segs,
def _summary(duration, sr, bpm, beats, segs, global_notes): 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:", lines = ["AUDIO GUIDE — align the video beats to this audio:",
f"- duration: {duration:.2f}s | sample_rate: {sr} Hz"] f"- duration: {duration:.2f}s | sample_rate: {sr} Hz"]
if bpm: if bpm:
+8 -8
View File
@@ -108,19 +108,19 @@ def _parse_range(sel, n):
def _segments_grouped(rms_n, times, duration, fps, group_frames, user_starts, beats): 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 """Segments come ONLY from the user's split points (the 721 group lines are just clip
frames). The group lines are hard splits, so a segment never crosses one — the last markers, NOT segments). Points = [0] + splits; a segment runs between consecutive
segment in a group ends exactly on the group boundary (e.g. frame 721).""" 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) gstep = max(group_frames, 1) / max(fps, 1)
gbounds, k = [], 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 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}) pts = [0.0] + sorted({round(s, 3) for s in user_starts if 0 < s < duration - 1e-6})
starts = [0.0] + allb
stages = ["establish", "build", "peak", "settle"] stages = ["establish", "build", "peak", "settle"]
segs, bounds = [], [0.0] segs, bounds = [], [0.0]
for i, t0 in enumerate(starts): for i in range(len(pts) - 1): # exclude the tail (last point -> end)
t1 = starts[i + 1] if i + 1 < len(starts) else duration t0, t1 = pts[i], pts[i + 1]
bounds.append(round(t1, 3)) bounds.append(round(t1, 3))
mask = (times >= t0) & (times < t1) mask = (times >= t0) & (times < t1)
e = float(rms_n[mask].mean()) if mask.any() else 0.0 e = float(rms_n[mask].mean()) if mask.any() else 0.0
+10 -7
View File
@@ -91,12 +91,12 @@ function setupWave(node) {
for (let t = step; t < st.duration - 1e-6; t += step) arr.push(t); for (let t = step; t < st.duration - 1e-6; t += step) arr.push(t);
return arr; 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]); 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 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); 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) function serialize() { // write ONLY user splits (small + stable)
const w = getW(node, "segments_json"); if (!w) return; 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 function syncNotesBox() { // one segN: line per segment, preserve notes
const nw = getW(node, "notes"); if (!nw) return; const nw = getW(node, "notes"); if (!nw) return;
const { seg, glob } = parseNotes(nw.value); 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] : ""}`); for (let i = 1; i <= n; i++) lines.push(`seg${i}: ${seg[i] !== undefined ? seg[i] : ""}`);
const val = lines.concat(glob).join("\n"); const val = lines.concat(glob).join("\n");
if (nw.value !== val) { nw.value = val; nw.callback?.(val); } if (nw.value !== val) { nw.value = val; nw.callback?.(val); }
@@ -127,10 +127,10 @@ function setupWave(node) {
function draw() { function draw() {
ctx.fillStyle = "#141418"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "#141418"; ctx.fillRect(0, 0, canvas.width, canvas.height);
const H = canvas.height, mid = H / 2; 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(); const sel = selRange();
if (sel) { // shade selected range if (sel) { // shade selected segment range
const a = starts[sel[0] - 1], b = sel[1] < starts.length ? starts[sel[1]] : st.duration; 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 (a !== undefined) { ctx.fillStyle = "#26364f"; ctx.fillRect(t2x(a), 0, t2x(b) - t2x(a), H); }
} }
if (st.peaks) { // mirrored waveform around centre if (st.peaks) { // mirrored waveform around centre
@@ -144,11 +144,14 @@ function setupWave(node) {
ctx.lineWidth = 1; ctx.lineWidth = 1;
const { seg } = parseNotes(getW(node, "notes")?.value); const { seg } = parseNotes(getW(node, "notes")?.value);
ctx.font = "10px monospace"; 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); const x = t2x(s);
ctx.strokeStyle = "#7ec8a0"; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); ctx.strokeStyle = "#7ec8a0"; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
if (i < nseg) {
ctx.fillStyle = "#fff"; ctx.fillText(`S${i + 1} ${s.toFixed(1)}s`, x + 3, 11); 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 (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.strokeStyle = "#ff5a3c"; const px = t2x(st.audio.currentTime || 0); // playhead
ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke(); ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke();