@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 === "crossfade" ? "Crossfade" : t === "dip" ? "Dip to colour" : "Wipe" }, 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 jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
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__ */ jsx6("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ jsx6("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)) });
770
- return /* @__PURE__ */ jsxs5("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
771
- /* @__PURE__ */ jsxs5("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
772
- editable && /* @__PURE__ */ jsx6(MediaBin, { controller, className: "min-w-0" }),
773
- /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-col gap-2", children: [
774
- /* @__PURE__ */ jsx6(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
775
- /* @__PURE__ */ jsx6(VideoTransport, { controller, editable })
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__ */ jsx6(ClipInspector, { controller, className: "min-w-0" })
984
+ editable && /* @__PURE__ */ jsx7(ClipInspector, { controller, className: "min-w-0" })
778
985
  ] }),
779
- /* @__PURE__ */ jsx6(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
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,