@softure-ai/marketing-kit 0.1.5 → 0.1.6
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/README.md +28 -6
- package/dist/cli/main.js +5 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/options.d.ts +11 -1
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/cli/options.js +12 -3
- package/dist/cli/options.js.map +1 -1
- package/dist/cli/voice.d.ts.map +1 -1
- package/dist/cli/voice.js +2 -1
- package/dist/cli/voice.js.map +1 -1
- package/dist/compose/compose.d.ts +3 -3
- package/dist/compose/compose.d.ts.map +1 -1
- package/dist/compose/compose.js +7 -7
- package/dist/compose/compose.js.map +1 -1
- package/dist/compose/timeline.d.ts +19 -0
- package/dist/compose/timeline.d.ts.map +1 -1
- package/dist/compose/timeline.js +42 -0
- package/dist/compose/timeline.js.map +1 -1
- package/dist/config/config.d.ts +2 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +1 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/day.d.ts +5 -0
- package/dist/config/day.d.ts.map +1 -0
- package/dist/config/day.js +10 -0
- package/dist/config/day.js.map +1 -0
- package/dist/config/schema.d.ts +6 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +12 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/film.d.ts +3 -1
- package/dist/film.d.ts.map +1 -1
- package/dist/film.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/record/record.d.ts +1 -1
- package/dist/record/record.d.ts.map +1 -1
- package/dist/record/record.js +2 -2
- package/dist/record/record.js.map +1 -1
- package/dist/render/render.js +10 -10
- package/dist/render/render.js.map +1 -1
- package/dist/voice/cache.d.ts +11 -2
- package/dist/voice/cache.d.ts.map +1 -1
- package/dist/voice/cache.js +30 -5
- package/dist/voice/cache.js.map +1 -1
- package/dist/voice/produce.d.ts +2 -0
- package/dist/voice/produce.d.ts.map +1 -1
- package/dist/voice/produce.js +14 -4
- package/dist/voice/produce.js.map +1 -1
- package/package.json +1 -1
- package/schema/marketing.schema.json +15 -0
- package/src/cli/main.ts +5 -4
- package/src/cli/options.ts +14 -3
- package/src/cli/voice.ts +2 -1
- package/src/compose/compose.ts +9 -9
- package/src/compose/timeline.ts +55 -0
- package/src/config/config.ts +3 -0
- package/src/config/day.ts +9 -0
- package/src/config/schema.ts +15 -1
- package/src/film.ts +3 -2
- package/src/index.ts +7 -1
- package/src/record/record.ts +3 -3
- package/src/render/render.ts +10 -10
- package/src/voice/cache.ts +33 -5
- package/src/voice/produce.ts +17 -4
package/dist/render/render.js
CHANGED
|
@@ -3,7 +3,7 @@ import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, extname, join } from "node:path";
|
|
5
5
|
import { composeFilm, filmTimes } from "../compose/compose.js";
|
|
6
|
-
import { getGeometry, getLayoutName,
|
|
6
|
+
import { crossfadeArgs, getGeometry, getLayoutName, getTransitionSeconds, transitionFrames } from "../compose/timeline.js";
|
|
7
7
|
import { runHyperframes } from "./hyperframes.js";
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
function run(command, args) {
|
|
@@ -47,18 +47,18 @@ export function buildComposition(input) {
|
|
|
47
47
|
mkdirSync(assetsDir, { recursive: true });
|
|
48
48
|
const ff = (args) => run("ffmpeg", ["-v", "error", "-y", ...args]);
|
|
49
49
|
ff(["-framerate", String(log.fps), "-i", join(dir, "frames", "f%05d.jpg"), "-c:v", "libx264", "-preset", "slow", "-crf", "14", "-pix_fmt", "yuv420p", join(assetsDir, "screen.mp4")]);
|
|
50
|
-
//
|
|
50
|
+
// The transition from the opening frame into the scene: stills joined by dissolves (none for a cut).
|
|
51
51
|
const still = log.stills[film.hook.still];
|
|
52
52
|
const firstBeat = log.beats[0];
|
|
53
53
|
if (still === undefined || firstBeat === undefined)
|
|
54
54
|
throw new Error(`The recording of ${film.id} has no opening frame or no sentences: record it again.`);
|
|
55
|
-
const
|
|
56
|
-
rmSync(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
const transitionPath = join(assetsDir, "transition.mp4");
|
|
56
|
+
rmSync(transitionPath, { force: true });
|
|
57
|
+
const frames = transitionFrames(film.hook.transition, firstBeat.f0, still);
|
|
58
|
+
if (frames.length > 0) {
|
|
59
|
+
const inputs = frames.map((index) => getFramePath(dir, index));
|
|
60
|
+
ff(crossfadeArgs({ inputs, fps: log.fps, seconds: getTransitionSeconds(film.hook.transition), output: transitionPath }));
|
|
61
|
+
}
|
|
62
62
|
cpSync(getFramePath(dir, still), join(assetsDir, "hook.jpg"));
|
|
63
63
|
cpSync(getFramePath(dir, log.frames - 1), join(assetsDir, "last.jpg"));
|
|
64
64
|
ff(["-i", input.voiceoverAudio, "-filter:a", `atempo=${film.voice.tempo}`, "-c:a", "libmp3lame", "-q:a", "2", join(assetsDir, "voiceover.mp3")]);
|
|
@@ -87,7 +87,7 @@ export function buildComposition(input) {
|
|
|
87
87
|
fonts,
|
|
88
88
|
assets: {
|
|
89
89
|
screen: "assets/screen.mp4",
|
|
90
|
-
|
|
90
|
+
transition: frames.length > 0 ? "assets/transition.mp4" : null,
|
|
91
91
|
hookStill: "assets/hook.jpg",
|
|
92
92
|
lastFrame: "assets/last.jpg",
|
|
93
93
|
voiceover: "assets/voiceover.mp3",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAoB,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"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"}
|
package/dist/voice/cache.d.ts
CHANGED
|
@@ -4,14 +4,23 @@ export interface VoiceoverPaths {
|
|
|
4
4
|
key: string;
|
|
5
5
|
audio: string;
|
|
6
6
|
words: string;
|
|
7
|
+
/** `video`: in the video's own folder; `flat`: `<key>.*` in the cache root, the layout before 0.1.6, still read. */
|
|
8
|
+
layout: "video" | "flat";
|
|
7
9
|
}
|
|
8
10
|
/** A voiceover on disk: the audio file's path and the timed words. */
|
|
9
11
|
export interface Voiceover {
|
|
10
12
|
audio: string;
|
|
11
13
|
words: TimedWord[];
|
|
12
14
|
}
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Where a recording of this input lives in the cache. With a video id: `<cacheDir>/<video-id>/<key>.mp3|json`, so
|
|
17
|
+
* a reader sees which film a paid file belongs to; a recording saved flat by an earlier version
|
|
18
|
+
* (`<cacheDir>/<key>.*`) is still found when the video's folder has none, so no paid file is recorded twice.
|
|
19
|
+
* Without a video id: the flat layout. The key (text, voice, model, language) is the same in both.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getVoiceoverPaths(cacheDir: string, input: TtsInput, videoId?: string): VoiceoverPaths;
|
|
22
|
+
/** Recordings in the video's folder other than `key`: what an earlier script of the film paid for. Sorted file names. */
|
|
23
|
+
export declare function findStaleVoiceovers(cacheDir: string, videoId: string, key: string): string[];
|
|
15
24
|
/** The cached voiceover, null when either file is missing, or an error naming a broken words file. */
|
|
16
25
|
export declare function readCachedVoiceover(paths: VoiceoverPaths): TtsResult<Voiceover | null>;
|
|
17
26
|
/** Writes a recording in FIRE_TRACKER's format, so files recorded there stay byte-identical. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/voice/cache.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1F,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE9D,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/voice/cache.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1F,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE9D,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,oHAAoH;IACpH,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAUD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,cAAc,CAMrG;AAED,yHAAyH;AACzH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAM5F;AAaD,sGAAsG;AACtG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,CAUtF;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,YAAY,GAAG,SAAS,CAM1G"}
|
package/dist/voice/cache.js
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { err, ok } from "./provider.js";
|
|
4
4
|
import { voiceoverKey } from "./voiceover.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
function getPathsIn(dir, key, layout) {
|
|
6
|
+
return { key, audio: join(dir, `${key}.mp3`), words: join(dir, `${key}.json`), layout };
|
|
7
|
+
}
|
|
8
|
+
function hasRecording(paths) {
|
|
9
|
+
return existsSync(paths.audio) && existsSync(paths.words);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Where a recording of this input lives in the cache. With a video id: `<cacheDir>/<video-id>/<key>.mp3|json`, so
|
|
13
|
+
* a reader sees which film a paid file belongs to; a recording saved flat by an earlier version
|
|
14
|
+
* (`<cacheDir>/<key>.*`) is still found when the video's folder has none, so no paid file is recorded twice.
|
|
15
|
+
* Without a video id: the flat layout. The key (text, voice, model, language) is the same in both.
|
|
16
|
+
*/
|
|
17
|
+
export function getVoiceoverPaths(cacheDir, input, videoId) {
|
|
7
18
|
const key = voiceoverKey(input.text, input.voiceId, input.model, input.language);
|
|
8
|
-
|
|
19
|
+
const flat = getPathsIn(cacheDir, key, "flat");
|
|
20
|
+
if (videoId === undefined)
|
|
21
|
+
return flat;
|
|
22
|
+
const own = getPathsIn(join(cacheDir, videoId), key, "video");
|
|
23
|
+
return hasRecording(own) || !hasRecording(flat) ? own : flat;
|
|
24
|
+
}
|
|
25
|
+
/** Recordings in the video's folder other than `key`: what an earlier script of the film paid for. Sorted file names. */
|
|
26
|
+
export function findStaleVoiceovers(cacheDir, videoId, key) {
|
|
27
|
+
const dir = join(cacheDir, videoId);
|
|
28
|
+
if (!existsSync(dir))
|
|
29
|
+
return [];
|
|
30
|
+
return readdirSync(dir)
|
|
31
|
+
.filter((name) => name.endsWith(".mp3") && name !== `${key}.mp3`)
|
|
32
|
+
.sort();
|
|
9
33
|
}
|
|
10
34
|
function isTimedWords(value) {
|
|
11
35
|
return (Array.isArray(value) &&
|
|
@@ -34,6 +58,7 @@ export function readCachedVoiceover(paths) {
|
|
|
34
58
|
/** Writes a recording in FIRE_TRACKER's format, so files recorded there stay byte-identical. */
|
|
35
59
|
export function writeVoiceover(cacheDir, paths, recording) {
|
|
36
60
|
mkdirSync(cacheDir, { recursive: true });
|
|
61
|
+
mkdirSync(dirname(paths.audio), { recursive: true });
|
|
37
62
|
writeFileSync(paths.audio, recording.audio);
|
|
38
63
|
writeFileSync(paths.words, `${JSON.stringify(recording.words, null, 1)}\n`);
|
|
39
64
|
return { audio: paths.audio, words: recording.words };
|
package/dist/voice/cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/voice/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/voice/cache.ts"],"names":[],"mappings":"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"}
|
package/dist/voice/produce.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { type TtsEstimate, type TtsInput, type TtsProvider, type TtsResult } fro
|
|
|
3
3
|
export interface ProduceVoiceoverOptions {
|
|
4
4
|
cacheDir: string;
|
|
5
5
|
input: TtsInput;
|
|
6
|
+
/** The film the voiceover belongs to: its recordings live in `<cacheDir>/<videoId>/`. Without it, the flat layout. */
|
|
7
|
+
videoId?: string;
|
|
6
8
|
provider: TtsProvider;
|
|
7
9
|
/** Only `true` lets the provider spend money. */
|
|
8
10
|
isCommit: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"produce.d.ts","sourceRoot":"","sources":["../../src/voice/produce.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"produce.d.ts","sourceRoot":"","sources":["../../src/voice/produce.ts"],"names":[],"mappings":"AAEA,OAAO,EAA+E,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AACzH,OAAO,EAAW,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAE3G,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,QAAQ,CAAC;IAChB,sHAAsH;IACtH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,WAAW,CAAC;IACtB,iDAAiD;IACjD,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC;AAEnF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,GAAG,MAAM,CAErF;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,CA6B7G"}
|
package/dist/voice/produce.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { findStaleVoiceovers, getVoiceoverPaths, readCachedVoiceover, writeVoiceover } from "./cache.js";
|
|
2
3
|
import { err, ok } from "./provider.js";
|
|
3
4
|
export function describeEstimate(provider, estimate) {
|
|
4
5
|
return `voiceover estimate (${provider.id}): ${estimate.characters} characters, at most ${estimate.maxCost} ${estimate.unit}.`;
|
|
@@ -8,13 +9,22 @@ export function describeEstimate(provider, estimate) {
|
|
|
8
9
|
* run and before every paid call; without `isCommit` the provider is never called.
|
|
9
10
|
*/
|
|
10
11
|
export async function produceVoiceover(options) {
|
|
11
|
-
const { cacheDir, input, provider, isCommit, log } = options;
|
|
12
|
-
const paths = getVoiceoverPaths(cacheDir, input);
|
|
12
|
+
const { cacheDir, input, videoId, provider, isCommit, log } = options;
|
|
13
|
+
const paths = getVoiceoverPaths(cacheDir, input, videoId);
|
|
13
14
|
const cached = readCachedVoiceover(paths);
|
|
14
15
|
if (!cached.ok)
|
|
15
16
|
return cached;
|
|
17
|
+
if (videoId !== undefined) {
|
|
18
|
+
const stale = findStaleVoiceovers(cacheDir, videoId, paths.key);
|
|
19
|
+
if (stale.length > 0) {
|
|
20
|
+
log(`voiceover: ${join(cacheDir, videoId)} also holds ${stale.join(", ")}, which the current script does not use; delete them once no film needs them.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
16
23
|
if (cached.value !== null) {
|
|
17
24
|
log(`voiceover: from the cache ${paths.key}, nothing spent.`);
|
|
25
|
+
if (videoId !== undefined && paths.layout === "flat") {
|
|
26
|
+
log(`voiceover: ${paths.key}.mp3 and .json sit in the cache root (the layout before 0.1.6); move them into ${join(cacheDir, videoId)}/ to keep them with their film.`);
|
|
27
|
+
}
|
|
18
28
|
return ok({ kind: "cached", key: paths.key, voiceover: cached.value });
|
|
19
29
|
}
|
|
20
30
|
const estimate = provider.estimate(input);
|
|
@@ -27,7 +37,7 @@ export async function produceVoiceover(options) {
|
|
|
27
37
|
if (!recording.ok)
|
|
28
38
|
return err(recording.error);
|
|
29
39
|
const voiceover = writeVoiceover(cacheDir, paths, recording.value);
|
|
30
|
-
log(`voiceover: recorded ${estimate.characters} characters, saved ${paths.key}.{mp3,json} in ${
|
|
40
|
+
log(`voiceover: recorded ${estimate.characters} characters, saved ${paths.key}.{mp3,json} in ${dirname(paths.audio)} (commit them).`);
|
|
31
41
|
return ok({ kind: "recorded", key: paths.key, voiceover, estimate });
|
|
32
42
|
}
|
|
33
43
|
//# sourceMappingURL=produce.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"produce.js","sourceRoot":"","sources":["../../src/voice/produce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,cAAc,EAAkB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"produce.js","sourceRoot":"","sources":["../../src/voice/produce.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/marketing-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "SOFTURE AI marketing kit: a CLI that turns a project's real app into a vertical film (cached voiceover, frame-by-frame recording, HTML composition, hyperframes render) plus per-platform post copy and OG images.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -2374,6 +2374,16 @@
|
|
|
2374
2374
|
"additionalProperties": false
|
|
2375
2375
|
},
|
|
2376
2376
|
"description": "Camera moves over the opening frame, at least one."
|
|
2377
|
+
},
|
|
2378
|
+
"transition": {
|
|
2379
|
+
"default": "fade",
|
|
2380
|
+
"description": "How the opening frame hands over to the scene: fade (a 0.8 s cross-fade), rewind (0.8 s back through the scene in five dissolving frames) or cut.",
|
|
2381
|
+
"type": "string",
|
|
2382
|
+
"enum": [
|
|
2383
|
+
"fade",
|
|
2384
|
+
"rewind",
|
|
2385
|
+
"cut"
|
|
2386
|
+
]
|
|
2377
2387
|
}
|
|
2378
2388
|
},
|
|
2379
2389
|
"required": [
|
|
@@ -2383,6 +2393,11 @@
|
|
|
2383
2393
|
"additionalProperties": false,
|
|
2384
2394
|
"description": "The opening: the result frame the first sentence plays over."
|
|
2385
2395
|
},
|
|
2396
|
+
"today": {
|
|
2397
|
+
"description": "The day the app is recorded as of (YYYY-MM-DD): pin it once the voiceover is paid for, so the numbers it says still match the screen in a later month. --today overrides it; without either, the day of the run.",
|
|
2398
|
+
"type": "string",
|
|
2399
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
|
2400
|
+
},
|
|
2386
2401
|
"screenGuard": {
|
|
2387
2402
|
"minItems": 1,
|
|
2388
2403
|
"type": "array",
|
package/src/cli/main.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { splitIntoBeats } from "../voice/voiceover.js";
|
|
|
16
16
|
import { CliFailure, fail } from "./failure.js";
|
|
17
17
|
import { loadFilm, type LoadedFilm } from "./films.js";
|
|
18
18
|
import { writeOgImages } from "./og.js";
|
|
19
|
-
import { readOptions, type FilmOptions, type ShotsOptions } from "./options.js";
|
|
19
|
+
import { getRecordingDay, readOptions, type FilmOptions, type ShotsOptions } from "./options.js";
|
|
20
20
|
import { ensureServer } from "./server.js";
|
|
21
21
|
import { getVoiceoverPaths, produceVoiceover, readJson, requireVoiceover } from "./voice.js";
|
|
22
22
|
|
|
@@ -25,7 +25,7 @@ import { getVoiceoverPaths, produceVoiceover, readJson, requireVoiceover } from
|
|
|
25
25
|
*
|
|
26
26
|
* all <film> voiceover from the cache -> recording -> render -> post copy
|
|
27
27
|
* voice <film> [--commit] voiceover (paid only with --commit)
|
|
28
|
-
* record <film> [--today=YYYY-MM-DD] [--url=...]
|
|
28
|
+
* record <film> [--today=YYYY-MM-DD] [--url=...] (--today overrides the video's today)
|
|
29
29
|
* render <film> [--quality=draft|standard|high]
|
|
30
30
|
* preview <film>
|
|
31
31
|
* posts <film>
|
|
@@ -48,16 +48,17 @@ async function record(config: MarketingConfig, film: LoadedFilm, options: FilmOp
|
|
|
48
48
|
const voices = splitIntoBeats(voiceover.words, film.beats, film.voice.tempo);
|
|
49
49
|
const server = await ensureServer(config, film, options.url);
|
|
50
50
|
mkdirSync(getBuildDir(config, film), { recursive: true });
|
|
51
|
+
const recordingDay = getRecordingDay(options.today, film.today);
|
|
51
52
|
let log: RecordingLog;
|
|
52
53
|
try {
|
|
53
|
-
console.log(`recording: ${server.url} frame by frame; this takes a few minutes.`);
|
|
54
|
+
console.log(`recording: ${server.url} frame by frame, the app as of ${recordingDay.day ?? "today"} (${recordingDay.source}); this takes a few minutes.`);
|
|
54
55
|
log = await recordFilm({
|
|
55
56
|
film,
|
|
56
57
|
url: server.url,
|
|
57
58
|
outDir: getBuildDir(config, film),
|
|
58
59
|
voices,
|
|
59
60
|
voiceoverKey: getVoiceoverPaths(config, film).key,
|
|
60
|
-
today:
|
|
61
|
+
today: recordingDay.day ?? undefined,
|
|
61
62
|
filmPath: film.scenePath,
|
|
62
63
|
executablePath: process.env.PLAYWRIGHT_CHROMIUM_PATH || undefined,
|
|
63
64
|
browser: {
|
package/src/cli/options.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isCalendarDay } from "../config/day.js";
|
|
1
2
|
import { DEFAULT_CONFIG_FILE, QUALITIES, type Quality } from "../config/schema.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -20,7 +21,7 @@ export interface FilmOptions {
|
|
|
20
21
|
filmId: string;
|
|
21
22
|
/** `voice` really pays for the voiceover only with `--commit`. */
|
|
22
23
|
isCommit: boolean;
|
|
23
|
-
/** The day the app counts from
|
|
24
|
+
/** The day the app counts from; overrides the video's `today` for one run. */
|
|
24
25
|
today?: string;
|
|
25
26
|
/** Another address of the recorded page. */
|
|
26
27
|
url?: string;
|
|
@@ -56,7 +57,7 @@ export const USAGE = [
|
|
|
56
57
|
"",
|
|
57
58
|
" all <film> voiceover from the cache -> recording -> render -> post copy",
|
|
58
59
|
" voice <film> [--commit] voiceover; without --commit it only counts the characters",
|
|
59
|
-
" record <film> [--today=YYYY-MM-DD] [--url=...]",
|
|
60
|
+
" record <film> [--today=YYYY-MM-DD] [--url=...] --today overrides the video's \"today\"",
|
|
60
61
|
" render <film> [--quality=draft|standard|high]",
|
|
61
62
|
" preview <film> open the composition in the hyperframes preview",
|
|
62
63
|
" posts <film> post copy for the configured platforms",
|
|
@@ -100,7 +101,7 @@ export function readOptions(argv: string[]): ReadOptionsResult {
|
|
|
100
101
|
if (positional.length > 1) return { ok: false, error: `one film at a time, got ${positional.join(", ")}.` };
|
|
101
102
|
if (!FILM_ID.test(filmId)) return { ok: false, error: `film name "${filmId}": lowercase letters, digits and hyphens only.` };
|
|
102
103
|
const today = flags.get("today");
|
|
103
|
-
if (today !== undefined &&
|
|
104
|
+
if (today !== undefined && !isCalendarDay(today)) return { ok: false, error: `--today=${today}: expected a real day as YYYY-MM-DD.` };
|
|
104
105
|
const quality = flags.get("quality");
|
|
105
106
|
if (quality !== undefined && !isQuality(quality)) return { ok: false, error: `--quality=${quality}: expected ${QUALITIES.join(" | ")}.` };
|
|
106
107
|
const commit = flags.get("commit");
|
|
@@ -141,3 +142,13 @@ function readShotsOptions(positional: string[], flags: Map<string, string>, conf
|
|
|
141
142
|
if (url !== undefined && !isHttpAddress(url)) return { ok: false, error: `--url=${url}: expected an address such as http://localhost:3000.` };
|
|
142
143
|
return { ok: true, options: { command: "shots", shotId, url, configPath } };
|
|
143
144
|
}
|
|
145
|
+
|
|
146
|
+
/** The day a recording starts its page clock at, and where it came from. */
|
|
147
|
+
export type RecordingDay = { day: string; source: "--today" | "videos[].today" } | { day: null; source: "the day of the run" };
|
|
148
|
+
|
|
149
|
+
/** `--today` wins over the video's `today`; without either, the page clock starts now. */
|
|
150
|
+
export function getRecordingDay(flag: string | undefined, configured: string | null): RecordingDay {
|
|
151
|
+
if (flag !== undefined) return { day: flag, source: "--today" };
|
|
152
|
+
if (configured !== null) return { day: configured, source: "videos[].today" };
|
|
153
|
+
return { day: null, source: "the day of the run" };
|
|
154
|
+
}
|
package/src/cli/voice.ts
CHANGED
|
@@ -16,7 +16,7 @@ function getTtsInput(film: Film): TtsInput {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function getVoiceoverPaths(config: MarketingConfig, film: Film): VoiceoverPaths {
|
|
19
|
-
return getCachePaths(config.voice.cacheDir, getTtsInput(film));
|
|
19
|
+
return getCachePaths(config.voice.cacheDir, getTtsInput(film), film.id);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** JSON from disk with the file name in the error, not a bare "Unexpected token". */
|
|
@@ -43,6 +43,7 @@ export async function produceVoiceover(config: MarketingConfig, film: Film, isCo
|
|
|
43
43
|
const result = await produce({
|
|
44
44
|
cacheDir: config.voice.cacheDir,
|
|
45
45
|
input: getTtsInput(film),
|
|
46
|
+
videoId: film.id,
|
|
46
47
|
provider: createTtsProvider(config.voice.provider, { env: process.env }),
|
|
47
48
|
isCommit,
|
|
48
49
|
log: (line) => console.log(line),
|
package/src/compose/compose.ts
CHANGED
|
@@ -3,13 +3,13 @@ import type { SfxEvent } from "../config/schema.js";
|
|
|
3
3
|
import type { Film } from "../film.js";
|
|
4
4
|
import { formatMessage, type MarketingMessages } from "../messages/index.js";
|
|
5
5
|
import type { RecordingLog } from "../record/record.js";
|
|
6
|
-
import { BROWSER_BAR_HEIGHT, cameraPose, captionChunks, widePose, type CameraPose, type Geometry } from "./timeline.js";
|
|
6
|
+
import { BROWSER_BAR_HEIGHT, cameraPose, captionChunks, getTransitionSeconds, widePose, type CameraPose, type Geometry } from "./timeline.js";
|
|
7
7
|
import type { BeatVoice } from "../voice/voiceover.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* HyperFrames composition from the recording: the phone (or, for a desktop recording, a browser window) with the real screen, a camera that follows
|
|
11
11
|
* the thumb, a touch marker, captions in one place, the persona card, an opening from the result
|
|
12
|
-
* frame with
|
|
12
|
+
* frame with its transition into the scene (fade, rewind or cut), the end card and sounds.
|
|
13
13
|
*
|
|
14
14
|
* HyperFrames 0.8.85 contract: a root with `data-composition-id`, clips with
|
|
15
15
|
* `data-start`/`data-duration`, `<audio>` with an `id`, a GSAP timeline `{ paused: true }` under
|
|
@@ -21,15 +21,14 @@ const COMPOSITION_ID = "film";
|
|
|
21
21
|
/** Pause before the first word of the opening. */
|
|
22
22
|
const HOOK_LEAD = 0.3;
|
|
23
23
|
const HOOK_MIN = 3.8;
|
|
24
|
-
const REWIND = 0.8;
|
|
25
24
|
const END_TAIL = 1.6;
|
|
26
25
|
/** The end card enters this long after the start of the last sentence. */
|
|
27
26
|
const END_CARD_DELAY = 0.9;
|
|
28
27
|
|
|
29
28
|
export interface ComposeAssets {
|
|
30
|
-
/** Screen recording (mp4 from frames), the
|
|
29
|
+
/** Screen recording (mp4 from frames), the transition clip (null for a cut), the opening frame, the last frame, the tempo-shifted voiceover. */
|
|
31
30
|
screen: string;
|
|
32
|
-
|
|
31
|
+
transition: string | null;
|
|
33
32
|
hookStill: string;
|
|
34
33
|
lastFrame: string;
|
|
35
34
|
voiceover: string;
|
|
@@ -186,7 +185,8 @@ export function filmTimes(film: Film, log: RecordingLog, voices: BeatVoice[]) {
|
|
|
186
185
|
const last = log.beats.at(-1);
|
|
187
186
|
if (firstBeat === undefined || last === undefined) throw new Error("The recording has no sentences: record the film again (softure-marketing record).");
|
|
188
187
|
const firstFrame = firstBeat.f0;
|
|
189
|
-
const
|
|
188
|
+
const transition = getTransitionSeconds(film.hook.transition);
|
|
189
|
+
const offset = hook + transition - firstFrame / log.fps;
|
|
190
190
|
const at = (frame: number): number => r3(frame / log.fps + offset);
|
|
191
191
|
const lastVoice = voice.get(last.id);
|
|
192
192
|
if (lastVoice === undefined) throw new Error(`No voiceover for sentence "${last.id}".`);
|
|
@@ -198,7 +198,7 @@ export function filmTimes(film: Film, log: RecordingLog, voices: BeatVoice[]) {
|
|
|
198
198
|
if (unplaced.length > 0) {
|
|
199
199
|
throw new Error(`The recording has no sentences ${unplaced.join(", ")}: record the film again (softure-marketing record).`);
|
|
200
200
|
}
|
|
201
|
-
return { hook,
|
|
201
|
+
return { hook, transition, firstFrame, at, endCard, end, screenEnd: at(log.frames), voiceStart };
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
export function composeFilm(input: ComposeInput): string {
|
|
@@ -314,7 +314,7 @@ export function composeFilm(input: ComposeInput): string {
|
|
|
314
314
|
.join("\n ");
|
|
315
315
|
|
|
316
316
|
const personaOut = log.cues.find((cue) => cue.name === "persona-out");
|
|
317
|
-
const screenStart = r3(times.hook + times.
|
|
317
|
+
const screenStart = r3(times.hook + times.transition);
|
|
318
318
|
const screenDuration = r3(times.screenEnd - screenStart);
|
|
319
319
|
const tail = r3(Math.max(0.04, times.end - times.screenEnd));
|
|
320
320
|
const c = colors;
|
|
@@ -375,7 +375,7 @@ ${windowParts.css}
|
|
|
375
375
|
${windowParts.open}
|
|
376
376
|
<div class="screen">
|
|
377
377
|
<img id="hook" class="clip" src="${assets.hookStill}" data-start="0" data-duration="${times.hook}" />
|
|
378
|
-
|
|
378
|
+
${assets.transition === null || times.transition === 0 ? "" : `<video id="transition" class="clip" src="${assets.transition}" data-start="${times.hook}" data-duration="${times.transition}" muted playsinline></video>`}
|
|
379
379
|
<video id="recording" class="clip" src="${assets.screen}" data-start="${screenStart}" data-media-start="${r3(times.firstFrame / log.fps)}" data-duration="${screenDuration}" muted playsinline></video>
|
|
380
380
|
<img id="last" class="clip" src="${assets.lastFrame}" data-start="${times.screenEnd}" data-duration="${tail}" />
|
|
381
381
|
${taps.map((tap) => `<div class="touch" style="left:${tap.x}px;top:${tap.y}px"><div class="dot" id="td${tap.i}"></div><div class="ring" id="tr${tap.i}"></div></div>`).join("\n ")}
|
package/src/compose/timeline.ts
CHANGED
|
@@ -327,3 +327,58 @@ export function rewindFrames(first: number, still: number, count = 24): number[]
|
|
|
327
327
|
if (count < 2) throw new Error("A rewind needs at least two frames.");
|
|
328
328
|
return Array.from({ length: count }, (_, k) => Math.round(still - ((still - first) * k) / (count - 1)));
|
|
329
329
|
}
|
|
330
|
+
|
|
331
|
+
/** How the opening frame hands over to the scene: a cross-fade, a rewind through the scene, or a hard cut. */
|
|
332
|
+
export const TRANSITIONS = ["fade", "rewind", "cut"] as const;
|
|
333
|
+
|
|
334
|
+
export type Transition = (typeof TRANSITIONS)[number];
|
|
335
|
+
|
|
336
|
+
const TRANSITION_SECONDS = 0.8;
|
|
337
|
+
/** Key frames of a rewind: few enough that each screen reads, joined by dissolves (24 blended frames flickered in FIRE). */
|
|
338
|
+
const REWIND_KEY_FRAMES = 5;
|
|
339
|
+
/** One dissolve between two key frames of a rewind. */
|
|
340
|
+
const REWIND_DISSOLVE = 0.2;
|
|
341
|
+
|
|
342
|
+
export function getTransitionSeconds(transition: Transition): number {
|
|
343
|
+
return transition === "cut" ? 0 : TRANSITION_SECONDS;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** The recording frames the transition clip is made of, from the opening frame (`still`) to the scene's first (`first`). */
|
|
347
|
+
export function transitionFrames(transition: Transition, first: number, still: number): number[] {
|
|
348
|
+
if (still < first) throw new Error(`The opening frame (${still}) is before the start of the scene (${first}).`);
|
|
349
|
+
if (transition === "cut") return [];
|
|
350
|
+
if (transition === "fade") return [still, first];
|
|
351
|
+
return rewindFrames(first, still, REWIND_KEY_FRAMES);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface CrossfadeInput {
|
|
355
|
+
/** Still images in order, at least two. */
|
|
356
|
+
inputs: string[];
|
|
357
|
+
fps: number;
|
|
358
|
+
/** The clip's length. */
|
|
359
|
+
seconds: number;
|
|
360
|
+
output: string;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const r3 = (value: number) => Math.round(value * 1000) / 1000;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* ffmpeg arguments of a clip that dissolves from still to still and lasts exactly `seconds`. Two stills fade over
|
|
367
|
+
* the whole clip; more are joined by `REWIND_DISSOLVE` dissolves, each still shown for the same time.
|
|
368
|
+
*/
|
|
369
|
+
export function crossfadeArgs(input: CrossfadeInput): string[] {
|
|
370
|
+
const { inputs, fps, seconds, output } = input;
|
|
371
|
+
const count = inputs.length;
|
|
372
|
+
if (count < 2) throw new Error(`A cross-fade needs at least two stills, got ${count}.`);
|
|
373
|
+
const dissolve = count === 2 ? seconds : REWIND_DISSOLVE;
|
|
374
|
+
// count × each - (count - 1) × dissolve = seconds
|
|
375
|
+
const each = r3((seconds + (count - 1) * dissolve) / count);
|
|
376
|
+
const step = r3(each - dissolve);
|
|
377
|
+
const sources = inputs.flatMap((path) => ["-loop", "1", "-framerate", String(fps), "-t", String(each), "-i", path]);
|
|
378
|
+
const filters = inputs.slice(1).map((_, k) => {
|
|
379
|
+
const from = k === 0 ? "[0:v]" : `[v${k}]`;
|
|
380
|
+
const last = k === count - 2 ? ",format=yuv420p" : "";
|
|
381
|
+
return `${from}[${k + 1}:v]xfade=transition=fade:duration=${dissolve}:offset=${r3((k + 1) * step)}${last}[v${k + 1}]`;
|
|
382
|
+
});
|
|
383
|
+
return [...sources, "-filter_complex", filters.join(";"), "-map", `[v${count - 1}]`, "-t", String(seconds), "-r", String(fps), "-c:v", "libx264", "-crf", "16", "-pix_fmt", "yuv420p", output];
|
|
384
|
+
}
|
package/src/config/config.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface VideoConfig extends FilmScript {
|
|
|
67
67
|
sceneSource: SceneSource;
|
|
68
68
|
/** The post copy, or null when `social.posts` has no entry for this video. */
|
|
69
69
|
post: VideoPost | null;
|
|
70
|
+
/** `videos[].today`: the day the app is recorded as of, or null for the day of the run. */
|
|
71
|
+
today: string | null;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export interface MarketingConfig {
|
|
@@ -143,6 +145,7 @@ function resolveVideos(data: MarketingJson, at: (relative: string) => string): V
|
|
|
143
145
|
beats: video.beats.map((beat) => ({ id: beat.id, text: beat.text })),
|
|
144
146
|
hook: video.hook,
|
|
145
147
|
screenGuard: video.screenGuard,
|
|
148
|
+
today: video.today ?? null,
|
|
146
149
|
endCard: video.endCard,
|
|
147
150
|
url: new URL(video.path, data.app.baseUrl).href,
|
|
148
151
|
ownUrl: `http://localhost:${data.app.port}${video.path}`,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A day as `YYYY-MM-DD`, the form of `--today` and `videos[].today`. */
|
|
2
|
+
export const DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
3
|
+
|
|
4
|
+
/** True for a `YYYY-MM-DD` that names a real calendar day (not 2026-02-30): the page clock would start at Invalid Date. */
|
|
5
|
+
export function isCalendarDay(value: string): boolean {
|
|
6
|
+
if (!DAY_PATTERN.test(value)) return false;
|
|
7
|
+
const date = new Date(`${value}T00:00:00Z`);
|
|
8
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(value);
|
|
9
|
+
}
|
package/src/config/schema.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
LAYOUTS,
|
|
6
6
|
LAYOUT_NAMES,
|
|
7
7
|
MIN_DESKTOP_WIDTH,
|
|
8
|
+
TRANSITIONS,
|
|
8
9
|
VIDEO_FORMATS,
|
|
9
10
|
fitsFrame,
|
|
10
11
|
isDesktopViewport,
|
|
@@ -17,6 +18,7 @@ import { CHANNEL_CODE_MAX_LENGTH, CHANNEL_CODE_PATTERN, DEFAULT_LINK_IN_BIO, PLA
|
|
|
17
18
|
import { ELEVENLABS_DEFAULT_MODEL } from "../voice/voiceover.js";
|
|
18
19
|
import { actionSchema, type SceneAction } from "./actions-schema.js";
|
|
19
20
|
import { COLOR_ROLES, COLOR_THEMES, isHexColor, type ColorRole } from "./colors.js";
|
|
21
|
+
import { DAY_PATTERN, isCalendarDay } from "./day.js";
|
|
20
22
|
import { getScreenshotNames } from "./screenshot-names.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -241,7 +243,7 @@ type VideoBeat = { id: string; text: string; pad?: number | undefined; actions?:
|
|
|
241
243
|
|
|
242
244
|
interface SceneShape {
|
|
243
245
|
beats: VideoBeat[];
|
|
244
|
-
hook: { still: string; shots: { mark: string }[] };
|
|
246
|
+
hook: { still: string; shots: { mark: string }[]; transition: string };
|
|
245
247
|
sceneModule?: string | undefined;
|
|
246
248
|
}
|
|
247
249
|
|
|
@@ -346,8 +348,20 @@ const videoSchema = z
|
|
|
346
348
|
)
|
|
347
349
|
.min(1, "the opening needs at least one shot")
|
|
348
350
|
.describe("Camera moves over the opening frame, at least one."),
|
|
351
|
+
transition: z
|
|
352
|
+
.enum(TRANSITIONS)
|
|
353
|
+
.default("fade")
|
|
354
|
+
.describe("How the opening frame hands over to the scene: fade (a 0.8 s cross-fade), rewind (0.8 s back through the scene in five dissolving frames) or cut."),
|
|
349
355
|
})
|
|
350
356
|
.describe("The opening: the result frame the first sentence plays over."),
|
|
357
|
+
today: z
|
|
358
|
+
.string()
|
|
359
|
+
.regex(DAY_PATTERN, "must be a day as YYYY-MM-DD")
|
|
360
|
+
.refine(isCalendarDay, "is not a day of the calendar")
|
|
361
|
+
.optional()
|
|
362
|
+
.describe(
|
|
363
|
+
"The day the app is recorded as of (YYYY-MM-DD): pin it once the voiceover is paid for, so the numbers it says still match the screen in a later month. --today overrides it; without either, the day of the run.",
|
|
364
|
+
),
|
|
351
365
|
screenGuard: z
|
|
352
366
|
.array(nonBlank)
|
|
353
367
|
.min(1, "the screen guard needs at least one phrase")
|
package/src/film.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Locator, Page } from "playwright";
|
|
2
2
|
|
|
3
|
-
import type { LayoutOverride, VideoFormat, Viewport } from "./compose/timeline.js";
|
|
3
|
+
import type { LayoutOverride, Transition, VideoFormat, Viewport } from "./compose/timeline.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Film = script (data from `marketing.json`) + scene: the beats' `actions` in the same file
|
|
@@ -86,7 +86,8 @@ export interface FilmScript {
|
|
|
86
86
|
* scene records the rest, and the **last** ends with the end card.
|
|
87
87
|
*/
|
|
88
88
|
beats: Beat[];
|
|
89
|
-
|
|
89
|
+
/** The opening: its still, the camera shots over it, and how it hands over to the scene. */
|
|
90
|
+
hook: { still: string; shots: HookShot[]; transition: Transition };
|
|
90
91
|
/**
|
|
91
92
|
* Phrases the voiceover says that the app's screen must show. If any is missing the recording
|
|
92
93
|
* stops and the render never starts.
|