Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| import type { Project, Scene } from "../api/client"; | |
| import { getSceneDurationFrames } from "../components/remotion/playbackSpeed"; | |
| const FPS = 30; | |
| /** Where within each scene to sample (0–1). 0.5 looked too early; ~0.85 matches full layout + imagery. */ | |
| export const SCENE_EXPORT_TIMELINE_FRACTION = 0.85; | |
| function framesForScene(scene: Scene): number { | |
| const base = Number(scene.duration_seconds) || 5; | |
| const extra = Number(scene.extra_hold_seconds) || 0; | |
| return getSceneDurationFrames(base + extra, FPS, 1); | |
| } | |
| function normalizeTimelineFractions(length: number, timelineFractions?: number[]): number[] { | |
| const def = SCENE_EXPORT_TIMELINE_FRACTION; | |
| if (!timelineFractions || timelineFractions.length === 0) { | |
| return Array.from({ length }, () => def); | |
| } | |
| const out: number[] = []; | |
| for (let i = 0; i < length; i++) { | |
| const v = timelineFractions[i]; | |
| out.push( | |
| typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : def | |
| ); | |
| } | |
| return out; | |
| } | |
| /** | |
| * Global composition frame for `sceneIndex` at `timelineFraction` (0–1 through that scene only). | |
| */ | |
| export function getSceneExportGlobalFrame( | |
| project: Project, | |
| sceneIndex: number, | |
| timelineFraction: number | |
| ): number { | |
| const t = Math.min(1, Math.max(0, timelineFraction)); | |
| const scenes = project.scenes; | |
| if (!scenes?.length || sceneIndex < 0 || sceneIndex >= scenes.length) return 0; | |
| let offset = 0; | |
| for (let i = 0; i < sceneIndex; i++) { | |
| offset += framesForScene(scenes[i]!); | |
| } | |
| const scene = scenes[sceneIndex]!; | |
| const durationFrames = framesForScene(scene); | |
| const local = Math.floor(durationFrames * t); | |
| const clampedLocal = Math.max(0, Math.min(durationFrames - 1, local)); | |
| return offset + clampedLocal; | |
| } | |
| /** | |
| * Per-scene target frame (global composition frames), matching VideoPreview / Remotion sequencing: | |
| * duration = duration_seconds + extra_hold_seconds at playback 1×. | |
| * Export frame = start + clamp(floor(duration * fraction)) per scene. | |
| * | |
| * @param timelineFractions Optional per-scene 0–1 positions; omitted entries fall back to {@link SCENE_EXPORT_TIMELINE_FRACTION}. | |
| */ | |
| export function getSceneExportFrameSchedule( | |
| project: Project, | |
| timelineFractions?: number[] | |
| ): { | |
| frame: number; | |
| title: string; | |
| order: number; | |
| safeSlug: string; | |
| }[] { | |
| if (!project.scenes?.length) return []; | |
| const fracs = normalizeTimelineFractions(project.scenes.length, timelineFractions); | |
| let offset = 0; | |
| return project.scenes.map((s, i) => { | |
| const durationFrames = framesForScene(s); | |
| const startFrame = offset; | |
| offset += durationFrames; | |
| const local = Math.floor(durationFrames * fracs[i]!); | |
| const clampedLocal = Math.max(0, Math.min(durationFrames - 1, local)); | |
| const exportFrame = startFrame + clampedLocal; | |
| const title = s.title || `Scene ${i + 1}`; | |
| const safeSlug = `${String(i + 1).padStart(2, "0")}_${title | |
| .replace(/[^a-z0-9]+/gi, "_") | |
| .replace(/^_|_$/g, "") | |
| .slice(0, 40) || "scene"}`; | |
| return { frame: exportFrame, title, order: i + 1, safeSlug }; | |
| }); | |
| } | |