@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.
Files changed (67) hide show
  1. package/README.md +28 -6
  2. package/dist/cli/main.js +5 -4
  3. package/dist/cli/main.js.map +1 -1
  4. package/dist/cli/options.d.ts +11 -1
  5. package/dist/cli/options.d.ts.map +1 -1
  6. package/dist/cli/options.js +12 -3
  7. package/dist/cli/options.js.map +1 -1
  8. package/dist/cli/voice.d.ts.map +1 -1
  9. package/dist/cli/voice.js +2 -1
  10. package/dist/cli/voice.js.map +1 -1
  11. package/dist/compose/compose.d.ts +3 -3
  12. package/dist/compose/compose.d.ts.map +1 -1
  13. package/dist/compose/compose.js +7 -7
  14. package/dist/compose/compose.js.map +1 -1
  15. package/dist/compose/timeline.d.ts +19 -0
  16. package/dist/compose/timeline.d.ts.map +1 -1
  17. package/dist/compose/timeline.js +42 -0
  18. package/dist/compose/timeline.js.map +1 -1
  19. package/dist/config/config.d.ts +2 -0
  20. package/dist/config/config.d.ts.map +1 -1
  21. package/dist/config/config.js +1 -0
  22. package/dist/config/config.js.map +1 -1
  23. package/dist/config/day.d.ts +5 -0
  24. package/dist/config/day.d.ts.map +1 -0
  25. package/dist/config/day.js +10 -0
  26. package/dist/config/day.js.map +1 -0
  27. package/dist/config/schema.d.ts +6 -0
  28. package/dist/config/schema.d.ts.map +1 -1
  29. package/dist/config/schema.js +12 -1
  30. package/dist/config/schema.js.map +1 -1
  31. package/dist/film.d.ts +3 -1
  32. package/dist/film.d.ts.map +1 -1
  33. package/dist/film.js.map +1 -1
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/record/record.d.ts +1 -1
  39. package/dist/record/record.d.ts.map +1 -1
  40. package/dist/record/record.js +2 -2
  41. package/dist/record/record.js.map +1 -1
  42. package/dist/render/render.js +10 -10
  43. package/dist/render/render.js.map +1 -1
  44. package/dist/voice/cache.d.ts +11 -2
  45. package/dist/voice/cache.d.ts.map +1 -1
  46. package/dist/voice/cache.js +30 -5
  47. package/dist/voice/cache.js.map +1 -1
  48. package/dist/voice/produce.d.ts +2 -0
  49. package/dist/voice/produce.d.ts.map +1 -1
  50. package/dist/voice/produce.js +14 -4
  51. package/dist/voice/produce.js.map +1 -1
  52. package/package.json +1 -1
  53. package/schema/marketing.schema.json +15 -0
  54. package/src/cli/main.ts +5 -4
  55. package/src/cli/options.ts +14 -3
  56. package/src/cli/voice.ts +2 -1
  57. package/src/compose/compose.ts +9 -9
  58. package/src/compose/timeline.ts +55 -0
  59. package/src/config/config.ts +3 -0
  60. package/src/config/day.ts +9 -0
  61. package/src/config/schema.ts +15 -1
  62. package/src/film.ts +3 -2
  63. package/src/index.ts +7 -1
  64. package/src/record/record.ts +3 -3
  65. package/src/render/render.ts +10 -10
  66. package/src/voice/cache.ts +33 -5
  67. package/src/voice/produce.ts +17 -4
@@ -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, rewindFrames } from "../compose/timeline.js";
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
- // Rewind: 24 frames from the opening frame back to the start of the scene (0.8 s), with motion blur.
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 rewindDir = join(dir, "rewind");
56
- rmSync(rewindDir, { recursive: true, force: true });
57
- mkdirSync(rewindDir, { recursive: true });
58
- rewindFrames(firstBeat.f0, still).forEach((index, k) => {
59
- cpSync(getFramePath(dir, index), join(rewindDir, `r${String(k).padStart(5, "0")}.jpg`));
60
- });
61
- ff(["-framerate", String(log.fps), "-i", join(rewindDir, "r%05d.jpg"), "-vf", "tmix=frames=3", "-c:v", "libx264", "-crf", "16", "-pix_fmt", "yuv420p", join(assetsDir, "rewind.mp4")]);
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
- rewind: "assets/rewind.mp4",
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":"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"}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"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"}
@@ -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
- /** Where a recording of this input lives in the cache: `<key>.mp3` and `<key>.json`. */
14
- export declare function getVoiceoverPaths(cacheDir: string, input: TtsInput): VoiceoverPaths;
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;CACf;AAED,sEAAsE;AACtE,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,cAAc,CAGnF;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,CAK1G"}
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"}
@@ -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
- /** Where a recording of this input lives in the cache: `<key>.mp3` and `<key>.json`. */
6
- export function getVoiceoverPaths(cacheDir, input) {
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
- return { key, audio: join(cacheDir, `${key}.mp3`), words: join(cacheDir, `${key}.json`) };
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 };
@@ -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;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAoD,MAAM,eAAe,CAAC;AAC1F,OAAO,EAAE,YAAY,EAAkB,MAAM,gBAAgB,CAAC;AAc9D,wFAAwF;AACxF,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,KAAe;IACjE,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IACjF,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,GAAG,GAAG,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,CAAC;AAC5F,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,CACL,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACpB,KAAK,CAAC,KAAK,CAAC,CAAC,IAAa,EAAE,EAAE;YAC5B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO,KAAK,CAAC;YAC5D,MAAM,IAAI,GAAG,IAA+B,CAAC;YAC7C,OAAO,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC;QACzG,CAAC,CAAC,CACH,CAAC;AACJ,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,mBAAmB,CAAC,KAAqB;IACvD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;IAC1E,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAY,CAAC;IACnE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,GAAG,CAAC,eAAe,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,gCAAgC,CAAC,CAAC;IACrF,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,KAAqB,EAAE,SAAuB;IAC7F,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,aAAa,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;IAC5C,aAAa,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAC;AACxD,CAAC"}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/voice/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1F,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,GAAG,EAAE,EAAE,EAAoD,MAAM,eAAe,CAAC;AAC1F,OAAO,EAAE,YAAY,EAAkB,MAAM,gBAAgB,CAAC;AAgB9D,SAAS,UAAU,CAAC,GAAW,EAAE,GAAW,EAAE,MAAgC;IAC5E,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;AAC1F,CAAC;AAED,SAAS,YAAY,CAAC,KAAqB;IACzC,OAAO,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,KAAe,EAAE,OAAgB;IACnF,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IACjF,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;IAC9D,OAAO,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/D,CAAC;AAED,yHAAyH;AACzH,MAAM,UAAU,mBAAmB,CAAC,QAAgB,EAAE,OAAe,EAAE,GAAW;IAChF,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,OAAO,WAAW,CAAC,GAAG,CAAC;SACpB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,GAAG,GAAG,MAAM,CAAC;SAChE,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,CACL,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACpB,KAAK,CAAC,KAAK,CAAC,CAAC,IAAa,EAAE,EAAE;YAC5B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO,KAAK,CAAC;YAC5D,MAAM,IAAI,GAAG,IAA+B,CAAC;YAC7C,OAAO,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC;QACzG,CAAC,CAAC,CACH,CAAC;AACJ,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,mBAAmB,CAAC,KAAqB;IACvD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;IAC1E,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAY,CAAC;IACnE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,GAAG,CAAC,eAAe,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,gCAAgC,CAAC,CAAC;IACrF,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,KAAqB,EAAE,SAAuB;IAC7F,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,aAAa,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;IAC5C,aAAa,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAC;AACxD,CAAC"}
@@ -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":"AAAA,OAAO,EAA0D,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AACpG,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,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,CAoB7G"}
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"}
@@ -1,4 +1,5 @@
1
- import { getVoiceoverPaths, readCachedVoiceover, writeVoiceover } from "./cache.js";
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 ${cacheDir} (commit them).`);
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;AACpG,OAAO,EAAE,GAAG,EAAE,EAAE,EAAqE,MAAM,eAAe,CAAC;AAgB3G,MAAM,UAAU,gBAAgB,CAAC,QAAqB,EAAE,QAAqB;IAC3E,OAAO,uBAAuB,QAAQ,CAAC,EAAE,MAAM,QAAQ,CAAC,UAAU,wBAAwB,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC,IAAI,GAAG,CAAC;AACjI,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgC;IACrE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IAC7D,MAAM,KAAK,GAAG,iBAAiB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC;IAC9B,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1B,GAAG,CAAC,6BAA6B,KAAK,CAAC,GAAG,kBAAkB,CAAC,CAAC;QAC9D,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC1C,GAAG,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC1C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,GAAG,CAAC,4BAA4B,KAAK,CAAC,OAAO,2DAA2D,CAAC,CAAC;QAC1G,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACnD,IAAI,CAAC,SAAS,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,cAAc,CAAC,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;IACnE,GAAG,CAAC,uBAAuB,QAAQ,CAAC,UAAU,sBAAsB,KAAK,CAAC,GAAG,kBAAkB,QAAQ,iBAAiB,CAAC,CAAC;IAC1H,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;AACvE,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.5",
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: options.today,
61
+ today: recordingDay.day ?? undefined,
61
62
  filmPath: film.scenePath,
62
63
  executablePath: process.env.PLAYWRIGHT_CHROMIUM_PATH || undefined,
63
64
  browser: {
@@ -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, only to reproduce an old film. */
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 && !/^\d{4}-\d{2}-\d{2}$/.test(today)) return { ok: false, error: `--today=${today}: expected YYYY-MM-DD.` };
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),
@@ -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 a rewind, the end card and sounds.
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 rewind clip, the opening frame, the last frame, the tempo-shifted voiceover. */
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
- rewind: string;
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 offset = hook + REWIND - firstFrame / log.fps;
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, rewind: REWIND, firstFrame, at, endCard, end, screenEnd: at(log.frames), voiceStart };
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.rewind);
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
- <video id="rewind" class="clip" src="${assets.rewind}" data-start="${times.hook}" data-duration="${times.rewind}" muted playsinline></video>
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 ")}
@@ -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
+ }
@@ -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
+ }
@@ -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
- hook: { still: string; shots: HookShot[] };
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.