@bycrux/editor 0.11.2 → 1.0.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/package.json +3 -1
- package/src/ControlsInfoModal.tsx +251 -61
- package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
- package/src/__tests__/adapter.test.ts +59 -1
- package/src/__tests__/schema-assignability.test.ts +93 -0
- package/src/__tests__/schema.test.ts +15 -0
- package/src/__tests__/video-adapter-contract.test.ts +128 -5
- package/src/carousel/AddElementMenu.tsx +10 -4
- package/src/carousel/CarouselEditor.tsx +46 -8
- package/src/carousel/CarouselRenderModal.tsx +15 -10
- package/src/carousel/OverlayPicker.tsx +10 -3
- package/src/carousel/SlidePropertyPanel.tsx +43 -21
- package/src/components/FilmstripScrubber.tsx +277 -0
- package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
- package/src/engine/__tests__/audio-clock.test.ts +655 -0
- package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
- package/src/engine/__tests__/batch-planner.test.ts +298 -0
- package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
- package/src/engine/__tests__/demux-ranged.test.ts +495 -0
- package/src/engine/__tests__/demux-truncated.test.ts +136 -0
- package/src/engine/__tests__/demux.test.ts +305 -0
- package/src/engine/__tests__/eligibility.test.ts +146 -0
- package/src/engine/__tests__/engine-recovery.test.ts +263 -0
- package/src/engine/__tests__/engine.test.ts +326 -0
- package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
- package/src/engine/__tests__/frame-server.test.ts +326 -0
- package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
- package/src/engine/__tests__/media-loader.test.ts +100 -0
- package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
- package/src/engine/__tests__/scheduler.test.ts +1310 -0
- package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
- package/src/engine/__tests__/scrub-source.test.ts +195 -0
- package/src/engine/__tests__/source-host.test.ts +455 -0
- package/src/engine/__tests__/time-stretch.test.ts +182 -0
- package/src/engine/audio-clock.ts +1179 -0
- package/src/engine/audio-worklet-source.ts +160 -0
- package/src/engine/batch-planner.ts +308 -0
- package/src/engine/debug-hud.tsx +54 -0
- package/src/engine/decode-worker-source.ts +142 -0
- package/src/engine/demux.ts +900 -0
- package/src/engine/eligibility.ts +140 -0
- package/src/engine/frame-server.ts +644 -0
- package/src/engine/index.ts +950 -0
- package/src/engine/media-loader.ts +380 -0
- package/src/engine/mp4box.d.ts +214 -0
- package/src/engine/scheduler.ts +1324 -0
- package/src/engine/scrub-resolve.ts +66 -0
- package/src/engine/scrub-source.ts +496 -0
- package/src/engine/time-stretch.ts +224 -0
- package/src/index.ts +83 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +146 -3
- package/src/state/__tests__/use-project-sync.test.tsx +56 -0
- package/src/state/use-project-sync.ts +17 -0
- package/src/test-setup.ts +32 -0
- package/src/text/FontPicker.tsx +85 -51
- package/src/text/TextFormattingToolbar.tsx +6 -1
- package/src/text/__tests__/FontPicker.options.test.ts +43 -0
- package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
- package/src/theme.ts +108 -3
- package/src/types.ts +601 -22
- package/src/ui/Loader.tsx +64 -0
- package/src/ui/NumberField.tsx +254 -0
- package/src/ui/Slider.tsx +128 -0
- package/src/ui/Tooltip.tsx +118 -0
- package/src/ui/__tests__/NumberField.test.tsx +298 -0
- package/src/ui/__tests__/Slider.test.tsx +150 -0
- package/src/ui/__tests__/Tooltip.test.tsx +105 -0
- package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
- package/src/ui/badge.tsx +12 -4
- package/src/ui/index.ts +6 -0
- package/src/ui/input.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +12 -2
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/usePersistentState.ts +63 -0
- package/src/video/AudioPolishModal.tsx +983 -0
- package/src/video/CaptionListPanel.test.tsx +944 -0
- package/src/video/CaptionListPanel.tsx +1106 -0
- package/src/video/CaptionRegenModal.tsx +37 -9
- package/src/video/CaptionSpecimen.tsx +160 -0
- package/src/video/CaptionStyleGallery.tsx +466 -0
- package/src/video/CommandPalette.tsx +165 -0
- package/src/video/ImageToneMenu.tsx +137 -0
- package/src/video/OverlayInspector.tsx +977 -0
- package/src/video/RenderModal.tsx +1035 -62
- package/src/video/VersionCompare.tsx +258 -0
- package/src/video/VersionPanel.tsx +117 -42
- package/src/video/VideoEditor.tsx +2229 -243
- package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
- package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
- package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
- package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
- package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
- package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
- package/src/video/__tests__/CommandPalette.test.tsx +119 -0
- package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
- package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
- package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
- package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
- package/src/video/__tests__/VersionCompare.test.tsx +182 -0
- package/src/video/__tests__/VersionPanel.test.tsx +279 -0
- package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
- package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
- package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
- package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
- package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
- package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
- package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
- package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
- package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
- package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
- package/src/video/__tests__/VideoEditor.test.tsx +403 -50
- package/src/video/__tests__/audioMagnet.test.ts +158 -0
- package/src/video/__tests__/audioPolish.test.ts +1202 -0
- package/src/video/__tests__/captionActiveWord.test.ts +107 -0
- package/src/video/__tests__/captionLanes.test.ts +262 -0
- package/src/video/__tests__/captionPositioning.test.tsx +8 -3
- package/src/video/__tests__/captionWordFloor.test.ts +228 -0
- package/src/video/__tests__/clipboard-ops.test.ts +430 -0
- package/src/video/__tests__/cuts.insert.test.ts +244 -0
- package/src/video/__tests__/cuts.test.ts +1213 -34
- package/src/video/__tests__/export-limits.test.ts +195 -0
- package/src/video/__tests__/hover-scrub.test.ts +163 -0
- package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
- package/src/video/__tests__/keyframeOps.test.ts +643 -0
- package/src/video/__tests__/keymap.test.tsx +171 -0
- package/src/video/__tests__/render-progress.test.tsx +20 -4
- package/src/video/__tests__/shuttle.test.ts +210 -0
- package/src/video/__tests__/source-preview.test.ts +60 -0
- package/src/video/__tests__/timecode.test.ts +77 -0
- package/src/video/__tests__/use-report-context.test.tsx +101 -0
- package/src/video/audioMagnet.ts +72 -0
- package/src/video/audioPolish.ts +774 -0
- package/src/video/captionActiveWord.ts +74 -0
- package/src/video/captionLanes.ts +202 -0
- package/src/video/captionRepair.ts +9 -5
- package/src/video/captionStyleDefaults.ts +100 -0
- package/src/video/captionWordFloor.ts +83 -0
- package/src/video/clipboard-ops.ts +377 -0
- package/src/video/cuts.ts +713 -37
- package/src/video/export-limits.ts +102 -0
- package/src/video/hover-scrub.ts +102 -0
- package/src/video/imageTone.ts +58 -0
- package/src/video/imageToneExamples.ts +10 -0
- package/src/video/keyframeOps.ts +384 -0
- package/src/video/keymap.ts +146 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
- package/src/video/panels/LeftPanelTabs.tsx +187 -0
- package/src/video/panels/OverlayContentPanel.tsx +351 -0
- package/src/video/panels/TabNav.tsx +60 -0
- package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
- package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
- package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
- package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
- package/src/video/preview/CaptionPreview.tsx +57 -69
- package/src/video/preview/EngineSurface.tsx +103 -0
- package/src/video/preview/OverlayItemsLayer.tsx +315 -103
- package/src/video/preview/PreviewPlayer.tsx +337 -56
- package/src/video/preview/SocialPreviewMenu.tsx +214 -0
- package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
- package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
- package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
- package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
- package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
- package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
- package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
- package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
- package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
- package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
- package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
- package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
- package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
- package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
- package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
- package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
- package/src/video/preview/audio-context.ts +111 -0
- package/src/video/preview/captionDragState.ts +91 -1
- package/src/video/preview/proxySupport.ts +86 -0
- package/src/video/preview/transformStyle.ts +22 -12
- package/src/video/preview/useDragOverlay.ts +92 -18
- package/src/video/preview/useEnginePlayback.ts +625 -0
- package/src/video/preview/useVideoPlayback.ts +211 -167
- package/src/video/sdrCurves.ts +56 -0
- package/src/video/shuttle.ts +159 -0
- package/src/video/source-preview.ts +66 -0
- package/src/video/timecode.ts +59 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +46 -174
- package/src/video/timeline/SpeedControl.tsx +95 -0
- package/src/video/timeline/Timeline.tsx +1061 -328
- package/src/video/timeline/TimelineContext.ts +17 -10
- package/src/video/timeline/TrackGutter.tsx +560 -0
- package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
- package/src/video/timeline/VolumeControl.tsx +113 -0
- package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
- package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
- package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
- package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
- package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
- package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
- package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
- package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
- package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
- package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
- package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
- package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
- package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
- package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
- package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
- package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
- package/src/video/timeline/__tests__/placement.test.ts +278 -0
- package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
- package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
- package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
- package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
- package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
- package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
- package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
- package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
- package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
- package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
- package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
- package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
- package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
- package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
- package/src/video/timeline/canvas/clip-bands.ts +56 -0
- package/src/video/timeline/canvas/draw.ts +2187 -0
- package/src/video/timeline/canvas/fade-curve.ts +111 -0
- package/src/video/timeline/canvas/filmstrips.ts +418 -0
- package/src/video/timeline/canvas/hit-test.ts +501 -0
- package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
- package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
- package/src/video/timeline/canvas/snap.ts +232 -0
- package/src/video/timeline/canvas/viewport.ts +457 -0
- package/src/video/timeline/canvas/waveforms.ts +664 -0
- package/src/video/timeline/makeCaptionEdit.ts +5 -1
- package/src/video/timeline/multiSelectOps.ts +214 -55
- package/src/video/timeline/placement.ts +282 -0
- package/src/video/timeline/timeline-model.ts +919 -0
- package/src/video/timeline/useItemDragDrop.ts +151 -178
- package/src/video/timeline/useTimelineZoom.ts +25 -60
- package/src/video/timeline/utils.ts +0 -12
- package/src/video/use-report-context.ts +75 -0
- package/src/video/preview/OverlayPropsModal.tsx +0 -292
- package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
- package/src/video/timeline/AudioTrackRow.tsx +0 -404
- package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
- package/src/video/timeline/CaptionTrackRow.tsx +0 -235
- package/src/video/timeline/PlayheadLine.tsx +0 -18
- package/src/video/timeline/TranscriptModal.tsx +0 -70
- package/src/video/timeline/TranscriptPanel.tsx +0 -273
- package/src/video/timeline/VisualTrackRow.tsx +0 -300
- package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
- package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
- package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
- package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
- package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
package/src/types.ts
CHANGED
|
@@ -23,6 +23,8 @@ import type {
|
|
|
23
23
|
OverlayElement,
|
|
24
24
|
Captions,
|
|
25
25
|
} from './schema'
|
|
26
|
+
import type { ImageTone } from './video/imageTone'
|
|
27
|
+
import type { SourcePreviewStore } from './video/source-preview'
|
|
26
28
|
|
|
27
29
|
// ── Overlay compiler ─────────────────────────────────────────────────────────
|
|
28
30
|
|
|
@@ -55,25 +57,86 @@ export type { Project, Slide, CarouselElement, ImageElement, OverlayElement }
|
|
|
55
57
|
*/
|
|
56
58
|
export type RenderEvent =
|
|
57
59
|
| { type: 'log'; message: string }
|
|
58
|
-
|
|
60
|
+
/**
|
|
61
|
+
* `outputPath` is always the primary (master) file. `outputPaths` is present
|
|
62
|
+
* only when the render emitted more than one file (an `--export both` HDR
|
|
63
|
+
* render: master first, derived SDR sibling second) AND the host was able to
|
|
64
|
+
* learn the full list — hosts that can't simply omit it.
|
|
65
|
+
*/
|
|
66
|
+
| { type: 'done'; outputPath: string; outputPaths?: string[] }
|
|
59
67
|
| { type: 'error'; message: string }
|
|
60
68
|
|
|
61
69
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
70
|
+
* Which file(s) a render produces for an HDR project:
|
|
71
|
+
* - 'auto' — one master in the project's own color space (an HDR project stays
|
|
72
|
+
* HDR). The historical behavior and the default.
|
|
73
|
+
* - 'sdr' — one standard-range file, tone-mapped through `sdrCurve`.
|
|
74
|
+
* - 'both' — the HDR master plus an SDR sibling derived from it.
|
|
75
|
+
* SDR projects are unaffected: every value renders the same single SDR file.
|
|
76
|
+
*/
|
|
77
|
+
export type RenderExport = 'auto' | 'sdr' | 'both'
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Options for a render request. Hosts ignore fields they don't support, and
|
|
81
|
+
* omitting the whole object keeps a host's default behavior.
|
|
66
82
|
*/
|
|
67
83
|
export interface RenderOptions {
|
|
68
84
|
/** Output scale multiplier (1 = native resolution). */
|
|
69
85
|
scale?: number
|
|
86
|
+
/** Which file(s) an HDR project exports. Defaults to 'auto' host-side. */
|
|
87
|
+
export?: RenderExport
|
|
88
|
+
/**
|
|
89
|
+
* Id of the HDR→SDR tone curve used for any SDR output (see
|
|
90
|
+
* `video/sdrCurves.ts` for the descriptors, and the `curves` keys in
|
|
91
|
+
* montaj_assets/luts/looks.json for what a host validates against). Typed as
|
|
92
|
+
* `string` so a host can ship curves the package doesn't know about; ignored
|
|
93
|
+
* when the render produces no SDR file.
|
|
94
|
+
*/
|
|
95
|
+
sdrCurve?: string
|
|
96
|
+
/**
|
|
97
|
+
* Output base filename (no extension) for the rendered file, from the export
|
|
98
|
+
* dialog's Name field. Hosts that support naming the output read this; others
|
|
99
|
+
* ignore it. Omitted → the host's default naming applies.
|
|
100
|
+
*/
|
|
101
|
+
name?: string
|
|
102
|
+
/**
|
|
103
|
+
* Poster/cover frame timecode in project-timeline seconds, from the export
|
|
104
|
+
* dialog's cover picker. Hosts that produce a cover read this; others ignore
|
|
105
|
+
* it. Omitted → the host's default (e.g. first frame) applies.
|
|
106
|
+
*/
|
|
107
|
+
cover?: number
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Options for a sample-frame request. Only the SDR curve for now — the frame
|
|
112
|
+
* itself is identified by the project id and timestamp.
|
|
113
|
+
*/
|
|
114
|
+
export interface SampleFrameOptions {
|
|
115
|
+
/** Tone curve to map an HDR project's frame through (see `RenderOptions.sdrCurve`). */
|
|
116
|
+
sdrCurve?: string
|
|
117
|
+
/**
|
|
118
|
+
* Prefer each clip's SDR proxy over the full-resolution master for a fast
|
|
119
|
+
* preview (cover posters, the cover-frame grid). The proxy is already SDR, so
|
|
120
|
+
* this is mutually exclusive with `sdrCurve` — a proxy frame cannot show a
|
|
121
|
+
* per-curve HDR→SDR grade. Adapters that can't sample from a proxy may ignore
|
|
122
|
+
* it. Falls back to the master per clip when no proxy exists.
|
|
123
|
+
*/
|
|
124
|
+
preferProxy?: boolean
|
|
70
125
|
}
|
|
71
126
|
|
|
72
127
|
/**
|
|
73
128
|
* Coarse phase of an async render pipeline. Ordered roughly by execution order;
|
|
74
|
-
* hosts may skip phases that don't apply to their pipeline
|
|
129
|
+
* hosts may skip phases that don't apply to their pipeline — `sdr_derive` in
|
|
130
|
+
* particular only runs when an HDR project exports SDR (`export: 'sdr' | 'both'`).
|
|
75
131
|
*/
|
|
76
|
-
export type RenderPhase =
|
|
132
|
+
export type RenderPhase =
|
|
133
|
+
| 'preparing'
|
|
134
|
+
| 'rendering'
|
|
135
|
+
| 'captions'
|
|
136
|
+
| 'encoding'
|
|
137
|
+
| 'sdr_derive'
|
|
138
|
+
| 'saving'
|
|
139
|
+
| 'done'
|
|
77
140
|
|
|
78
141
|
/**
|
|
79
142
|
* Point-in-time snapshot of an async render's progress. Returned by
|
|
@@ -168,6 +231,19 @@ export interface VersionEntry {
|
|
|
168
231
|
timestamp: string
|
|
169
232
|
}
|
|
170
233
|
|
|
234
|
+
// ── Host path resolution ─────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolves a host-internal asset path into a displayable URL (e.g. Montaj's
|
|
238
|
+
* `/api/files?path=…`). This is `EditorAdapter.fileUrl`'s signature, named so
|
|
239
|
+
* the components it is threaded down to as a prop can type it without
|
|
240
|
+
* depending on the whole adapter: the canvas timeline takes it to display the
|
|
241
|
+
* `path`s that come back on `WaveformChunk` and `FilmstripSheet` below.
|
|
242
|
+
* Optional wherever it is a prop — a host that omits it gets identity, and the
|
|
243
|
+
* feature that needed the URL degrades to nothing rather than erroring.
|
|
244
|
+
*/
|
|
245
|
+
export type ResolveFilePath = (path: string) => string
|
|
246
|
+
|
|
171
247
|
// ── Waveform chunks (optional capability) ─────────────────────────────────────
|
|
172
248
|
|
|
173
249
|
/**
|
|
@@ -182,6 +258,156 @@ export interface WaveformChunk {
|
|
|
182
258
|
end: number
|
|
183
259
|
}
|
|
184
260
|
|
|
261
|
+
// ── Waveform peaks & filmstrips (optional capability) ─────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Zoom-bucketed audio peak data for a scrubbable waveform view — the
|
|
265
|
+
* canvas-rendered replacement for `WaveformChunk`'s fixed PNGs. `peaks` is
|
|
266
|
+
* interleaved `[min, max, min, max, ...]`, one pair per sample bucket, values
|
|
267
|
+
* in int16 range. `samplesPerSecond` is normally one of the requested
|
|
268
|
+
* resolution buckets (50 | 200 | 800) but may come back as a lower,
|
|
269
|
+
* non-bucketed number when the host's total-samples clamp forced a step-down
|
|
270
|
+
* for a long window — the actual resolution used, never silently truncated,
|
|
271
|
+
* hence `number` rather than a literal union. `start`/`duration` echo the
|
|
272
|
+
* decoded source-time window in seconds. Maps to Montaj's `waveform_peaks`
|
|
273
|
+
* step.
|
|
274
|
+
*/
|
|
275
|
+
export interface PeaksData {
|
|
276
|
+
samplesPerSecond: number
|
|
277
|
+
start: number
|
|
278
|
+
duration: number
|
|
279
|
+
peaks: number[]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The three bucketed resolutions `getWaveformPeaks` may request. */
|
|
283
|
+
export type PeaksResolution = 50 | 200 | 800
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Args for `EditorAdapter.getWaveformPeaks`. `projectId` scopes the host's
|
|
287
|
+
* output cache (mirrors `getWaveformChunks`'s explicit `projectId`, since a
|
|
288
|
+
* single adapter instance isn't itself project-scoped); `src` is the
|
|
289
|
+
* source-identity path — a proxy or original file, per the caller's
|
|
290
|
+
* input-selection policy (see the Montaj adapter implementation comment);
|
|
291
|
+
* `samplesPerSecond` is the requested resolution bucket; `start`/`duration`
|
|
292
|
+
* optionally window the request to part of the source (omit for the whole
|
|
293
|
+
* file).
|
|
294
|
+
*/
|
|
295
|
+
export interface GetWaveformPeaksArgs {
|
|
296
|
+
projectId: string
|
|
297
|
+
src: string
|
|
298
|
+
samplesPerSecond: PeaksResolution
|
|
299
|
+
start?: number
|
|
300
|
+
duration?: number
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* One tiled contact sheet in a `FilmstripIndex`. `path` is a host-resolvable
|
|
305
|
+
* image path (route through `fileUrl` to display — same convention as
|
|
306
|
+
* `WaveformChunk.path`). `tiles` maps each cell to its source timestamp `t`
|
|
307
|
+
* (seconds) and its `row`/`col` position in the sheet's `cols` x `rows` grid.
|
|
308
|
+
*/
|
|
309
|
+
export interface FilmstripSheet {
|
|
310
|
+
path: string
|
|
311
|
+
cols: number
|
|
312
|
+
rows: number
|
|
313
|
+
tiles: Array<{ t: number; row: number; col: number }>
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* A video's uniform time-grid thumbnail strip, tiled across one or more
|
|
318
|
+
* `FilmstripSheet`s. `interval` is the seconds between consecutive tiles
|
|
319
|
+
* (uniform across the whole filmstrip); `tileWidth` is the pixel width every
|
|
320
|
+
* tile was scaled to. Maps to Montaj's `filmstrip` step.
|
|
321
|
+
*/
|
|
322
|
+
export interface FilmstripIndex {
|
|
323
|
+
sheets: FilmstripSheet[]
|
|
324
|
+
interval: number
|
|
325
|
+
tileWidth: number
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Args for `EditorAdapter.getFilmstrip`. `projectId` scopes the host's output
|
|
330
|
+
* cache; `src` is the source-identity path (proxy-only, per the video
|
|
331
|
+
* timeline's filmstrip policy — see the Montaj adapter implementation
|
|
332
|
+
* comment). The grid params mirror the `filmstrip` step's own knobs and are
|
|
333
|
+
* optional — omit to use the step's defaults (`max-tiles=100`,
|
|
334
|
+
* `min-interval=1.0`, `tile-width=160`).
|
|
335
|
+
*/
|
|
336
|
+
export interface GetFilmstripArgs {
|
|
337
|
+
projectId: string
|
|
338
|
+
src: string
|
|
339
|
+
maxTiles?: number
|
|
340
|
+
minInterval?: number
|
|
341
|
+
tileWidth?: number
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Audio polish (optional capability) ────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Args for `EditorAdapter.analyzeAudioPolish`. `projectId` scopes the host's
|
|
348
|
+
* job/output cache (mirrors `getWaveformPeaks`/`getFilmstrip`); `piece`
|
|
349
|
+
* selects which of Montaj's four audio-polish steps to run (or the
|
|
350
|
+
* `'silence-check'` dry run — see `AudioPolishAnalysis`); `src` is the
|
|
351
|
+
* source-identity path being analyzed. `window`, when given, restricts
|
|
352
|
+
* analysis to a slice of the source — in **source** seconds, not timeline
|
|
353
|
+
* seconds (see `AudioPolishAnalysis` for why that distinction matters).
|
|
354
|
+
* `options` are piece-specific knobs: `language`/`model` steer `fillers`'
|
|
355
|
+
* speech recognition, `targetLufs` steers `loudness`'s gain calculation, and
|
|
356
|
+
* `maxWordGap`/`sentenceEdge` steer `silence`/`silence-check`'s gap-merging
|
|
357
|
+
* heuristics.
|
|
358
|
+
*/
|
|
359
|
+
export interface AnalyzeAudioPolishArgs {
|
|
360
|
+
projectId: string
|
|
361
|
+
piece: 'silence' | 'fillers' | 'loudness' | 'voice' | 'silence-check'
|
|
362
|
+
src: string
|
|
363
|
+
window?: { in: number; out: number }
|
|
364
|
+
options?: {
|
|
365
|
+
/**
|
|
366
|
+
* Speech-recognition language hint for `fillers` (e.g. `'en'`). Defaults
|
|
367
|
+
* to `'en'` at the call site and should be surfaced as a visible user
|
|
368
|
+
* control — a wrong language silently corrupts detection rather than
|
|
369
|
+
* failing loudly.
|
|
370
|
+
*/
|
|
371
|
+
language?: string
|
|
372
|
+
model?: string
|
|
373
|
+
targetLufs?: number
|
|
374
|
+
maxWordGap?: number
|
|
375
|
+
sentenceEdge?: number
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Result of `EditorAdapter.analyzeAudioPolish`, discriminated on `piece`.
|
|
381
|
+
* **Every time in here is source time** — an offset into the source file
|
|
382
|
+
* named by the request's `src`, never timeline time. Mixing the two is the
|
|
383
|
+
* single easiest way to misuse this type; a caller must convert through the
|
|
384
|
+
* clip's own timeline↔source mapping before applying a removal/keep to the
|
|
385
|
+
* project.
|
|
386
|
+
*
|
|
387
|
+
* - `'silence'` / `'fillers'` — `removals`, each a source-time span to cut,
|
|
388
|
+
* with optional `text` (the recognized words, for `fillers`).
|
|
389
|
+
* - `'silence-check'` — `keeps`, the source-time spans that WOULD survive
|
|
390
|
+
* silence removal at the current settings — the inverse framing of
|
|
391
|
+
* `'silence'`, for previewing a threshold before committing to it.
|
|
392
|
+
* - `'loudness'` — the measured integrated/true-peak/LRA loudness, the
|
|
393
|
+
* requested target, and the gain in dB needed to reach it.
|
|
394
|
+
* - `'voice'` — the isolated vocal track: `vocalsPath` is the host path,
|
|
395
|
+
* `url` a directly displayable/fetchable URL for it (same convention as
|
|
396
|
+
* `getSampleFrame`'s `url`).
|
|
397
|
+
*/
|
|
398
|
+
export type AudioPolishAnalysis =
|
|
399
|
+
| { piece: 'silence' | 'fillers'; removals: Array<{ start: number; end: number; text?: string }> }
|
|
400
|
+
| { piece: 'silence-check'; keeps: Array<[number, number]> }
|
|
401
|
+
| {
|
|
402
|
+
piece: 'loudness'
|
|
403
|
+
measuredI: number
|
|
404
|
+
measuredTP: number
|
|
405
|
+
measuredLRA: number
|
|
406
|
+
targetI: number
|
|
407
|
+
gainDb: number
|
|
408
|
+
}
|
|
409
|
+
| { piece: 'voice'; vocalsPath: string; url: string }
|
|
410
|
+
|
|
185
411
|
// ── Media (optional capability) ───────────────────────────────────────────────
|
|
186
412
|
|
|
187
413
|
/**
|
|
@@ -206,8 +432,93 @@ export interface MediaItem {
|
|
|
206
432
|
name?: string
|
|
207
433
|
}
|
|
208
434
|
|
|
435
|
+
// ── Footage bin drag-and-drop (optional capability) ───────────────────────────
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The drag payload for dropping a bin clip onto the timeline. A subset of
|
|
439
|
+
* `VisualItem`'s fields — just enough for the timeline drop target to insert a
|
|
440
|
+
* new clip without round-tripping through the host.
|
|
441
|
+
*/
|
|
442
|
+
export interface FootageDropPayload {
|
|
443
|
+
src: string
|
|
444
|
+
proxySrc?: string
|
|
445
|
+
sourceDuration: number
|
|
446
|
+
sourceWidth?: number
|
|
447
|
+
sourceHeight?: number
|
|
448
|
+
name?: string
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* The custom drag-and-drop MIME type carrying a `FootageDropPayload` JSON
|
|
453
|
+
* string from the footage bin to the timeline drop target.
|
|
454
|
+
*/
|
|
455
|
+
export const FOOTAGE_DND_MIME = 'application/x-montaj-footage'
|
|
456
|
+
|
|
457
|
+
// ── Filesystem drops onto the timeline (optional capability) ──────────────────
|
|
458
|
+
// The SECOND way footage reaches the timeline by drag: real files from the OS,
|
|
459
|
+
// dropped straight onto the track surface. Unlike a bin drag (above) the
|
|
460
|
+
// package cannot place these itself — a `File` has no duration, no proxy and no
|
|
461
|
+
// host-resolvable path until the host has probed and ingested it — so the
|
|
462
|
+
// package's half is only "where did they drop it, and what did they drop";
|
|
463
|
+
// everything after that is the host's job, and it reports progress back as the
|
|
464
|
+
// `PendingDrop` ghosts below.
|
|
465
|
+
|
|
466
|
+
/** What a filesystem drop reports about ITSELF: where the pointer released on
|
|
467
|
+
* the timeline, which row it released over, and the ripple/magnet mode
|
|
468
|
+
* captured at that instant. Named once here — `onImportFilesToTimeline`
|
|
469
|
+
* below, `Timeline.tsx`, and `TimelineCanvas.tsx` all threaded the same
|
|
470
|
+
* shape as an inline object literal, three chances for it to drift — and
|
|
471
|
+
* re-exported from `index.ts` so a host (`timelineImport.ts`) can name it
|
|
472
|
+
* too instead of keeping its own duplicate. */
|
|
473
|
+
export interface TimelineDropPlacement {
|
|
474
|
+
atTime: number
|
|
475
|
+
preferredTrackIndex: number
|
|
476
|
+
ripple: boolean
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** A filesystem file mid-import from a timeline drop, drawn as a ghost band at
|
|
480
|
+
* the drop point until the host's import resolves. */
|
|
481
|
+
export interface PendingDrop {
|
|
482
|
+
/** Host-owned id, so the host can retract this ghost when its import
|
|
483
|
+
* resolves or fails. */
|
|
484
|
+
id: string
|
|
485
|
+
/** Where the ghost band starts, in timeline seconds. */
|
|
486
|
+
atTime: number
|
|
487
|
+
/** The band's length in seconds — the host's fast local probe of the file. */
|
|
488
|
+
durationSec: number
|
|
489
|
+
/** Which video row it is drawn on, in normalized track order. -1 → the base
|
|
490
|
+
* video row. */
|
|
491
|
+
trackIndex: number
|
|
492
|
+
/** The clip will land on a NEW video track (no existing row matches
|
|
493
|
+
* `trackIndex` — it names a row that does not exist yet, per
|
|
494
|
+
* `resolveDropTrackIndex`'s own doc). Draw the ghost on a FRESH row where
|
|
495
|
+
* that track will appear once ingest finishes and `placeDroppedClip`
|
|
496
|
+
* actually creates it, not on the base video row: the base row may already
|
|
497
|
+
* carry real footage, and a ghost sitting on top of it reads as
|
|
498
|
+
* overlapping a clip that has nothing to do with this drop. Absent/false →
|
|
499
|
+
* `trackIndex` names (or falls back to) an existing row, drawn exactly as
|
|
500
|
+
* before. */
|
|
501
|
+
newTrack?: boolean
|
|
502
|
+
/** Filename, drawn inside the band. */
|
|
503
|
+
label?: string
|
|
504
|
+
}
|
|
505
|
+
|
|
209
506
|
// ── Adapter ────────────────────────────────────────────────────────────────
|
|
210
507
|
|
|
508
|
+
/**
|
|
509
|
+
* What the editor is looking at right now — ephemeral UI state, never part of
|
|
510
|
+
* the project document. Reported to the host so an agent can resolve "this
|
|
511
|
+
* section" against the actual playhead instead of guessing.
|
|
512
|
+
*/
|
|
513
|
+
export interface EditorContext {
|
|
514
|
+
/** Playhead position in project seconds. */
|
|
515
|
+
playheadSec: number
|
|
516
|
+
/** All selected timeline item ids; [0] is the primary. */
|
|
517
|
+
selectedIds: string[]
|
|
518
|
+
/** Selected caption segment id, if any. */
|
|
519
|
+
selectedCaptionId: string | null
|
|
520
|
+
}
|
|
521
|
+
|
|
211
522
|
/**
|
|
212
523
|
* The contract a host implements to drive the editor. All transport,
|
|
213
524
|
* authentication, and URL-shape concerns live behind this interface; the
|
|
@@ -270,6 +581,19 @@ export interface EditorAdapter<P extends Project = Project> {
|
|
|
270
581
|
*/
|
|
271
582
|
listMedia?(scope: MediaScope): Promise<MediaItem[]>
|
|
272
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Optional: kick a background ingest of a new source clip (probe →
|
|
586
|
+
* normalize to the project's color space → proxy → register in
|
|
587
|
+
* `project.sources`). `input` is either a host-resolvable path already on
|
|
588
|
+
* the host's filesystem, or a `File` the adapter uploads itself. Resolves
|
|
589
|
+
* with a job id the caller can poll. Optional — hosts that don't support
|
|
590
|
+
* post-init ingest omit it.
|
|
591
|
+
*/
|
|
592
|
+
ingestSource?(
|
|
593
|
+
projectId: string,
|
|
594
|
+
input: { path: string } | File,
|
|
595
|
+
): Promise<{ jobId: string }>
|
|
596
|
+
|
|
273
597
|
/**
|
|
274
598
|
* Compile a JSX overlay template file into an `OverlayFactory`.
|
|
275
599
|
* The host supplies this because the compilation pipeline (Babel, fetch
|
|
@@ -362,11 +686,31 @@ export interface EditorAdapter<P extends Project = Project> {
|
|
|
362
686
|
restoreVersion?(id: string, hash: string): Promise<P>
|
|
363
687
|
|
|
364
688
|
/**
|
|
365
|
-
* Optional:
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
|
|
369
|
-
|
|
689
|
+
* Optional: save the current project state as a named version. Maps to
|
|
690
|
+
* Montaj's `POST /api/projects/:id/versions` with `{ name? }`. Returns the
|
|
691
|
+
* updated version list.
|
|
692
|
+
*/
|
|
693
|
+
saveVersion?(id: string, name?: string): Promise<VersionEntry[]>
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Optional: build the URL for a rendered frame from a specific version
|
|
697
|
+
* (git commit hash, or the string `"working"` for the live on-disk state)
|
|
698
|
+
* at time `t` seconds. The URL is used as an `<img src>`; the host serves
|
|
699
|
+
* the PNG. Maps to Montaj's `GET /api/projects/:id/versions/:commit/frame?t=`.
|
|
700
|
+
*/
|
|
701
|
+
versionFrameUrl?(id: string, commit: string, t: number): string
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* RETIRED — the editor no longer calls this. It produced rendered
|
|
705
|
+
* waveform-image chunks (fixed PNG strips) for the DOM timeline's audio
|
|
706
|
+
* rows; those rows are gone and the canvas timeline draws from
|
|
707
|
+
* `getWaveformPeaks` instead, which is now the package's only waveform
|
|
708
|
+
* path. The signature is kept so hosts that still implement it keep
|
|
709
|
+
* compiling, and so a host can go on serving it to its own chrome — but
|
|
710
|
+
* nothing in this package reads the result. Mapped to Montaj's
|
|
711
|
+
* `waveform_image` step; args were the project id, the track id (which
|
|
712
|
+
* namespaced the output cache), the track's source path, and an optional
|
|
713
|
+
* chunk duration in seconds.
|
|
370
714
|
*/
|
|
371
715
|
getWaveformChunks?(
|
|
372
716
|
projectId: string,
|
|
@@ -375,6 +719,48 @@ export interface EditorAdapter<P extends Project = Project> {
|
|
|
375
719
|
chunkDurationS?: number,
|
|
376
720
|
): Promise<WaveformChunk[]>
|
|
377
721
|
|
|
722
|
+
/**
|
|
723
|
+
* Optional: produce zoom-bucketed audio peak data for a scrubbable
|
|
724
|
+
* waveform view. This is the package's ONLY waveform path — the canvas
|
|
725
|
+
* timeline draws every waveform from it (see the retired
|
|
726
|
+
* `getWaveformChunks` above). Input-selection policy is the *caller's*
|
|
727
|
+
* responsibility, not this method's: `item.proxySrc` (proxy only — no
|
|
728
|
+
* fallback to the original) for per-clip waveforms on visual tracks,
|
|
729
|
+
* `track.src` for audio lanes (see the Montaj adapter implementation
|
|
730
|
+
* comment). Maps to Montaj's
|
|
731
|
+
* `waveform_peaks` step. Optional: a host without a peaks step omits it and
|
|
732
|
+
* the editor feature-detects its absence, drawing no waveforms.
|
|
733
|
+
*/
|
|
734
|
+
getWaveformPeaks?(args: GetWaveformPeaksArgs): Promise<PeaksData>
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Optional: produce a uniform time-grid filmstrip (thumbnail strip) for a
|
|
738
|
+
* video source, tiled into one or more contact sheets with a timestamp
|
|
739
|
+
* index. Maps to Montaj's `filmstrip` step. Optional: a host without a
|
|
740
|
+
* filmstrip step omits it and the editor feature-detects its absence,
|
|
741
|
+
* drawing no tile strips or hover-scrub thumbs.
|
|
742
|
+
*/
|
|
743
|
+
getFilmstrip?(args: GetFilmstripArgs): Promise<FilmstripIndex>
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Optional: render one fully composited project frame at `at` (project
|
|
747
|
+
* timeline seconds) and return a directly displayable URL for it — a still
|
|
748
|
+
* the editor can put straight into an `<img>`. `opts.sdrCurve` picks the
|
|
749
|
+
* HDR→SDR tone curve so the same frame can be sampled through each curve for
|
|
750
|
+
* a side-by-side comparison (the RenderModal's curve picker).
|
|
751
|
+
*
|
|
752
|
+
* URL rather than a host path because the resolution rule is the host's:
|
|
753
|
+
* Montaj returns its `/api/files?path=` URL for the produced PNG, a Hub
|
|
754
|
+
* client would return a presigned one. Hosts without a frame sampler omit
|
|
755
|
+
* this; the editor feature-detects its absence and shows the picker without
|
|
756
|
+
* thumbnails. Maps to Montaj's `sample_frame` step.
|
|
757
|
+
*/
|
|
758
|
+
getSampleFrame?(
|
|
759
|
+
projectId: string,
|
|
760
|
+
at: number,
|
|
761
|
+
opts?: SampleFrameOptions,
|
|
762
|
+
): Promise<{ url: string }>
|
|
763
|
+
|
|
378
764
|
/**
|
|
379
765
|
* Optional: invalidate the host's compiled-overlay cache. When `src` is given,
|
|
380
766
|
* only that entry is dropped; hosts may treat a missing `src` as a no-op or a
|
|
@@ -399,8 +785,49 @@ export interface EditorAdapter<P extends Project = Project> {
|
|
|
399
785
|
* the editor patches `project.captions` from the 'done' event. Hosts without a
|
|
400
786
|
* transcription pipeline omit this; the editor feature-detects its absence and
|
|
401
787
|
* hides the "Regenerate captions" control.
|
|
788
|
+
*
|
|
789
|
+
* The 'done' `Captions` REPLACES `project.captions` wholesale, not just its
|
|
790
|
+
* segments in row 0 — a project with more than one caption row (see the
|
|
791
|
+
* `timeline` prop doc above) loses every row but the single fresh one this
|
|
792
|
+
* produces. `CaptionRegenModal` warns before that happens whenever the
|
|
793
|
+
* project has more than one row; there is no partial/per-row regeneration.
|
|
402
794
|
*/
|
|
403
795
|
generateCaptions?(id: string, opts?: GenerateCaptionsOptions): AsyncIterable<CaptionEvent>
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Optional: report the editor's live playhead and selection to the host.
|
|
799
|
+
*
|
|
800
|
+
* Fire-and-forget and already throttled by the editor (see
|
|
801
|
+
* `useReportContext`) — a host must not add its own debounce. Hosts with
|
|
802
|
+
* nowhere to put ephemeral UI state omit this entirely; the editor feature-
|
|
803
|
+
* detects its absence and reports nothing. A rejected promise is swallowed:
|
|
804
|
+
* context sync is a convenience and must never surface as an editor error.
|
|
805
|
+
*/
|
|
806
|
+
reportContext?(id: string, context: EditorContext): Promise<void>
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Optional: analyze a clip's audio for one of four cleanup pieces (or a
|
|
810
|
+
* `'silence-check'` dry run) and return proposed edits for the user to
|
|
811
|
+
* review before applying — detect silence/filler words to trim, measure
|
|
812
|
+
* loudness and the gain needed to hit a target, or isolate vocals. Maps to
|
|
813
|
+
* Montaj's four underlying audio-polish steps.
|
|
814
|
+
*
|
|
815
|
+
* Promise-based, not an async iterable, and deliberately so: these are
|
|
816
|
+
* polled jobs with no log stream, unlike `generateCaptions`'s streaming
|
|
817
|
+
* transcription — modeled instead on `getWaveformPeaks`/`getFilmstrip`.
|
|
818
|
+
*
|
|
819
|
+
* Optional so Hub keeps compiling against the released `@bycrux/editor`
|
|
820
|
+
* unchanged; the UI hides the audio-polish entry point on hosts that omit
|
|
821
|
+
* this, the same `generateCaptions` precedent. Hosts without an
|
|
822
|
+
* audio-polish pipeline simply don't implement it.
|
|
823
|
+
*
|
|
824
|
+
* `args.window` and every time in the result are **source** time — offsets
|
|
825
|
+
* into `args.src`, never timeline time (see `AudioPolishAnalysis`). This is
|
|
826
|
+
* the single easiest way to misuse this method; a caller must convert
|
|
827
|
+
* through the clip's own timeline↔source mapping before applying a
|
|
828
|
+
* removal/keep to the project.
|
|
829
|
+
*/
|
|
830
|
+
analyzeAudioPolish?(args: AnalyzeAudioPolishArgs): Promise<AudioPolishAnalysis>
|
|
404
831
|
}
|
|
405
832
|
|
|
406
833
|
// ── Theme ────────────────────────────────────────────────────────────────────
|
|
@@ -462,6 +889,19 @@ export interface EditorSlots {
|
|
|
462
889
|
exportActions?: ReactNode
|
|
463
890
|
/** Rendered into the editor's assets/media panel area. */
|
|
464
891
|
assetsPanel?: ReactNode
|
|
892
|
+
/**
|
|
893
|
+
* Rendered in the left media column of the CapCut layout. When present, the
|
|
894
|
+
* editor renders the three-column + full-width-timeline layout; otherwise
|
|
895
|
+
* the classic layout is unchanged.
|
|
896
|
+
*/
|
|
897
|
+
mediaPanel?: ReactNode
|
|
898
|
+
/**
|
|
899
|
+
* Rendered in the CapCut layout's right properties panel when nothing is
|
|
900
|
+
* selected, in place of the editor's generic centered "Select an element"
|
|
901
|
+
* empty state. Hosts use it to brand the empty panel (Montaj shows its
|
|
902
|
+
* logo). Absent → the generic default shows. No effect in the classic layout.
|
|
903
|
+
*/
|
|
904
|
+
propertiesEmptyState?: ReactNode
|
|
465
905
|
/**
|
|
466
906
|
* Rendered in the pending/empty view in place of the default
|
|
467
907
|
* "Message your agent to start" copy. Hosts use this to surface live agent
|
|
@@ -573,23 +1013,38 @@ export interface VideoEditorProps<P extends Project = Project> {
|
|
|
573
1013
|
*/
|
|
574
1014
|
onProvideRenderTrigger?: (openRender: () => void) => void
|
|
575
1015
|
|
|
1016
|
+
/**
|
|
1017
|
+
* Opt a host OUT of the package's built-in toolbar image-tone button so it
|
|
1018
|
+
* can surface the setting in its own chrome (e.g. the top-of-page header).
|
|
1019
|
+
* Mirrors `onProvideRenderTrigger`: when provided, the package (a) hides its
|
|
1020
|
+
* toolbar control and (b) calls this callback with the current state
|
|
1021
|
+
* whenever it changes. The host renders its own control (the package exports
|
|
1022
|
+
* `ImageToneMenu` with `variant="header"` for exactly this) and calls
|
|
1023
|
+
* `set(tone)` to persist a choice; the editor owns the save path.
|
|
1024
|
+
*
|
|
1025
|
+
* Called with `null` when the control should not be shown (SDR project: the
|
|
1026
|
+
* tone only affects HDR renders). Hosts must render nothing in that case.
|
|
1027
|
+
*/
|
|
1028
|
+
onProvideImageTone?: (api: { value: ImageTone; set: (tone: ImageTone) => void } | null) => void
|
|
1029
|
+
|
|
576
1030
|
// ── Host-supplied Montaj-specific UI (render-prop seams) ──────────────────
|
|
577
|
-
// The
|
|
1031
|
+
// The generation panel and the subcut-regeneration tool read host-only
|
|
578
1032
|
// fields (regenQueue, storyboard, the host's full Project) the package types
|
|
579
1033
|
// don't know. The editor surfaces them as render-props it threads/renders so
|
|
580
1034
|
// those components can stay host-side; the editor stays Montaj-agnostic.
|
|
1035
|
+
// Both take the clip id rather than a project entity — the editor owns the
|
|
1036
|
+
// selection, the host owns what to draw for it.
|
|
581
1037
|
|
|
582
1038
|
/**
|
|
583
|
-
* Render-prop seam for the host's clip
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* `
|
|
587
|
-
*
|
|
1039
|
+
* Render-prop seam for the host's per-clip generation panel (Montaj's AI
|
|
1040
|
+
* regenerate surface), rendered inside the right properties panel beneath
|
|
1041
|
+
* the clip properties whenever a VIDEO clip is selected. It reads and writes
|
|
1042
|
+
* `project.regenQueue` and `project.storyboard` — host-only fields this
|
|
1043
|
+
* package deliberately knows nothing about (see EditorProject's index-
|
|
1044
|
+
* signature comment) — so the content stays host-side and the editor only
|
|
1045
|
+
* says WHERE it goes and WHICH clip it is for. Absent → nothing rendered.
|
|
588
1046
|
*/
|
|
589
|
-
|
|
590
|
-
item: { kind: 'clip' | 'audio'; id: string }
|
|
591
|
-
onClose: () => void
|
|
592
|
-
}) => ReactNode
|
|
1047
|
+
renderGenerationPanel?: (ctx: { clipId: string }) => ReactNode
|
|
593
1048
|
|
|
594
1049
|
/**
|
|
595
1050
|
* Render-prop seam for the host's subcut-regeneration tool (Montaj's
|
|
@@ -612,4 +1067,128 @@ export interface VideoEditorProps<P extends Project = Project> {
|
|
|
612
1067
|
* reads `regenQueue`.
|
|
613
1068
|
*/
|
|
614
1069
|
isClipQueued?: (itemId: string) => boolean
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* SP4 — opt into the WebCodecs playback engine for the video preview.
|
|
1073
|
+
* Follows the `assetsPlacement`/`regenEnabled` host-knob precedent: an
|
|
1074
|
+
* optional prop, absent by default, that a host passes to change editor
|
|
1075
|
+
* behavior. Threaded straight through to `PreviewPlayer`'s own `engine`
|
|
1076
|
+
* prop (see `video/preview/PreviewPlayer.tsx`).
|
|
1077
|
+
*
|
|
1078
|
+
* Default (prop omitted) or `{ enabled: false }`: the legacy `<video>`-slot
|
|
1079
|
+
* player, completely unchanged — this is the non-regression guarantee the
|
|
1080
|
+
* SP4 plan tests against (the entire editor suite stays green with this
|
|
1081
|
+
* prop untouched).
|
|
1082
|
+
*
|
|
1083
|
+
* `{ enabled: true }` does not itself force engine mode: the editor
|
|
1084
|
+
* evaluates per-project eligibility (`engine/eligibility.ts` — WebCodecs
|
|
1085
|
+
* avc1/opus decode support, plus every track-0 video item proxied and none
|
|
1086
|
+
* requiring the WebM `nobg_preview_src` alpha path) once per project load,
|
|
1087
|
+
* and falls back to the legacy player, reasoned via console, whenever a
|
|
1088
|
+
* project doesn't pass. `debugHud` additionally renders the
|
|
1089
|
+
* fps/drops/buffer/clock-kind readout; it has no effect while `enabled` is
|
|
1090
|
+
* false.
|
|
1091
|
+
*
|
|
1092
|
+
* No flag mechanism existed before this — hosts opt in explicitly, and this
|
|
1093
|
+
* prop stays absent-by-default for every consumer of the package. The montaj
|
|
1094
|
+
* ui app passes `enabled: true` unconditionally; other hosts are unaffected
|
|
1095
|
+
* and must still opt in.
|
|
1096
|
+
*/
|
|
1097
|
+
engine?: { enabled: boolean; debugHud?: boolean }
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Opt-in seam letting a host's footage bin drive the MAIN preview on hover.
|
|
1101
|
+
* When the host hovers a bin clip card it sets `{ url, fraction }` on this
|
|
1102
|
+
* store; the editor mounts a paused `<video>` overlay above the preview and
|
|
1103
|
+
* seeks it to `fraction × duration`, so the operator source-scrubs an
|
|
1104
|
+
* OFF-TIMELINE clip without disturbing the playhead. Clearing it to `null`
|
|
1105
|
+
* unmounts the overlay and the normal timeline preview shows again.
|
|
1106
|
+
*
|
|
1107
|
+
* Default (prop omitted): totally inert — no overlay is ever mounted and the
|
|
1108
|
+
* main preview behaves exactly as before, so the classic layout, Hub and LP
|
|
1109
|
+
* are unaffected. A host opts in by creating the store
|
|
1110
|
+
* (`createSourcePreviewStore`) and passing it here AND to the card that writes
|
|
1111
|
+
* to it. Mirrors the `hover-scrub` store pattern; see
|
|
1112
|
+
* `video/source-preview.ts`.
|
|
1113
|
+
*/
|
|
1114
|
+
sourcePreview?: SourcePreviewStore
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Opt-in seam for a non-blocking, host-driven caption job. When provided,
|
|
1118
|
+
* the editor delegates the caption generate/regenerate trigger to this
|
|
1119
|
+
* callback instead of opening its own blocking `CaptionRegenModal` — the
|
|
1120
|
+
* host is asserting it owns the job (e.g. running it as a background task
|
|
1121
|
+
* and reconciling `project.captions` itself via its own transport). Wins
|
|
1122
|
+
* over `adapter.generateCaptions` when both are present, since a host that
|
|
1123
|
+
* passes this prop typically still implements `generateCaptions` to power
|
|
1124
|
+
* the job it triggers.
|
|
1125
|
+
*
|
|
1126
|
+
* Absent (the default): the editor's existing built-in `CaptionRegenModal`
|
|
1127
|
+
* path runs completely unchanged — this is the Hub/Los Parceros backward-
|
|
1128
|
+
* compat guarantee. Neither host currently passes this prop.
|
|
1129
|
+
*/
|
|
1130
|
+
onRegenerateCaptions?: () => void
|
|
1131
|
+
/**
|
|
1132
|
+
* Lets the host tell the caption panel that ITS background caption job is
|
|
1133
|
+
* in flight, so the generate/regenerate trigger button disables while it
|
|
1134
|
+
* runs. Meaningful only alongside `onRegenerateCaptions` — a host that owns
|
|
1135
|
+
* the trigger also owns knowing when the job is still running, since the
|
|
1136
|
+
* editor has no visibility into a job it didn't start.
|
|
1137
|
+
*
|
|
1138
|
+
* OR'd with the editor's own internal modal-open state at the call site —
|
|
1139
|
+
* it never replaces that state, only adds to it, so the built-in modal's
|
|
1140
|
+
* "disable the trigger while it's open" behavior keeps working even when a
|
|
1141
|
+
* host also sets this. Absent → treated as `false`, no effect.
|
|
1142
|
+
*/
|
|
1143
|
+
captionsGenerating?: boolean
|
|
1144
|
+
|
|
1145
|
+
// ── Filesystem drop onto the timeline (opt-in) ────────────────────────────
|
|
1146
|
+
// Both halves of the same seam, and both FEATURE-DETECTED: a host that
|
|
1147
|
+
// passes neither is byte-unchanged, because the surface only claims an
|
|
1148
|
+
// OS-file drag when the hook below is actually present (see the note on it).
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* A drop of real FILES from the OS onto the timeline. Absent → an OS-file
|
|
1152
|
+
* drag is not accepted at all and the browser keeps its default handling,
|
|
1153
|
+
* which is exactly what a host that predates this feature gets.
|
|
1154
|
+
*
|
|
1155
|
+
* The package hands over only what the browser told IT, plus the one piece
|
|
1156
|
+
* of its own state the host cannot see:
|
|
1157
|
+
* - `placement.atTime` — the timeline second under the pointer.
|
|
1158
|
+
* - `placement.preferredTrackIndex` — the video row released over, in
|
|
1159
|
+
* NORMALIZED track order, or `-1` when the pointer was over the ruler, a
|
|
1160
|
+
* caption band, an audio lane or the gap between rows.
|
|
1161
|
+
* - `placement.ripple` — the editor's ripple/magnet mode, CAPTURED AT DROP
|
|
1162
|
+
* TIME rather than read live. This is deliberate and is the whole reason
|
|
1163
|
+
* the field exists: `rippleMode` is internal editor state with no other
|
|
1164
|
+
* route to the host, and the host places the clip seconds later, when its
|
|
1165
|
+
* background import resolves — by which point the operator may well have
|
|
1166
|
+
* toggled the magnet again. The mode in force during the GESTURE is what
|
|
1167
|
+
* the user meant by that drop, so that is the one that has to survive the
|
|
1168
|
+
* wait. Feed it straight back into `placeDroppedClip`'s `ripple`.
|
|
1169
|
+
*
|
|
1170
|
+
* Fire-and-forget: the editor does not wait on this and does not mutate the
|
|
1171
|
+
* project for it. Importing a filesystem file means probing it, normalizing
|
|
1172
|
+
* it and building a proxy — all host-side work, of unbounded duration — so
|
|
1173
|
+
* the host owns the whole job and commits the resulting clip itself (route
|
|
1174
|
+
* it through the exported `placeDroppedClip` with this same `atTime` /
|
|
1175
|
+
* `preferredTrackIndex` and the placement will match what the drop
|
|
1176
|
+
* indicated). Feed `pendingDrops` below while that runs.
|
|
1177
|
+
*/
|
|
1178
|
+
onImportFilesToTimeline?: (files: File[], placement: TimelineDropPlacement) => void
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Ghost bands for imports still in flight — one per file the host has
|
|
1182
|
+
* accepted from `onImportFilesToTimeline` and not yet landed as a real clip.
|
|
1183
|
+
* Drawn on the timeline's overlay layer as a dashed, translucent band at the
|
|
1184
|
+
* drop point, so a slow import is visibly "coming" at the place it was
|
|
1185
|
+
* dropped rather than nothing at all until it appears.
|
|
1186
|
+
*
|
|
1187
|
+
* The host owns the list entirely: it adds an entry when it starts an
|
|
1188
|
+
* import and drops that entry when the import lands (or fails). The package
|
|
1189
|
+
* never adds, mutates or expires one — a ghost that is never retracted stays
|
|
1190
|
+
* on screen forever, which is the host's bug to fix, not something the
|
|
1191
|
+
* editor guesses at. Absent or empty → nothing is drawn.
|
|
1192
|
+
*/
|
|
1193
|
+
pendingDrops?: readonly PendingDrop[]
|
|
615
1194
|
}
|