mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +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
5dc93a25ad
commit
351b219d2b
@@ -0,0 +1,70 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { waitForMediaJob } from "../components/studioMediaJobs";
|
||||
import type { BackgroundRemovalProgress } from "../components/editor/propertyPanelTypes";
|
||||
|
||||
interface RemoveBackgroundOptions {
|
||||
createBackgroundPlate?: boolean;
|
||||
quality?: "fast" | "balanced" | "best";
|
||||
onProgress?: (progress: BackgroundRemovalProgress) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* One removal in flight at a time: starting a second one aborts whichever job
|
||||
* is still running, so a stale progress callback can't overwrite a newer
|
||||
* result. Unmounting aborts too, or the job would keep running against a
|
||||
* panel that is no longer there to show its progress.
|
||||
*/
|
||||
export function useRemoveBackground(
|
||||
projectId: string,
|
||||
refreshFileTree: () => Promise<void>,
|
||||
showToast: (message: string, kind?: "info" | "error") => void,
|
||||
) {
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
abortRef.current?.abort();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return useCallback(
|
||||
// fallow-ignore-next-line complexity
|
||||
async (inputPath: string, options: RemoveBackgroundOptions) => {
|
||||
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");
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.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 (abortRef.current === controller) {
|
||||
abortRef.current = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
[projectId, refreshFileTree, showToast],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user