mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
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>
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
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],
|
|
);
|
|
}
|