mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 22:08:03 +00:00
style: fix oxfmt formatting in CaptionOverlay, CaptionPropertyPanel, and transcript-guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c014c45f02
commit
36cd1daff5
@@ -70,7 +70,10 @@ function readWordBoxes(
|
||||
if (!wordEl) continue;
|
||||
const rect = wordEl.getBoundingClientRect();
|
||||
boxes.push({
|
||||
segmentId: segId, groupId, groupIndex: gi, wordIndex: wi,
|
||||
segmentId: segId,
|
||||
groupId,
|
||||
groupIndex: gi,
|
||||
wordIndex: wi,
|
||||
x: rect.left * cssScale + offsetX,
|
||||
y: rect.top * cssScale + offsetY,
|
||||
width: rect.width * cssScale,
|
||||
@@ -81,9 +84,17 @@ function readWordBoxes(
|
||||
return boxes;
|
||||
}
|
||||
|
||||
function getWordEl(iframe: HTMLIFrameElement, groupIndex: number, wordIndex: number): HTMLElement | null {
|
||||
function getWordEl(
|
||||
iframe: HTMLIFrameElement,
|
||||
groupIndex: number,
|
||||
wordIndex: number,
|
||||
): HTMLElement | null {
|
||||
let doc: Document | null = null;
|
||||
try { doc = iframe.contentDocument; } catch { return null; }
|
||||
try {
|
||||
doc = iframe.contentDocument;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!doc) return null;
|
||||
const groupEl = doc.querySelectorAll<HTMLElement>(".caption-group")[groupIndex];
|
||||
if (!groupEl) return null;
|
||||
@@ -108,8 +119,13 @@ function getWordEl(iframe: HTMLIFrameElement, groupIndex: number, wordIndex: num
|
||||
* Read GSAP's internal transform state for an element.
|
||||
* GSAP stores transforms in its own cache, not in el.style.transform.
|
||||
*/
|
||||
function readGsapTransform(el: HTMLElement, iframeWin: Window): { x: number; y: number; scale: number; rotation: number } {
|
||||
const gsap = (iframeWin as unknown as { gsap?: { getProperty?: (el: HTMLElement, prop: string) => number } }).gsap;
|
||||
function readGsapTransform(
|
||||
el: HTMLElement,
|
||||
iframeWin: Window,
|
||||
): { x: number; y: number; scale: number; rotation: number } {
|
||||
const gsap = (
|
||||
iframeWin as unknown as { gsap?: { getProperty?: (el: HTMLElement, prop: string) => number } }
|
||||
).gsap;
|
||||
if (gsap && gsap.getProperty) {
|
||||
return {
|
||||
x: gsap.getProperty(el, "x") || 0,
|
||||
@@ -155,9 +171,20 @@ function getOrCreateWrapper(el: HTMLElement): HTMLElement {
|
||||
* Write transform values to a wrapper span around the word element.
|
||||
* The word keeps its GSAP animations; the wrapper handles editor transforms.
|
||||
*/
|
||||
function writeTransform(el: HTMLElement, iframeWin: Window, x: number, y: number, scale: number, rotation: number) {
|
||||
function writeTransform(
|
||||
el: HTMLElement,
|
||||
iframeWin: Window,
|
||||
x: number,
|
||||
y: number,
|
||||
scale: number,
|
||||
rotation: number,
|
||||
) {
|
||||
const wrapper = getOrCreateWrapper(el);
|
||||
const gsap = (iframeWin as unknown as { gsap?: { set?: (el: HTMLElement, props: Record<string, number>) => void } }).gsap;
|
||||
const gsap = (
|
||||
iframeWin as unknown as {
|
||||
gsap?: { set?: (el: HTMLElement, props: Record<string, number>) => void };
|
||||
}
|
||||
).gsap;
|
||||
if (gsap && gsap.set) {
|
||||
gsap.set(wrapper, { x, y, scale, rotation });
|
||||
} else {
|
||||
@@ -173,7 +200,10 @@ function syncToStore(segmentId: string, el: HTMLElement, iframeWin: Window) {
|
||||
const style: Record<string, number> = {};
|
||||
if (Math.abs(x) > 0.5) style.x = x;
|
||||
if (Math.abs(y) > 0.5) style.y = y;
|
||||
if (Math.abs(scale - 1) > 0.001) { style.scaleX = scale; style.scaleY = scale; }
|
||||
if (Math.abs(scale - 1) > 0.001) {
|
||||
style.scaleX = scale;
|
||||
style.scaleY = scale;
|
||||
}
|
||||
if (Math.abs(rotation) > 0.1) style.rotation = rotation;
|
||||
if (Object.keys(style).length > 0) {
|
||||
useCaptionStore.getState().updateSegmentStyle(segmentId, style);
|
||||
@@ -183,9 +213,7 @@ function syncToStore(segmentId: string, el: HTMLElement, iframeWin: Window) {
|
||||
const HANDLE = 8;
|
||||
const ROTATION_OFFSET = 20; // px above the selection box
|
||||
|
||||
export const CaptionOverlay = memo(function CaptionOverlay({
|
||||
iframeRef,
|
||||
}: CaptionOverlayProps) {
|
||||
export const CaptionOverlay = memo(function CaptionOverlay({ iframeRef }: CaptionOverlayProps) {
|
||||
const isEditMode = useCaptionStore((s) => s.isEditMode);
|
||||
const model = useCaptionStore((s) => s.model);
|
||||
const selectedSegmentIds = useCaptionStore((s) => s.selectedSegmentIds);
|
||||
@@ -199,9 +227,40 @@ export const CaptionOverlay = memo(function CaptionOverlay({
|
||||
|
||||
// Interaction mode — only one active at a time
|
||||
const interactionRef = useRef<
|
||||
| { type: "move"; wordEl: HTMLElement; segmentId: string; startMX: number; startMY: number; origTX: number; origTY: number; origScale: number; origRotation: number }
|
||||
| { type: "scale"; wordEl: HTMLElement; segmentId: string; startMX: number; startWidth: number; origTX: number; origTY: number; origScale: number; origRotation: number }
|
||||
| { type: "rotate"; wordEl: HTMLElement; segmentId: string; centerX: number; centerY: number; startAngle: number; origTX: number; origTY: number; origRotation: number; origScale: number }
|
||||
| {
|
||||
type: "move";
|
||||
wordEl: HTMLElement;
|
||||
segmentId: string;
|
||||
startMX: number;
|
||||
startMY: number;
|
||||
origTX: number;
|
||||
origTY: number;
|
||||
origScale: number;
|
||||
origRotation: number;
|
||||
}
|
||||
| {
|
||||
type: "scale";
|
||||
wordEl: HTMLElement;
|
||||
segmentId: string;
|
||||
startMX: number;
|
||||
startWidth: number;
|
||||
origTX: number;
|
||||
origTY: number;
|
||||
origScale: number;
|
||||
origRotation: number;
|
||||
}
|
||||
| {
|
||||
type: "rotate";
|
||||
wordEl: HTMLElement;
|
||||
segmentId: string;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
startAngle: number;
|
||||
origTX: number;
|
||||
origTY: number;
|
||||
origRotation: number;
|
||||
origScale: number;
|
||||
}
|
||||
| null
|
||||
>(null);
|
||||
|
||||
@@ -215,8 +274,13 @@ export const CaptionOverlay = memo(function CaptionOverlay({
|
||||
if (!iframe || !m || !overlay) return;
|
||||
const next = readWordBoxes(iframe, m, overlay);
|
||||
// Skip state update if nothing changed (avoids re-render every 66ms)
|
||||
if (next.length === prevBoxes.length &&
|
||||
next.every((b, i) => Math.abs(b.x - prevBoxes[i].x) < 0.5 && Math.abs(b.y - prevBoxes[i].y) < 0.5)) return;
|
||||
if (
|
||||
next.length === prevBoxes.length &&
|
||||
next.every(
|
||||
(b, i) => Math.abs(b.x - prevBoxes[i].x) < 0.5 && Math.abs(b.y - prevBoxes[i].y) < 0.5,
|
||||
)
|
||||
)
|
||||
return;
|
||||
prevBoxes = next;
|
||||
setWordBoxes(next);
|
||||
};
|
||||
@@ -273,93 +337,125 @@ export const CaptionOverlay = memo(function CaptionOverlay({
|
||||
}, [iframeRef]);
|
||||
|
||||
// --- Move ---
|
||||
const startMove = useCallback((groupIndex: number, wordIndex: number, segmentId: string, e: React.PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
const wordEl = getWordEl(iframe, groupIndex, wordIndex);
|
||||
const win = iframe.contentWindow;
|
||||
if (!wordEl || !win) return;
|
||||
const state = readGsapTransform(getOrCreateWrapper(wordEl), win);
|
||||
interactionRef.current = {
|
||||
type: "move", wordEl, segmentId,
|
||||
startMX: e.clientX, startMY: e.clientY,
|
||||
origTX: state.x, origTY: state.y,
|
||||
origScale: state.scale, origRotation: state.rotation,
|
||||
};
|
||||
}, [iframeRef]);
|
||||
const startMove = useCallback(
|
||||
(groupIndex: number, wordIndex: number, segmentId: string, e: React.PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
const wordEl = getWordEl(iframe, groupIndex, wordIndex);
|
||||
const win = iframe.contentWindow;
|
||||
if (!wordEl || !win) return;
|
||||
const state = readGsapTransform(getOrCreateWrapper(wordEl), win);
|
||||
interactionRef.current = {
|
||||
type: "move",
|
||||
wordEl,
|
||||
segmentId,
|
||||
startMX: e.clientX,
|
||||
startMY: e.clientY,
|
||||
origTX: state.x,
|
||||
origTY: state.y,
|
||||
origScale: state.scale,
|
||||
origRotation: state.rotation,
|
||||
};
|
||||
},
|
||||
[iframeRef],
|
||||
);
|
||||
|
||||
// --- Scale ---
|
||||
const startScale = useCallback((groupIndex: number, wordIndex: number, segmentId: string, e: React.PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
const wordEl = getWordEl(iframe, groupIndex, wordIndex);
|
||||
const win = iframe.contentWindow;
|
||||
if (!wordEl || !win) return;
|
||||
const rect = wordEl.getBoundingClientRect();
|
||||
const state = readGsapTransform(getOrCreateWrapper(wordEl), win);
|
||||
interactionRef.current = {
|
||||
type: "scale", wordEl, segmentId,
|
||||
startMX: e.clientX, startWidth: rect.width,
|
||||
origTX: state.x, origTY: state.y,
|
||||
origScale: state.scale, origRotation: state.rotation,
|
||||
};
|
||||
}, [iframeRef]);
|
||||
const startScale = useCallback(
|
||||
(groupIndex: number, wordIndex: number, segmentId: string, e: React.PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
const wordEl = getWordEl(iframe, groupIndex, wordIndex);
|
||||
const win = iframe.contentWindow;
|
||||
if (!wordEl || !win) return;
|
||||
const rect = wordEl.getBoundingClientRect();
|
||||
const state = readGsapTransform(getOrCreateWrapper(wordEl), win);
|
||||
interactionRef.current = {
|
||||
type: "scale",
|
||||
wordEl,
|
||||
segmentId,
|
||||
startMX: e.clientX,
|
||||
startWidth: rect.width,
|
||||
origTX: state.x,
|
||||
origTY: state.y,
|
||||
origScale: state.scale,
|
||||
origRotation: state.rotation,
|
||||
};
|
||||
},
|
||||
[iframeRef],
|
||||
);
|
||||
|
||||
// --- Rotate ---
|
||||
const startRotate = useCallback((box: WordBox, e: React.PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
const wordEl = getWordEl(iframe, box.groupIndex, box.wordIndex);
|
||||
const win = iframe.contentWindow;
|
||||
if (!wordEl || !win) return;
|
||||
const cx = box.x + box.width / 2;
|
||||
const cy = box.y + box.height / 2;
|
||||
const startAngle = Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI);
|
||||
const state = readGsapTransform(getOrCreateWrapper(wordEl), win);
|
||||
interactionRef.current = {
|
||||
type: "rotate", wordEl, segmentId: box.segmentId,
|
||||
centerX: cx, centerY: cy,
|
||||
startAngle, origTX: state.x, origTY: state.y,
|
||||
origRotation: state.rotation, origScale: state.scale,
|
||||
};
|
||||
}, [iframeRef]);
|
||||
const startRotate = useCallback(
|
||||
(box: WordBox, e: React.PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
const wordEl = getWordEl(iframe, box.groupIndex, box.wordIndex);
|
||||
const win = iframe.contentWindow;
|
||||
if (!wordEl || !win) return;
|
||||
const cx = box.x + box.width / 2;
|
||||
const cy = box.y + box.height / 2;
|
||||
const startAngle = Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI);
|
||||
const state = readGsapTransform(getOrCreateWrapper(wordEl), win);
|
||||
interactionRef.current = {
|
||||
type: "rotate",
|
||||
wordEl,
|
||||
segmentId: box.segmentId,
|
||||
centerX: cx,
|
||||
centerY: cy,
|
||||
startAngle,
|
||||
origTX: state.x,
|
||||
origTY: state.y,
|
||||
origRotation: state.rotation,
|
||||
origScale: state.scale,
|
||||
};
|
||||
},
|
||||
[iframeRef],
|
||||
);
|
||||
|
||||
/** Get iframe contentWindow, needed for gsap calls */
|
||||
const getIframeWin = useCallback((): Window | null => {
|
||||
try { return iframeRef.current?.contentWindow ?? null; } catch { return null; }
|
||||
try {
|
||||
return iframeRef.current?.contentWindow ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [iframeRef]);
|
||||
|
||||
// --- Unified pointer move ---
|
||||
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
||||
const i = interactionRef.current;
|
||||
if (!i) return;
|
||||
const win = getIframeWin();
|
||||
if (!win) return;
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
const i = interactionRef.current;
|
||||
if (!i) return;
|
||||
const win = getIframeWin();
|
||||
if (!win) return;
|
||||
|
||||
if (i.type === "move") {
|
||||
const cssScale = getCssScale();
|
||||
const dx = (e.clientX - i.startMX) / cssScale;
|
||||
const dy = (e.clientY - i.startMY) / cssScale;
|
||||
writeTransform(i.wordEl, win, i.origTX + dx, i.origTY + dy, i.origScale, i.origRotation);
|
||||
} else if (i.type === "scale") {
|
||||
const dx = e.clientX - i.startMX;
|
||||
const factor = 1 + dx / Math.max(i.startWidth, 50);
|
||||
const newScale = Math.max(0.1, i.origScale * factor);
|
||||
writeTransform(i.wordEl, win, i.origTX, i.origTY, newScale, i.origRotation);
|
||||
} else if (i.type === "rotate") {
|
||||
const angle = Math.atan2(e.clientY - i.centerY, e.clientX - i.centerX) * (180 / Math.PI);
|
||||
const delta = angle - i.startAngle;
|
||||
writeTransform(i.wordEl, win, i.origTX, i.origTY, i.origScale, i.origRotation + delta);
|
||||
}
|
||||
}, [getCssScale, getIframeWin]);
|
||||
if (i.type === "move") {
|
||||
const cssScale = getCssScale();
|
||||
const dx = (e.clientX - i.startMX) / cssScale;
|
||||
const dy = (e.clientY - i.startMY) / cssScale;
|
||||
writeTransform(i.wordEl, win, i.origTX + dx, i.origTY + dy, i.origScale, i.origRotation);
|
||||
} else if (i.type === "scale") {
|
||||
const dx = e.clientX - i.startMX;
|
||||
const factor = 1 + dx / Math.max(i.startWidth, 50);
|
||||
const newScale = Math.max(0.1, i.origScale * factor);
|
||||
writeTransform(i.wordEl, win, i.origTX, i.origTY, newScale, i.origRotation);
|
||||
} else if (i.type === "rotate") {
|
||||
const angle = Math.atan2(e.clientY - i.centerY, e.clientX - i.centerX) * (180 / Math.PI);
|
||||
const delta = angle - i.startAngle;
|
||||
writeTransform(i.wordEl, win, i.origTX, i.origTY, i.origScale, i.origRotation + delta);
|
||||
}
|
||||
},
|
||||
[getCssScale, getIframeWin],
|
||||
);
|
||||
|
||||
// --- Unified pointer up — sync back to store ---
|
||||
const handlePointerUp = useCallback(() => {
|
||||
@@ -371,9 +467,12 @@ export const CaptionOverlay = memo(function CaptionOverlay({
|
||||
}
|
||||
}, [getIframeWin]);
|
||||
|
||||
const handleBackgroundClick = useCallback((e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) clearSelection();
|
||||
}, [clearSelection]);
|
||||
const handleBackgroundClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) clearSelection();
|
||||
},
|
||||
[clearSelection],
|
||||
);
|
||||
|
||||
if (!isEditMode) return null;
|
||||
|
||||
@@ -397,11 +496,18 @@ export const CaptionOverlay = memo(function CaptionOverlay({
|
||||
isSelected ? "ring-2 ring-studio-accent" : "hover:ring-1 hover:ring-white/30",
|
||||
].join(" ")}
|
||||
style={{
|
||||
left: box.x, top: box.y, width: box.width, height: box.height,
|
||||
left: box.x,
|
||||
top: box.y,
|
||||
width: box.width,
|
||||
height: box.height,
|
||||
cursor: isSelected ? "move" : "pointer",
|
||||
touchAction: "none", borderRadius: 2,
|
||||
touchAction: "none",
|
||||
borderRadius: 2,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectSegment(box.segmentId, e.shiftKey);
|
||||
}}
|
||||
onClick={(e) => { e.stopPropagation(); selectSegment(box.segmentId, e.shiftKey); }}
|
||||
onPointerDown={(e) => {
|
||||
if (isSelected) startMove(box.groupIndex, box.wordIndex, box.segmentId, e);
|
||||
}}
|
||||
@@ -412,13 +518,16 @@ export const CaptionOverlay = memo(function CaptionOverlay({
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%", top: -ROTATION_OFFSET - HANDLE,
|
||||
left: "50%",
|
||||
top: -ROTATION_OFFSET - HANDLE,
|
||||
marginLeft: -HANDLE / 2,
|
||||
width: HANDLE, height: HANDLE,
|
||||
width: HANDLE,
|
||||
height: HANDLE,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--hf-accent, #3CE6AC)",
|
||||
border: "1px solid rgba(0,0,0,0.5)",
|
||||
cursor: "grab", touchAction: "none",
|
||||
cursor: "grab",
|
||||
touchAction: "none",
|
||||
}}
|
||||
onPointerDown={(e) => startRotate(box, e)}
|
||||
/>
|
||||
@@ -426,11 +535,14 @@ export const CaptionOverlay = memo(function CaptionOverlay({
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%", top: -ROTATION_OFFSET,
|
||||
width: 1, height: ROTATION_OFFSET,
|
||||
left: "50%",
|
||||
top: -ROTATION_OFFSET,
|
||||
width: 1,
|
||||
height: ROTATION_OFFSET,
|
||||
marginLeft: -0.5,
|
||||
backgroundColor: "var(--hf-accent, #3CE6AC)",
|
||||
opacity: 0.5, pointerEvents: "none",
|
||||
opacity: 0.5,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
{/* Scale handles — four corners */}
|
||||
@@ -443,13 +555,18 @@ export const CaptionOverlay = memo(function CaptionOverlay({
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
position: "absolute", ...pos,
|
||||
width: HANDLE, height: HANDLE,
|
||||
position: "absolute",
|
||||
...pos,
|
||||
width: HANDLE,
|
||||
height: HANDLE,
|
||||
backgroundColor: "var(--hf-accent, #3CE6AC)",
|
||||
border: "1px solid rgba(0,0,0,0.5)",
|
||||
borderRadius: 2, touchAction: "none",
|
||||
borderRadius: 2,
|
||||
touchAction: "none",
|
||||
}}
|
||||
onPointerDown={(e) => startScale(box.groupIndex, box.wordIndex, box.segmentId, e)}
|
||||
onPointerDown={(e) =>
|
||||
startScale(box.groupIndex, box.wordIndex, box.segmentId, e)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -76,7 +76,6 @@ export const CaptionPropertyPanel = memo(function CaptionPropertyPanel({
|
||||
...segmentOverrides,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Apply a CSS style change to selected word elements in the iframe DOM in real time.
|
||||
* Maps CaptionStyle property names to CSS properties.
|
||||
@@ -112,9 +111,15 @@ export const CaptionPropertyPanel = memo(function CaptionPropertyPanel({
|
||||
const c = child as HTMLElement;
|
||||
if (c.dataset.captionWrapper === "true") {
|
||||
const inner = c.querySelector<HTMLElement>(":scope > span");
|
||||
if (inner && idx === wi) { targetEls.push(inner); break; }
|
||||
if (inner && idx === wi) {
|
||||
targetEls.push(inner);
|
||||
break;
|
||||
}
|
||||
} else if (c.tagName === "SPAN") {
|
||||
if (idx === wi) { targetEls.push(c); break; }
|
||||
if (idx === wi) {
|
||||
targetEls.push(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
@@ -123,15 +128,23 @@ export const CaptionPropertyPanel = memo(function CaptionPropertyPanel({
|
||||
}
|
||||
|
||||
// Apply transform updates via gsap.set on the WRAPPER (not the word span)
|
||||
const hasTransform = updates.x !== undefined || updates.y !== undefined ||
|
||||
updates.scaleX !== undefined || updates.scaleY !== undefined || updates.rotation !== undefined;
|
||||
const hasTransform =
|
||||
updates.x !== undefined ||
|
||||
updates.y !== undefined ||
|
||||
updates.scaleX !== undefined ||
|
||||
updates.scaleY !== undefined ||
|
||||
updates.rotation !== undefined;
|
||||
|
||||
if (hasTransform) {
|
||||
try {
|
||||
const iframeGsap = (iframeRef.current?.contentWindow as unknown as {
|
||||
gsap?: { set: (el: HTMLElement, props: Record<string, unknown>) => void;
|
||||
getProperty: (el: HTMLElement, prop: string) => number };
|
||||
})?.gsap;
|
||||
const iframeGsap = (
|
||||
iframeRef.current?.contentWindow as unknown as {
|
||||
gsap?: {
|
||||
set: (el: HTMLElement, props: Record<string, unknown>) => void;
|
||||
getProperty: (el: HTMLElement, prop: string) => number;
|
||||
};
|
||||
}
|
||||
)?.gsap;
|
||||
if (iframeGsap) {
|
||||
for (const el of targetEls) {
|
||||
// Get or create wrapper
|
||||
@@ -156,7 +169,9 @@ export const CaptionPropertyPanel = memo(function CaptionPropertyPanel({
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch { /* cross-origin */ }
|
||||
} catch {
|
||||
/* cross-origin */
|
||||
}
|
||||
}
|
||||
},
|
||||
[iframeRef, model, selectedSegmentIds],
|
||||
@@ -194,18 +209,14 @@ export const CaptionPropertyPanel = memo(function CaptionPropertyPanel({
|
||||
const scaleX = effectiveStyle.scaleX ?? 1;
|
||||
|
||||
// Count label
|
||||
const countLabel = selectedSegmentIds.size === 1
|
||||
? "1 word"
|
||||
: `${selectedSegmentIds.size} words`;
|
||||
const countLabel = selectedSegmentIds.size === 1 ? "1 word" : `${selectedSegmentIds.size} words`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 border-b border-neutral-800 flex-shrink-0">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-2xs text-neutral-500">
|
||||
{countLabel}
|
||||
</span>
|
||||
<span className="text-2xs text-neutral-500">{countLabel}</span>
|
||||
</div>
|
||||
{/* Tab switcher */}
|
||||
<div className="flex gap-1">
|
||||
|
||||
@@ -38,13 +38,13 @@ The CLI auto-detects and normalizes these formats:
|
||||
|
||||
The default model (`small.en`) balances accuracy and speed. For better results, use a larger model:
|
||||
|
||||
| Model | Size | Speed | Accuracy | When to use |
|
||||
| ----------- | ------ | -------- | --------- | ------------------------------------- |
|
||||
| `tiny` | 75 MB | Fastest | Low | Quick previews, testing pipeline |
|
||||
| `base` | 142 MB | Fast | Fair | Short clips, clear audio |
|
||||
| `small` | 466 MB | Moderate | Good | **Default** — good for most content |
|
||||
| `medium` | 1.5 GB | Slow | Very good | Important content, noisy audio, music |
|
||||
| `large-v3` | 3.1 GB | Slowest | Best | Production quality |
|
||||
| Model | Size | Speed | Accuracy | When to use |
|
||||
| ---------- | ------ | -------- | --------- | ------------------------------------- |
|
||||
| `tiny` | 75 MB | Fastest | Low | Quick previews, testing pipeline |
|
||||
| `base` | 142 MB | Fast | Fair | Short clips, clear audio |
|
||||
| `small` | 466 MB | Moderate | Good | **Default** — good for most content |
|
||||
| `medium` | 1.5 GB | Slow | Very good | Important content, noisy audio, music |
|
||||
| `large-v3` | 3.1 GB | Slowest | Best | Production quality |
|
||||
|
||||
**Only add `.en` suffix when the user explicitly says the audio is English.** `.en` models are slightly more accurate for English but will TRANSLATE non-English audio instead of transcribing it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user