@tongue-core/sdk 0.1.0-rc.7 → 0.1.0-rc.9
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 +80 -17
- package/dist/build/build.d.ts +5 -2
- package/dist/build/build.js +29 -16
- package/dist/check.d.ts +2 -1
- package/dist/check.js +3 -2
- package/dist/client.d.ts +5 -1
- package/dist/client.js +7 -7
- package/dist/encode/encoders.js +2 -2
- package/dist/errors.d.ts +3 -2
- package/dist/errors.js +3 -1
- package/dist/import/convert.js +4 -2
- package/dist/import/importer.d.ts +23 -8
- package/dist/import/importer.js +28 -94
- package/dist/import/local.d.ts +17 -0
- package/dist/import/local.js +97 -0
- package/dist/import/media.d.ts +1 -1
- package/dist/import/media.js +6 -4
- package/dist/import/types.d.ts +10 -0
- package/dist/import/types.js +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -0
- package/dist/media/cache.d.ts +40 -0
- package/dist/media/cache.js +161 -0
- package/dist/media/prepare.d.ts +44 -0
- package/dist/media/prepare.js +206 -0
- package/dist/media/snapshot.d.ts +16 -0
- package/dist/media/snapshot.js +38 -0
- package/dist/project.d.ts +8 -2
- package/dist/project.js +38 -9
- package/dist/render/budget.d.ts +45 -0
- package/dist/render/budget.js +83 -0
- package/dist/render/events.d.ts +36 -0
- package/dist/render/events.js +1 -1
- package/dist/render/film-loader.js +15 -11
- package/dist/render/frame-pipe.d.ts +18 -0
- package/dist/render/frame-pipe.js +46 -0
- package/dist/render/frame.d.ts +2 -1
- package/dist/render/frame.js +16 -3
- package/dist/render/inputs.d.ts +1 -1
- package/dist/render/inputs.js +11 -1
- package/dist/render/pipeline.d.ts +8 -6
- package/dist/render/pipeline.js +79 -26
- package/dist/render/protocol.d.ts +28 -4
- package/dist/render/protocol.js +54 -13
- package/dist/render/runs.d.ts +10 -0
- package/dist/render/runs.js +42 -0
- package/dist/render/stills.d.ts +4 -4
- package/dist/render/stills.js +6 -5
- package/dist/render/stopwatch.d.ts +10 -0
- package/dist/render/stopwatch.js +16 -0
- package/dist/render/video-frames.d.ts +22 -2
- package/dist/render/video-frames.js +162 -58
- package/dist/render/worker.js +46 -14
- package/dist/studio/server.d.ts +3 -1
- package/dist/studio/server.js +25 -5
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -51,12 +51,12 @@ processes, from Bun): provide it once, where your application runs its Effects.
|
|
|
51
51
|
|---|---|
|
|
52
52
|
| `initProject(dir, { template, name })` | a film project in a new or empty folder, from a built-in template or a template folder |
|
|
53
53
|
| `listTemplates()` / `readTemplate(nameOrPath)` | the built-in templates, or one by name or path |
|
|
54
|
-
| `buildFilm(project, { out })` | builds a film project into `out` (default `dist/`): build.json, film.json, a bundle per scene |
|
|
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
|
|
54
|
+
| `buildFilm(project, { out, media })` | builds a film project into `out` (default `dist/`): build.json, film.json, a bundle per scene; `media` configures acquisition and hosted previews |
|
|
55
|
+
| `openFilm(path, { scene, sources, media })` | 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 either way. `scene: { id, props }` opens one scene of a project on its own; `sources` overrides video originals; `media` configures hosted-media acquisition and caching ([Footage](#footage)) |
|
|
56
56
|
| `describeFilm(film)` | the plan as text (for agents to read) and as a JSON summary |
|
|
57
57
|
| `checkFilm(path)` | problems and warnings as data |
|
|
58
58
|
| `renderStills(film, { frames, scale, outDir })` / `renderSheet(…)` | PNGs drawn by the wasm raster (the player's), or tiled into one labelled sheet |
|
|
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
|
|
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, encoder and workers are chosen (`budget` says why that many workers: one per physical core, fewer when memory is short), `onProgress` while it draws. A frame that throws fails the render with `Frame <n>: <message>`. The result's `profile` says where the time went: milliseconds per stage of drawing, decoder starts, pieces encoded, and the wall time of each phase |
|
|
60
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
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
62
|
| `videoInputs(film)`, `checkVideoInputs(film)` | where each video is decoded from, and a check that every one is there |
|
|
@@ -81,8 +81,9 @@ it. The folder may install `@tongue-core/film` too, for its editor's types
|
|
|
81
81
|
(`"jsx": "react-jsx", "jsxImportSource": "@tongue-core/film"` in its tsconfig); that copy isn't the one
|
|
82
82
|
that runs.
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
Local asset paths are relative to the root. Tongue writes `.tongue/` there for builds and render
|
|
85
|
+
work, and renders to `out/`. Local converted assets use the project's cache; hosted-media
|
|
86
|
+
downloads and preparation use the shared cache outside the project.
|
|
86
87
|
|
|
87
88
|
A film's assets are what its modules declare (`font()`, `image()`, `audio()`, `video()`) while
|
|
88
89
|
the SDK loads it, plus the media its clips play; `<Film assets={[…]}>` replaces the declared ones.
|
|
@@ -91,6 +92,59 @@ and a poster frame.
|
|
|
91
92
|
|
|
92
93
|
## Footage
|
|
93
94
|
|
|
95
|
+
Hosted URLs are the normal source for application-owned media. Declare them with `video`, `audio`,
|
|
96
|
+
`image` or `font`, with an explicit name and a SHA-256 when the application knows it. The SDK
|
|
97
|
+
streams downloads into a shared cache outside the project, verifies content, and atomically
|
|
98
|
+
promotes completed files. Concurrent requests for the same pinned content share a download.
|
|
99
|
+
Unpinned URLs are revalidated with ETag/Last-Modified when available. Temporary network failures,
|
|
100
|
+
408, 429 and server errors retry twice by default, with bounded backoff and Retry-After support;
|
|
101
|
+
missing files, denied access and integrity failures are reported as `MediaFailure`.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const tongue = createTongue({
|
|
105
|
+
media: {
|
|
106
|
+
cacheDir: "/var/cache/tongue/media",
|
|
107
|
+
store: { directory: "/srv/media", baseUrl: "https://media.example/prepared/" },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
await tongue.build("my-film");
|
|
111
|
+
await tongue.render("my-film/dist", { out: "out/film.mp4" });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A portable build contains manifests and scene bundles, with media locations and hashes in
|
|
115
|
+
`build.json` version 2. Originals and generated remote media are never copied into it. Local
|
|
116
|
+
assets can still be packaged, and version 1 builds remain readable. The host must expose the
|
|
117
|
+
configured store at its `baseUrl` (with CORS for browser clients). The CLI reads the equivalent
|
|
118
|
+
`TONGUE_MEDIA_DIR`, `TONGUE_MEDIA_URL` and `TONGUE_MEDIA_CACHE` environment variables. The default
|
|
119
|
+
cache is `$XDG_CACHE_HOME/tongue/media` or `~/.cache/tongue/media`. `tongue studio` supplies a
|
|
120
|
+
local media store and serves it automatically.
|
|
121
|
+
|
|
122
|
+
For object storage, provide `media.publish(upload)`, an Effect returning the uploaded file's
|
|
123
|
+
HTTP(S) URL. `upload` holds the local `file`, `hash`, `bytes` and `contentType`. Storage belongs
|
|
124
|
+
to the application. A publish callback must finish storing the bytes before returning its URL.
|
|
125
|
+
Use the hash as an idempotent storage key and return a stable URL, so projects share uploaded
|
|
126
|
+
bytes. The built-in directory store does this automatically. Invalid publisher locations fail
|
|
127
|
+
before a portable build is written.
|
|
128
|
+
|
|
129
|
+
Applications can prepare media at ingestion: `prepareMedia(asset, mediaOptions)` returns a
|
|
130
|
+
`tongue.media` manifest with a canonical asset name, import recipe, source hash, duration and
|
|
131
|
+
prepared files. Host that JSON and pass its URL as the declaration's `prepared` option. The same
|
|
132
|
+
preparation is shared across projects and asset names. Preparation recipes include sizing and
|
|
133
|
+
format versions, so incompatible results are rebuilt. Audio previews are sample-indexed WAV
|
|
134
|
+
segments; video previews use the existing segmented H.264 format. Full-quality decoded audio
|
|
135
|
+
remains available to final renders.
|
|
136
|
+
|
|
137
|
+
`media.request(url, signal, headers)` can add authentication or refresh a signed URL without
|
|
138
|
+
serializing credentials into the build. Preserve the supplied signal and conditional headers.
|
|
139
|
+
`retries`, `retryDelayMs`, `timeoutMs`, `concurrency` and `onProgress` control acquisition. Original
|
|
140
|
+
downloads default to a 30-minute timeout; metadata/preview requests default to two minutes.
|
|
141
|
+
Cache contents are shared and host-managed; changing the cache directory isolates a job.
|
|
142
|
+
|
|
143
|
+
Final rendering acquires and verifies its video originals before starting any frame worker.
|
|
144
|
+
Converted assets are also acquired before use. Workers decode local files, with no networking
|
|
145
|
+
in the per-frame path. A fully cached pinned build can render with networking disabled. Build
|
|
146
|
+
snapshots retain immutable prepared-file bindings when a later version changes its assets.
|
|
147
|
+
|
|
94
148
|
A render decodes every video from its **original file**: a build holds only a video's facts, a
|
|
95
149
|
poster and a preview-quality proxy (what the studio plays). Before a frame is drawn, `renderVideo`
|
|
96
150
|
and `renderStills` check that each video's file is there and fail with `AssetFailure` naming the
|
|
@@ -98,7 +152,7 @@ ones that aren't; a frame ffmpeg can't decode, or a file shorter than its import
|
|
|
98
152
|
render with `Frame <n>: Video "<name>": …` and ffmpeg's reason. A poster never stands in for
|
|
99
153
|
footage in a render (that is the player's fallback while frames arrive).
|
|
100
154
|
|
|
101
|
-
A video is decoded from its source path under the film's root, unless `sources` says otherwise:
|
|
155
|
+
A local video is decoded from its source path under the film's root, unless `sources` says otherwise:
|
|
102
156
|
|
|
103
157
|
```ts
|
|
104
158
|
const film = yield* openFilm("renders/1234/dist", { // a build, without the project around it
|
|
@@ -106,8 +160,9 @@ const film = yield* openFilm("renders/1234/dist", { // a build, without the pr
|
|
|
106
160
|
});
|
|
107
161
|
```
|
|
108
162
|
|
|
109
|
-
A build inside a project (`my-film/dist`) finds
|
|
110
|
-
needs `sources` for
|
|
163
|
+
A build inside a project (`my-film/dist`) finds local footage in the project; a build on its own
|
|
164
|
+
needs `sources` for local videos that aren't beside it. Remote originals are resolved automatically
|
|
165
|
+
from the pinned registry in the manifest.
|
|
111
166
|
|
|
112
167
|
## Sound processing (extensions)
|
|
113
168
|
|
|
@@ -200,20 +255,28 @@ child process), or it slows the hand-off of frames to the workers.
|
|
|
200
255
|
## How a video renders
|
|
201
256
|
|
|
202
257
|
```
|
|
203
|
-
coordinator ──segments──▶ worker 1: frames ─▶ raster ─▶ encoder ─▶
|
|
204
|
-
◀─progress── worker 2 …
|
|
258
|
+
coordinator ──segments──▶ worker 1: frames ─▶ raster ─▶ encoder ─▶ piece-000000.mp4
|
|
259
|
+
◀─progress── worker 2 … piece-000280.mp4
|
|
205
260
|
soundtrack: sounds + audio clips ─▶ mix ─▶ AAC audio.m4a
|
|
206
261
|
with sound processing: stems ─▶ processors (here, cached) ─▶ sum ─▶ master ─▶ limiter ─▶ AAC
|
|
207
262
|
join: concat + audio ─(stream copy)─▶ film.mp4, frame count probed
|
|
208
263
|
```
|
|
209
264
|
|
|
210
|
-
- **Workers** are processes (`src/render/worker.ts`), one per physical core by default
|
|
211
|
-
the
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
265
|
+
- **Workers** are processes (`src/render/worker.ts`), one per physical core by default, fewer when
|
|
266
|
+
the memory available wouldn't hold that many (`src/render/budget.ts`). Each loads
|
|
267
|
+
the film and its raster once and draws every segment it is sent, so the JIT stays warm and glyphs
|
|
268
|
+
and images stay uploaded.
|
|
269
|
+
- **Segments** are a quarter to half a second of frames: what a worker is given at a time. Each
|
|
270
|
+
worker starts on its own contiguous run of them; one that finishes early takes over the later
|
|
271
|
+
half of the longest run left (`src/render/runs.ts`). So every worker draws long contiguous
|
|
272
|
+
stretches, and its video decoders read straight through instead of seeking.
|
|
273
|
+
- **Pieces** are what is encoded: each contiguous stretch a worker draws goes to one encoder, as one
|
|
274
|
+
closed-GOP file; the join copies streams without re-encoding. Which pieces a render makes depends
|
|
275
|
+
on how the work was shared out; the frames don't.
|
|
276
|
+
- **Video clips** are decoded by ffmpeg from their original files, in order, frame after frame, a
|
|
277
|
+
few frames ahead, through a FIFO that holds ffmpeg back when it gets ahead; restarted on jumps,
|
|
278
|
+
a window at a time for clips playing backwards, and closed when unused. A frame that can't be
|
|
279
|
+
decoded fails the render ([Footage](#footage)).
|
|
217
280
|
- **The soundtrack** is mixed in its own process while the workers draw (`soundtrack-process.ts`);
|
|
218
281
|
with sound processing, it writes the stems, the processors run in the coordinator, and it adds
|
|
219
282
|
them back, limits and encodes (`sound/soundtrack.ts`).
|
package/dist/build/build.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type Build, type ModuleHost, type ProjectFiles } from "@tongue-core/pro
|
|
|
3
3
|
import { Effect, FileSystem, type PlatformError } from "effect";
|
|
4
4
|
import { type ImportReport } from "../import/importer.js";
|
|
5
5
|
import type { Project } from "../project.js";
|
|
6
|
+
import { type MediaOptions } from "../media/cache.js";
|
|
6
7
|
/** A project's code and data: every .ts, .tsx and .json file outside hidden folders and the ones
|
|
7
8
|
* tools write. */
|
|
8
9
|
export declare const readProjectFiles: (root: string) => Effect.Effect<ProjectFiles, PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
@@ -11,6 +12,7 @@ export declare const fileModuleHost: (dir: string) => ModuleHost;
|
|
|
11
12
|
/** Points the project's `.tongue/types/film` at this SDK's @tongue-core/film (tsconfig.json reads it). */
|
|
12
13
|
export declare const linkFilmTypes: (root: string) => Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
13
14
|
export interface BuiltProject {
|
|
15
|
+
readonly assetsDir: string;
|
|
14
16
|
readonly build: Build;
|
|
15
17
|
/** Where the build was written. */
|
|
16
18
|
readonly dir: string;
|
|
@@ -25,6 +27,7 @@ export declare const importedFiles: (project: Project, assets: readonly Asset[])
|
|
|
25
27
|
readonly path: string;
|
|
26
28
|
readonly bytes: number;
|
|
27
29
|
readonly hash: string;
|
|
30
|
+
readonly url?: string | undefined;
|
|
28
31
|
}[], PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
29
32
|
/** How a written build reaches its assets: copies of the files (a build to publish), or a link to
|
|
30
33
|
* the project's imported assets (a build for Tongue's own use). */
|
|
@@ -43,6 +46,6 @@ export declare const writeBuildFiles: (dir: string, build: Build, assets: AssetP
|
|
|
43
46
|
export declare const keepBuild: (project: Project, build: Build) => Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
44
47
|
/** Builds a film project into `dir` (what `tongue build` does: dist/ by default), replacing what
|
|
45
48
|
* was there, with copies of its imported assets: a folder any static host can serve. */
|
|
46
|
-
export declare const buildFilmProject: (project: Project, dir: string) => Effect.Effect<BuiltProject, import("../errors.js").AssetFailure | 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 | PlatformError.PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | import("effect/Path").Path>;
|
|
49
|
+
export declare const buildFilmProject: (project: Project, dir: string, options?: MediaOptions | undefined) => Effect.Effect<BuiltProject, import("../errors.js").AssetFailure | import("../errors.js").ToolFailure | import("@tongue-core/project/media").MediaFailure | 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 | PlatformError.PlatformError | import("effect/Schema").SchemaError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | import("effect/Path").Path>;
|
|
47
50
|
/** Builds a film project for Tongue's own use (stills, renders, dev): kept in .tongue/builds. */
|
|
48
|
-
export declare const buildForWork: (project: Project) => Effect.Effect<BuiltProject, import("../errors.js").AssetFailure | 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 | PlatformError.PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | import("effect/Path").Path>;
|
|
51
|
+
export declare const buildForWork: (project: Project, options?: MediaOptions | undefined) => Effect.Effect<BuiltProject, import("../errors.js").AssetFailure | import("../errors.js").ToolFailure | import("@tongue-core/project/media").MediaFailure | 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 | PlatformError.PlatformError | import("effect/Schema").SchemaError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | import("effect/Path").Path>;
|
package/dist/build/build.js
CHANGED
|
@@ -14,6 +14,9 @@ import { compileProject, extractFilm, IGNORED_DIRS, loadProject, sha256, TYPES_L
|
|
|
14
14
|
import { Effect, FileSystem } from "effect";
|
|
15
15
|
import { importAssets } from "../import/importer.js";
|
|
16
16
|
import { sdkVersion } from "../version.js";
|
|
17
|
+
import { isRemoteSource } from "@tongue-core/project/media";
|
|
18
|
+
import { mediaCacheDir, mediaLock } from "../media/cache.js";
|
|
19
|
+
import { snapshotAssets } from "../media/snapshot.js";
|
|
17
20
|
// Building a film project on this machine: read its code, compile and bundle it, run the bundles,
|
|
18
21
|
// import the assets they declare (media lengths go into film.json), extract film.json and write
|
|
19
22
|
// the build, with the imported asset files listed in build.json (and copied beside it, or linked,
|
|
@@ -99,6 +102,8 @@ export const writeBuildFiles = Effect.fn("writeBuildFiles")(function* (dir, buil
|
|
|
99
102
|
return;
|
|
100
103
|
}
|
|
101
104
|
for (const file of build.manifest.assets.files) {
|
|
105
|
+
if (file.url !== undefined)
|
|
106
|
+
continue;
|
|
102
107
|
const path = join(assetsDir, file.path);
|
|
103
108
|
yield* fs.makeDirectory(dirname(path), { recursive: true });
|
|
104
109
|
yield* fs.copyFile(join(assets.from, file.path), path);
|
|
@@ -135,28 +140,36 @@ const pruneBuilds = Effect.fn("pruneBuilds")(function* (builds, keep) {
|
|
|
135
140
|
});
|
|
136
141
|
/** A film project built in memory: compiled, its bundles run, its assets imported (their lengths
|
|
137
142
|
* go into film.json), film.json extracted. */
|
|
138
|
-
const buildInMemory = Effect.fn("buildInMemory")(function* (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
const buildInMemory = Effect.fn("buildInMemory")(function* (input, options = {}) {
|
|
144
|
+
return yield* mediaLock(`project-build:${input.root}`, Effect.gen(function* () {
|
|
145
|
+
let project = input;
|
|
146
|
+
const files = yield* readProjectFiles(project.root);
|
|
147
|
+
const compiled = yield* compileProject(files);
|
|
148
|
+
const loaded = yield* loadProject(compiled, fileModuleHost(join(project.workDir, "modules")));
|
|
149
|
+
if (loaded.assets.some((asset) => isRemoteSource(asset.source)))
|
|
150
|
+
project = { ...project, assetsDir: join(mediaCacheDir(options), "projects", createHash("sha256").update(project.root).digest("hex"), "assets") };
|
|
151
|
+
const imported = yield* importAssets(project, loaded.assets, options);
|
|
152
|
+
const media = { duration: (asset) => imported.durations.get(`${asset.kind}:${asset.name}`) ?? null };
|
|
153
|
+
const doc = yield* extractFilm(loaded, { media });
|
|
154
|
+
const local = yield* importedFiles(project, loaded.assets.filter((asset) => !isRemoteSource(asset.source)));
|
|
155
|
+
const build = writeBuild(compiled, doc, { tongue: yield* sdkVersion, assets: [...local, ...(imported.remoteFiles ?? [])], originals: imported.originals });
|
|
156
|
+
if ((imported.originals?.length ?? 0) > 0 && !options.portable)
|
|
157
|
+
project = { ...project, assetsDir: yield* snapshotAssets(project.assetsDir, build.manifest.assets.files, imported.remoteFiles ?? [], options) };
|
|
158
|
+
yield* linkFilmTypes(project.root);
|
|
159
|
+
return { build, imported, scenes: new Map(loaded.scenes.map((s) => [s.id, s.scene])), media, assetsDir: project.assetsDir };
|
|
160
|
+
}));
|
|
148
161
|
});
|
|
149
162
|
/** Builds a film project into `dir` (what `tongue build` does: dist/ by default), replacing what
|
|
150
163
|
* was there, with copies of its imported assets: a folder any static host can serve. */
|
|
151
|
-
export const buildFilmProject = Effect.fn("buildFilmProject")(function* (project, dir) {
|
|
152
|
-
const built = yield* buildInMemory(project);
|
|
153
|
-
yield* writeBuildFiles(dir, built.build, { kind: "copy", from:
|
|
164
|
+
export const buildFilmProject = Effect.fn("buildFilmProject")(function* (project, dir, options = {}) {
|
|
165
|
+
const built = yield* buildInMemory(project, { ...options, portable: true });
|
|
166
|
+
yield* writeBuildFiles(dir, built.build, { kind: "copy", from: built.assetsDir });
|
|
154
167
|
const out = { ...built, dir };
|
|
155
168
|
return out;
|
|
156
169
|
});
|
|
157
170
|
/** Builds a film project for Tongue's own use (stills, renders, dev): kept in .tongue/builds. */
|
|
158
|
-
export const buildForWork = Effect.fn("buildForWork")(function* (project) {
|
|
159
|
-
const built = yield* buildInMemory(project);
|
|
160
|
-
const out = { ...built, dir: yield* keepBuild(project, built.build) };
|
|
171
|
+
export const buildForWork = Effect.fn("buildForWork")(function* (project, options = {}) {
|
|
172
|
+
const built = yield* buildInMemory(project, options);
|
|
173
|
+
const out = { ...built, dir: yield* keepBuild({ ...project, assetsDir: built.assetsDir }, built.build) };
|
|
161
174
|
return out;
|
|
162
175
|
});
|
package/dist/check.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PlanSummary } from "@tongue-core/film";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
+
import { type OpenOptions } from "./project.js";
|
|
3
4
|
export interface CheckProblem {
|
|
4
5
|
/** layout, compile, load, timeline, doc (film.json), asset. */
|
|
5
6
|
readonly kind: string;
|
|
@@ -15,4 +16,4 @@ export interface CheckReport {
|
|
|
15
16
|
readonly summary: PlanSummary | null;
|
|
16
17
|
readonly text: string | null;
|
|
17
18
|
}
|
|
18
|
-
export declare const checkFilm: (path: string) => Effect.Effect<CheckReport, import("./errors.js").InvalidRequest | import("./errors.js").ToolFailure | import("effect/Config").ConfigError | import("effect/PlatformError").PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/FileSystem").FileSystem | import("effect/Path").Path>;
|
|
19
|
+
export declare const checkFilm: (path: string, options?: OpenOptions | undefined) => Effect.Effect<CheckReport, import("./errors.js").InvalidRequest | import("./errors.js").ToolFailure | import("effect/Config").ConfigError | import("effect/PlatformError").PlatformError | import("effect/Schema").SchemaError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/FileSystem").FileSystem | import("effect/Path").Path>;
|
package/dist/check.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import { describeFilm, openFilm } from "./project.js";
|
|
3
3
|
const failed = (problems) => ({ ok: false, problems, warnings: [], summary: null, text: null });
|
|
4
|
-
export const checkFilm = Effect.fn("checkFilm")(function* (path) {
|
|
5
|
-
return yield* openFilm(path).pipe(Effect.map((film) => {
|
|
4
|
+
export const checkFilm = Effect.fn("checkFilm")(function* (path, options = {}) {
|
|
5
|
+
return yield* openFilm(path, options).pipe(Effect.map((film) => {
|
|
6
6
|
const { text, summary } = describeFilm(film);
|
|
7
7
|
return { ok: true, problems: [], warnings: film.plan.warnings, summary, text };
|
|
8
8
|
}), Effect.catchTags({
|
|
9
9
|
FilmFailure: (e) => Effect.succeed(failed([{ kind: e.phase === "load" ? "load" : "timeline", file: e.film, message: e.message }])),
|
|
10
10
|
AssetFailure: (e) => Effect.succeed(failed([{ kind: "asset", file: null, message: e.message }])),
|
|
11
|
+
MediaFailure: (e) => Effect.succeed(failed([{ kind: "asset", file: null, message: e.message }])),
|
|
11
12
|
LayoutError: (e) => Effect.succeed(failed(e.problems.map((p) => ({ kind: "layout", file: p.file, message: p.message })))),
|
|
12
13
|
CompileError: (e) => Effect.succeed(failed([{ kind: "compile", file: e.line === null ? e.file : `${e.file}:${e.line}`, message: e.message }])),
|
|
13
14
|
LoadError: (e) => Effect.succeed(failed([{ kind: "load", file: e.file, message: e.message }])),
|
package/dist/client.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { type InitOptions } from "./init.js";
|
|
|
2
2
|
import { type OpenOptions } from "./project.js";
|
|
3
3
|
import { type VideoOptions } from "./render/pipeline.js";
|
|
4
4
|
import { type StillsOptions } from "./render/stills.js";
|
|
5
|
-
|
|
5
|
+
import type { MediaOptions } from "./media/cache.js";
|
|
6
|
+
export declare function createTongue(configuration?: {
|
|
7
|
+
readonly media?: MediaOptions;
|
|
8
|
+
}): {
|
|
6
9
|
/** Makes a film project in a new or empty folder from a template. */
|
|
7
10
|
init: (dir: string, options?: InitOptions) => Promise<import("./init.js").InitReport>;
|
|
8
11
|
/** The templates that come with Tongue. */
|
|
@@ -18,6 +21,7 @@ export declare function createTongue(): {
|
|
|
18
21
|
/** Builds a film project: build.json, film.json and a bundle per scene, in its dist folder. */
|
|
19
22
|
build: (project: string, options?: {
|
|
20
23
|
readonly out?: string;
|
|
24
|
+
readonly media?: MediaOptions;
|
|
21
25
|
}) => Promise<import("./index.js").BuiltProject>;
|
|
22
26
|
/** Imports the film's assets and plans it; resolves to its timeline as text and JSON. */
|
|
23
27
|
info: (film: string) => Promise<{
|
package/dist/client.js
CHANGED
|
@@ -14,24 +14,24 @@ const run = async (effect) => {
|
|
|
14
14
|
return exit.value;
|
|
15
15
|
throw Cause.squash(exit.cause);
|
|
16
16
|
};
|
|
17
|
-
export function createTongue() {
|
|
17
|
+
export function createTongue(configuration = {}) {
|
|
18
18
|
return {
|
|
19
19
|
/** Makes a film project in a new or empty folder from a template. */
|
|
20
20
|
init: (dir, options) => run(initProject(dir, options)),
|
|
21
21
|
/** The templates that come with Tongue. */
|
|
22
22
|
templates: () => run(listTemplates()),
|
|
23
23
|
/** Builds a film project: build.json, film.json and a bundle per scene, in its dist folder. */
|
|
24
|
-
build: (project, options) => run(buildFilm(project, options)),
|
|
24
|
+
build: (project, options) => run(buildFilm(project, { media: configuration.media, ...options })),
|
|
25
25
|
/** Imports the film's assets and plans it; resolves to its timeline as text and JSON. */
|
|
26
|
-
info: (film) => run(openFilm(film).pipe(Effect.map(describeFilm))),
|
|
26
|
+
info: (film) => run(openFilm(film, configuration).pipe(Effect.map(describeFilm))),
|
|
27
27
|
/** Problems and warnings as data: never rejects for a broken film. */
|
|
28
|
-
check: (film) => run(checkFilm(film)),
|
|
28
|
+
check: (film) => run(checkFilm(film, configuration)),
|
|
29
29
|
/** Frames as PNG files. */
|
|
30
|
-
stills: (film, options) => run(openFilm(film, options).pipe(Effect.flatMap((f) => renderStills(f, options)))),
|
|
30
|
+
stills: (film, options) => run(openFilm(film, { ...configuration, ...options }).pipe(Effect.flatMap((f) => renderStills(f, options)))),
|
|
31
31
|
/** Frames tiled into one labelled image. */
|
|
32
|
-
sheet: (film, options) => run(openFilm(film, options).pipe(Effect.flatMap((f) => renderSheet(f, options)))),
|
|
32
|
+
sheet: (film, options) => run(openFilm(film, { ...configuration, ...options }).pipe(Effect.flatMap((f) => renderSheet(f, options)))),
|
|
33
33
|
/** The film (or a range of it, or one scene of a project) as an MP4. */
|
|
34
|
-
render: (film, options) => run(openFilm(film, options).pipe(Effect.flatMap((f) => renderVideo(f, options)))),
|
|
34
|
+
render: (film, options) => run(openFilm(film, { ...configuration, ...options }).pipe(Effect.flatMap((f) => renderVideo(f, options)))),
|
|
35
35
|
/** The rasters and encoders this machine can use. */
|
|
36
36
|
backends: () => run(listBackends()),
|
|
37
37
|
};
|
package/dist/encode/encoders.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// (VAAPI on AMD and Intel under Linux, NVENC on NVIDIA, Quick Sync on Intel) differ only in the
|
|
3
3
|
// arguments around the input. `available()` runs a short encode through the exact same arguments,
|
|
4
4
|
// so it proves the ffmpeg build, the device and the driver all work. Plain TypeScript: encoders run
|
|
5
|
-
// in the render loop. The settings are the ones measured in docs/render-
|
|
5
|
+
// in the render loop. The settings are the ones measured in docs/render-performance.md.
|
|
6
6
|
export class EncoderError extends Error {
|
|
7
7
|
name = "EncoderError";
|
|
8
8
|
}
|
|
@@ -94,7 +94,7 @@ export async function encoderWorks(ffmpeg, spec, size = { width: 320, height: 24
|
|
|
94
94
|
}
|
|
95
95
|
export const DEFAULT_SETTINGS = { preset: "veryfast", crf: 14, threads: 2, qp: null, gop: null, x264Params: "" };
|
|
96
96
|
export const X264_PRESETS = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"];
|
|
97
|
-
/** Constant QP that clears the master-quality bar on our films
|
|
97
|
+
/** Constant QP that clears the master-quality bar on our films. */
|
|
98
98
|
const HARDWARE_QP = { h264: 16, hevc: 18 };
|
|
99
99
|
const profile = (codec) => (codec === "hevc" ? "main" : "high");
|
|
100
100
|
const vaapiDevice = () => process.env["TONGUE_VAAPI_DEVICE"] ?? "/dev/dri/renderD128";
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CompileError, DocError, LayoutError, LoadError, TemplateError, TimelineError } from "@tongue-core/project";
|
|
2
2
|
import { Schema } from "effect";
|
|
3
|
+
import { MediaFailure } from "@tongue-core/project/media";
|
|
3
4
|
declare const FilmFailure_base: Schema.Class<FilmFailure, Schema.TaggedStruct<"FilmFailure", {
|
|
4
5
|
readonly film: Schema.String;
|
|
5
6
|
/** "load" (the module threw or isn't there), "film" (not a <Film>, or the timeline is wrong). */
|
|
@@ -54,9 +55,9 @@ declare const ProcessorFailure_base: Schema.Class<ProcessorFailure, Schema.Tagge
|
|
|
54
55
|
* channels, another length) or isn't audio (a sample that isn't a finite number). */
|
|
55
56
|
export declare class ProcessorFailure extends ProcessorFailure_base {
|
|
56
57
|
}
|
|
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]>;
|
|
58
|
+
export declare const SdkError: Schema.Union<readonly [typeof FilmFailure, typeof AssetFailure, typeof InvalidRequest, typeof ToolFailure, typeof BackendUnavailable, typeof RenderFailure, typeof ProcessorFailure, typeof MediaFailure, typeof LayoutError, typeof CompileError, typeof LoadError, typeof TimelineError, typeof DocError, typeof TemplateError]>;
|
|
58
59
|
export type SdkError = typeof SdkError.Type;
|
|
59
|
-
export declare const isSdkError: <I>(input: I) => input is I & (FilmFailure | AssetFailure | InvalidRequest | ToolFailure | BackendUnavailable | RenderFailure | ProcessorFailure | LayoutError | CompileError | LoadError | TimelineError | DocError | TemplateError);
|
|
60
|
+
export declare const isSdkError: <I>(input: I) => input is I & (FilmFailure | AssetFailure | InvalidRequest | ToolFailure | BackendUnavailable | RenderFailure | ProcessorFailure | MediaFailure | LayoutError | CompileError | LoadError | TimelineError | DocError | TemplateError);
|
|
60
61
|
/** Exhaustive: a new error without a case is a compile error. */
|
|
61
62
|
export declare function exitCodeOf(error: SdkError): number;
|
|
62
63
|
/** The message of anything thrown. */
|
package/dist/errors.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CompileError, DocError, LayoutError, LoadError, TemplateError, TimelineError } from "@tongue-core/project";
|
|
2
2
|
import { Schema } from "effect";
|
|
3
|
+
import { MediaFailure } from "@tongue-core/project/media";
|
|
3
4
|
// Every way the SDK can fail, as tagged errors with a message an agent (or a person) can act on.
|
|
4
5
|
// `exitCodeOf` maps each to the CLI's exit code: 2 when the input is wrong (fix the film or the
|
|
5
6
|
// arguments), 3 when the machine is (a missing tool, a device, a crash). Building a film project
|
|
@@ -52,7 +53,7 @@ export class ProcessorFailure extends Schema.TaggedError()("ProcessorFailure", {
|
|
|
52
53
|
}) {
|
|
53
54
|
}
|
|
54
55
|
export const SdkError = Schema.Union([
|
|
55
|
-
FilmFailure, AssetFailure, InvalidRequest, ToolFailure, BackendUnavailable, RenderFailure, ProcessorFailure,
|
|
56
|
+
FilmFailure, AssetFailure, InvalidRequest, ToolFailure, BackendUnavailable, RenderFailure, ProcessorFailure, MediaFailure,
|
|
56
57
|
LayoutError, CompileError, LoadError, TimelineError, DocError, TemplateError,
|
|
57
58
|
]);
|
|
58
59
|
export const isSdkError = Schema.is(SdkError);
|
|
@@ -60,6 +61,7 @@ export const isSdkError = Schema.is(SdkError);
|
|
|
60
61
|
export function exitCodeOf(error) {
|
|
61
62
|
switch (error._tag) {
|
|
62
63
|
case "FilmFailure":
|
|
64
|
+
case "MediaFailure":
|
|
63
65
|
case "AssetFailure":
|
|
64
66
|
case "InvalidRequest":
|
|
65
67
|
case "LayoutError":
|
package/dist/import/convert.js
CHANGED
|
@@ -11,9 +11,11 @@ export function fontToTtf(bytes) {
|
|
|
11
11
|
}
|
|
12
12
|
/** Decodes the image formats the engine reads itself; null for the rest (ffmpeg decodes those). */
|
|
13
13
|
export function decodeImage(bytes, extension) {
|
|
14
|
-
|
|
14
|
+
const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
15
|
+
const jpeg = bytes[0] === 255 && bytes[1] === 216;
|
|
16
|
+
if (png || extension === ".png")
|
|
15
17
|
return decodePng(bytes, (z) => new Uint8Array(inflateSync(z)));
|
|
16
|
-
if (extension === ".jpg" || extension === ".jpeg")
|
|
18
|
+
if (jpeg || extension === ".jpg" || extension === ".jpeg")
|
|
17
19
|
return decodeJpeg(bytes);
|
|
18
20
|
return null;
|
|
19
21
|
}
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import type { Asset } from "@tongue-core/film";
|
|
2
2
|
import { Effect, FileSystem, Path } from "effect";
|
|
3
|
-
import { AssetFailure } from "../errors.js";
|
|
4
3
|
import type { Project } from "../project.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
readonly
|
|
10
|
-
}
|
|
11
|
-
|
|
4
|
+
import { type MediaOptions } from "../media/cache.js";
|
|
5
|
+
export type { ImportReport } from "./types.js";
|
|
6
|
+
/** Both source kinds converge on the engine's virtual asset paths. */
|
|
7
|
+
export declare const importAssets: (project: Project, assets: readonly Asset[], configuration?: (MediaOptions & {
|
|
8
|
+
readonly portable?: boolean;
|
|
9
|
+
}) | undefined) => Effect.Effect<{
|
|
10
|
+
imported: string[];
|
|
11
|
+
skipped: string[];
|
|
12
|
+
durations: Map<string, number>;
|
|
13
|
+
remoteFiles: {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly bytes: number;
|
|
16
|
+
readonly hash: string;
|
|
17
|
+
readonly url?: string | undefined;
|
|
18
|
+
}[];
|
|
19
|
+
originals: {
|
|
20
|
+
readonly kind: "font" | "image" | "audio" | "video";
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly source: string;
|
|
23
|
+
readonly hash: string;
|
|
24
|
+
readonly bytes: number;
|
|
25
|
+
}[];
|
|
26
|
+
}, import("../errors.js").AssetFailure | import("../errors.js").ToolFailure | import("@tongue-core/project/media").MediaFailure | import("effect/Config").ConfigError | import("effect/PlatformError").PlatformError | import("effect/Schema").SchemaError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
|
package/dist/import/importer.js
CHANGED
|
@@ -1,96 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Effect, FileSystem, Path
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
yield* fs.writeFile(target, bytes);
|
|
30
|
-
});
|
|
31
|
-
const results = yield* Effect.forEach(assets, (asset) => Effect.gen(function* () {
|
|
32
|
-
const key = keyOf(asset);
|
|
33
|
-
const source = path.resolve(project.root, asset.source);
|
|
34
|
-
const stat = yield* fs.stat(source).pipe(Effect.mapError(() => new AssetFailure({ asset: key, message: `${asset.kind} "${asset.name}": no file at ${asset.source} (relative to ${project.root}).` })));
|
|
35
|
-
const modified = stat.mtime._tag === "Some" ? stat.mtime.value.getTime() : 0;
|
|
36
|
-
const fingerprint = `${Number(stat.size)}:${modified}:${optionsOf(asset)}`;
|
|
37
|
-
const known = previous.assets[key];
|
|
38
|
-
const output = path.join(project.assetsDir, importedPath(asset));
|
|
39
|
-
if (known?.fingerprint === fingerprint && (yield* fs.exists(output).pipe(Effect.orElseSucceed(() => false)))) {
|
|
40
|
-
return { key, entry: known, imported: false };
|
|
41
|
-
}
|
|
42
|
-
const read = fs.readFile(source).pipe(Effect.mapError((e) => new AssetFailure({ asset: key, message: `Could not read ${source}: ${e.message}` })));
|
|
43
|
-
const convert = (run) => Effect.try({ try: run, catch: (e) => new AssetFailure({ asset: key, message: `${asset.kind} "${asset.name}" (${asset.source}): ${messageOf(e)}` }) });
|
|
44
|
-
let duration;
|
|
45
|
-
switch (asset.kind) {
|
|
46
|
-
case "font": {
|
|
47
|
-
const bytes = yield* read;
|
|
48
|
-
yield* write(importedPath(asset), yield* convert(() => fontToTtf(bytes)));
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
case "image": {
|
|
52
|
-
const bytes = yield* read;
|
|
53
|
-
const decoded = yield* Effect.try({ try: () => decodeImage(bytes, path.extname(source).toLowerCase()), catch: (e) => new AssetFailure({ asset: key, message: `${asset.source}: ${messageOf(e)}` }) });
|
|
54
|
-
const image = decoded ?? (yield* imageViaFfmpeg(key, source));
|
|
55
|
-
yield* write(importedPath(asset), yield* convert(() => imageToQoi(image, asset.maxSize)));
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
case "audio": {
|
|
59
|
-
duration = (yield* probeMedia(key, source)).duration;
|
|
60
|
-
yield* write(importedPath(asset), yield* audioToWav(source));
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
case "video": {
|
|
64
|
-
const video = yield* importVideo(key, source, asset.maxSize);
|
|
65
|
-
duration = video.duration;
|
|
66
|
-
// The proxy, made fresh into its folder; its segments' facts go into info.json.
|
|
67
|
-
const proxyDir = path.join(project.assetsDir, videoProxyDir(asset));
|
|
68
|
-
yield* fs.remove(proxyDir, { recursive: true, force: true });
|
|
69
|
-
yield* fs.makeDirectory(proxyDir, { recursive: true });
|
|
70
|
-
yield* makeProxy(source, video.info, proxyDir).pipe(Effect.mapError((e) => new AssetFailure({ asset: key, message: `video "${asset.name}": its proxy couldn't be made: ${e.message}` })));
|
|
71
|
-
const names = (yield* fs.readDirectory(proxyDir)).filter((n) => n.endsWith(".mp4")).sort();
|
|
72
|
-
const segments = yield* Effect.forEach(names, (n) => fs.readFile(path.join(proxyDir, n)));
|
|
73
|
-
const proxy = yield* proxyInfo(asset.name, segments);
|
|
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 })));
|
|
77
|
-
yield* write(videoPosterPath(asset), video.poster);
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return { key, entry: duration === undefined ? { fingerprint } : { fingerprint, duration }, imported: true };
|
|
82
|
-
}), { concurrency: 4 });
|
|
83
|
-
const manifest = { version: 1, assets: Object.fromEntries(results.map((r) => [r.key, r.entry])) };
|
|
84
|
-
yield* fs.makeDirectory(project.assetsDir, { recursive: true });
|
|
85
|
-
yield* fs.writeFileString(manifestPath, JSON.stringify(manifest, null, 2));
|
|
86
|
-
const durations = new Map();
|
|
87
|
-
for (const r of results)
|
|
88
|
-
if (r.entry.duration !== undefined)
|
|
89
|
-
durations.set(r.key, r.entry.duration);
|
|
90
|
-
const report = {
|
|
91
|
-
imported: results.filter((r) => r.imported).map((r) => r.key),
|
|
92
|
-
skipped: results.filter((r) => !r.imported).map((r) => r.key),
|
|
93
|
-
durations,
|
|
1
|
+
import { isRemoteSource } from "@tongue-core/project/media";
|
|
2
|
+
import { Effect, FileSystem, Path } from "effect";
|
|
3
|
+
import { importRemoteAsset } from "../media/prepare.js";
|
|
4
|
+
import { mediaOptions } from "../media/cache.js";
|
|
5
|
+
import { decodeImportManifest, importLocalAssets } from "./local.js";
|
|
6
|
+
/** Both source kinds converge on the engine's virtual asset paths. */
|
|
7
|
+
export const importAssets = Effect.fn("importAssets")(function* (project, assets, configuration = {}) {
|
|
8
|
+
const options = mediaOptions(configuration);
|
|
9
|
+
const local = yield* importLocalAssets(project, assets.filter((asset) => !isRemoteSource(asset.source)));
|
|
10
|
+
const remote = yield* Effect.forEach(assets.filter((asset) => isRemoteSource(asset.source)), (asset) => importRemoteAsset(project, asset, options, configuration.portable ?? false), { concurrency: options.concurrency ?? 4 });
|
|
11
|
+
const durations = new Map(local.durations);
|
|
12
|
+
for (const item of remote)
|
|
13
|
+
if (item.duration !== undefined)
|
|
14
|
+
durations.set(item.key, item.duration);
|
|
15
|
+
if (remote.length > 0) {
|
|
16
|
+
const fs = yield* FileSystem.FileSystem;
|
|
17
|
+
const path = yield* Path.Path;
|
|
18
|
+
const file = path.join(project.assetsDir, "manifest.json");
|
|
19
|
+
const manifest = yield* fs.readFileString(file).pipe(Effect.flatMap(decodeImportManifest));
|
|
20
|
+
const entries = { ...manifest.assets };
|
|
21
|
+
for (const item of remote)
|
|
22
|
+
entries[item.key] = { fingerprint: `remote:${item.original.hash}`, ...(item.duration === undefined ? {} : { duration: item.duration }) };
|
|
23
|
+
yield* fs.writeFileString(file, JSON.stringify({ version: 1, assets: entries }, null, 2));
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
imported: [...local.imported, ...remote.filter((item) => !item.cached).map((item) => item.key)],
|
|
27
|
+
skipped: [...local.skipped, ...remote.filter((item) => item.cached).map((item) => item.key)],
|
|
28
|
+
durations, remoteFiles: remote.flatMap((item) => item.files), originals: remote.map((item) => item.original),
|
|
94
29
|
};
|
|
95
|
-
return report;
|
|
96
30
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ImportReport } from "./types.js";
|
|
2
|
+
import type { Asset } from "@tongue-core/film";
|
|
3
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
4
|
+
import { AssetFailure } from "../errors.js";
|
|
5
|
+
import type { Project } from "../project.js";
|
|
6
|
+
export declare const decodeImportManifest: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Effect.Effect<{
|
|
7
|
+
readonly version: 1;
|
|
8
|
+
readonly assets: {
|
|
9
|
+
readonly [x: string]: {
|
|
10
|
+
readonly fingerprint: string;
|
|
11
|
+
readonly duration?: number | undefined;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}, Schema.SchemaError, never>;
|
|
15
|
+
/** What else an import depends on: a different size, or proxies made a new way, imports again. */
|
|
16
|
+
export declare const PROXY_VERSION = 2;
|
|
17
|
+
export declare const importLocalAssets: (project: Project, assets: readonly Asset[]) => Effect.Effect<ImportReport, AssetFailure | import("../errors.js").ToolFailure | import("effect/Config").ConfigError | import("effect/PlatformError").PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
|