@bitmagic/cli 0.1.60 → 0.1.61-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.
- package/README.md +47 -8
- package/assets/fonts/NotoSans-Bold.ttf +0 -0
- package/assets/fonts/OFL.txt +94 -0
- package/dist/chrome-launch.d.ts +18 -0
- package/dist/chrome-launch.js +24 -0
- package/dist/chrome-launch.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/generate.d.ts +44 -0
- package/dist/commands/generate.js +143 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/trailer-publish.d.ts +15 -0
- package/dist/commands/trailer-publish.js +45 -0
- package/dist/commands/trailer-publish.js.map +1 -0
- package/dist/commands/trailer.d.ts +3 -0
- package/dist/commands/trailer.js +396 -0
- package/dist/commands/trailer.js.map +1 -0
- package/dist/editor/hud-replay-page.d.ts +7 -0
- package/dist/editor/hud-replay-page.js +219 -0
- package/dist/editor/hud-replay-page.js.map +1 -0
- package/dist/editor/recording-panel.d.ts +3 -1
- package/dist/editor/recording-panel.js +6 -2
- package/dist/editor/recording-panel.js.map +1 -1
- package/dist/editor/recording-sink.d.ts +3 -20
- package/dist/editor/recording-sink.js +14 -35
- package/dist/editor/recording-sink.js.map +1 -1
- package/dist/editor/server.js +15 -18
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.d.ts +0 -8
- package/dist/editor/shell-page.js +23 -19
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/generate/music.d.ts +51 -0
- package/dist/generate/music.js +93 -0
- package/dist/generate/music.js.map +1 -0
- package/dist/generate/stream.d.ts +13 -0
- package/dist/generate/stream.js +17 -1
- package/dist/generate/stream.js.map +1 -1
- package/dist/project/jobs.d.ts +1 -1
- package/dist/scaffold/project-files.d.ts +0 -1
- package/dist/scaffold/project-files.js +6 -33
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.js +3 -1
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/skill-files.d.ts +22 -0
- package/dist/scaffold/skill-files.js +198 -0
- package/dist/scaffold/skill-files.js.map +1 -0
- package/dist/trailer/analyze.d.ts +23 -0
- package/dist/trailer/analyze.js +186 -0
- package/dist/trailer/analyze.js.map +1 -0
- package/dist/trailer/auto-edl.d.ts +46 -0
- package/dist/trailer/auto-edl.js +143 -0
- package/dist/trailer/auto-edl.js.map +1 -0
- package/dist/trailer/beat-cut.d.ts +57 -0
- package/dist/trailer/beat-cut.js +232 -0
- package/dist/trailer/beat-cut.js.map +1 -0
- package/dist/trailer/client.d.ts +52 -0
- package/dist/trailer/client.js +133 -0
- package/dist/trailer/client.js.map +1 -0
- package/dist/trailer/edl-time.d.ts +61 -0
- package/dist/trailer/edl-time.js +105 -0
- package/dist/trailer/edl-time.js.map +1 -0
- package/dist/trailer/ffmpeg.d.ts +8 -0
- package/dist/trailer/ffmpeg.js +55 -0
- package/dist/trailer/ffmpeg.js.map +1 -0
- package/dist/trailer/hud-raster.d.ts +28 -0
- package/dist/trailer/hud-raster.js +114 -0
- package/dist/trailer/hud-raster.js.map +1 -0
- package/dist/trailer/hud-state.d.ts +110 -0
- package/dist/trailer/hud-state.js +410 -0
- package/dist/trailer/hud-state.js.map +1 -0
- package/dist/trailer/io.d.ts +20 -0
- package/dist/trailer/io.js +15 -0
- package/dist/trailer/io.js.map +1 -0
- package/dist/trailer/make.d.ts +54 -0
- package/dist/trailer/make.js +205 -0
- package/dist/trailer/make.js.map +1 -0
- package/dist/trailer/merge.d.ts +7 -0
- package/dist/trailer/merge.js +144 -0
- package/dist/trailer/merge.js.map +1 -0
- package/dist/trailer/music-beats.d.ts +123 -0
- package/dist/trailer/music-beats.js +707 -0
- package/dist/trailer/music-beats.js.map +1 -0
- package/dist/trailer/paths.d.ts +28 -0
- package/dist/trailer/paths.js +101 -0
- package/dist/trailer/paths.js.map +1 -0
- package/dist/trailer/publish-record.d.ts +17 -0
- package/dist/trailer/publish-record.js +59 -0
- package/dist/trailer/publish-record.js.map +1 -0
- package/dist/trailer/publish.d.ts +45 -0
- package/dist/trailer/publish.js +149 -0
- package/dist/trailer/publish.js.map +1 -0
- package/dist/trailer/record-session.d.ts +72 -0
- package/dist/trailer/record-session.js +328 -0
- package/dist/trailer/record-session.js.map +1 -0
- package/dist/trailer/render.d.ts +57 -0
- package/dist/trailer/render.js +475 -0
- package/dist/trailer/render.js.map +1 -0
- package/dist/trailer/revision.d.ts +8 -0
- package/dist/trailer/revision.js +31 -0
- package/dist/trailer/revision.js.map +1 -0
- package/dist/trailer/synth.d.ts +9 -0
- package/dist/trailer/synth.js +120 -0
- package/dist/trailer/synth.js.map +1 -0
- package/dist/trailer/timeline-types.d.ts +176 -0
- package/dist/trailer/timeline-types.js +379 -0
- package/dist/trailer/timeline-types.js.map +1 -0
- package/dist/trailer/title-raster.d.ts +6 -0
- package/dist/trailer/title-raster.js +47 -0
- package/dist/trailer/title-raster.js.map +1 -0
- package/dist/trailer/titles.d.ts +44 -0
- package/dist/trailer/titles.js +119 -0
- package/dist/trailer/titles.js.map +1 -0
- package/dist/trailer/video-chain.d.ts +41 -0
- package/dist/trailer/video-chain.js +78 -0
- package/dist/trailer/video-chain.js.map +1 -0
- package/dist/verify/browser.js +12 -19
- package/dist/verify/browser.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,205 @@
|
|
|
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 { CliError } from '../errors.js';
|
|
18
|
+
import { aliasSourceDir } from '../scaffold/aliases.js';
|
|
19
|
+
import { runAnalyze } from './analyze.js';
|
|
20
|
+
import { buildAutoShots } from './auto-edl.js';
|
|
21
|
+
import { cutShots, describeCut, loadShots } from './beat-cut.js';
|
|
22
|
+
import { out, warn } from './io.js';
|
|
23
|
+
import { analyzeBeats } from './music-beats.js';
|
|
24
|
+
import { shotsPath, trailerSlug, workDir } from './paths.js';
|
|
25
|
+
import { runRender } from './render.js';
|
|
26
|
+
import { loadTimeline } from './timeline-types.js';
|
|
27
|
+
export const DEFAULT_TRAILER_SECONDS = 30;
|
|
28
|
+
/** Music runs a little past the cut so the fade-out has material. */
|
|
29
|
+
export const MUSIC_TAIL_SEC = 4;
|
|
30
|
+
/** The render's music/SFX balance the make-trailer skill settled on. */
|
|
31
|
+
export const MAKE_MUSIC_GAIN_DB = -4;
|
|
32
|
+
export const MAKE_SFX_GAIN_DB = -3;
|
|
33
|
+
/** The game's name as the work copy knows it, for the end card and the file name. */
|
|
34
|
+
export function gameNameFor(root) {
|
|
35
|
+
const gameJson = path.join(root, aliasSourceDir('work'), 'game.json');
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(fs.readFileSync(gameJson, 'utf-8'));
|
|
38
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
39
|
+
const name = parsed.name;
|
|
40
|
+
if (typeof name === 'string' && name.trim().length > 0)
|
|
41
|
+
return name.trim();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// No work copy or no name in it — the directory name is the honest fallback.
|
|
46
|
+
}
|
|
47
|
+
return path.basename(root);
|
|
48
|
+
}
|
|
49
|
+
function newerThan(a, b) {
|
|
50
|
+
return fs.existsSync(a) && fs.existsSync(b) && fs.statSync(a).mtimeMs >= fs.statSync(b).mtimeMs;
|
|
51
|
+
}
|
|
52
|
+
function readAnalysis(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
55
|
+
if (typeof parsed === 'object' && parsed !== null && Array.isArray(parsed.windows)) {
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Fall through: re-analyze.
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The music bed: a file the creator supplied, a bed generated from a prompt (reused while the
|
|
66
|
+
* prompt is unchanged), or none.
|
|
67
|
+
*/
|
|
68
|
+
async function resolveMusic(options, work, seconds) {
|
|
69
|
+
if (options.music !== undefined) {
|
|
70
|
+
const resolved = path.resolve(options.music);
|
|
71
|
+
if (!fs.existsSync(resolved))
|
|
72
|
+
throw new CliError(`Music file not found: ${resolved}`);
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
75
|
+
if (options.musicPrompt === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (options.generateMusic === undefined) {
|
|
78
|
+
throw new CliError('Music generation is not available in this build — pass --music <file> instead.');
|
|
79
|
+
}
|
|
80
|
+
const musicPath = path.join(work, 'music.mp3');
|
|
81
|
+
const recordPath = path.join(work, 'music.json');
|
|
82
|
+
const wanted = { prompt: options.musicPrompt, seconds: seconds + MUSIC_TAIL_SEC, at: '' };
|
|
83
|
+
if (fs.existsSync(musicPath) && fs.existsSync(recordPath)) {
|
|
84
|
+
try {
|
|
85
|
+
const previous = JSON.parse(fs.readFileSync(recordPath, 'utf-8'));
|
|
86
|
+
if (previous.prompt === wanted.prompt && previous.seconds === wanted.seconds) {
|
|
87
|
+
out(`music: reusing the bed generated earlier for this prompt (${path.basename(musicPath)})`);
|
|
88
|
+
return musicPath;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Unreadable record: generate again.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
out(`music: generating a ${wanted.seconds}s bed — "${wanted.prompt}"`);
|
|
96
|
+
await options.generateMusic(wanted.prompt, wanted.seconds, musicPath);
|
|
97
|
+
fs.writeFileSync(recordPath, `${JSON.stringify({ ...wanted, at: new Date().toISOString() }, null, 2)}\n`);
|
|
98
|
+
return musicPath;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The shots to cut: the committed file when it belongs to this recording, else a fresh auto cut.
|
|
102
|
+
* A file cut from ANOTHER recording is kept beside the new one rather than overwritten — its frame
|
|
103
|
+
* indices mean nothing here, but the labels, order and titles are the creator's work.
|
|
104
|
+
*/
|
|
105
|
+
function resolveShots(options, analysis, recordingName, beats, title) {
|
|
106
|
+
const file = shotsPath(options.root);
|
|
107
|
+
if (fs.existsSync(file) && options.resetShots !== true) {
|
|
108
|
+
const existing = loadShots(file);
|
|
109
|
+
if (existing.recording === undefined || existing.recording === recordingName) {
|
|
110
|
+
out(`shots: cutting ${path.relative(options.root, file)} (${existing.shots.length} shots)`);
|
|
111
|
+
return { shots: existing, written: false };
|
|
112
|
+
}
|
|
113
|
+
const kept = path.join(path.dirname(file), `shots.${trailerSlug(existing.recording)}.json`);
|
|
114
|
+
fs.copyFileSync(file, kept);
|
|
115
|
+
warn(`shots: ${path.relative(options.root, file)} was cut from recording "${existing.recording}", not this one — ` +
|
|
116
|
+
`kept it as ${path.relative(options.root, kept)} and writing a fresh first cut.`);
|
|
117
|
+
}
|
|
118
|
+
const auto = buildAutoShots({
|
|
119
|
+
analysis,
|
|
120
|
+
recordingName,
|
|
121
|
+
targetSeconds: options.seconds ?? DEFAULT_TRAILER_SECONDS,
|
|
122
|
+
beats,
|
|
123
|
+
title,
|
|
124
|
+
...(options.sub !== undefined ? { sub: options.sub } : {}),
|
|
125
|
+
...(options.ui === true ? { ui: true } : {}),
|
|
126
|
+
});
|
|
127
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
128
|
+
fs.writeFileSync(file, `${JSON.stringify(auto, null, 2)}\n`);
|
|
129
|
+
out(`shots: wrote a first cut to ${path.relative(options.root, file)} (${auto.shots.length} shots) — edit it and run make again to tune`);
|
|
130
|
+
return { shots: auto, written: true };
|
|
131
|
+
}
|
|
132
|
+
export async function makeTrailer(options) {
|
|
133
|
+
const recordingDir = options.recordingDir;
|
|
134
|
+
const timeline = loadTimeline(recordingDir);
|
|
135
|
+
const recordingName = path.basename(recordingDir);
|
|
136
|
+
const work = workDir(options.root, recordingName);
|
|
137
|
+
fs.mkdirSync(work, { recursive: true });
|
|
138
|
+
const seconds = options.seconds ?? DEFAULT_TRAILER_SECONDS;
|
|
139
|
+
const title = options.title ?? gameNameFor(options.root);
|
|
140
|
+
// 1. Analyze (cached while the recording is unchanged — contact sheets are the slow part).
|
|
141
|
+
const analysisPath = path.join(work, 'analysis.json');
|
|
142
|
+
let analysis = newerThan(analysisPath, path.join(recordingDir, 'timeline.json')) ? readAnalysis(analysisPath) : null;
|
|
143
|
+
if (analysis === null) {
|
|
144
|
+
analysis = await runAnalyze(recordingDir, 3, 24, work);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
out(`analysis: reusing ${path.relative(options.root, analysisPath)}`);
|
|
148
|
+
}
|
|
149
|
+
// 2. Music + beat grid.
|
|
150
|
+
const musicPath = await resolveMusic(options, work, seconds);
|
|
151
|
+
let beats = null;
|
|
152
|
+
let beatsPath;
|
|
153
|
+
if (musicPath !== undefined) {
|
|
154
|
+
beats = await analyzeBeats(musicPath);
|
|
155
|
+
beatsPath = path.join(work, 'beats.json');
|
|
156
|
+
fs.writeFileSync(beatsPath, `${JSON.stringify(beats, null, 2)}\n`);
|
|
157
|
+
out(`beats: ${beats.bpm.toFixed(1)} bpm, downbeat at ${beats.beatTimes[beats.downbeatIndex].toFixed(2)}s`);
|
|
158
|
+
}
|
|
159
|
+
// 3. Shots → EDL.
|
|
160
|
+
const { shots, written } = resolveShots(options, analysis, recordingName, beats, title);
|
|
161
|
+
const cut = cutShots(shots, beats, timeline.recording.fps, { frameCount: timeline.recording.frameCount });
|
|
162
|
+
if (cut.truncatedAt !== undefined) {
|
|
163
|
+
warn(`cut: the music ran out before "${cut.truncatedAt}" — the shots after it were dropped`);
|
|
164
|
+
}
|
|
165
|
+
const edlPath = path.join(work, 'edl.json');
|
|
166
|
+
fs.writeFileSync(edlPath, `${JSON.stringify(cut.edl, null, 2)}\n`);
|
|
167
|
+
out(`cut: ${describeCut(cut, beats)}`);
|
|
168
|
+
// 4. Render.
|
|
169
|
+
const render = await runRender(recordingDir, {
|
|
170
|
+
edlPath,
|
|
171
|
+
outPath: path.join(work, `${trailerSlug(title)}-trailer.mp4`),
|
|
172
|
+
...(options.ui === true ? { ui: true } : {}),
|
|
173
|
+
...(options.harnessUrl !== undefined ? { harnessUrl: options.harnessUrl } : {}),
|
|
174
|
+
...(musicPath !== undefined && beats !== null
|
|
175
|
+
? {
|
|
176
|
+
musicPath,
|
|
177
|
+
musicStartSec: beats.beatTimes[beats.downbeatIndex],
|
|
178
|
+
musicGainDb: MAKE_MUSIC_GAIN_DB,
|
|
179
|
+
sfxGainDb: MAKE_SFX_GAIN_DB,
|
|
180
|
+
}
|
|
181
|
+
: {}),
|
|
182
|
+
});
|
|
183
|
+
const result = {
|
|
184
|
+
recordingDir,
|
|
185
|
+
recordingName,
|
|
186
|
+
workDir: work,
|
|
187
|
+
analysisPath,
|
|
188
|
+
shotsPath: shotsPath(options.root),
|
|
189
|
+
shotsWritten: written,
|
|
190
|
+
edlPath,
|
|
191
|
+
mp4: render.outPath,
|
|
192
|
+
durationSec: render.durationSec,
|
|
193
|
+
clipCount: render.clipCount,
|
|
194
|
+
};
|
|
195
|
+
if (musicPath !== undefined)
|
|
196
|
+
result.musicPath = musicPath;
|
|
197
|
+
if (beatsPath !== undefined)
|
|
198
|
+
result.beatsPath = beatsPath;
|
|
199
|
+
// 5. Publish.
|
|
200
|
+
if (options.publish !== undefined) {
|
|
201
|
+
result.published = await options.publish(render.outPath);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
//# 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,7 @@
|
|
|
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;
|
|
@@ -0,0 +1,144 @@
|
|
|
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 { CliError } from '../errors.js';
|
|
28
|
+
import { loadTimeline } from './timeline-types.js';
|
|
29
|
+
import { out, warn } from './io.js';
|
|
30
|
+
/** Frame-indexed arrays that must shift when a run is appended. */
|
|
31
|
+
const FRAME_INDEXED_TRACKS = ['events', 'sounds', 'music', 'hud'];
|
|
32
|
+
function shiftRows(rows, offset) {
|
|
33
|
+
if (!Array.isArray(rows))
|
|
34
|
+
return [];
|
|
35
|
+
return rows.map((row) => {
|
|
36
|
+
if (typeof row !== 'object' || row === null)
|
|
37
|
+
return row;
|
|
38
|
+
const entry = row;
|
|
39
|
+
if (typeof entry.frame !== 'number')
|
|
40
|
+
return row;
|
|
41
|
+
return { ...entry, frame: entry.frame + offset };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Hard-link when possible; copying is the cross-filesystem fallback. */
|
|
45
|
+
function linkOrCopy(from, to) {
|
|
46
|
+
try {
|
|
47
|
+
fs.linkSync(from, to);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
fs.copyFileSync(from, to);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function runMerge(dirs, options = {}) {
|
|
54
|
+
if (dirs.length < 2)
|
|
55
|
+
throw new CliError('merge needs at least two recording dirs');
|
|
56
|
+
const runs = [];
|
|
57
|
+
let offset = 0;
|
|
58
|
+
for (const dir of dirs) {
|
|
59
|
+
const resolved = path.resolve(dir);
|
|
60
|
+
loadTimeline(resolved); // validation only — the merge uses the raw JSON
|
|
61
|
+
const raw = JSON.parse(fs.readFileSync(path.join(resolved, 'timeline.json'), 'utf8'));
|
|
62
|
+
runs.push({ dir: resolved, raw, offset });
|
|
63
|
+
offset += raw.recording.frameCount;
|
|
64
|
+
}
|
|
65
|
+
const totalFrames = offset;
|
|
66
|
+
const first = runs[0].raw.recording;
|
|
67
|
+
for (const run of runs.slice(1)) {
|
|
68
|
+
const rec = run.raw.recording;
|
|
69
|
+
if (rec.width !== first.width || rec.height !== first.height) {
|
|
70
|
+
throw new CliError(`resolution mismatch: ${path.basename(runs[0].dir)} is ${first.width}x${first.height}, ` +
|
|
71
|
+
`${path.basename(run.dir)} is ${rec.width}x${rec.height}`);
|
|
72
|
+
}
|
|
73
|
+
if (rec.fps !== first.fps) {
|
|
74
|
+
throw new CliError(`fps mismatch: ${first.fps} vs ${rec.fps} in ${path.basename(run.dir)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Footage recorded before the post-FX fix has no bloom/SSR/tone mapping and
|
|
78
|
+
// is visibly flatter; cutting it against fixed footage looks like a bug.
|
|
79
|
+
const postFx = runs.map((r) => r.raw.recording.postProcessing ?? 'unknown');
|
|
80
|
+
const distinct = Array.from(new Set(postFx));
|
|
81
|
+
if (distinct.length > 1) {
|
|
82
|
+
const detail = runs.map((r, i) => `${path.basename(r.dir)}=${postFx[i]}`).join(', ');
|
|
83
|
+
if (options.allowMixedPostFx !== true) {
|
|
84
|
+
throw new CliError(`post-processing mismatch (${detail}). These will not match visually — re-record ` +
|
|
85
|
+
`the odd one out, or pass --allow-mixed-postfx to override.`);
|
|
86
|
+
}
|
|
87
|
+
warn(`merging mismatched post-processing (${detail}) — the cut will change look between runs`);
|
|
88
|
+
}
|
|
89
|
+
else if (distinct[0] === 'unknown') {
|
|
90
|
+
warn('none of these recordings state their post-processing (recorded before that was tracked)');
|
|
91
|
+
}
|
|
92
|
+
const outDir = path.resolve(options.outDir ?? `${runs[0].dir}-merged`);
|
|
93
|
+
const name = path.basename(outDir);
|
|
94
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
95
|
+
let linked = 0;
|
|
96
|
+
const absent = [];
|
|
97
|
+
for (const run of runs) {
|
|
98
|
+
const srcName = run.raw.recording.name;
|
|
99
|
+
for (let i = 0; i < run.raw.recording.frameCount; i++) {
|
|
100
|
+
const src = path.join(run.dir, `${srcName}_${String(i).padStart(6, '0')}.png`);
|
|
101
|
+
if (!fs.existsSync(src)) {
|
|
102
|
+
absent.push(run.offset + i);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const dst = path.join(outDir, `${name}_${String(run.offset + i).padStart(6, '0')}.png`);
|
|
106
|
+
if (!fs.existsSync(dst))
|
|
107
|
+
linkOrCopy(src, dst);
|
|
108
|
+
linked++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const merged = {
|
|
112
|
+
...runs[0].raw,
|
|
113
|
+
version: 1,
|
|
114
|
+
recording: {
|
|
115
|
+
...first,
|
|
116
|
+
name,
|
|
117
|
+
frameCount: totalFrames,
|
|
118
|
+
missingFrames: [
|
|
119
|
+
...runs.flatMap((r) => (r.raw.recording.missingFrames ?? []).map((f) => f + r.offset)),
|
|
120
|
+
...absent,
|
|
121
|
+
].sort((a, b) => a - b),
|
|
122
|
+
frameClock: runs.flatMap((r) => (r.raw.recording.frameClock ?? []).map((s) => ({ ...s, frame: s.frame + r.offset }))),
|
|
123
|
+
// Where each source run begins, so a cut can avoid straddling a join.
|
|
124
|
+
mergedFrom: runs.map((r) => ({
|
|
125
|
+
name: r.raw.recording.name,
|
|
126
|
+
startFrame: r.offset,
|
|
127
|
+
frameCount: r.raw.recording.frameCount,
|
|
128
|
+
})),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
for (const track of FRAME_INDEXED_TRACKS) {
|
|
132
|
+
merged[track] = runs.flatMap((r) => shiftRows(r.raw[track], r.offset));
|
|
133
|
+
}
|
|
134
|
+
fs.writeFileSync(path.join(outDir, 'timeline.json'), JSON.stringify(merged, null, 2));
|
|
135
|
+
const counts = FRAME_INDEXED_TRACKS.map((t) => `${t}: ${merged[t].length}`).join(', ');
|
|
136
|
+
const joins = runs.slice(1).map((r) => String(r.offset)).join(', ');
|
|
137
|
+
out(`merged ${runs.length} recordings -> ${outDir} | ${totalFrames} frames ` +
|
|
138
|
+
`(${(totalFrames / first.fps).toFixed(1)}s) | ${linked} frames linked | ${counts}` +
|
|
139
|
+
(joins ? ` | joins at frame ${joins}` : ''));
|
|
140
|
+
if (absent.length > 0) {
|
|
141
|
+
warn(`${absent.length} source frame(s) were absent and are listed in missingFrames`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# 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,123 @@
|
|
|
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>;
|