@pixerate/editor-react 0.53.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.
|
@@ -420,10 +420,22 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
420
420
|
import {
|
|
421
421
|
findVideoClip,
|
|
422
422
|
formatVideoSeconds as formatVideoSeconds2,
|
|
423
|
+
VIDEO_CLIP_ANIMATION_EFFECTS,
|
|
424
|
+
VIDEO_CLIP_ANIMATION_LABELS,
|
|
425
|
+
normalizeVideoGrade,
|
|
426
|
+
VIDEO_GRADE_CONTROLS,
|
|
427
|
+
VIDEO_GRADE_PRESET_NAMES,
|
|
428
|
+
videoGradePreset,
|
|
429
|
+
VIDEO_BLEND_MODE_LABELS,
|
|
430
|
+
VIDEO_BLEND_MODES,
|
|
423
431
|
VIDEO_FADE_CURVE_LABELS,
|
|
424
432
|
VIDEO_FADE_CURVES,
|
|
433
|
+
VIDEO_TRANSITION_LABELS,
|
|
425
434
|
VIDEO_TRANSITION_TYPES,
|
|
435
|
+
VIDEO_WIPE_DIRECTIONS,
|
|
436
|
+
videoTransitionHasDirection,
|
|
426
437
|
videoKeyframeRows,
|
|
438
|
+
videoTransitionAudioCurve,
|
|
427
439
|
videoKeyframeToggleEdit,
|
|
428
440
|
videoKeyframeValueEdit
|
|
429
441
|
} from "@pixerate/editor/video-editor";
|
|
@@ -536,6 +548,62 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
536
548
|
),
|
|
537
549
|
row.keyed && /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-[10px] tabular-nums text-muted-foreground", "data-keyframe-count": row.property, children: row.keys })
|
|
538
550
|
] }) }, row.property)),
|
|
551
|
+
at.track.kind === "video" && /* @__PURE__ */ jsx4(Row, { label: "Blend", name: "blend", children: /* @__PURE__ */ jsx4("select", { className: field, value: clip.blend ?? "normal", disabled, "aria-label": "Blend mode", onChange: (e) => set("blend", e.target.value === "normal" ? void 0 : e.target.value, "Change blend mode"), children: VIDEO_BLEND_MODES.map((m) => /* @__PURE__ */ jsx4("option", { value: m, children: VIDEO_BLEND_MODE_LABELS[m] }, m)) }) }),
|
|
552
|
+
at.track.kind === "video" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
553
|
+
/* @__PURE__ */ jsx4(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ jsxs3(
|
|
554
|
+
"select",
|
|
555
|
+
{
|
|
556
|
+
className: field,
|
|
557
|
+
value: "",
|
|
558
|
+
disabled,
|
|
559
|
+
"aria-label": "Apply a look",
|
|
560
|
+
onChange: (e) => {
|
|
561
|
+
const grade = videoGradePreset(e.target.value);
|
|
562
|
+
if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
|
|
563
|
+
},
|
|
564
|
+
children: [
|
|
565
|
+
/* @__PURE__ */ jsx4("option", { value: "", children: clip.grade ? "Graded" : "None" }),
|
|
566
|
+
VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ jsx4("option", { value: name, children: name.replace(/-/g, " ") }, name))
|
|
567
|
+
]
|
|
568
|
+
}
|
|
569
|
+
) }),
|
|
570
|
+
VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */ jsx4(Row, { label: c.label, name: `grade.${c.key}`, children: /* @__PURE__ */ jsx4(
|
|
571
|
+
Commit,
|
|
572
|
+
{
|
|
573
|
+
type: "number",
|
|
574
|
+
min: c.min,
|
|
575
|
+
max: c.max,
|
|
576
|
+
step: c.step,
|
|
577
|
+
value: clip.grade?.[c.key] === void 0 ? "" : String(clip.grade[c.key]),
|
|
578
|
+
placeholder: "0",
|
|
579
|
+
"aria-label": c.label,
|
|
580
|
+
disabled,
|
|
581
|
+
onCommit: (raw) => {
|
|
582
|
+
const n = raw.trim() ? Number(raw) : 0;
|
|
583
|
+
if (!Number.isFinite(n)) return;
|
|
584
|
+
const grade = normalizeVideoGrade({ ...clip.grade, [c.key]: n });
|
|
585
|
+
set("grade", grade, `Change ${c.label.toLowerCase()}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
) }, c.key))
|
|
589
|
+
] }),
|
|
590
|
+
at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ jsx4(Row, { label: key === "animateIn" ? "Animate in" : "Animate out", name: key, children: /* @__PURE__ */ jsxs3(
|
|
591
|
+
"select",
|
|
592
|
+
{
|
|
593
|
+
className: field,
|
|
594
|
+
value: clip[key]?.effect ?? "",
|
|
595
|
+
disabled,
|
|
596
|
+
"aria-label": key === "animateIn" ? "Animate in" : "Animate out",
|
|
597
|
+
onChange: (e) => {
|
|
598
|
+
const effect = e.target.value;
|
|
599
|
+
set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
|
|
600
|
+
},
|
|
601
|
+
children: [
|
|
602
|
+
/* @__PURE__ */ jsx4("option", { value: "", children: "None" }),
|
|
603
|
+
VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */ jsx4("option", { value: effect, children: VIDEO_CLIP_ANIMATION_LABELS[effect] }, effect))
|
|
604
|
+
]
|
|
605
|
+
}
|
|
606
|
+
) }, key)),
|
|
539
607
|
/* @__PURE__ */ jsx4(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
|
|
540
608
|
/* @__PURE__ */ jsx4(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
|
|
541
609
|
clip.type === "media" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
@@ -581,7 +649,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
581
649
|
},
|
|
582
650
|
children: [
|
|
583
651
|
/* @__PURE__ */ jsx4("option", { value: "", children: "None" }),
|
|
584
|
-
VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ jsx4("option", { value: t, children: t
|
|
652
|
+
VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ jsx4("option", { value: t, children: VIDEO_TRANSITION_LABELS[t] }, t))
|
|
585
653
|
]
|
|
586
654
|
}
|
|
587
655
|
) }),
|
|
@@ -599,6 +667,33 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
599
667
|
if (raw.trim() && Number.isFinite(n) && n > 0) controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, duration: n } }, "Change transition length");
|
|
600
668
|
}
|
|
601
669
|
}
|
|
670
|
+
) }),
|
|
671
|
+
transition && videoTransitionHasDirection(transition.type) && /* @__PURE__ */ jsx4(Row, { label: "Direction", name: "transitionDirection", children: /* @__PURE__ */ jsx4(
|
|
672
|
+
"select",
|
|
673
|
+
{
|
|
674
|
+
className: field,
|
|
675
|
+
value: transition.direction ?? "right",
|
|
676
|
+
disabled,
|
|
677
|
+
"aria-label": "Transition direction",
|
|
678
|
+
onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
|
|
679
|
+
children: VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ jsx4("option", { value: d, children: d }, d))
|
|
680
|
+
}
|
|
681
|
+
) }),
|
|
682
|
+
transition && /* @__PURE__ */ jsx4(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ jsx4(
|
|
683
|
+
"select",
|
|
684
|
+
{
|
|
685
|
+
className: field,
|
|
686
|
+
value: videoTransitionAudioCurve(transition),
|
|
687
|
+
disabled,
|
|
688
|
+
"aria-label": "Transition sound curve",
|
|
689
|
+
onChange: (e) => {
|
|
690
|
+
const { audioCurve: _, ...rest } = transition;
|
|
691
|
+
const curve = e.target.value;
|
|
692
|
+
const next = curve === videoTransitionAudioCurve(rest) ? rest : { ...rest, audioCurve: curve };
|
|
693
|
+
controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
|
|
694
|
+
},
|
|
695
|
+
children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ jsx4("option", { value: c, children: VIDEO_FADE_CURVE_LABELS[c] }, c))
|
|
696
|
+
}
|
|
602
697
|
) })
|
|
603
698
|
] });
|
|
604
699
|
}
|
|
@@ -754,10 +849,122 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
754
849
|
);
|
|
755
850
|
}
|
|
756
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
|
+
|
|
757
964
|
// src/video-editor/VideoEditor.tsx
|
|
758
965
|
import { useEffect as useEffect4 } from "react";
|
|
759
|
-
import { jsx as
|
|
760
|
-
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 = "" }) {
|
|
761
968
|
const { controller, state } = useVideoEditor(given ?? options ?? {});
|
|
762
969
|
useEffect4(() => {
|
|
763
970
|
if (editable) return;
|
|
@@ -766,23 +973,25 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
|
|
|
766
973
|
return () => controller.setReadOnly(was);
|
|
767
974
|
}, [controller, editable]);
|
|
768
975
|
const { issues } = state;
|
|
769
|
-
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */
|
|
770
|
-
return /* @__PURE__ */
|
|
771
|
-
/* @__PURE__ */
|
|
772
|
-
editable && /* @__PURE__ */
|
|
773
|
-
/* @__PURE__ */
|
|
774
|
-
/* @__PURE__ */
|
|
775
|
-
/* @__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 })
|
|
776
983
|
] }),
|
|
777
|
-
editable && /* @__PURE__ */
|
|
984
|
+
editable && /* @__PURE__ */ jsx7(ClipInspector, { controller, className: "min-w-0" })
|
|
778
985
|
] }),
|
|
779
|
-
/* @__PURE__ */
|
|
986
|
+
/* @__PURE__ */ jsx7(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
|
|
987
|
+
showTranscript && /* @__PURE__ */ jsx7(TranscriptPanel, { controller, editable }),
|
|
780
988
|
issueList
|
|
781
989
|
] });
|
|
782
990
|
}
|
|
783
991
|
export {
|
|
784
992
|
ClipInspector,
|
|
785
993
|
MediaBin,
|
|
994
|
+
TranscriptPanel,
|
|
786
995
|
VideoEditor,
|
|
787
996
|
VideoPreview,
|
|
788
997
|
VideoTimeline,
|