fix: loudness navigation shows rank position, not time-order index
When stepping the clip queue by loudness (U/I, or "highlights only"
auto-advance), the position count showed the section's time-order index
(e.g. "30 / 426") instead of its place in the loudest-first ranking, so
walking highlights produced a count that jumped around. playClip now
takes a byScore flag: the displayed position is the rank in scoreOrder()
when navigating highlights ("1 / 426" = loudest), the time-order index
otherwise. The in-player U/I path already announced the rank.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ Dependencies: `requests` (streams), `numpy` + `soundfile` (FLAC output and FLAC
|
|||||||
- Serving: `_stream()` (Range support), `_copy_to_response()`, `_safe_path()` (path traversal guard).
|
- Serving: `_stream()` (Range support), `_copy_to_response()`, `_safe_path()` (path traversal guard).
|
||||||
|
|
||||||
`webui.html` (one `<script>` block):
|
`webui.html` (one `<script>` block):
|
||||||
- Clip review: `clipQueue`/`clipCursor` globals, `playClip()`, `playFileSection()`, `hideClipBar()`; markup is the `#clip-bar` div. The clip label shows the wall-clock occurrence time + queue position (`03:46:20 to 03:46:22 (73 / 187)`): queue entries carry `absStart` (epoch s), derived from `fileStartEpoch(f.date)` — the filename clock — with in-file offsets as fallback for non-standard names; filename/score live in the label tooltip.
|
- Clip review: `clipQueue`/`clipCursor` globals, `playClip()`, `playFileSection()`, `hideClipBar()`; markup is the `#clip-bar` div. The clip label shows wall-clock occurrence time + dB prominence + position (`17:09:56 to 17:09:57 · +30 dB (30 / 426)`): queue entries carry `absStart` (epoch s), derived from `fileStartEpoch(f.date)` — the filename clock — with in-file offsets as fallback for non-standard names; only the filename/in-file offset lives in the tooltip now. **`playClip()` builds one `text` string used for both `label.textContent` and the `announce()` aria-live message** — they must never diverge (a past bug had the announcement read `Clip N of M: …` while the label read the wall-clock form). `playClip(i, byScore)`: when `byScore` (a U/I or "highlights only" loudness walk), the position shown is the rank in `scoreOrder()` ("3rd loudest of 426"), not the time-order index `i`; `stepClip`'s by-score branch passes `true`, every other caller (chip click, J/K, Prev/Next) leaves it false for the time-order index.
|
||||||
- Day review: `dayHighlights()` builds `dayActiveSections` (chronological); `jumpToDaySection()` arms the queue. Section `absStart` comes from `fileStartEpoch(f.date)` (filename clock), mtime−duration only as fallback. **The user is blind and uses a screen reader — there is deliberately no day-timeline SVG** (one existed and was removed on request as useless); the highlights panel is linear text/buttons: summary line → key-hint note → chips toggle → chips. Do not add decorative visualizations; any future graphic must be aria-hidden and must not be the only carrier of information. Chip lists longer than 12 are collapsed behind an `aria-expanded` toggle button (the `.chips[hidden]{display:none}` rule is required — the author-level `display:flex` on `.chips` would otherwise override the UA `[hidden]` rule). Group `aria-label`s stay short ("Day loud sections") — the J/K/U/I key explanation lives only in the visible note, per user feedback against repeating info text in labels. The Highlights button is a collapse/expand toggle (`setHlExpanded()` keeps arrow + `aria-expanded` in sync, also from the day-collapse path): a built panel is kept and re-armed from `dayHlSections` instead of recomputing, keyed by `hlRow.dataset.loaded = hlParams()` (margin|gap|minDur string) so changed params force a re-run. The `#dayhls-<dayId>` "· analysed" suffix appears when every file's `cached_analysis` passes `cachedParamsMatch()`; `fetchAnalysis()` updates `f.cached_analysis` client-side so the marker survives re-renders without refetching `/api/files`. **`aria-label` on a button replaces its visible content for screen readers** — any status text rendered inside a labelled button (like the analysed suffix) must be mirrored into the label (render path + the end of `dayHighlights()`), or the blind user never hears it. Likewise, the accessible text of a button built as `aria-hidden arrow span + text node` must not start the text node with a space (the hidden arrow drops out and the leading space reads as an indent) — keep separator spaces inside the aria-hidden span.
|
- Day review: `dayHighlights()` builds `dayActiveSections` (chronological); `jumpToDaySection()` arms the queue. Section `absStart` comes from `fileStartEpoch(f.date)` (filename clock), mtime−duration only as fallback. **The user is blind and uses a screen reader — there is deliberately no day-timeline SVG** (one existed and was removed on request as useless); the highlights panel is linear text/buttons: summary line → key-hint note → chips toggle → chips. Do not add decorative visualizations; any future graphic must be aria-hidden and must not be the only carrier of information. Chip lists longer than 12 are collapsed behind an `aria-expanded` toggle button (the `.chips[hidden]{display:none}` rule is required — the author-level `display:flex` on `.chips` would otherwise override the UA `[hidden]` rule). Group `aria-label`s stay short ("Day loud sections") — the J/K/U/I key explanation lives only in the visible note, per user feedback against repeating info text in labels. The Highlights button is a collapse/expand toggle (`setHlExpanded()` keeps arrow + `aria-expanded` in sync, also from the day-collapse path): a built panel is kept and re-armed from `dayHlSections` instead of recomputing, keyed by `hlRow.dataset.loaded = hlParams()` (margin|gap|minDur string) so changed params force a re-run. The `#dayhls-<dayId>` "· analysed" suffix appears when every file's `cached_analysis` passes `cachedParamsMatch()`; `fetchAnalysis()` updates `f.cached_analysis` client-side so the marker survives re-renders without refetching `/api/files`. **`aria-label` on a button replaces its visible content for screen readers** — any status text rendered inside a labelled button (like the analysed suffix) must be mirrored into the label (render path + the end of `dayHighlights()`), or the blind user never hears it. Likewise, the accessible text of a button built as `aria-hidden arrow span + text node` must not start the text node with a space (the hidden arrow drops out and the leading space reads as an indent) — keep separator spaces inside the aria-hidden span.
|
||||||
- J/K/U/I/O: single document-level `keydown` listener — clip queue takes priority, in-player `currentTime` stepping is the fallback when no queue is armed; O calls `openClipInFile()` (shared with the "Open in file" button). J/K (and Prev/Next) always step in time order; U/I walk the loudest-first ranking from `scoreOrder()` — no top-N cutoff (the `#clip-top` input and `#clip-hl-only` checkbox were removed deliberately; J/K must never be affected by an auto-advance/highlights setting). Auto-advance is the `input[name="clip-adv"]` radio (off / next in time / next by loudness), read by `advanceMode()`; `stepClip(dir, byScore)` is the shared queue-stepping path. In-player U/I anchor the ranking on the section under the playhead, else start at the loudest.
|
- J/K/U/I/O: single document-level `keydown` listener — clip queue takes priority, in-player `currentTime` stepping is the fallback when no queue is armed; O calls `openClipInFile()` (shared with the "Open in file" button). J/K (and Prev/Next) always step in time order; U/I walk the loudest-first ranking from `scoreOrder()` — no top-N cutoff (the `#clip-top` input and `#clip-hl-only` checkbox were removed deliberately; J/K must never be affected by an auto-advance/highlights setting). Auto-advance is the `input[name="clip-adv"]` radio (off / next in time / next by loudness), read by `advanceMode()`; `stepClip(dir, byScore)` is the shared queue-stepping path. In-player U/I anchor the ranking on the section under the playhead, else start at the loudest.
|
||||||
- Analysis: `fetchAnalysis()` (session `analysisCache`), `analyse()` (per-row render: meta line with section count + params, then chips — no waveform SVG, see day-review note on the blind user), `cachedParamsMatch()` (autoload guard).
|
- Analysis: `fetchAnalysis()` (session `analysisCache`), `analyse()` (per-row render: meta line with section count + params, then chips — no waveform SVG, see day-review note on the blind user), `cachedParamsMatch()` (autoload guard).
|
||||||
|
|||||||
+7
-3
@@ -352,7 +352,10 @@ function seekToSection(idx, filename, startSec, endSec, sectionIdx) {
|
|||||||
let clipQueue = [];
|
let clipQueue = [];
|
||||||
let clipCursor = -1;
|
let clipCursor = -1;
|
||||||
|
|
||||||
function playClip(i) {
|
// byScore: this play is part of a loudness walk (U/I or "highlights only"
|
||||||
|
// auto-advance), so the position shown is the rank in the loudest-first
|
||||||
|
// ranking ("3rd loudest of 426"), not the time-order index.
|
||||||
|
function playClip(i, byScore) {
|
||||||
if (i < 0 || i >= clipQueue.length) return;
|
if (i < 0 || i >= clipQueue.length) return;
|
||||||
clipCursor = i;
|
clipCursor = i;
|
||||||
const c = clipQueue[i];
|
const c = clipQueue[i];
|
||||||
@@ -370,7 +373,8 @@ function playClip(i) {
|
|||||||
? `${fmtClock(c.absStart)} to ${fmtClock(c.absStart + (c.end - c.start))}`
|
? `${fmtClock(c.absStart)} to ${fmtClock(c.absStart + (c.end - c.start))}`
|
||||||
: `${fmtDur(c.start)} to ${fmtDur(c.end)}`;
|
: `${fmtDur(c.start)} to ${fmtDur(c.end)}`;
|
||||||
const score = c.score != null ? ` · +${Math.round(c.score)} dB` : '';
|
const score = c.score != null ? ` · +${Math.round(c.score)} dB` : '';
|
||||||
const text = `${when}${score} (${i + 1} / ${clipQueue.length})`;
|
const pos = byScore ? scoreOrder(clipQueue).indexOf(i) : i;
|
||||||
|
const text = `${when}${score} (${pos + 1} / ${clipQueue.length})`;
|
||||||
const label = document.getElementById('clip-label');
|
const label = document.getElementById('clip-label');
|
||||||
label.textContent = text;
|
label.textContent = text;
|
||||||
label.title = `${c.filename} @ ${fmtDur(c.start)}–${fmtDur(c.end)}`;
|
label.title = `${c.filename} @ ${fmtDur(c.start)}–${fmtDur(c.end)}`;
|
||||||
@@ -422,7 +426,7 @@ function stepClip(dir, byScore, jump) {
|
|||||||
const pos = order.indexOf(clipCursor); // -1: nothing played yet
|
const pos = order.indexOf(clipCursor); // -1: nothing played yet
|
||||||
const next = jump ? (dir > 0 ? order.length - 1 : 0)
|
const next = jump ? (dir > 0 ? order.length - 1 : 0)
|
||||||
: pos === -1 ? (dir > 0 ? 0 : -1) : pos + dir;
|
: pos === -1 ? (dir > 0 ? 0 : -1) : pos + dir;
|
||||||
if (next >= 0 && next < order.length) playClip(order[next]);
|
if (next >= 0 && next < order.length) playClip(order[next], true);
|
||||||
else announce(dir < 0 ? 'Loudest highlight reached' : 'End of highlights');
|
else announce(dir < 0 ? 'Loudest highlight reached' : 'End of highlights');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user