@kheleshikhi/studio-agent 0.8.0 → 0.9.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 +54 -0
- package/dist/attempt.d.ts +4 -0
- package/dist/attempt.js +11 -1
- package/dist/attempt.js.map +1 -1
- package/dist/browser.d.ts +18 -4
- package/dist/browser.js +84 -22
- package/dist/browser.js.map +1 -1
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +429 -97
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +72 -8
- package/dist/daemon.js.map +1 -1
- package/dist/episode-runner.d.ts +109 -0
- package/dist/episode-runner.js +381 -0
- package/dist/episode-runner.js.map +1 -0
- package/dist/episode.d.ts +332 -0
- package/dist/episode.js +648 -0
- package/dist/episode.js.map +1 -0
- package/dist/project.d.ts +30 -0
- package/dist/project.js +204 -0
- package/dist/project.js.map +1 -0
- package/dist/render-jobs.d.ts +101 -0
- package/dist/render-jobs.js +241 -0
- package/dist/render-jobs.js.map +1 -0
- package/dist/shot-provenance.d.ts +66 -0
- package/dist/shot-provenance.js +114 -0
- package/dist/shot-provenance.js.map +1 -0
- package/dist/state.js +10 -6
- package/dist/state.js.map +1 -1
- package/dist/video.d.ts +38 -5
- package/dist/video.js +73 -12
- package/dist/video.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { type RenderAction, type RenderProfile, type RenderRecipe, type RenderSubject } from "./video.js";
|
|
2
|
+
export declare const EPISODE_SCHEMA = "kheleshikhi.episode.v1";
|
|
3
|
+
export declare const EPISODE_MANIFEST_KIND = "kheleshikhi.episode-render.v1";
|
|
4
|
+
export declare const SHOT_CACHE_KIND = "kheleshikhi.episode-shot-cache.v1";
|
|
5
|
+
/** Bump when the shot render or its encode changes meaning: every key moves. */
|
|
6
|
+
export declare const SHOT_KEY_VERSION = 1;
|
|
7
|
+
export declare const EPISODE_LIMITS: {
|
|
8
|
+
maxShots: number;
|
|
9
|
+
maxSeconds: number;
|
|
10
|
+
maxAudioItems: number;
|
|
11
|
+
maxCues: number;
|
|
12
|
+
maxText: number;
|
|
13
|
+
};
|
|
14
|
+
export type Caption = {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
text: string;
|
|
18
|
+
};
|
|
19
|
+
export type EpisodeShot = {
|
|
20
|
+
id: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
subject: RenderSubject;
|
|
23
|
+
/** Scene shots: a Studio asset; `local` renders the workspace checkout, `approved` the asset's approved source. */
|
|
24
|
+
scene?: {
|
|
25
|
+
asset: string;
|
|
26
|
+
source: "local" | "approved";
|
|
27
|
+
revision?: string;
|
|
28
|
+
};
|
|
29
|
+
/** Experience shots: a stable id; `local` renders the workspace source, `saved` the saved head. */
|
|
30
|
+
experience?: {
|
|
31
|
+
id: string;
|
|
32
|
+
source: "local" | "saved";
|
|
33
|
+
};
|
|
34
|
+
/** The subject's own clock second the shot starts at. */
|
|
35
|
+
start: number;
|
|
36
|
+
/** Seconds of video: a whole number of frames at the episode fps. */
|
|
37
|
+
duration: number;
|
|
38
|
+
/** Learner actions at the subject's own clock seconds (camera cues, events, pause/play). */
|
|
39
|
+
actions: RenderAction[];
|
|
40
|
+
/** `scene` keeps the subject's own sounds (shot SFX) in the episode mix. */
|
|
41
|
+
audio: "scene" | "silent";
|
|
42
|
+
/** Gain on the shot's own sounds in the episode mix. */
|
|
43
|
+
sound_volume: number;
|
|
44
|
+
/** How this shot begins: a cut, or a crossfade from the previous shot. */
|
|
45
|
+
transition: {
|
|
46
|
+
type: "cut";
|
|
47
|
+
} | {
|
|
48
|
+
type: "crossfade";
|
|
49
|
+
duration: number;
|
|
50
|
+
};
|
|
51
|
+
/** Captions, in seconds from the shot's own first frame. */
|
|
52
|
+
captions: Caption[];
|
|
53
|
+
};
|
|
54
|
+
export type TimedAudio = {
|
|
55
|
+
id: string;
|
|
56
|
+
file: string;
|
|
57
|
+
at: number;
|
|
58
|
+
shot?: string;
|
|
59
|
+
volume: number;
|
|
60
|
+
};
|
|
61
|
+
export type MusicBed = {
|
|
62
|
+
id: string;
|
|
63
|
+
file: string;
|
|
64
|
+
at: number;
|
|
65
|
+
shot?: string;
|
|
66
|
+
end: number | null;
|
|
67
|
+
loop: boolean;
|
|
68
|
+
volume: number;
|
|
69
|
+
duck: false | {
|
|
70
|
+
threshold: number;
|
|
71
|
+
ratio: number;
|
|
72
|
+
attack_ms: number;
|
|
73
|
+
release_ms: number;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export type Episode = {
|
|
77
|
+
schema: typeof EPISODE_SCHEMA;
|
|
78
|
+
id: string;
|
|
79
|
+
title: string;
|
|
80
|
+
/** Library and project context: labels only, never cache inputs. */
|
|
81
|
+
context: {
|
|
82
|
+
project: string | null;
|
|
83
|
+
library: string | null;
|
|
84
|
+
};
|
|
85
|
+
output: {
|
|
86
|
+
profile: RenderProfile;
|
|
87
|
+
width: number;
|
|
88
|
+
height: number;
|
|
89
|
+
fps: number;
|
|
90
|
+
crf: number;
|
|
91
|
+
audio_bitrate: string;
|
|
92
|
+
loudness: {
|
|
93
|
+
integrated: number;
|
|
94
|
+
true_peak: number;
|
|
95
|
+
lra: number;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
shots: EpisodeShot[];
|
|
99
|
+
audio: {
|
|
100
|
+
narration: TimedAudio[];
|
|
101
|
+
music: MusicBed[];
|
|
102
|
+
sfx: TimedAudio[];
|
|
103
|
+
};
|
|
104
|
+
subtitles: {
|
|
105
|
+
cues: Array<Caption & {
|
|
106
|
+
shot?: string;
|
|
107
|
+
}>;
|
|
108
|
+
include_shot_captions: boolean;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
/** Parse and bound an episode manifest. Unknown fields are refused, never ignored. */
|
|
112
|
+
export declare function parseEpisode(value: unknown): Episode;
|
|
113
|
+
/** The render recipe of one shot: the same contract `render-video` takes. */
|
|
114
|
+
export declare function shotRecipe(episode: Episode, shot: EpisodeShot): RenderRecipe;
|
|
115
|
+
export type ShotTiming = {
|
|
116
|
+
id: string;
|
|
117
|
+
index: number;
|
|
118
|
+
frames: number;
|
|
119
|
+
offset_frames: number;
|
|
120
|
+
overlap_frames: number;
|
|
121
|
+
offset_seconds: number;
|
|
122
|
+
duration_seconds: number;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Where every shot lands on the episode timeline, in frames. A crossfade
|
|
126
|
+
* into shot k overlaps it with the end of shot k-1 by its length.
|
|
127
|
+
*/
|
|
128
|
+
export declare function episodeTimeline(episode: Episode): {
|
|
129
|
+
shots: ShotTiming[];
|
|
130
|
+
total_frames: number;
|
|
131
|
+
total_seconds: number;
|
|
132
|
+
};
|
|
133
|
+
/** An item's episode second: absolute, or relative to its anchoring shot's first frame. */
|
|
134
|
+
export declare function anchoredSeconds(timeline: ReturnType<typeof episodeTimeline>, at: number, shot?: string): number;
|
|
135
|
+
/** Every encode setting a shot's MP4 depends on (part of every key). */
|
|
136
|
+
export declare function encoderSettings(episode: Episode, encoderVersion: string): Record<string, unknown>;
|
|
137
|
+
export type ShotKeyInputs = {
|
|
138
|
+
subject: RenderSubject;
|
|
139
|
+
recipe: RenderRecipe;
|
|
140
|
+
/** Scene: { main: source hash }; Experience: Experience source hash plus each Scene's identity. */
|
|
141
|
+
sources: Record<string, string | null>;
|
|
142
|
+
/** Dependency-record digests by Scene. Null when unknown (then the Scene's revision identity carries it). */
|
|
143
|
+
dependency_digests: Record<string, string | null>;
|
|
144
|
+
runtime_fingerprint: string | null;
|
|
145
|
+
/** sha256 of every sound the subject names, by id. */
|
|
146
|
+
audio_sha256: Record<string, string>;
|
|
147
|
+
profile: {
|
|
148
|
+
profile: RenderProfile;
|
|
149
|
+
width: number;
|
|
150
|
+
height: number;
|
|
151
|
+
fps: number;
|
|
152
|
+
};
|
|
153
|
+
encoder: Record<string, unknown>;
|
|
154
|
+
};
|
|
155
|
+
/** The content-addressed key of one shot: sha256 over its canonical inputs, nothing else. */
|
|
156
|
+
export declare function shotCacheKey(inputs: ShotKeyInputs): string;
|
|
157
|
+
export type ShotCacheRecord = {
|
|
158
|
+
kind: typeof SHOT_CACHE_KIND;
|
|
159
|
+
key: string;
|
|
160
|
+
shot: string;
|
|
161
|
+
inputs: ShotKeyInputs;
|
|
162
|
+
video_sha256: string;
|
|
163
|
+
frames: number;
|
|
164
|
+
bytes: number;
|
|
165
|
+
created_at: string;
|
|
166
|
+
canonical: false;
|
|
167
|
+
};
|
|
168
|
+
export type CachedShotCheck = {
|
|
169
|
+
valid: true;
|
|
170
|
+
} | {
|
|
171
|
+
valid: false;
|
|
172
|
+
reason: string;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* A cached shot is reusable only when its record names this key, its MP4
|
|
176
|
+
* hashes to the recorded sha256 and probes to the expected frame count.
|
|
177
|
+
* Missing or corrupt means rerender. Modification times are not read.
|
|
178
|
+
*/
|
|
179
|
+
export declare function checkCachedShot(record: unknown, key: string, expectedFrames: number, videoSha256: string | null, probedFrames: number | null): CachedShotCheck;
|
|
180
|
+
/** One stretch of the episode video: a copied shot, or a re-encoded crossfade run. */
|
|
181
|
+
export type VideoSegment = {
|
|
182
|
+
kind: "copy";
|
|
183
|
+
shots: number[];
|
|
184
|
+
} | {
|
|
185
|
+
kind: "crossfade";
|
|
186
|
+
shots: number[];
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Split the shots into segments: shots joined to a neighbour by a crossfade
|
|
190
|
+
* form one re-encoded segment; every other shot is stream-copied as it was
|
|
191
|
+
* rendered. Only crossfade boundaries are ever re-encoded.
|
|
192
|
+
*/
|
|
193
|
+
export declare function videoSegments(episode: Episode): VideoSegment[];
|
|
194
|
+
/** FFmpeg arguments that crossfade consecutive shots into one segment (video only). */
|
|
195
|
+
export declare function crossfadeArguments(episode: Episode, timeline: ReturnType<typeof episodeTimeline>, segment: number[], files: string[], output: string): string[];
|
|
196
|
+
/** FFmpeg arguments that copy one shot's picture unchanged (video only). */
|
|
197
|
+
export declare function copyVideoArguments(file: string, output: string): string[];
|
|
198
|
+
/** The concat-demuxer list for the segments, in order. */
|
|
199
|
+
export declare function concatList(files: string[]): string;
|
|
200
|
+
export declare function concatArguments(list: string, output: string, frames: number): string[];
|
|
201
|
+
export type MixInput = {
|
|
202
|
+
file: string;
|
|
203
|
+
at_seconds: number;
|
|
204
|
+
volume: number;
|
|
205
|
+
loop?: boolean;
|
|
206
|
+
length_seconds?: number | null;
|
|
207
|
+
fade_in?: number;
|
|
208
|
+
fade_out?: number;
|
|
209
|
+
trim_seconds?: number;
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* The episode audio mix as FFmpeg arguments: three buses summed without
|
|
213
|
+
* normalisation into exactly `total_samples` of 48 kHz stereo float PCM.
|
|
214
|
+
*
|
|
215
|
+
* - shots: each shot's own sounds at its episode offset (faded across a
|
|
216
|
+
* crossfade), plus episode-level sound effects;
|
|
217
|
+
* - narration: every narration item at its second;
|
|
218
|
+
* - music: each bed (looped when asked), ducked under the narration bus by
|
|
219
|
+
* `sidechaincompress` keyed on the narration.
|
|
220
|
+
*
|
|
221
|
+
* Every placement is counted in samples. Returns the arguments and the
|
|
222
|
+
* label of the narration-only bus written to `narrationOutput` (QC uses it
|
|
223
|
+
* to find narration onsets without the music under it).
|
|
224
|
+
*/
|
|
225
|
+
export declare function episodeMixArguments(input: {
|
|
226
|
+
shots: MixInput[];
|
|
227
|
+
narration: MixInput[];
|
|
228
|
+
music: Array<MixInput & {
|
|
229
|
+
duck: MusicBed["duck"];
|
|
230
|
+
}>;
|
|
231
|
+
total_samples: number;
|
|
232
|
+
output: string;
|
|
233
|
+
narrationOutput?: string;
|
|
234
|
+
}): string[];
|
|
235
|
+
export type LoudnessTarget = Episode["output"]["loudness"];
|
|
236
|
+
export type LoudnessMeasurement = {
|
|
237
|
+
input_i: string;
|
|
238
|
+
input_tp: string;
|
|
239
|
+
input_lra: string;
|
|
240
|
+
input_thresh: string;
|
|
241
|
+
target_offset: string;
|
|
242
|
+
};
|
|
243
|
+
/** Pass one of two-pass `loudnorm`: measure only. */
|
|
244
|
+
export declare function loudnessMeasureArguments(file: string, target: LoudnessTarget): string[];
|
|
245
|
+
/** The JSON block `loudnorm` prints at the end of stderr. */
|
|
246
|
+
export declare function parseLoudnorm(stderr: string): LoudnessMeasurement;
|
|
247
|
+
/**
|
|
248
|
+
* Pass two: normalise with the measured values (linear when possible), back
|
|
249
|
+
* to 48 kHz, padded and trimmed to exactly `total_samples`.
|
|
250
|
+
*/
|
|
251
|
+
/**
|
|
252
|
+
* The loudness range `loudnorm` is given in pass two: the target, or the
|
|
253
|
+
* mix's own range when that is wider. loudnorm normalises linearly (one
|
|
254
|
+
* gain, dynamics kept) only when the input range fits the target range;
|
|
255
|
+
* otherwise it compresses dynamically, which undershoots the integrated
|
|
256
|
+
* target on short programmes (a 7 s episode measured -17.2 LUFS for a
|
|
257
|
+
* -16 target). Keeping the mix's own range keeps it linear.
|
|
258
|
+
*/
|
|
259
|
+
export declare function loudnormRange(target: LoudnessTarget, measured: LoudnessMeasurement): number;
|
|
260
|
+
export declare function loudnessApplyArguments(file: string, target: LoudnessTarget, measured: LoudnessMeasurement, totalSamples: number, output: string): string[];
|
|
261
|
+
/**
|
|
262
|
+
* The safety pass after normalisation: when the result is still more than
|
|
263
|
+
* 0.5 LU from target (loudnorm could not stay linear under the true-peak
|
|
264
|
+
* ceiling), one gain to the target and a peak limiter 0.5 dB under the
|
|
265
|
+
* ceiling, at the exact sample count. Null when no correction is needed.
|
|
266
|
+
*/
|
|
267
|
+
export declare function loudnessCorrectionArguments(file: string, target: LoudnessTarget, measuredIntegrated: number | null, totalSamples: number, output: string): string[] | null;
|
|
268
|
+
/** Mux the assembled picture and the episode mix: the picture is copied, the audio encoded once. */
|
|
269
|
+
export declare function muxArguments(video: string, audio: string, episode: Episode, totalFrames: number, output: string): string[];
|
|
270
|
+
export type EpisodeCue = Caption & {
|
|
271
|
+
source: string;
|
|
272
|
+
};
|
|
273
|
+
/** Every subtitle cue in episode seconds: episode cues (absolute or shot-anchored) and shot captions. */
|
|
274
|
+
export declare function episodeCues(episode: Episode, timeline?: {
|
|
275
|
+
shots: ShotTiming[];
|
|
276
|
+
total_frames: number;
|
|
277
|
+
total_seconds: number;
|
|
278
|
+
}): EpisodeCue[];
|
|
279
|
+
export declare function srtTimestamp(seconds: number): string;
|
|
280
|
+
/** SubRip text: numbered cues, CRLF-free, one blank line between cues. */
|
|
281
|
+
export declare function toSrt(cues: Caption[]): string;
|
|
282
|
+
export type QcInput = {
|
|
283
|
+
expected_frames: number;
|
|
284
|
+
fps: number;
|
|
285
|
+
width: number;
|
|
286
|
+
height: number;
|
|
287
|
+
probe: {
|
|
288
|
+
frames: number;
|
|
289
|
+
width: number;
|
|
290
|
+
height: number;
|
|
291
|
+
video_codec: string;
|
|
292
|
+
audio_codec: string | null;
|
|
293
|
+
duration: number;
|
|
294
|
+
video_duration: number | null;
|
|
295
|
+
audio_duration: number | null;
|
|
296
|
+
video_start: number | null;
|
|
297
|
+
audio_start: number | null;
|
|
298
|
+
};
|
|
299
|
+
loudness: {
|
|
300
|
+
integrated: number | null;
|
|
301
|
+
true_peak: number | null;
|
|
302
|
+
lra: number | null;
|
|
303
|
+
};
|
|
304
|
+
target: LoudnessTarget;
|
|
305
|
+
narration_onsets?: Array<{
|
|
306
|
+
id: string;
|
|
307
|
+
expected: number;
|
|
308
|
+
observed: number | null;
|
|
309
|
+
}>;
|
|
310
|
+
};
|
|
311
|
+
export type QcCheck = {
|
|
312
|
+
check: string;
|
|
313
|
+
pass: boolean;
|
|
314
|
+
detail: string;
|
|
315
|
+
};
|
|
316
|
+
/** The episode QC verdict: frames, size, codecs, duration, A/V sync, loudness, narration placement. */
|
|
317
|
+
export declare function qcChecks(input: QcInput): QcCheck[];
|
|
318
|
+
/** Integrated loudness, LRA and true peak from FFmpeg `ebur128=peak=true` stderr. */
|
|
319
|
+
export declare function parseEbur128(stderr: string): {
|
|
320
|
+
integrated: number | null;
|
|
321
|
+
lra: number | null;
|
|
322
|
+
true_peak: number | null;
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Where sound begins, from FFmpeg `silencedetect` stderr: 0 when the file
|
|
326
|
+
* does not open silent, then every `silence_end`.
|
|
327
|
+
*/
|
|
328
|
+
export declare function onsets(stderr: string): number[];
|
|
329
|
+
/** The onset nearest `expected`, or null when there is none within a second. */
|
|
330
|
+
export declare function nearestOnset(values: number[], expected: number): number | null;
|
|
331
|
+
/** The identity of a whole assembly: shot keys, timeline, audio and subtitle inputs, loudness and output settings. */
|
|
332
|
+
export declare function assemblyKey(episode: Episode, shotKeys: string[], audioSha256: Record<string, string>): string;
|