@tongue-core/sdk 0.1.0-rc.4 → 0.1.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -12
- package/dist/TongueServices.d.ts +5 -0
- package/dist/TongueServices.js +2 -0
- package/dist/client.js +2 -2
- package/dist/errors.d.ts +13 -2
- package/dist/errors.js +12 -1
- package/dist/import/importer.js +3 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +9 -3
- package/dist/points.d.ts +28 -0
- package/dist/points.js +76 -0
- package/dist/project.d.ts +30 -6
- package/dist/project.js +75 -13
- package/dist/render/entries.d.ts +2 -0
- package/dist/render/entries.js +7 -0
- package/dist/render/events.d.ts +71 -0
- package/dist/render/events.js +1 -0
- package/dist/render/frame.d.ts +9 -1
- package/dist/render/frame.js +16 -7
- package/dist/render/inputs.d.ts +5 -0
- package/dist/render/inputs.js +25 -0
- package/dist/render/pipeline.d.ts +17 -26
- package/dist/render/pipeline.js +51 -32
- package/dist/render/protocol.d.ts +45 -1
- package/dist/render/protocol.js +55 -1
- package/dist/render/soundtrack-process.js +77 -23
- package/dist/render/stills.d.ts +6 -5
- package/dist/render/stills.js +11 -5
- package/dist/render/video-frames.d.ts +6 -0
- package/dist/render/video-frames.js +60 -9
- package/dist/render/worker.js +2 -1
- package/dist/sound/AudioProcessor.d.ts +59 -0
- package/dist/sound/AudioProcessor.js +18 -0
- package/dist/sound/Sound.d.ts +48 -0
- package/dist/sound/Sound.js +92 -0
- package/dist/sound/soundtrack.d.ts +21 -0
- package/dist/sound/soundtrack.js +141 -0
- package/package.json +11 -9
package/README.md
CHANGED
|
@@ -7,9 +7,12 @@ here, with the same results. Built with Effect v4 (orchestration only: the per-f
|
|
|
7
7
|
TypeScript); a Promise facade is included for applications that don't use Effect.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
bun add @tongue-core/sdk@next # Bun ≥ 1.3; ffmpeg and ffprobe on the PATH
|
|
10
|
+
bun add @tongue-core/sdk@next effect@4.0.6 # Bun ≥ 1.3; ffmpeg and ffprobe on the PATH
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
`effect` is a peer dependency: your application and the SDK share one copy of it, so the SDK's
|
|
14
|
+
services, errors and Layers are the same values as yours.
|
|
15
|
+
|
|
13
16
|
## Promise API
|
|
14
17
|
|
|
15
18
|
```ts
|
|
@@ -31,27 +34,32 @@ Failures reject with the SDK's tagged errors (below).
|
|
|
31
34
|
## Effect API
|
|
32
35
|
|
|
33
36
|
```ts
|
|
34
|
-
import {
|
|
35
|
-
import { openFilm, renderVideo } from "@tongue-core/sdk";
|
|
37
|
+
import { openFilm, renderVideo, TongueServices } from "@tongue-core/sdk";
|
|
36
38
|
import { Effect } from "effect";
|
|
37
39
|
|
|
38
40
|
const program = Effect.gen(function* () {
|
|
39
41
|
const film = yield* openFilm("src/film.tsx"); // load, import assets, plan
|
|
40
42
|
return yield* renderVideo(film, { out: "out/film.mp4", encoder: "auto", raster: "auto" });
|
|
41
43
|
});
|
|
42
|
-
await Effect.runPromise(program.pipe(Effect.provide(
|
|
44
|
+
await Effect.runPromise(program.pipe(Effect.provide(TongueServices.layer)));
|
|
43
45
|
```
|
|
44
46
|
|
|
47
|
+
`TongueServices.layer` is what every SDK function runs on (the file system, paths and child
|
|
48
|
+
processes, from Bun): provide it once, where your application runs its Effects.
|
|
49
|
+
|
|
45
50
|
| Function | Does |
|
|
46
51
|
|---|---|
|
|
47
52
|
| `initProject(dir, { template, name })` | a film project in a new or empty folder, from a built-in template or a template folder |
|
|
48
53
|
| `listTemplates()` / `readTemplate(nameOrPath)` | the built-in templates, or one by name or path |
|
|
49
54
|
| `buildFilm(project, { out })` | builds a film project into `out` (default `dist/`): build.json, film.json, a bundle per scene |
|
|
50
|
-
| `openFilm(path, { scene })` | a film ready to render: a project is built (into `.tongue/builds/<id>`, named by its content) and planned from film.json, a film module is imported and planned; assets imported (cached by size, mtime and options) either way. `scene: { id, props }` opens one scene of a project on its own |
|
|
55
|
+
| `openFilm(path, { scene, sources })` | a film ready to render: a project is built (into `.tongue/builds/<id>`, named by its content) and planned from film.json, a build folder (what `buildFilm` writes) is opened as it is, a film module is imported and planned; assets imported (cached by size, mtime and options) either way. `scene: { id, props }` opens one scene of a project on its own; `sources` says where videos are decoded from ([Footage](#footage)) |
|
|
51
56
|
| `describeFilm(film)` | the plan as text (for agents to read) and as a JSON summary |
|
|
52
57
|
| `checkFilm(path)` | problems and warnings as data |
|
|
53
58
|
| `renderStills(film, { frames, scale, outDir })` / `renderSheet(…)` | PNGs drawn by the wasm raster (the player's), or tiled into one labelled sheet |
|
|
54
|
-
| `renderVideo(film, options)` | an MP4 with sound: the whole film or `[from, to)`, at any scale; `onStart` once the request is checked and the raster and encoder are chosen, `onProgress` while it draws. A frame that throws fails the render with `Frame <n>: <message>` |
|
|
59
|
+
| `renderVideo(film, options)` | an MP4 with sound: the whole film or `[from, to)`, at any scale (`out` defaults to `out/<name>.mp4`); `sound` runs [sound processing](#sound-processing-extensions); `onStart` once the request is checked and the raster and encoder are chosen, `onProgress` while it draws. A frame that throws fails the render with `Frame <n>: <message>` |
|
|
60
|
+
| `renderVideoEvents(film, options)` | the same render as a `Stream` of `RenderEvent`s: `Started`, `Frames`, `Sound` (each processor's progress), `Joining`, then `Done` with the result |
|
|
61
|
+
| `parsePoint`, `parseFrame`, `parseRange`, `clipRange`, `spreadFrames`, `everyFrames` | points in a film as people write them (`90`, `3s`, `50%`, `end`, `site`, `site+30`), for a command line or form of your own; bad ones fail with `InvalidRequest` saying how to write them |
|
|
62
|
+
| `videoInputs(film)`, `checkVideoInputs(film)` | where each video is decoded from, and a check that every one is there |
|
|
55
63
|
| `listBackends()` | every raster and encoder, each tried for real |
|
|
56
64
|
| `studioServer(path, { port, remote })` | the studio on a project (rebuilt and reloaded on every edit) or on a build served anywhere |
|
|
57
65
|
|
|
@@ -81,12 +89,121 @@ the SDK loads it, plus the media its clips play; `<Film assets={[…]}>` replace
|
|
|
81
89
|
Importing turns them into what the engine reads: TrueType fonts, QOI images, WAV audio, video facts
|
|
82
90
|
and a poster frame.
|
|
83
91
|
|
|
92
|
+
## Footage
|
|
93
|
+
|
|
94
|
+
A render decodes every video from its **original file**: a build holds only a video's facts, a
|
|
95
|
+
poster and a preview-quality proxy (what the studio plays). Before a frame is drawn, `renderVideo`
|
|
96
|
+
and `renderStills` check that each video's file is there and fail with `AssetFailure` naming the
|
|
97
|
+
ones that aren't; a frame ffmpeg can't decode, or a file shorter than its import counted, fails the
|
|
98
|
+
render with `Frame <n>: Video "<name>": …` and ffmpeg's reason. A poster never stands in for
|
|
99
|
+
footage in a render (that is the player's fallback while frames arrive).
|
|
100
|
+
|
|
101
|
+
A video is decoded from its source path under the film's root, unless `sources` says otherwise:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const film = yield* openFilm("renders/1234/dist", { // a build, without the project around it
|
|
105
|
+
sources: { screen: "/data/media/screen.mp4", camera: "/data/media/camera.mp4" },
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A build inside a project (`my-film/dist`) finds its footage in the project; a build on its own
|
|
110
|
+
needs `sources` for every video that isn't beside it.
|
|
111
|
+
|
|
112
|
+
## Sound processing (extensions)
|
|
113
|
+
|
|
114
|
+
A render can put part of its soundtrack through processing of your own (voice enhancement, noise
|
|
115
|
+
removal, levelling, effects) and still produce one MP4, in sync. The SDK defines the contract and
|
|
116
|
+
runs it; what a processor does and what it needs (a model, a native tool, a service) is yours, so
|
|
117
|
+
nobody who doesn't use it installs it.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { AudioProcessor, openFilm, renderVideo, Sound } from "@tongue-core/sdk";
|
|
121
|
+
|
|
122
|
+
const enhance = yield* enhanceVoice({ peakDb: -3 }); // an AudioProcessor (below)
|
|
123
|
+
yield* renderVideo(film, {
|
|
124
|
+
sound: [
|
|
125
|
+
Sound.track("voiceover", enhance), // the clips of the track with id "voiceover"
|
|
126
|
+
Sound.track("voiceover", level), // the same selection again: runs after enhance
|
|
127
|
+
Sound.master(loudness), // then the whole mix
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**What a rule takes.** `Sound.track(id, p)`; `Sound.stem({ clips: ["a", "b"] }, p)`;
|
|
133
|
+
`Sound.stem({ voice: true }, p)` (every narration clip); `Sound.master(p)` (the whole mix, after
|
|
134
|
+
the stems, before the limiter). A selection is data (`Sound.AudioSelection` decodes it from flags or
|
|
135
|
+
JSON). The selected clips are mixed into a **stem** of their own (their trims, speed, gain and fades
|
|
136
|
+
applied) across the whole film, processed, and added back; everything else sounds as it always has.
|
|
137
|
+
A track or clip the film doesn't have, or a visual track, is an `InvalidRequest` listing what it
|
|
138
|
+
does have; a clip goes into one stem, so overlapping selections are refused unless they are the same
|
|
139
|
+
(then their processors run in order).
|
|
140
|
+
|
|
141
|
+
**The contract.** A processor gets a `Stem`: `samples` per channel (the whole film at 48 kHz, from
|
|
142
|
+
its first frame), `channels` (2), the stem as a float WAV `file` and as decoded `pcm`, the `clips` in
|
|
143
|
+
it and the `plan`, a `workDir`, and `progress(0..1)`. It returns `{ pcm }` or `{ file }`, plus an
|
|
144
|
+
optional `report` (any JSON). What it returns must have exactly the stem's rate, channels and
|
|
145
|
+
length, and only finite samples, or the render fails with `ProcessorFailure`: that is what keeps
|
|
146
|
+
the sound in sync, so a processor with a delay (a model's lookahead) takes it out itself. Any
|
|
147
|
+
failure of the processor, or a defect in it, is a `ProcessorFailure` naming it.
|
|
148
|
+
|
|
149
|
+
**Two ways to make one.** `AudioProcessor.fromPcm({ key, process: (pcm, stem) => pcm })` for
|
|
150
|
+
processing that needs only the samples. `AudioProcessor.make({ key, process: (stem) => Effect })`
|
|
151
|
+
for anything else: the services `process` uses are taken from where `make` runs and travel with the
|
|
152
|
+
processor, so the render itself never needs them:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// voice-enhance: its own package, depending on @tongue-core/sdk and on what it needs.
|
|
156
|
+
interface DenoiserOperations {
|
|
157
|
+
readonly name: string;
|
|
158
|
+
readonly denoise: (channel: Float32Array, sampleRate: number) => Effect.Effect<Float32Array>;
|
|
159
|
+
}
|
|
160
|
+
export class Denoiser extends Context.Service<Denoiser, DenoiserOperations>()("voice-enhance/Denoiser") {
|
|
161
|
+
static readonly layer = Layer.effect(Denoiser, loadModel); // the heavy part, paid once, only by those who provide it
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const enhanceVoice = (settings: EnhanceSettings) =>
|
|
165
|
+
AudioProcessor.make({
|
|
166
|
+
key: `voice-enhance/v1 peak ${settings.peakDb} dB`,
|
|
167
|
+
process: (stem) => Effect.gen(function* () {
|
|
168
|
+
const denoiser = yield* Denoiser;
|
|
169
|
+
const pcm = yield* stem.pcm;
|
|
170
|
+
const channels = yield* Effect.forEach(pcm.channels, (c) => denoiser.denoise(c, pcm.sampleRate));
|
|
171
|
+
yield* stem.progress(1);
|
|
172
|
+
return { pcm: { sampleRate: pcm.sampleRate, channels }, report: { denoiser: denoiser.name } };
|
|
173
|
+
}),
|
|
174
|
+
}); // Effect<AudioProcessor, never, Denoiser>
|
|
175
|
+
|
|
176
|
+
// In your command line: make the processor where its Layer is provided.
|
|
177
|
+
const render = Command.make("render", { project: Argument.String("project"), enhance: Flag.String("enhance").pipe(Flag.atLeast(0)) },
|
|
178
|
+
Effect.fn(function* (flags) {
|
|
179
|
+
const film = yield* openFilm(flags.project);
|
|
180
|
+
const enhance = yield* enhanceVoice({ peakDb: -3 });
|
|
181
|
+
return yield* renderVideo(film, { sound: flags.enhance.map((track) => Sound.track(track, enhance)) });
|
|
182
|
+
}),
|
|
183
|
+
).pipe(Command.provide(Denoiser.layer));
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**The key and the cache.** A processor's `key` names it, its version and its settings. A result is
|
|
187
|
+
cached by the key and the exact audio it was given (`.tongue/cache/sound`, the newest 32 kept): render
|
|
188
|
+
again after changing anything that doesn't change that audio (the picture, another track, the
|
|
189
|
+
range, the scale) and the processor doesn't run; the result says `cached: true`. Change the key
|
|
190
|
+
whenever the output would change.
|
|
191
|
+
|
|
192
|
+
**What happened.** `VideoResult.sound` lists each processor run: the stem, its clips, the
|
|
193
|
+
processor's key, whether it was cached, its seconds and its report. `renderVideoEvents` streams
|
|
194
|
+
`Sound` events (`started`, `progress`, `done`, `cached`) while it runs.
|
|
195
|
+
|
|
196
|
+
**Where it runs.** Processors run in your process, alongside the render workers, while the frames
|
|
197
|
+
are drawn (the soundtrack always has); keep heavy work off its main thread (`effect/workers`, a
|
|
198
|
+
child process), or it slows the hand-off of frames to the workers.
|
|
199
|
+
|
|
84
200
|
## How a video renders
|
|
85
201
|
|
|
86
202
|
```
|
|
87
203
|
coordinator ──segments──▶ worker 1: frames ─▶ raster ─▶ encoder ─▶ seg-0000.mp4
|
|
88
204
|
◀─progress── worker 2 … seg-0001.mp4
|
|
89
205
|
soundtrack: sounds + audio clips ─▶ mix ─▶ AAC audio.m4a
|
|
206
|
+
with sound processing: stems ─▶ processors (here, cached) ─▶ sum ─▶ master ─▶ limiter ─▶ AAC
|
|
90
207
|
join: concat + audio ─(stream copy)─▶ film.mp4, frame count probed
|
|
91
208
|
```
|
|
92
209
|
|
|
@@ -95,8 +212,11 @@ coordinator ──segments──▶ worker 1: frames ─▶ raster ─▶ encode
|
|
|
95
212
|
encoder, so the JIT stays warm and glyphs and images stay uploaded.
|
|
96
213
|
- **Segments** are one to two seconds of frames, shrinking towards the end so the last ones finish
|
|
97
214
|
together. Each is a closed-GOP encode; the join copies streams without re-encoding.
|
|
98
|
-
- **Video clips** are decoded by ffmpeg in order, frame after frame,
|
|
99
|
-
|
|
215
|
+
- **Video clips** are decoded by ffmpeg from their original files, in order, frame after frame,
|
|
216
|
+
and restarted on jumps; a frame that can't be decoded fails the render ([Footage](#footage)).
|
|
217
|
+
- **The soundtrack** is mixed in its own process while the workers draw (`soundtrack-process.ts`);
|
|
218
|
+
with sound processing, it writes the stems, the processors run in the coordinator, and it adds
|
|
219
|
+
them back, limits and encodes (`sound/soundtrack.ts`).
|
|
100
220
|
|
|
101
221
|
Rasters (who draws) and encoders (who encodes) are chosen by name, because the choice is made here
|
|
102
222
|
but runs inside the workers:
|
|
@@ -122,11 +242,12 @@ gives the CLI's exit code.
|
|
|
122
242
|
| Error | When | Exit |
|
|
123
243
|
|---|---|---|
|
|
124
244
|
| `FilmFailure` | the module is missing, throws, isn't a `<Film>`, or its timeline doesn't plan | 2 |
|
|
125
|
-
| `AssetFailure` | an asset's file is missing or unreadable | 2 |
|
|
245
|
+
| `AssetFailure` | an asset's file is missing or unreadable, or a video's original file isn't there to render from | 2 |
|
|
126
246
|
| `InvalidRequest` | a frame outside the film, a bad range or scale | 2 |
|
|
127
247
|
| `ToolFailure` | ffmpeg or ffprobe is missing or failed | 3 |
|
|
128
248
|
| `BackendUnavailable` | the raster or encoder asked for doesn't work here | 3 |
|
|
129
|
-
| `RenderFailure` | a worker died or a frame
|
|
249
|
+
| `RenderFailure` | a worker died, a frame failed, or a video frame couldn't be decoded | 3 |
|
|
250
|
+
| `ProcessorFailure` | a sound processor failed, or returned audio that would break sync (rate, channels, length) or isn't audio | 3 |
|
|
130
251
|
| `LayoutError`, `CompileError`, `LoadError`, `TimelineError`, `DocError`, `TemplateError` | a film project doesn't build (from `@tongue-core/project`: each names its file, line or path) | 2 |
|
|
131
252
|
|
|
132
253
|
## Environment
|
|
@@ -147,10 +268,14 @@ src/
|
|
|
147
268
|
init.ts init from templates
|
|
148
269
|
check.ts, backends.ts checkFilm, listBackends
|
|
149
270
|
import/ asset import: fonts, images, audio, video (ffprobe, posters)
|
|
150
|
-
render/ stills, the video pipeline, its worker and soundtrack processes, video decoding
|
|
271
|
+
render/ stills, the video pipeline and its events, its worker and soundtrack processes, video decoding and inputs
|
|
272
|
+
sound/ sound processing: Sound (rules), AudioProcessor (the contract), the soundtrack around them
|
|
273
|
+
points.ts points and ranges in a film, as people write them
|
|
274
|
+
TongueServices.ts the layer the SDK runs on
|
|
151
275
|
encode/encoders.ts the ffmpeg encoders and how each is tried
|
|
152
276
|
raster/ the native and GPU raster bindings (index.ts) and the registry (rasters.ts)
|
|
153
277
|
studio/server.ts the studio's server: the app bundled, a project's build served and rebuilt
|
|
154
|
-
test/ unit tests, end-to-end ones on fixtures/tiny (a film with text, video and sound
|
|
278
|
+
test/ unit tests, end-to-end ones on fixtures/tiny (a film with text, video and sound; sound
|
|
279
|
+
processing and footage that can't be decoded on copies of it), builds rendered with `sources`, and
|
|
155
280
|
projects from every template (project.test.ts: film.json draws the same frames as film.tsx)
|
|
156
281
|
```
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { BunServices } from "@effect/platform-bun";
|
|
2
1
|
import { Cause, Effect, Exit } from "effect";
|
|
3
2
|
import { listBackends } from "./backends.js";
|
|
4
3
|
import { checkFilm } from "./check.js";
|
|
@@ -6,10 +5,11 @@ import { initProject, listTemplates } from "./init.js";
|
|
|
6
5
|
import { buildFilm, describeFilm, openFilm } from "./project.js";
|
|
7
6
|
import { renderVideo } from "./render/pipeline.js";
|
|
8
7
|
import { renderSheet, renderStills } from "./render/stills.js";
|
|
8
|
+
import * as TongueServices from "./TongueServices.js";
|
|
9
9
|
// The SDK as plain Promises, for applications that don't use Effect. Each call runs the same Effect
|
|
10
10
|
// program the CLI runs; a failure rejects with the SDK's tagged error (FilmFailure, ToolFailure…).
|
|
11
11
|
const run = async (effect) => {
|
|
12
|
-
const exit = await Effect.runPromiseExit(effect.pipe(Effect.provide(
|
|
12
|
+
const exit = await Effect.runPromiseExit(effect.pipe(Effect.provide(TongueServices.layer)));
|
|
13
13
|
if (Exit.isSuccess(exit))
|
|
14
14
|
return exit.value;
|
|
15
15
|
throw Cause.squash(exit.cause);
|
package/dist/errors.d.ts
CHANGED
|
@@ -43,9 +43,20 @@ declare const RenderFailure_base: Schema.Class<RenderFailure, Schema.TaggedStruc
|
|
|
43
43
|
/** A frame failed to render, or a render worker died. */
|
|
44
44
|
export declare class RenderFailure extends RenderFailure_base {
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
declare const ProcessorFailure_base: Schema.Class<ProcessorFailure, Schema.TaggedStruct<"ProcessorFailure", {
|
|
47
|
+
/** The processor's key. */
|
|
48
|
+
readonly processor: Schema.String;
|
|
49
|
+
/** What it was processing: "track voiceover", "master"… */
|
|
50
|
+
readonly stem: Schema.String;
|
|
51
|
+
readonly message: Schema.String;
|
|
52
|
+
}>, import("effect/Cause").YieldableError>;
|
|
53
|
+
/** A sound processor failed, or gave back audio that would break the film's sync (another rate, other
|
|
54
|
+
* channels, another length) or isn't audio (a sample that isn't a finite number). */
|
|
55
|
+
export declare class ProcessorFailure extends ProcessorFailure_base {
|
|
56
|
+
}
|
|
57
|
+
export declare const SdkError: Schema.Union<readonly [typeof FilmFailure, typeof AssetFailure, typeof InvalidRequest, typeof ToolFailure, typeof BackendUnavailable, typeof RenderFailure, typeof ProcessorFailure, typeof LayoutError, typeof CompileError, typeof LoadError, typeof TimelineError, typeof DocError, typeof TemplateError]>;
|
|
47
58
|
export type SdkError = typeof SdkError.Type;
|
|
48
|
-
export declare const isSdkError: <I>(input: I) => input is I & (FilmFailure | AssetFailure | InvalidRequest | ToolFailure | BackendUnavailable | RenderFailure | LayoutError | CompileError | LoadError | TimelineError | DocError | TemplateError);
|
|
59
|
+
export declare const isSdkError: <I>(input: I) => input is I & (FilmFailure | AssetFailure | InvalidRequest | ToolFailure | BackendUnavailable | RenderFailure | ProcessorFailure | LayoutError | CompileError | LoadError | TimelineError | DocError | TemplateError);
|
|
49
60
|
/** Exhaustive: a new error without a case is a compile error. */
|
|
50
61
|
export declare function exitCodeOf(error: SdkError): number;
|
|
51
62
|
/** The message of anything thrown. */
|
package/dist/errors.js
CHANGED
|
@@ -41,8 +41,18 @@ export class RenderFailure extends Schema.TaggedError()("RenderFailure", {
|
|
|
41
41
|
message: Schema.String,
|
|
42
42
|
}) {
|
|
43
43
|
}
|
|
44
|
+
/** A sound processor failed, or gave back audio that would break the film's sync (another rate, other
|
|
45
|
+
* channels, another length) or isn't audio (a sample that isn't a finite number). */
|
|
46
|
+
export class ProcessorFailure extends Schema.TaggedError()("ProcessorFailure", {
|
|
47
|
+
/** The processor's key. */
|
|
48
|
+
processor: Schema.String,
|
|
49
|
+
/** What it was processing: "track voiceover", "master"… */
|
|
50
|
+
stem: Schema.String,
|
|
51
|
+
message: Schema.String,
|
|
52
|
+
}) {
|
|
53
|
+
}
|
|
44
54
|
export const SdkError = Schema.Union([
|
|
45
|
-
FilmFailure, AssetFailure, InvalidRequest, ToolFailure, BackendUnavailable, RenderFailure,
|
|
55
|
+
FilmFailure, AssetFailure, InvalidRequest, ToolFailure, BackendUnavailable, RenderFailure, ProcessorFailure,
|
|
46
56
|
LayoutError, CompileError, LoadError, TimelineError, DocError, TemplateError,
|
|
47
57
|
]);
|
|
48
58
|
export const isSdkError = Schema.is(SdkError);
|
|
@@ -62,6 +72,7 @@ export function exitCodeOf(error) {
|
|
|
62
72
|
case "ToolFailure":
|
|
63
73
|
case "BackendUnavailable":
|
|
64
74
|
case "RenderFailure":
|
|
75
|
+
case "ProcessorFailure":
|
|
65
76
|
return 3;
|
|
66
77
|
}
|
|
67
78
|
}
|
package/dist/import/importer.js
CHANGED
|
@@ -71,7 +71,9 @@ export const importAssets = Effect.fn("importAssets")(function* (project, assets
|
|
|
71
71
|
const names = (yield* fs.readDirectory(proxyDir)).filter((n) => n.endsWith(".mp4")).sort();
|
|
72
72
|
const segments = yield* Effect.forEach(names, (n) => fs.readFile(path.join(proxyDir, n)));
|
|
73
73
|
const proxy = yield* proxyInfo(asset.name, segments);
|
|
74
|
-
|
|
74
|
+
// The facts and the proxy only: never the source's absolute path, which names this
|
|
75
|
+
// machine and is wrong on any other (renders find the source themselves, see videoInputs).
|
|
76
|
+
yield* write(importedPath(asset), new TextEncoder().encode(JSON.stringify({ ...video.info, proxy })));
|
|
75
77
|
yield* write(videoPosterPath(asset), video.poster);
|
|
76
78
|
break;
|
|
77
79
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
export { createTongue, type Tongue } from "./client.js";
|
|
2
|
-
export
|
|
2
|
+
export * as TongueServices from "./TongueServices.js";
|
|
3
|
+
export { openFilm, openProject, buildFilm, describeFilm, videoInputs, outputName, defaultVideoOut, defaultStillsDir, type OpenFilm, type OpenOptions, type Project, } from "./project.js";
|
|
3
4
|
export { buildFilmProject, readProjectFiles, fileModuleHost, linkFilmTypes, type BuiltProject } from "./build/build.js";
|
|
4
5
|
export { initProject, listTemplates, readTemplate, type InitOptions, type InitReport } from "./init.js";
|
|
5
6
|
export type { FilmSource } from "./render/protocol.js";
|
|
6
7
|
export { importAssets, type ImportReport } from "./import/importer.js";
|
|
7
8
|
export { checkFilm, type CheckReport, type CheckProblem } from "./check.js";
|
|
8
9
|
export { renderStills, renderSheet, type StillsOptions, type Still } from "./render/stills.js";
|
|
9
|
-
export { renderVideo,
|
|
10
|
+
export { renderVideo, renderVideoEvents, defaultWorkers, type VideoOptions } from "./render/pipeline.js";
|
|
11
|
+
export type { RenderEvent, RenderStart, SoundReport, VideoResult } from "./render/events.js";
|
|
12
|
+
export { checkVideoInputs } from "./render/inputs.js";
|
|
13
|
+
export { parsePoint, parseFrame, parseRange, clipRange, spreadFrames, everyFrames, type Planned } from "./points.js";
|
|
10
14
|
export { listBackends, type BackendsReport } from "./backends.js";
|
|
11
15
|
export { studioServer, type StudioOptions } from "./studio/server.js";
|
|
12
16
|
export { ENCODER_NAMES, AUTO_ENCODERS, DEFAULT_SETTINGS, isEncoderName, type EncoderName, type EncoderSettings } from "./encode/encoders.js";
|
|
13
17
|
export { RASTER_NAMES, isRasterName, type RasterName } from "./raster/rasters.js";
|
|
14
18
|
export { NativeRaster, nativeRasterAvailable, GpuRaster, gpuRasterAvailable, type NativeRasterOptions } from "./raster/index.js";
|
|
15
|
-
export { FilmFailure, AssetFailure, InvalidRequest, ToolFailure, BackendUnavailable, RenderFailure, SdkError, isSdkError, exitCodeOf } from "./errors.js";
|
|
16
|
-
export
|
|
19
|
+
export { FilmFailure, AssetFailure, InvalidRequest, ToolFailure, BackendUnavailable, RenderFailure, ProcessorFailure, SdkError, isSdkError, exitCodeOf } from "./errors.js";
|
|
20
|
+
export * as Sound from "./sound/Sound.js";
|
|
21
|
+
export * as AudioProcessor from "./sound/AudioProcessor.js";
|
|
22
|
+
export type { FilmPlan, PlanSummary, ClipPlan, TrackPlan, Json } from "@tongue-core/film";
|
|
23
|
+
export type { PcmAudio } from "@tongue-core/engine";
|
package/dist/index.js
CHANGED
|
@@ -3,17 +3,23 @@
|
|
|
3
3
|
// of truth (the CLI is built on it); `createTongue()` offers the same as Promises.
|
|
4
4
|
export { createTongue } from "./client.js";
|
|
5
5
|
// Effect API
|
|
6
|
-
export
|
|
6
|
+
export * as TongueServices from "./TongueServices.js";
|
|
7
|
+
export { openFilm, openProject, buildFilm, describeFilm, videoInputs, outputName, defaultVideoOut, defaultStillsDir, } from "./project.js";
|
|
7
8
|
export { buildFilmProject, readProjectFiles, fileModuleHost, linkFilmTypes } from "./build/build.js";
|
|
8
9
|
export { initProject, listTemplates, readTemplate } from "./init.js";
|
|
9
10
|
export { importAssets } from "./import/importer.js";
|
|
10
11
|
export { checkFilm } from "./check.js";
|
|
11
12
|
export { renderStills, renderSheet } from "./render/stills.js";
|
|
12
|
-
export { renderVideo, defaultWorkers } from "./render/pipeline.js";
|
|
13
|
+
export { renderVideo, renderVideoEvents, defaultWorkers } from "./render/pipeline.js";
|
|
14
|
+
export { checkVideoInputs } from "./render/inputs.js";
|
|
15
|
+
export { parsePoint, parseFrame, parseRange, clipRange, spreadFrames, everyFrames } from "./points.js";
|
|
13
16
|
export { listBackends } from "./backends.js";
|
|
14
17
|
export { studioServer } from "./studio/server.js";
|
|
15
18
|
export { ENCODER_NAMES, AUTO_ENCODERS, DEFAULT_SETTINGS, isEncoderName } from "./encode/encoders.js";
|
|
16
19
|
export { RASTER_NAMES, isRasterName } from "./raster/rasters.js";
|
|
17
20
|
// The server rasters, for applications that drive a raster themselves (FilmRenderer from @tongue-core/engine)
|
|
18
21
|
export { NativeRaster, nativeRasterAvailable, GpuRaster, gpuRasterAvailable } from "./raster/index.js";
|
|
19
|
-
export { FilmFailure, AssetFailure, InvalidRequest, ToolFailure, BackendUnavailable, RenderFailure, SdkError, isSdkError, exitCodeOf } from "./errors.js";
|
|
22
|
+
export { FilmFailure, AssetFailure, InvalidRequest, ToolFailure, BackendUnavailable, RenderFailure, ProcessorFailure, SdkError, isSdkError, exitCodeOf } from "./errors.js";
|
|
23
|
+
// Sound processing: what extensions plug into a render with
|
|
24
|
+
export * as Sound from "./sound/Sound.js";
|
|
25
|
+
export * as AudioProcessor from "./sound/AudioProcessor.js";
|
package/dist/points.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FilmPlan } from "@tongue-core/film";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { InvalidRequest } from "./errors.js";
|
|
4
|
+
/** What points are read against: a planned film's rate, length and clips. */
|
|
5
|
+
export type Planned = Pick<FilmPlan, "fps" | "length" | "tracks">;
|
|
6
|
+
/** A clip's frames: [first, end), the frames on which it is on screen. */
|
|
7
|
+
export declare const clipRange: (plan: Planned, id: string) => Effect.Effect<{
|
|
8
|
+
readonly from: number;
|
|
9
|
+
readonly to: number;
|
|
10
|
+
}, InvalidRequest, never>;
|
|
11
|
+
/** A point in the film, as a frame: where a range starts or ends ("end" is the film's length). */
|
|
12
|
+
export declare const parsePoint: (plan: Planned, text: string) => Effect.Effect<number, InvalidRequest, never>;
|
|
13
|
+
/** A frame to draw: a point, where the film's end ("end", "100%") is its last frame. */
|
|
14
|
+
export declare const parseFrame: (plan: Planned, text: string) => Effect.Effect<number, InvalidRequest, never>;
|
|
15
|
+
/** The frames [from, to) a render covers: a clip's when `clip` is given, else the whole film, with
|
|
16
|
+
* `from` and `to` (points) moving either end. */
|
|
17
|
+
export declare const parseRange: (plan: Planned, request: {
|
|
18
|
+
readonly from?: string;
|
|
19
|
+
readonly to?: string;
|
|
20
|
+
readonly clip?: string;
|
|
21
|
+
}) => Effect.Effect<{
|
|
22
|
+
readonly from: number;
|
|
23
|
+
readonly to: number;
|
|
24
|
+
}, InvalidRequest, never>;
|
|
25
|
+
/** `count` frames spread evenly over a film of `length` frames, first frame to last. */
|
|
26
|
+
export declare function spreadFrames(length: number, count: number): ReadonlyArray<number>;
|
|
27
|
+
/** Every `step` frames from the first, plus the last. */
|
|
28
|
+
export declare function everyFrames(length: number, step: number): ReadonlyArray<number>;
|
package/dist/points.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { InvalidRequest } from "./errors.js";
|
|
3
|
+
const refuse = (message) => new InvalidRequest({ message });
|
|
4
|
+
/** A clip's frames: [first, end), the frames on which it is on screen. */
|
|
5
|
+
export const clipRange = Effect.fnUntraced(function* (plan, id) {
|
|
6
|
+
const clip = plan.tracks.flatMap((t) => t.clips).find((c) => c.id === id);
|
|
7
|
+
if (!clip)
|
|
8
|
+
return yield* refuse(`There is no clip "${id}" in the film (clips: ${plan.tracks.flatMap((t) => t.clips.map((c) => c.id)).join(", ")}).`);
|
|
9
|
+
const range = { from: Math.max(0, Math.ceil(clip.start - 1e-6)), to: Math.ceil(clip.end - 1e-6) };
|
|
10
|
+
return range;
|
|
11
|
+
});
|
|
12
|
+
/** "90", "3s" or "50%" (of `whole` frames) as frames; null when `text` is none of those. */
|
|
13
|
+
const amount = Effect.fnUntraced(function* (text, plan, whole) {
|
|
14
|
+
const match = /^(\d+(?:\.\d+)?)(s|%)?$/.exec(text);
|
|
15
|
+
if (!match)
|
|
16
|
+
return null;
|
|
17
|
+
const n = Number(match[1]);
|
|
18
|
+
const unit = match[2];
|
|
19
|
+
if (unit === "s")
|
|
20
|
+
return Math.round(n * plan.fps);
|
|
21
|
+
if (unit === "%")
|
|
22
|
+
return Math.round((n / 100) * whole);
|
|
23
|
+
if (!Number.isInteger(n))
|
|
24
|
+
return yield* refuse(`"${text}" isn't a whole frame; write seconds as "${text}s".`);
|
|
25
|
+
return n;
|
|
26
|
+
});
|
|
27
|
+
/** A point in the film, as a frame: where a range starts or ends ("end" is the film's length). */
|
|
28
|
+
export const parsePoint = Effect.fnUntraced(function* (plan, text) {
|
|
29
|
+
const t = text.trim();
|
|
30
|
+
if (t === "end")
|
|
31
|
+
return plan.length;
|
|
32
|
+
const plain = yield* amount(t, plan, plan.length);
|
|
33
|
+
if (plain !== null)
|
|
34
|
+
return plain;
|
|
35
|
+
const inClip = /^([A-Za-z_][\w.-]*?)(?:\+(.+))?$/.exec(t);
|
|
36
|
+
if (inClip) {
|
|
37
|
+
const range = yield* clipRange(plan, inClip[1] ?? "");
|
|
38
|
+
const offset = inClip[2] === undefined ? 0 : yield* amount(inClip[2], plan, range.to - range.from);
|
|
39
|
+
if (offset === null)
|
|
40
|
+
return yield* refuse(`"${text}": after "+" write frames (30), seconds (1.5s) or a share of the clip (50%).`);
|
|
41
|
+
return range.from + offset;
|
|
42
|
+
}
|
|
43
|
+
return yield* refuse(`"${text}" isn't a point in the film: write a frame (90), seconds (3s), a share (50%) or a clip (site, site+30).`);
|
|
44
|
+
});
|
|
45
|
+
/** A frame to draw: a point, where the film's end ("end", "100%") is its last frame. */
|
|
46
|
+
export const parseFrame = Effect.fnUntraced(function* (plan, text) {
|
|
47
|
+
const point = yield* parsePoint(plan, text);
|
|
48
|
+
if (point === plan.length)
|
|
49
|
+
return Math.max(0, plan.length - 1);
|
|
50
|
+
if (point > plan.length) {
|
|
51
|
+
const what = text.trim() === String(point) ? `Frame ${point} is` : `"${text}" is frame ${point},`;
|
|
52
|
+
return yield* refuse(`${what} past the film's last frame (${plan.length - 1}).`);
|
|
53
|
+
}
|
|
54
|
+
return point;
|
|
55
|
+
});
|
|
56
|
+
/** The frames [from, to) a render covers: a clip's when `clip` is given, else the whole film, with
|
|
57
|
+
* `from` and `to` (points) moving either end. */
|
|
58
|
+
export const parseRange = Effect.fnUntraced(function* (plan, request) {
|
|
59
|
+
const base = request.clip === undefined ? { from: 0, to: plan.length } : yield* clipRange(plan, request.clip);
|
|
60
|
+
const from = request.from === undefined ? base.from : yield* parsePoint(plan, request.from);
|
|
61
|
+
const to = request.to === undefined ? base.to : yield* parsePoint(plan, request.to);
|
|
62
|
+
const range = { from, to };
|
|
63
|
+
return range;
|
|
64
|
+
});
|
|
65
|
+
/** `count` frames spread evenly over a film of `length` frames, first frame to last. */
|
|
66
|
+
export function spreadFrames(length, count) {
|
|
67
|
+
if (count <= 1 || length <= 1)
|
|
68
|
+
return [0];
|
|
69
|
+
const n = Math.min(count, length);
|
|
70
|
+
return Array.from({ length: n }, (_, i) => Math.round((i * (length - 1)) / (n - 1)));
|
|
71
|
+
}
|
|
72
|
+
/** Every `step` frames from the first, plus the last. */
|
|
73
|
+
export function everyFrames(length, step) {
|
|
74
|
+
const out = Array.from({ length: Math.ceil(length / step) }, (_, i) => i * step);
|
|
75
|
+
return out.at(-1) === length - 1 ? out : [...out, length - 1];
|
|
76
|
+
}
|
package/dist/project.d.ts
CHANGED
|
@@ -4,17 +4,21 @@ import { FilmFailure, InvalidRequest } from "./errors.js";
|
|
|
4
4
|
import { type ImportReport } from "./import/importer.js";
|
|
5
5
|
import type { FilmSource } from "./render/protocol.js";
|
|
6
6
|
export interface Project {
|
|
7
|
-
/** "project": a film project (tongue.json). "
|
|
8
|
-
|
|
7
|
+
/** "project": a film project (tongue.json). "build": a build folder (build.json). "module": one
|
|
8
|
+
* film module. */
|
|
9
|
+
readonly kind: "project" | "build" | "module";
|
|
10
|
+
/** What asset sources are relative to: the project, the module's folder, or for a build, the
|
|
11
|
+
* project it sits in (dist/ of a project) or else the build folder itself. */
|
|
9
12
|
readonly root: string;
|
|
10
|
-
/** The film
|
|
13
|
+
/** The film (absolute): a project's film.tsx, a module, or a build's build.json. */
|
|
11
14
|
readonly film: string;
|
|
12
|
-
/** Generated files: imported assets, builds, render work. */
|
|
15
|
+
/** Generated files: imported assets, builds, render work, caches. */
|
|
13
16
|
readonly workDir: string;
|
|
14
17
|
readonly assetsDir: string;
|
|
15
18
|
readonly outDir: string;
|
|
16
19
|
}
|
|
17
|
-
/** The project `path` is: a project folder, or a film module (inside a project, or
|
|
20
|
+
/** The project `path` is: a project folder, a build folder, or a film module (inside a project, or
|
|
21
|
+
* not). */
|
|
18
22
|
export declare const openProject: (path: string) => Effect.Effect<Project, FilmFailure, FileSystem.FileSystem | Path.Path>;
|
|
19
23
|
/** A film ready to render: its project, its planned timeline, and its imported assets. */
|
|
20
24
|
export interface OpenFilm {
|
|
@@ -24,6 +28,11 @@ export interface OpenFilm {
|
|
|
24
28
|
readonly imported: ImportReport;
|
|
25
29
|
/** How render processes load the same film. */
|
|
26
30
|
readonly source: FilmSource;
|
|
31
|
+
/** Where videos are decoded from when the film renders, by asset name (absolute paths). A
|
|
32
|
+
* video not named here is decoded from its source path under the root. */
|
|
33
|
+
readonly sources?: Readonly<Record<string, string>>;
|
|
34
|
+
/** The scene opened on its own, when one was. */
|
|
35
|
+
readonly scene?: string;
|
|
27
36
|
}
|
|
28
37
|
export interface OpenOptions {
|
|
29
38
|
/** One scene of a film project on its own, with these props (default: those of the first clip
|
|
@@ -32,9 +41,24 @@ export interface OpenOptions {
|
|
|
32
41
|
readonly id: string;
|
|
33
42
|
readonly props?: Json;
|
|
34
43
|
};
|
|
44
|
+
/** Where videos are decoded from when the film renders, by asset name: for footage that isn't
|
|
45
|
+
* at its source path under the root, such as a build rendered on a machine where the original
|
|
46
|
+
* files live somewhere else. Relative paths are from the working directory. A render decodes
|
|
47
|
+
* every video from its original file, and fails when one isn't there. */
|
|
48
|
+
readonly sources?: Readonly<Record<string, string>>;
|
|
35
49
|
}
|
|
36
|
-
/** Opens a film: a project folder (or a file in one), or a film module. */
|
|
50
|
+
/** Opens a film: a project folder (or a file in one), a build folder, or a film module. */
|
|
37
51
|
export declare const openFilm: (path: string, options?: OpenOptions | undefined) => Effect.Effect<OpenFilm, FilmFailure | import("./errors.js").AssetFailure | InvalidRequest | import("./errors.js").ToolFailure | import("@tongue-core/project").LayoutError | import("@tongue-core/project").CompileError | import("@tongue-core/project").LoadError | import("@tongue-core/project").TimelineError | import("@tongue-core/project").DocError | import("effect/Config").ConfigError | import("effect/PlatformError").PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
|
|
52
|
+
/** Where each of the film's videos is decoded from when it renders, by asset name: its `sources`
|
|
53
|
+
* entry, else its source path under the root. */
|
|
54
|
+
export declare const videoInputs: (film: OpenFilm) => Readonly<Record<string, string>>;
|
|
55
|
+
/** What a film's outputs are called: the project's folder (or the module's file) name, and the
|
|
56
|
+
* scene's id when one scene was opened on its own. */
|
|
57
|
+
export declare const outputName: (film: OpenFilm) => string;
|
|
58
|
+
/** Where a render goes when no `out` is given: out/<name>.mp4 under the root. */
|
|
59
|
+
export declare const defaultVideoOut: (film: OpenFilm) => string;
|
|
60
|
+
/** Where stills go when no `outDir` is given: out/stills (out/stills-<scene> for one scene). */
|
|
61
|
+
export declare const defaultStillsDir: (film: OpenFilm) => string;
|
|
38
62
|
/** Builds a film project into `out` (default: its dist folder): build.json, film.json and a bundle
|
|
39
63
|
* per scene. */
|
|
40
64
|
export declare const buildFilm: (path: string, options?: {
|