@rightkit/video-ui 0.0.0-stage → 0.1.1
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.
- package/CHANGELOG.md +10 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +47 -2
- package/dist/appName.d.ts +2 -0
- package/dist/appName.js +8 -0
- package/dist/components/DesktopWorkspace.d.ts +4 -0
- package/dist/components/DesktopWorkspace.js +527 -0
- package/dist/components/Player.d.ts +49 -0
- package/dist/components/Player.js +335 -0
- package/dist/components/RangePreview.d.ts +9 -0
- package/dist/components/RangePreview.js +22 -0
- package/dist/components/Workspace.css +15 -0
- package/dist/demo/DemoSettings.d.ts +1 -0
- package/dist/demo/DemoSettings.js +43 -0
- package/dist/demo/bindings.d.ts +121 -0
- package/dist/demo/bindings.js +57 -0
- package/dist/demo/demo.css +124 -0
- package/dist/demo/useDemo.d.ts +8 -0
- package/dist/demo/useDemo.js +37 -0
- package/dist/editor/CaptionInspector.d.ts +8 -0
- package/dist/editor/CaptionInspector.js +33 -0
- package/dist/editor/ChangesPanel.d.ts +13 -0
- package/dist/editor/ChangesPanel.js +87 -0
- package/dist/editor/ColourInspector.d.ts +5 -0
- package/dist/editor/ColourInspector.js +341 -0
- package/dist/editor/Inspector.d.ts +15 -0
- package/dist/editor/Inspector.js +202 -0
- package/dist/editor/Timeline.d.ts +39 -0
- package/dist/editor/Timeline.js +506 -0
- package/dist/editor/TimelineEditorView.d.ts +9 -0
- package/dist/editor/TimelineEditorView.js +237 -0
- package/dist/editor/TranscriptPanel.d.ts +20 -0
- package/dist/editor/TranscriptPanel.js +180 -0
- package/dist/editor/actions.d.ts +63 -0
- package/dist/editor/actions.js +124 -0
- package/dist/editor/adapter.d.ts +76 -0
- package/dist/editor/adapter.js +181 -0
- package/dist/editor/capabilities.d.ts +37 -0
- package/dist/editor/capabilities.js +33 -0
- package/dist/editor/gesture.d.ts +29 -0
- package/dist/editor/gesture.js +119 -0
- package/dist/editor/index.d.ts +10 -0
- package/dist/editor/index.js +7 -0
- package/dist/editor/keys.d.ts +15 -0
- package/dist/editor/keys.js +42 -0
- package/dist/editor/model.d.ts +88 -0
- package/dist/editor/model.js +87 -0
- package/dist/editor/transcript.d.ts +27 -0
- package/dist/editor/transcript.js +59 -0
- package/dist/editor/vendor/hyperframes/PlayheadIndicator.d.ts +9 -0
- package/dist/editor/vendor/hyperframes/PlayheadIndicator.js +38 -0
- package/dist/editor/vendor/hyperframes/audioWaveformPeaks.d.ts +7 -0
- package/dist/editor/vendor/hyperframes/audioWaveformPeaks.js +47 -0
- package/dist/editor/vendor/hyperframes/rulerGeometry.d.ts +13 -0
- package/dist/editor/vendor/hyperframes/rulerGeometry.js +133 -0
- package/dist/editor/vendor/hyperframes/useInspectorGestureTransaction.d.ts +33 -0
- package/dist/editor/vendor/hyperframes/useInspectorGestureTransaction.js +126 -0
- package/dist/env.d.ts +66 -0
- package/dist/hooks/useEngine.d.ts +10 -0
- package/dist/hooks/useEngine.js +68 -0
- package/dist/hooks/useHdrToSdr.d.ts +11 -0
- package/dist/hooks/useHdrToSdr.js +19 -0
- package/dist/hooks/useProgress.d.ts +10 -0
- package/dist/hooks/useProgress.js +32 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +57 -0
- package/dist/legal/legalConfig.d.ts +38 -0
- package/dist/legal/legalConfig.js +77 -0
- package/dist/lib/assemble.d.ts +100 -0
- package/dist/lib/assemble.js +72 -0
- package/dist/lib/asset.d.ts +2 -0
- package/dist/lib/asset.js +3 -0
- package/dist/lib/dialog.d.ts +4 -0
- package/dist/lib/dialog.js +44 -0
- package/dist/lib/engine.d.ts +44 -0
- package/dist/lib/engine.js +51 -0
- package/dist/lib/genright.d.ts +90 -0
- package/dist/lib/genright.js +160 -0
- package/dist/lib/media.d.ts +1045 -0
- package/dist/lib/media.js +171 -0
- package/dist/lib/mediaAdapters.d.ts +18 -0
- package/dist/lib/mediaAdapters.js +46 -0
- package/dist/lib/mic.d.ts +38 -0
- package/dist/lib/mic.js +14 -0
- package/dist/lib/models.d.ts +53 -0
- package/dist/lib/models.js +72 -0
- package/dist/lib/modelsBackend.d.ts +3 -0
- package/dist/lib/modelsBackend.js +68 -0
- package/dist/lib/progress.d.ts +77 -0
- package/dist/lib/progress.js +192 -0
- package/dist/lib/project.d.ts +138 -0
- package/dist/lib/project.js +48 -0
- package/dist/lib/services.d.ts +27 -0
- package/dist/lib/services.js +38 -0
- package/dist/lib/settingsBackend.d.ts +2 -0
- package/dist/lib/settingsBackend.js +57 -0
- package/dist/lib/shell.d.ts +172 -0
- package/dist/lib/shell.js +32 -0
- package/dist/styles.css +24 -0
- package/dist/ui/icons.d.ts +7 -0
- package/dist/ui/icons.js +62 -0
- package/dist/ui/index.d.ts +45 -0
- package/dist/ui/index.js +43 -0
- package/dist/ui/ui.css +104 -0
- package/dist/views/Assets.css +99 -0
- package/dist/views/AssetsView.d.ts +6 -0
- package/dist/views/AssetsView.js +371 -0
- package/dist/views/AutoEditView.d.ts +16 -0
- package/dist/views/AutoEditView.js +452 -0
- package/dist/views/BatchDelivery.d.ts +3 -0
- package/dist/views/BatchDelivery.js +331 -0
- package/dist/views/BundleExport.d.ts +3 -0
- package/dist/views/BundleExport.js +81 -0
- package/dist/views/Exports.css +117 -0
- package/dist/views/ExportsView.d.ts +12 -0
- package/dist/views/ExportsView.js +354 -0
- package/dist/views/FinishedEdit.css +546 -0
- package/dist/views/Home.css +97 -0
- package/dist/views/Home.d.ts +14 -0
- package/dist/views/Home.js +192 -0
- package/dist/views/Media.css +65 -0
- package/dist/views/OnboardingGate.d.ts +10 -0
- package/dist/views/OnboardingGate.js +54 -0
- package/dist/views/Phrases.css +45 -0
- package/dist/views/PhrasesView.d.ts +4 -0
- package/dist/views/PhrasesView.js +104 -0
- package/dist/views/ProgressView.d.ts +19 -0
- package/dist/views/ProgressView.js +36 -0
- package/dist/views/Prompt.css +84 -0
- package/dist/views/PromptPanel.d.ts +8 -0
- package/dist/views/PromptPanel.js +169 -0
- package/dist/views/Recipe.css +84 -0
- package/dist/views/RecipeLibrary.d.ts +7 -0
- package/dist/views/RecipeLibrary.js +104 -0
- package/dist/views/Retention.css +49 -0
- package/dist/views/RetentionView.d.ts +7 -0
- package/dist/views/RetentionView.js +145 -0
- package/dist/views/SettingsView.d.ts +9 -0
- package/dist/views/SettingsView.js +235 -0
- package/dist/views/Shorts.css +112 -0
- package/dist/views/ShortsView.d.ts +4 -0
- package/dist/views/ShortsView.js +205 -0
- package/dist/views/SourcesView.d.ts +6 -0
- package/dist/views/SourcesView.js +417 -0
- package/dist/views/TimelineView.d.ts +20 -0
- package/dist/views/TimelineView.js +12 -0
- package/package.json +68 -3
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read model: folds `project.show` (`ProjectShow`) into flat editor lanes with
|
|
3
|
+
* second-based geometry. Rust owns canonical ticks/frames — this model only
|
|
4
|
+
* translates for display and gesture maths, and converts back on commit.
|
|
5
|
+
*/
|
|
6
|
+
import { fpsValue, ticksToSeconds, titleLabel, } from "../lib/project.js";
|
|
7
|
+
export function buildEditorModel(show) {
|
|
8
|
+
const corrected = show;
|
|
9
|
+
const tl = corrected.timeline;
|
|
10
|
+
const fps = fpsValue(tl.fps);
|
|
11
|
+
const sourceName = (id) => show.sources.find((s) => s.info.id === id)?.name ?? id.slice(0, 8);
|
|
12
|
+
const placements = new Map(show.layout.map((p) => [p.clip, p]));
|
|
13
|
+
const lanes = [];
|
|
14
|
+
for (const track of tl.video_tracks) {
|
|
15
|
+
lanes.push({
|
|
16
|
+
id: track.id,
|
|
17
|
+
kind: "video",
|
|
18
|
+
label: track.id,
|
|
19
|
+
clips: track.clips.map((c, index) => {
|
|
20
|
+
const p = placements.get(c.id);
|
|
21
|
+
return {
|
|
22
|
+
id: c.id,
|
|
23
|
+
kind: "video",
|
|
24
|
+
start: (p?.start_frame ?? 0) / fps,
|
|
25
|
+
duration: (p?.frames ?? 0) / fps,
|
|
26
|
+
label: sourceName(c.source),
|
|
27
|
+
ref: { kind: "video", clip: c, trackId: track.id, index },
|
|
28
|
+
};
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
for (const track of tl.audio_tracks) {
|
|
33
|
+
lanes.push({
|
|
34
|
+
id: track.id,
|
|
35
|
+
kind: "audio",
|
|
36
|
+
label: track.role === "narration" ? "Narr" : track.role,
|
|
37
|
+
clips: track.clips.map((c) => ({
|
|
38
|
+
id: c.id,
|
|
39
|
+
kind: "audio",
|
|
40
|
+
start: ticksToSeconds(c.start),
|
|
41
|
+
duration: ticksToSeconds(c.trim_out - c.trim_in),
|
|
42
|
+
label: sourceName(c.source),
|
|
43
|
+
link: c.link,
|
|
44
|
+
ref: { kind: "audio", clip: c, trackId: track.id, role: track.role },
|
|
45
|
+
})),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
for (const track of tl.overlay_tracks) {
|
|
49
|
+
lanes.push({
|
|
50
|
+
id: track.id,
|
|
51
|
+
kind: "overlay",
|
|
52
|
+
label: track.id,
|
|
53
|
+
clips: track.clips.map((c) => ({
|
|
54
|
+
id: c.id,
|
|
55
|
+
kind: "overlay",
|
|
56
|
+
start: c.start_frame / fps,
|
|
57
|
+
duration: c.frames / fps,
|
|
58
|
+
label: titleLabel(c.template),
|
|
59
|
+
ref: { kind: "title", clip: c, trackId: track.id },
|
|
60
|
+
})),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
corrections: tl.corrections ?? { captions: {}, positions: {}, audio: {} },
|
|
65
|
+
captions: corrected.editor_captions ?? [],
|
|
66
|
+
sources: show.sources,
|
|
67
|
+
fps,
|
|
68
|
+
durationS: Math.max(show.summary.duration_ms / 1000, 0),
|
|
69
|
+
revisionCursor: show.revision.cursor,
|
|
70
|
+
revisionTotal: show.revision.total,
|
|
71
|
+
canUndo: show.revision.can_undo,
|
|
72
|
+
canRedo: show.revision.can_redo,
|
|
73
|
+
lanes,
|
|
74
|
+
clipCount: show.summary.video_clips + show.summary.audio_clips,
|
|
75
|
+
sourceDurationS: new Map(show.sources.map((s) => [s.info.id, ticksToSeconds(s.info.duration)])),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function findClip(model, id) {
|
|
79
|
+
for (const lane of model.lanes) {
|
|
80
|
+
const hit = lane.clips.find((c) => c.id === id);
|
|
81
|
+
if (hit)
|
|
82
|
+
return hit;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// Compatibility alias for consumers importing the shorter name.
|
|
87
|
+
export const buildModel = buildEditorModel;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source↔output time mapping for transcript-driven editing. Mirrors
|
|
3
|
+
* `clip_ranges` in crates/cutright-engine/src/media_cmd.rs: every placed clip
|
|
4
|
+
* range (video gapless accumulation, audio explicit starts) maps a source-ms
|
|
5
|
+
* window onto output seconds. Word-edge evidence stays in the engine's
|
|
6
|
+
* validated `transcript.show` payload — this module only maps numbers.
|
|
7
|
+
*/
|
|
8
|
+
import type { ProjectShow } from "../lib/project.js";
|
|
9
|
+
/** One placed source range: source [inS,outS) lands at outOffS + (t - inS). */
|
|
10
|
+
export type ClipRange = {
|
|
11
|
+
inS: number;
|
|
12
|
+
outS: number;
|
|
13
|
+
outOffS: number;
|
|
14
|
+
};
|
|
15
|
+
/** source id → placed ranges, in output order (same walk as the engine). */
|
|
16
|
+
export declare function clipRanges(show: ProjectShow): Map<string, ClipRange[]>;
|
|
17
|
+
/** Output seconds for a source timestamp in ms, or null when unplaced. */
|
|
18
|
+
export declare function sourceToOutput(ranges: readonly ClipRange[], sourceMs: number): number | null;
|
|
19
|
+
/**
|
|
20
|
+
* Output ranges (seconds) covered by a source window [startMs, endMs). A word
|
|
21
|
+
* or span can be placed more than once; every covered piece is returned so a
|
|
22
|
+
* ripple-delete excises all of them.
|
|
23
|
+
*/
|
|
24
|
+
export declare function coveredOutputRanges(ranges: readonly ClipRange[], startMs: number, endMs: number): {
|
|
25
|
+
startS: number;
|
|
26
|
+
endS: number;
|
|
27
|
+
}[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { fpsValue, ticksToSeconds } from "../lib/project.js";
|
|
2
|
+
/** source id → placed ranges, in output order (same walk as the engine). */
|
|
3
|
+
export function clipRanges(show) {
|
|
4
|
+
const tl = show.timeline;
|
|
5
|
+
const fps = fpsValue(tl.fps);
|
|
6
|
+
const ranges = new Map();
|
|
7
|
+
const push = (source, r) => {
|
|
8
|
+
const list = ranges.get(source) ?? [];
|
|
9
|
+
list.push(r);
|
|
10
|
+
ranges.set(source, list);
|
|
11
|
+
};
|
|
12
|
+
const placements = new Map(show.layout.map((p) => [p.clip, p]));
|
|
13
|
+
for (const track of tl.video_tracks) {
|
|
14
|
+
for (const clip of track.clips) {
|
|
15
|
+
const p = placements.get(clip.id);
|
|
16
|
+
const inS = ticksToSeconds(clip.source_in);
|
|
17
|
+
const outS = ticksToSeconds(clip.source_out);
|
|
18
|
+
if (!p || outS <= inS)
|
|
19
|
+
continue;
|
|
20
|
+
push(clip.source, { inS, outS, outOffS: p.start_frame / fps });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const track of tl.audio_tracks) {
|
|
24
|
+
for (const clip of track.clips) {
|
|
25
|
+
const inS = ticksToSeconds(clip.trim_in);
|
|
26
|
+
const outS = ticksToSeconds(clip.trim_out);
|
|
27
|
+
if (outS <= inS)
|
|
28
|
+
continue;
|
|
29
|
+
push(clip.source, { inS, outS, outOffS: ticksToSeconds(clip.start) });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const list of ranges.values())
|
|
33
|
+
list.sort((a, b) => a.outOffS - b.outOffS);
|
|
34
|
+
return ranges;
|
|
35
|
+
}
|
|
36
|
+
/** Output seconds for a source timestamp in ms, or null when unplaced. */
|
|
37
|
+
export function sourceToOutput(ranges, sourceMs) {
|
|
38
|
+
const s = sourceMs / 1000;
|
|
39
|
+
const r = ranges.find((r) => s >= r.inS && s < r.outS);
|
|
40
|
+
return r ? r.outOffS + (s - r.inS) : null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Output ranges (seconds) covered by a source window [startMs, endMs). A word
|
|
44
|
+
* or span can be placed more than once; every covered piece is returned so a
|
|
45
|
+
* ripple-delete excises all of them.
|
|
46
|
+
*/
|
|
47
|
+
export function coveredOutputRanges(ranges, startMs, endMs) {
|
|
48
|
+
const out = [];
|
|
49
|
+
const a = startMs / 1000;
|
|
50
|
+
const b = endMs / 1000;
|
|
51
|
+
for (const r of ranges) {
|
|
52
|
+
const lo = Math.max(a, r.inS);
|
|
53
|
+
const hi = Math.min(b, r.outS);
|
|
54
|
+
if (hi <= lo)
|
|
55
|
+
continue;
|
|
56
|
+
out.push({ startS: r.outOffS + (lo - r.inS), endS: r.outOffS + (hi - r.inS) });
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flat playhead: 1px accent line over the track height plus a small rotated
|
|
3
|
+
* grab-handle head pinned sticky to the ruler row.
|
|
4
|
+
*/
|
|
5
|
+
export declare function PlayheadIndicator({ color, scrubbing, stickyHead, }: {
|
|
6
|
+
color?: string;
|
|
7
|
+
scrubbing?: boolean;
|
|
8
|
+
stickyHead?: boolean;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Adapted from heygen-com/hyperframes (Apache-2.0),
|
|
4
|
+
* packages/studio @ 1d9b3673fade7511dd13cde5c3776a0e45e47ca3.
|
|
5
|
+
* Upstream file: packages/studio/src/player/components/PlayheadIndicator.tsx.
|
|
6
|
+
* Modifications: inlined PLAYHEAD_HEAD_W (from ./timelineLayout); retargeted
|
|
7
|
+
* the colour props at the host app shell tokens; removed the radial-gradient glow —
|
|
8
|
+
* the fused the host app/the host app shell is flat with no gradients.
|
|
9
|
+
* See src/editor/NOTICES.md for the licence text and provenance.
|
|
10
|
+
*/
|
|
11
|
+
const PLAYHEAD_HEAD_W = 14;
|
|
12
|
+
const HEAD_H = 9;
|
|
13
|
+
/**
|
|
14
|
+
* Flat playhead: 1px accent line over the track height plus a small rotated
|
|
15
|
+
* grab-handle head pinned sticky to the ruler row.
|
|
16
|
+
*/
|
|
17
|
+
export function PlayheadIndicator({ color = "var(--rk-accent)", scrubbing = false, stickyHead = true, }) {
|
|
18
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": "true", className: "absolute bottom-0", style: {
|
|
19
|
+
left: "50%",
|
|
20
|
+
top: 1 + HEAD_H,
|
|
21
|
+
width: 1,
|
|
22
|
+
marginLeft: -0.5,
|
|
23
|
+
background: color,
|
|
24
|
+
} }), _jsx("div", { className: stickyHead ? "sticky" : "absolute", style: {
|
|
25
|
+
left: 0,
|
|
26
|
+
top: stickyHead ? 0 : 1,
|
|
27
|
+
height: stickyHead ? 0 : undefined,
|
|
28
|
+
}, children: _jsx("div", { style: {
|
|
29
|
+
width: PLAYHEAD_HEAD_W,
|
|
30
|
+
height: HEAD_H,
|
|
31
|
+
borderRadius: 2,
|
|
32
|
+
marginTop: 1,
|
|
33
|
+
background: scrubbing ? color : "transparent",
|
|
34
|
+
border: `1.5px solid ${color}`,
|
|
35
|
+
boxSizing: "border-box",
|
|
36
|
+
transform: "rotate(45deg)",
|
|
37
|
+
} }) })] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Peak is 0–1 against the file's own maximum. Hotter peaks paint more opaque. */
|
|
2
|
+
export declare function loudnessToOpacity(peak: number): number;
|
|
3
|
+
/**
|
|
4
|
+
* Max-in-bucket over the trimmed peak window, one value per bar.
|
|
5
|
+
* A point sample drops a transient that sits between bar columns.
|
|
6
|
+
*/
|
|
7
|
+
export declare function decimatePeaks(peaks: readonly number[], startFraction: number, endFraction: number, barCount: number): number[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from heygen-com/hyperframes (Apache-2.0),
|
|
3
|
+
* packages/studio @ 1d9b3673fade7511dd13cde5c3776a0e45e47ca3.
|
|
4
|
+
* Upstream file: player/components/audioWaveformPeaks.ts. Modifications: none — copied unmodified.
|
|
5
|
+
* See src/editor/NOTICES.md for the licence text and provenance.
|
|
6
|
+
*/
|
|
7
|
+
/** Quietest bar that still clears the timeline contrast gate on the 70% pill. */
|
|
8
|
+
const LOUDNESS_OPACITY_FLOOR = 0.64;
|
|
9
|
+
const LOUDNESS_OPACITY_SPAN = 1 - LOUDNESS_OPACITY_FLOOR;
|
|
10
|
+
/** Peak is 0–1 against the file's own maximum. Hotter peaks paint more opaque. */
|
|
11
|
+
export function loudnessToOpacity(peak) {
|
|
12
|
+
if (!Number.isFinite(peak))
|
|
13
|
+
return LOUDNESS_OPACITY_FLOOR;
|
|
14
|
+
const clamped = Math.min(1, Math.max(0, peak));
|
|
15
|
+
return LOUDNESS_OPACITY_FLOOR + LOUDNESS_OPACITY_SPAN * clamped;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Max-in-bucket over the trimmed peak window, one value per bar.
|
|
19
|
+
* A point sample drops a transient that sits between bar columns.
|
|
20
|
+
*/
|
|
21
|
+
export function decimatePeaks(peaks, startFraction, endFraction, barCount) {
|
|
22
|
+
if (barCount <= 0 || peaks.length === 0)
|
|
23
|
+
return [];
|
|
24
|
+
const startFractionClamped = Math.min(1, Math.max(0, startFraction));
|
|
25
|
+
const endFractionClamped = Math.min(1, Math.max(startFractionClamped, endFraction));
|
|
26
|
+
const start = Math.floor(startFractionClamped * peaks.length);
|
|
27
|
+
const endIndex = Math.ceil(endFractionClamped * peaks.length);
|
|
28
|
+
const end = Math.max(start + 1, Math.min(peaks.length, endIndex));
|
|
29
|
+
const span = end - start;
|
|
30
|
+
const bars = [];
|
|
31
|
+
for (let index = 0; index < barCount; index++) {
|
|
32
|
+
let from = start + Math.floor((index * span) / barCount);
|
|
33
|
+
let to = start + Math.floor(((index + 1) * span) / barCount);
|
|
34
|
+
if (to <= from) {
|
|
35
|
+
from = Math.min(from, end - 1);
|
|
36
|
+
to = from + 1;
|
|
37
|
+
}
|
|
38
|
+
let max = 0;
|
|
39
|
+
for (let cursor = from; cursor < to; cursor++) {
|
|
40
|
+
const value = peaks[cursor] ?? 0;
|
|
41
|
+
if (value > max)
|
|
42
|
+
max = value;
|
|
43
|
+
}
|
|
44
|
+
bars.push(max);
|
|
45
|
+
}
|
|
46
|
+
return bars;
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type EditorTimeRange = {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
};
|
|
5
|
+
export type EditorTimeDisplayMode = "time" | "frame";
|
|
6
|
+
export declare function getTimelineMajorTickInterval(duration: number, pixelsPerSecond?: number, frameRate?: number): number;
|
|
7
|
+
/** Seconds between neighbouring ruler lines (minor ticks when drawn, else major): the snap grid. */
|
|
8
|
+
export declare function getTimelineGridStep(duration: number, pixelsPerSecond?: number, frameRate?: number): number;
|
|
9
|
+
export declare function generateTicks(duration: number, pixelsPerSecond?: number, frameRate?: number, range?: EditorTimeRange): {
|
|
10
|
+
major: number[];
|
|
11
|
+
minor: number[];
|
|
12
|
+
};
|
|
13
|
+
export declare function formatTimelineTickLabel(time: number, duration: number, majorInterval: number): string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from heygen-com/hyperframes (Apache-2.0),
|
|
3
|
+
* packages/studio @ 1d9b3673fade7511dd13cde5c3776a0e45e47ca3.
|
|
4
|
+
* Upstream file: packages/studio/src/player/components/timelineRulerGeometry.ts.
|
|
5
|
+
* Modifications: inlined formatTime/secondsToFrame from ../lib/time; replaced
|
|
6
|
+
* STUDIO_PREVIEW_FPS with an explicit frameRate parameter; replaced
|
|
7
|
+
* TimelineTimeDisplayMode/TimelineTimeRange imports with local types;
|
|
8
|
+
* removed rulerFrameRate() (the display-mode branch is owned by the caller).
|
|
9
|
+
* See src/editor/NOTICES.md for the licence text and provenance.
|
|
10
|
+
*/
|
|
11
|
+
function formatTime(time) {
|
|
12
|
+
if (!Number.isFinite(time) || time < 0)
|
|
13
|
+
return "00:00";
|
|
14
|
+
const mins = Math.floor(time / 60);
|
|
15
|
+
const secs = Math.floor(time % 60);
|
|
16
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
17
|
+
}
|
|
18
|
+
// fallow-ignore-next-line complexity
|
|
19
|
+
export function getTimelineMajorTickInterval(duration, pixelsPerSecond, frameRate) {
|
|
20
|
+
const zoomIntervals = [
|
|
21
|
+
0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600,
|
|
22
|
+
];
|
|
23
|
+
let interval;
|
|
24
|
+
if (Number.isFinite(pixelsPerSecond) && (pixelsPerSecond ?? 0) > 0) {
|
|
25
|
+
const targetMajorPx = 88;
|
|
26
|
+
interval =
|
|
27
|
+
zoomIntervals.find((candidate) => candidate * (pixelsPerSecond ?? 0) >= targetMajorPx) ??
|
|
28
|
+
3600;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const durationIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60];
|
|
32
|
+
const target = duration / 6;
|
|
33
|
+
interval = durationIntervals.find((candidate) => candidate >= target) ?? 60;
|
|
34
|
+
}
|
|
35
|
+
if (Number.isFinite(frameRate) && (frameRate ?? 0) > 0) {
|
|
36
|
+
const fps = frameRate ?? 0;
|
|
37
|
+
return Math.max(1, Math.ceil(interval * fps - 1e-6)) / fps;
|
|
38
|
+
}
|
|
39
|
+
return interval;
|
|
40
|
+
}
|
|
41
|
+
// fallow-ignore-next-line complexity
|
|
42
|
+
function getMinorSubdivisions(majorInterval, pixelsPerSecond, frameRate) {
|
|
43
|
+
const pps = Number.isFinite(pixelsPerSecond) ? (pixelsPerSecond ?? 0) : 0;
|
|
44
|
+
if (pps <= 0)
|
|
45
|
+
return 4;
|
|
46
|
+
const fps = Number.isFinite(frameRate) ? (frameRate ?? 0) : 0;
|
|
47
|
+
const majorFrames = fps > 0 ? Math.round(majorInterval * fps) : 0;
|
|
48
|
+
const candidates = fps > 0 ? [4, 5, 3, 2] : [4, 2];
|
|
49
|
+
for (const parts of candidates) {
|
|
50
|
+
if (fps > 0 && majorFrames % parts !== 0)
|
|
51
|
+
continue;
|
|
52
|
+
if ((majorInterval / parts) * pps >= 8)
|
|
53
|
+
return parts;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
function roundTickValue(time) {
|
|
58
|
+
return Math.round(time * 1e6) / 1e6;
|
|
59
|
+
}
|
|
60
|
+
function isSupportedTickDuration(duration) {
|
|
61
|
+
return duration > 0 && Number.isFinite(duration) && duration <= 14400;
|
|
62
|
+
}
|
|
63
|
+
function getTickRange(duration, range) {
|
|
64
|
+
return {
|
|
65
|
+
start: Math.max(0, range?.start ?? 0),
|
|
66
|
+
end: Math.min(duration, range?.end ?? duration),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function appendMinorTicks(minor, majorTime, minorInterval, subdivisions, range, maxTicks, majorCount) {
|
|
70
|
+
for (let part = 1; part < subdivisions && majorCount + minor.length < maxTicks; part++) {
|
|
71
|
+
const time = majorTime + part * minorInterval;
|
|
72
|
+
if (time >= range.start - 0.001 && time <= range.end + 0.001) {
|
|
73
|
+
minor.push(roundTickValue(time));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function tickSpacing(duration, pixelsPerSecond, frameRate) {
|
|
78
|
+
const majorInterval = getTimelineMajorTickInterval(duration, pixelsPerSecond, frameRate);
|
|
79
|
+
const subdivisions = getMinorSubdivisions(majorInterval, pixelsPerSecond, frameRate);
|
|
80
|
+
return { majorInterval, subdivisions };
|
|
81
|
+
}
|
|
82
|
+
/** Seconds between neighbouring ruler lines (minor ticks when drawn, else major): the snap grid. */
|
|
83
|
+
export function getTimelineGridStep(duration, pixelsPerSecond, frameRate) {
|
|
84
|
+
if (!isSupportedTickDuration(duration))
|
|
85
|
+
return 0;
|
|
86
|
+
const { majorInterval, subdivisions } = tickSpacing(duration, pixelsPerSecond, frameRate);
|
|
87
|
+
return subdivisions > 0 ? majorInterval / subdivisions : majorInterval;
|
|
88
|
+
}
|
|
89
|
+
export function generateTicks(duration, pixelsPerSecond, frameRate, range) {
|
|
90
|
+
if (!isSupportedTickDuration(duration))
|
|
91
|
+
return { major: [], minor: [] };
|
|
92
|
+
const { majorInterval, subdivisions } = tickSpacing(duration, pixelsPerSecond, frameRate);
|
|
93
|
+
const minorInterval = subdivisions > 0 ? majorInterval / subdivisions : 0;
|
|
94
|
+
const major = [];
|
|
95
|
+
const minor = [];
|
|
96
|
+
const maxTicks = 2000;
|
|
97
|
+
const tickRange = getTickRange(duration, range);
|
|
98
|
+
const firstMajorIndex = Math.max(0, Math.floor(tickRange.start / majorInterval));
|
|
99
|
+
for (let index = firstMajorIndex; major.length < maxTicks; index++) {
|
|
100
|
+
const time = index * majorInterval;
|
|
101
|
+
if (time > tickRange.end + 0.001)
|
|
102
|
+
break;
|
|
103
|
+
if (time >= tickRange.start - 0.001)
|
|
104
|
+
major.push(roundTickValue(time));
|
|
105
|
+
appendMinorTicks(minor, time, minorInterval, subdivisions, tickRange, maxTicks, major.length);
|
|
106
|
+
}
|
|
107
|
+
return { major, minor };
|
|
108
|
+
}
|
|
109
|
+
export function formatTimelineTickLabel(time, duration, majorInterval) {
|
|
110
|
+
if (!Number.isFinite(time))
|
|
111
|
+
return "00:00";
|
|
112
|
+
const safeTime = Math.max(0, time);
|
|
113
|
+
if (majorInterval < 0.1) {
|
|
114
|
+
const totalHundredths = Math.round(safeTime * 100);
|
|
115
|
+
const wholeSeconds = Math.floor(totalHundredths / 100);
|
|
116
|
+
const hundredth = totalHundredths % 100;
|
|
117
|
+
return `${formatTime(wholeSeconds)}.${hundredth.toString().padStart(2, "0")}`;
|
|
118
|
+
}
|
|
119
|
+
if (majorInterval < 1) {
|
|
120
|
+
const totalTenths = Math.round(safeTime * 10);
|
|
121
|
+
const wholeSeconds = Math.floor(totalTenths / 10);
|
|
122
|
+
const tenth = totalTenths % 10;
|
|
123
|
+
return `${formatTime(wholeSeconds)}.${tenth}`;
|
|
124
|
+
}
|
|
125
|
+
if (duration >= 3600 || safeTime >= 3600) {
|
|
126
|
+
const totalSeconds = Math.floor(safeTime);
|
|
127
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
128
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
129
|
+
const seconds = totalSeconds % 60;
|
|
130
|
+
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
131
|
+
}
|
|
132
|
+
return formatTime(safeTime);
|
|
133
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** One owner for continuous inspector edits: preview freely, persist once. */
|
|
2
|
+
export declare function useInspectorGestureTransaction<T>({ sourceValue, onPreview, onCommit, }: {
|
|
3
|
+
sourceValue: T;
|
|
4
|
+
onPreview: (value: T) => void;
|
|
5
|
+
onCommit: (value: T) => void | Promise<unknown>;
|
|
6
|
+
}): {
|
|
7
|
+
begin: () => void;
|
|
8
|
+
preview: (value: T) => void;
|
|
9
|
+
settle: () => void;
|
|
10
|
+
cancel: () => void;
|
|
11
|
+
activeRef: import("react").RefObject<{
|
|
12
|
+
before: T;
|
|
13
|
+
latest: T;
|
|
14
|
+
} | null>;
|
|
15
|
+
};
|
|
16
|
+
export declare function useInspectorGestureDraft<T>({ sourceValue, onPreview, onCommit, }: {
|
|
17
|
+
sourceValue: T;
|
|
18
|
+
onPreview: (value: T) => void;
|
|
19
|
+
onCommit: (value: T) => void | Promise<unknown>;
|
|
20
|
+
}): {
|
|
21
|
+
draft: T;
|
|
22
|
+
setDraft: import("react").Dispatch<import("react").SetStateAction<T>>;
|
|
23
|
+
transaction: {
|
|
24
|
+
begin: () => void;
|
|
25
|
+
preview: (value: T) => void;
|
|
26
|
+
settle: () => void;
|
|
27
|
+
cancel: () => void;
|
|
28
|
+
activeRef: import("react").RefObject<{
|
|
29
|
+
before: T;
|
|
30
|
+
latest: T;
|
|
31
|
+
} | null>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from heygen-com/hyperframes (Apache-2.0),
|
|
3
|
+
* packages/studio @ 1d9b3673fade7511dd13cde5c3776a0e45e47ca3.
|
|
4
|
+
* Upstream file: components/editor/useInspectorGestureTransaction.ts. Modifications: none — copied unmodified.
|
|
5
|
+
* See src/editor/NOTICES.md for the licence text and provenance.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
function isPromiseCommit(result) {
|
|
9
|
+
return Boolean(result && typeof result.then === "function");
|
|
10
|
+
}
|
|
11
|
+
/** One owner for continuous inspector edits: preview freely, persist once. */
|
|
12
|
+
export function useInspectorGestureTransaction({ sourceValue, onPreview, onCommit, }) {
|
|
13
|
+
const sourceRef = useRef(sourceValue);
|
|
14
|
+
const activeRef = useRef(null);
|
|
15
|
+
const previewRef = useRef(onPreview);
|
|
16
|
+
const commitRef = useRef(onCommit);
|
|
17
|
+
const generationRef = useRef(0);
|
|
18
|
+
const pendingRef = useRef(null);
|
|
19
|
+
const awaitingSourceAckRef = useRef(null);
|
|
20
|
+
const lastSourceValueRef = useRef(sourceValue);
|
|
21
|
+
if (!Object.is(lastSourceValueRef.current, sourceValue)) {
|
|
22
|
+
lastSourceValueRef.current = sourceValue;
|
|
23
|
+
const matchesSourceAck = Object.is(awaitingSourceAckRef.current?.value, sourceValue);
|
|
24
|
+
const matchesOptimisticValue = (activeRef.current && Object.is(activeRef.current.latest, sourceValue)) ||
|
|
25
|
+
(pendingRef.current && Object.is(pendingRef.current.latest, sourceValue)) ||
|
|
26
|
+
matchesSourceAck;
|
|
27
|
+
if (matchesSourceAck)
|
|
28
|
+
awaitingSourceAckRef.current = null;
|
|
29
|
+
if (!matchesOptimisticValue) {
|
|
30
|
+
generationRef.current += 1;
|
|
31
|
+
activeRef.current = null;
|
|
32
|
+
pendingRef.current = null;
|
|
33
|
+
awaitingSourceAckRef.current = null;
|
|
34
|
+
sourceRef.current = sourceValue;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
sourceRef.current = sourceValue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
previewRef.current = onPreview;
|
|
41
|
+
commitRef.current = onCommit;
|
|
42
|
+
const begin = useCallback(() => {
|
|
43
|
+
if (!activeRef.current) {
|
|
44
|
+
generationRef.current += 1;
|
|
45
|
+
activeRef.current = { before: sourceRef.current, latest: sourceRef.current };
|
|
46
|
+
}
|
|
47
|
+
}, []);
|
|
48
|
+
const preview = useCallback((value) => {
|
|
49
|
+
if (!activeRef.current) {
|
|
50
|
+
generationRef.current += 1;
|
|
51
|
+
activeRef.current = { before: sourceRef.current, latest: sourceRef.current };
|
|
52
|
+
}
|
|
53
|
+
activeRef.current.latest = value;
|
|
54
|
+
previewRef.current(value);
|
|
55
|
+
}, []);
|
|
56
|
+
const rollbackCommit = useCallback((active, generation) => {
|
|
57
|
+
if (generation !== generationRef.current)
|
|
58
|
+
return;
|
|
59
|
+
pendingRef.current = null;
|
|
60
|
+
if (awaitingSourceAckRef.current?.generation === generation) {
|
|
61
|
+
awaitingSourceAckRef.current = null;
|
|
62
|
+
}
|
|
63
|
+
sourceRef.current = active.before;
|
|
64
|
+
previewRef.current(active.before);
|
|
65
|
+
}, []);
|
|
66
|
+
const settle = useCallback(() => {
|
|
67
|
+
const active = activeRef.current;
|
|
68
|
+
activeRef.current = null;
|
|
69
|
+
if (active && !Object.is(active.before, active.latest)) {
|
|
70
|
+
const generation = ++generationRef.current;
|
|
71
|
+
sourceRef.current = active.latest;
|
|
72
|
+
pendingRef.current = active;
|
|
73
|
+
awaitingSourceAckRef.current = { generation, value: active.latest };
|
|
74
|
+
try {
|
|
75
|
+
const result = commitRef.current(active.latest);
|
|
76
|
+
if (isPromiseCommit(result)) {
|
|
77
|
+
void result.then(() => {
|
|
78
|
+
if (generation === generationRef.current)
|
|
79
|
+
pendingRef.current = null;
|
|
80
|
+
}, () => rollbackCommit(active, generation));
|
|
81
|
+
}
|
|
82
|
+
else if (generation === generationRef.current) {
|
|
83
|
+
pendingRef.current = null;
|
|
84
|
+
// Synchronous inspector consumers historically restore their preview
|
|
85
|
+
// after persisting (color pickers close, curves release the pointer).
|
|
86
|
+
// Async source mutations keep the optimistic preview until the write
|
|
87
|
+
// resolves so they do not flash back to the baseline while pending.
|
|
88
|
+
previewRef.current(active.before);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
rollbackCommit(active, generation);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, [rollbackCommit]);
|
|
96
|
+
const cancel = useCallback(() => {
|
|
97
|
+
generationRef.current += 1;
|
|
98
|
+
const active = activeRef.current;
|
|
99
|
+
activeRef.current = null;
|
|
100
|
+
if (active && !Object.is(active.before, active.latest)) {
|
|
101
|
+
sourceRef.current = active.before;
|
|
102
|
+
previewRef.current(active.before);
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
useEffect(() => cancel, [cancel]);
|
|
106
|
+
return { begin, preview, settle, cancel, activeRef };
|
|
107
|
+
}
|
|
108
|
+
export function useInspectorGestureDraft({ sourceValue, onPreview, onCommit, }) {
|
|
109
|
+
const [draft, setDraft] = useState(sourceValue);
|
|
110
|
+
const transaction = useInspectorGestureTransaction({
|
|
111
|
+
sourceValue,
|
|
112
|
+
onPreview: (next) => {
|
|
113
|
+
setDraft(next);
|
|
114
|
+
onPreview(next);
|
|
115
|
+
},
|
|
116
|
+
onCommit: (next) => {
|
|
117
|
+
setDraft(next);
|
|
118
|
+
return onCommit(next);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!transaction.activeRef.current)
|
|
123
|
+
setDraft(sourceValue);
|
|
124
|
+
}, [sourceValue, transaction.activeRef]);
|
|
125
|
+
return { draft, setDraft, transaction };
|
|
126
|
+
}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
declare module "*.css";
|
|
4
|
+
declare module "*.svg" {
|
|
5
|
+
const src: string;
|
|
6
|
+
export default src;
|
|
7
|
+
}
|
|
8
|
+
declare module "*.png" {
|
|
9
|
+
const src: string;
|
|
10
|
+
export default src;
|
|
11
|
+
}
|
|
12
|
+
declare module "*.txt?raw" {
|
|
13
|
+
const text: string;
|
|
14
|
+
export default text;
|
|
15
|
+
}
|
|
16
|
+
declare module "*.tsv?raw" {
|
|
17
|
+
const text: string;
|
|
18
|
+
export default text;
|
|
19
|
+
}
|
|
20
|
+
declare module "*?url" {
|
|
21
|
+
const src: string;
|
|
22
|
+
export default src;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Build-time switches a host may define (Vite `define`); every read is typeof-guarded.
|
|
26
|
+
declare const __RIGHTKIT_QA__: boolean | undefined;
|
|
27
|
+
declare const __RIGHTKIT_DEMO__: boolean | undefined;
|
|
28
|
+
declare const __RIGHTKIT_MOCK__: boolean | undefined;
|
|
29
|
+
declare const __APP_VERSION__: string | undefined;
|
|
30
|
+
|
|
31
|
+
interface Window {
|
|
32
|
+
/** UI QA only: the data source a QA shell selects. */
|
|
33
|
+
__RIGHTKIT_QA_DATA_SOURCE__?: "engine";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare module "@rightkit-host/legal/legal-manifest.json" {
|
|
37
|
+
const manifest: {
|
|
38
|
+
documents: Array<{ id: string; role: string; title: string; publicUrl: string; version?: string; sha256?: string; [key: string]: unknown }>;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
};
|
|
41
|
+
export default manifest;
|
|
42
|
+
}
|
|
43
|
+
declare module "@rightkit-host/legal/legal-manifest.json?raw" {
|
|
44
|
+
const text: string;
|
|
45
|
+
export default text;
|
|
46
|
+
}
|
|
47
|
+
declare module "@rightkit-host/src/generated/legal-contract.json" {
|
|
48
|
+
const contract: { acceptanceVersion: string; materialSha256: string; [key: string]: unknown };
|
|
49
|
+
export default contract;
|
|
50
|
+
}
|
|
51
|
+
declare module "@rightkit-host/package.json" {
|
|
52
|
+
export const version: string;
|
|
53
|
+
const pkg: { name: string; version: string };
|
|
54
|
+
export default pkg;
|
|
55
|
+
}
|
|
56
|
+
declare module "@rightkit-host/legal/*.md?raw" {
|
|
57
|
+
const text: string;
|
|
58
|
+
export default text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare module "virtual:rightkit-legal" {
|
|
62
|
+
export const LEGAL_DOCUMENTS: readonly { title: string; content: string }[];
|
|
63
|
+
export const LEGAL_VERSION: string;
|
|
64
|
+
export const LEGAL_DOCUMENTS_HASH: string;
|
|
65
|
+
export const LEGAL_RECEIPT_DOCUMENTS: readonly { id: string; role: string; version: string; sha256: string; treatment: string; material: boolean }[];
|
|
66
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type DoctorReport, type EngineStatus } from "../lib/engine.js";
|
|
2
|
+
export declare function subscribeDoctor(listener: () => void): () => void;
|
|
3
|
+
export declare const getDoctor: () => DoctorReport | null;
|
|
4
|
+
/** Polls `engine_status` and loads the doctor report once the engine is ready. */
|
|
5
|
+
export declare function useEngine(): {
|
|
6
|
+
status: EngineStatus | null;
|
|
7
|
+
doctor: DoctorReport | null;
|
|
8
|
+
doctorError: string | null;
|
|
9
|
+
statusError: string | null;
|
|
10
|
+
};
|