@pixerate/editor-react 0.54.0 → 0.56.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.
|
@@ -434,12 +434,16 @@ import {
|
|
|
434
434
|
VIDEO_TRANSITION_TYPES,
|
|
435
435
|
VIDEO_WIPE_DIRECTIONS,
|
|
436
436
|
videoTransitionHasDirection,
|
|
437
|
+
videoFreezeFrameEdit,
|
|
437
438
|
videoKeyframeRows,
|
|
439
|
+
videoSlowMotionRamp,
|
|
440
|
+
videoSpeedRampEdit,
|
|
438
441
|
videoTransitionAudioCurve,
|
|
439
442
|
videoKeyframeToggleEdit,
|
|
440
443
|
videoKeyframeValueEdit
|
|
441
444
|
} from "@pixerate/editor/video-editor";
|
|
442
445
|
import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
446
|
+
var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
443
447
|
var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
|
|
444
448
|
var DEFAULT_TRANSITION = 500;
|
|
445
449
|
function Commit({
|
|
@@ -607,7 +611,24 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
607
611
|
/* @__PURE__ */ jsx4(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
|
|
608
612
|
/* @__PURE__ */ jsx4(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
|
|
609
613
|
clip.type === "media" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
610
|
-
/* @__PURE__ */ jsx4(Row, { label: "Speed", name: "speed", children:
|
|
614
|
+
/* @__PURE__ */ jsx4(Row, { label: "Speed", name: "speed", children: clip.timeRemap ? /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-1", "data-ramped": "", children: [
|
|
615
|
+
/* @__PURE__ */ jsx4("span", { className: "text-muted-foreground", children: "ramped" }),
|
|
616
|
+
/* @__PURE__ */ jsx4("button", { type: "button", className: btn2, disabled, onClick: () => controller.apply({ op: "set_time_remap", clip: clip.id, keys: null, ripple: true }, "Remove speed ramp"), children: "Remove ramp" })
|
|
617
|
+
] }) : number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
|
|
618
|
+
project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */ jsx4(Row, { label: "Retime", name: "retime", children: /* @__PURE__ */ jsxs3("span", { className: "flex flex-wrap gap-1", children: [
|
|
619
|
+
/* @__PURE__ */ jsx4(
|
|
620
|
+
"button",
|
|
621
|
+
{
|
|
622
|
+
type: "button",
|
|
623
|
+
className: btn2,
|
|
624
|
+
disabled: disabled || time < clip.start || time > clip.start + clip.duration,
|
|
625
|
+
title: "Hold the frame at the playhead for a second, pushing later clips along",
|
|
626
|
+
onClick: () => controller.apply(videoFreezeFrameEdit(clip, time - clip.start, 1e3), "Freeze frame"),
|
|
627
|
+
children: "Freeze 1 s here"
|
|
628
|
+
}
|
|
629
|
+
),
|
|
630
|
+
/* @__PURE__ */ jsx4("button", { type: "button", className: btn2, disabled, title: "Normal speed, then a glide into 0.3\xD7 slow motion", onClick: () => controller.apply(videoSpeedRampEdit(clip, videoSlowMotionRamp(clip.duration)), "Speed ramp"), children: "Slow-mo ramp" })
|
|
631
|
+
] }) }),
|
|
611
632
|
/* @__PURE__ */ jsx4(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ jsx4("input", { type: "checkbox", className: "justify-self-start", checked: !!clip.muted, disabled, "aria-label": "Muted", onChange: (e) => set("muted", e.target.checked || void 0, e.target.checked ? "Mute clip" : "Unmute clip") }) }),
|
|
612
633
|
["fadeInCurve", "fadeOutCurve"].map(
|
|
613
634
|
(key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ jsx4(Row, { label: key === "fadeInCurve" ? "Fade in sound" : "Fade out sound", name: key, children: /* @__PURE__ */ jsx4(
|
|
@@ -707,7 +728,7 @@ import {
|
|
|
707
728
|
videoAssetKindOf
|
|
708
729
|
} from "@pixerate/editor/video-editor";
|
|
709
730
|
import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
710
|
-
var
|
|
731
|
+
var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
711
732
|
function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMedia, className = "" }) {
|
|
712
733
|
const { project, readOnly } = useVideoEditorState(controller);
|
|
713
734
|
const canEdit = editable && !readOnly;
|
|
@@ -786,7 +807,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
786
807
|
/* @__PURE__ */ jsx5("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
|
|
787
808
|
/* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
|
|
788
809
|
asset.duration !== void 0 && /* @__PURE__ */ jsx5("span", { className: "tabular-nums text-muted-foreground", children: formatVideoSeconds3(asset.duration) }),
|
|
789
|
-
editable && /* @__PURE__ */ jsx5("button", { type: "button", className:
|
|
810
|
+
editable && /* @__PURE__ */ jsx5("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
|
|
790
811
|
] }, asset.id)) }) : /* @__PURE__ */ jsxs4("p", { className: "text-xs text-muted-foreground", children: [
|
|
791
812
|
"No media yet.",
|
|
792
813
|
editable ? " Choose or drop files, or add a URL." : ""
|
|
@@ -837,7 +858,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
837
858
|
/* @__PURE__ */ jsx5("option", { value: "audio", children: "Audio" }),
|
|
838
859
|
/* @__PURE__ */ jsx5("option", { value: "image", children: "Image" })
|
|
839
860
|
] }),
|
|
840
|
-
/* @__PURE__ */ jsx5("button", { type: "submit", className:
|
|
861
|
+
/* @__PURE__ */ jsx5("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
|
|
841
862
|
]
|
|
842
863
|
}
|
|
843
864
|
)
|
|
@@ -849,10 +870,122 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
849
870
|
);
|
|
850
871
|
}
|
|
851
872
|
|
|
873
|
+
// src/video-editor/TranscriptPanel.tsx
|
|
874
|
+
import React4, { useMemo as useMemo3, useState as useState3 } from "react";
|
|
875
|
+
import {
|
|
876
|
+
findVideoFillers,
|
|
877
|
+
findVideoTranscriptPauses,
|
|
878
|
+
videoRemoveRangesEdits,
|
|
879
|
+
videoTimelineTranscript,
|
|
880
|
+
videoTranscriptEdit,
|
|
881
|
+
videoWordAt,
|
|
882
|
+
videoWordsRange
|
|
883
|
+
} from "@pixerate/editor/video-editor";
|
|
884
|
+
import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
885
|
+
var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
886
|
+
function TranscriptPanel({ controller, track, editable = true, className = "" }) {
|
|
887
|
+
const state = useVideoEditorState(controller);
|
|
888
|
+
const { project, time, readOnly } = state;
|
|
889
|
+
const canEdit = editable && !readOnly;
|
|
890
|
+
const [anchor, setAnchor] = useState3(null);
|
|
891
|
+
const [focus, setFocus] = useState3(null);
|
|
892
|
+
const [message, setMessage] = useState3(null);
|
|
893
|
+
const words = useMemo3(() => {
|
|
894
|
+
try {
|
|
895
|
+
return videoTimelineTranscript(project, { track });
|
|
896
|
+
} catch {
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
899
|
+
}, [project, track]);
|
|
900
|
+
const current = videoWordAt(words, time);
|
|
901
|
+
const selected = anchor !== null && focus !== null && anchor < words.length && focus < words.length ? [Math.min(anchor, focus), Math.max(anchor, focus)] : null;
|
|
902
|
+
const sound = project.assets.filter((a) => a.kind !== "image");
|
|
903
|
+
const cut = (ranges, label, none) => {
|
|
904
|
+
if (!ranges.length) return setMessage(none);
|
|
905
|
+
const r = controller.apply(videoRemoveRangesEdits(ranges), label);
|
|
906
|
+
setMessage(r.ok ? null : r.error);
|
|
907
|
+
setAnchor(null);
|
|
908
|
+
setFocus(null);
|
|
909
|
+
};
|
|
910
|
+
const load = async (assetId, file) => {
|
|
911
|
+
if (!file) return;
|
|
912
|
+
try {
|
|
913
|
+
const r = controller.apply(videoTranscriptEdit(assetId, await file.text()), "Attach transcript");
|
|
914
|
+
setMessage(r.ok ? null : r.error);
|
|
915
|
+
} catch (e) {
|
|
916
|
+
setMessage(e instanceof Error ? e.message : String(e));
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
return /* @__PURE__ */ jsxs5("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
|
|
920
|
+
words.length > 0 ? /* @__PURE__ */ jsxs5(Fragment5, { children: [
|
|
921
|
+
/* @__PURE__ */ jsx6(
|
|
922
|
+
"div",
|
|
923
|
+
{
|
|
924
|
+
className: "max-h-48 overflow-y-auto leading-6",
|
|
925
|
+
role: "listbox",
|
|
926
|
+
"aria-label": "Transcript",
|
|
927
|
+
"aria-multiselectable": "true",
|
|
928
|
+
tabIndex: 0,
|
|
929
|
+
onKeyDown: (e) => {
|
|
930
|
+
if (canEdit && selected && (e.key === "Delete" || e.key === "Backspace")) {
|
|
931
|
+
e.preventDefault();
|
|
932
|
+
cut([videoWordsRange(words, selected[0], selected[1])], "Cut words", "Select words to cut.");
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
children: words.map((w, i) => {
|
|
936
|
+
const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
|
|
937
|
+
return /* @__PURE__ */ jsxs5(React4.Fragment, { children: [
|
|
938
|
+
/* @__PURE__ */ jsx6(
|
|
939
|
+
"span",
|
|
940
|
+
{
|
|
941
|
+
role: "option",
|
|
942
|
+
"aria-selected": inSelection,
|
|
943
|
+
tabIndex: -1,
|
|
944
|
+
className: `cursor-pointer rounded px-0.5 ${inSelection ? "bg-rose-500/30" : i === current ? "bg-amber-400/40" : "hover:bg-muted"}`,
|
|
945
|
+
"data-word": i,
|
|
946
|
+
"data-current": i === current ? "" : void 0,
|
|
947
|
+
"data-selected": inSelection ? "" : void 0,
|
|
948
|
+
onClick: (e) => {
|
|
949
|
+
if (e.shiftKey && anchor !== null) setFocus(i);
|
|
950
|
+
else {
|
|
951
|
+
setAnchor(i);
|
|
952
|
+
setFocus(i);
|
|
953
|
+
controller.seek(w.start);
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
children: w.word
|
|
957
|
+
}
|
|
958
|
+
),
|
|
959
|
+
" "
|
|
960
|
+
] }, `${w.clip}:${w.index}`);
|
|
961
|
+
})
|
|
962
|
+
}
|
|
963
|
+
),
|
|
964
|
+
canEdit && /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap gap-1", children: [
|
|
965
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn4, disabled: !selected, onClick: () => selected && cut([videoWordsRange(words, selected[0], selected[1])], "Cut words", "Select words to cut."), children: "Delete selected words" }),
|
|
966
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn4, onClick: () => cut(findVideoFillers(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
|
|
967
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn4, onClick: () => cut(findVideoTranscriptPauses(words, { fps: project.fps }), "Tighten pauses", "No long pauses found."), children: "Tighten pauses" })
|
|
968
|
+
] })
|
|
969
|
+
] }) : /* @__PURE__ */ jsx6("p", { className: "text-muted-foreground", "data-transcript-empty": "", children: "No transcribed media on the timeline. Load a transcript (Whisper JSON, .srt or .vtt) for a video or audio asset to edit by text." }),
|
|
970
|
+
canEdit && sound.length > 0 && /* @__PURE__ */ jsxs5("details", { children: [
|
|
971
|
+
/* @__PURE__ */ jsx6("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
|
|
972
|
+
/* @__PURE__ */ jsx6("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ jsxs5("li", { className: "flex items-center gap-2", children: [
|
|
973
|
+
/* @__PURE__ */ jsxs5("span", { className: "min-w-0 flex-1 truncate", children: [
|
|
974
|
+
a.name ?? a.id,
|
|
975
|
+
" ",
|
|
976
|
+
a.transcript ? `\xB7 ${a.transcript.length} words` : ""
|
|
977
|
+
] }),
|
|
978
|
+
/* @__PURE__ */ jsx6("input", { type: "file", accept: ".json,.srt,.vtt,application/json,text/vtt", "aria-label": `Load a transcript for ${a.name ?? a.id}`, className: "w-40 text-[10px]", onChange: (e) => void load(a.id, e.target.files?.[0]) })
|
|
979
|
+
] }, a.id)) })
|
|
980
|
+
] }),
|
|
981
|
+
message && /* @__PURE__ */ jsx6("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
|
|
982
|
+
] });
|
|
983
|
+
}
|
|
984
|
+
|
|
852
985
|
// src/video-editor/VideoEditor.tsx
|
|
853
986
|
import { useEffect as useEffect4 } from "react";
|
|
854
|
-
import { jsx as
|
|
855
|
-
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, className = "" }) {
|
|
987
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
988
|
+
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, className = "" }) {
|
|
856
989
|
const { controller, state } = useVideoEditor(given ?? options ?? {});
|
|
857
990
|
useEffect4(() => {
|
|
858
991
|
if (editable) return;
|
|
@@ -861,23 +994,25 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
|
|
|
861
994
|
return () => controller.setReadOnly(was);
|
|
862
995
|
}, [controller, editable]);
|
|
863
996
|
const { issues } = state;
|
|
864
|
-
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */
|
|
865
|
-
return /* @__PURE__ */
|
|
866
|
-
/* @__PURE__ */
|
|
867
|
-
editable && /* @__PURE__ */
|
|
868
|
-
/* @__PURE__ */
|
|
869
|
-
/* @__PURE__ */
|
|
870
|
-
/* @__PURE__ */
|
|
997
|
+
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx7("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ jsx7("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", "data-issue": issue.code, children: issue.message }, i)) });
|
|
998
|
+
return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
|
|
999
|
+
/* @__PURE__ */ jsxs6("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
|
|
1000
|
+
editable && /* @__PURE__ */ jsx7(MediaBin, { controller, className: "min-w-0" }),
|
|
1001
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
1002
|
+
/* @__PURE__ */ jsx7(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
|
|
1003
|
+
/* @__PURE__ */ jsx7(VideoTransport, { controller, editable })
|
|
871
1004
|
] }),
|
|
872
|
-
editable && /* @__PURE__ */
|
|
1005
|
+
editable && /* @__PURE__ */ jsx7(ClipInspector, { controller, className: "min-w-0" })
|
|
873
1006
|
] }),
|
|
874
|
-
/* @__PURE__ */
|
|
1007
|
+
/* @__PURE__ */ jsx7(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
|
|
1008
|
+
showTranscript && /* @__PURE__ */ jsx7(TranscriptPanel, { controller, editable }),
|
|
875
1009
|
issueList
|
|
876
1010
|
] });
|
|
877
1011
|
}
|
|
878
1012
|
export {
|
|
879
1013
|
ClipInspector,
|
|
880
1014
|
MediaBin,
|
|
1015
|
+
TranscriptPanel,
|
|
881
1016
|
VideoEditor,
|
|
882
1017
|
VideoPreview,
|
|
883
1018
|
VideoTimeline,
|