PageSourceSearch

https://video-to-notes.com/lib/real-length-planning.mjs

js video-to-notes.com collected 2026-10-02 01:47:58 UTC 4,575 bytes, 112 lines download raw bytes

1// Plan website transcription chunks from the audio the browser ACTUALLY extracted
2// (PRD 41 T04, F8/F9), and probe a file's length with ffmpeg when the browser
3// cannot read it.
4//
5// The backend rejects a chunk whose decoded length differs from its declared
6// duration by more than 0.15 s. The widgets used to plan from the selected range,
7// so a file whose real audio is shorter than the range (an ALAC .m4a Chrome cannot
8// read → 60-minute default range; a truncated MP3 whose header claims more than
9// decodes) declared chunks that did not exist and every one came back 422.
10//
11// Pure (Node-testable): the browser-only pieces (probeMediaFile, terminating the
12// ffmpeg instance) are injected by the widgets.
13
14import { planAudioChunks } from "./audio-chunking.mjs";
15
16// How long the file-pick probe may take before the widget gives up and keeps the
17// "we couldn't read this file's length" note (risk R10: huge unreadable files).
18export const DURATION_PROBE_BUDGET_MS = 20_000;
19
20function positiveOrNull(value) {
21  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
22}
23
24function roundSeconds(value) {
25  return Math.round(value * 1000) / 1000;
26}
27
28/**
29 * effective = min(selected, measured); a missing/unusable measurement falls back
30 * to the selection (today's behaviour).
31 */
32export function effectiveSelectionDuration(selectedDurationSeconds, measuredDurationSeconds) {
33  const selected = positiveOrNull(selectedDurationSeconds);
34  const measured = positiveOrNull(measuredDurationSeconds);
35  if (selected == null) return measured;
36  if (measured == null) return selected;
37  return Math.min(selected, roundSeconds(measured));
38}
39
40/**
41 * Chunk plan + the selection to declare to the backend for one extraction.
42 *
43 * @returns {{effectiveDurationSeconds:number, endSeconds:number, measured:boolean,
44 *            plan:Array<{index:number,startSeconds:number,endSeconds:number,durationSeconds:number}>}}
45 */
46export function planFromMeasuredAudio({ startSeconds, selectedDurationSeconds, measuredDurationSeconds }) {
47  const effective = effectiveSelectionDuration(selectedDurationSeconds, measuredDurationSeconds);
48  const measured = positiveOrNull(measuredDurationSeconds) != null;
49  if (!measured) {
50    console.warn("[real-length] extracted audio length unknown; planning from the selected range");
51  }
52  return {
53    effectiveDurationSeconds: effective,
54    endSeconds: roundSeconds((Number(startSeconds) || 0) + effective),
55    measured,
56    plan: planAudioChunks(effective),
57  };
58}
59
60/**
61 * The effective length of a cached audio slot: the stored value, else the end of
62 * its last chunk (chunk spans are selection-relative), else the selection.
63 */
64export function cachedEffectiveDuration(cachedEffectiveSeconds, cachedChunks, selectedDurationSeconds) {
65  const stored = positiveOrNull(cachedEffectiveSeconds);
66  if (stored != null) return effectiveSelectionDuration(selectedDurationSeconds, stored);
67  const lastEnd = Array.isArray(cachedChunks) && cachedChunks.length > 0
68    ? positiveOrNull(cachedChunks[cachedChunks.length - 1]?.endSeconds)
69    : null;
70  return effectiveSelectionDuration(selectedDurationSeconds, lastEnd);
71}
72
73/**
74 * Probe a file the browser could not read the length of. Resolves (never rejects)
75 * to the probed duration in seconds, or null when the probe failed, found no
76 * duration, or ran past the budget. On a timeout `onTimeout` is called so the
77 * caller can stop the stuck ffmpeg run (it would otherwise keep the source mounted
78 * under the extraction's mount point).
79 */
80export async function probeUnknownDuration(file, {
81  probe,
82  timeoutMs = DURATION_PROBE_BUDGET_MS,
83  onTimeout,
84  setTimeoutFn = setTimeout,
85  clearTimeoutFn = clearTimeout,
86} = {}) {
87  if (!file || typeof probe !== "function") return null;
88  let timer = null;
89  const timeout = new Promise((resolve) => {
90    timer = setTimeoutFn(() => resolve({ timedOut: true }), timeoutMs);
91  });
92  const probing = Promise.resolve()
93    .then(() => probe(file))
94    .then((result) => ({ result }))
95    .catch((error) => ({ error }));
96  const outcome = await Promise.race([probing, timeout]);
97  clearTimeoutFn(timer);
98  if (outcome.timedOut) {
99    console.warn("[duration-probe] ffmpeg probe exceeded its budget; keeping the unknown-length note");
100    try {
101      await onTimeout?.();
102    } catch {
103      /* best effort */
104    }
105    return null;
106  }
107  if (outcome.error) {
108    console.warn("[duration-probe] ffmpeg probe failed", outcome.error);
109    return null;
110  }
111  return positiveOrNull(outcome.result?.probe?.durationSeconds);
112}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.