From dcf5d119a67698cb4d8ab6f746c7888952a80f0e Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:32:18 +1100 Subject: [PATCH] feat: persist caption and editor control state Persist the new caption, cursor, and zoom timing controls in editor preferences and project files. Normalize those saved values so older projects continue loading with stable defaults. --- .../video-editor/editorPreferences.test.ts | 42 +++++ .../video-editor/editorPreferences.ts | 55 ++++++ .../video-editor/projectPersistence.ts | 176 +++++++++++++++++- 3 files changed, 272 insertions(+), 1 deletion(-) diff --git a/src/components/video-editor/editorPreferences.test.ts b/src/components/video-editor/editorPreferences.test.ts index 009aff45..848240a2 100644 --- a/src/components/video-editor/editorPreferences.test.ts +++ b/src/components/video-editor/editorPreferences.test.ts @@ -78,8 +78,17 @@ describe("editorPreferences", () => { backgroundBlur: 3.5, zoomMotionBlur: DEFAULT_EDITOR_PREFERENCES.zoomMotionBlur, connectZooms: DEFAULT_EDITOR_PREFERENCES.connectZooms, + zoomInDurationMs: DEFAULT_EDITOR_PREFERENCES.zoomInDurationMs, + zoomInOverlapMs: DEFAULT_EDITOR_PREFERENCES.zoomInOverlapMs, + zoomOutDurationMs: DEFAULT_EDITOR_PREFERENCES.zoomOutDurationMs, + connectedZoomGapMs: DEFAULT_EDITOR_PREFERENCES.connectedZoomGapMs, + connectedZoomDurationMs: DEFAULT_EDITOR_PREFERENCES.connectedZoomDurationMs, + zoomInEasing: DEFAULT_EDITOR_PREFERENCES.zoomInEasing, + zoomOutEasing: DEFAULT_EDITOR_PREFERENCES.zoomOutEasing, + connectedZoomEasing: DEFAULT_EDITOR_PREFERENCES.connectedZoomEasing, showCursor: false, loopCursor: DEFAULT_EDITOR_PREFERENCES.loopCursor, + cursorStyle: DEFAULT_EDITOR_PREFERENCES.cursorStyle, cursorSize: DEFAULT_EDITOR_PREFERENCES.cursorSize, cursorSmoothing: DEFAULT_EDITOR_PREFERENCES.cursorSmoothing, cursorMotionBlur: DEFAULT_EDITOR_PREFERENCES.cursorMotionBlur, @@ -98,6 +107,8 @@ describe("editorPreferences", () => { customAspectWidth: "21", customAspectHeight: "9", customWallpapers: ["data:image/jpeg;base64,abc"], + whisperExecutablePath: DEFAULT_EDITOR_PREFERENCES.whisperExecutablePath, + whisperModelPath: DEFAULT_EDITOR_PREFERENCES.whisperModelPath, }); }); @@ -133,8 +144,17 @@ describe("editorPreferences", () => { backgroundBlur: DEFAULT_EDITOR_PREFERENCES.backgroundBlur, zoomMotionBlur: DEFAULT_EDITOR_PREFERENCES.zoomMotionBlur, connectZooms: DEFAULT_EDITOR_PREFERENCES.connectZooms, + zoomInDurationMs: DEFAULT_EDITOR_PREFERENCES.zoomInDurationMs, + zoomInOverlapMs: DEFAULT_EDITOR_PREFERENCES.zoomInOverlapMs, + zoomOutDurationMs: DEFAULT_EDITOR_PREFERENCES.zoomOutDurationMs, + connectedZoomGapMs: DEFAULT_EDITOR_PREFERENCES.connectedZoomGapMs, + connectedZoomDurationMs: DEFAULT_EDITOR_PREFERENCES.connectedZoomDurationMs, + zoomInEasing: DEFAULT_EDITOR_PREFERENCES.zoomInEasing, + zoomOutEasing: DEFAULT_EDITOR_PREFERENCES.zoomOutEasing, + connectedZoomEasing: DEFAULT_EDITOR_PREFERENCES.connectedZoomEasing, showCursor: DEFAULT_EDITOR_PREFERENCES.showCursor, loopCursor: DEFAULT_EDITOR_PREFERENCES.loopCursor, + cursorStyle: DEFAULT_EDITOR_PREFERENCES.cursorStyle, cursorSize: DEFAULT_EDITOR_PREFERENCES.cursorSize, cursorSmoothing: DEFAULT_EDITOR_PREFERENCES.cursorSmoothing, cursorMotionBlur: DEFAULT_EDITOR_PREFERENCES.cursorMotionBlur, @@ -152,6 +172,8 @@ describe("editorPreferences", () => { customAspectWidth: "21", customAspectHeight: "9", customWallpapers: DEFAULT_EDITOR_PREFERENCES.customWallpapers, + whisperExecutablePath: DEFAULT_EDITOR_PREFERENCES.whisperExecutablePath, + whisperModelPath: DEFAULT_EDITOR_PREFERENCES.whisperModelPath, }); }); @@ -165,8 +187,17 @@ describe("editorPreferences", () => { backgroundBlur: 1.5, zoomMotionBlur: 0.75, connectZooms: false, + zoomInDurationMs: DEFAULT_EDITOR_PREFERENCES.zoomInDurationMs, + zoomInOverlapMs: DEFAULT_EDITOR_PREFERENCES.zoomInOverlapMs, + zoomOutDurationMs: DEFAULT_EDITOR_PREFERENCES.zoomOutDurationMs, + connectedZoomGapMs: DEFAULT_EDITOR_PREFERENCES.connectedZoomGapMs, + connectedZoomDurationMs: DEFAULT_EDITOR_PREFERENCES.connectedZoomDurationMs, + zoomInEasing: DEFAULT_EDITOR_PREFERENCES.zoomInEasing, + zoomOutEasing: DEFAULT_EDITOR_PREFERENCES.zoomOutEasing, + connectedZoomEasing: DEFAULT_EDITOR_PREFERENCES.connectedZoomEasing, showCursor: false, loopCursor: true, + cursorStyle: "figma", cursorSize: 3, cursorSmoothing: 1.25, cursorMotionBlur: 0.5, @@ -192,8 +223,17 @@ describe("editorPreferences", () => { backgroundBlur: 1.5, zoomMotionBlur: 0.75, connectZooms: false, + zoomInDurationMs: DEFAULT_EDITOR_PREFERENCES.zoomInDurationMs, + zoomInOverlapMs: DEFAULT_EDITOR_PREFERENCES.zoomInOverlapMs, + zoomOutDurationMs: DEFAULT_EDITOR_PREFERENCES.zoomOutDurationMs, + connectedZoomGapMs: DEFAULT_EDITOR_PREFERENCES.connectedZoomGapMs, + connectedZoomDurationMs: DEFAULT_EDITOR_PREFERENCES.connectedZoomDurationMs, + zoomInEasing: DEFAULT_EDITOR_PREFERENCES.zoomInEasing, + zoomOutEasing: DEFAULT_EDITOR_PREFERENCES.zoomOutEasing, + connectedZoomEasing: DEFAULT_EDITOR_PREFERENCES.connectedZoomEasing, showCursor: false, loopCursor: true, + cursorStyle: "figma", cursorSize: 3, cursorSmoothing: 1.25, cursorMotionBlur: 0.5, @@ -212,6 +252,8 @@ describe("editorPreferences", () => { customAspectWidth: "4", customAspectHeight: "5", customWallpapers: ["data:image/jpeg;base64,abc"], + whisperExecutablePath: DEFAULT_EDITOR_PREFERENCES.whisperExecutablePath, + whisperModelPath: DEFAULT_EDITOR_PREFERENCES.whisperModelPath, }); }); }); diff --git a/src/components/video-editor/editorPreferences.ts b/src/components/video-editor/editorPreferences.ts index 12d6d058..6523de7b 100644 --- a/src/components/video-editor/editorPreferences.ts +++ b/src/components/video-editor/editorPreferences.ts @@ -7,8 +7,17 @@ type PersistedEditorControls = Pick< | "backgroundBlur" | "zoomMotionBlur" | "connectZooms" + | "zoomInDurationMs" + | "zoomInOverlapMs" + | "zoomOutDurationMs" + | "connectedZoomGapMs" + | "connectedZoomDurationMs" + | "zoomInEasing" + | "zoomOutEasing" + | "connectedZoomEasing" | "showCursor" | "loopCursor" + | "cursorStyle" | "cursorSize" | "cursorSmoothing" | "cursorMotionBlur" @@ -32,6 +41,8 @@ export interface EditorPreferences extends PersistedEditorControls { customAspectWidth: string; customAspectHeight: string; customWallpapers: string[]; + whisperExecutablePath: string | null; + whisperModelPath: string | null; } export const EDITOR_PREFERENCES_STORAGE_KEY = "recordly.editor.preferences"; @@ -44,8 +55,17 @@ export const DEFAULT_EDITOR_PREFERENCES: EditorPreferences = { backgroundBlur: DEFAULT_EDITOR_CONTROLS.backgroundBlur, zoomMotionBlur: DEFAULT_EDITOR_CONTROLS.zoomMotionBlur, connectZooms: DEFAULT_EDITOR_CONTROLS.connectZooms, + zoomInDurationMs: DEFAULT_EDITOR_CONTROLS.zoomInDurationMs, + zoomInOverlapMs: DEFAULT_EDITOR_CONTROLS.zoomInOverlapMs, + zoomOutDurationMs: DEFAULT_EDITOR_CONTROLS.zoomOutDurationMs, + connectedZoomGapMs: DEFAULT_EDITOR_CONTROLS.connectedZoomGapMs, + connectedZoomDurationMs: DEFAULT_EDITOR_CONTROLS.connectedZoomDurationMs, + zoomInEasing: DEFAULT_EDITOR_CONTROLS.zoomInEasing, + zoomOutEasing: DEFAULT_EDITOR_CONTROLS.zoomOutEasing, + connectedZoomEasing: DEFAULT_EDITOR_CONTROLS.connectedZoomEasing, showCursor: DEFAULT_EDITOR_CONTROLS.showCursor, loopCursor: DEFAULT_EDITOR_CONTROLS.loopCursor, + cursorStyle: DEFAULT_EDITOR_CONTROLS.cursorStyle, cursorSize: DEFAULT_EDITOR_CONTROLS.cursorSize, cursorSmoothing: DEFAULT_EDITOR_CONTROLS.cursorSmoothing, cursorMotionBlur: DEFAULT_EDITOR_CONTROLS.cursorMotionBlur, @@ -64,6 +84,8 @@ export const DEFAULT_EDITOR_PREFERENCES: EditorPreferences = { customAspectWidth: "16", customAspectHeight: "9", customWallpapers: [], + whisperExecutablePath: null, + whisperModelPath: null, }; function normalizePositiveIntegerString(value: unknown, fallback: string): string { @@ -89,6 +111,15 @@ function normalizeCustomWallpapers(value: unknown, fallback: string[]): string[] ); } +function normalizeNullablePath(value: unknown): string | null { + if (typeof value !== "string") { + return null; + } + + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + function normalizeEditorControls( raw: Partial, fallback: EditorPreferences, @@ -99,8 +130,19 @@ function normalizeEditorControls( backgroundBlur: raw.backgroundBlur ?? fallback.backgroundBlur, zoomMotionBlur: raw.zoomMotionBlur ?? fallback.zoomMotionBlur, connectZooms: raw.connectZooms ?? fallback.connectZooms, + zoomInDurationMs: raw.zoomInDurationMs ?? fallback.zoomInDurationMs, + zoomInOverlapMs: raw.zoomInOverlapMs ?? fallback.zoomInOverlapMs, + zoomOutDurationMs: raw.zoomOutDurationMs ?? fallback.zoomOutDurationMs, + connectedZoomGapMs: raw.connectedZoomGapMs ?? fallback.connectedZoomGapMs, + connectedZoomDurationMs: + raw.connectedZoomDurationMs ?? fallback.connectedZoomDurationMs, + zoomInEasing: raw.zoomInEasing ?? fallback.zoomInEasing, + zoomOutEasing: raw.zoomOutEasing ?? fallback.zoomOutEasing, + connectedZoomEasing: + raw.connectedZoomEasing ?? fallback.connectedZoomEasing, showCursor: raw.showCursor ?? fallback.showCursor, loopCursor: raw.loopCursor ?? fallback.loopCursor, + cursorStyle: raw.cursorStyle ?? fallback.cursorStyle, cursorSize: raw.cursorSize ?? fallback.cursorSize, cursorSmoothing: raw.cursorSmoothing ?? fallback.cursorSmoothing, cursorMotionBlur: raw.cursorMotionBlur ?? fallback.cursorMotionBlur, @@ -127,8 +169,17 @@ function normalizeEditorControls( backgroundBlur: normalized.backgroundBlur, zoomMotionBlur: normalized.zoomMotionBlur, connectZooms: normalized.connectZooms, + zoomInDurationMs: normalized.zoomInDurationMs, + zoomInOverlapMs: normalized.zoomInOverlapMs, + zoomOutDurationMs: normalized.zoomOutDurationMs, + connectedZoomGapMs: normalized.connectedZoomGapMs, + connectedZoomDurationMs: normalized.connectedZoomDurationMs, + zoomInEasing: normalized.zoomInEasing, + zoomOutEasing: normalized.zoomOutEasing, + connectedZoomEasing: normalized.connectedZoomEasing, showCursor: normalized.showCursor, loopCursor: normalized.loopCursor, + cursorStyle: normalized.cursorStyle, cursorSize: normalized.cursorSize, cursorSmoothing: normalized.cursorSmoothing, cursorMotionBlur: normalized.cursorMotionBlur, @@ -165,6 +216,10 @@ export function normalizeEditorPreferences( fallback.customAspectHeight, ), customWallpapers: normalizeCustomWallpapers(raw.customWallpapers, fallback.customWallpapers), + whisperExecutablePath: + normalizeNullablePath(raw.whisperExecutablePath) ?? fallback.whisperExecutablePath, + whisperModelPath: + normalizeNullablePath(raw.whisperModelPath) ?? fallback.whisperModelPath, }; } diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts index e7d90232..7720b975 100644 --- a/src/components/video-editor/projectPersistence.ts +++ b/src/components/video-editor/projectPersistence.ts @@ -2,9 +2,16 @@ import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@ import { DEFAULT_WALLPAPER_PATH } from "@/lib/wallpapers"; import { ASPECT_RATIOS, type AspectRatio, isCustomAspectRatio } from "@/utils/aspectRatioUtils"; import { + type AutoCaptionAnimation, + type AutoCaptionSettings, + type CaptionCue, + type CaptionCueWord, type AnnotationRegion, type AudioRegion, type CropRegion, + type CursorStyle, + DEFAULT_AUTO_CAPTION_SETTINGS, + getDefaultCaptionFontFamily, DEFAULT_ANNOTATION_POSITION, DEFAULT_ANNOTATION_SIZE, DEFAULT_ANNOTATION_STYLE, @@ -13,8 +20,12 @@ import { DEFAULT_CURSOR_CLICK_BOUNCE_DURATION, DEFAULT_CURSOR_MOTION_BLUR, DEFAULT_CURSOR_SIZE, + DEFAULT_CURSOR_STYLE, DEFAULT_CURSOR_SMOOTHING, DEFAULT_CURSOR_SWAY, + DEFAULT_CONNECTED_ZOOM_DURATION_MS, + DEFAULT_CONNECTED_ZOOM_EASING, + DEFAULT_CONNECTED_ZOOM_GAP_MS, DEFAULT_WEBCAM_CORNER_RADIUS, DEFAULT_WEBCAM_MARGIN, DEFAULT_WEBCAM_OVERLAY, @@ -27,10 +38,16 @@ import { DEFAULT_FIGURE_DATA, DEFAULT_PLAYBACK_SPEED, DEFAULT_ZOOM_DEPTH, + DEFAULT_ZOOM_IN_DURATION_MS, + DEFAULT_ZOOM_IN_EASING, + DEFAULT_ZOOM_IN_OVERLAP_MS, DEFAULT_ZOOM_MOTION_BLUR, + DEFAULT_ZOOM_OUT_DURATION_MS, + DEFAULT_ZOOM_OUT_EASING, type SpeedRegion, type TrimRegion, type WebcamOverlaySettings, + type ZoomTransitionEasing, type ZoomRegion, } from "./types"; @@ -42,8 +59,17 @@ export interface ProjectEditorState { backgroundBlur: number; zoomMotionBlur: number; connectZooms: boolean; + zoomInDurationMs: number; + zoomInOverlapMs: number; + zoomOutDurationMs: number; + connectedZoomGapMs: number; + connectedZoomDurationMs: number; + zoomInEasing: ZoomTransitionEasing; + zoomOutEasing: ZoomTransitionEasing; + connectedZoomEasing: ZoomTransitionEasing; showCursor: boolean; loopCursor: boolean; + cursorStyle: CursorStyle; cursorSize: number; cursorSmoothing: number; cursorMotionBlur: number; @@ -58,6 +84,8 @@ export interface ProjectEditorState { speedRegions: SpeedRegion[]; annotationRegions: AnnotationRegion[]; audioRegions: AudioRegion[]; + autoCaptions: CaptionCue[]; + autoCaptionSettings: AutoCaptionSettings; webcam: WebcamOverlaySettings; aspectRatio: AspectRatio; exportQuality: ExportQuality; @@ -81,6 +109,28 @@ function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } +function normalizeZoomTransitionEasing( + value: unknown, + fallback: ZoomTransitionEasing, +): ZoomTransitionEasing { + return value === "recordly" || + value === "glide" || + value === "smooth" || + value === "snappy" || + value === "linear" + ? value + : fallback; +} + +function normalizeAutoCaptionAnimation( + value: unknown, + fallback: AutoCaptionAnimation, +): AutoCaptionAnimation { + return value === "none" || value === "fade" || value === "rise" || value === "pop" + ? value + : fallback; +} + function isFileUrl(value: string): boolean { return /^file:\/\//i.test(value); } @@ -187,6 +237,21 @@ export function normalizeProjectEditor(editor: Partial): Pro : legacyShowBlur ? 2 : 0; + const normalizedZoomInDurationMs = isFiniteNumber(editor.zoomInDurationMs) + ? clamp(editor.zoomInDurationMs, 60, 4000) + : DEFAULT_ZOOM_IN_DURATION_MS; + const normalizedZoomInOverlapMs = isFiniteNumber(editor.zoomInOverlapMs) + ? clamp(editor.zoomInOverlapMs, 0, normalizedZoomInDurationMs) + : DEFAULT_ZOOM_IN_OVERLAP_MS; + const normalizedZoomOutDurationMs = isFiniteNumber(editor.zoomOutDurationMs) + ? clamp(editor.zoomOutDurationMs, 60, 4000) + : DEFAULT_ZOOM_OUT_DURATION_MS; + const normalizedConnectedZoomGapMs = isFiniteNumber(editor.connectedZoomGapMs) + ? clamp(editor.connectedZoomGapMs, 0, 5000) + : DEFAULT_CONNECTED_ZOOM_GAP_MS; + const normalizedConnectedZoomDurationMs = isFiniteNumber(editor.connectedZoomDurationMs) + ? clamp(editor.connectedZoomDurationMs, 60, 4000) + : DEFAULT_CONNECTED_ZOOM_DURATION_MS; const normalizedZoomRegions: ZoomRegion[] = Array.isArray(editor.zoomRegions) ? editor.zoomRegions @@ -342,6 +407,95 @@ export function normalizeProjectEditor(editor: Partial): Pro }) : []; + const normalizedAutoCaptions: CaptionCue[] = Array.isArray( + (editor as Partial).autoCaptions, + ) + ? ((editor as Partial).autoCaptions as CaptionCue[]) + .filter((cue): cue is CaptionCue => Boolean(cue && typeof cue.id === "string")) + .map((cue) => { + const rawStart = isFiniteNumber(cue.startMs) ? Math.round(cue.startMs) : 0; + const rawEnd = isFiniteNumber(cue.endMs) ? Math.round(cue.endMs) : rawStart + 1000; + const startMs = Math.max(0, Math.min(rawStart, rawEnd)); + const endMs = Math.max(startMs + 1, rawEnd); + const words: CaptionCueWord[] | undefined = Array.isArray(cue.words) + ? cue.words + .filter( + (word): word is CaptionCueWord => Boolean(word && typeof word.text === "string"), + ) + .map((word) => { + const rawWordStart = isFiniteNumber(word.startMs) ? Math.round(word.startMs) : startMs; + const rawWordEnd = isFiniteNumber(word.endMs) ? Math.round(word.endMs) : rawWordStart + 1; + const normalizedWordStart = clamp(rawWordStart, startMs, endMs - 1); + const normalizedWordEnd = clamp(rawWordEnd, normalizedWordStart + 1, endMs); + + return { + text: word.text.trim(), + startMs: normalizedWordStart, + endMs: normalizedWordEnd, + ...(word.leadingSpace ? { leadingSpace: true } : {}), + }; + }) + .filter((word) => word.text.length > 0) + : undefined; + + return { + id: cue.id, + startMs, + endMs, + text: typeof cue.text === "string" ? cue.text.trim() : "", + ...(words && words.length > 0 ? { words } : {}), + }; + }) + .filter((cue) => cue.text.length > 0) + : []; + + const rawAutoCaptionSettings: Partial = + editor.autoCaptionSettings && typeof editor.autoCaptionSettings === "object" + ? (editor.autoCaptionSettings as Partial) + : {}; + const normalizedAutoCaptionSettings: AutoCaptionSettings = { + enabled: + typeof rawAutoCaptionSettings.enabled === "boolean" + ? rawAutoCaptionSettings.enabled + : DEFAULT_AUTO_CAPTION_SETTINGS.enabled, + language: + typeof rawAutoCaptionSettings.language === "string" && rawAutoCaptionSettings.language.trim() + ? rawAutoCaptionSettings.language.trim() + : DEFAULT_AUTO_CAPTION_SETTINGS.language, + fontFamily: + getDefaultCaptionFontFamily(), + fontSize: isFiniteNumber(rawAutoCaptionSettings.fontSize) + ? clamp(rawAutoCaptionSettings.fontSize, 16, 72) + : DEFAULT_AUTO_CAPTION_SETTINGS.fontSize, + bottomOffset: isFiniteNumber(rawAutoCaptionSettings.bottomOffset) + ? clamp(rawAutoCaptionSettings.bottomOffset, 0, 30) + : DEFAULT_AUTO_CAPTION_SETTINGS.bottomOffset, + maxWidth: isFiniteNumber(rawAutoCaptionSettings.maxWidth) + ? clamp(rawAutoCaptionSettings.maxWidth, 40, 95) + : DEFAULT_AUTO_CAPTION_SETTINGS.maxWidth, + maxRows: isFiniteNumber(rawAutoCaptionSettings.maxRows) + ? clamp(Math.round(rawAutoCaptionSettings.maxRows), 1, 4) + : DEFAULT_AUTO_CAPTION_SETTINGS.maxRows, + animationStyle: normalizeAutoCaptionAnimation( + rawAutoCaptionSettings.animationStyle, + DEFAULT_AUTO_CAPTION_SETTINGS.animationStyle, + ), + boxRadius: isFiniteNumber(rawAutoCaptionSettings.boxRadius) + ? clamp(rawAutoCaptionSettings.boxRadius, 0, 40) + : DEFAULT_AUTO_CAPTION_SETTINGS.boxRadius, + textColor: + typeof rawAutoCaptionSettings.textColor === "string" && rawAutoCaptionSettings.textColor.trim() + ? rawAutoCaptionSettings.textColor + : DEFAULT_AUTO_CAPTION_SETTINGS.textColor, + inactiveTextColor: + typeof rawAutoCaptionSettings.inactiveTextColor === "string" && rawAutoCaptionSettings.inactiveTextColor.trim() + ? rawAutoCaptionSettings.inactiveTextColor + : DEFAULT_AUTO_CAPTION_SETTINGS.inactiveTextColor, + backgroundOpacity: isFiniteNumber(rawAutoCaptionSettings.backgroundOpacity) + ? clamp(rawAutoCaptionSettings.backgroundOpacity, 0, 1) + : DEFAULT_AUTO_CAPTION_SETTINGS.backgroundOpacity, + }; + const rawCropX = isFiniteNumber(editor.cropRegion?.x) ? editor.cropRegion.x : DEFAULT_CROP_REGION.x; @@ -374,8 +528,26 @@ export function normalizeProjectEditor(editor: Partial): Pro backgroundBlur: normalizedBackgroundBlur, zoomMotionBlur: normalizedZoomMotionBlur, connectZooms: typeof editor.connectZooms === "boolean" ? editor.connectZooms : true, + zoomInDurationMs: normalizedZoomInDurationMs, + zoomInOverlapMs: normalizedZoomInOverlapMs, + zoomOutDurationMs: normalizedZoomOutDurationMs, + connectedZoomGapMs: normalizedConnectedZoomGapMs, + connectedZoomDurationMs: normalizedConnectedZoomDurationMs, + zoomInEasing: normalizeZoomTransitionEasing(editor.zoomInEasing, DEFAULT_ZOOM_IN_EASING), + zoomOutEasing: normalizeZoomTransitionEasing(editor.zoomOutEasing, DEFAULT_ZOOM_OUT_EASING), + connectedZoomEasing: normalizeZoomTransitionEasing( + editor.connectedZoomEasing, + DEFAULT_CONNECTED_ZOOM_EASING, + ), showCursor: typeof editor.showCursor === "boolean" ? editor.showCursor : true, loopCursor: typeof editor.loopCursor === "boolean" ? editor.loopCursor : false, + cursorStyle: + editor.cursorStyle === "dot" || + editor.cursorStyle === "figma" || + editor.cursorStyle === "mono" || + editor.cursorStyle === "tahoe" + ? editor.cursorStyle + : DEFAULT_CURSOR_STYLE, cursorSize: isFiniteNumber(editor.cursorSize) ? clamp(editor.cursorSize, 0.5, 10) : DEFAULT_CURSOR_SIZE, @@ -395,7 +567,7 @@ export function normalizeProjectEditor(editor: Partial): Pro ? clamp((editor as Partial).cursorSway as number, 0, 2) : DEFAULT_CURSOR_SWAY, borderRadius: typeof editor.borderRadius === "number" ? editor.borderRadius : 12.5, - padding: isFiniteNumber(editor.padding) ? clamp(editor.padding, 0, 100) : 50, + padding: isFiniteNumber(editor.padding) ? clamp(editor.padding, 0, 100) : 20, cropRegion: { x: cropX, y: cropY, @@ -407,6 +579,8 @@ export function normalizeProjectEditor(editor: Partial): Pro speedRegions: normalizedSpeedRegions, annotationRegions: normalizedAnnotationRegions, audioRegions: normalizedAudioRegions, + autoCaptions: normalizedAutoCaptions, + autoCaptionSettings: normalizedAutoCaptionSettings, webcam: { enabled: typeof webcam.enabled === "boolean" ? webcam.enabled : DEFAULT_WEBCAM_OVERLAY.enabled,