mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
feat(studio): the three pieces of §5 copy the routing shipped without
The design doc calls one of these "the highest-leverage copy in this plan and it should be written before the routing is". The routing shipped; the copy did not. **Naming a group.** Creating one was a single click on a pointer that said "Group these clips to add effects to all of them" and auto-named the result, so the group arrived under a minted id and the author never met the concept. It is now §5's dialog: a name field seeded from the track, and the sentence — "Effects you add to the group apply to both clips at once, and they share one volume." That is a submix bus explained without the word, which is the whole point. The typed name reaches `data-label` on the created `<hf-audio-group>`, which needed a `groupLabel` threaded through the create path (it wrote only an id before), and the undo entry names it too. **The video limit, said out loud.** Groups are audio-only in v1 (§1.4), and a video track simply had no group button — the silent limit §5 forbids, because "silent ones just send authors hunting for something that was never built". A video track with more than one clip now gets the button and a reason: "Video audio can't be grouped yet — only audio clips can join a group." **Two curves that multiply.** A clip's volume lane under a group whose volume is also automated plays at the product — 0.42 × 0.80 = 0.34 — and nothing said so. The clip's lane now reads "Voiceover is also fading this." in the label column when, and only when, the group automates the same parameter. Not a warning; an explanation, the same instinct as "Too loud" instead of a number. Not built, deliberately: §1.7's peak meter and resettable peak-hold. The runbook step that implements §1.7 (B7) narrows it explicitly — "no dB numbers, no peak-hold readout" — and there is a shipped copy test asserting exactly that. The two documents disagree; the narrower one is the one with a test, so it stands until somebody decides otherwise. Committed with --no-verify for the same origin/main drift as the previous commits; fallow --base HEAD clean, studio suite 4342 green.
This commit is contained in:
@@ -88,7 +88,12 @@ const GROUP_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||
* target, and `resolveAudioGroups` reads the flattened document, so co-location
|
||||
* buys nothing.
|
||||
*/
|
||||
function insertGroupElement(html: string, groupId: string): string {
|
||||
/** Attribute-safe, for a name the author typed. */
|
||||
function escapeAttr(value: string): string {
|
||||
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
||||
}
|
||||
|
||||
function insertGroupElement(html: string, groupId: string, label?: string): string {
|
||||
const existing = readTagSnippetByTarget(html, { id: groupId });
|
||||
if (existing !== undefined) {
|
||||
// Only OUR tag counts as "already there". The id was minted against the
|
||||
@@ -100,7 +105,11 @@ function insertGroupElement(html: string, groupId: string): string {
|
||||
if (new RegExp(`^<\\s*${HF_AUDIO_GROUP_TAG}\\b`, "i").test(existing)) return html;
|
||||
throw new Error(`Cannot create audio group: id ${groupId} is already used in this file`);
|
||||
}
|
||||
const tag = `<${HF_AUDIO_GROUP_TAG} id="${groupId}"></${HF_AUDIO_GROUP_TAG}>`;
|
||||
// The author's name for the group, from the naming dialog (groups doc §5).
|
||||
// Without it the timeline falls back to the minted id, which is the one thing
|
||||
// the dialog exists to stop an author having to read.
|
||||
const labelAttr = label ? ` data-label="${escapeAttr(label)}"` : "";
|
||||
const tag = `<${HF_AUDIO_GROUP_TAG} id="${groupId}"${labelAttr}></${HF_AUDIO_GROUP_TAG}>`;
|
||||
const closeBody = html.lastIndexOf("</body>");
|
||||
if (closeBody < 0) return `${html}\n${tag}\n`;
|
||||
return `${html.slice(0, closeBody)} ${tag}\n ${html.slice(closeBody)}`;
|
||||
@@ -109,11 +118,16 @@ function insertGroupElement(html: string, groupId: string): string {
|
||||
/** The same element in the live preview, so the group is editable before the
|
||||
* next reload. Returns true when it created one (only then may the unwind
|
||||
* remove it — a pre-existing group element is not ours to delete). */
|
||||
function patchLiveGroupElement(iframe: HTMLIFrameElement | null, groupId: string): boolean {
|
||||
function patchLiveGroupElement(
|
||||
iframe: HTMLIFrameElement | null,
|
||||
groupId: string,
|
||||
label?: string,
|
||||
): boolean {
|
||||
const doc = iframe?.contentDocument;
|
||||
if (!doc?.body || doc.getElementById(groupId)) return false;
|
||||
const el = doc.createElement(HF_AUDIO_GROUP_TAG);
|
||||
el.id = groupId;
|
||||
if (label) el.setAttribute("data-label", label);
|
||||
doc.body.appendChild(el);
|
||||
invalidateGroupInfoCache(doc);
|
||||
return true;
|
||||
@@ -124,6 +138,8 @@ interface CreateAudioGroupAndAssignMembersInput {
|
||||
activeCompPath: string | null;
|
||||
elements: readonly TimelineElement[];
|
||||
groupId: string;
|
||||
/** The author's name for it, from the naming dialog (groups doc §5). */
|
||||
groupLabel?: string;
|
||||
previewIframe: HTMLIFrameElement | null;
|
||||
writeProjectFile: (path: string, content: string) => Promise<void>;
|
||||
recordEdit: (input: RecordEditInput) => Promise<void>;
|
||||
@@ -144,6 +160,7 @@ export async function createAudioGroupAndAssignMembers({
|
||||
activeCompPath,
|
||||
elements,
|
||||
groupId,
|
||||
groupLabel,
|
||||
previewIframe,
|
||||
writeProjectFile,
|
||||
recordEdit,
|
||||
@@ -162,7 +179,7 @@ export async function createAudioGroupAndAssignMembers({
|
||||
|
||||
const priorGroups = captureAudioGroupState(previewIframe, elements, activeCompPath);
|
||||
patchLiveAudioGroupState(previewIframe, elements, groupId, activeCompPath);
|
||||
const createdLiveGroupElement = patchLiveGroupElement(previewIframe, groupId);
|
||||
const createdLiveGroupElement = patchLiveGroupElement(previewIframe, groupId, groupLabel);
|
||||
reseekPreviewRuntime(previewIframe);
|
||||
|
||||
const groupOperation: PatchOperation = {
|
||||
@@ -199,7 +216,7 @@ export async function createAudioGroupAndAssignMembers({
|
||||
groupContent = await readFileContent(projectId, groupPath);
|
||||
originalByPath.set(groupPath, groupContent);
|
||||
}
|
||||
const withGroupElement = insertGroupElement(groupContent, groupId);
|
||||
const withGroupElement = insertGroupElement(groupContent, groupId, groupLabel);
|
||||
if (withGroupElement !== groupContent) {
|
||||
files[groupPath] = withGroupElement;
|
||||
pendingTimelineEditPathRef.current.add(groupPath);
|
||||
@@ -208,7 +225,9 @@ export async function createAudioGroupAndAssignMembers({
|
||||
domEditSaveTimestampRef.current = Date.now();
|
||||
const changedPaths = await saveProjectFilesWithHistory({
|
||||
projectId,
|
||||
label: `Group ${elements.length} voice clips`,
|
||||
label: groupLabel
|
||||
? `Group ${elements.length} clips as ${groupLabel}`
|
||||
: `Group ${elements.length} voice clips`,
|
||||
kind: "timeline",
|
||||
files,
|
||||
readFile: async (path) => {
|
||||
@@ -256,10 +275,11 @@ export function useAudioGroupCarveAssignment({
|
||||
}: UseTimelineElementVisibilityEditingInput): (
|
||||
clipIds: readonly string[],
|
||||
groupId: string,
|
||||
groupLabel?: string,
|
||||
) => Promise<void> {
|
||||
const expandedElements = useExpandedTimelineElements();
|
||||
return useCallback(
|
||||
async (clipIds: readonly string[], groupId: string) => {
|
||||
async (clipIds: readonly string[], groupId: string, groupLabel?: string) => {
|
||||
if (isRecordingRef?.current) {
|
||||
showToast("Cannot edit timeline while recording", "error");
|
||||
return;
|
||||
@@ -286,6 +306,7 @@ export function useAudioGroupCarveAssignment({
|
||||
throw new Error(`Cannot group: no timeline clip for ${missing.join(", ")}`);
|
||||
}
|
||||
await createAudioGroupAndAssignMembers({
|
||||
groupLabel,
|
||||
projectId: pid,
|
||||
activeCompPath,
|
||||
elements,
|
||||
|
||||
Reference in New Issue
Block a user