frameyard 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -7
- package/GLOSSARY.md +152 -18
- package/README.md +328 -285
- package/dist/assets/integrations.d.ts +8 -4
- package/dist/assets/integrations.d.ts.map +1 -1
- package/dist/assets/integrations.js +8 -4
- package/dist/assets/integrations.js.map +1 -1
- package/dist/assets/provenance.d.ts +2 -2
- package/dist/assets/provenance.d.ts.map +1 -1
- package/dist/assets/provenance.js +5 -3
- package/dist/assets/provenance.js.map +1 -1
- package/dist/assets/tts-supertonic.d.ts +28 -0
- package/dist/assets/tts-supertonic.d.ts.map +1 -0
- package/dist/assets/tts-supertonic.js +156 -0
- package/dist/assets/tts-supertonic.js.map +1 -0
- package/dist/assets/tts.d.ts +18 -6
- package/dist/assets/tts.d.ts.map +1 -1
- package/dist/assets/tts.js +219 -97
- package/dist/assets/tts.js.map +1 -1
- package/dist/cli.js +1705 -106
- package/dist/cli.js.map +1 -1
- package/dist/composition/descriptor.d.ts +17 -3
- package/dist/composition/descriptor.d.ts.map +1 -1
- package/dist/composition/descriptor.js +51 -28
- package/dist/composition/descriptor.js.map +1 -1
- package/dist/composition/format.d.ts +12 -0
- package/dist/composition/format.d.ts.map +1 -0
- package/dist/composition/format.js +28 -0
- package/dist/composition/format.js.map +1 -0
- package/dist/composition/index.d.ts +2 -1
- package/dist/composition/index.d.ts.map +1 -1
- package/dist/composition/index.js +1 -0
- package/dist/composition/index.js.map +1 -1
- package/dist/context.d.ts +82 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +277 -0
- package/dist/context.js.map +1 -0
- package/dist/contracts.d.ts +80 -6
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +3 -0
- package/dist/contracts.js.map +1 -1
- package/dist/doctor.d.ts +14 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +166 -7
- package/dist/doctor.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fonts/add.d.ts +41 -0
- package/dist/fonts/add.d.ts.map +1 -0
- package/dist/fonts/add.js +259 -0
- package/dist/fonts/add.js.map +1 -0
- package/dist/fonts/binary.d.ts +4 -0
- package/dist/fonts/binary.d.ts.map +1 -0
- package/dist/fonts/binary.js +21 -0
- package/dist/fonts/binary.js.map +1 -0
- package/dist/fonts/css.d.ts +4 -0
- package/dist/fonts/css.d.ts.map +1 -0
- package/dist/fonts/css.js +50 -0
- package/dist/fonts/css.js.map +1 -0
- package/dist/fonts/fontsource.d.ts +107 -0
- package/dist/fonts/fontsource.d.ts.map +1 -0
- package/dist/fonts/fontsource.js +565 -0
- package/dist/fonts/fontsource.js.map +1 -0
- package/dist/fonts/index.d.ts +11 -0
- package/dist/fonts/index.d.ts.map +1 -0
- package/dist/fonts/index.js +11 -0
- package/dist/fonts/index.js.map +1 -0
- package/dist/fonts/install.d.ts +24 -0
- package/dist/fonts/install.d.ts.map +1 -0
- package/dist/fonts/install.js +145 -0
- package/dist/fonts/install.js.map +1 -0
- package/dist/fonts/manifest.d.ts +21 -0
- package/dist/fonts/manifest.d.ts.map +1 -0
- package/dist/fonts/manifest.js +96 -0
- package/dist/fonts/manifest.js.map +1 -0
- package/dist/fonts/options.d.ts +9 -0
- package/dist/fonts/options.d.ts.map +1 -0
- package/dist/fonts/options.js +66 -0
- package/dist/fonts/options.js.map +1 -0
- package/dist/fonts/schema.d.ts +4 -0
- package/dist/fonts/schema.d.ts.map +1 -0
- package/dist/fonts/schema.js +316 -0
- package/dist/fonts/schema.js.map +1 -0
- package/dist/fonts/summary.d.ts +16 -0
- package/dist/fonts/summary.d.ts.map +1 -0
- package/dist/fonts/summary.js +22 -0
- package/dist/fonts/summary.js.map +1 -0
- package/dist/fonts/types.d.ts +56 -0
- package/dist/fonts/types.d.ts.map +1 -0
- package/dist/fonts/types.js +8 -0
- package/dist/fonts/types.js.map +1 -0
- package/dist/fonts/verify.d.ts +33 -0
- package/dist/fonts/verify.d.ts.map +1 -0
- package/dist/fonts/verify.js +200 -0
- package/dist/fonts/verify.js.map +1 -0
- package/dist/html/document-adapter.d.ts +3 -3
- package/dist/html/document-adapter.d.ts.map +1 -1
- package/dist/html/document-adapter.js +1 -1
- package/dist/html/document-adapter.js.map +1 -1
- package/dist/html/index.d.ts +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/parse.d.ts +10 -2
- package/dist/html/parse.d.ts.map +1 -1
- package/dist/html/parse.js +14 -5
- package/dist/html/parse.js.map +1 -1
- package/dist/html/validate.d.ts +12 -1
- package/dist/html/validate.d.ts.map +1 -1
- package/dist/html/validate.js +292 -61
- package/dist/html/validate.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +61 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +325 -0
- package/dist/inspect.js.map +1 -0
- package/dist/lint.d.ts +63 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +265 -0
- package/dist/lint.js.map +1 -0
- package/dist/policy.d.ts +69 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +361 -0
- package/dist/policy.js.map +1 -0
- package/dist/preview/main.d.ts +2 -2
- package/dist/preview/main.d.ts.map +1 -1
- package/dist/preview/main.js +35 -14
- package/dist/preview/main.js.map +1 -1
- package/dist/preview/timeline-data.d.ts +17 -65
- package/dist/preview/timeline-data.d.ts.map +1 -1
- package/dist/preview/timeline-host.d.ts.map +1 -1
- package/dist/preview/timeline-host.js +73 -38
- package/dist/preview/timeline-host.js.map +1 -1
- package/dist/preview/timeline-main.d.ts.map +1 -1
- package/dist/preview/timeline-main.js +47 -3
- package/dist/preview/timeline-main.js.map +1 -1
- package/dist/project.d.ts +13 -8
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +20 -17
- package/dist/project.js.map +1 -1
- package/dist/renderer/browser.d.ts +2 -2
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +10 -4
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/ffmpeg.d.ts +44 -1
- package/dist/renderer/ffmpeg.d.ts.map +1 -1
- package/dist/renderer/ffmpeg.js +371 -73
- package/dist/renderer/ffmpeg.js.map +1 -1
- package/dist/renderer/index.d.ts +40 -4
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +100 -25
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/keyframe-expressions.d.ts +39 -0
- package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
- package/dist/renderer/keyframe-expressions.js +180 -0
- package/dist/renderer/keyframe-expressions.js.map +1 -0
- package/dist/renderer/timeline.d.ts +2 -0
- package/dist/renderer/timeline.d.ts.map +1 -1
- package/dist/renderer/timeline.js +168 -106
- package/dist/renderer/timeline.js.map +1 -1
- package/dist/runtime/host.d.ts.map +1 -1
- package/dist/runtime/host.js +159 -31
- package/dist/runtime/host.js.map +1 -1
- package/dist/scaffold.d.ts +3 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +48 -317
- package/dist/scaffold.js.map +1 -1
- package/dist/server/composition-html.d.ts +15 -7
- package/dist/server/composition-html.d.ts.map +1 -1
- package/dist/server/composition-html.js +72 -16
- package/dist/server/composition-html.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +56 -6
- package/dist/server/server.js.map +1 -1
- package/dist/server/timeline-preview-data.d.ts.map +1 -1
- package/dist/server/timeline-preview-data.js +12 -23
- package/dist/server/timeline-preview-data.js.map +1 -1
- package/dist/server/timeline-preview-html.d.ts.map +1 -1
- package/dist/server/timeline-preview-html.js +20 -3
- package/dist/server/timeline-preview-html.js.map +1 -1
- package/dist/templates.d.ts +16 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +186 -0
- package/dist/templates.js.map +1 -0
- package/dist/timeline/easing.d.ts +36 -0
- package/dist/timeline/easing.d.ts.map +1 -0
- package/dist/timeline/easing.js +225 -0
- package/dist/timeline/easing.js.map +1 -0
- package/dist/timeline/keyframes.d.ts +23 -0
- package/dist/timeline/keyframes.d.ts.map +1 -0
- package/dist/timeline/keyframes.js +95 -0
- package/dist/timeline/keyframes.js.map +1 -0
- package/dist/timeline/plan.d.ts +6 -12
- package/dist/timeline/plan.d.ts.map +1 -1
- package/dist/timeline/plan.js +17 -30
- package/dist/timeline/plan.js.map +1 -1
- package/dist/timeline/resolve.d.ts +16 -3
- package/dist/timeline/resolve.d.ts.map +1 -1
- package/dist/timeline/resolve.js +119 -13
- package/dist/timeline/resolve.js.map +1 -1
- package/dist/timeline/scene.d.ts +126 -0
- package/dist/timeline/scene.d.ts.map +1 -0
- package/dist/timeline/scene.js +109 -0
- package/dist/timeline/scene.js.map +1 -0
- package/dist/timeline/schema.d.ts +38 -0
- package/dist/timeline/schema.d.ts.map +1 -1
- package/dist/timeline/schema.js +290 -9
- package/dist/timeline/schema.js.map +1 -1
- package/dist/timeline/transitions.d.ts +21 -0
- package/dist/timeline/transitions.d.ts.map +1 -0
- package/dist/timeline/transitions.js +23 -0
- package/dist/timeline/transitions.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +2 -2
- package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
- package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
- package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
- package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
- package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +38 -0
- package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
- package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
- package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
- package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
- package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
- package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
- package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
- package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
- package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
- package/docs/migration-0.4.md +5 -3
- package/docs/migration-0.8.md +163 -0
- package/docs/preview-testing.md +23 -3
- package/docs/{v0.1-design.md → v0.x-design.md} +86 -53
- package/examples/blend/README.md +106 -0
- package/examples/blend/assets/ink.mp4 +0 -0
- package/examples/blend/compositions/aurora.html +55 -0
- package/examples/blend/compositions/backdrop.html +74 -0
- package/examples/blend/compositions/labels.html +149 -0
- package/examples/blend/fy.config.js +11 -0
- package/examples/blend/scripts/make-ink.sh +26 -0
- package/examples/blend/timelines/blend.timeline.json +155 -0
- package/examples/demo/README.md +2 -2
- package/examples/demo/compositions/demo.html +243 -251
- package/examples/intro/README.md +71 -0
- package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
- package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
- package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
- package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
- package/examples/intro/assets/grain.png +0 -0
- package/examples/intro/assets/score.ogg +0 -0
- package/examples/intro/compositions/brand.html +412 -0
- package/examples/intro/compositions/chrome.controller.js +63 -0
- package/examples/intro/compositions/chrome.html +256 -0
- package/examples/intro/compositions/lattice.controller.js +148 -0
- package/examples/intro/compositions/lattice.html +308 -0
- package/examples/intro/compositions/opening.html +504 -0
- package/examples/intro/compositions/studio.html +930 -0
- package/examples/intro/compositions/theme.css +218 -0
- package/examples/intro/fy.config.js +13 -0
- package/examples/intro/fy.fonts.json +100 -0
- package/examples/intro/scripts/make-grain.mjs +79 -0
- package/examples/intro/scripts/make-score.mjs +702 -0
- package/examples/intro/timelines/intro.timeline.json +60 -0
- package/examples/keyframes/README.md +76 -0
- package/examples/keyframes/compositions/backdrop.html +60 -0
- package/examples/keyframes/compositions/card.html +99 -0
- package/examples/keyframes/fy.config.js +10 -0
- package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
- package/examples/nested/README.md +112 -0
- package/examples/nested/assets/pulse.wav +0 -0
- package/examples/nested/compositions/backdrop.html +84 -0
- package/examples/nested/compositions/card.html +275 -0
- package/examples/nested/compositions/overlay.html +307 -0
- package/examples/nested/fy.config.js +13 -0
- package/examples/nested/scripts/make-pulse.sh +20 -0
- package/examples/nested/timelines/chapter-a.timeline.json +39 -0
- package/examples/nested/timelines/chapter-b.timeline.json +39 -0
- package/examples/nested/timelines/nested-reel.timeline.json +52 -0
- package/examples/static/README.md +17 -0
- package/examples/static/compositions/still.html +37 -0
- package/examples/static/fy.config.js +9 -0
- package/examples/static/timelines/still.timeline.json +13 -0
- package/examples/transitions/README.md +128 -0
- package/examples/transitions/assets/alpha.wav +0 -0
- package/examples/transitions/assets/beta.wav +0 -0
- package/examples/transitions/assets/gamma.wav +0 -0
- package/examples/transitions/compositions/alpha.html +56 -0
- package/examples/transitions/compositions/beta.html +56 -0
- package/examples/transitions/compositions/gamma.html +56 -0
- package/examples/transitions/fy.config.js +12 -0
- package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
- package/examples/transitions/timelines/transitions.timeline.json +31 -0
- package/examples/video/README.md +4 -4
- package/examples/video/compositions/video-mp4.html +349 -357
- package/examples/video/compositions/video-overlay.html +337 -345
- package/examples/video/compositions/video.html +349 -357
- package/examples/webgl/README.md +1 -1
- package/examples/webgl/compositions/webgl.html +205 -213
- package/package.json +13 -5
- package/scaffolds/init/AGENTS.md +131 -0
- package/scaffolds/init/README.md +56 -0
- package/scaffolds/init/compositions/hello.html +79 -0
- package/scaffolds/init/fy.config.js +9 -0
- package/scaffolds/init/gitignore.template +1 -0
- package/scaffolds/init/package.json +11 -0
- package/scaffolds/init/props/hello.json +4 -0
- package/scaffolds/init/props/main.json +6 -0
- package/scaffolds/init/timelines/main.timeline.json +20 -0
- package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
- package/scaffolds/local-models/fy.policy.json +5 -0
- package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
- package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
- package/scaffolds/local-models/pi/sandbox.json +15 -0
- package/scaffolds/local-models/pi/settings.json +15 -0
- package/skills/frameyard/README.md +1 -0
- package/skills/frameyard/SKILL.md +59 -59
- package/skills/frameyard/references/audio-assets.md +32 -3
- package/skills/frameyard/references/common-mistakes.md +81 -7
- package/skills/frameyard/references/controllers.md +5 -4
- package/skills/frameyard/references/deterministic-authoring.md +49 -37
- package/skills/frameyard/references/diagnostics.md +42 -4
- package/skills/frameyard/references/failure-recovery.md +56 -10
- package/skills/frameyard/references/fonts.md +132 -0
- package/skills/frameyard/references/json-results.md +165 -10
- package/skills/frameyard/references/quick-reference.md +222 -0
- package/skills/frameyard/references/timelines.md +109 -15
- /package/examples/demo/{video.config.js → fy.config.js} +0 -0
- /package/examples/video/{video.config.js → fy.config.js} +0 -0
- /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
|
@@ -8,9 +8,13 @@ import { validateHtmlComposition } from "../html/validate.js";
|
|
|
8
8
|
import { startPreviewServer } from "../server/server.js";
|
|
9
9
|
import { inspectTimelineAudio } from "./audio.js";
|
|
10
10
|
import { openCompositionSession } from "./browser.js";
|
|
11
|
-
import { assertContainerCodec, assertFfmpegCapabilities, compositeTimelineFrame, compositeTimelineSegment, concatTimelineSegments, CONTAINER_EXTENSION, defaultCodecForContainer, ENCODER_SESSION_LIMIT, encodePngSequenceToFfv1, mixAudioTimeline, muxVideoWithAudio, resolveVideoEncoder, } from "./ffmpeg.js";
|
|
11
|
+
import { assertContainerCodec, assertFfmpegCapabilities, compositeTimelineFrame, compositeTimelineSegment, concatTimelineSegments, CONTAINER_EXTENSION, defaultCodecForContainer, ENCODER_SESSION_LIMIT, encodePngSequenceToFfv1, mixAudioTimeline, muxVideoWithAudio, resolveVideoEncoder, sceneLayerToCompositeLayer, } from "./ffmpeg.js";
|
|
12
12
|
import { inspectTranscriptClips } from "./transcripts.js";
|
|
13
|
+
import { evaluatePresentation } from "../timeline/keyframes.js";
|
|
14
|
+
import { transitionProgress } from "../timeline/transitions.js";
|
|
15
|
+
import { KEYFRAME_CHANNELS } from "../timeline/schema.js";
|
|
13
16
|
import { planTimelineRender } from "../timeline/plan.js";
|
|
17
|
+
import { projectScene, transitionSpans } from "../timeline/scene.js";
|
|
14
18
|
import { resolveProjectTimeline, } from "../timeline/resolve.js";
|
|
15
19
|
export const MAX_TIMELINE_WORKERS = 16;
|
|
16
20
|
function validateWorkers(value) {
|
|
@@ -136,19 +140,6 @@ function compositionDiagnostic(diagnostic, timelineId, clip, compositionId) {
|
|
|
136
140
|
function clampVolume(value) {
|
|
137
141
|
return Math.min(1, Math.max(0, value));
|
|
138
142
|
}
|
|
139
|
-
function visualSourceSize(clip, timeline) {
|
|
140
|
-
const source = clip.source;
|
|
141
|
-
if (source.kind === "composition") {
|
|
142
|
-
return { width: source.metadata.width, height: source.metadata.height };
|
|
143
|
-
}
|
|
144
|
-
if (source.kind === "video" || source.kind === "image") {
|
|
145
|
-
if (source.width === undefined || source.height === undefined) {
|
|
146
|
-
throw new FrameyardError("TIMELINE_MEDIA_INVALID", `Media ${source.src} is missing width/height metadata for clip "${clip.id}".`);
|
|
147
|
-
}
|
|
148
|
-
return { width: source.width, height: source.height };
|
|
149
|
-
}
|
|
150
|
-
return { width: timeline.format.width, height: timeline.format.height };
|
|
151
|
-
}
|
|
152
143
|
function mapCompositionAudio(clip, audio, fps, window) {
|
|
153
144
|
if (window.muted) {
|
|
154
145
|
return null;
|
|
@@ -178,11 +169,13 @@ function mapCompositionAudio(clip, audio, fps, window) {
|
|
|
178
169
|
trimStart: audio.trimStart + (effectiveStart - audio.startFrame) / fps,
|
|
179
170
|
trimDuration: duration,
|
|
180
171
|
volume: clampVolume(audio.volume * window.gain),
|
|
172
|
+
...(window.envelopes.length > 0 ? { envelopes: window.envelopes } : {}),
|
|
181
173
|
};
|
|
182
174
|
}
|
|
183
175
|
function collectTimelineAudio(timeline, window, clips, transcripts) {
|
|
184
176
|
const fps = timeline.format.fps;
|
|
185
177
|
const trackById = new Map(timeline.tracks.map((track) => [track.id, track]));
|
|
178
|
+
const spansByClip = transitionSpans(timeline);
|
|
186
179
|
for (const clip of timeline.clips) {
|
|
187
180
|
const source = clip.source;
|
|
188
181
|
const mappedStart = clip.start + window.frameOffset;
|
|
@@ -194,6 +187,13 @@ function collectTimelineAudio(timeline, window, clips, transcripts) {
|
|
|
194
187
|
}
|
|
195
188
|
const clipGain = window.gain * (clip.audio?.gain ?? 1);
|
|
196
189
|
const clipMuted = window.muted || (clip.audio?.muted ?? false);
|
|
190
|
+
const ownEnvelopes = (spansByClip.get(clip.id) ?? []).map((span) => ({
|
|
191
|
+
startFrame: span.start + window.frameOffset,
|
|
192
|
+
endFrame: span.end + window.frameOffset,
|
|
193
|
+
...(span.easing !== undefined ? { easing: span.easing } : {}),
|
|
194
|
+
role: span.role,
|
|
195
|
+
}));
|
|
196
|
+
const envelopes = ownEnvelopes.length > 0 ? [...window.envelopes, ...ownEnvelopes] : window.envelopes;
|
|
197
197
|
if (source.kind === "composition") {
|
|
198
198
|
for (const transcript of source.transcriptClips) {
|
|
199
199
|
transcripts.set(`${transcript.src}\u0000${transcript.words}`, transcript);
|
|
@@ -202,13 +202,13 @@ function collectTimelineAudio(timeline, window, clips, transcripts) {
|
|
|
202
202
|
continue;
|
|
203
203
|
}
|
|
204
204
|
for (const audio of source.audioClips) {
|
|
205
|
-
const mapped = mapCompositionAudio(clip, audio, fps, { ...window, gain: clipGain });
|
|
205
|
+
const mapped = mapCompositionAudio(clip, audio, fps, { ...window, gain: clipGain, envelopes });
|
|
206
206
|
if (mapped) {
|
|
207
207
|
clips.push(mapped);
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
for (const audio of source.embeddedAudioClips) {
|
|
211
|
-
const mapped = mapCompositionAudio(clip, audio, fps, { ...window, gain: clipGain });
|
|
211
|
+
const mapped = mapCompositionAudio(clip, audio, fps, { ...window, gain: clipGain, envelopes });
|
|
212
212
|
if (mapped) {
|
|
213
213
|
clips.push({ ...mapped, allowSilent: true });
|
|
214
214
|
}
|
|
@@ -222,6 +222,7 @@ function collectTimelineAudio(timeline, window, clips, transcripts) {
|
|
|
222
222
|
endFrame,
|
|
223
223
|
gain: clipGain,
|
|
224
224
|
muted: clipMuted,
|
|
225
|
+
envelopes,
|
|
225
226
|
}, clips, transcripts);
|
|
226
227
|
continue;
|
|
227
228
|
}
|
|
@@ -233,6 +234,7 @@ function collectTimelineAudio(timeline, window, clips, transcripts) {
|
|
|
233
234
|
trimStart: clip.sourceIn / fps + (startFrame - mappedStart) / fps,
|
|
234
235
|
trimDuration: (endFrame - startFrame) / fps,
|
|
235
236
|
volume: clampVolume(clipGain),
|
|
237
|
+
...(envelopes.length > 0 ? { envelopes } : {}),
|
|
236
238
|
});
|
|
237
239
|
}
|
|
238
240
|
continue;
|
|
@@ -248,6 +250,7 @@ function collectTimelineAudio(timeline, window, clips, transcripts) {
|
|
|
248
250
|
trimStart: clip.sourceIn / fps + (startFrame - mappedStart) / fps,
|
|
249
251
|
trimDuration: (endFrame - startFrame) / fps,
|
|
250
252
|
volume: clampVolume(clipGain * (track.gain ?? 1)),
|
|
253
|
+
...(envelopes.length > 0 ? { envelopes } : {}),
|
|
251
254
|
});
|
|
252
255
|
}
|
|
253
256
|
}
|
|
@@ -255,7 +258,7 @@ function collectTimelineAudio(timeline, window, clips, transcripts) {
|
|
|
255
258
|
function buildAudioClips(timeline) {
|
|
256
259
|
const clips = [];
|
|
257
260
|
const transcripts = new Map();
|
|
258
|
-
collectTimelineAudio(timeline, { frameOffset: 0, startFrame: 0, endFrame: timeline.durationInFrames, gain: 1, muted: false }, clips, transcripts);
|
|
261
|
+
collectTimelineAudio(timeline, { frameOffset: 0, startFrame: 0, endFrame: timeline.durationInFrames, gain: 1, muted: false, envelopes: [] }, clips, transcripts);
|
|
259
262
|
return { clips, transcripts: [...transcripts.values()] };
|
|
260
263
|
}
|
|
261
264
|
/** Resolved timeline audio for preview surfaces. */
|
|
@@ -269,6 +272,58 @@ export function resolveTimelineAudio(timeline) {
|
|
|
269
272
|
transcripts,
|
|
270
273
|
};
|
|
271
274
|
}
|
|
275
|
+
async function captureStaticCompositionLayer(options) {
|
|
276
|
+
const { project, timeline, clip, source, serverUrl, scratchRoot, infoCache, staticCache } = options;
|
|
277
|
+
const propsKey = JSON.stringify(clip.props ?? null);
|
|
278
|
+
const cacheKey = `${source.entry}\u0000${propsKey}`;
|
|
279
|
+
const cached = staticCache.get(cacheKey);
|
|
280
|
+
if (cached) {
|
|
281
|
+
return cached;
|
|
282
|
+
}
|
|
283
|
+
const pending = (async () => {
|
|
284
|
+
let validation = infoCache.get(cacheKey);
|
|
285
|
+
if (!validation) {
|
|
286
|
+
validation = await validateHtmlComposition({
|
|
287
|
+
projectRoot: project.root,
|
|
288
|
+
entry: source.entry,
|
|
289
|
+
...(clip.props !== undefined ? { props: clip.props } : {}),
|
|
290
|
+
...(project.format ? { format: project.format } : {}),
|
|
291
|
+
});
|
|
292
|
+
infoCache.set(cacheKey, validation);
|
|
293
|
+
}
|
|
294
|
+
if (!validation.info) {
|
|
295
|
+
throw new FrameyardError("TIMELINE_CLIP_INVALID", `Composition "${source.id}" could not be loaded for clip "${clip.id}".`, { diagnostics: validation.diagnostics });
|
|
296
|
+
}
|
|
297
|
+
const session = await openCompositionSession({
|
|
298
|
+
serverUrl,
|
|
299
|
+
entry: source.entry,
|
|
300
|
+
props: clip.props,
|
|
301
|
+
info: validation.info,
|
|
302
|
+
staticDiagnostics: validation.diagnostics,
|
|
303
|
+
transparentBackground: true,
|
|
304
|
+
});
|
|
305
|
+
try {
|
|
306
|
+
throwIfAborted(options.signal);
|
|
307
|
+
await session.seek(0);
|
|
308
|
+
const png = await session.capture();
|
|
309
|
+
const filePath = path.join(scratchRoot, `static-${randomUUID()}.png`);
|
|
310
|
+
await writeFile(filePath, png);
|
|
311
|
+
const diagnostics = (await session.readDiagnostics()).map((diagnostic) => compositionDiagnostic(diagnostic, timeline.id, clip, source.id));
|
|
312
|
+
return { path: filePath, diagnostics };
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
await session.close();
|
|
316
|
+
}
|
|
317
|
+
})();
|
|
318
|
+
staticCache.set(cacheKey, pending);
|
|
319
|
+
try {
|
|
320
|
+
return await pending;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
staticCache.delete(cacheKey);
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
272
327
|
async function captureCompositionLayer(options) {
|
|
273
328
|
const { project, timeline, clip, source, segment, layerIndex, serverUrl, segmentDir, infoCache } = options;
|
|
274
329
|
const propsKey = JSON.stringify(clip.props ?? null);
|
|
@@ -279,6 +334,7 @@ async function captureCompositionLayer(options) {
|
|
|
279
334
|
projectRoot: project.root,
|
|
280
335
|
entry: source.entry,
|
|
281
336
|
...(clip.props !== undefined ? { props: clip.props } : {}),
|
|
337
|
+
...(project.format ? { format: project.format } : {}),
|
|
282
338
|
});
|
|
283
339
|
infoCache.set(cacheKey, validation);
|
|
284
340
|
}
|
|
@@ -341,6 +397,7 @@ async function renderNestedTimelineRange(options) {
|
|
|
341
397
|
serverUrl: options.serverUrl,
|
|
342
398
|
scratchRoot: options.scratchRoot,
|
|
343
399
|
infoCache: options.infoCache,
|
|
400
|
+
staticCache: options.staticCache,
|
|
344
401
|
codec: "ffv1",
|
|
345
402
|
background: options.timeline.background ?? "transparent",
|
|
346
403
|
prefix,
|
|
@@ -377,26 +434,23 @@ async function processTimelineSegment(options) {
|
|
|
377
434
|
throwIfAborted(options.signal);
|
|
378
435
|
const clip = layer.clip;
|
|
379
436
|
const source = clip.source;
|
|
380
|
-
const transform = clip.transform ?? {
|
|
381
|
-
fit: "contain",
|
|
382
|
-
x: 0,
|
|
383
|
-
y: 0,
|
|
384
|
-
scale: 1,
|
|
385
|
-
rotation: 0,
|
|
386
|
-
opacity: 1,
|
|
387
|
-
};
|
|
388
|
-
const base = {
|
|
389
|
-
fit: transform.fit,
|
|
390
|
-
x: transform.x,
|
|
391
|
-
y: transform.y,
|
|
392
|
-
...(transform.width !== undefined && transform.height !== undefined
|
|
393
|
-
? { width: transform.width, height: transform.height }
|
|
394
|
-
: {}),
|
|
395
|
-
scale: transform.scale,
|
|
396
|
-
rotation: transform.rotation,
|
|
397
|
-
opacity: transform.opacity,
|
|
398
|
-
};
|
|
399
437
|
if (source.kind === "composition") {
|
|
438
|
+
if (layer.capture === "still") {
|
|
439
|
+
const captured = await captureStaticCompositionLayer({
|
|
440
|
+
project,
|
|
441
|
+
timeline,
|
|
442
|
+
clip,
|
|
443
|
+
source,
|
|
444
|
+
serverUrl,
|
|
445
|
+
scratchRoot,
|
|
446
|
+
infoCache,
|
|
447
|
+
staticCache: options.staticCache,
|
|
448
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
449
|
+
});
|
|
450
|
+
diagnostics.push(...captured.diagnostics);
|
|
451
|
+
layers.push(sceneLayerToCompositeLayer(layer, { path: captured.path, input: "image" }));
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
400
454
|
const captured = await captureCompositionLayer({
|
|
401
455
|
project,
|
|
402
456
|
timeline,
|
|
@@ -410,25 +464,15 @@ async function processTimelineSegment(options) {
|
|
|
410
464
|
...(options.signal ? { signal: options.signal } : {}),
|
|
411
465
|
});
|
|
412
466
|
diagnostics.push(...captured.diagnostics);
|
|
413
|
-
layers.push({
|
|
414
|
-
kind: "composition",
|
|
415
|
-
path: captured.path,
|
|
416
|
-
sourceWidth: source.metadata.width,
|
|
417
|
-
sourceHeight: source.metadata.height,
|
|
418
|
-
...base,
|
|
419
|
-
});
|
|
467
|
+
layers.push(sceneLayerToCompositeLayer(layer, { path: captured.path, input: "sequence" }));
|
|
420
468
|
continue;
|
|
421
469
|
}
|
|
422
470
|
if (source.kind === "video" || source.kind === "image") {
|
|
423
|
-
|
|
424
|
-
layers.push({
|
|
425
|
-
kind: source.kind,
|
|
471
|
+
layers.push(sceneLayerToCompositeLayer(layer, {
|
|
426
472
|
path: source.absolutePath,
|
|
427
|
-
|
|
428
|
-
sourceHeight: size.height,
|
|
429
|
-
...base,
|
|
473
|
+
input: source.kind,
|
|
430
474
|
...(source.kind === "video" ? { sourceStartFrame: layer.sourceStart } : {}),
|
|
431
|
-
});
|
|
475
|
+
}));
|
|
432
476
|
continue;
|
|
433
477
|
}
|
|
434
478
|
if (source.kind === "timeline") {
|
|
@@ -440,17 +484,12 @@ async function processTimelineSegment(options) {
|
|
|
440
484
|
serverUrl,
|
|
441
485
|
scratchRoot,
|
|
442
486
|
infoCache,
|
|
487
|
+
staticCache: options.staticCache,
|
|
443
488
|
depth: depth + 1,
|
|
444
489
|
...(options.signal ? { signal: options.signal } : {}),
|
|
445
490
|
});
|
|
446
491
|
diagnostics.push(...nested.diagnostics);
|
|
447
|
-
layers.push({
|
|
448
|
-
kind: "timeline",
|
|
449
|
-
path: nested.path,
|
|
450
|
-
sourceWidth: source.timeline.format.width,
|
|
451
|
-
sourceHeight: source.timeline.format.height,
|
|
452
|
-
...base,
|
|
453
|
-
});
|
|
492
|
+
layers.push(sceneLayerToCompositeLayer(layer, { path: nested.path, input: "sequence" }));
|
|
454
493
|
continue;
|
|
455
494
|
}
|
|
456
495
|
throw new FrameyardError("TIMELINE_INVALID", `Unsupported visual source for clip "${clip.id}".`);
|
|
@@ -466,6 +505,7 @@ async function processTimelineSegment(options) {
|
|
|
466
505
|
background,
|
|
467
506
|
layers,
|
|
468
507
|
codec,
|
|
508
|
+
frameOffset: segment.start,
|
|
469
509
|
...(options.container !== undefined ? { container: options.container } : {}),
|
|
470
510
|
...(options.encoder !== undefined ? { encoder: options.encoder } : {}),
|
|
471
511
|
...(options.signal ? { signal: options.signal } : {}),
|
|
@@ -473,6 +513,51 @@ async function processTimelineSegment(options) {
|
|
|
473
513
|
await rm(segmentDir, { recursive: true, force: true });
|
|
474
514
|
return { path: segmentPath, diagnostics };
|
|
475
515
|
}
|
|
516
|
+
function checkKeyframeEvaluation(timeline, segments) {
|
|
517
|
+
for (const segment of segments) {
|
|
518
|
+
for (const layer of segment.layers) {
|
|
519
|
+
const keyframes = layer.keyframes;
|
|
520
|
+
if (!keyframes) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
for (let frame = segment.start; frame < segment.end; frame += 1) {
|
|
524
|
+
const values = evaluatePresentation(layer.transform, keyframes, frame - layer.clip.start);
|
|
525
|
+
for (const channel of KEYFRAME_CHANNELS) {
|
|
526
|
+
if (!Number.isFinite(values[channel])) {
|
|
527
|
+
throw new FrameyardError("TIMELINE_CLIP_INVALID", `Clip "${layer.clip.id}" keyframes.${channel} produced a non-finite value at timeline frame ${frame}.`, {
|
|
528
|
+
details: {
|
|
529
|
+
timeline: timeline.id,
|
|
530
|
+
clip: layer.clip.id,
|
|
531
|
+
track: layer.clip.track,
|
|
532
|
+
channel,
|
|
533
|
+
frame: frame - layer.clip.start,
|
|
534
|
+
timelineFrame: frame,
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function checkTransitionEvaluation(timeline) {
|
|
544
|
+
for (const transition of timeline.transitions) {
|
|
545
|
+
for (let frame = transition.start; frame < transition.end; frame += 1) {
|
|
546
|
+
const progress = transitionProgress(transition, frame);
|
|
547
|
+
if (!Number.isFinite(progress)) {
|
|
548
|
+
throw new FrameyardError("TIMELINE_TRANSITION_INVALID", `Transition "${transition.from}" → "${transition.to}" produced a non-finite progress at timeline frame ${frame}.`, {
|
|
549
|
+
details: {
|
|
550
|
+
timeline: timeline.id,
|
|
551
|
+
track: transition.track,
|
|
552
|
+
from: transition.from,
|
|
553
|
+
to: transition.to,
|
|
554
|
+
timelineFrame: frame,
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
476
561
|
async function checkResolvedTimelineRange(options) {
|
|
477
562
|
if (options.context.depth > 32) {
|
|
478
563
|
throw new FrameyardError("TIMELINE_INVALID", "Nested timeline depth exceeds 32.");
|
|
@@ -480,6 +565,8 @@ async function checkResolvedTimelineRange(options) {
|
|
|
480
565
|
const { timeline, startFrame, endFrame, context } = options;
|
|
481
566
|
const plan = planTimelineRender(timeline, { startFrame, endFrame });
|
|
482
567
|
const diagnostics = [];
|
|
568
|
+
checkKeyframeEvaluation(timeline, plan.segments);
|
|
569
|
+
checkTransitionEvaluation(timeline);
|
|
483
570
|
const targets = new Map();
|
|
484
571
|
for (const segment of plan.segments) {
|
|
485
572
|
for (const layer of segment.layers) {
|
|
@@ -493,6 +580,7 @@ async function checkResolvedTimelineRange(options) {
|
|
|
493
580
|
projectRoot: context.project.root,
|
|
494
581
|
entry: source.entry,
|
|
495
582
|
...(layer.clip.props !== undefined ? { props: layer.clip.props } : {}),
|
|
583
|
+
...(context.project.format ? { format: context.project.format } : {}),
|
|
496
584
|
});
|
|
497
585
|
if (!validation.info) {
|
|
498
586
|
throw new FrameyardError("TIMELINE_CLIP_INVALID", `Composition "${source.id}" could not be loaded for clip "${layer.clip.id}".`, { diagnostics: validation.diagnostics });
|
|
@@ -512,6 +600,9 @@ async function checkResolvedTimelineRange(options) {
|
|
|
512
600
|
staticDiagnostics: target.validation.diagnostics,
|
|
513
601
|
});
|
|
514
602
|
sessions.set(key, session);
|
|
603
|
+
if (target.source.metadata.static === true) {
|
|
604
|
+
await session.seek(0);
|
|
605
|
+
}
|
|
515
606
|
}
|
|
516
607
|
for (const segment of plan.segments) {
|
|
517
608
|
for (let frame = segment.start; frame < segment.end; frame += 1) {
|
|
@@ -521,6 +612,9 @@ async function checkResolvedTimelineRange(options) {
|
|
|
521
612
|
continue;
|
|
522
613
|
}
|
|
523
614
|
const source = layer.clip.source;
|
|
615
|
+
if (source.metadata.static === true) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
524
618
|
const key = `${source.entry}\u0000${JSON.stringify(layer.clip.props ?? null)}`;
|
|
525
619
|
const session = sessions.get(key);
|
|
526
620
|
if (!session) {
|
|
@@ -579,7 +673,7 @@ export async function checkTimeline(options) {
|
|
|
579
673
|
const startedAt = performance.now();
|
|
580
674
|
const projectTimeline = options.project.timelines.get(options.timelineId);
|
|
581
675
|
if (!projectTimeline) {
|
|
582
|
-
throw new FrameyardError("TIMELINE_NOT_FOUND", `Timeline "${options.timelineId}" is not listed in
|
|
676
|
+
throw new FrameyardError("TIMELINE_NOT_FOUND", `Timeline "${options.timelineId}" is not listed in fy.config.js.`);
|
|
583
677
|
}
|
|
584
678
|
const resolved = await resolveProjectTimeline({
|
|
585
679
|
project: options.project,
|
|
@@ -655,6 +749,7 @@ export async function checkTimeline(options) {
|
|
|
655
749
|
framesChecked: timeline.durationInFrames,
|
|
656
750
|
tracks: timeline.tracks.length,
|
|
657
751
|
clips: timeline.clips.length,
|
|
752
|
+
transitions: timeline.transitions.length,
|
|
658
753
|
audio: { clips: audioInspection.clips.length, details: audioInspection.clips },
|
|
659
754
|
transcripts: {
|
|
660
755
|
clips: transcripts.clips.length,
|
|
@@ -669,40 +764,18 @@ async function renderResolvedTimelineFrame(options) {
|
|
|
669
764
|
throw new FrameyardError("TIMELINE_INVALID", "Nested timeline depth exceeds 32.");
|
|
670
765
|
}
|
|
671
766
|
const { timeline, frame, context } = options;
|
|
672
|
-
const
|
|
673
|
-
const segment = plan.segments[0];
|
|
674
|
-
if (!segment) {
|
|
675
|
-
throw new FrameyardError("FRAME_OUT_OF_RANGE", `Frame ${frame} does not belong to a timeline segment.`);
|
|
676
|
-
}
|
|
767
|
+
const scene = projectScene(timeline, frame, frame + 1);
|
|
677
768
|
const diagnostics = [];
|
|
678
769
|
const layers = [];
|
|
679
|
-
for (const [layerIndex, layer] of
|
|
770
|
+
for (const [layerIndex, layer] of scene.layers.entries()) {
|
|
680
771
|
const clip = layer.clip;
|
|
681
772
|
const source = clip.source;
|
|
682
|
-
const transform = clip.transform ?? {
|
|
683
|
-
fit: "contain",
|
|
684
|
-
x: 0,
|
|
685
|
-
y: 0,
|
|
686
|
-
scale: 1,
|
|
687
|
-
rotation: 0,
|
|
688
|
-
opacity: 1,
|
|
689
|
-
};
|
|
690
|
-
const base = {
|
|
691
|
-
fit: transform.fit,
|
|
692
|
-
x: transform.x,
|
|
693
|
-
y: transform.y,
|
|
694
|
-
...(transform.width !== undefined && transform.height !== undefined
|
|
695
|
-
? { width: transform.width, height: transform.height }
|
|
696
|
-
: {}),
|
|
697
|
-
scale: transform.scale,
|
|
698
|
-
rotation: transform.rotation,
|
|
699
|
-
opacity: transform.opacity,
|
|
700
|
-
};
|
|
701
773
|
if (source.kind === "composition") {
|
|
702
774
|
const validation = await validateHtmlComposition({
|
|
703
775
|
projectRoot: context.project.root,
|
|
704
776
|
entry: source.entry,
|
|
705
777
|
...(clip.props !== undefined ? { props: clip.props } : {}),
|
|
778
|
+
...(context.project.format ? { format: context.project.format } : {}),
|
|
706
779
|
});
|
|
707
780
|
if (!validation.info) {
|
|
708
781
|
throw new FrameyardError("TIMELINE_CLIP_INVALID", `Composition "${source.id}" could not be loaded for clip "${clip.id}".`, { diagnostics: validation.diagnostics });
|
|
@@ -727,25 +800,15 @@ async function renderResolvedTimelineFrame(options) {
|
|
|
727
800
|
finally {
|
|
728
801
|
await session.close();
|
|
729
802
|
}
|
|
730
|
-
layers.push({
|
|
731
|
-
kind: "image",
|
|
732
|
-
path: framePath,
|
|
733
|
-
sourceWidth: source.metadata.width,
|
|
734
|
-
sourceHeight: source.metadata.height,
|
|
735
|
-
...base,
|
|
736
|
-
});
|
|
803
|
+
layers.push(sceneLayerToCompositeLayer(layer, { path: framePath, input: "image" }));
|
|
737
804
|
continue;
|
|
738
805
|
}
|
|
739
806
|
if (source.kind === "video" || source.kind === "image") {
|
|
740
|
-
|
|
741
|
-
layers.push({
|
|
742
|
-
kind: source.kind,
|
|
807
|
+
layers.push(sceneLayerToCompositeLayer(layer, {
|
|
743
808
|
path: source.absolutePath,
|
|
744
|
-
|
|
745
|
-
sourceHeight: size.height,
|
|
746
|
-
...base,
|
|
809
|
+
input: source.kind,
|
|
747
810
|
...(source.kind === "video" ? { sourceStartFrame: layer.sourceStart } : {}),
|
|
748
|
-
});
|
|
811
|
+
}));
|
|
749
812
|
continue;
|
|
750
813
|
}
|
|
751
814
|
if (source.kind === "timeline") {
|
|
@@ -758,13 +821,7 @@ async function renderResolvedTimelineFrame(options) {
|
|
|
758
821
|
context: { ...context, depth: context.depth + 1 },
|
|
759
822
|
});
|
|
760
823
|
diagnostics.push(...nestedDiagnostics);
|
|
761
|
-
layers.push({
|
|
762
|
-
kind: "image",
|
|
763
|
-
path: nestedPath,
|
|
764
|
-
sourceWidth: source.timeline.format.width,
|
|
765
|
-
sourceHeight: source.timeline.format.height,
|
|
766
|
-
...base,
|
|
767
|
-
});
|
|
824
|
+
layers.push(sceneLayerToCompositeLayer(layer, { path: nestedPath, input: "image" }));
|
|
768
825
|
continue;
|
|
769
826
|
}
|
|
770
827
|
throw new FrameyardError("TIMELINE_INVALID", `Unsupported visual source for clip "${clip.id}".`);
|
|
@@ -776,6 +833,7 @@ async function renderResolvedTimelineFrame(options) {
|
|
|
776
833
|
fps: timeline.format.fps,
|
|
777
834
|
background: options.background,
|
|
778
835
|
layers,
|
|
836
|
+
frameOffset: frame,
|
|
779
837
|
...(context.signal ? { signal: context.signal } : {}),
|
|
780
838
|
});
|
|
781
839
|
return diagnostics;
|
|
@@ -808,7 +866,7 @@ export async function captureTimelineStills(options) {
|
|
|
808
866
|
const startedAt = performance.now();
|
|
809
867
|
const projectTimeline = options.project.timelines.get(options.timelineId);
|
|
810
868
|
if (!projectTimeline) {
|
|
811
|
-
throw new FrameyardError("TIMELINE_NOT_FOUND", `Timeline "${options.timelineId}" is not listed in
|
|
869
|
+
throw new FrameyardError("TIMELINE_NOT_FOUND", `Timeline "${options.timelineId}" is not listed in fy.config.js.`);
|
|
812
870
|
}
|
|
813
871
|
const resolved = await resolveProjectTimeline({
|
|
814
872
|
project: options.project,
|
|
@@ -919,7 +977,7 @@ export async function renderTimeline(options) {
|
|
|
919
977
|
const workers = clampTimelineWorkers(requestedWorkers, encoder);
|
|
920
978
|
const projectTimeline = options.project.timelines.get(options.timelineId);
|
|
921
979
|
if (!projectTimeline) {
|
|
922
|
-
throw new FrameyardError("TIMELINE_NOT_FOUND", `Timeline "${options.timelineId}" is not listed in
|
|
980
|
+
throw new FrameyardError("TIMELINE_NOT_FOUND", `Timeline "${options.timelineId}" is not listed in fy.config.js.`);
|
|
923
981
|
}
|
|
924
982
|
const resolved = await resolveProjectTimeline({
|
|
925
983
|
project: options.project,
|
|
@@ -940,6 +998,7 @@ export async function renderTimeline(options) {
|
|
|
940
998
|
const scratchRoot = await mkdtemp(path.join(scratchBase, "frameyard-timeline-"));
|
|
941
999
|
const server = await startPreviewServer({ projectRoot: options.project.root, port: 0 });
|
|
942
1000
|
const infoCache = new Map();
|
|
1001
|
+
const staticCache = new Map();
|
|
943
1002
|
const diagnostics = [...resolved.diagnostics];
|
|
944
1003
|
if (workers < requestedWorkers) {
|
|
945
1004
|
diagnostics.push({
|
|
@@ -999,6 +1058,7 @@ export async function renderTimeline(options) {
|
|
|
999
1058
|
serverUrl: server.url,
|
|
1000
1059
|
scratchRoot,
|
|
1001
1060
|
infoCache,
|
|
1061
|
+
staticCache,
|
|
1002
1062
|
codec,
|
|
1003
1063
|
container,
|
|
1004
1064
|
encoder,
|
|
@@ -1040,6 +1100,7 @@ export async function renderTimeline(options) {
|
|
|
1040
1100
|
trimStart: asset.clip.trimStart,
|
|
1041
1101
|
...(asset.clip.trimDuration !== undefined ? { trimDuration: asset.clip.trimDuration } : {}),
|
|
1042
1102
|
volume: asset.clip.volume,
|
|
1103
|
+
...(asset.clip.envelopes !== undefined ? { volumeEnvelopes: asset.clip.envelopes } : {}),
|
|
1043
1104
|
})),
|
|
1044
1105
|
fps: timeline.format.fps,
|
|
1045
1106
|
durationSeconds: total / timeline.format.fps,
|
|
@@ -1073,6 +1134,7 @@ export async function renderTimeline(options) {
|
|
|
1073
1134
|
framesRendered: total,
|
|
1074
1135
|
segments: plan.segments.length,
|
|
1075
1136
|
workers,
|
|
1137
|
+
transitions: timeline.transitions.length,
|
|
1076
1138
|
codec,
|
|
1077
1139
|
container,
|
|
1078
1140
|
encoder,
|