@hraness/slopcamera 3.2.6 → 3.2.8

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.
Files changed (78) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +41 -13
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +0 -9
  6. package/apps/desktop/application/default-registry.ts +0 -10
  7. package/apps/desktop/application/operation.ts +1 -5
  8. package/apps/desktop/application/operations/index.ts +0 -1
  9. package/apps/desktop/cli/args.ts +22 -86
  10. package/apps/desktop/cli/command-host-resources.ts +5 -1
  11. package/apps/desktop/cli/commands.ts +37 -67
  12. package/apps/desktop/cli/help.ts +24 -20
  13. package/apps/desktop/cli/html-scene.ts +479 -0
  14. package/apps/desktop/cli/io.ts +2 -1
  15. package/apps/desktop/cli/main.ts +28 -47
  16. package/apps/desktop/cli/media-ingest-model.ts +2 -0
  17. package/apps/desktop/cli/media-ingest-program.ts +9 -4
  18. package/apps/desktop/cli/menubar.ts +244 -0
  19. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  20. package/apps/desktop/cli/portable-surface.ts +13 -4
  21. package/apps/desktop/cli/root-help-intro.ts +14 -0
  22. package/apps/desktop/code/application-node-planner.ts +0 -47
  23. package/apps/desktop/code/host-source-layout.ts +46 -0
  24. package/apps/desktop/code/public.ts +0 -1
  25. package/apps/desktop/code/runtime-identity.ts +50 -8
  26. package/apps/desktop/code/semantic-builder.ts +0 -43
  27. package/apps/desktop/code/source-bundle.ts +23 -6
  28. package/apps/desktop/code/source-typecheck.ts +131 -43
  29. package/apps/desktop/code/worker-client.ts +14 -2
  30. package/apps/desktop/core/music-analysis.ts +124 -1
  31. package/apps/desktop/dist/cli/main.js +261 -248
  32. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  33. package/apps/desktop/html-overlay/contracts.ts +3 -0
  34. package/apps/desktop/html-overlay/index.ts +28 -0
  35. package/apps/desktop/html-overlay/music-clock.ts +126 -0
  36. package/apps/desktop/html-overlay/rigged-glb.ts +429 -0
  37. package/apps/desktop/html-overlay/runtime.ts +4 -0
  38. package/apps/desktop/html-overlay/scaffolds.ts +2 -1
  39. package/apps/desktop/html-overlay/scene.ts +76 -0
  40. package/dist/cli.js +83 -81
  41. package/dist/{index-jchqst8w.js → index-7131pg1c.js} +43 -236
  42. package/dist/{index-mcy8z0br.js → index-fava6pge.js} +1 -1
  43. package/dist/{index-z7239b4h.js → index-h1k0fnjq.js} +1 -1
  44. package/dist/{index-p63wavx0.js → index-zfnddgay.js} +1 -1
  45. package/dist/index.js +4 -18
  46. package/dist/operations.js +2 -2
  47. package/dist/vectorize/worker.js +1 -1
  48. package/dist/workflow.js +3 -3
  49. package/docs/README.md +2 -1
  50. package/docs/studio.md +1 -1
  51. package/examples/html/music-video.html +514 -0
  52. package/examples/html/music-video.json +16 -0
  53. package/package.json +8 -21
  54. package/skills/slopcamera/SKILL.md +10 -1
  55. package/skills/slopcamera/references/diagrams.md +5 -14
  56. package/skills/slopcamera/references/install.md +2 -2
  57. package/skills/slopcamera/references/music-video.md +169 -0
  58. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  59. package/skills/slopcamera/references/support.md +25 -0
  60. package/skills/slopcamera/references/video-projects.md +6 -2
  61. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  62. package/skills/slopcamera/references/workflows-sdk.md +1 -1
  63. package/src/cli.ts +28 -98
  64. package/src/index.ts +0 -23
  65. package/src/support-completion.ts +16 -0
  66. package/src/support.ts +42 -0
  67. package/src/version.ts +1 -1
  68. package/apps/desktop/application/operations/recording/index.ts +0 -5
  69. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  70. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  71. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  72. package/apps/desktop/application/operations/recording/start.ts +0 -169
  73. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  74. package/apps/desktop/capture/protocol.ts +0 -689
  75. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  76. package/apps/desktop/cli/recording-controller.ts +0 -1567
  77. package/apps/desktop/cli/recording-daemon.ts +0 -931
  78. package/src/desktop.ts +0 -253
@@ -0,0 +1,127 @@
1
+ import { z } from "zod";
2
+
3
+ import type { AudioReactivityBands } from "../core/music-analysis";
4
+
5
+ export const HTML_AUDIO_REACTIVITY_RESOURCE_NAME = "audio-reactivity";
6
+ export const HTML_AUDIO_REACTIVITY_RESOURCE_URL_PATH = "slopcamera/audio-reactivity.json";
7
+ export const HTML_AUDIO_REACTIVITY_MAX_BYTES = 4 * 1024 * 1024;
8
+
9
+ export type HtmlOverlayAudioReactivity = Readonly<{
10
+ kind: "slopcamera.audio-reactivity";
11
+ schemaVersion: 1;
12
+ profile: "bands-v1";
13
+ source: Readonly<{ bytes: number; sha256: string }>;
14
+ pcm: Readonly<{ sampleCount: number; sha256: string }>;
15
+ durationUs: number;
16
+ rateHz: 60;
17
+ bands: AudioReactivityBands;
18
+ }>;
19
+
20
+ export type HtmlOverlayAudioSample = Readonly<{
21
+ bass: number; mid: number; treble: number; energy: number;
22
+ }>;
23
+ export type PreparedHtmlOverlayAudioReactivity = Readonly<{
24
+ durationUs: number;
25
+ sample: (timeUs: number) => HtmlOverlayAudioSample;
26
+ }>;
27
+
28
+ const LIMITS = Object.freeze({ maxSamples: 14_400_000, maxSourceBytes: 512 * 1024 * 1024,
29
+ sampleRateHz: 24_000, hopSize: 400, rateHz: 60, maxTimeUs: 3_600_000_000 });
30
+
31
+ /** One implementation validates the SDK and browser resource, before frame sampling. */
32
+ function createAudioReactivityRuntime(limits: typeof LIMITS) {
33
+ const object = (value: unknown, keys: readonly string[]): Record<string, unknown> => {
34
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
35
+ throw new TypeError("Audio reactivity requires a strict JSON object.");
36
+ }
37
+ const descriptors = Object.getOwnPropertyDescriptors(value);
38
+ if (Reflect.ownKeys(value).length !== keys.length
39
+ || keys.some(key => descriptors[key] === undefined || !("value" in descriptors[key]!))) {
40
+ throw new TypeError("Audio reactivity contains missing, unknown, or accessor fields.");
41
+ }
42
+ return value as Record<string, unknown>;
43
+ };
44
+ const integer = (value: unknown, minimum: number, maximum: number): number => {
45
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
46
+ throw new RangeError("Audio reactivity integer is outside its bound.");
47
+ }
48
+ return value;
49
+ };
50
+ const digest = (value: unknown): string => {
51
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
52
+ throw new TypeError("Audio reactivity requires a SHA-256 digest.");
53
+ }
54
+ return value;
55
+ };
56
+ const parse = (value: unknown): HtmlOverlayAudioReactivity => {
57
+ const input = object(value, ["kind", "schemaVersion", "profile", "source", "pcm", "durationUs", "rateHz", "bands"]);
58
+ if (input.kind !== "slopcamera.audio-reactivity" || input.schemaVersion !== 1
59
+ || input.profile !== "bands-v1" || input.rateHz !== limits.rateHz) {
60
+ throw new TypeError("Unsupported audio reactivity profile or version.");
61
+ }
62
+ const source = object(input.source, ["bytes", "sha256"]);
63
+ const pcm = object(input.pcm, ["sampleCount", "sha256"]);
64
+ const sampleCount = integer(pcm.sampleCount, 1, limits.maxSamples);
65
+ const durationUs = Math.round(sampleCount * 1_000_000 / limits.sampleRateHz);
66
+ if (input.durationUs !== durationUs) throw new RangeError("Audio reactivity duration differs from its PCM sample count.");
67
+ const length = Math.ceil(sampleCount / limits.hopSize) + 1;
68
+ const bands = object(input.bands, ["bass", "mid", "treble", "energy"]);
69
+ const channel = (value: unknown): readonly number[] => {
70
+ const keys = value !== null && typeof value === "object" ? Reflect.ownKeys(value) : [];
71
+ if (!Array.isArray(value) || value.length !== length || keys.length !== length + 1
72
+ || keys.some(key => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key)))) {
73
+ throw new RangeError("Audio reactivity bands must be dense arrays covering the declared duration.");
74
+ }
75
+ const copy: number[] = [];
76
+ for (let index = 0; index < length; index++) {
77
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
78
+ const sample: unknown = descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined;
79
+ if (typeof sample !== "number" || !Number.isFinite(sample) || sample < 0 || sample > 1) {
80
+ throw new RangeError("Audio reactivity band values must be finite numbers in [0, 1].");
81
+ }
82
+ copy.push(sample === 0 ? 0 : sample);
83
+ }
84
+ if (copy.at(-1) !== 0) throw new RangeError("Audio reactivity requires a zero endpoint at the exact duration.");
85
+ return Object.freeze(copy);
86
+ };
87
+ return Object.freeze({
88
+ kind: "slopcamera.audio-reactivity", schemaVersion: 1, profile: "bands-v1", rateHz: 60, durationUs,
89
+ source: Object.freeze({ bytes: integer(source.bytes, 1, limits.maxSourceBytes), sha256: digest(source.sha256) }),
90
+ pcm: Object.freeze({ sampleCount, sha256: digest(pcm.sha256) }),
91
+ bands: Object.freeze({ bass: channel(bands.bass), mid: channel(bands.mid), treble: channel(bands.treble), energy: channel(bands.energy) }),
92
+ });
93
+ };
94
+ const prepareAudioReactivity = (input: unknown): PreparedHtmlOverlayAudioReactivity => {
95
+ const data = parse(input);
96
+ const zero = Object.freeze({ bass: 0, mid: 0, treble: 0, energy: 0 });
97
+ return Object.freeze({ durationUs: data.durationUs, sample(timeUs: number): HtmlOverlayAudioSample {
98
+ integer(timeUs, -limits.maxTimeUs, limits.maxTimeUs);
99
+ if (timeUs < 0 || timeUs >= data.durationUs) return zero;
100
+ const index = Math.min(data.bands.bass.length - 2, Math.floor(timeUs * limits.rateHz / 1_000_000));
101
+ const startUs = index * 1_000_000 / limits.rateHz;
102
+ const endUs = Math.min(data.durationUs, (index + 1) * 1_000_000 / limits.rateHz);
103
+ const mix = Math.max(0, Math.min(1, (timeUs - startUs) / (endUs - startUs)));
104
+ const interpolate = (band: readonly number[]) => band[index]! * (1 - mix) + band[index + 1]! * mix;
105
+ return Object.freeze({ bass: interpolate(data.bands.bass), mid: interpolate(data.bands.mid),
106
+ treble: interpolate(data.bands.treble), energy: interpolate(data.bands.energy) });
107
+ } });
108
+ };
109
+ return { parse, prepareAudioReactivity };
110
+ }
111
+
112
+ const runtime = createAudioReactivityRuntime(LIMITS);
113
+ export const HtmlOverlayAudioReactivitySchema = z.unknown().transform((input, context): HtmlOverlayAudioReactivity => {
114
+ try { return runtime.parse(input); } catch (error) {
115
+ context.addIssue({ code: "custom", message: error instanceof Error ? error.message : "Invalid audio reactivity." });
116
+ return z.NEVER;
117
+ }
118
+ });
119
+
120
+ /** Validate and copy once. Samples depend only on absolute source time, in integer microseconds. */
121
+ export function prepareHtmlOverlayAudioReactivity(input: unknown): PreparedHtmlOverlayAudioReactivity {
122
+ return runtime.prepareAudioReactivity(input);
123
+ }
124
+
125
+ export function createHtmlOverlayAudioReactivityRuntimeSource(): string {
126
+ return `(() => { const runtime = (${createAudioReactivityRuntime.toString()})(${JSON.stringify(LIMITS)}); return { prepareAudioReactivity: runtime.prepareAudioReactivity }; })()`;
127
+ }
@@ -304,6 +304,9 @@ export const HtmlOverlayAuthoringInputSchema = z.strictObject({
304
304
  seed: Uint32Schema,
305
305
  timing: HtmlOverlayTimingSchema,
306
306
  }).superRefine((input, context) => {
307
+ // Zod may run refinements after a numeric bound fails. Leave that issue in
308
+ // the parse result instead of throwing from the checked frame-count helper.
309
+ if (!HtmlOverlayTimingSchema.safeParse(input.timing).success) return;
307
310
  const frames = htmlOverlayFrameCount(input.timing);
308
311
  if (frames > HTML_OVERLAY_MAX_FRAMES) {
309
312
  context.addIssue({
@@ -32,6 +32,16 @@ export {
32
32
  type HtmlOverlayRuntimeFrame,
33
33
  type HtmlOverlayTiming,
34
34
  } from "./contracts";
35
+ export { HtmlSceneInputSchema, type HtmlSceneInput } from "./scene";
36
+ export {
37
+ HTML_AUDIO_REACTIVITY_RESOURCE_NAME,
38
+ HTML_AUDIO_REACTIVITY_RESOURCE_URL_PATH,
39
+ HtmlOverlayAudioReactivitySchema,
40
+ prepareHtmlOverlayAudioReactivity,
41
+ type HtmlOverlayAudioReactivity,
42
+ type HtmlOverlayAudioSample,
43
+ type PreparedHtmlOverlayAudioReactivity,
44
+ } from "./audio-reactivity";
35
45
  export {
36
46
  HTML_OVERLAY_SCAFFOLD_CLOCK_INTEGRATIONS,
37
47
  HTML_OVERLAY_SCAFFOLD_KINDS,
@@ -96,3 +106,21 @@ export {
96
106
  type HtmlOverlayScaffoldInput,
97
107
  type ThreeReferenceScaffoldInput,
98
108
  } from "./scaffolds";
109
+ export {
110
+ HtmlOverlayMusicTimingSchema,
111
+ htmlOverlayMusicPulse,
112
+ sampleHtmlOverlayMusicClock,
113
+ type HtmlOverlayMusicSample,
114
+ type HtmlOverlayMusicTiming,
115
+ } from "./music-clock";
116
+ export {
117
+ THREE_RIGGED_GLB_LIMITS,
118
+ THREE_RIGGED_GLB_PROFILE,
119
+ PreparedThreeRiggedGlb,
120
+ createThreeRiggedGlbModule,
121
+ prepareThreeRiggedGlb,
122
+ type ThreeRiggedGlbInspection,
123
+ type ThreeRiggedGlbPrepareOptions,
124
+ type ThreeRiggedGlbProvenance,
125
+ type ThreeRiggedGlbResource,
126
+ } from "./rigged-glb";
@@ -0,0 +1,126 @@
1
+ import { z } from "zod";
2
+
3
+ import { HTML_OVERLAY_MAX_DURATION_US } from "./contracts";
4
+
5
+ const MUSIC_CLOCK_LIMITS = Object.freeze({
6
+ maxBeatsPerBar: 32,
7
+ maxBpm: 400,
8
+ maxTimeUs: HTML_OVERLAY_MAX_DURATION_US,
9
+ minBeatsPerBar: 1,
10
+ minBpm: 20,
11
+ });
12
+
13
+ /** Explicit constant tempo and the absolute project time of beat zero. */
14
+ export const HtmlOverlayMusicTimingSchema = z.strictObject({
15
+ beatOffsetUs: z.number().int().safe()
16
+ .min(-MUSIC_CLOCK_LIMITS.maxTimeUs).max(MUSIC_CLOCK_LIMITS.maxTimeUs),
17
+ beatsPerBar: z.number().int().safe()
18
+ .min(MUSIC_CLOCK_LIMITS.minBeatsPerBar).max(MUSIC_CLOCK_LIMITS.maxBeatsPerBar),
19
+ bpm: z.number().finite().min(MUSIC_CLOCK_LIMITS.minBpm).max(MUSIC_CLOCK_LIMITS.maxBpm),
20
+ });
21
+ export type HtmlOverlayMusicTiming = Readonly<z.infer<typeof HtmlOverlayMusicTimingSchema>>;
22
+
23
+ export type HtmlOverlayMusicSample = Readonly<{
24
+ /** Zero-based signed bar index; pre-roll bars have negative indices. */
25
+ barIndex: number;
26
+ /** Normalized position within the bar, in [0, 1). */
27
+ barPhase: number;
28
+ /** Zero-based signed beat index, rounded toward negative infinity. */
29
+ beatIndex: number;
30
+ /** Normalized position within the beat, in [0, 1). */
31
+ beatPhase: number;
32
+ /** Signed elapsed beats since the explicitly supplied beat-zero offset. */
33
+ beatPosition: number;
34
+ }>;
35
+
36
+ type MusicClockRuntime = Readonly<{
37
+ musicClock: (timeUs: number, timing: unknown) => HtmlOverlayMusicSample;
38
+ musicPulse: (beatPhase: number, widthBeats?: number) => number;
39
+ }>;
40
+
41
+ /** This closure is self-contained so the browser and SDK execute the same code. */
42
+ function createMusicClockRuntime(limits: typeof MUSIC_CLOCK_LIMITS): MusicClockRuntime {
43
+ const checkTimeUs = (timeUs: unknown): timeUs is number => (
44
+ typeof timeUs === "number"
45
+ && Number.isSafeInteger(timeUs)
46
+ && Math.abs(timeUs) <= limits.maxTimeUs
47
+ );
48
+
49
+ const musicClock = (timeUs: number, timing: unknown): HtmlOverlayMusicSample => {
50
+ if (!checkTimeUs(timeUs)) {
51
+ throw new RangeError(`HTML overlay music timeUs must be an integer within ±${String(limits.maxTimeUs)} microseconds.`);
52
+ }
53
+ if (timing === null || typeof timing !== "object" || Array.isArray(timing)) {
54
+ throw new TypeError("HTML overlay music timing must be an object with bpm, beatOffsetUs, and beatsPerBar.");
55
+ }
56
+ for (const key in timing) {
57
+ if (key !== "bpm" && key !== "beatOffsetUs" && key !== "beatsPerBar") {
58
+ throw new TypeError(`Unknown HTML overlay music timing field ${key}.`);
59
+ }
60
+ }
61
+ const { bpm, beatOffsetUs, beatsPerBar } = timing as Record<string, unknown>;
62
+ if (typeof bpm !== "number" || !Number.isFinite(bpm)
63
+ || bpm < limits.minBpm || bpm > limits.maxBpm) {
64
+ throw new RangeError(`HTML overlay music bpm must be between ${String(limits.minBpm)} and ${String(limits.maxBpm)}.`);
65
+ }
66
+ if (!checkTimeUs(beatOffsetUs)) {
67
+ throw new RangeError(`HTML overlay music beatOffsetUs must be an integer within ±${String(limits.maxTimeUs)} microseconds.`);
68
+ }
69
+ if (typeof beatsPerBar !== "number" || !Number.isSafeInteger(beatsPerBar)
70
+ || beatsPerBar < limits.minBeatsPerBar || beatsPerBar > limits.maxBeatsPerBar) {
71
+ throw new RangeError(`HTML overlay music beatsPerBar must be an integer between ${String(limits.minBeatsPerBar)} and ${String(limits.maxBeatsPerBar)}.`);
72
+ }
73
+
74
+ // Keep the continuous clock unquantized; floor gives consistent pre-roll.
75
+ // Canonicalize negative zero without changing any nonzero beat position.
76
+ const beatPosition = ((timeUs - beatOffsetUs) * bpm) / 60_000_000 || 0;
77
+ const beatIndex = Math.floor(beatPosition);
78
+ const beatPhase = beatPosition - beatIndex;
79
+ const barIndex = Math.floor(beatIndex / beatsPerBar);
80
+ const barPhase = (beatIndex - barIndex * beatsPerBar + beatPhase) / beatsPerBar;
81
+ return Object.freeze({ barIndex, barPhase, beatIndex, beatPhase, beatPosition });
82
+ };
83
+
84
+ const musicPulse = (beatPhase: number, widthBeats = 0.5): number => {
85
+ if (!Number.isFinite(beatPhase) || beatPhase < 0 || beatPhase >= 1) {
86
+ throw new RangeError("HTML overlay music beatPhase must be in [0, 1).");
87
+ }
88
+ if (!Number.isFinite(widthBeats) || widthBeats <= 0 || widthBeats > 1) {
89
+ throw new RangeError("HTML overlay music pulse widthBeats must be in (0, 1].");
90
+ }
91
+ const distance = Math.min(beatPhase, 1 - beatPhase);
92
+ // Divide before doubling so even a positive subnormal width stays defined.
93
+ const position = Math.min(1, (distance / widthBeats) * 2);
94
+ return 1 - position * position * (3 - 2 * position);
95
+ };
96
+
97
+ return Object.freeze({ musicClock, musicPulse });
98
+ }
99
+
100
+ const musicRuntime = createMusicClockRuntime(MUSIC_CLOCK_LIMITS);
101
+
102
+ /** Samples absolute integer project time. This does not detect tempo or downbeats. */
103
+ export function sampleHtmlOverlayMusicClock(
104
+ timeUs: number,
105
+ timing: HtmlOverlayMusicTiming,
106
+ ): HtmlOverlayMusicSample {
107
+ // Validate the original value with the shared browser boundary as well as the
108
+ // public schema; neither path silently strips an undeclared timing field.
109
+ const sample = musicRuntime.musicClock(timeUs, timing);
110
+ HtmlOverlayMusicTimingSchema.parse(timing);
111
+ return sample;
112
+ }
113
+
114
+ /**
115
+ * A periodic [0, 1] smoothstep pulse centered on each beat. widthBeats is the
116
+ * complete nonzero width, with equal anticipation and decay. Both the beat seam
117
+ * and the support edges have zero slope. This is an envelope, not flash analysis.
118
+ */
119
+ export function htmlOverlayMusicPulse(beatPhase: number, widthBeats = 0.5): number {
120
+ return musicRuntime.musicPulse(beatPhase, widthBeats);
121
+ }
122
+
123
+ /** Internal browser integration: a complete expression with musicClock/musicPulse. */
124
+ export function createHtmlOverlayMusicClockRuntimeSource(): string {
125
+ return `(${createMusicClockRuntime.toString()})(${JSON.stringify(MUSIC_CLOCK_LIMITS)})`;
126
+ }