@bitmagic/asset-core 0.2.25-dev.0 → 0.2.25-dev.1

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 (89) hide show
  1. package/dist/trailer/analyze.d.ts +24 -0
  2. package/dist/trailer/analyze.d.ts.map +1 -0
  3. package/dist/trailer/analyze.js +186 -0
  4. package/dist/trailer/analyze.js.map +1 -0
  5. package/dist/trailer/auto-edl.d.ts +51 -0
  6. package/dist/trailer/auto-edl.d.ts.map +1 -0
  7. package/dist/trailer/auto-edl.js +266 -0
  8. package/dist/trailer/auto-edl.js.map +1 -0
  9. package/dist/trailer/beat-cut.d.ts +58 -0
  10. package/dist/trailer/beat-cut.d.ts.map +1 -0
  11. package/dist/trailer/beat-cut.js +232 -0
  12. package/dist/trailer/beat-cut.js.map +1 -0
  13. package/dist/trailer/capture.d.ts +102 -0
  14. package/dist/trailer/capture.d.ts.map +1 -0
  15. package/dist/trailer/capture.js +268 -0
  16. package/dist/trailer/capture.js.map +1 -0
  17. package/dist/trailer/edl-time.d.ts +62 -0
  18. package/dist/trailer/edl-time.d.ts.map +1 -0
  19. package/dist/trailer/edl-time.js +105 -0
  20. package/dist/trailer/edl-time.js.map +1 -0
  21. package/dist/trailer/errors.d.ts +15 -0
  22. package/dist/trailer/errors.d.ts.map +1 -0
  23. package/dist/trailer/errors.js +22 -0
  24. package/dist/trailer/errors.js.map +1 -0
  25. package/dist/trailer/ffmpeg.d.ts +19 -0
  26. package/dist/trailer/ffmpeg.d.ts.map +1 -0
  27. package/dist/trailer/ffmpeg.js +100 -0
  28. package/dist/trailer/ffmpeg.js.map +1 -0
  29. package/dist/trailer/hud-state.d.ts +182 -0
  30. package/dist/trailer/hud-state.d.ts.map +1 -0
  31. package/dist/trailer/hud-state.js +523 -0
  32. package/dist/trailer/hud-state.js.map +1 -0
  33. package/dist/trailer/index.d.ts +30 -0
  34. package/dist/trailer/index.d.ts.map +1 -0
  35. package/dist/trailer/index.js +30 -0
  36. package/dist/trailer/index.js.map +1 -0
  37. package/dist/trailer/io.d.ts +23 -0
  38. package/dist/trailer/io.d.ts.map +1 -0
  39. package/dist/trailer/io.js +20 -0
  40. package/dist/trailer/io.js.map +1 -0
  41. package/dist/trailer/link.d.ts +2 -0
  42. package/dist/trailer/link.d.ts.map +1 -0
  43. package/dist/trailer/link.js +15 -0
  44. package/dist/trailer/link.js.map +1 -0
  45. package/dist/trailer/make.d.ts +60 -0
  46. package/dist/trailer/make.d.ts.map +1 -0
  47. package/dist/trailer/make.js +196 -0
  48. package/dist/trailer/make.js.map +1 -0
  49. package/dist/trailer/merge.d.ts +8 -0
  50. package/dist/trailer/merge.d.ts.map +1 -0
  51. package/dist/trailer/merge.js +165 -0
  52. package/dist/trailer/merge.js.map +1 -0
  53. package/dist/trailer/music-beats.d.ts +124 -0
  54. package/dist/trailer/music-beats.d.ts.map +1 -0
  55. package/dist/trailer/music-beats.js +672 -0
  56. package/dist/trailer/music-beats.js.map +1 -0
  57. package/dist/trailer/paths.d.ts +29 -0
  58. package/dist/trailer/paths.d.ts.map +1 -0
  59. package/dist/trailer/paths.js +101 -0
  60. package/dist/trailer/paths.js.map +1 -0
  61. package/dist/trailer/png.d.ts +10 -0
  62. package/dist/trailer/png.d.ts.map +1 -0
  63. package/dist/trailer/png.js +37 -0
  64. package/dist/trailer/png.js.map +1 -0
  65. package/dist/trailer/render.d.ts +93 -0
  66. package/dist/trailer/render.d.ts.map +1 -0
  67. package/dist/trailer/render.js +527 -0
  68. package/dist/trailer/render.js.map +1 -0
  69. package/dist/trailer/revision.d.ts +9 -0
  70. package/dist/trailer/revision.d.ts.map +1 -0
  71. package/dist/trailer/revision.js +31 -0
  72. package/dist/trailer/revision.js.map +1 -0
  73. package/dist/trailer/synth.d.ts +10 -0
  74. package/dist/trailer/synth.d.ts.map +1 -0
  75. package/dist/trailer/synth.js +120 -0
  76. package/dist/trailer/synth.js.map +1 -0
  77. package/dist/trailer/timeline-types.d.ts +196 -0
  78. package/dist/trailer/timeline-types.d.ts.map +1 -0
  79. package/dist/trailer/timeline-types.js +419 -0
  80. package/dist/trailer/timeline-types.js.map +1 -0
  81. package/dist/trailer/titles.d.ts +38 -0
  82. package/dist/trailer/titles.d.ts.map +1 -0
  83. package/dist/trailer/titles.js +110 -0
  84. package/dist/trailer/titles.js.map +1 -0
  85. package/dist/trailer/video-chain.d.ts +130 -0
  86. package/dist/trailer/video-chain.d.ts.map +1 -0
  87. package/dist/trailer/video-chain.js +261 -0
  88. package/dist/trailer/video-chain.js.map +1 -0
  89. package/package.json +5 -1
@@ -0,0 +1,20 @@
1
+ const STDERR_LOG = {
2
+ info: (line) => process.stderr.write(`${line}\n`),
3
+ warn: (line) => process.stderr.write(`${line}\n`),
4
+ };
5
+ let bound = STDERR_LOG;
6
+ /** The command layer calls this once with `createOutput(json)` before running any pipeline step. */
7
+ export function bindTrailerLog(log) {
8
+ bound = log;
9
+ }
10
+ /** Back to stderr — for tests that bound a capturing log. */
11
+ export function resetTrailerLog() {
12
+ bound = STDERR_LOG;
13
+ }
14
+ export function out(line) {
15
+ bound.info(line);
16
+ }
17
+ export function warn(line) {
18
+ bound.warn(line);
19
+ }
20
+ //# sourceMappingURL=io.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.js","sourceRoot":"","sources":["../../src/trailer/io.ts"],"names":[],"mappings":"AAiBA,MAAM,UAAU,GAAe;IAC7B,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IACjD,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;CAClD,CAAC;AAEF,IAAI,KAAK,GAAe,UAAU,CAAC;AAEnC,oGAAoG;AACpG,MAAM,UAAU,cAAc,CAAC,GAAe;IAC5C,KAAK,GAAG,GAAG,CAAC;AACd,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,eAAe;IAC7B,KAAK,GAAG,UAAU,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,IAAY;IAC9B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,IAAY;IAC/B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function linkOrCopy(from: string, to: string): void;
2
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/trailer/link.ts"],"names":[],"mappings":"AAOA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAMzD"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Hard-link when possible, copy otherwise — the cross-filesystem (and link-count / no-link
3
+ * filesystem) fallback both frame-sequence builders share: `merge` renumbering a session's frames,
4
+ * and the render's densified HUD overlay sequence.
5
+ */
6
+ import * as fs from 'fs';
7
+ export function linkOrCopy(from, to) {
8
+ try {
9
+ fs.linkSync(from, to);
10
+ }
11
+ catch {
12
+ fs.copyFileSync(from, to);
13
+ }
14
+ }
15
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../../src/trailer/link.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,EAAU;IACjD,IAAI,CAAC;QACH,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC"}
@@ -0,0 +1,60 @@
1
+ import { type TrailerRasterizers } from './render.js';
2
+ export declare const DEFAULT_TRAILER_SECONDS = 30;
3
+ /** Music runs a little past the cut so the fade-out has material. */
4
+ export declare const MUSIC_TAIL_SEC = 4;
5
+ /** The render's music/SFX balance the make-trailer skill settled on. */
6
+ export declare const MAKE_MUSIC_GAIN_DB = -4;
7
+ export declare const MAKE_SFX_GAIN_DB = -3;
8
+ export interface TrailerPublished {
9
+ publicUrl: string;
10
+ pageUrl?: string;
11
+ attached: boolean;
12
+ }
13
+ export interface MakeOptions {
14
+ root: string;
15
+ /** A finished recording dir (see paths.resolveRecording). */
16
+ recordingDir: string;
17
+ /** A music file to bed under the cut. */
18
+ music?: string;
19
+ /** Generate the bed from this prompt through `generateMusic`. */
20
+ musicPrompt?: string;
21
+ /** Target length including the end card. */
22
+ seconds?: number;
23
+ /** End-card text; defaults to `gameName`. */
24
+ title?: string;
25
+ /** The game's name, for the end card and the file name; the root directory's name when absent. */
26
+ gameName?: string;
27
+ sub?: string;
28
+ /** Draw the HUD replay (needs `harnessUrl`). */
29
+ ui?: boolean;
30
+ /** `/hud-replay.html` on the running dev sidecar. */
31
+ harnessUrl?: string;
32
+ /** Ceiling on HUD overlay changes per second of output; the render's default otherwise. */
33
+ uiKeyframesPerSec?: number;
34
+ /** Throw away trailer/shots.json and write a fresh auto cut. */
35
+ resetShots?: boolean;
36
+ /** The music lane: write an mp3 of `seconds` for `prompt` at `outPath`. */
37
+ generateMusic?: (prompt: string, seconds: number, outPath: string) => Promise<void>;
38
+ /** Draws the HUD and title layers (see render.ts); without it the trailer has neither. */
39
+ rasterizers?: TrailerRasterizers;
40
+ /** The publish lane: upload the render and return where it went. */
41
+ publish?: (mp4: string) => Promise<TrailerPublished>;
42
+ }
43
+ export interface MakeResult {
44
+ recordingDir: string;
45
+ recordingName: string;
46
+ workDir: string;
47
+ analysisPath: string;
48
+ shotsPath: string;
49
+ /** Whether this run wrote the shots file (first cut) or cut an existing one. */
50
+ shotsWritten: boolean;
51
+ edlPath: string;
52
+ musicPath?: string;
53
+ beatsPath?: string;
54
+ mp4: string;
55
+ durationSec: number;
56
+ clipCount: number;
57
+ published?: TrailerPublished;
58
+ }
59
+ export declare function makeTrailer(options: MakeOptions): Promise<MakeResult>;
60
+ //# sourceMappingURL=make.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"make.d.ts","sourceRoot":"","sources":["../../src/trailer/make.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAa,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGjE,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,qEAAqE;AACrE,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kGAAkG;IAClG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2EAA2E;IAC3E,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,0FAA0F;IAC1F,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,oEAAoE;IACpE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAqGD,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAoF3E"}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * `bitmagic trailer make` — one command from a recording to a rendered trailer, and the SAME
3
+ * command for every tuning loop after it.
4
+ *
5
+ * The pipeline is analyze → music → beats → shots → cut → render (→ publish). Every derived file
6
+ * lands in the recording's work dir under `.bitmagic/trailer/`; the one authored file is
7
+ * `trailer/shots.json` at the project root. The first run writes it (the auto cut in auto-edl.ts);
8
+ * every later run cuts and renders whatever the agent edited, so "make it faster" is: edit the
9
+ * shots, run `make` again, watch the new `-rN` render.
10
+ *
11
+ * Music is never generated silently: `--music-prompt` spends sparks, so it is opt-in, and a bed
12
+ * generated once is reused on the next run as long as the prompt is unchanged (music.json records
13
+ * what was asked for).
14
+ */
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import { TrailerError } from './errors.js';
18
+ import { runAnalyze } from './analyze.js';
19
+ import { buildAutoShots } from './auto-edl.js';
20
+ import { cutShots, describeCut, loadShots } from './beat-cut.js';
21
+ import { out, warn } from './io.js';
22
+ import { analyzeBeats } from './music-beats.js';
23
+ import { shotsPath, trailerSlug, workDir } from './paths.js';
24
+ import { runRender } from './render.js';
25
+ import { loadTimeline } from './timeline-types.js';
26
+ export const DEFAULT_TRAILER_SECONDS = 30;
27
+ /** Music runs a little past the cut so the fade-out has material. */
28
+ export const MUSIC_TAIL_SEC = 4;
29
+ /** The render's music/SFX balance the make-trailer skill settled on. */
30
+ export const MAKE_MUSIC_GAIN_DB = -4;
31
+ export const MAKE_SFX_GAIN_DB = -3;
32
+ function newerThan(a, b) {
33
+ return fs.existsSync(a) && fs.existsSync(b) && fs.statSync(a).mtimeMs >= fs.statSync(b).mtimeMs;
34
+ }
35
+ function readAnalysis(filePath) {
36
+ try {
37
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
38
+ if (typeof parsed === 'object' && parsed !== null && Array.isArray(parsed.windows)) {
39
+ return parsed;
40
+ }
41
+ }
42
+ catch {
43
+ // Fall through: re-analyze.
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * The music bed: a file the creator supplied, a bed generated from a prompt (reused while the
49
+ * prompt is unchanged), or none.
50
+ */
51
+ async function resolveMusic(options, work, seconds) {
52
+ if (options.music !== undefined) {
53
+ const resolved = path.resolve(options.music);
54
+ if (!fs.existsSync(resolved))
55
+ throw new TrailerError(`Music file not found: ${resolved}`);
56
+ return resolved;
57
+ }
58
+ if (options.musicPrompt === undefined)
59
+ return undefined;
60
+ if (options.generateMusic === undefined) {
61
+ throw new TrailerError('Music generation is not available in this build — pass --music <file> instead.');
62
+ }
63
+ const musicPath = path.join(work, 'music.mp3');
64
+ const recordPath = path.join(work, 'music.json');
65
+ const wanted = { prompt: options.musicPrompt, seconds: seconds + MUSIC_TAIL_SEC, at: '' };
66
+ if (fs.existsSync(musicPath) && fs.existsSync(recordPath)) {
67
+ try {
68
+ const previous = JSON.parse(fs.readFileSync(recordPath, 'utf-8'));
69
+ if (previous.prompt === wanted.prompt && previous.seconds === wanted.seconds) {
70
+ out(`music: reusing the bed generated earlier for this prompt (${path.basename(musicPath)})`);
71
+ return musicPath;
72
+ }
73
+ }
74
+ catch {
75
+ // Unreadable record: generate again.
76
+ }
77
+ }
78
+ out(`music: generating a ${wanted.seconds}s bed — "${wanted.prompt}"`);
79
+ await options.generateMusic(wanted.prompt, wanted.seconds, musicPath);
80
+ fs.writeFileSync(recordPath, `${JSON.stringify({ ...wanted, at: new Date().toISOString() }, null, 2)}\n`);
81
+ return musicPath;
82
+ }
83
+ /**
84
+ * The shots to cut: the committed file when it belongs to this recording, else a fresh auto cut.
85
+ * A file cut from ANOTHER recording is kept beside the new one rather than overwritten — its frame
86
+ * indices mean nothing here, but the labels, order and titles are the creator's work.
87
+ */
88
+ function resolveShots(options, analysis, timeline, recordingName, beats, title) {
89
+ const file = shotsPath(options.root);
90
+ if (fs.existsSync(file) && options.resetShots !== true) {
91
+ const existing = loadShots(file);
92
+ if (existing.recording === undefined || existing.recording === recordingName) {
93
+ out(`shots: cutting ${path.relative(options.root, file)} (${existing.shots.length} shots)`);
94
+ return { shots: existing, written: false };
95
+ }
96
+ const kept = path.join(path.dirname(file), `shots.${trailerSlug(existing.recording)}.json`);
97
+ fs.copyFileSync(file, kept);
98
+ warn(`shots: ${path.relative(options.root, file)} was cut from recording "${existing.recording}", not this one — ` +
99
+ `kept it as ${path.relative(options.root, kept)} and writing a fresh first cut.`);
100
+ }
101
+ const auto = buildAutoShots({
102
+ analysis,
103
+ recordingName,
104
+ targetSeconds: options.seconds ?? DEFAULT_TRAILER_SECONDS,
105
+ beats,
106
+ title,
107
+ missingFrames: timeline.recording.missingFrames ?? [],
108
+ ...(options.sub !== undefined ? { sub: options.sub } : {}),
109
+ ...(options.ui === true ? { ui: true } : {}),
110
+ });
111
+ fs.mkdirSync(path.dirname(file), { recursive: true });
112
+ fs.writeFileSync(file, `${JSON.stringify(auto, null, 2)}\n`);
113
+ out(`shots: wrote a first cut to ${path.relative(options.root, file)} (${auto.shots.length} shots) — edit it and run make again to tune`);
114
+ return { shots: auto, written: true };
115
+ }
116
+ export async function makeTrailer(options) {
117
+ const recordingDir = options.recordingDir;
118
+ const timeline = loadTimeline(recordingDir);
119
+ const recordingName = path.basename(recordingDir);
120
+ const work = workDir(options.root, recordingName);
121
+ fs.mkdirSync(work, { recursive: true });
122
+ const seconds = options.seconds ?? DEFAULT_TRAILER_SECONDS;
123
+ const title = options.title ?? options.gameName ?? path.basename(options.root);
124
+ // 1. Analyze (cached while the recording is unchanged — contact sheets are the slow part).
125
+ const analysisPath = path.join(work, 'analysis.json');
126
+ let analysis = newerThan(analysisPath, path.join(recordingDir, 'timeline.json')) ? readAnalysis(analysisPath) : null;
127
+ if (analysis === null) {
128
+ analysis = await runAnalyze(recordingDir, 3, 24, work);
129
+ }
130
+ else {
131
+ out(`analysis: reusing ${path.relative(options.root, analysisPath)}`);
132
+ }
133
+ // 2. Music + beat grid.
134
+ const musicPath = await resolveMusic(options, work, seconds);
135
+ let beats = null;
136
+ let beatsPath;
137
+ if (musicPath !== undefined) {
138
+ beats = await analyzeBeats(musicPath);
139
+ beatsPath = path.join(work, 'beats.json');
140
+ fs.writeFileSync(beatsPath, `${JSON.stringify(beats, null, 2)}\n`);
141
+ out(`beats: ${beats.bpm.toFixed(1)} bpm, downbeat at ${beats.beatTimes[beats.downbeatIndex].toFixed(2)}s`);
142
+ }
143
+ // 3. Shots → EDL.
144
+ const { shots, written } = resolveShots(options, analysis, timeline, recordingName, beats, title);
145
+ const cut = cutShots(shots, beats, timeline.recording.fps, { frameCount: timeline.recording.frameCount });
146
+ if (cut.truncatedAt !== undefined) {
147
+ warn(`cut: the music ran out before "${cut.truncatedAt}" — the shots after it were dropped`);
148
+ }
149
+ const edlPath = path.join(work, 'edl.json');
150
+ fs.writeFileSync(edlPath, `${JSON.stringify(cut.edl, null, 2)}\n`);
151
+ out(`cut: ${describeCut(cut, beats)}`);
152
+ if (written && cut.totalSec < seconds * 0.7) {
153
+ const usable = (timeline.recording.frameCount - (timeline.recording.missingFrames?.length ?? 0)) / timeline.recording.fps;
154
+ warn(`cut: ${cut.totalSec.toFixed(1)}s for a ${seconds}s ask — the recording holds only ${usable.toFixed(1)}s of usable footage and a first cut ` +
155
+ 'never repeats a second. Record longer (about 3x the trailer length in footage) for a fuller trailer.');
156
+ }
157
+ // 4. Render.
158
+ const render = await runRender(recordingDir, {
159
+ edlPath,
160
+ outPath: path.join(work, `${trailerSlug(title)}-trailer.mp4`),
161
+ ...(options.ui === true ? { ui: true } : {}),
162
+ ...(options.harnessUrl !== undefined ? { harnessUrl: options.harnessUrl } : {}),
163
+ ...(options.uiKeyframesPerSec !== undefined ? { uiKeyframesPerSec: options.uiKeyframesPerSec } : {}),
164
+ ...(options.rasterizers !== undefined ? { rasterizers: options.rasterizers } : {}),
165
+ ...(musicPath !== undefined && beats !== null
166
+ ? {
167
+ musicPath,
168
+ musicStartSec: beats.beatTimes[beats.downbeatIndex],
169
+ musicGainDb: MAKE_MUSIC_GAIN_DB,
170
+ sfxGainDb: MAKE_SFX_GAIN_DB,
171
+ }
172
+ : {}),
173
+ });
174
+ const result = {
175
+ recordingDir,
176
+ recordingName,
177
+ workDir: work,
178
+ analysisPath,
179
+ shotsPath: shotsPath(options.root),
180
+ shotsWritten: written,
181
+ edlPath,
182
+ mp4: render.outPath,
183
+ durationSec: render.durationSec,
184
+ clipCount: render.clipCount,
185
+ };
186
+ if (musicPath !== undefined)
187
+ result.musicPath = musicPath;
188
+ if (beatsPath !== undefined)
189
+ result.beatsPath = beatsPath;
190
+ // 5. Publish.
191
+ if (options.publish !== undefined) {
192
+ result.published = await options.publish(render.outPath);
193
+ }
194
+ return result;
195
+ }
196
+ //# sourceMappingURL=make.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"make.js","sourceRoot":"","sources":["../../src/trailer/make.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export interface MergeOptions {
2
+ /** Directory to create. Default "<first>-merged" beside the first input. */
3
+ outDir?: string;
4
+ /** Allow inputs whose post-processing state differs. */
5
+ allowMixedPostFx?: boolean;
6
+ }
7
+ export declare function runMerge(dirs: string[], options?: MergeOptions): void;
8
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/trailer/merge.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAuCD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,YAAiB,GAAG,IAAI,CAuIzE"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Join several recordings into one that the rest of the toolchain treats as
3
+ * normal.
4
+ *
5
+ * A session often spans multiple sittings — one run for the rain track, another
6
+ * for the rest — and a trailer wants to cut across all of them. Frames are just
7
+ * numbered PNGs and every timeline entry is frame-indexed, so merging is
8
+ * renumbering: run N's frames continue after run N-1's, and its events, sounds,
9
+ * HUD ops and frame clock shift by the same offset.
10
+ *
11
+ * Frames are hard-linked rather than copied, so merging a 30k-frame session
12
+ * costs kilobytes and leaves the originals untouched (copy is the
13
+ * cross-filesystem fallback).
14
+ *
15
+ * The merge walks the RAW timeline JSON rather than the parsed `Timeline`: the
16
+ * parser deliberately drops `music[]` and doesn't model the newer manifest
17
+ * fields, and a merge that silently discarded data would be worse than no
18
+ * merge. `loadTimeline` still runs first, purely to reject malformed input.
19
+ *
20
+ * The guards matter more than the mechanics. Mixing recordings of different
21
+ * sizes yields a trailer that changes resolution mid-cut; mixing
22
+ * post-processed footage with the pre-fix kind (no bloom, SSR or tone mapping)
23
+ * yields one that visibly changes look mid-cut. Both are refused by default.
24
+ */
25
+ import * as fs from 'fs';
26
+ import * as path from 'path';
27
+ import { TrailerError } from './errors.js';
28
+ import { loadTimeline } from './timeline-types.js';
29
+ import { out, warn } from './io.js';
30
+ import { linkOrCopy } from './link.js';
31
+ /** Frame-indexed arrays that must shift when a run is appended. */
32
+ const FRAME_INDEXED_TRACKS = ['events', 'sounds', 'music', 'hud'];
33
+ function shiftRows(rows, offset) {
34
+ if (!Array.isArray(rows))
35
+ return [];
36
+ return rows.map((row) => {
37
+ if (typeof row !== 'object' || row === null)
38
+ return row;
39
+ const entry = row;
40
+ if (typeof entry.frame !== 'number')
41
+ return row;
42
+ return { ...entry, frame: entry.frame + offset };
43
+ });
44
+ }
45
+ export function runMerge(dirs, options = {}) {
46
+ if (dirs.length < 2)
47
+ throw new TrailerError('merge needs at least two recording dirs');
48
+ const runs = [];
49
+ let offset = 0;
50
+ for (const dir of dirs) {
51
+ const resolved = path.resolve(dir);
52
+ // The merge works from the raw JSON so unknown tracks survive untouched,
53
+ // but `window` is taken from the PARSED timeline: parseTimeline is
54
+ // deliberately tolerant there and drops a malformed block instead of
55
+ // throwing, so the raw one is not guaranteed to have the shape its type
56
+ // claims — and it gets copied into the merged manifest verbatim.
57
+ const parsed = loadTimeline(resolved);
58
+ const raw = JSON.parse(fs.readFileSync(path.join(resolved, 'timeline.json'), 'utf8'));
59
+ runs.push({ dir: resolved, raw, offset, window: parsed.recording.window });
60
+ offset += raw.recording.frameCount;
61
+ }
62
+ const totalFrames = offset;
63
+ const first = runs[0].raw.recording;
64
+ for (const run of runs.slice(1)) {
65
+ const rec = run.raw.recording;
66
+ if (rec.width !== first.width || rec.height !== first.height) {
67
+ throw new TrailerError(`resolution mismatch: ${path.basename(runs[0].dir)} is ${first.width}x${first.height}, ` +
68
+ `${path.basename(run.dir)} is ${rec.width}x${rec.height}`);
69
+ }
70
+ if (rec.fps !== first.fps) {
71
+ throw new TrailerError(`fps mismatch: ${first.fps} vs ${rec.fps} in ${path.basename(run.dir)}`);
72
+ }
73
+ }
74
+ // The merged manifest can carry only ONE recording.window, and the renderer
75
+ // lays every overlay out against it — so runs played at different window
76
+ // sizes (which the resolution guard above cannot see, since the recording
77
+ // resolution is independent of the window) would all raster at whichever
78
+ // window won. Take the first one that exists rather than run 0's blindly,
79
+ // so a pre-window run at the front does not discard the others' geometry.
80
+ const windows = runs.map((r) => r.window);
81
+ const mergedWindow = windows.find((w) => w !== undefined);
82
+ // Keyed on the dimensions alone: the raster geometry lays the HUD out from
83
+ // the window's height and the recording's aspect, never from its DPR, so two
84
+ // sittings on screens of different density raster identically.
85
+ const windowKey = (w) => w === undefined ? 'absent' : `${w.innerWidth}x${w.innerHeight}`;
86
+ const distinctWindows = Array.from(new Set(windows.map(windowKey)));
87
+ if (distinctWindows.length > 1) {
88
+ warn(`these runs were played at different window sizes (${runs
89
+ .map((r, i) => `${path.basename(r.dir)}=${windowKey(windows[i])}`)
90
+ .join(', ')}) — every overlay will be laid out at ${windowKey(mergedWindow)}, so the HUD ` +
91
+ 'will be the wrong size relative to the footage for the other runs');
92
+ }
93
+ // Footage recorded before the post-FX fix has no bloom/SSR/tone mapping and
94
+ // is visibly flatter; cutting it against fixed footage looks like a bug.
95
+ const postFx = runs.map((r) => r.raw.recording.postProcessing ?? 'unknown');
96
+ const distinct = Array.from(new Set(postFx));
97
+ if (distinct.length > 1) {
98
+ const detail = runs.map((r, i) => `${path.basename(r.dir)}=${postFx[i]}`).join(', ');
99
+ if (options.allowMixedPostFx !== true) {
100
+ throw new TrailerError(`post-processing mismatch (${detail}). These will not match visually — re-record ` +
101
+ `the odd one out, or pass --allow-mixed-postfx to override.`);
102
+ }
103
+ warn(`merging mismatched post-processing (${detail}) — the cut will change look between runs`);
104
+ }
105
+ else if (distinct[0] === 'unknown') {
106
+ warn('none of these recordings state their post-processing (recorded before that was tracked)');
107
+ }
108
+ const outDir = path.resolve(options.outDir ?? `${runs[0].dir}-merged`);
109
+ const name = path.basename(outDir);
110
+ fs.mkdirSync(outDir, { recursive: true });
111
+ let linked = 0;
112
+ const absent = [];
113
+ for (const run of runs) {
114
+ const srcName = run.raw.recording.name;
115
+ for (let i = 0; i < run.raw.recording.frameCount; i++) {
116
+ const src = path.join(run.dir, `${srcName}_${String(i).padStart(6, '0')}.png`);
117
+ if (!fs.existsSync(src)) {
118
+ absent.push(run.offset + i);
119
+ continue;
120
+ }
121
+ const dst = path.join(outDir, `${name}_${String(run.offset + i).padStart(6, '0')}.png`);
122
+ if (!fs.existsSync(dst))
123
+ linkOrCopy(src, dst);
124
+ linked++;
125
+ }
126
+ }
127
+ const merged = {
128
+ ...runs[0].raw,
129
+ version: 1,
130
+ recording: {
131
+ ...first,
132
+ // Always authoritative, never merely additive: `first` is the RAW block,
133
+ // so a window that failed validation would otherwise ride the spread
134
+ // straight into the merged manifest. undefined is dropped by
135
+ // JSON.stringify, which is exactly the "no window" encoding.
136
+ window: mergedWindow,
137
+ name,
138
+ frameCount: totalFrames,
139
+ missingFrames: [
140
+ ...runs.flatMap((r) => (r.raw.recording.missingFrames ?? []).map((f) => f + r.offset)),
141
+ ...absent,
142
+ ].sort((a, b) => a - b),
143
+ frameClock: runs.flatMap((r) => (r.raw.recording.frameClock ?? []).map((s) => ({ ...s, frame: s.frame + r.offset }))),
144
+ // Where each source run begins, so a cut can avoid straddling a join.
145
+ mergedFrom: runs.map((r) => ({
146
+ name: r.raw.recording.name,
147
+ startFrame: r.offset,
148
+ frameCount: r.raw.recording.frameCount,
149
+ })),
150
+ },
151
+ };
152
+ for (const track of FRAME_INDEXED_TRACKS) {
153
+ merged[track] = runs.flatMap((r) => shiftRows(r.raw[track], r.offset));
154
+ }
155
+ fs.writeFileSync(path.join(outDir, 'timeline.json'), JSON.stringify(merged, null, 2));
156
+ const counts = FRAME_INDEXED_TRACKS.map((t) => `${t}: ${merged[t].length}`).join(', ');
157
+ const joins = runs.slice(1).map((r) => String(r.offset)).join(', ');
158
+ out(`merged ${runs.length} recordings -> ${outDir} | ${totalFrames} frames ` +
159
+ `(${(totalFrames / first.fps).toFixed(1)}s) | ${linked} frames linked | ${counts}` +
160
+ (joins ? ` | joins at frame ${joins}` : ''));
161
+ if (absent.length > 0) {
162
+ warn(`${absent.length} source frame(s) were absent and are listed in missingFrames`);
163
+ }
164
+ }
165
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../../src/trailer/merge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /** Analysis decode rate: covers kick through hat, and keeps the FFTs cheap. */
2
+ export declare const BEAT_SAMPLE_RATE = 22050;
3
+ export declare const FRAME_SIZE = 1024;
4
+ export declare const HOP_SIZE = 512;
5
+ export declare const BEATS_PER_BAR = 4;
6
+ export interface BeatAnalysis {
7
+ /** Detected tempo, or the forced `opts.bpm`. */
8
+ bpm: number;
9
+ /** 0..1 — how strongly the beat grid matches the onset envelope. */
10
+ confidence: number;
11
+ /** Seconds, ascending, from the first beat to the end of the track. */
12
+ beatTimes: number[];
13
+ /** Every 4th beat starting at the detected downbeat. */
14
+ barTimes: number[];
15
+ /** Index into beatTimes of the first downbeat (0..3). */
16
+ downbeatIndex: number;
17
+ durationSec: number;
18
+ /** RMS in dBFS per bar — the structure hint for build/drop. */
19
+ energyPerBar: number[];
20
+ }
21
+ export interface BeatOptions {
22
+ /** Force the tempo (skips detection); the phase is still detected. */
23
+ bpm?: number;
24
+ /** Decode rate for analysis. Default BEAT_SAMPLE_RATE. */
25
+ sampleRate?: number;
26
+ }
27
+ /**
28
+ * Iterative in-place radix-2 Cooley-Tukey FFT. Power-of-two lengths only —
29
+ * the frame size is ours to choose, so there is no mixed-radix fallback.
30
+ */
31
+ export declare function fftInPlace(re: Float64Array, im: Float64Array): void;
32
+ /** Magnitude spectrum of a real frame: bins 0..n/2 inclusive. */
33
+ export declare function fftMagnitudes(frame: ArrayLike<number>): Float64Array;
34
+ export interface OnsetEnvelope {
35
+ /** Baseline-subtracted, half-wave-rectified spectral flux, one value per hop. */
36
+ strength: Float64Array;
37
+ /** The same measure over bins under LOW_BAND_HZ only (kick band). */
38
+ low: Float64Array;
39
+ /** Seconds per hop. Frame t is centred on t * hopSec. */
40
+ hopSec: number;
41
+ }
42
+ /**
43
+ * Spectral-flux onset envelope. Frames are centred (the signal is treated as
44
+ * zero-padded by frameSize/2 on the left) so frame t sits exactly on
45
+ * t * hopSec — the beat grid is built in that time base.
46
+ */
47
+ export declare function computeOnsetEnvelope(samples: Float32Array, sampleRate: number, frameSize?: number, hopSize?: number): OnsetEnvelope;
48
+ export interface TempoEstimate {
49
+ /** Tempo after octave correction. */
50
+ bpm: number;
51
+ /** Autocorrelation score of the chosen lag. */
52
+ score: number;
53
+ /** The raw autocorrelation peak, before octave correction. */
54
+ rawBpm: number;
55
+ }
56
+ /**
57
+ * Tempo from the onset envelope: autocorrelation peak over 60-200 BPM, refined
58
+ * to a fractional lag, then octave-corrected. The refinement matters — at a
59
+ * 23ms hop the integer lags around 120 BPM are 117.4 and 123.0 BPM, so an
60
+ * integer-lag answer alone cannot be within 1 BPM of the truth.
61
+ */
62
+ export declare function detectTempo(env: OnsetEnvelope): TempoEstimate;
63
+ /**
64
+ * With the period fixed, the offset within one period whose grid collects the
65
+ * most onset strength. Offsets are hop-sized; refineGrid takes it from there.
66
+ */
67
+ export declare function detectPhase(env: OnsetEnvelope, periodSec: number, durationSec: number): number;
68
+ /**
69
+ * Pull the grid onto the onsets it found. Each beat contributes the
70
+ * strength-weighted centroid of the envelope within a quarter beat of it, and
71
+ * a weighted least-squares line through those centroids gives period+phase at
72
+ * sub-hop resolution.
73
+ *
74
+ * This is not a nicety. The autocorrelation peak around the beat lag is a
75
+ * near-flat trapezoid — one beat of a 120 BPM track is 21.53 frames, and both
76
+ * lag 21 (123.0 BPM) and lag 22 (117.4 BPM) score about the same — so the
77
+ * tempo alone is only good to a couple of percent, and no hop-sized phase can
78
+ * be better than +-12ms.
79
+ *
80
+ * `lockPeriod` keeps a forced --bpm exactly as given and refines only the phase.
81
+ */
82
+ export declare function refineGrid(env: OnsetEnvelope, periodSec: number, phaseSec: number, durationSec: number, lockPeriod: boolean): {
83
+ periodSec: number;
84
+ phaseSec: number;
85
+ };
86
+ /**
87
+ * Of the four candidate downbeat phases, the one whose beats carry the most
88
+ * kick-band onset energy. When the low band is flat (no drums, or a mix with
89
+ * nothing under 150Hz) the four scores are meaningless, so it falls back to
90
+ * total onset strength.
91
+ */
92
+ export declare function pickDownbeat(env: OnsetEnvelope, beatTimes: number[]): number;
93
+ /** The whole pipeline over decoded mono samples — no ffmpeg, no filesystem. */
94
+ export declare function analyzeBeatsFromSamples(samples: Float32Array, sampleRate: number, opts?: BeatOptions): BeatAnalysis;
95
+ /**
96
+ * Playable seconds in an audio file. Asks for the audio stream's own duration
97
+ * first and falls back to the container's; throws if neither is a number.
98
+ */
99
+ export declare function probeAudioDurationSec(filePath: string): Promise<number>;
100
+ /**
101
+ * Decode any audio file to mono float samples at `sampleRate`.
102
+ *
103
+ * `-vn` plus an explicit `-map 0:a:0` matter: music files routinely carry
104
+ * embedded cover art as a video stream, and without both, ffmpeg tries to push
105
+ * that image into a PCM stream and fails.
106
+ */
107
+ export declare function decodeMonoPcm(audioPath: string, sampleRate: number): Promise<Float32Array>;
108
+ export declare function analyzeBeats(audioPath: string, opts?: BeatOptions): Promise<BeatAnalysis>;
109
+ /**
110
+ * The music with a click burst on every beat (downbeats an octave-ish up), as
111
+ * a 48kHz mono WAV. The grid is only verifiable by ear, so this file is the
112
+ * point of the whole `beats` command.
113
+ */
114
+ export declare function renderClickTrack(audioPath: string, analysis: BeatAnalysis): Promise<Buffer>;
115
+ export interface BeatsCommandOptions {
116
+ /** Where to write the BeatAnalysis JSON. */
117
+ jsonPath?: string;
118
+ /** Force the tempo instead of detecting it. */
119
+ bpm?: number;
120
+ /** Where to write the music+click verification WAV. */
121
+ clickPath?: string;
122
+ }
123
+ export declare function runBeats(audioPath: string, options?: BeatsCommandOptions): Promise<void>;
124
+ //# sourceMappingURL=music-beats.d.ts.map