@hyperframes/studio 0.8.106 → 0.8.108
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.
- package/dist/assets/{hyperframes-player-dl6K6ABp.js → hyperframes-player-BakGew45.js} +1 -1
- package/dist/assets/{index-ooaZZhKf.js → index-C5dyUHwk.js} +1 -1
- package/dist/assets/{index-CMiYVGv5.js → index-C842tcVa.js} +203 -208
- package/dist/assets/{index-DKT5NF1Y.js → index-CPe7d_wO.js} +1 -1
- package/dist/assets/{index-lrT-gfHa.css → index-ncVJ5IVu.css} +1 -1
- package/dist/chunk-6LUNLTHR.js +611 -0
- package/dist/chunk-6LUNLTHR.js.map +1 -0
- package/dist/{chunk-ICTZJPQQ.js → chunk-AOWVEVEU.js} +3 -494
- package/dist/chunk-AOWVEVEU.js.map +1 -0
- package/dist/{chunk-46XP7J3B.js → chunk-PQFEBX32.js} +5 -3
- package/dist/chunk-PQFEBX32.js.map +1 -0
- package/dist/{domEditingLayers-AQBDFNUC.js → domEditingLayers-JQPQGGT7.js} +3 -3
- package/dist/index.d.ts +377 -12
- package/dist/index.html +2 -2
- package/dist/index.js +13445 -11874
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/App.tsx +6 -3
- package/src/agentToolsExport.test.ts +8 -0
- package/src/captions/keyboard.test.ts +6 -0
- package/src/components/EditorShell.tsx +2 -0
- package/src/components/StudioRightPanels.tsx +2 -0
- package/src/components/TimelineToolbar.test.tsx +27 -0
- package/src/components/TimelineToolbar.tsx +2 -5
- package/src/components/editor/CropPresetBar.tsx +69 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +44 -0
- package/src/components/editor/DomEditCropHandles.tsx +123 -95
- package/src/components/editor/DomEditOverlay.tsx +12 -18
- package/src/components/editor/PropertyPanel.tsx +1 -14
- package/src/components/editor/PropertyPanelFlat.tsx +3 -6
- package/src/components/editor/SnapToolbar.test.tsx +32 -1
- package/src/components/editor/SnapToolbar.tsx +3 -4
- package/src/components/editor/carveOutput.ts +54 -0
- package/src/components/editor/cropPresetStore.ts +33 -0
- package/src/components/editor/cropPresets.test.ts +41 -0
- package/src/components/editor/cropPresets.ts +24 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
- package/src/components/editor/domEditingLayers.test.ts +21 -0
- package/src/components/editor/domEditingLayers.ts +2 -0
- package/src/components/editor/dragUndoPaint.test.ts +71 -0
- package/src/components/editor/fontAssets.test.ts +12 -22
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +96 -33
- package/src/components/editor/gsapAnimatesProperty.ts +3 -0
- package/src/components/editor/manualEdits.test.ts +4 -3
- package/src/components/editor/manualEditsDom.ts +28 -5
- package/src/components/editor/manualEditsDomPatches.ts +5 -1
- package/src/components/editor/manualOffsetDrag.ts +15 -1
- package/src/components/editor/mediaAudioEdits.test.ts +271 -0
- package/src/components/editor/mediaAudioEdits.ts +282 -0
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +32 -4
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +3 -1
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +290 -87
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +17 -20
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +17 -27
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useDomEditNudge.test.tsx +134 -2
- package/src/components/editor/useDomEditNudge.ts +46 -23
- package/src/components/editor/useDomEditOverlayGestures.ts +17 -6
- package/src/components/editor/useFxCarve.ts +3 -61
- package/src/components/editor/useFxCarveGrouping.test.ts +13 -0
- package/src/components/editor/useFxCarveGrouping.ts +8 -3
- package/src/components/nle/AudioMeterStrip.test.tsx +53 -0
- package/src/components/nle/AudioMeterStrip.tsx +60 -2
- package/src/components/nle/useTimelineEditCallbacks.ts +4 -0
- package/src/components/panels/SlideshowPanel.test.ts +27 -0
- package/src/components/panels/SlideshowPanel.tsx +12 -7
- package/src/contexts/DomEditContext.test.tsx +62 -0
- package/src/contexts/DomEditContext.tsx +94 -77
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/hooks/canvasQuickSaves.test.tsx +72 -7
- package/src/hooks/domEditCommitTypes.ts +10 -0
- package/src/hooks/domEditPersistFailure.ts +12 -1
- package/src/hooks/domStyleCommit.fonts.test.ts +5 -4
- package/src/hooks/domStyleCommit.ts +2 -8
- package/src/hooks/gsapDragCommit.ts +3 -8
- package/src/hooks/gsapDragPositionCommit.ts +6 -1
- package/src/hooks/gsapEditOutcome.ts +2 -1
- package/src/hooks/gsapResizeElementSize.test.tsx +136 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +3 -1
- package/src/hooks/gsapResizeIntercept.test.ts +3 -11
- package/src/hooks/gsapResizeIntercept.ts +6 -2
- package/src/hooks/gsapResizeMixedTween.test.ts +1 -2
- package/src/hooks/gsapRuntimeBridge.test.ts +55 -0
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +1 -3
- package/src/hooks/inspectorSaveReload.test.tsx +107 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +16 -1
- package/src/hooks/useAppHotkeys.test.ts +17 -0
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -1
- package/src/hooks/useAppHotkeys.ts +3 -3
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +17 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +3 -9
- package/src/hooks/useDomEditAttributeCommits.ts +71 -29
- package/src/hooks/useDomEditCommits.test.tsx +172 -92
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditCommitsHelpers.ts +4 -6
- package/src/hooks/useDomEditPersist.ts +28 -13
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -3
- package/src/hooks/useDomEditTextCommits.ts +4 -8
- package/src/hooks/useDomGeometryCommits.ts +11 -2
- package/src/hooks/useEditHistoryActions.paint.test.tsx +163 -3
- package/src/hooks/useEditHistoryActions.ts +13 -4
- package/src/hooks/useElementPicker.test.ts +62 -13
- package/src/hooks/useElementPicker.ts +10 -2
- package/src/hooks/useFreezeFrame.test.ts +56 -0
- package/src/hooks/useFreezeFrame.ts +121 -0
- package/src/hooks/useGsapAwareEditing.ts +6 -6
- package/src/hooks/useGsapScriptCommits.test.tsx +19 -0
- package/src/hooks/useGsapScriptCommits.ts +5 -4
- package/src/hooks/useInlineTextEdit.test.tsx +27 -0
- package/src/hooks/useInlineTextEdit.ts +9 -7
- package/src/hooks/useKeyframeKeyboard.test.tsx +15 -0
- package/src/hooks/useKeyframeKeyboard.ts +2 -8
- package/src/hooks/usePersistentEditHistory.test.ts +9 -6
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/usePreviewPersistence.ts +6 -0
- package/src/hooks/useRazorSplit.ts +14 -1
- package/src/hooks/useRenderClipContent.test.ts +102 -0
- package/src/hooks/useRenderClipContent.ts +30 -23
- package/src/hooks/useTimelineEditing.ts +2 -2
- package/src/hooks/useTrackPendingTimelineEdit.ts +2 -6
- package/src/index.ts +8 -0
- package/src/player/components/AudibleVideoClipContent.tsx +29 -0
- package/src/player/components/ClipBadges.test.tsx +82 -0
- package/src/player/components/ClipBadges.tsx +67 -0
- package/src/player/components/ClipContextMenu.tsx +22 -0
- package/src/player/components/ClipPeakMarks.test.tsx +41 -0
- package/src/player/components/ClipPeakMarks.tsx +71 -0
- package/src/player/components/Timeline.test.ts +0 -21
- package/src/player/components/Timeline.tsx +0 -1
- package/src/player/components/TimelineClip.tsx +2 -0
- package/src/player/components/TimelineFxButton.test.tsx +2 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +37 -0
- package/src/player/components/TimelineTrackHeader.tsx +12 -25
- package/src/player/components/clipMenuAudioItems.test.tsx +125 -0
- package/src/player/components/clipMenuAudioItems.tsx +108 -0
- package/src/player/components/clipMenuDuck.test.ts +117 -0
- package/src/player/components/clipMenuDuck.ts +142 -0
- package/src/player/components/clipMenuNormalize.test.ts +95 -0
- package/src/player/components/clipMenuNormalize.ts +96 -0
- package/src/player/components/clipMenuToolItems.test.tsx +139 -0
- package/src/player/components/clipMenuToolItems.tsx +235 -0
- package/src/player/components/clipPeakRuns.test.ts +38 -0
- package/src/player/components/clipPeakRuns.ts +59 -0
- package/src/player/components/clipToolAttrs.test.ts +161 -0
- package/src/player/components/clipToolAttrs.ts +187 -0
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineLayout.ts +0 -28
- package/src/player/components/useClipToolState.ts +92 -0
- package/src/player/hooks/usePlaybackKeyboard.test.ts +43 -0
- package/src/player/lib/playbackShortcuts.ts +3 -7
- package/src/player/lib/timelineElementHelpers.test.ts +16 -0
- package/src/player/lib/timelineElementHelpers.ts +25 -2
- package/src/player/lib/timelineViewportBudgets.test.ts +2 -0
- package/src/player/lib/timelineViewportBudgets.ts +4 -0
- package/src/timelineChromeExports.test.tsx +7 -0
- package/src/utils/audioMeterMath.test.ts +37 -0
- package/src/utils/audioMeterMath.ts +9 -3
- package/src/utils/exportPeakCeiling.ts +1 -0
- package/src/utils/gsapSoftReload.ts +3 -2
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -0
- package/src/utils/gsapUndoRestore.test.ts +145 -1
- package/src/utils/gsapUndoRestore.ts +70 -21
- package/src/utils/previewRelativePath.ts +19 -0
- package/src/utils/sourcePatcher.ts +2 -2
- package/src/utils/studioFontHelpers.ts +2 -23
- package/src/utils/studioHelpers.test.ts +30 -0
- package/src/utils/studioHelpers.ts +18 -4
- package/src/utils/studioPendingEdits.test.ts +129 -0
- package/src/utils/studioPendingEdits.ts +61 -18
- package/src/utils/timelineInspector.test.ts +33 -2
- package/src/utils/timelineInspector.ts +16 -4
- package/src/utils/typingTarget.test.ts +13 -0
- package/src/utils/typingTarget.ts +5 -2
- package/src/webmcp/useStudioAgentTools.ts +3 -1
- package/dist/chunk-46XP7J3B.js.map +0 -1
- package/dist/chunk-DI4LCJHL.js +0 -102
- package/dist/chunk-DI4LCJHL.js.map +0 -1
- package/dist/chunk-ICTZJPQQ.js.map +0 -1
- /package/dist/{domEditingLayers-AQBDFNUC.js.map → domEditingLayers-JQPQGGT7.js.map} +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HF_AUDIO_AUTOMATION_ATTR,
|
|
3
|
+
RATE_TARGET,
|
|
4
|
+
parseAutomation,
|
|
5
|
+
serializeAutomation,
|
|
6
|
+
type HfAutomationLane,
|
|
7
|
+
} from "@hyperframes/core/audio-automation";
|
|
8
|
+
import { HF_AUDIO_CARVE_ATTR } from "@hyperframes/core/audio-carve";
|
|
9
|
+
import { HF_AUDIO_FADE_IN_ATTR, HF_AUDIO_FADE_OUT_ATTR } from "@hyperframes/core/audio-fade";
|
|
10
|
+
import { HF_AUDIO_FX_ATTR } from "@hyperframes/core/audio-fx";
|
|
11
|
+
import { HF_AUDIO_GROUP_ATTR } from "@hyperframes/core/audio-groups";
|
|
12
|
+
import {
|
|
13
|
+
escapeHtmlAttribute,
|
|
14
|
+
findTagByTarget,
|
|
15
|
+
applyPatchByTarget,
|
|
16
|
+
readAttributeByTarget,
|
|
17
|
+
unescapeHtmlAttribute,
|
|
18
|
+
type PatchOperation,
|
|
19
|
+
type PatchTarget,
|
|
20
|
+
} from "../../utils/sourcePatcher";
|
|
21
|
+
import { collectHtmlIds, resolveAssetHasAudio } from "../../utils/studioHelpers";
|
|
22
|
+
import type { CommitDomAttributeBatch } from "../../hooks/domEditCommitTypes";
|
|
23
|
+
import { buildDomEditPatchTarget, type DomEditSelection } from "./domEditing";
|
|
24
|
+
import { generateId } from "../../utils/generateId";
|
|
25
|
+
|
|
26
|
+
const MEDIA_LINK_ATTR = "data-link";
|
|
27
|
+
const SOUND_KEPT_ON_LINKED_AUDIO_STAGE = "Background removed. Sound kept on a linked audio track.";
|
|
28
|
+
|
|
29
|
+
const SOUND_NOT_KEPT_MESSAGE =
|
|
30
|
+
"Background removed, but the sound could not be kept on a linked audio track. Undo to restore the original clip.";
|
|
31
|
+
|
|
32
|
+
const MOVED_SOUND_ATTRS = [
|
|
33
|
+
"data-volume",
|
|
34
|
+
HF_AUDIO_FADE_IN_ATTR,
|
|
35
|
+
HF_AUDIO_FADE_OUT_ATTR,
|
|
36
|
+
HF_AUDIO_FX_ATTR,
|
|
37
|
+
HF_AUDIO_CARVE_ATTR,
|
|
38
|
+
HF_AUDIO_GROUP_ATTR,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const COPIED_TIMING_ATTRS = [
|
|
42
|
+
"start",
|
|
43
|
+
"end",
|
|
44
|
+
"duration",
|
|
45
|
+
"media-start",
|
|
46
|
+
"playback-start",
|
|
47
|
+
"playback-rate",
|
|
48
|
+
"source-duration",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const htmlAttr = (property: string, value: string | null): PatchOperation => ({
|
|
52
|
+
type: "html-attribute",
|
|
53
|
+
property,
|
|
54
|
+
value,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const dataAttr = (property: string, value: string | null): PatchOperation => ({
|
|
58
|
+
type: "attribute",
|
|
59
|
+
property,
|
|
60
|
+
value,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function stripDataPrefix(name: string): string {
|
|
64
|
+
return name.startsWith("data-") ? name.slice("data-".length) : name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function probedHasAudioValue(probedHasAudio: boolean | null): string | null {
|
|
68
|
+
if (probedHasAudio === null) return null;
|
|
69
|
+
return probedHasAudio ? "true" : "false";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function mutedToggleOps(input: {
|
|
73
|
+
isVideo: boolean;
|
|
74
|
+
nextMuted: boolean;
|
|
75
|
+
probedHasAudio: boolean | null;
|
|
76
|
+
}): PatchOperation[] {
|
|
77
|
+
const muted = htmlAttr("muted", input.nextMuted ? "true" : null);
|
|
78
|
+
if (!input.isVideo) return [muted];
|
|
79
|
+
const hasAudio = input.nextMuted ? null : probedHasAudioValue(input.probedHasAudio);
|
|
80
|
+
return [muted, dataAttr("has-audio", hasAudio)];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function hasAudioToggleOps(next: boolean): PatchOperation[] {
|
|
84
|
+
return next
|
|
85
|
+
? [dataAttr("has-audio", "true"), htmlAttr("muted", null)]
|
|
86
|
+
: [dataAttr("has-audio", null), htmlAttr("muted", "true")];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function cutoutOps(input: { isVideo: boolean; cutoutSrc: string }): PatchOperation[] {
|
|
90
|
+
const src = htmlAttr("src", input.cutoutSrc);
|
|
91
|
+
return input.isVideo ? [src, htmlAttr("muted", "true"), dataAttr("has-audio", null)] : [src];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function firstFreeName(base: string, taken: Set<string>): string {
|
|
95
|
+
if (!taken.has(base)) return base;
|
|
96
|
+
let n = 2;
|
|
97
|
+
while (taken.has(`${base}-${n}`)) n += 1;
|
|
98
|
+
return `${base}-${n}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function mintLinkId(doc: Document): string {
|
|
102
|
+
const taken = new Set<string>();
|
|
103
|
+
for (const el of Array.from(doc.querySelectorAll(`[id], [${MEDIA_LINK_ATTR}]`))) {
|
|
104
|
+
if (el.id) taken.add(el.id);
|
|
105
|
+
const link = el.getAttribute(MEDIA_LINK_ATTR);
|
|
106
|
+
if (link) taken.add(link);
|
|
107
|
+
}
|
|
108
|
+
return firstFreeName("link", taken);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function firstFreeTrackIndex(source: string): number {
|
|
112
|
+
const used = new Set(
|
|
113
|
+
Array.from(source.matchAll(/\bdata-track-index=["'](-?\d+)["']/g), (m) => Number(m[1])),
|
|
114
|
+
);
|
|
115
|
+
let track = 0;
|
|
116
|
+
while (used.has(track)) track += 1;
|
|
117
|
+
return track;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function splitAutomation(raw: string): { videoKeeps: string | null; audioTakes: string } {
|
|
121
|
+
let lanes: HfAutomationLane[];
|
|
122
|
+
try {
|
|
123
|
+
lanes = parseAutomation(raw).lanes;
|
|
124
|
+
} catch {
|
|
125
|
+
return { videoKeeps: null, audioTakes: raw };
|
|
126
|
+
}
|
|
127
|
+
const rateLanes = lanes.filter((lane) => lane.target === RATE_TARGET);
|
|
128
|
+
const soundLanes = lanes.filter((lane) => lane.target !== RATE_TARGET);
|
|
129
|
+
return {
|
|
130
|
+
videoKeeps: rateLanes.length > 0 ? serializeAutomation({ version: 1, lanes: rateLanes }) : null,
|
|
131
|
+
audioTakes: serializeAutomation({ version: 1, lanes: [...soundLanes, ...rateLanes] }),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatAttrs(attrs: Array<[string, string]>): string {
|
|
136
|
+
return attrs.map(([name, value]) => `${name}="${escapeHtmlAttribute(value)}"`).join(" ");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readAuthoredSrc(source: string, target: PatchTarget): string {
|
|
140
|
+
const tag = findTagByTarget(source, target);
|
|
141
|
+
if (!tag) return "";
|
|
142
|
+
const own = /\bsrc=(["'])([^"']*)\1/.exec(tag.tag);
|
|
143
|
+
if (own?.[2]) return unescapeHtmlAttribute(own[2]);
|
|
144
|
+
const firstSource = /^\s*<source\b[^>]*?\bsrc=(["'])([^"']*)\1/i.exec(source.slice(tag.end + 1));
|
|
145
|
+
return firstSource?.[2] ? unescapeHtmlAttribute(firstSource[2]) : "";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function insertBeforeTarget(source: string, target: PatchTarget, markup: string): string {
|
|
149
|
+
const match = findTagByTarget(source, target);
|
|
150
|
+
if (!match) return source;
|
|
151
|
+
const lineStart = source.lastIndexOf("\n", match.start) + 1;
|
|
152
|
+
const indent = /^[ \t]*$/.test(source.slice(lineStart, match.start))
|
|
153
|
+
? source.slice(lineStart, match.start)
|
|
154
|
+
: "";
|
|
155
|
+
const separator = indent ? `\n${indent}` : "";
|
|
156
|
+
return `${source.slice(0, match.start)}${markup}${separator}${source.slice(match.start)}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface KeepSoundCutoutEdit {
|
|
160
|
+
ops: PatchOperation[];
|
|
161
|
+
prepareContent: (source: string) => string;
|
|
162
|
+
audioInserted: () => boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function buildKeepSoundCutoutEdit(input: {
|
|
166
|
+
video: Element;
|
|
167
|
+
videoId: string | null;
|
|
168
|
+
target: PatchTarget;
|
|
169
|
+
cutoutSrc: string;
|
|
170
|
+
}): KeepSoundCutoutEdit {
|
|
171
|
+
const { video, target } = input;
|
|
172
|
+
const linkId = mintLinkId(video.ownerDocument);
|
|
173
|
+
let audioInserted = false;
|
|
174
|
+
|
|
175
|
+
const movedSound: Array<[string, string]> = [];
|
|
176
|
+
const cutoutSrcOp = htmlAttr("src", input.cutoutSrc);
|
|
177
|
+
const ops: PatchOperation[] = [htmlAttr("muted", "true"), dataAttr("has-audio", null)];
|
|
178
|
+
for (const name of MOVED_SOUND_ATTRS) {
|
|
179
|
+
const value = video.getAttribute(name);
|
|
180
|
+
if (value === null) continue;
|
|
181
|
+
movedSound.push([name, value]);
|
|
182
|
+
ops.push(dataAttr(stripDataPrefix(name), null));
|
|
183
|
+
}
|
|
184
|
+
const automation = video.getAttribute(HF_AUDIO_AUTOMATION_ATTR);
|
|
185
|
+
if (automation !== null) {
|
|
186
|
+
const { videoKeeps, audioTakes } = splitAutomation(automation);
|
|
187
|
+
movedSound.push([HF_AUDIO_AUTOMATION_ATTR, audioTakes]);
|
|
188
|
+
ops.push(dataAttr(stripDataPrefix(HF_AUDIO_AUTOMATION_ATTR), videoKeeps));
|
|
189
|
+
}
|
|
190
|
+
ops.push(dataAttr(stripDataPrefix(MEDIA_LINK_ATTR), linkId));
|
|
191
|
+
|
|
192
|
+
const prepareContent = (source: string): string => {
|
|
193
|
+
const originalSrc = readAuthoredSrc(source, target);
|
|
194
|
+
const audioId = firstFreeName(
|
|
195
|
+
`${input.videoId || "video"}-audio`,
|
|
196
|
+
new Set(collectHtmlIds(source)),
|
|
197
|
+
);
|
|
198
|
+
const timing: Array<[string, string]> = [];
|
|
199
|
+
for (const name of COPIED_TIMING_ATTRS) {
|
|
200
|
+
const value = readAttributeByTarget(source, target, name);
|
|
201
|
+
if (value !== undefined) timing.push([`data-${name}`, value]);
|
|
202
|
+
}
|
|
203
|
+
const attrs: Array<[string, string]> = [
|
|
204
|
+
["id", audioId],
|
|
205
|
+
["data-hf-id", `hf-${generateId()}`],
|
|
206
|
+
["class", "clip"],
|
|
207
|
+
["src", originalSrc],
|
|
208
|
+
...timing,
|
|
209
|
+
["data-track-index", String(firstFreeTrackIndex(source))],
|
|
210
|
+
[MEDIA_LINK_ATTR, linkId],
|
|
211
|
+
...movedSound,
|
|
212
|
+
];
|
|
213
|
+
if (video.hasAttribute("loop")) attrs.push(["loop", ""]);
|
|
214
|
+
const cut = applyPatchByTarget(source, target, cutoutSrcOp);
|
|
215
|
+
const inserted = insertBeforeTarget(cut, target, `<audio ${formatAttrs(attrs)}></audio>`);
|
|
216
|
+
audioInserted = inserted !== cut;
|
|
217
|
+
return inserted;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return { ops, prepareContent, audioInserted: () => audioInserted };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface MediaEditContext {
|
|
224
|
+
element: DomEditSelection;
|
|
225
|
+
projectId: string | null;
|
|
226
|
+
projectSrc: string;
|
|
227
|
+
commit: CommitDomAttributeBatch;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const isVideoSelection = (element: DomEditSelection) => element.tagName === "video";
|
|
231
|
+
|
|
232
|
+
export async function commitMutedToggle(ctx: MediaEditContext, nextMuted: boolean): Promise<void> {
|
|
233
|
+
const isVideo = isVideoSelection(ctx.element);
|
|
234
|
+
const probedHasAudio =
|
|
235
|
+
isVideo && !nextMuted && ctx.projectId && ctx.projectSrc
|
|
236
|
+
? await resolveAssetHasAudio(ctx.projectId, ctx.projectSrc)
|
|
237
|
+
: null;
|
|
238
|
+
await ctx.commit(ctx.element, mutedToggleOps({ isVideo, nextMuted, probedHasAudio }), {
|
|
239
|
+
label: "Edit muted",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function commitHasAudioToggle(ctx: MediaEditContext, next: boolean): Promise<void> {
|
|
244
|
+
await ctx.commit(ctx.element, hasAudioToggleOps(next), { label: "Edit has audio" });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function commitCutout(
|
|
248
|
+
ctx: MediaEditContext,
|
|
249
|
+
cutoutSrc: string,
|
|
250
|
+
keepSound: boolean,
|
|
251
|
+
): Promise<string> {
|
|
252
|
+
const isVideo = isVideoSelection(ctx.element);
|
|
253
|
+
const label = "Remove background";
|
|
254
|
+
const keep = isVideo && keepSound;
|
|
255
|
+
if (!keep) {
|
|
256
|
+
const applied = await ctx.commit(ctx.element, cutoutOps({ isVideo, cutoutSrc }), { label });
|
|
257
|
+
if (!applied) throw new Error("Couldn't apply the cutout");
|
|
258
|
+
return "Applied cutout";
|
|
259
|
+
}
|
|
260
|
+
const { landed, soundKept } = await commitKeepSoundCutout(ctx, cutoutSrc, label);
|
|
261
|
+
if (!landed) throw new Error("Couldn't apply the cutout");
|
|
262
|
+
if (!soundKept) throw new Error(SOUND_NOT_KEPT_MESSAGE);
|
|
263
|
+
return SOUND_KEPT_ON_LINKED_AUDIO_STAGE;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function commitKeepSoundCutout(
|
|
267
|
+
ctx: MediaEditContext,
|
|
268
|
+
cutoutSrc: string,
|
|
269
|
+
label: string,
|
|
270
|
+
): Promise<{ landed: boolean; soundKept: boolean }> {
|
|
271
|
+
const edit = buildKeepSoundCutoutEdit({
|
|
272
|
+
video: ctx.element.element,
|
|
273
|
+
videoId: ctx.element.id ?? null,
|
|
274
|
+
target: buildDomEditPatchTarget(ctx.element),
|
|
275
|
+
cutoutSrc,
|
|
276
|
+
});
|
|
277
|
+
const landed = await ctx.commit(ctx.element, edit.ops, {
|
|
278
|
+
label,
|
|
279
|
+
prepareContent: edit.prepareContent,
|
|
280
|
+
});
|
|
281
|
+
return { landed, soundKept: landed && edit.audioInserted() };
|
|
282
|
+
}
|
|
@@ -103,11 +103,11 @@ function mount(dataAttributes: Record<string, string>, alone = false, voices = 2
|
|
|
103
103
|
return { host, onSetAttributeQuiet, onSetAttributeLive };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
function mountGroup(memberStart: number) {
|
|
106
|
+
function mountGroup(memberStart: number, memberTag: "audio" | "video" = "audio") {
|
|
107
107
|
const bus = document.createElement("hf-audio-group");
|
|
108
108
|
bus.id = "voiceover";
|
|
109
109
|
document.body.append(bus);
|
|
110
|
-
const member = document.createElement(
|
|
110
|
+
const member = document.createElement(memberTag);
|
|
111
111
|
member.id = "vo-1";
|
|
112
112
|
member.setAttribute("data-audio-group", "voiceover");
|
|
113
113
|
member.setAttribute("data-start", String(memberStart));
|
|
@@ -649,6 +649,16 @@ describe("AudioFxGroup dynamic carve", () => {
|
|
|
649
649
|
leaveShelf(host);
|
|
650
650
|
});
|
|
651
651
|
|
|
652
|
+
it("seeks a group audition to an audible video member's span", () => {
|
|
653
|
+
act(() =>
|
|
654
|
+
usePlayerStore.setState({ isPlaying: false, currentTime: 2, requestedSeekTime: null }),
|
|
655
|
+
);
|
|
656
|
+
const host = mountGroup(7, "video");
|
|
657
|
+
hoverPreset(host);
|
|
658
|
+
expect(store().requestedSeekTime).toBe(7);
|
|
659
|
+
leaveShelf(host);
|
|
660
|
+
});
|
|
661
|
+
|
|
652
662
|
it("leaves a transport the author started alone", () => {
|
|
653
663
|
// Stopping their playback because they passed over a preset would be the
|
|
654
664
|
// panel taking a decision nobody offered it.
|
|
@@ -1621,7 +1631,14 @@ describe("AudioFxGroup carve by default", () => {
|
|
|
1621
1631
|
describe("AudioFxGroup carve source list", () => {
|
|
1622
1632
|
/** Mount a bed alongside tracks named however the test needs. */
|
|
1623
1633
|
const mountWith = (
|
|
1624
|
-
tracks: {
|
|
1634
|
+
tracks: {
|
|
1635
|
+
id: string;
|
|
1636
|
+
src?: string;
|
|
1637
|
+
start?: string;
|
|
1638
|
+
duration?: string;
|
|
1639
|
+
tag?: "audio" | "video";
|
|
1640
|
+
muted?: boolean;
|
|
1641
|
+
}[],
|
|
1625
1642
|
bedAttrs: Record<string, string> = {},
|
|
1626
1643
|
) => {
|
|
1627
1644
|
const bed = document.createElement("audio");
|
|
@@ -1629,8 +1646,9 @@ describe("AudioFxGroup carve source list", () => {
|
|
|
1629
1646
|
for (const [k, v] of Object.entries(bedAttrs)) bed.setAttribute(`data-${k}`, v);
|
|
1630
1647
|
document.body.append(bed);
|
|
1631
1648
|
for (const t of tracks) {
|
|
1632
|
-
const el = document.createElement("audio");
|
|
1649
|
+
const el = document.createElement(t.tag ?? "audio");
|
|
1633
1650
|
el.id = t.id;
|
|
1651
|
+
if (t.muted) el.setAttribute("muted", "");
|
|
1634
1652
|
if (t.src) el.setAttribute("src", t.src);
|
|
1635
1653
|
if (t.start !== undefined) el.setAttribute("data-start", t.start);
|
|
1636
1654
|
if (t.duration !== undefined) el.setAttribute("data-duration", t.duration);
|
|
@@ -1660,6 +1678,16 @@ describe("AudioFxGroup carve source list", () => {
|
|
|
1660
1678
|
return { host, options, boxes, onSetAttributeQuiet };
|
|
1661
1679
|
};
|
|
1662
1680
|
|
|
1681
|
+
it("offers an audible video as a voice and never a muted one", () => {
|
|
1682
|
+
const { options } = mountWith([
|
|
1683
|
+
{ id: "a-roll", tag: "video" },
|
|
1684
|
+
{ id: "b-roll", tag: "video", muted: true },
|
|
1685
|
+
{ id: "narration" },
|
|
1686
|
+
]);
|
|
1687
|
+
expect(options).toContain("a-roll");
|
|
1688
|
+
expect(options).not.toContain("b-roll");
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1663
1691
|
it("reads the one voice out instead of offering a picker with one entry", () => {
|
|
1664
1692
|
// A question with one answer is not a question. It is also the common case: a
|
|
1665
1693
|
// narration and a bed, with a couple of stings that cannot be the voice.
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
resolveAudioGroups,
|
|
55
55
|
} from "@hyperframes/core/audio-groups";
|
|
56
56
|
import { useFxLevelling } from "./useFxLevelling.js";
|
|
57
|
+
import { isCarveVoiceElement } from "./useFxCarveGrouping.js";
|
|
57
58
|
|
|
58
59
|
function auditionSpan(startRaw: string | undefined, durationRaw: string | undefined) {
|
|
59
60
|
const start = Number.parseFloat(startRaw ?? "");
|
|
@@ -70,8 +71,9 @@ function auditionSpansFor(element: DomEditSelection): AuditionSpan[] {
|
|
|
70
71
|
if (element.tagName?.toLowerCase() !== HF_AUDIO_GROUP_TAG || !element.id) return [];
|
|
71
72
|
const doc = element.element?.ownerDocument;
|
|
72
73
|
if (!doc) return [];
|
|
73
|
-
return [...doc.querySelectorAll(`
|
|
74
|
+
return [...doc.querySelectorAll(`[${HF_AUDIO_GROUP_ATTR}]`)]
|
|
74
75
|
.filter((member) => member.getAttribute(HF_AUDIO_GROUP_ATTR) === element.id)
|
|
76
|
+
.filter(isCarveVoiceElement)
|
|
75
77
|
.flatMap((member) => {
|
|
76
78
|
const span = auditionSpan(
|
|
77
79
|
member.getAttribute("data-start") ?? undefined,
|