@pixerate/editor-react 0.54.0 → 0.55.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.
|
@@ -849,10 +849,122 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
849
849
|
);
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
+
// src/video-editor/TranscriptPanel.tsx
|
|
853
|
+
import React4, { useMemo as useMemo3, useState as useState3 } from "react";
|
|
854
|
+
import {
|
|
855
|
+
findVideoFillers,
|
|
856
|
+
findVideoTranscriptPauses,
|
|
857
|
+
videoRemoveRangesEdits,
|
|
858
|
+
videoTimelineTranscript,
|
|
859
|
+
videoTranscriptEdit,
|
|
860
|
+
videoWordAt,
|
|
861
|
+
videoWordsRange
|
|
862
|
+
} from "@pixerate/editor/video-editor";
|
|
863
|
+
import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
864
|
+
var btn3 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
865
|
+
function TranscriptPanel({ controller, track, editable = true, className = "" }) {
|
|
866
|
+
const state = useVideoEditorState(controller);
|
|
867
|
+
const { project, time, readOnly } = state;
|
|
868
|
+
const canEdit = editable && !readOnly;
|
|
869
|
+
const [anchor, setAnchor] = useState3(null);
|
|
870
|
+
const [focus, setFocus] = useState3(null);
|
|
871
|
+
const [message, setMessage] = useState3(null);
|
|
872
|
+
const words = useMemo3(() => {
|
|
873
|
+
try {
|
|
874
|
+
return videoTimelineTranscript(project, { track });
|
|
875
|
+
} catch {
|
|
876
|
+
return [];
|
|
877
|
+
}
|
|
878
|
+
}, [project, track]);
|
|
879
|
+
const current = videoWordAt(words, time);
|
|
880
|
+
const selected = anchor !== null && focus !== null && anchor < words.length && focus < words.length ? [Math.min(anchor, focus), Math.max(anchor, focus)] : null;
|
|
881
|
+
const sound = project.assets.filter((a) => a.kind !== "image");
|
|
882
|
+
const cut = (ranges, label, none) => {
|
|
883
|
+
if (!ranges.length) return setMessage(none);
|
|
884
|
+
const r = controller.apply(videoRemoveRangesEdits(ranges), label);
|
|
885
|
+
setMessage(r.ok ? null : r.error);
|
|
886
|
+
setAnchor(null);
|
|
887
|
+
setFocus(null);
|
|
888
|
+
};
|
|
889
|
+
const load = async (assetId, file) => {
|
|
890
|
+
if (!file) return;
|
|
891
|
+
try {
|
|
892
|
+
const r = controller.apply(videoTranscriptEdit(assetId, await file.text()), "Attach transcript");
|
|
893
|
+
setMessage(r.ok ? null : r.error);
|
|
894
|
+
} catch (e) {
|
|
895
|
+
setMessage(e instanceof Error ? e.message : String(e));
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
return /* @__PURE__ */ jsxs5("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
|
|
899
|
+
words.length > 0 ? /* @__PURE__ */ jsxs5(Fragment5, { children: [
|
|
900
|
+
/* @__PURE__ */ jsx6(
|
|
901
|
+
"div",
|
|
902
|
+
{
|
|
903
|
+
className: "max-h-48 overflow-y-auto leading-6",
|
|
904
|
+
role: "listbox",
|
|
905
|
+
"aria-label": "Transcript",
|
|
906
|
+
"aria-multiselectable": "true",
|
|
907
|
+
tabIndex: 0,
|
|
908
|
+
onKeyDown: (e) => {
|
|
909
|
+
if (canEdit && selected && (e.key === "Delete" || e.key === "Backspace")) {
|
|
910
|
+
e.preventDefault();
|
|
911
|
+
cut([videoWordsRange(words, selected[0], selected[1])], "Cut words", "Select words to cut.");
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
children: words.map((w, i) => {
|
|
915
|
+
const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
|
|
916
|
+
return /* @__PURE__ */ jsxs5(React4.Fragment, { children: [
|
|
917
|
+
/* @__PURE__ */ jsx6(
|
|
918
|
+
"span",
|
|
919
|
+
{
|
|
920
|
+
role: "option",
|
|
921
|
+
"aria-selected": inSelection,
|
|
922
|
+
tabIndex: -1,
|
|
923
|
+
className: `cursor-pointer rounded px-0.5 ${inSelection ? "bg-rose-500/30" : i === current ? "bg-amber-400/40" : "hover:bg-muted"}`,
|
|
924
|
+
"data-word": i,
|
|
925
|
+
"data-current": i === current ? "" : void 0,
|
|
926
|
+
"data-selected": inSelection ? "" : void 0,
|
|
927
|
+
onClick: (e) => {
|
|
928
|
+
if (e.shiftKey && anchor !== null) setFocus(i);
|
|
929
|
+
else {
|
|
930
|
+
setAnchor(i);
|
|
931
|
+
setFocus(i);
|
|
932
|
+
controller.seek(w.start);
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
children: w.word
|
|
936
|
+
}
|
|
937
|
+
),
|
|
938
|
+
" "
|
|
939
|
+
] }, `${w.clip}:${w.index}`);
|
|
940
|
+
})
|
|
941
|
+
}
|
|
942
|
+
),
|
|
943
|
+
canEdit && /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap gap-1", children: [
|
|
944
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn3, disabled: !selected, onClick: () => selected && cut([videoWordsRange(words, selected[0], selected[1])], "Cut words", "Select words to cut."), children: "Delete selected words" }),
|
|
945
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn3, onClick: () => cut(findVideoFillers(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
|
|
946
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: btn3, onClick: () => cut(findVideoTranscriptPauses(words, { fps: project.fps }), "Tighten pauses", "No long pauses found."), children: "Tighten pauses" })
|
|
947
|
+
] })
|
|
948
|
+
] }) : /* @__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." }),
|
|
949
|
+
canEdit && sound.length > 0 && /* @__PURE__ */ jsxs5("details", { children: [
|
|
950
|
+
/* @__PURE__ */ jsx6("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
|
|
951
|
+
/* @__PURE__ */ jsx6("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ jsxs5("li", { className: "flex items-center gap-2", children: [
|
|
952
|
+
/* @__PURE__ */ jsxs5("span", { className: "min-w-0 flex-1 truncate", children: [
|
|
953
|
+
a.name ?? a.id,
|
|
954
|
+
" ",
|
|
955
|
+
a.transcript ? `\xB7 ${a.transcript.length} words` : ""
|
|
956
|
+
] }),
|
|
957
|
+
/* @__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]) })
|
|
958
|
+
] }, a.id)) })
|
|
959
|
+
] }),
|
|
960
|
+
message && /* @__PURE__ */ jsx6("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
|
|
961
|
+
] });
|
|
962
|
+
}
|
|
963
|
+
|
|
852
964
|
// src/video-editor/VideoEditor.tsx
|
|
853
965
|
import { useEffect as useEffect4 } from "react";
|
|
854
|
-
import { jsx as
|
|
855
|
-
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, className = "" }) {
|
|
966
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
967
|
+
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, className = "" }) {
|
|
856
968
|
const { controller, state } = useVideoEditor(given ?? options ?? {});
|
|
857
969
|
useEffect4(() => {
|
|
858
970
|
if (editable) return;
|
|
@@ -861,23 +973,25 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
|
|
|
861
973
|
return () => controller.setReadOnly(was);
|
|
862
974
|
}, [controller, editable]);
|
|
863
975
|
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__ */
|
|
976
|
+
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)) });
|
|
977
|
+
return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
|
|
978
|
+
/* @__PURE__ */ jsxs6("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
|
|
979
|
+
editable && /* @__PURE__ */ jsx7(MediaBin, { controller, className: "min-w-0" }),
|
|
980
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
981
|
+
/* @__PURE__ */ jsx7(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
|
|
982
|
+
/* @__PURE__ */ jsx7(VideoTransport, { controller, editable })
|
|
871
983
|
] }),
|
|
872
|
-
editable && /* @__PURE__ */
|
|
984
|
+
editable && /* @__PURE__ */ jsx7(ClipInspector, { controller, className: "min-w-0" })
|
|
873
985
|
] }),
|
|
874
|
-
/* @__PURE__ */
|
|
986
|
+
/* @__PURE__ */ jsx7(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
|
|
987
|
+
showTranscript && /* @__PURE__ */ jsx7(TranscriptPanel, { controller, editable }),
|
|
875
988
|
issueList
|
|
876
989
|
] });
|
|
877
990
|
}
|
|
878
991
|
export {
|
|
879
992
|
ClipInspector,
|
|
880
993
|
MediaBin,
|
|
994
|
+
TranscriptPanel,
|
|
881
995
|
VideoEditor,
|
|
882
996
|
VideoPreview,
|
|
883
997
|
VideoTimeline,
|