@pixerate/editor-svelte 0.63.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ <script lang="ts">
2
+ import {
3
+ findVideoFillers,
4
+ findVideoTranscriptPauses,
5
+ videoRemoveRangesEdits,
6
+ videoTimelineTranscript,
7
+ videoTranscriptEdit,
8
+ videoWordAt,
9
+ videoWordsRange,
10
+ type VideoEditorController,
11
+ type VideoTimelineWord
12
+ } from '@pixerate/editor/video-editor';
13
+ import { videoEditorStateOf } from './createVideoEditor.svelte.js';
14
+
15
+ interface Props {
16
+ controller: VideoEditorController;
17
+ /** Track whose words to show (default: the first audio track with a transcribed clip, else the first video track with one). */
18
+ track?: string;
19
+ /** Allow cutting by text and loading transcripts (default true). */
20
+ editable?: boolean;
21
+ class?: string;
22
+ }
23
+
24
+ let { controller, track, editable = true, class: className = '' }: Props = $props();
25
+ const view = videoEditorStateOf(() => controller);
26
+ const editor = $derived(view.current);
27
+ const canEdit = $derived(editable && !editor.readOnly);
28
+ const words: VideoTimelineWord[] = $derived.by(() => {
29
+ try {
30
+ return videoTimelineTranscript(editor.project, { track });
31
+ } catch {
32
+ return [];
33
+ }
34
+ });
35
+ const current = $derived(videoWordAt(words, editor.time));
36
+ let anchor = $state<number | null>(null);
37
+ let focus = $state<number | null>(null);
38
+ let message = $state<string | null>(null);
39
+ const selected = $derived(anchor !== null && focus !== null && anchor < words.length && focus < words.length ? ([Math.min(anchor, focus), Math.max(anchor, focus)] as const) : null);
40
+ const sound = $derived(editor.project.assets.filter((a) => a.kind !== 'image'));
41
+ const btn = 'rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40';
42
+
43
+ function cut(ranges: { from: number; to: number }[], label: string, none: string) {
44
+ if (!ranges.length) {
45
+ message = none;
46
+ return;
47
+ }
48
+ const r = controller.apply(videoRemoveRangesEdits(ranges), label);
49
+ message = r.ok ? null : r.error;
50
+ anchor = null;
51
+ focus = null;
52
+ }
53
+ function cutSelection() {
54
+ if (selected) cut([videoWordsRange(words, selected[0], selected[1])], 'Cut words', 'Select words to cut.');
55
+ }
56
+ function pick(e: MouseEvent, i: number) {
57
+ if (e.shiftKey && anchor !== null) focus = i;
58
+ else {
59
+ anchor = i;
60
+ focus = i;
61
+ controller.seek(words[i].start);
62
+ }
63
+ }
64
+ async function load(assetId: string, file: File | undefined) {
65
+ if (!file) return;
66
+ try {
67
+ const r = controller.apply(videoTranscriptEdit(assetId, await file.text()), 'Attach transcript');
68
+ message = r.ok ? null : r.error;
69
+ } catch (e) {
70
+ message = e instanceof Error ? e.message : String(e);
71
+ }
72
+ }
73
+ </script>
74
+
75
+ <!--
76
+ Edit by text: the words the timeline plays, from its media's transcripts. Click a word to go to it,
77
+ Shift-click to select a run of words, then delete them to cut that stretch out. "Remove fillers"
78
+ and "Tighten pauses" cut every um and long gap at once. Each cut is one undo step.
79
+ -->
80
+ <div class="flex flex-col gap-2 text-xs {className}" data-transcript-panel="">
81
+ {#if words.length}
82
+ <div
83
+ class="max-h-48 overflow-y-auto leading-6"
84
+ role="listbox"
85
+ aria-label="Transcript"
86
+ aria-multiselectable="true"
87
+ tabindex="0"
88
+ onkeydown={(e) => {
89
+ if (canEdit && selected && (e.key === 'Delete' || e.key === 'Backspace')) {
90
+ e.preventDefault();
91
+ cutSelection();
92
+ }
93
+ }}
94
+ >
95
+ {#each words as w, i (`${w.clip}:${w.index}`)}
96
+ {@const inSelection = selected !== null && i >= selected[0] && i <= selected[1]}
97
+ <span
98
+ role="option"
99
+ aria-selected={inSelection}
100
+ tabindex="-1"
101
+ class="cursor-pointer rounded px-0.5 {inSelection ? 'bg-rose-500/30' : i === current ? 'bg-amber-400/40' : 'hover:bg-muted'}"
102
+ data-word={i}
103
+ data-current={i === current ? '' : undefined}
104
+ data-selected={inSelection ? '' : undefined}
105
+ onclick={(e) => pick(e, i)}
106
+ onkeydown={() => {}}>{w.word}</span
107
+ >{' '}
108
+ {/each}
109
+ </div>
110
+ {#if canEdit}
111
+ <div class="flex flex-wrap gap-1">
112
+ <button type="button" class={btn} disabled={!selected} onclick={cutSelection}>Delete selected words</button>
113
+ <button type="button" class={btn} onclick={() => cut(findVideoFillers(words), 'Remove fillers', 'No filler words found.')}>Remove fillers</button>
114
+ <button type="button" class={btn} onclick={() => cut(findVideoTranscriptPauses(words, { fps: editor.project.fps }), 'Tighten pauses', 'No long pauses found.')}>Tighten pauses</button>
115
+ </div>
116
+ {/if}
117
+ {:else}
118
+ <p class="text-muted-foreground" data-transcript-empty="">No transcribed media on the timeline. Load a transcript (Whisper JSON, .srt or .vtt) for a video or audio asset to edit by text.</p>
119
+ {/if}
120
+ {#if canEdit && sound.length}
121
+ <details>
122
+ <summary class="cursor-pointer text-muted-foreground">Transcripts</summary>
123
+ <ul class="mt-1 space-y-1">
124
+ {#each sound as a (a.id)}
125
+ <li class="flex items-center gap-2">
126
+ <span class="min-w-0 flex-1 truncate">{a.name ?? a.id} {a.transcript ? `· ${a.transcript.length} words` : ''}</span>
127
+ <input type="file" accept=".json,.srt,.vtt,application/json,text/vtt" aria-label="Load a transcript for {a.name ?? a.id}" class="w-40 text-[10px]" onchange={(e) => void load(a.id, e.currentTarget.files?.[0])} />
128
+ </li>
129
+ {/each}
130
+ </ul>
131
+ </details>
132
+ {/if}
133
+ {#if message}
134
+ <p class="text-red-500" role="alert" data-transcript-error="">{message}</p>
135
+ {/if}
136
+ </div>
@@ -0,0 +1,13 @@
1
+ import { type VideoEditorController } from '@pixerate/editor/video-editor';
2
+ interface Props {
3
+ controller: VideoEditorController;
4
+ /** Track whose words to show (default: the first audio track with a transcribed clip, else the first video track with one). */
5
+ track?: string;
6
+ /** Allow cutting by text and loading transcripts (default true). */
7
+ editable?: boolean;
8
+ class?: string;
9
+ }
10
+ declare const TranscriptPanel: import("svelte").Component<Props, {}, "">;
11
+ type TranscriptPanel = ReturnType<typeof TranscriptPanel>;
12
+ export default TranscriptPanel;
13
+ //# sourceMappingURL=TranscriptPanel.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TranscriptPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/TranscriptPanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAQH,KAAK,qBAAqB,EAE3B,MAAM,+BAA+B,CAAC;AAIvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,+HAA+H;IAC/H,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqGH,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -3,6 +3,7 @@
3
3
  import { VideoEditorController, type VideoEditorOptions } from '@pixerate/editor/video-editor';
4
4
  import ClipInspector from './ClipInspector.svelte';
5
5
  import MediaBin from './MediaBin.svelte';
6
+ import TranscriptPanel from './TranscriptPanel.svelte';
6
7
  import VideoPreview from './VideoPreview.svelte';
7
8
  import VideoTimeline from './VideoTimeline.svelte';
8
9
  import VideoTransport from './VideoTransport.svelte';
@@ -22,10 +23,12 @@
22
23
  pxPerSecond?: number;
23
24
  /** List the project's issues under the timeline (default true). */
24
25
  showIssues?: boolean;
26
+ /** Show the transcript panel for editing by text (default true). */
27
+ showTranscript?: boolean;
25
28
  class?: string;
26
29
  }
27
30
 
28
- let { controller, options, editable = true, pxPerSecond = $bindable(100), showIssues = true, class: className = '' }: Props = $props();
31
+ let { controller, options, editable = true, pxPerSecond = $bindable(100), showIssues = true, showTranscript = true, class: className = '' }: Props = $props();
29
32
  let owned: VideoEditorController | null = null;
30
33
  // A controller is made only when none is passed, and kept for the component's lifetime.
31
34
  const ctl = $derived(controller ?? (owned ??= new VideoEditorController(options)));
@@ -54,6 +57,7 @@
54
57
  <VideoPreview controller={ctl} class="rounded-md" />
55
58
  <VideoTransport controller={ctl} editable={false} />
56
59
  <VideoTimeline controller={ctl} bind:pxPerSecond editable={false} />
60
+ {#if showTranscript}<TranscriptPanel controller={ctl} editable={false} />{/if}
57
61
  {@render issueList()}
58
62
  </div>
59
63
  {:else}
@@ -66,6 +70,7 @@
66
70
  <ClipInspector controller={ctl} class="min-w-0" />
67
71
  <div class="flex min-w-0 flex-col gap-2 lg:col-span-3">
68
72
  <VideoTimeline controller={ctl} bind:pxPerSecond />
73
+ {#if showTranscript}<TranscriptPanel controller={ctl} />{/if}
69
74
  {@render issueList()}
70
75
  </div>
71
76
  </div>
@@ -13,6 +13,8 @@ interface Props {
13
13
  pxPerSecond?: number;
14
14
  /** List the project's issues under the timeline (default true). */
15
15
  showIssues?: boolean;
16
+ /** Show the transcript panel for editing by text (default true). */
17
+ showTranscript?: boolean;
16
18
  class?: string;
17
19
  }
18
20
  declare const VideoEditor: import("svelte").Component<Props, {}, "pxPerSecond">;
@@ -1 +1 @@
1
- {"version":3,"file":"VideoEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/VideoEditor.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,qBAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAS7F,UAAU,KAAK;IACb,2FAA2F;IAC3F,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAwDH,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"VideoEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/video-editor/VideoEditor.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,qBAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAU7F,UAAU,KAAK;IACb,2FAA2F;IAC3F,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oEAAoE;IACpE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2DH,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -5,12 +5,14 @@ import type VideoTransportComponent from "./VideoTransport.svelte";
5
5
  import type VideoTimelineComponent from "./VideoTimeline.svelte";
6
6
  import type ClipInspectorComponent from "./ClipInspector.svelte";
7
7
  import type MediaBinComponent from "./MediaBin.svelte";
8
+ import type TranscriptPanelComponent from "./TranscriptPanel.svelte";
8
9
  export { default as VideoEditor } from "./VideoEditor.svelte";
9
10
  export { default as VideoPreview } from "./VideoPreview.svelte";
10
11
  export { default as VideoTransport } from "./VideoTransport.svelte";
11
12
  export { default as VideoTimeline } from "./VideoTimeline.svelte";
12
13
  export { default as ClipInspector } from "./ClipInspector.svelte";
13
14
  export { default as MediaBin } from "./MediaBin.svelte";
15
+ export { default as TranscriptPanel } from "./TranscriptPanel.svelte";
14
16
  export * from "./createVideoEditor.svelte.js";
15
17
  export type VideoEditorProps = ComponentProps<typeof VideoEditorComponent>;
16
18
  export type VideoPreviewProps = ComponentProps<typeof VideoPreviewComponent>;
@@ -18,4 +20,5 @@ export type VideoTransportProps = ComponentProps<typeof VideoTransportComponent>
18
20
  export type VideoTimelineProps = ComponentProps<typeof VideoTimelineComponent>;
19
21
  export type ClipInspectorProps = ComponentProps<typeof ClipInspectorComponent>;
20
22
  export type MediaBinProps = ComponentProps<typeof MediaBinComponent>;
23
+ export type TranscriptPanelProps = ComponentProps<typeof TranscriptPanelComponent>;
21
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/video-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,qBAAqB,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,uBAAuB,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,sBAAsB,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,sBAAsB,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,iBAAiB,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,cAAc,+BAA+B,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3E,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC7E,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACjF,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC/E,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC/E,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/video-editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,qBAAqB,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,uBAAuB,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,sBAAsB,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,sBAAsB,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,iBAAiB,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,wBAAwB,MAAM,0BAA0B,CAAC;AAErE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,cAAc,+BAA+B,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3E,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC7E,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACjF,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC/E,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC/E,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACrE,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
@@ -4,4 +4,5 @@ export { default as VideoTransport } from "./VideoTransport.svelte";
4
4
  export { default as VideoTimeline } from "./VideoTimeline.svelte";
5
5
  export { default as ClipInspector } from "./ClipInspector.svelte";
6
6
  export { default as MediaBin } from "./MediaBin.svelte";
7
+ export { default as TranscriptPanel } from "./TranscriptPanel.svelte";
7
8
  export * from "./createVideoEditor.svelte.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -131,7 +131,7 @@
131
131
  "@tiptap/extension-bubble-menu": "^2.11.5",
132
132
  "@tiptap/extension-placeholder": "^2.11.5",
133
133
  "@tiptap/starter-kit": "^2.11.5",
134
- "@pixerate/editor": "0.84.0"
134
+ "@pixerate/editor": "0.85.0"
135
135
  },
136
136
  "peerDependencies": {
137
137
  "@dagrejs/dagre": "^1.1.4",