mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
feat(studio,lint): carve targets voiceover groups — always, when plural
Plural voiceover carve now targets a group instead of naming each clip: `resolveCarveSourceIds` (core `audioGroups.ts`) expands a group id to its current members at analysis time, so a clip added to the group later is covered without touching `sources`. The picker (`useFxCarve.ts`) offers a grouped voice as one option instead of one row per member, tests overlap as a union of member spans (a group overlaps the bed if ANY member does), and prefers a qualifying group over its individual members in `autoSourceIds`. Picking two or more ungrouped voice clips in the carve flow now mints a group behind them (`mintGroupId`, de-duped against every id in the document) and writes `data-audio-group` on each picked clip atomically, one undo entry — `createAudioGroupAndAssignMembers` in `timelineTrackVisibility.ts` copies `setElementsHidden`'s multi-target write shape. The DSP is untouched: `mixCarveSources` already sums multiple sources correctly (verified in the design doc's own investigation) — this only fixes the picker. New lint rule `audio_carve_ungrouped_sources` (`packages/lint/src/rules/ media.ts`, alongside `audio_volume_double_automation`) warns when a `data-fx-carve`'s `sources` names two or more plain clip ids instead of a group — the shape that silently rots when a clip is added. `/hyperframes- audio` states the same rule as an invariant, not a tip, with the grouped- narration HTML example from the design doc. The group-matching and auto-group logic (`withAutoGroupedSources`, `collectCarveCandidates`) is split into `useFxCarveGrouping.ts` — `useFxCarve.ts` was pushing past the 600-line cap. `resolveNextCarveSettings` is deliberately NOT an `async function`: wrapping it in one would force a microtask on every call, including the synchronous branch — the exact bug `withAutoGroupedSources`'s own sync-when-possible contract exists to avoid, and one caught via `propertyPanelAudioFxGroup.test.tsx` (10 failures) before fixing it back to a plain function the caller conditionally awaits. Also extracted `useEffectiveTimelineDuration` out of `App.tsx` and `useRemoveBackground` out of `StudioRightPanel.tsx` (both pushed past 600 lines from an added prop wire), and decomposed `useFxCarve.ts`'s picker IIFE to clear fallow's complexity gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8d48a6f52f
commit
1abad17650
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useCallback } from "react";
|
||||
import type { StudioRightPanelProps } from "./StudioRightPanel.types";
|
||||
|
||||
export type { StudioRightPanelProps };
|
||||
@@ -20,15 +20,14 @@ import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
||||
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
||||
import { useDomEditContext } from "../contexts/DomEditContext";
|
||||
import { usePlayerStore } from "../player";
|
||||
import { waitForMediaJob } from "./studioMediaJobs";
|
||||
import {
|
||||
applyColorGradingScopeUpdate,
|
||||
EMPTY_COLOR_GRADING_SCOPE_RESULT,
|
||||
type ColorGradingScope,
|
||||
} from "./studioColorGradingScope";
|
||||
import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
|
||||
import { timelineKeysForSelections } from "../utils/studioHelpers";
|
||||
import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";
|
||||
import { useRemoveBackground } from "../hooks/useRemoveBackground";
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function StudioRightPanel({
|
||||
@@ -45,6 +44,7 @@ export function StudioRightPanel({
|
||||
domEditSaveTimestampRef,
|
||||
recordEdit,
|
||||
onToggleElementHidden,
|
||||
onAutoGroupCarveSources,
|
||||
onAddMediaOverlay,
|
||||
}: StudioRightPanelProps) {
|
||||
const {
|
||||
@@ -163,14 +163,6 @@ export function StudioRightPanel({
|
||||
handleInspectorSplitResizeMove,
|
||||
handleInspectorSplitResizeEnd,
|
||||
} = useInspectorSplitResize();
|
||||
const backgroundRemovalAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
backgroundRemovalAbortRef.current?.abort();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const renderJobs = renderQueue.jobs as RenderJob[];
|
||||
const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
|
||||
@@ -237,52 +229,7 @@ export function StudioRightPanel({
|
||||
],
|
||||
);
|
||||
|
||||
const handleRemoveBackground = useCallback(
|
||||
// fallow-ignore-next-line complexity
|
||||
async (
|
||||
inputPath: string,
|
||||
options: {
|
||||
createBackgroundPlate?: boolean;
|
||||
quality?: "fast" | "balanced" | "best";
|
||||
onProgress?: (progress: BackgroundRemovalProgress) => void;
|
||||
},
|
||||
) => {
|
||||
const response = await fetch(
|
||||
`/api/projects/${encodeURIComponent(projectId)}/media/remove-background`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
inputPath,
|
||||
createBackgroundPlate: options.createBackgroundPlate === true,
|
||||
quality: options.quality ?? "balanced",
|
||||
}),
|
||||
},
|
||||
);
|
||||
const data = (await response.json().catch(() => ({}))) as {
|
||||
jobId?: string;
|
||||
error?: string;
|
||||
};
|
||||
if (!response.ok || !data.jobId) {
|
||||
throw new Error(data.error || `Background removal failed (${response.status})`);
|
||||
}
|
||||
showToast("Removing background...", "info");
|
||||
backgroundRemovalAbortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
backgroundRemovalAbortRef.current = controller;
|
||||
try {
|
||||
const result = await waitForMediaJob(data.jobId, options.onProgress, controller.signal);
|
||||
await refreshFileTree();
|
||||
showToast(`Created transparent asset: ${result.outputPath.split("/").pop()}`, "info");
|
||||
return result;
|
||||
} finally {
|
||||
if (backgroundRemovalAbortRef.current === controller) {
|
||||
backgroundRemovalAbortRef.current = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
[projectId, refreshFileTree, showToast],
|
||||
);
|
||||
const handleRemoveBackground = useRemoveBackground(projectId, refreshFileTree, showToast);
|
||||
|
||||
/**
|
||||
* A dial being dragged writes to the preview and stops there.
|
||||
@@ -328,6 +275,7 @@ export function StudioRightPanel({
|
||||
copiedAgentPrompt={copiedAgentPrompt}
|
||||
onClearSelection={clearDomSelection}
|
||||
onToggleElementHidden={onToggleElementHidden}
|
||||
onAutoGroupCarveSources={onAutoGroupCarveSources}
|
||||
onUngroup={handleUngroupSelection}
|
||||
onSetStyle={handleDomStyleCommit}
|
||||
onSetAttribute={handleDomAttributeCommit}
|
||||
|
||||
Reference in New Issue
Block a user