frameyard 0.0.0-stage → 0.2.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 +69 -0
- package/GLOSSARY.md +145 -0
- package/LICENSE +21 -0
- package/README.md +311 -2
- package/dist/assets/executable.d.ts +16 -0
- package/dist/assets/executable.d.ts.map +1 -0
- package/dist/assets/executable.js +59 -0
- package/dist/assets/executable.js.map +1 -0
- package/dist/assets/files.d.ts +4 -0
- package/dist/assets/files.d.ts.map +1 -0
- package/dist/assets/files.js +30 -0
- package/dist/assets/files.js.map +1 -0
- package/dist/assets/integrations.d.ts +15 -0
- package/dist/assets/integrations.d.ts.map +1 -0
- package/dist/assets/integrations.js +15 -0
- package/dist/assets/integrations.js.map +1 -0
- package/dist/assets/process.d.ts +29 -0
- package/dist/assets/process.d.ts.map +1 -0
- package/dist/assets/process.js +97 -0
- package/dist/assets/process.js.map +1 -0
- package/dist/assets/provenance.d.ts +77 -0
- package/dist/assets/provenance.d.ts.map +1 -0
- package/dist/assets/provenance.js +302 -0
- package/dist/assets/provenance.js.map +1 -0
- package/dist/assets/transcribe.d.ts +38 -0
- package/dist/assets/transcribe.d.ts.map +1 -0
- package/dist/assets/transcribe.js +357 -0
- package/dist/assets/transcribe.js.map +1 -0
- package/dist/assets/tts.d.ts +29 -0
- package/dist/assets/tts.d.ts.map +1 -0
- package/dist/assets/tts.js +180 -0
- package/dist/assets/tts.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +795 -0
- package/dist/cli.js.map +1 -0
- package/dist/contracts.d.ts +106 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +297 -0
- package/dist/contracts.js.map +1 -0
- package/dist/doctor.d.ts +56 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +476 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/preview/main.d.ts +19 -0
- package/dist/preview/main.d.ts.map +1 -0
- package/dist/preview/main.js +480 -0
- package/dist/preview/main.js.map +1 -0
- package/dist/project.d.ts +16 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +88 -0
- package/dist/project.js.map +1 -0
- package/dist/renderer/audio.d.ts +31 -0
- package/dist/renderer/audio.d.ts.map +1 -0
- package/dist/renderer/audio.js +88 -0
- package/dist/renderer/audio.js.map +1 -0
- package/dist/renderer/browser.d.ts +22 -0
- package/dist/renderer/browser.d.ts.map +1 -0
- package/dist/renderer/browser.js +175 -0
- package/dist/renderer/browser.js.map +1 -0
- package/dist/renderer/ffmpeg.d.ts +58 -0
- package/dist/renderer/ffmpeg.d.ts.map +1 -0
- package/dist/renderer/ffmpeg.js +268 -0
- package/dist/renderer/ffmpeg.js.map +1 -0
- package/dist/renderer/index.d.ts +72 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +281 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/transcripts.d.ts +30 -0
- package/dist/renderer/transcripts.d.ts.map +1 -0
- package/dist/renderer/transcripts.js +86 -0
- package/dist/renderer/transcripts.js.map +1 -0
- package/dist/runtime/clock.d.ts +5 -0
- package/dist/runtime/clock.d.ts.map +1 -0
- package/dist/runtime/clock.js +16 -0
- package/dist/runtime/clock.js.map +1 -0
- package/dist/runtime/index.d.ts +29 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +289 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/random.d.ts +8 -0
- package/dist/runtime/random.d.ts.map +1 -0
- package/dist/runtime/random.js +21 -0
- package/dist/runtime/random.js.map +1 -0
- package/dist/scaffold.d.ts +3 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +176 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/server/preview-html.d.ts +2 -0
- package/dist/server/preview-html.d.ts.map +1 -0
- package/dist/server/preview-html.js +229 -0
- package/dist/server/preview-html.js.map +1 -0
- package/dist/server/server.d.ts +17 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +237 -0
- package/dist/server/server.js.map +1 -0
- package/dist/transcript.d.ts +44 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +130 -0
- package/dist/transcript.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/docs/adr/0001-keep-composition-authoring-web-native.md +3 -0
- package/docs/adr/0002-make-frame-and-props-the-only-animation-state.md +3 -0
- package/docs/adr/0003-render-browser-frames-offline.md +3 -0
- package/docs/adr/0004-produce-local-ai-assets-outside-rendering.md +3 -0
- package/docs/v0.1-design.md +238 -0
- package/examples/demo/README.md +24 -0
- package/examples/demo/assets/chime.wav +0 -0
- package/examples/demo/compositions/demo.js +206 -0
- package/examples/demo/props/demo.json +5 -0
- package/examples/demo/video.config.js +5 -0
- package/package.json +73 -3
- package/skills/frameyard/README.md +35 -0
- package/skills/frameyard/SKILL.md +60 -0
- package/skills/frameyard/references/audio-assets.md +86 -0
- package/skills/frameyard/references/deterministic-authoring.md +107 -0
- package/skills/frameyard/references/failure-recovery.md +57 -0
- package/skills/frameyard/references/json-results.md +203 -0
- package/skills/frameyard/references/subtitles-karaoke.md +95 -0
- package/skills/frameyard/scripts/frameyard.mjs +60 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FrameyardError } from "../errors.js";
|
|
4
|
+
import { probeMedia } from "./ffmpeg.js";
|
|
5
|
+
function contains(root, candidate) {
|
|
6
|
+
return candidate === root || candidate.startsWith(root + path.sep);
|
|
7
|
+
}
|
|
8
|
+
/** Resolves local audio files, probes durations, and reports timeline overruns. */
|
|
9
|
+
export async function inspectTimelineAudio(options) {
|
|
10
|
+
const compositionDuration = options.durationInFrames / options.fps;
|
|
11
|
+
const assets = [];
|
|
12
|
+
const warnings = [];
|
|
13
|
+
for (const [clipIndex, clip] of options.clips.entries()) {
|
|
14
|
+
if (options.signal?.aborted) {
|
|
15
|
+
throw new FrameyardError("RENDER_FAILED", "Operation cancelled.");
|
|
16
|
+
}
|
|
17
|
+
const absolutePath = path.resolve(options.projectRoot, clip.src);
|
|
18
|
+
if (!contains(options.projectRoot, absolutePath)) {
|
|
19
|
+
throw new FrameyardError("AUDIO_INVALID", `Audio clip ${clipIndex + 1} source escapes the project root: ${clip.src}`);
|
|
20
|
+
}
|
|
21
|
+
let fileStat;
|
|
22
|
+
try {
|
|
23
|
+
fileStat = await stat(absolutePath);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new FrameyardError("ASSET_NOT_FOUND", `Audio clip ${clipIndex + 1} source does not exist: ${clip.src}`);
|
|
27
|
+
}
|
|
28
|
+
if (!fileStat.isFile()) {
|
|
29
|
+
throw new FrameyardError("ASSET_NOT_FOUND", `Audio clip ${clipIndex + 1} source is not a file: ${clip.src}`);
|
|
30
|
+
}
|
|
31
|
+
const probe = await probeMedia(absolutePath, { signal: options.signal });
|
|
32
|
+
if (!probe.streams.some((stream) => stream.codec_type === "audio")) {
|
|
33
|
+
throw new FrameyardError("AUDIO_INVALID", `Audio clip ${clipIndex + 1} source has no audio stream: ${clip.src}`);
|
|
34
|
+
}
|
|
35
|
+
const rawDuration = Number(probe.format?.duration);
|
|
36
|
+
const streamDuration = Math.max(0, ...probe.streams
|
|
37
|
+
.filter((stream) => stream.codec_type === "audio")
|
|
38
|
+
.map((stream) => Number(stream.duration))
|
|
39
|
+
.filter((value) => Number.isFinite(value)));
|
|
40
|
+
const sourceDurationSeconds = Number.isFinite(rawDuration) && rawDuration > 0 ? rawDuration : streamDuration;
|
|
41
|
+
if (!Number.isFinite(sourceDurationSeconds) || sourceDurationSeconds <= 0) {
|
|
42
|
+
throw new FrameyardError("AUDIO_INVALID", `Audio clip ${clipIndex + 1} source duration could not be determined: ${clip.src}`);
|
|
43
|
+
}
|
|
44
|
+
if (clip.trimStart >= sourceDurationSeconds) {
|
|
45
|
+
warnings.push(`Audio clip ${clipIndex + 1} (${clip.src}) trimStart ${clip.trimStart}s is at or beyond the source duration ${sourceDurationSeconds.toFixed(2)}s.`);
|
|
46
|
+
}
|
|
47
|
+
if (clip.trimDuration !== undefined && clip.trimStart + clip.trimDuration > sourceDurationSeconds + 0.001) {
|
|
48
|
+
warnings.push(`Audio clip ${clipIndex + 1} (${clip.src}) trim range ends at ${(clip.trimStart + clip.trimDuration).toFixed(2)}s, beyond the source duration ${sourceDurationSeconds.toFixed(2)}s.`);
|
|
49
|
+
}
|
|
50
|
+
const timelineStartSeconds = clip.startFrame / options.fps;
|
|
51
|
+
const usedDuration = clip.trimDuration ?? Math.max(0, sourceDurationSeconds - clip.trimStart);
|
|
52
|
+
const timelineEndSeconds = timelineStartSeconds + usedDuration;
|
|
53
|
+
if (timelineStartSeconds >= compositionDuration) {
|
|
54
|
+
warnings.push(`Audio clip ${clipIndex + 1} (${clip.src}) starts at ${timelineStartSeconds.toFixed(2)}s, after the composition ends at ${compositionDuration.toFixed(2)}s.`);
|
|
55
|
+
}
|
|
56
|
+
else if (timelineEndSeconds > compositionDuration + 0.001) {
|
|
57
|
+
warnings.push(`Audio clip ${clipIndex + 1} (${clip.src}) ends at ${timelineEndSeconds.toFixed(2)}s, beyond the composition duration ${compositionDuration.toFixed(2)}s.`);
|
|
58
|
+
}
|
|
59
|
+
assets.push({
|
|
60
|
+
clip,
|
|
61
|
+
path: absolutePath,
|
|
62
|
+
sourceDurationSeconds,
|
|
63
|
+
timelineStartSeconds,
|
|
64
|
+
timelineEndSeconds,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
assets,
|
|
69
|
+
clips: assets.map((asset) => {
|
|
70
|
+
const clip = asset.clip;
|
|
71
|
+
const result = {
|
|
72
|
+
src: clip.src,
|
|
73
|
+
startFrame: clip.startFrame,
|
|
74
|
+
startSeconds: asset.timelineStartSeconds,
|
|
75
|
+
endSeconds: asset.timelineEndSeconds,
|
|
76
|
+
sourceDurationSeconds: asset.sourceDurationSeconds,
|
|
77
|
+
trimStart: clip.trimStart,
|
|
78
|
+
volume: clip.volume,
|
|
79
|
+
};
|
|
80
|
+
if (clip.trimDuration !== undefined) {
|
|
81
|
+
result.trimDuration = clip.trimDuration;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}),
|
|
85
|
+
warnings,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../../src/renderer/audio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAyBzC,SAAS,QAAQ,CAAC,IAAY,EAAE,SAAiB;IAC/C,OAAO,SAAS,KAAK,IAAI,IAAI,SAAS,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;AACrE,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,OAM1C;IACC,MAAM,mBAAmB,GAAG,OAAO,CAAC,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC;IACnE,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QACxD,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,cAAc,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACjE,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,cAAc,CACtB,eAAe,EACf,cAAc,SAAS,GAAG,CAAC,qCAAqC,IAAI,CAAC,GAAG,EAAE,CAC3E,CAAC;QACJ,CAAC;QAED,IAAI,QAAQ,CAAC;QACb,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,cAAc,CAAC,iBAAiB,EAAE,cAAc,SAAS,GAAG,CAAC,2BAA2B,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAChH,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YACvB,MAAM,IAAI,cAAc,CAAC,iBAAiB,EAAE,cAAc,SAAS,GAAG,CAAC,0BAA0B,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/G,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,KAAK,OAAO,CAAC,EAAE,CAAC;YACnE,MAAM,IAAI,cAAc,CAAC,eAAe,EAAE,cAAc,SAAS,GAAG,CAAC,gCAAgC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACnH,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACnD,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAC7B,CAAC,EACD,GAAG,KAAK,CAAC,OAAO;aACb,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,KAAK,OAAO,CAAC;aACjD,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;aACxC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAC7C,CAAC;QACF,MAAM,qBAAqB,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC;QAE7G,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAC,IAAI,qBAAqB,IAAI,CAAC,EAAE,CAAC;YAC1E,MAAM,IAAI,cAAc,CACtB,eAAe,EACf,cAAc,SAAS,GAAG,CAAC,6CAA6C,IAAI,CAAC,GAAG,EAAE,CACnF,CAAC;QACJ,CAAC;QAED,IAAI,IAAI,CAAC,SAAS,IAAI,qBAAqB,EAAE,CAAC;YAC5C,QAAQ,CAAC,IAAI,CACX,cAAc,SAAS,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,eAAe,IAAI,CAAC,SAAS,yCAAyC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CACnJ,CAAC;QACJ,CAAC;QACD,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,YAAY,GAAG,qBAAqB,GAAG,KAAK,EAAE,CAAC;YAC1G,QAAQ,CAAC,IAAI,CACX,cAAc,SAAS,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,wBAAwB,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,iCAAiC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CACrL,CAAC;QACJ,CAAC;QAED,MAAM,oBAAoB,GAAG,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC;QAC3D,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9F,MAAM,kBAAkB,GAAG,oBAAoB,GAAG,YAAY,CAAC;QAE/D,IAAI,oBAAoB,IAAI,mBAAmB,EAAE,CAAC;YAChD,QAAQ,CAAC,IAAI,CACX,cAAc,SAAS,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,eAAe,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC,oCAAoC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAC7J,CAAC;QACJ,CAAC;aAAM,IAAI,kBAAkB,GAAG,mBAAmB,GAAG,KAAK,EAAE,CAAC;YAC5D,QAAQ,CAAC,IAAI,CACX,cAAc,SAAS,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,aAAa,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,sCAAsC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAC3J,CAAC;QACJ,CAAC;QAED,MAAM,CAAC,IAAI,CAAC;YACV,IAAI;YACJ,IAAI,EAAE,YAAY;YAClB,qBAAqB;YACrB,oBAAoB;YACpB,kBAAkB;SACnB,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,MAAM;QACN,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;YACxB,MAAM,MAAM,GAAqC;gBAC/C,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,YAAY,EAAE,KAAK,CAAC,oBAAoB;gBACxC,UAAU,EAAE,KAAK,CAAC,kBAAkB;gBACpC,qBAAqB,EAAE,KAAK,CAAC,qBAAqB;gBAClD,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;aACpB,CAAC;YACF,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;gBACpC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;YAC1C,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC;QACF,QAAQ;KACT,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Browser } from "playwright";
|
|
2
|
+
import type { CompositionMetadata, Props, ResolvedAudioClip, ResolvedTranscriptClip } from "../contracts.js";
|
|
3
|
+
export interface CompositionSession {
|
|
4
|
+
readonly metadata: CompositionMetadata;
|
|
5
|
+
readonly props: Props;
|
|
6
|
+
readonly audioClips: ResolvedAudioClip[];
|
|
7
|
+
readonly transcriptClips: ResolvedTranscriptClip[];
|
|
8
|
+
seek(frame: number): Promise<void>;
|
|
9
|
+
capture(): Promise<Buffer>;
|
|
10
|
+
assertHealthy(): void;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare const BROWSER_ARGS: string[];
|
|
14
|
+
type BrowserContextOptions = Parameters<Browser["newContext"]>[0];
|
|
15
|
+
export declare const BROWSER_CONTEXT_OPTIONS: BrowserContextOptions;
|
|
16
|
+
export declare function openCompositionSession(options: {
|
|
17
|
+
serverUrl: string;
|
|
18
|
+
entry: string;
|
|
19
|
+
props?: unknown;
|
|
20
|
+
}): Promise<CompositionSession>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/renderer/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,OAAO,EAAmD,MAAM,YAAY,CAAC;AACrG,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAG7G,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,iBAAiB,EAAE,CAAC;IACzC,QAAQ,CAAC,eAAe,EAAE,sBAAsB,EAAE,CAAC;IACnD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3B,aAAa,IAAI,IAAI,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAkBD,eAAO,MAAM,YAAY,EAAE,MAAM,EAShC,CAAC;AAEF,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAElE,eAAO,MAAM,uBAAuB,EAAE,qBAOrC,CAAC;AAiCF,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA4J9B"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { FrameyardError, isFrameyardError } from "../errors.js";
|
|
3
|
+
export const BROWSER_ARGS = [
|
|
4
|
+
"--force-color-profile=srgb",
|
|
5
|
+
"--disable-lcd-text",
|
|
6
|
+
"--hide-scrollbars",
|
|
7
|
+
"--mute-audio",
|
|
8
|
+
"--disable-background-timer-throttling",
|
|
9
|
+
"--disable-backgrounding-occluded-windows",
|
|
10
|
+
"--disable-renderer-backgrounding",
|
|
11
|
+
"--disable-dev-shm-usage",
|
|
12
|
+
];
|
|
13
|
+
export const BROWSER_CONTEXT_OPTIONS = {
|
|
14
|
+
viewport: { width: 1280, height: 720 },
|
|
15
|
+
deviceScaleFactor: 1,
|
|
16
|
+
colorScheme: "light",
|
|
17
|
+
reducedMotion: "no-preference",
|
|
18
|
+
locale: "en-US",
|
|
19
|
+
timezoneId: "UTC",
|
|
20
|
+
};
|
|
21
|
+
function browserError(error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
return message
|
|
24
|
+
.replace(/^page\.evaluate: /, "")
|
|
25
|
+
.replace(/^Error: /, "")
|
|
26
|
+
.split("\n")
|
|
27
|
+
.filter((line) => !line.trimStart().startsWith("at "))
|
|
28
|
+
.join("\n")
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
function browserCommandError(error) {
|
|
32
|
+
if (isFrameyardError(error)) {
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
const message = browserError(error);
|
|
36
|
+
const match = /^\[([A-Z_]+)\]\s*([\s\S]*)$/.exec(message);
|
|
37
|
+
if (match) {
|
|
38
|
+
return new FrameyardError(match[1], match[2].trim(), { cause: error });
|
|
39
|
+
}
|
|
40
|
+
return new FrameyardError("COMPOSITION_ERROR", message, { cause: error });
|
|
41
|
+
}
|
|
42
|
+
async function closeQuietly(action) {
|
|
43
|
+
try {
|
|
44
|
+
await action();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Cleanup must not hide the original failure.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function openCompositionSession(options) {
|
|
51
|
+
let browser;
|
|
52
|
+
try {
|
|
53
|
+
browser = await chromium.launch({
|
|
54
|
+
headless: true,
|
|
55
|
+
args: BROWSER_ARGS,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new FrameyardError("BROWSER_FAILED", `Chromium could not start. Install the pinned browser with "npx playwright install chromium".\n${browserError(error)}`, { cause: error });
|
|
60
|
+
}
|
|
61
|
+
let context;
|
|
62
|
+
let page;
|
|
63
|
+
let cdp;
|
|
64
|
+
try {
|
|
65
|
+
context = await browser.newContext(BROWSER_CONTEXT_OPTIONS);
|
|
66
|
+
page = await context.newPage();
|
|
67
|
+
page.setDefaultTimeout(60_000);
|
|
68
|
+
const pageErrors = [];
|
|
69
|
+
page.on("pageerror", (error) => {
|
|
70
|
+
pageErrors.push(error.message);
|
|
71
|
+
});
|
|
72
|
+
page.on("crash", () => {
|
|
73
|
+
pageErrors.push("The browser page crashed.");
|
|
74
|
+
});
|
|
75
|
+
await page.goto(new URL("/__frameyard/?render=1", options.serverUrl).href, {
|
|
76
|
+
waitUntil: "domcontentloaded",
|
|
77
|
+
timeout: 60_000,
|
|
78
|
+
});
|
|
79
|
+
await page.waitForFunction(() => Boolean(window.__frameyardPreview), undefined, { timeout: 30_000 });
|
|
80
|
+
let loaded;
|
|
81
|
+
try {
|
|
82
|
+
loaded = await page.evaluate(async ({ entry, props }) => {
|
|
83
|
+
const api = window.__frameyardPreview;
|
|
84
|
+
return api.load(entry, props);
|
|
85
|
+
}, { entry: options.entry, props: options.props });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw browserCommandError(error);
|
|
89
|
+
}
|
|
90
|
+
await page.setViewportSize({ width: loaded.metadata.width, height: loaded.metadata.height });
|
|
91
|
+
try {
|
|
92
|
+
await page.waitForLoadState("networkidle", { timeout: 30_000 });
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new FrameyardError("RENDER_FAILED", "Timed out waiting for composition assets and network requests to finish loading.", { cause: error });
|
|
96
|
+
}
|
|
97
|
+
await page.evaluate(() => new Promise((resolve) => {
|
|
98
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
99
|
+
}));
|
|
100
|
+
// Loading is complete. Frame production must not depend on new network data.
|
|
101
|
+
await page.route("**/*", (route) => route.abort("blockedbyclient"));
|
|
102
|
+
cdp = await context.newCDPSession(page);
|
|
103
|
+
const activePage = page;
|
|
104
|
+
const activeCdp = cdp;
|
|
105
|
+
const assertHealthy = () => {
|
|
106
|
+
if (pageErrors.length > 0) {
|
|
107
|
+
throw new FrameyardError("COMPOSITION_ERROR", `Unhandled browser error: ${pageErrors.join("\n")}`);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
assertHealthy();
|
|
111
|
+
const session = {
|
|
112
|
+
metadata: loaded.metadata,
|
|
113
|
+
props: loaded.props,
|
|
114
|
+
audioClips: loaded.audioClips,
|
|
115
|
+
transcriptClips: loaded.transcriptClips,
|
|
116
|
+
async seek(frame) {
|
|
117
|
+
assertHealthy();
|
|
118
|
+
try {
|
|
119
|
+
await activePage.evaluate((targetFrame) => {
|
|
120
|
+
const api = window.__frameyardPreview;
|
|
121
|
+
api.seek(targetFrame);
|
|
122
|
+
}, frame);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
throw browserCommandError(error);
|
|
126
|
+
}
|
|
127
|
+
assertHealthy();
|
|
128
|
+
},
|
|
129
|
+
async capture() {
|
|
130
|
+
assertHealthy();
|
|
131
|
+
let result;
|
|
132
|
+
try {
|
|
133
|
+
result = (await activeCdp.send("Page.captureScreenshot", {
|
|
134
|
+
format: "png",
|
|
135
|
+
fromSurface: true,
|
|
136
|
+
captureBeyondViewport: false,
|
|
137
|
+
// Lossless PNG with fast compression. The default zlib level is the
|
|
138
|
+
// dominant cost of a 1080p frame; decoded pixels are identical.
|
|
139
|
+
optimizeForSpeed: true,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (isFrameyardError(error)) {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
throw new FrameyardError("RENDER_FAILED", `Could not capture a browser frame: ${browserError(error)}`, {
|
|
147
|
+
cause: error,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
assertHealthy();
|
|
151
|
+
return Buffer.from(result.data, "base64");
|
|
152
|
+
},
|
|
153
|
+
assertHealthy,
|
|
154
|
+
async close() {
|
|
155
|
+
await closeQuietly(async () => {
|
|
156
|
+
await activePage.evaluate(async () => {
|
|
157
|
+
const api = window.__frameyardPreview;
|
|
158
|
+
await api?.dispose?.();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
await closeQuietly(async () => cdp?.detach());
|
|
162
|
+
await closeQuietly(async () => context?.close());
|
|
163
|
+
await closeQuietly(async () => browser.close());
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
return session;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
await closeQuietly(async () => cdp?.detach());
|
|
170
|
+
await closeQuietly(async () => context?.close());
|
|
171
|
+
await closeQuietly(async () => browser.close());
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/renderer/browser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Fails early when the installed FFmpeg cannot produce the required output. */
|
|
2
|
+
export declare function assertFfmpegCapabilities(signal?: AbortSignal): Promise<void>;
|
|
3
|
+
interface ProcessOptions {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
}
|
|
6
|
+
export interface ProbeStream {
|
|
7
|
+
codec_type?: string;
|
|
8
|
+
codec_name?: string;
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
channels?: number;
|
|
12
|
+
sample_rate?: string;
|
|
13
|
+
duration?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ProbeResult {
|
|
16
|
+
streams: ProbeStream[];
|
|
17
|
+
format?: {
|
|
18
|
+
duration?: string;
|
|
19
|
+
format_name?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare function probeMedia(filePath: string, options?: ProcessOptions): Promise<ProbeResult>;
|
|
23
|
+
export interface MixClip {
|
|
24
|
+
path: string;
|
|
25
|
+
startFrame: number;
|
|
26
|
+
trimStart: number;
|
|
27
|
+
trimDuration?: number;
|
|
28
|
+
volume: number;
|
|
29
|
+
}
|
|
30
|
+
/** Normalizes any decodable source to the 16 kHz mono PCM WAV that whisper.cpp expects. */
|
|
31
|
+
export declare function normalizeAudioForTranscription(options: {
|
|
32
|
+
inputPath: string;
|
|
33
|
+
outputPath: string;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
export declare function mixAudioTimeline(options: {
|
|
37
|
+
clips: MixClip[];
|
|
38
|
+
fps: number;
|
|
39
|
+
durationSeconds: number;
|
|
40
|
+
outputPath: string;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
export interface VideoEncoder {
|
|
44
|
+
writeFrame(png: Buffer): Promise<void>;
|
|
45
|
+
finish(): Promise<void>;
|
|
46
|
+
abort(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export declare function startVideoEncoder(options: {
|
|
49
|
+
outputPath: string;
|
|
50
|
+
durationSeconds: number;
|
|
51
|
+
/** Exact number of frames that must be written. */
|
|
52
|
+
frames: number;
|
|
53
|
+
fps: number;
|
|
54
|
+
audioPath?: string;
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}): Promise<VideoEncoder>;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=ffmpeg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/renderer/ffmpeg.ts"],"names":[],"mappings":"AAwBA,gFAAgF;AAChF,wBAAsB,wBAAwB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAED,UAAU,cAAc;IACtB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAwED,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACtD;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CAWrG;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,2FAA2F;AAC3F,wBAAsB,8BAA8B,CAAC,OAAO,EAAE;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBhB;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC9C,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2DhB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,YAAY,CAAC,CAyKxB"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { FrameyardError } from "../errors.js";
|
|
3
|
+
const STDERR_LIMIT = 16 * 1024;
|
|
4
|
+
function captureTail(child) {
|
|
5
|
+
let tail = "";
|
|
6
|
+
child.stderr.on("data", (chunk) => {
|
|
7
|
+
tail += chunk.toString();
|
|
8
|
+
if (tail.length > STDERR_LIMIT) {
|
|
9
|
+
tail = tail.slice(-STDERR_LIMIT);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
return () => tail.trim();
|
|
13
|
+
}
|
|
14
|
+
function abortError() {
|
|
15
|
+
return new FrameyardError("RENDER_FAILED", "Operation cancelled.");
|
|
16
|
+
}
|
|
17
|
+
function formatNumber(value) {
|
|
18
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
|
|
19
|
+
}
|
|
20
|
+
/** Fails early when the installed FFmpeg cannot produce the required output. */
|
|
21
|
+
export async function assertFfmpegCapabilities(signal) {
|
|
22
|
+
const { stdout, stderr } = await runCaptured("ffmpeg", ["-hide_banner", "-encoders"], { signal });
|
|
23
|
+
const encoders = `${stdout}\n${stderr}`;
|
|
24
|
+
const missing = ["libx264", "aac"].filter((name) => !new RegExp(`(^|\\s)${name}(\\s|$)`, "m").test(encoders));
|
|
25
|
+
if (missing.length > 0) {
|
|
26
|
+
throw new FrameyardError("FFMPEG_FAILED", `FFmpeg is missing required encoder${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}. Install a build with H.264 and AAC support.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function runCaptured(command, args, options = {}) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
if (options.signal?.aborted) {
|
|
32
|
+
reject(abortError());
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
36
|
+
let stdout = "";
|
|
37
|
+
let stderr = "";
|
|
38
|
+
child.stdout.on("data", (chunk) => {
|
|
39
|
+
stdout += chunk.toString();
|
|
40
|
+
});
|
|
41
|
+
child.stderr.on("data", (chunk) => {
|
|
42
|
+
stderr += chunk.toString();
|
|
43
|
+
});
|
|
44
|
+
const onAbort = () => child.kill("SIGKILL");
|
|
45
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
46
|
+
child.once("error", (error) => {
|
|
47
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
48
|
+
if (options.signal?.aborted) {
|
|
49
|
+
reject(abortError());
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (error.code === "ENOENT") {
|
|
53
|
+
reject(new FrameyardError(command === "ffmpeg" ? "FFMPEG_FAILED" : "FFPROBE_FAILED", `${command === "ffmpeg" ? "FFmpeg" : "FFprobe"} was not found on PATH. Install FFmpeg and ensure both executables are available.`, { cause: error }));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
reject(new FrameyardError(command === "ffmpeg" ? "FFMPEG_FAILED" : "FFPROBE_FAILED", `${command} could not start: ${error.message}`, { cause: error }));
|
|
57
|
+
});
|
|
58
|
+
child.once("close", (code) => {
|
|
59
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
60
|
+
if (options.signal?.aborted) {
|
|
61
|
+
reject(abortError());
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (code !== 0) {
|
|
65
|
+
reject(new FrameyardError(command === "ffmpeg" ? "FFMPEG_FAILED" : "FFPROBE_FAILED", `${command} exited with code ${code}${stderr ? `:\n${stderr.trim()}` : "."}`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
resolve({ stdout, stderr });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export async function probeMedia(filePath, options = {}) {
|
|
73
|
+
const { stdout } = await runCaptured("ffprobe", ["-v", "error", "-show_format", "-show_streams", "-of", "json", filePath], options);
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(stdout);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new FrameyardError("FFPROBE_FAILED", `Could not parse ffprobe output for ${filePath}.`, { cause: error });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Normalizes any decodable source to the 16 kHz mono PCM WAV that whisper.cpp expects. */
|
|
82
|
+
export async function normalizeAudioForTranscription(options) {
|
|
83
|
+
await runCaptured("ffmpeg", [
|
|
84
|
+
"-hide_banner",
|
|
85
|
+
"-loglevel",
|
|
86
|
+
"error",
|
|
87
|
+
"-nostdin",
|
|
88
|
+
"-y",
|
|
89
|
+
"-i",
|
|
90
|
+
options.inputPath,
|
|
91
|
+
"-map",
|
|
92
|
+
"0:a:0",
|
|
93
|
+
"-vn",
|
|
94
|
+
"-ac",
|
|
95
|
+
"1",
|
|
96
|
+
"-ar",
|
|
97
|
+
"16000",
|
|
98
|
+
"-c:a",
|
|
99
|
+
"pcm_s16le",
|
|
100
|
+
options.outputPath,
|
|
101
|
+
], { signal: options.signal });
|
|
102
|
+
}
|
|
103
|
+
export async function mixAudioTimeline(options) {
|
|
104
|
+
if (options.clips.length === 0) {
|
|
105
|
+
throw new FrameyardError("AUDIO_INVALID", "Cannot mix an empty audio timeline.");
|
|
106
|
+
}
|
|
107
|
+
const args = ["-hide_banner", "-loglevel", "error", "-nostdin", "-y"];
|
|
108
|
+
for (const clip of options.clips) {
|
|
109
|
+
args.push("-i", clip.path);
|
|
110
|
+
}
|
|
111
|
+
const filters = [];
|
|
112
|
+
const labels = [];
|
|
113
|
+
options.clips.forEach((clip, index) => {
|
|
114
|
+
const trim = [`start=${formatNumber(clip.trimStart)}`];
|
|
115
|
+
if (clip.trimDuration !== undefined) {
|
|
116
|
+
trim.push(`duration=${formatNumber(clip.trimDuration)}`);
|
|
117
|
+
}
|
|
118
|
+
const delayMs = Math.round((clip.startFrame * 1000) / options.fps);
|
|
119
|
+
const chain = [
|
|
120
|
+
`atrim=${trim.join(":")}`,
|
|
121
|
+
"asetpts=PTS-STARTPTS",
|
|
122
|
+
`volume=${formatNumber(clip.volume)}`,
|
|
123
|
+
"aresample=48000",
|
|
124
|
+
"aformat=sample_fmts=fltp:channel_layouts=stereo",
|
|
125
|
+
];
|
|
126
|
+
if (delayMs > 0) {
|
|
127
|
+
chain.push(`adelay=${delayMs}|${delayMs}`);
|
|
128
|
+
}
|
|
129
|
+
const label = options.clips.length === 1 ? "audio" : `clip${index}`;
|
|
130
|
+
filters.push(`[${index}:a]${chain.join(",")}[${label}]`);
|
|
131
|
+
if (options.clips.length > 1) {
|
|
132
|
+
labels.push(`[${label}]`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
if (options.clips.length > 1) {
|
|
136
|
+
filters.push(`${labels.join("")}amix=inputs=${options.clips.length}:duration=longest:normalize=0[audio]`);
|
|
137
|
+
}
|
|
138
|
+
args.push("-filter_complex", filters.join(";"), "-map", "[audio]", "-t", formatNumber(options.durationSeconds), "-ar", "48000", "-ac", "2", "-c:a", "pcm_s16le", options.outputPath);
|
|
139
|
+
await runCaptured("ffmpeg", args, { signal: options.signal });
|
|
140
|
+
}
|
|
141
|
+
export async function startVideoEncoder(options) {
|
|
142
|
+
if (options.signal?.aborted) {
|
|
143
|
+
throw abortError();
|
|
144
|
+
}
|
|
145
|
+
const args = [
|
|
146
|
+
"-hide_banner",
|
|
147
|
+
"-loglevel",
|
|
148
|
+
"error",
|
|
149
|
+
"-nostdin",
|
|
150
|
+
"-y",
|
|
151
|
+
"-f",
|
|
152
|
+
"image2pipe",
|
|
153
|
+
"-framerate",
|
|
154
|
+
formatNumber(options.fps),
|
|
155
|
+
"-i",
|
|
156
|
+
"pipe:0",
|
|
157
|
+
];
|
|
158
|
+
if (options.audioPath) {
|
|
159
|
+
args.push("-i", options.audioPath);
|
|
160
|
+
}
|
|
161
|
+
args.push("-map", "0:v:0");
|
|
162
|
+
if (options.audioPath) {
|
|
163
|
+
args.push("-map", "1:a:0");
|
|
164
|
+
}
|
|
165
|
+
args.push("-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-r", formatNumber(options.fps), "-frames:v", String(options.frames), "-t", formatNumber(options.durationSeconds));
|
|
166
|
+
if (options.audioPath) {
|
|
167
|
+
args.push("-c:a", "aac", "-b:a", "192k", "-ar", "48000", "-ac", "2");
|
|
168
|
+
}
|
|
169
|
+
args.push("-movflags", "+faststart", options.outputPath);
|
|
170
|
+
let child;
|
|
171
|
+
try {
|
|
172
|
+
child = spawn("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
throw new FrameyardError("FFMPEG_FAILED", "Could not start FFmpeg.", { cause: error });
|
|
176
|
+
}
|
|
177
|
+
const stderrTail = captureTail(child);
|
|
178
|
+
await new Promise((resolve, reject) => {
|
|
179
|
+
child.once("spawn", resolve);
|
|
180
|
+
child.once("error", (error) => {
|
|
181
|
+
if (error.code === "ENOENT") {
|
|
182
|
+
reject(new FrameyardError("FFMPEG_FAILED", "FFmpeg was not found on PATH. Install FFmpeg and ensure it is available.", { cause: error }));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
reject(new FrameyardError("FFMPEG_FAILED", `FFmpeg could not start: ${error.message}`, { cause: error }));
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
const closed = new Promise((resolve) => {
|
|
190
|
+
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
191
|
+
});
|
|
192
|
+
let finished = false;
|
|
193
|
+
let framesWritten = 0;
|
|
194
|
+
const onAbort = () => {
|
|
195
|
+
child.stdin.destroy();
|
|
196
|
+
child.kill("SIGKILL");
|
|
197
|
+
};
|
|
198
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
199
|
+
async function abort() {
|
|
200
|
+
if (finished) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
finished = true;
|
|
204
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
205
|
+
child.stdin.destroy();
|
|
206
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
207
|
+
child.kill("SIGKILL");
|
|
208
|
+
}
|
|
209
|
+
await closed.catch(() => undefined);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
async writeFrame(png) {
|
|
213
|
+
if (finished) {
|
|
214
|
+
throw new FrameyardError("FFMPEG_FAILED", "Cannot write a frame after the encoder finished.");
|
|
215
|
+
}
|
|
216
|
+
if (options.signal?.aborted) {
|
|
217
|
+
throw abortError();
|
|
218
|
+
}
|
|
219
|
+
await new Promise((resolve, reject) => {
|
|
220
|
+
const onError = (error) => {
|
|
221
|
+
child.stdin.off("error", onError);
|
|
222
|
+
if (options.signal?.aborted) {
|
|
223
|
+
reject(abortError());
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
reject(new FrameyardError("FFMPEG_FAILED", `FFmpeg stopped while accepting frames: ${error.message}`, {
|
|
227
|
+
cause: error,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
child.stdin.once("error", onError);
|
|
232
|
+
child.stdin.write(png, (error) => {
|
|
233
|
+
child.stdin.off("error", onError);
|
|
234
|
+
if (error) {
|
|
235
|
+
reject(new FrameyardError("FFMPEG_FAILED", `Could not write a frame to FFmpeg: ${error.message}`, {
|
|
236
|
+
cause: error,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
framesWritten += 1;
|
|
241
|
+
resolve();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
async finish() {
|
|
247
|
+
if (finished) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
finished = true;
|
|
251
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
252
|
+
child.stdin.end();
|
|
253
|
+
const { code, signal } = await closed;
|
|
254
|
+
if (options.signal?.aborted) {
|
|
255
|
+
throw abortError();
|
|
256
|
+
}
|
|
257
|
+
if (code !== 0) {
|
|
258
|
+
const detail = stderrTail();
|
|
259
|
+
throw new FrameyardError("FFMPEG_FAILED", `FFmpeg encoding failed${signal ? ` (${signal})` : code === null ? "" : ` (exit ${code})`}${detail ? `:\n${detail}` : "."}`);
|
|
260
|
+
}
|
|
261
|
+
if (framesWritten !== options.frames) {
|
|
262
|
+
throw new FrameyardError("FFMPEG_FAILED", `FFmpeg encoded ${framesWritten} frames but ${options.frames} were required.`);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
abort,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=ffmpeg.js.map
|