@joewinke/jatui 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,69 +1,26 @@
1
1
  /**
2
- * Playback-rate arbitration for VideoPlayer (mos-5lzns.28 internal split).
2
+ * Rate module (mos-5lzns.32) — the rate composer and its advisors: smart
3
+ * speed, per-speaker pace, skip ad reads. Ported from extension-library
4
+ * FEATURES.md §The rate composer.
3
5
  *
4
- * THE RATE COMPOSER CONTRACT (extension-library FEATURES.md — "The rate
5
- * composer") — this is the one non-obvious rule, stated up front because it
6
- * looks like a bug from the outside:
6
+ * rate = base × speaker × silence (clamp [0.0625, 16], 0.001 deadband)
7
7
  *
8
- * Three features want `playbackRate`, and NONE of them writes it:
9
- * · you — the speed keys (−/=/Backspace, alt+wheel)
10
- * · speaker — "this person talks fast" (mos-5lzns.32 fills)
11
- * · silence — "nothing is being said here" (mos-5lzns.32 fills)
8
+ * `base` is the only factor a human chose and the only one the UI reports.
12
9
  *
13
- * They contribute to a product: rate = base × speaker × silence
10
+ * Layout (every file pure and node-testable — `npm run test:unit`):
14
11
  *
15
- * `base` is the only one a human chose, and therefore the only one the UI
16
- * reports. The speed readout must keep saying `1×` while a gap is being taken
17
- * at 2.2×, or the label becomes a strobe.
18
- *
19
- * 1. Both advisors collapse to `1` the moment their setting is off or their
20
- * data has not arrived — `1` is the multiplicative identity, so the
21
- * composer runs identically on a video with no transcript, no waveform
22
- * and no speakers. There is no "feature absent" branch anywhere.
23
- * 2. `base === 0` is a state, not a level (the frozen-frame end of the
24
- * staircase) — it returns early, before any multiplication, because no
25
- * advisor gets to un-pause the video by multiplying its way out of zero.
26
- * 3. Only real changes are written. `playbackRate` is a live audio
27
- * parameter: writing a *slightly* different value is audible as a wobble.
28
- * Hence the 0.001 deadband.
29
- * 4. The clamp is [0.0625, 16] — the range HTML media elements actually
30
- * support. A product of three multipliers can leave it; a single one
31
- * cannot.
32
- *
33
- * The SPEED_STEPS staircase and formatSpeedLabel are ported from the
34
- * extension's theater (linear 0.1 steps below 1×, all the way down to 0 —
35
- * playbackRate=0 is a genuine frozen frame; above 1× the familiar jumps).
36
- */
37
- /** The speed staircase — one step per press of −/=, Backspace resets to 1. */
38
- export declare const SPEED_STEPS: number[];
39
- /**
40
- * "A quarter speed" reads faster as "−.75" than "0.25×" — below 1× the label
41
- * is the delta, not the multiplier. The 0 step is the frozen-frame end of the
42
- * staircase — a state, not a level — and the label says so ("0×"), because a
43
- * frozen frame labeled "1×" lies. Garbage degrades to `1×`.
44
- */
45
- export declare function formatSpeedLabel(rate: number): string;
46
- /**
47
- * One step along the staircase. Unknown current rate snaps to 1 first (a
48
- * hand-set rate off the staircase still steps from where the user expects).
49
- */
50
- export declare function stepRate(current: number, direction: 1 | -1): number;
51
- /** The clamp the composer applies — the range HTML media elements actually support. */
52
- export declare const RATE_MIN = 0.0625;
53
- export declare const RATE_MAX = 16;
54
- /** Writes only on real change — a slightly-different write is audible as a wobble. */
55
- export declare const RATE_DEADBAND = 0.001;
56
- export interface ComposedRate {
57
- rate: number;
58
- /** False when the deadband swallowed the write (keep the previous rate). */
59
- changed: boolean;
60
- }
61
- /**
62
- * The whole arbitration. `speaker` and `silence` are advisor products in
63
- * [0.0625, 16] that COLLAPSE TO 1 when their feature is off or its data has
64
- * not arrived — mos-5lzns.32 fills both; until then they are always 1 and
65
- * this is exactly `base`.
66
- *
67
- * `base === 0` returns early: a frozen frame is a state, not a level.
12
+ * · rate/compose.ts — the contract: SPEED_STEPS staircase, formatSpeedLabel,
13
+ * composeRate (base 0 is a state, advisors collapse to 1, clamp), and
14
+ * shouldWriteRate (deadband, except an exact return to base).
15
+ * · rate/speaker.ts — props → advisors/speaker-rate; manual rates keyed by
16
+ * NAME always beat measured ones.
17
+ * · rate/silence.ts — waveform → advisors/smart-speed map + boost factor.
18
+ * · rate/ads.ts — adSpans ∪ `ad: true` turns; skip only when PLAYBACK
19
+ * runs into an ad, never when a seek lands in one.
20
+ * · rate/tick.ts — the 120ms loop's one-tick decision.
68
21
  */
69
- export declare function composeRate(base: number, speaker: number, silence: number): ComposedRate;
22
+ export * from './rate/compose';
23
+ export * from './rate/speaker';
24
+ export * from './rate/silence';
25
+ export * from './rate/ads';
26
+ export * from './rate/tick';
@@ -1,88 +1,26 @@
1
1
  /**
2
- * Playback-rate arbitration for VideoPlayer (mos-5lzns.28 internal split).
2
+ * Rate module (mos-5lzns.32) — the rate composer and its advisors: smart
3
+ * speed, per-speaker pace, skip ad reads. Ported from extension-library
4
+ * FEATURES.md §The rate composer.
3
5
  *
4
- * THE RATE COMPOSER CONTRACT (extension-library FEATURES.md — "The rate
5
- * composer") — this is the one non-obvious rule, stated up front because it
6
- * looks like a bug from the outside:
6
+ * rate = base × speaker × silence (clamp [0.0625, 16], 0.001 deadband)
7
7
  *
8
- * Three features want `playbackRate`, and NONE of them writes it:
9
- * · you — the speed keys (−/=/Backspace, alt+wheel)
10
- * · speaker — "this person talks fast" (mos-5lzns.32 fills)
11
- * · silence — "nothing is being said here" (mos-5lzns.32 fills)
8
+ * `base` is the only factor a human chose and the only one the UI reports.
12
9
  *
13
- * They contribute to a product: rate = base × speaker × silence
10
+ * Layout (every file pure and node-testable — `npm run test:unit`):
14
11
  *
15
- * `base` is the only one a human chose, and therefore the only one the UI
16
- * reports. The speed readout must keep saying `1×` while a gap is being taken
17
- * at 2.2×, or the label becomes a strobe.
18
- *
19
- * 1. Both advisors collapse to `1` the moment their setting is off or their
20
- * data has not arrived — `1` is the multiplicative identity, so the
21
- * composer runs identically on a video with no transcript, no waveform
22
- * and no speakers. There is no "feature absent" branch anywhere.
23
- * 2. `base === 0` is a state, not a level (the frozen-frame end of the
24
- * staircase) — it returns early, before any multiplication, because no
25
- * advisor gets to un-pause the video by multiplying its way out of zero.
26
- * 3. Only real changes are written. `playbackRate` is a live audio
27
- * parameter: writing a *slightly* different value is audible as a wobble.
28
- * Hence the 0.001 deadband.
29
- * 4. The clamp is [0.0625, 16] — the range HTML media elements actually
30
- * support. A product of three multipliers can leave it; a single one
31
- * cannot.
32
- *
33
- * The SPEED_STEPS staircase and formatSpeedLabel are ported from the
34
- * extension's theater (linear 0.1 steps below 1×, all the way down to 0 —
35
- * playbackRate=0 is a genuine frozen frame; above 1× the familiar jumps).
36
- */
37
- /** The speed staircase — one step per press of −/=, Backspace resets to 1. */
38
- export const SPEED_STEPS = [
39
- 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.75, 2, 2.5, 5, 10
40
- ];
41
- /**
42
- * "A quarter speed" reads faster as "−.75" than "0.25×" — below 1× the label
43
- * is the delta, not the multiplier. The 0 step is the frozen-frame end of the
44
- * staircase — a state, not a level — and the label says so ("0×"), because a
45
- * frozen frame labeled "1×" lies. Garbage degrades to `1×`.
46
- */
47
- export function formatSpeedLabel(rate) {
48
- if (!Number.isFinite(rate) || rate < 0)
49
- return '1×';
50
- if (rate === 0)
51
- return '0×';
52
- if (rate < 1) {
53
- return (rate - 1).toFixed(2).replace(/\.?0+$/, '').replace(/^-0\./, '-.') + '×';
54
- }
55
- return rate.toFixed(2).replace(/\.?0+$/, '') + '×';
56
- }
57
- /**
58
- * One step along the staircase. Unknown current rate snaps to 1 first (a
59
- * hand-set rate off the staircase still steps from where the user expects).
60
- */
61
- export function stepRate(current, direction) {
62
- const i = SPEED_STEPS.indexOf(current);
63
- const cur = i === -1 ? SPEED_STEPS.indexOf(1) : i;
64
- return SPEED_STEPS[Math.min(SPEED_STEPS.length - 1, Math.max(0, cur + direction))];
65
- }
66
- /** The clamp the composer applies — the range HTML media elements actually support. */
67
- export const RATE_MIN = 0.0625;
68
- export const RATE_MAX = 16;
69
- /** Writes only on real change — a slightly-different write is audible as a wobble. */
70
- export const RATE_DEADBAND = 0.001;
71
- /**
72
- * The whole arbitration. `speaker` and `silence` are advisor products in
73
- * [0.0625, 16] that COLLAPSE TO 1 when their feature is off or its data has
74
- * not arrived — mos-5lzns.32 fills both; until then they are always 1 and
75
- * this is exactly `base`.
76
- *
77
- * `base === 0` returns early: a frozen frame is a state, not a level.
12
+ * · rate/compose.ts — the contract: SPEED_STEPS staircase, formatSpeedLabel,
13
+ * composeRate (base 0 is a state, advisors collapse to 1, clamp), and
14
+ * shouldWriteRate (deadband, except an exact return to base).
15
+ * · rate/speaker.ts — props → advisors/speaker-rate; manual rates keyed by
16
+ * NAME always beat measured ones.
17
+ * · rate/silence.ts — waveform → advisors/smart-speed map + boost factor.
18
+ * · rate/ads.ts — adSpans ∪ `ad: true` turns; skip only when PLAYBACK
19
+ * runs into an ad, never when a seek lands in one.
20
+ * · rate/tick.ts — the 120ms loop's one-tick decision.
78
21
  */
79
- export function composeRate(base, speaker, silence) {
80
- if (!Number.isFinite(base) || base <= 0) {
81
- return { rate: Math.max(0, Number.isFinite(base) ? base : 0), changed: false };
82
- }
83
- const sp = Number.isFinite(speaker) && speaker > 0 ? speaker : 1;
84
- const si = Number.isFinite(silence) && silence > 0 ? silence : 1;
85
- const rate = Math.min(RATE_MAX, Math.max(RATE_MIN, base * sp * si));
86
- const changed = Math.abs(rate - base) > RATE_DEADBAND;
87
- return { rate: changed ? rate : base, changed };
88
- }
22
+ export * from './rate/compose';
23
+ export * from './rate/speaker';
24
+ export * from './rate/silence';
25
+ export * from './rate/ads';
26
+ export * from './rate/tick';
@@ -40,7 +40,14 @@ export const VIDEO_SETTINGS = [
40
40
  { id: 'audio.target', type: 'range', default: -20, min: -30, max: -12, step: 2, owner: 'mos-5lzns.31', wired: true },
41
41
  { id: 'audio.quiet', type: 'bool', default: true, owner: 'mos-5lzns.31', wired: true },
42
42
  { id: 'audio.limiter', type: 'bool', default: true, owner: 'mos-5lzns.31', wired: true },
43
- // { id: 'player.speakerRate', type: 'bool', default: false, owner: 'mos-5lzns.32' },
43
+ // Read by VideoPlayer's `smartSpeed` / `speakerPace` (+ `speakerRates`) /
44
+ // `skipAds` (+ `adSpans`) props → video/rate/* (silenceMapFor + silenceFactor,
45
+ // speakerRateMap + speakerFactor, normalizeAdSpans + adSkipTarget), composed
46
+ // by rate/compose.ts composeRate. All default off: an automatic rate change
47
+ // is the host's opt-in.
48
+ { id: 'player.smartSpeed', type: 'bool', default: false, owner: 'mos-5lzns.32', wired: true },
49
+ { id: 'player.speakerRate', type: 'bool', default: false, owner: 'mos-5lzns.32', wired: true },
50
+ { id: 'player.skipAds', type: 'bool', default: false, owner: 'mos-5lzns.32', wired: true },
44
51
  // Read by VideoPlayer's `night` / `nightDepth` / `biasLight` / `predictive` /
45
52
  // `strobeGuard` props → video/night/* (clock.nightArmed, levels.nightLevels,
46
53
  // meter.biasCells, track.lumaAhead / inFlash / composeNight). Unlike the
@@ -51,7 +58,23 @@ export const VIDEO_SETTINGS = [
51
58
  { id: 'night.bias', type: 'bool', default: true, owner: 'mos-5lzns.33', wired: true },
52
59
  { id: 'player.predictive', type: 'bool', default: true, owner: 'mos-5lzns.33', wired: true },
53
60
  { id: 'player.strobeGuard', type: 'bool', default: true, owner: 'mos-5lzns.33', wired: true },
54
- // { id: 'sleep.enabled', type: 'bool', default: true, owner: 'mos-5lzns.34' },
61
+ // Read by VideoPlayer's `windDown` / `windDownAfter` / `windDownNightOnly` /
62
+ // `idleDetection` props → video/presence/winddown.ts (shouldBeginWindDown,
63
+ // windDownAfterMs) + presence/idle.ts (startDesktopWatch). As with night,
64
+ // the DEFAULT is the host's opt-in: an absent `windDown` prop means the
65
+ // feature does not exist on that player. `sleep.desktopIdle` defaults OFF
66
+ // because turning it on is what asks the browser for a permission — the
67
+ // host calls `requestIdleDetection()` from the click that turns it on.
68
+ { id: 'sleep.enabled', type: 'bool', default: true, owner: 'mos-5lzns.34', wired: true },
69
+ { id: 'sleep.after', type: 'range', default: 30, min: 10, max: 90, step: 5, owner: 'mos-5lzns.34', wired: true },
70
+ { id: 'sleep.nightOnly', type: 'bool', default: true, owner: 'mos-5lzns.34', wired: true },
71
+ { id: 'sleep.desktopIdle', type: 'bool', default: false, owner: 'mos-5lzns.34', wired: true },
72
+ // Read by VideoPlayer's `resumeSnap` prop (with `resumeKey` present) →
73
+ // video/presence/resume.ts planResumeSnap → advisors/resume-point snapResume.
74
+ { id: 'player.resumeSnap', type: 'bool', default: true, owner: 'mos-5lzns.34', wired: true },
75
+ // Read by VideoPlayer's `openMotion` prop (with `openFrom` present) →
76
+ // video/presence/flip.ts flipOpen. Off ⇒ the poster still paints.
77
+ { id: 'player.openMotion', type: 'bool', default: true, owner: 'mos-5lzns.34', wired: true },
55
78
  ];
56
79
  /** Coerce a raw stored value against a spec — never throws, never returns junk. */
57
80
  export function coerce(spec, raw) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",