Prioritize native video and Bun migration

This commit is contained in:
aphrody-dev committed 2026-10-13 12:09:26 +02:00
1 parent e2f499cbd5
commit 4500d36811
37 files changed
+1414 -169

No files matched your search

+6 -5
View File
@@ -1,8 +1,9 @@
# Goal
Create and publish an Aphrody/Bun monorepo with selected Storytold repositories
as open source libraries in Git submodules. Use our Bun 1.4.4 runtime and pinned
Bun Tauri v3 integration. Preserve Rust owners and source licenses.
Prioritize the FilmCraft source library for Bun video integration. Reuse its
Rust project, media and export owners through a bounded typed Bun adapter.
Preserve exact timeline integers, native ownership, Aphrody/Bun identity and
explicit runtime qualification. Existing source forks remain independent.
Deliver pinned forks, portable library commands, the Aphrody shell, Bun frontend
tooling and explicit build qualification.
Use native Bun quality commands, migrate JavaScript with aggressive n2b, and
improve its internal Rust owner for bounded large monorepo scans.
+15 -1
View File
@@ -5,6 +5,20 @@
- Imported sources are libraries. The shell exposes capabilities and headless
CLI inspection, without launching upstream branded GUIs.
- Runtime: Bun 1.4.4 at 2ae8fc65fba1d05c423a2433b1195835109942b6.
- SDK: published Bun commit c209b95eb0dcc964745f4684f127abb83cbe2697.
- SDK: published Bun commit 939c835adf6479bc1014da3843cda4ae29851dbd.
- Preserve independent Cargo workspaces and dependency revisions.
- Provider authentication and raw sessions remain in isolated stores.
- Video is the current priority. Reuse the native source CLI and Bun.Video;
no codec implementation or FFmpeg subprocess belongs in this adapter.
- FilmCraft schema 15 uses signed i64 ticks and unsigned u64 IDs. Preserve
integer precision across JSON rather than routing them through unsafe Numbers.
- The installed Bun revision currently lacks Bun.Video. The parallel native
owner is replacing the optional media provider; this batch leaves it untouched.
- Quality gates use native bun check, bun lint and bun fmt. The formatter config
is bunfmt.json; external checker/linter/formatter packages are removed.
- Aggressive n2b changes require review: embedded host scripts retain their own
runtime, and Buffer or callback rewrites must preserve their actual contract.
- Media export uses a fresh destination directory because native formats derive
sibling and sequence filenames. Failed operations retain structured results.
- Paths remain checkout-relative under the selected C:\src workspace. Native
owners are resolved from live tools and configuration after workspace changes.
+16
View File
@@ -11,3 +11,19 @@
- [x] Apply current Linux, Debian, COSMIC and Bun upstream conventions.
- [x] Complete scoped type, lint, format and build checks.
- [x] Publish the modified library and prepare the verified monorepo branch.
## Video priority
- [x] Audit FilmCraft format, media, time, CLI and export contracts.
- [x] Coordinate native Bun.Video ownership with the parallel Bun session.
- [x] Add bounded native library inspection, probing, editing and export APIs.
- [x] Preserve exact i64/u64 JSON and timeline ticks.
- [x] Surface video priority and native availability in the Aphrody shell.
- [x] Complete scoped gates and publish the integration.
## Native tooling
- [x] Replace external quality packages with native Bun commands.
- [x] Apply aggressive n2b to authored code and review its recommendations.
- [x] Complete aggressive library and SDK migration through their owners.
- [x] Patch the internal n2b owner for large monorepos through a subagent.
+6 -1
View File
@@ -3,6 +3,9 @@
A Bun 1.4.4 monorepo with 16 open source library forks and our pinned Bun Tauri
v3 SDK. Aphrody owns the desktop identity; Bun owns JavaScript tooling.
Video is the current priority. [The video adapter](docs/video.md) reuses native
project, timeline, codec and export owners through Bun.
## Setup
Requires our `aphrody-labs/bun` runtime, Git and native Rust. Windows uses MSVC
@@ -25,6 +28,7 @@ bun run catalog # library inventory
bun run status --json # pinned source state
bun run dev # local Aphrody shell
bun run build # Bun frontend/backend build
bun run fmt # native Bun formatting
bun run desktop:dev # native Bun Tauri shell
bun run desktop:build # native bundle and Bun sidecar
bun run workspace run photocraft --help # headless library inspection
@@ -54,5 +58,6 @@ The imported React source builds with Bun. Its historical Tauri v2 shell and
tests remain source references; our desktop uses Bun Tauri v3.
[Architecture](docs/architecture.md), [provenance](docs/provenance.md),
[style](docs/style.md), [qualification](docs/qualification.md). Source licenses and notices apply.
[style](docs/style.md), [migration](docs/migration.md), [qualification](docs/qualification.md).
Source licenses and notices apply.
JavaScript builds do not qualify native engines, CEF, installers or providers.
+23 -22
View File
@@ -12,17 +12,35 @@
"source": {
"path": "toolchains/bun",
"repository": "aphrody-labs/bun",
"revision": "c209b95eb0dcc964745f4684f127abb83cbe2697"
"revision": "939c835adf6479bc1014da3843cda4ae29851dbd"
}
},
"modules": [
{
"id": "filmcraft",
"path": "libraries/filmcraft",
"upstream": "storytold/filmcraft",
"fork": "aphrody-labs/filmcraft",
"branch": "main",
"revision": "87b845138e2cecf43e97e2c2e011b820a6e70ecd",
"upstreamRevision": "cee1feb185e675a108f78067f71ad86a640ace3d",
"license": "MIT OR Apache-2.0",
"kind": "library",
"description": "Video",
"manifestPath": "Cargo.toml",
"binaries": ["filmcraft-cli"],
"stars": 11833,
"binary": "filmcraft-cli",
"cargoPackage": "filmcraft-cli",
"role": "engine"
},
{
"id": "artcraft",
"path": "libraries/artcraft",
"upstream": "storytold/artcraft",
"fork": "aphrody-labs/artcraft",
"branch": "main",
"revision": "e8de597d0bb13be2ed12c1085fd84dcc1c913f27",
"revision": "f8dae6cec51cc84c8b565132db2c7471fad96148",
"upstreamRevision": "d4e4ac69bbd93d237eaf5d0260bc6a5ff3a5e373",
"license": "MIT OR Apache-2.0",
"kind": "library",
@@ -68,31 +86,13 @@
"cargoPackage": "vectorcraft-cli",
"role": "engine"
},
{
"id": "filmcraft",
"path": "libraries/filmcraft",
"upstream": "storytold/filmcraft",
"fork": "aphrody-labs/filmcraft",
"branch": "main",
"revision": "cee1feb185e675a108f78067f71ad86a640ace3d",
"upstreamRevision": "cee1feb185e675a108f78067f71ad86a640ace3d",
"license": "MIT OR Apache-2.0",
"kind": "library",
"description": "Video",
"manifestPath": "Cargo.toml",
"binaries": ["filmcraft-cli"],
"stars": 11833,
"binary": "filmcraft-cli",
"cargoPackage": "filmcraft-cli",
"role": "engine"
},
{
"id": "effectcraft",
"path": "libraries/effectcraft",
"upstream": "storytold/effectcraft",
"fork": "aphrody-labs/effectcraft",
"branch": "main",
"revision": "470ea7a3af82ee1dd1d2f86c777fe6891bd0c37b",
"revision": "e5beea940ed245dc8e6c9293d6f7717b6c3f2724",
"upstreamRevision": "470ea7a3af82ee1dd1d2f86c777fe6891bd0c37b",
"license": "MIT OR Apache-2.0",
"kind": "library",
@@ -296,5 +296,6 @@
"stars": 296,
"role": "fonts"
}
]
],
"priorities": ["filmcraft"]
}
+3
View File
@@ -1,6 +1,7 @@
import { join, resolve } from "node:path";
import { loadConfig, assertRuntime, repositoryRoot } from "@aphrody/bun-workspace";
import { capabilityFor, type Catalog } from "./shared.ts";
import { nativeVideoCapabilities } from "@aphrody/bun-video-project/native";
export const desktopRoot = import.meta.dir;
@@ -9,10 +10,12 @@ export async function buildDesktop() {
assertRuntime(config);
const catalog: Catalog = {
runtime: { version: config.runtime.version, repository: config.runtime.repository },
video: { nativeAvailable: nativeVideoCapabilities().available },
libraries: config.modules
.filter(module => module.kind === "library")
.map(module => ({
id: module.id,
priority: config.priorities?.includes(module.id) ?? false,
...capabilityFor(module.id),
license: module.license,
initialized: false,
+1 -1
View File
@@ -1,5 +1,5 @@
import { buildDesktop } from "./build.ts";
const output = await buildDesktop();
process.env.APHRODY_WEB_ROOT = output + "/web";
Bun.env.APHRODY_WEB_ROOT = output + "/web";
await import("./server.ts");
+1
View File
@@ -12,6 +12,7 @@
"desktop:build": "bun native.ts build"
},
"dependencies": {
"@aphrody/bun-video-project": "workspace:*",
"@aphrody/bun-workspace": "workspace:*",
"@aphrody/tauri": "file:../../toolchains/bun/packages/bun-tauri"
},
+7 -4
View File
@@ -2,10 +2,11 @@ import { dirname, join, resolve } from "node:path";
import { serveForTauri } from "@aphrody/tauri/server";
import { assertRuntime, listModuleStatus, loadConfig, runModule } from "@aphrody/bun-workspace";
import { capabilityFor, type Catalog, type Inspection } from "./shared.ts";
import { nativeVideoCapabilities } from "@aphrody/bun-video-project/native";
async function findWorkspace(): Promise<string> {
if (process.env.APHRODY_ROOT) {
const root = resolve(process.env.APHRODY_ROOT);
if (Bun.env.APHRODY_ROOT) {
const root = resolve(Bun.env.APHRODY_ROOT);
if (!(await Bun.file(join(root, "aphrody.config.json")).exists())) {
throw new Error("APHRODY_ROOT must contain aphrody.config.json.");
}
@@ -28,7 +29,7 @@ const config = await loadConfig(root);
assertRuntime(config);
const session = crypto.randomUUID();
const inspections = new Map<string, Inspection>();
const staticRoot = process.env.APHRODY_WEB_ROOT ? resolve(process.env.APHRODY_WEB_ROOT) : join(import.meta.dir, "web");
const staticRoot = Bun.env.APHRODY_WEB_ROOT ? resolve(Bun.env.APHRODY_WEB_ROOT) : join(import.meta.dir, "web");
const headers = {
"Cache-Control": "no-store",
"X-Content-Type-Options": "nosniff",
@@ -45,12 +46,14 @@ async function catalog(): Promise<Catalog> {
const statuses = await listModuleStatus(config, root);
return {
runtime: { version: config.runtime.version, repository: config.runtime.repository },
video: { nativeAvailable: nativeVideoCapabilities().available },
libraries: config.modules
.filter(module => module.kind === "library")
.map(module => {
const status = statuses.find(item => item.id === module.id);
return {
id: module.id,
priority: config.priorities?.includes(module.id) ?? false,
...capabilityFor(module.id),
license: module.license,
initialized: status?.initialized ?? false,
@@ -64,7 +67,7 @@ async function catalog(): Promise<Catalog> {
}
const server = serveForTauri({
port: process.env.PORT ? Number(process.env.PORT) : 0,
port: Bun.env.PORT ? Number(Bun.env.PORT) : 0,
maxRequestBodySize: 1024,
async fetch(request) {
const url = new URL(request.url);
+2
View File
@@ -6,6 +6,7 @@ export interface Library {
initialized: boolean;
inspectable: boolean;
inspectReason?: string;
priority?: boolean;
}
export interface Inspection {
@@ -21,6 +22,7 @@ export interface Catalog {
runtime: { version: string; repository: string };
inspections: Inspection[];
session?: string;
video?: { nativeAvailable: boolean };
}
const capabilities: Record<string, [string, string]> = {
+10 -9
View File
@@ -127,6 +127,12 @@ function libraryIcon(library: Library): HTMLDivElement {
node.append(svg(categories.find(item => item.id === category(library))!.path));
return node;
}
function libraryState(library: Library): string {
if (latest(library.id)?.state === "running") return "Inspecting";
if (!library.initialized) return "Source unavailable";
if (library.priority) return "Priority";
return library.inspectable ? "CLI available" : "Library source";
}
function card(library: Library): HTMLElement {
const node = document.createElement("article");
node.className = "app-card";
@@ -135,14 +141,7 @@ function card(library: Library): HTMLElement {
top.append(libraryIcon(library));
const status = document.createElement("span");
status.className = "app-state";
status.textContent =
latest(library.id)?.state === "running"
? "Inspecting"
: !library.initialized
? "Source unavailable"
: library.inspectable
? "CLI available"
: "Library source";
status.textContent = libraryState(library);
top.append(status);
const title = document.createElement("h3");
title.textContent = library.title;
@@ -197,7 +196,9 @@ function render() {
const title = document.createElement("h2");
title.textContent = "Aphrody";
const description = document.createElement("p");
description.textContent = "Native libraries. Bun orchestration. One local workspace.";
description.textContent = catalog.video?.nativeAvailable
? "Video first. Native media and exact timelines."
: "Video first. Native Bun video is not available in this binary.";
copy.append(title, description);
const mark = document.createElement("div");
mark.className = "featured-mark";
+13 -101
View File
@@ -6,19 +6,27 @@
"name": "@aphrody/craft",
"devDependencies": {
"@aphrody/bun-types": "file:toolchains/bun/packages/bun-types",
"@typescript/native-preview": "7.0.0-dev.20260707.2",
"oxfmt": "0.72.0",
"oxlint": "1.87.0",
},
},
"apps/desktop": {
"name": "@aphrody/bun-desktop",
"version": "0.1.0",
"dependencies": {
"@aphrody/bun-video-project": "workspace:*",
"@aphrody/bun-workspace": "workspace:*",
"@aphrody/tauri": "file:../../toolchains/bun/packages/bun-tauri",
},
},
"packages/video": {
"name": "@aphrody/bun-video-project",
"version": "0.1.0",
"bin": {
"aphrody-video": "./src/cli.ts",
},
"dependencies": {
"@aphrody/bun-workspace": "workspace:*",
},
},
"packages/workspace": {
"name": "@aphrody/bun-workspace",
"version": "0.1.0",
@@ -32,108 +40,12 @@
"@aphrody/bun-types": ["@aphrody/bun-types@file:toolchains/bun/packages/bun-types", { "dependencies": { "@types/node": "*", "undici-types": "*" } }],
"@aphrody/bun-video-project": ["@aphrody/bun-video-project@workspace:packages/video"],
"@aphrody/bun-workspace": ["@aphrody/bun-workspace@workspace:packages/workspace"],
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.72.0", "", { "os": "android", "cpu": "arm" }, "sha512-u4U1uaDOVBuorGNC/07NLm48nECHOiJJka002YGML16Md2DRvf38RGBt01lH60EOGvNlFVmbLoksQedNBoZHKw=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.72.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GCnM+Ae2mWOKC+92y3FOb6eZJeVnBAVz82e3lITUUg+kxukYKzBNAvJ/hhHIfa2A0md55SYYhdWPddSSy3KPVA=="],
"@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.72.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LjQ8tlevdtwraFlWC6AnQ1Forzde0clRmnsP3QmK+ydSElDt/J+5Jc+m+//Ly1G97aP7WCNUw110QMIUw2K7xA=="],
"@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.72.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+ZbcxhB1C34RQR5IldSyY6Z/yalJIxHXhP36PGE4sEq0f/lgL5jCLmOKoDeVm8dqd9gFGDsd1bNWHSGCAwdSkw=="],
"@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.72.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yGPhA8sePEBFV9iGcprhyOk3Bz3fZshswMH3DpYIPZFLT1DG5LjiZ0j6AJQpSueaL6bUK0av1K+5+fe+ldy5EQ=="],
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.72.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/g5axuqFMcAtoQrFLR5EJEMDjR5Ea4GuzpdDmMpPWAgrF1DmL4kD1NPNVlpXEOxpFJnD6lPvLKYj1lzdlrfifA=="],
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.72.0", "", { "os": "linux", "cpu": "arm" }, "sha512-InVitdHbS09PM+w+WinIiyX37rC9R3L7lUP9mKy9uKTEDSUFaLjWsLoreRIxKivnv7Ndqc6Trvi+K+b6ZzhiJg=="],
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.72.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKjlyRAxpPjXoqeuwFBim5LscgvY+RNEiZ971C9JlRQZONhJTsXDki/oHFYQE+fzzRpzXh3owZvOdT3kNL2g+g=="],
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.72.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-poafnbsPpJhR+5Mbwpf8CscugTyiRAzjTOxRVn8ra3X2jnURs2xL5Du2xl5g2iW85lICMglpmLf8jDjZEbNgzQ=="],
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.72.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zNUi0gKRsO7S3IfPzsTbnm88OedPuX32hmZFWZRWHvWJlAoZRwVbeO6Nb3hng4vDgL7qUKp4TGqzPz1mhsycYg=="],
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.72.0", "", { "os": "linux", "cpu": "none" }, "sha512-qTY7z/iyF/rWI2HWwWMevXqZW5PZUOofUy2afK7v6rseXnpld3yjgyIk6pCux3DqWQ0lB+f5atvLJNBwzLvFsw=="],
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.72.0", "", { "os": "linux", "cpu": "none" }, "sha512-w18qfo8vEMD6EKEHtzTvjc6jH8yCyXIT1Bn7PFWJd0PpZyzH/VQDx4xvjCh0LGz6GRzqiezbisTdG9AoXtpkZQ=="],
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.72.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-grW8HwEir+vK7fCcq1ZgL3NHQCfZ1PidiERE9Hbf+24xGys+DoDaoqTht4ccfn0PcBwbViLkwkda/KNpJu2KRQ=="],
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.72.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wQJRQfWBRIn88Pkr449EUViwRayrYUScg7ty+Ihiem21mQ9BUA6S9e/4RAgyNk9lMag0KBouyKskjq61rOC0WQ=="],
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.72.0", "", { "os": "linux", "cpu": "x64" }, "sha512-T1B0uQzafxuGPjjaPrZJ497nxoi1BiI65JfsYc56nNpmev3msfb/iaTmH2kQuNFqa8PKxpMlf3GhWrWBvBPORQ=="],
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.72.0", "", { "os": "none", "cpu": "arm64" }, "sha512-OKxxgTurl7+hypVplO8QRAbbQlwEs0j1Y6FZv+ZN2qNbdxQk2muMlzuJNBGTCWwxQ0rkkOD1uK/DW1fvzoUSJA=="],
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.72.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-L78TKzURqvjuZOCo88ixMbPXf/RP7u31Y0cmjJ/KEmrHmfYhmhEvsDaTYPiyTRne64jUgCbADfY0YeoQk4/Xyw=="],
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.72.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-gQtFW+Ii8BWgtJTAD17TY/WsdNS5gUfAPov8iI1jeq4r9lRRYImmxPL08UU00/RLaCfBJwh2UgINnDY0iOiZ+A=="],
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.72.0", "", { "os": "win32", "cpu": "x64" }, "sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.87.0", "", { "os": "android", "cpu": "arm" }, "sha512-22tVIHlB9898OYsBKmYB6He1MCSqdTmK1BVWuSNUTVrpS0KWNege+U9uEYqGJ+KatQag0r/oU44i3OOs46z7HQ=="],
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.87.0", "", { "os": "android", "cpu": "arm64" }, "sha512-JfZQ3MWf74n8ov0DojZpkOduIubYJvZL5tbTMMhDutsal6u1xH1QuNpN+lK+EQwF8AeAfgwTR2fgh5U2br27XA=="],
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.87.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rkx6Xvvw93z2GImVOyvbZwAZcoY0Z6MJW177XNsSB+i/CBsH7ANyelTp01VZyIkM3iSXtqKVb4+ptBgPHap5pQ=="],
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.87.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cw5nYw7L/pQ2SgGBgOcg/Qo89llQre3vIc/kuBLhSICzu1r5s8L76H3ug6AIFsD98WpROj8/z5JVzkf4daxHaQ=="],
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.87.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-x/SuznE6loRZK9gz9AS4ZmzNzSu+5KdeoCS+sVHhnrE8HM6AD8UyTjwldQTTqlXGvkwHASgB0NiTNOc3o3PdAg=="],
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.87.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+JEDmCAXiiFmKhAjZhgAOwaHWY6WENOQ5QA+lFuY59tgsUCDOTRi46B+lENK6CWvtxsXpwkZXNOgpjAQcpIpnA=="],
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.87.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/uC9XqXLAC9AHFLNycwmSQEvKqGrOCgU+uXALI7gNW0DG81TJ/3T2eHnKdRvcb5R1gFEfjxDwI4e4zIa29rICQ=="],
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.87.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-FTRYPsBy/M728vnUv7l+KAwSAa3Qi4BQbhVvy7NYuMTZ0sUa/OWsbMx79HVFw6u/5KWcaYayDsoOiv7xbevzDg=="],
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.87.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6sC+N7IY0h1egd8SrYUGNN7CUyKJZ4bIZakKa7V7hfinf4jfi8Kq4+klJvFp5DqXcwMn44I5MeBwmJluO1odFg=="],
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.87.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-d5Mnn+ICbhoJPi37Ccjy+86T5qfH2+mSTrTMBXNPfB+wqSWOayBBLLIrCXdUy1YssluCVdGkl+DO9w5r5ODmUA=="],
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.87.0", "", { "os": "linux", "cpu": "none" }, "sha512-sf5YWuUo1Dx9loqWCgF8P278MycfcD4JHjDC/LV5j11L3Lqa4PFiwCjD6B8kP4AjsgZZkLn3xbpJEtZBaccs4Q=="],
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.87.0", "", { "os": "linux", "cpu": "none" }, "sha512-+aCraYbmK3uU3P0rFojjeaK9pGY6JvBDXh/0CM+gPyWKmNmXmnFe0qj4qj02U4ibFpbd9Pa9CmDvIdjJUd29rA=="],
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.87.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-7rzbZQIljo6VLWVPqUyLa2P/WV3VFsJB4M52R4NCUJn1A50yjtUsHxiMHFL9opEKGFIiTbZ3VQzmtcJOPUq4NQ=="],
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.87.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dry46Fa+AXE/xt9J/YriujU1pU9mzfNWHGQwtOYibAFH8HLrKWCi93AhIGnuUMiwn2JbuBkRyz51f50Ly4r4ow=="],
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.87.0", "", { "os": "linux", "cpu": "x64" }, "sha512-f/HMZez74gJvnRpGz+b5sdGyWrEgRLmFTXg9sNoqjPWogGxYHUhXKTKyMiYsu9rUGR2ciYqL3fk+Ua4JhMJbRA=="],
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.87.0", "", { "os": "none", "cpu": "arm64" }, "sha512-lEC6ZfPZm/07JlB7FsOWX9o1RiHlXiBW594LSUGuXmekC8LZmvUPP9XzE2WZcNxJi5cHA/Fw56o02c3JxgMA5g=="],
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.87.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-afBWKOD36dUiA9AenaiWpCg8xfyUVPYznyAAVXu413brK4RA5UI/Xoc4yfeGIT8G1a44MV+JR7hioryerBDzGQ=="],
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.87.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-daptNsEOuru4/nJTxV4DBHNJ5GepzmaBVQf+jjgKQGi9NRlZKik+RFq4hUL7ewsF1pwjKM+SkYaMbF82UwFGnw=="],
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.87.0", "", { "os": "win32", "cpu": "x64" }, "sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw=="],
"@types/node": ["@types/node@26.6.5", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260707.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260707.2" }, "bin": { "tsgo": "bin/tsgo" } }, "sha512-oUGp+Rep/hqMhPunyinsALUwSlzHINSxitifPiSaeqoKOKD2OlR9NE3TaPqwsl4NlGslsOSUXI1JotWQzpYCPg=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wny2pgKjGbiZtnOIHVa3tXC1UfDqxNEFzyPGmiqybedG8hipG2Nfp0l5UxbaKCjkLacUpH/W5bP2hBOMVhCOzg=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Afc7M5zOwo+GpfcYwz5Z8HMB2tPVsui7nNIqEuuFB73MPdVqNn/Wmpe4tP4MRri0AtJnJknoHBaTJ/VDAp/Jhw=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm" }, "sha512-hJm/UOqZTr9FHmR7uNm8VGX4oKtfWk0Jem0zPeJFNC8ckGUfSBueyiEYMZB+XmRc1aG4x1E46y3CplP4CLHvGQ=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-iITBa2WjjTI5N9t5l7Z4KoOSI+2zBlhbvFzsD/f8qX8QoKjz/Y4DPyBDgezYi8nkqjjksbgSOJ3/ykzhwrB9cg=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "x64" }, "sha512-du0dzi6y97Po5vDNdPJTyyijHCpaS22JLRnKZEJXBDaO9gCIymOv/5QQokFRuOlQm0bWl3i9PF4OVdGP6uAOQA=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SsAwfhyHJ1akgBc+99z4+hwdbHsdWaKB8EwCNIMA6JfSLMeUjffrYvxu+vfMyxVtOVOz7RrRXRoiDiu4a2sCtg=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "x64" }, "sha512-DL4u27stv0fo71sVhOzHSwE+YMZsbBijVI+kg5dLDLilSH79WFTJ8RSQ46vJrCMt+Gjlv/JOZP1PuLJDfioYeQ=="],
"oxfmt": ["oxfmt@0.72.0", "", { "dependencies": { "tinypool": "2.2.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.72.0", "@oxfmt/binding-android-arm64": "0.72.0", "@oxfmt/binding-darwin-arm64": "0.72.0", "@oxfmt/binding-darwin-x64": "0.72.0", "@oxfmt/binding-freebsd-x64": "0.72.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.72.0", "@oxfmt/binding-linux-arm-musleabihf": "0.72.0", "@oxfmt/binding-linux-arm64-gnu": "0.72.0", "@oxfmt/binding-linux-arm64-musl": "0.72.0", "@oxfmt/binding-linux-ppc64-gnu": "0.72.0", "@oxfmt/binding-linux-riscv64-gnu": "0.72.0", "@oxfmt/binding-linux-riscv64-musl": "0.72.0", "@oxfmt/binding-linux-s390x-gnu": "0.72.0", "@oxfmt/binding-linux-x64-gnu": "0.72.0", "@oxfmt/binding-linux-x64-musl": "0.72.0", "@oxfmt/binding-openharmony-arm64": "0.72.0", "@oxfmt/binding-win32-arm64-msvc": "0.72.0", "@oxfmt/binding-win32-ia32-msvc": "0.72.0", "@oxfmt/binding-win32-x64-msvc": "0.72.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g=="],
"oxlint": ["oxlint@1.87.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.87.0", "@oxlint/binding-android-arm64": "1.87.0", "@oxlint/binding-darwin-arm64": "1.87.0", "@oxlint/binding-darwin-x64": "1.87.0", "@oxlint/binding-freebsd-x64": "1.87.0", "@oxlint/binding-linux-arm-gnueabihf": "1.87.0", "@oxlint/binding-linux-arm-musleabihf": "1.87.0", "@oxlint/binding-linux-arm64-gnu": "1.87.0", "@oxlint/binding-linux-arm64-musl": "1.87.0", "@oxlint/binding-linux-ppc64-gnu": "1.87.0", "@oxlint/binding-linux-riscv64-gnu": "1.87.0", "@oxlint/binding-linux-riscv64-musl": "1.87.0", "@oxlint/binding-linux-s390x-gnu": "1.87.0", "@oxlint/binding-linux-x64-gnu": "1.87.0", "@oxlint/binding-linux-x64-musl": "1.87.0", "@oxlint/binding-openharmony-arm64": "1.87.0", "@oxlint/binding-win32-arm64-msvc": "1.87.0", "@oxlint/binding-win32-ia32-msvc": "1.87.0", "@oxlint/binding-win32-x64-msvc": "1.87.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2003", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg=="],
"tinypool": ["tinypool@2.2.0", "", {}, "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ=="],
"undici-types": ["undici-types@8.11.2", "", {}, "sha512-iMVNmWZ0leK/goS6eXMizSzmm9CDWtyphwbaCms3DNLqRxDL+mMoNVcZMTyyVgXP0N+Z8neAMzDoUOUJL8veKg=="],
"@aphrody/bun-desktop/@aphrody/tauri": ["@aphrody/tauri@file:toolchains/bun/packages/bun-tauri", {}],
View File
File renamed without changes.
+4
View File
@@ -6,12 +6,16 @@ flowchart TD
UI --> Backend[Bun backend]
Backend --> Workspace[Native library orchestration]
Workspace --> Sources[Pinned library submodules]
Backend --> Video[Typed video adapter]
Video --> Headless[Native video CLI]
Video --> NativeVideo[Bun.Video when available]
Sources --> Rust[Existing Rust engines and headless CLIs]
Sources --> Shared[Shared libraries and fonts]
```
- `apps/desktop`: Aphrody shell, built and served by Bun.
- `packages/workspace`: config, provenance, source state and native execution.
- `packages/video`: exact JSON, bounded native operations and runtime availability.
- `libraries/*`: 16 independent open source forks.
- `toolchains/bun`: pinned SDK and runtime types.
+22
View File
@@ -0,0 +1,22 @@
# Native Bun migration
Quality commands are native `bun check`, `bun lint` and `bun fmt`. Root tooling
uses the pinned fork and bunfmt.json. The native checker covers 41 authored
files; the imported frontend covers 1,479 files.
Aggressive n2b ran across the 16 library inputs and the SDK with two workers.
Rewrites were reviewed. Browser and embedded host scripts retain their runtime;
native Buffer compatibility remains where a replacement cannot preserve its
contract. Unsupported mock helpers and invalid callback rewrites were corrected.
The internal Rust owner is
[packages/bun-n2b](https://github.com/aphrody-labs/bun/tree/176fa03201a78cd9f4ac6e839294d81ef4852435/packages/bun-n2b).
The patch adds explicit submodule traversal, bounded scan/report retention,
accurate clean-file counts, nested package resolution and AST rewrite guards.
Directory pruning avoids repeated ancestor checks. Limit failures explicitly
report an incomplete scan.
The native Cargo check passed for the core, CLI and addon on Windows. Rust 1.97
remains the declared minimum; the gate used the installed Rust 1.99 toolchain.
The installed n2b addon remains 0.7.0. Its fresh artifact has not been built or
installed, so new scanner flags are source-qualified only.
+1 -1
View File
@@ -34,6 +34,6 @@ source NOTICE, which identifies restricted assets.
Our runtime is Bun 1.4.4 at `2ae8fc65fba1d05c423a2433b1195835109942b6`.
Its declared sourceUrl belongs to our fork; this does not verify an archive hash.
The SDK uses published commit `c209b95eb0dcc964745f4684f127abb83cbe2697`.
The SDK uses published commit `939c835adf6479bc1014da3843cda4ae29851dbd`.
It contains our Tauri v3 alpha.4 fork, versioned 1.4.4. The npm SDK was unavailable,
so JS and Rust resolve the pinned source. Native CLI: `aphrody tauri`.
+11 -6
View File
@@ -2,23 +2,28 @@
Native Windows, 2026-10-13. Bun 1.4.4 fork revision
`2ae8fc65fba1d05c423a2433b1195835109942b6`; SDK commit
`c209b95eb0dcc964745f4684f127abb83cbe2697`.
`939c835adf6479bc1014da3843cda4ae29851dbd`.
Passed:
- Fork creation and Git submodule initialization: 16 library sources plus SDK.
- Runtime version, revision, provenance and external Bun resolution.
- TypeScript native compiler, oxlint and oxfmt for authored workspace/desktop code.
- Native Bun check, lint and fmt for authored workspace, video and desktop code.
- Bun frontend/backend build: 16 capabilities in the Aphrody shell.
- Imported React frontend build: 33 artifacts using Bun, PostCSS and existing assets.
- Frozen Bun installs and source diff checks.
- Offline native Cargo metadata and lockfile generation: 526 packages.
- Rust source formatting and native wrapper output forwarding.
- Native video CLI release build: locked dependencies, one Cargo job, Windows MSVC.
- Video doctor, command discovery and description through the bounded adapter.
- Aggressive n2b across all 16 library inputs and the SDK, with reviewed rewrites.
- Imported frontend native Bun check: 1,479 files; strict lint and frozen install.
- Internal Rust n2b core, CLI and addon Cargo check on Windows; see [migration](migration.md).
Not qualified: Rust engine compilation, headless CLI execution, native CEF
windows, installers, provider access, signing and CI execution. No tests were
added or run. Existing upstream launcher tests still expect Vite/HMR; their
test configurations retain required dependencies.
Not qualified: other Rust engines, media decoding, rendering, export accuracy,
native Bun.Video, CEF windows, installers, provider access, signing and CI
execution. No tests were added or run. Upstream launcher specifications retain
their original frontend expectations.
The source migration covers JavaScript build and development tooling. Rust
engines remain native libraries. The imported historical shell remains Tauri v2;
+2 -1
View File
@@ -11,7 +11,8 @@ licenses and owner conventions. These references govern authored code.
| Sources/builds | Independent dependencies, exact revisions, preserved notices and non-interactive builds | [Debian Policy](https://www.debian.org/doc/debian-policy/ch-source.html) |
Use guard clauses and shared cleanup. Comments explain ownership constraints.
.editorconfig, .oxfmtrc.json and rustfmt enforce formatting.
.editorconfig, bunfmt.json and rustfmt enforce formatting. JavaScript gates use
`bun check`, `bun lint` and `bun fmt` through the native runtime.
Reviewed through `aphrody docs`, `aphrody awesome` and scoped remote
`aphrody git ingest`. Local ingestion is owned by Bun MCP in this runtime.
+54
View File
@@ -0,0 +1,54 @@
# Bun video integration
Video is the current priority. The source library is pinned to FilmCraft
`87b845138e2cecf43e97e2c2e011b820a6e70ecd` (0.7.2, MIT OR Apache-2.0).
Its Rust workspace remains independent.
The fork adds Bun-native JavaScript helpers. Rust and Cargo remain identical to
the upstream baseline `cee1feb185e675a108f78067f71ad86a640ace3d`, also used by
the parallel native codec owner.
## Owners
| Capability | Owner |
| --- | --- |
| Project loading, schemas 1–15 and atomic saves | filmcraft-format |
| Exact time and rational frame rates | filmcraft-time |
| Container probing and codecs | filmcraft-codecs / filmcraft-media |
| Editing and command validation | filmcraft-engine |
| Rendering and export | filmcraft-render / filmcraft-export |
| Native Bun metadata and frame API | Bun.Video |
| Typed process boundary | @aphrody/bun-video-project |
The adapter calls the existing headless CLI with structured arguments and JSON.
It adds output limits, cancellation and typed operations. Rust retains project
migrations, editing rules, codecs and export. No FFmpeg subprocess or copied
decoder is introduced.
Failed native operations retain structured results and failure status. Export
uses a new destination directory to isolate filenames derived by native formats.
Tick values use 254,016,000,000 units per second. Ticks and IDs span i64/u64;
the JSON boundary preserves large integers as bigint. JSONL serialization emits
their exact decimal representation rather than rounded Numbers.
## Commands
```sh
bun run video doctor
bun run video:build
bun run video commands
bun run video probe /path/to/media.mp4
bun run video inspect /path/to/project.fcproj sequence
```
Use `.local/config.json` to select an existing `filmcraft-cli` binary, or build
the pinned library through the native workspace owner. API calls do not build
the engine implicitly. Save destinations are explicit; protected provider
stores, captures and datasets are outside this integration.
The parallel Bun session owns native codec internalization. The installed
`1.4.4` runtime at `2ae8fc65fba1d05c423a2433b1195835109942b6` currently has no
Bun.Video API. The adapter reports this and does not substitute an unqualified
provider. Native availability alone does not qualify codecs, video encoding or
frame accuracy. See [qualification](qualification.md).
+10 -9
View File
@@ -5,7 +5,8 @@
"license": "MIT",
"workspaces": [
"packages/workspace",
"apps/desktop"
"apps/desktop",
"packages/video"
],
"type": "module",
"scripts": {
@@ -18,19 +19,19 @@
"build": "bun run --cwd apps/desktop build",
"desktop:dev": "bun run --cwd apps/desktop desktop:dev",
"desktop:build": "bun run --cwd apps/desktop desktop:build",
"typecheck": "bun run --bun node_modules/@typescript/native-preview/bin/tsgo --noEmit -p tsconfig.json",
"lint": "bun run --bun node_modules/oxlint/bin/oxlint packages/workspace/src apps/desktop scripts",
"format:check": "bun run --bun node_modules/oxfmt/bin/oxfmt --check packages/workspace/src apps/desktop scripts package.json aphrody.config.json tsconfig.json",
"typecheck": "bun check -p tsconfig.json --threads 2 --no-pretty",
"lint": "bun lint --deny-warnings packages/workspace/src packages/video/src apps/desktop scripts",
"format:check": "bun fmt --config bunfmt.json --check packages/workspace/src packages/video/src apps/desktop/build.ts apps/desktop/dev.ts apps/desktop/native.ts apps/desktop/server.ts apps/desktop/shared.ts apps/desktop/src/app.ts apps/desktop/package.json scripts/quality.ts package.json aphrody.config.json tsconfig.json bunfmt.json",
"check": "bun scripts/quality.ts",
"workspace": "bun packages/workspace/src/cli.ts",
"frontend:dev": "bun run --cwd libraries/artcraft/frontend dev",
"frontend:build": "bun run --cwd libraries/artcraft/frontend build"
"frontend:build": "bun run --cwd libraries/artcraft/frontend build",
"video": "bun packages/video/src/cli.ts",
"video:build": "bun packages/workspace/src/cli.ts build filmcraft",
"fmt": "bun fmt --config bunfmt.json --write packages/workspace/src packages/video/src apps/desktop/build.ts apps/desktop/dev.ts apps/desktop/native.ts apps/desktop/server.ts apps/desktop/shared.ts apps/desktop/src/app.ts apps/desktop/package.json scripts/quality.ts package.json aphrody.config.json tsconfig.json bunfmt.json"
},
"devDependencies": {
"@aphrody/bun-types": "file:toolchains/bun/packages/bun-types",
"@typescript/native-preview": "7.0.0-dev.20260707.2",
"oxfmt": "0.72.0",
"oxlint": "1.87.0"
"@aphrody/bun-types": "file:toolchains/bun/packages/bun-types"
},
"engines": {
"bun": "=1.4.4"
+55
View File
@@ -0,0 +1,55 @@
# Aphrody / Bun Video
`@aphrody/bun-video-project` calls the pinned native video library through
structured arguments and exact JSON. Rust owns project loading, migrations,
editing, codecs, rendering and export.
```ts
import { createVideoLibrary, ticksFromDecimalSeconds } from "@aphrody/bun-video-project";
const controller = new AbortController();
const video = await createVideoLibrary({ signal: controller.signal });
const media = await video.probe("media.mp4");
const sequence = await video.inspect("cut.fcproj");
const time = ticksFromDecimalSeconds("0.1");
await video.run("cut.fcproj", [{ id: "timeline.razor", params: { time } }], { saveAs: "edited.fcproj" });
```
APIs: `commands`, `describe`, `inspect`, `probe`, `run`, `render`, `export`.
`inspect` defaults to the active sequence. CLI syntax is available through
`bun run video help`. Each call opens its own native session.
Resolution uses `binary`, then `.local/config.json`, then Cargo's offline
metadata and an existing release/debug artifact. Paths come from the selected
checkout and its configuration. Source pins and native version are checked.
API calls never compile the engine. Build it with
`bun run workspace build filmcraft`.
Defaults: 30 seconds, 16 MiB combined stdout/stderr. Options allow at most one
hour and 64 MiB; cancellation terminates the owned process tree. Native stderr
is discarded. Errors expose a stable code, optional exit status and structured
native results. Failed exports retain their receipt and offline media entries;
failed command batches retain their completed outcomes. The CLI prints these
results while keeping a failure exit status. `run`
accepts at most 10000 operations and 16 MiB of JSONL. Project persistence uses
an explicit `saveAs`. Render rejects existing aliases to the source project.
Export requires a new destination directory inside an existing parent: native
formats can derive additional filenames, and that directory isolates all of them.
Ticks use 254016000000 units/second. Rust integer JSON tokens outside the safe
Number range become `bigint`; `stringifyNativeJSON` emits exact integer tokens.
Floating point values retain the native float contract. Decimal time input
accepts at most 12 fractional digits and rejects values between ticks.
`ticksToDecimalSeconds` truncates toward zero at the requested precision.
Native render/export seconds use the engine's floating point boundary.
Export returns native receipts and offline media entries; `allowOffline` is
explicit. Render writes PNG and returns `{ output }`. Project migrations and
schemas 1–15 remain in the native library. Resources default to
`.local/video/presets`; provider stores are outside this package.
Library input: `storytold/filmcraft`, commit
`87b845138e2cecf43e97e2c2e011b820a6e70ecd`, version 0.7.2,
MIT OR Apache-2.0. The input is an open source library. `Bun.Video` is exposed
separately through `./native`; its availability does not qualify codecs or
encoding. No FFmpeg subprocess or copied decoder is introduced.
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@aphrody/bun-video-project",
"version": "0.1.0",
"private": true,
"license": "MIT",
"type": "module",
"description": "Aphrody / Bun native video project bridge",
"exports": {
".": "./src/index.ts",
"./native": "./src/native.ts"
},
"bin": {
"aphrody-video": "./src/cli.ts"
},
"dependencies": {
"@aphrody/bun-workspace": "workspace:*"
},
"engines": {
"bun": "=1.4.4"
}
}
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env bun
import {
createVideoLibrary,
stringifyNativeJSON,
VideoLibraryError,
videoLibraryDoctor,
type VideoLibraryOptions,
} from "./index.ts";
const help = `Aphrody / Bun Video
aphrody-video doctor
aphrody-video commands [filter]
aphrody-video describe <id>
aphrody-video inspect <project> [project|sequence]
aphrody-video probe <media>
aphrody-video render <project> <output.png> --seconds N [--scale N]
aphrody-video export <project> <output> [--format F] [--start N] [--end N] [--allow-offline]
Options: --binary PATH, --data-dir PATH, --timeout-ms N, --max-output-bytes N.
Output is exact JSON. Native artifacts are built through the workspace owner.`;
const valued = new Set([
"--binary",
"--data-dir",
"--timeout-ms",
"--max-output-bytes",
"--seconds",
"--scale",
"--format",
"--start",
"--end",
]);
const flags = new Set(["--json", "--allow-offline"]);
const globalOptions = ["--binary", "--data-dir", "--timeout-ms", "--max-output-bytes", "--json"];
interface Arguments {
positional: string[];
values: Map<string, string | true>;
}
function usage(): never {
throw new VideoLibraryError("INVALID_OPTIONS", "Invalid video arguments. Run aphrody-video help.");
}
function parse(args: string[]): Arguments {
const positional: string[] = [];
const values = new Map<string, string | true>();
for (let index = 0; index < args.length; index++) {
const entry = args[index]!;
if (entry === "--") {
positional.push(...args.slice(index + 1));
break;
}
if (!entry.startsWith("--")) {
positional.push(entry);
continue;
}
const equal = entry.indexOf("=");
const name = equal < 0 ? entry : entry.slice(0, equal);
if (values.has(name)) usage();
if (flags.has(name)) {
if (equal >= 0) usage();
values.set(name, true);
} else if (valued.has(name)) {
const value = equal < 0 ? args[++index] : entry.slice(equal + 1);
if (value === undefined || !value || value.includes("\0")) usage();
values.set(name, value);
} else {
usage();
}
}
return { positional, values };
}
function stringOption(args: Arguments, key: string): string | undefined {
const value = args.values.get(key);
return typeof value === "string" ? value : undefined;
}
function integerOption(args: Arguments, key: string): number | undefined {
const value = stringOption(args, key);
if (value === undefined) return;
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value))) usage();
return Number(value);
}
function allowed(args: Arguments, extra: string[], minimum: number, maximum = minimum): void {
const options = new Set([...globalOptions, ...extra]);
if (
args.positional.length < minimum ||
args.positional.length > maximum ||
[...args.values.keys()].some(key => !options.has(key))
)
usage();
}
async function main(): Promise<number> {
const [action = "help", ...rest] = Bun.argv.slice(2);
if (["help", "--help", "-h"].includes(action)) {
if (rest.length) usage();
console.log(help);
return 0;
}
const args = parse(rest);
const options: VideoLibraryOptions = {
binary: stringOption(args, "--binary"),
dataDir: stringOption(args, "--data-dir"),
timeoutMs: integerOption(args, "--timeout-ms"),
maxOutputBytes: integerOption(args, "--max-output-bytes"),
};
if (action === "doctor") {
allowed(args, [], 0);
const report = await videoLibraryDoctor(options);
console.log(stringifyNativeJSON(report, 2));
return report.engine.available &&
report.runtime.recognizedFork &&
report.runtime.versionMatches &&
report.runtime.revisionMatches &&
report.source.initialized &&
report.source.pointerMatches &&
report.source.revisionMatches
? 0
: 1;
}
switch (action) {
case "commands":
allowed(args, [], 0, 1);
break;
case "describe":
allowed(args, [], 1);
break;
case "inspect":
allowed(args, [], 1, 2);
break;
case "probe":
allowed(args, [], 1);
break;
case "render":
allowed(args, ["--seconds", "--scale"], 2);
break;
case "export":
allowed(args, ["--format", "--start", "--end", "--allow-offline"], 2);
break;
default:
usage();
}
const library = await createVideoLibrary(options);
let result: unknown;
switch (action) {
case "commands":
result = await library.commands(args.positional[0]);
break;
case "describe":
result = await library.describe(args.positional[0]!);
break;
case "inspect": {
const kind = args.positional[1] ?? "sequence";
if (kind === "project") result = await library.inspect(args.positional[0]!, "project");
else if (kind === "sequence") result = await library.inspect(args.positional[0]!, "sequence");
else usage();
break;
}
case "probe":
result = await library.probe(args.positional[0]!);
break;
case "render": {
const seconds = stringOption(args, "--seconds");
if (seconds === undefined) usage();
const scale = stringOption(args, "--scale");
result = await library.render(args.positional[0]!, {
output: args.positional[1]!,
seconds,
scale: scale === undefined ? undefined : Number(scale),
});
break;
}
case "export":
result = await library.export(args.positional[0]!, {
output: args.positional[1]!,
format: stringOption(args, "--format"),
start: stringOption(args, "--start"),
end: stringOption(args, "--end"),
allowOffline: args.values.has("--allow-offline"),
});
break;
}
console.log(stringifyNativeJSON(result, 2));
return 0;
}
try {
process.exitCode = await main();
} catch (error) {
if (error instanceof VideoLibraryError) {
if (error.result !== undefined) console.log(stringifyNativeJSON(error.result, 2));
console.error(`Aphrody / Bun: ${error.message} (${error.code})`);
process.exitCode = error.code === "INVALID_OPTIONS" ? 2 : 1;
} else {
console.error("Aphrody / Bun: The video operation failed.");
process.exitCode = 1;
}
}
+30
View File
@@ -0,0 +1,30 @@
import type { NativeJSON } from "./json.ts";
export type VideoErrorCode =
| "INVALID_OPTIONS"
| "RUNTIME_MISMATCH"
| "SOURCE_MISMATCH"
| "ENGINE_MISSING"
| "NATIVE_START_FAILED"
| "NATIVE_FAILED"
| "NATIVE_TIMEOUT"
| "NATIVE_INTERRUPTED"
| "OUTPUT_LIMIT"
| "INVALID_NATIVE_JSON"
| "INVALID_JSON_VALUE"
| "INVALID_TIME";
/** Structured results remain available on failure; native stderr stays private. */
export class VideoLibraryError extends Error {
readonly code: VideoErrorCode;
readonly exitCode?: number;
readonly result?: NativeJSON;
constructor(code: VideoErrorCode, message: string, exitCode?: number, result?: NativeJSON) {
super(message);
this.name = "VideoLibraryError";
this.code = code;
this.exitCode = exitCode;
this.result = result;
}
}
+24
View File
@@ -0,0 +1,24 @@
export { VideoLibraryError, type VideoErrorCode } from "./errors.ts";
export {
parseNativeJSON,
stringifyNativeJSON,
type NativeInteger,
type NativeJSON,
type NativeObject,
} from "./json.ts";
export {
createVideoLibrary,
videoLibraryDoctor,
type CommandDescription,
type CommandOutcome,
type ExportOptions,
type ExportReceipt,
type ProjectInspection,
type RenderOptions,
type SequenceInspection,
type VideoCommand,
type VideoLibrary,
type VideoLibraryOptions,
} from "./library.ts";
export { nativeVideoCapabilities, openNativeVideo, type NativeVideo, type NativeVideoMetadata } from "./native.ts";
export { TICKS_PER_SECOND, ticksFromDecimalSeconds, ticksToDecimalSeconds } from "./time.ts";
+75
View File
@@ -0,0 +1,75 @@
import { VideoLibraryError } from "./errors.ts";
export type NativeInteger = number | bigint;
export type NativeJSON = null | boolean | string | number | bigint | NativeJSON[] | NativeObject;
export interface NativeObject {
[key: string]: NativeJSON;
}
interface JSONSourceContext {
source?: string;
}
interface ExactJSON {
parse(text: string, reviver: (key: string, value: unknown, context?: JSONSourceContext) => unknown): unknown;
rawJSON(text: string): object;
}
const exactJSON = JSON as unknown as ExactJSON;
const safeInteger = BigInt(Number.MAX_SAFE_INTEGER);
/** Rust emits i64/u64 as decimal integer tokens. Preserve those tokens beyond Number's range. */
export function parseNativeJSON<T = NativeJSON>(text: string): T {
try {
return exactJSON.parse(text, (_key, value, context) => {
if (typeof value !== "number") return value;
if (!Number.isFinite(value) || !context?.source) {
throw new VideoLibraryError("INVALID_NATIVE_JSON", "Native JSON requires finite numbers and source context.");
}
if (/^-?\d+$/.test(context.source)) {
const integer = BigInt(context.source);
return integer > safeInteger || integer < -safeInteger ? integer : value;
}
// Fractional/exponent tokens are native floating point values, rather than Rust integer fields.
return value;
}) as T;
} catch (error) {
if (error instanceof VideoLibraryError) throw error;
throw new VideoLibraryError("INVALID_NATIVE_JSON", "The video engine returned invalid JSON.");
}
}
/** Serialize bigint as an exact JSON integer, retaining the native Rust command contract. */
export function stringifyNativeJSON(value: unknown, space?: number): string {
if (space !== undefined && (!Number.isInteger(space) || space < 0 || space > 10)) {
throw new VideoLibraryError("INVALID_JSON_VALUE", "JSON indentation must be between 0 and 10.");
}
try {
const text = JSON.stringify(
value,
(_key, entry: unknown) => {
if (typeof entry === "bigint") {
if (typeof exactJSON.rawJSON !== "function") {
throw new VideoLibraryError("INVALID_JSON_VALUE", "Exact JSON integers require the Aphrody Bun runtime.");
}
return exactJSON.rawJSON(entry.toString());
}
if (
(typeof entry === "number" &&
(!Number.isFinite(entry) || (Number.isInteger(entry) && !Number.isSafeInteger(entry)))) ||
typeof entry === "function" ||
typeof entry === "symbol"
) {
throw new VideoLibraryError("INVALID_JSON_VALUE", "Use finite JSON values and bigint for large integers.");
}
return entry;
},
space,
);
if (text === undefined) throw new VideoLibraryError("INVALID_JSON_VALUE", "A JSON value is required.");
return text;
} catch (error) {
if (error instanceof VideoLibraryError) throw error;
throw new VideoLibraryError("INVALID_JSON_VALUE", "The value cannot be serialized as native JSON.");
}
}
+467
View File
@@ -0,0 +1,467 @@
import { mkdir, stat } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import {
assertRuntime,
findModule,
inspectRuntime,
listModuleStatus,
loadConfig,
localBinaries,
repositoryRoot,
resolveCargoModule,
type Config,
} from "@aphrody/bun-workspace";
import { VideoLibraryError } from "./errors.ts";
import {
parseNativeJSON,
stringifyNativeJSON,
type NativeInteger,
type NativeJSON,
type NativeObject,
} from "./json.ts";
import { nativeVideoCapabilities } from "./native.ts";
import { runBounded, type ProcessLimits, type ProcessResult } from "./process.ts";
const buildInstruction = "Build the video engine with bun run workspace build filmcraft";
const maxInputBytes = 16 * 1024 * 1024;
const maxCommands = 10_000;
const nativeFormats = new Set([
"h264",
"hevc",
"prores",
"dnxhr",
"apv",
"mjpeg",
"mxf-op1a",
"mxf-opatom",
"png",
"tiff",
"bmp",
"jpg",
"tga",
"dpx",
"exr",
"gif",
"wav",
"aiff",
"aac",
"flac",
]);
export interface VideoLibraryOptions {
binary?: string;
timeoutMs?: number;
maxOutputBytes?: number;
dataDir?: string;
signal?: AbortSignal;
}
interface ResolvedOptions extends ProcessLimits {
binary?: string;
dataDir: string;
}
export interface VideoCommand {
id: string;
params?: NativeObject;
}
export interface CommandDescription {
id: string;
label: string;
menu: string[];
shortcut: string | null;
shortcuts: string[];
defaultShortcut: string | null;
params: string;
enabled: boolean;
}
export type ProjectInspection = NativeObject & {
name: string;
path: string | null;
dirty: boolean;
revision: NativeInteger;
activeSequence: NativeInteger | null;
};
export type SequenceInspection = NativeObject & {
id: NativeInteger;
name: string | null;
settings: NativeObject;
duration: NativeInteger;
durationFrames: NativeInteger;
video: NativeJSON[];
audio: NativeJSON[];
};
export type CommandOutcome = NativeObject & {
line: number;
id: string;
ok: boolean;
};
export interface RenderOptions {
output: string;
seconds: number | string;
scale?: number;
}
export interface ExportOptions {
output: string;
format?: string;
start?: number | string;
end?: number | string;
allowOffline?: boolean;
}
export type ExportReceipt = NativeObject & {
job: NativeInteger;
path: string;
result: NativeObject;
offlineMedia: NativeJSON[];
};
export interface VideoLibrary {
commands(filter?: string): Promise<CommandDescription[]>;
describe(id: string): Promise<CommandDescription>;
inspect(projectPath: string, kind: "project"): Promise<ProjectInspection>;
inspect(projectPath: string, kind?: "sequence"): Promise<SequenceInspection>;
probe(mediaPath: string): Promise<NativeObject>;
run(projectPath: string, commands: VideoCommand[], options?: { saveAs?: string }): Promise<CommandOutcome[]>;
render(projectPath: string, options: RenderOptions): Promise<{ output: string }>;
export(projectPath: string, options: ExportOptions): Promise<ExportReceipt>;
}
function pathArgument(path: string): string {
if (typeof path !== "string" || !path.trim() || path.length > 32_768 || path.includes("\0")) {
throw new VideoLibraryError("INVALID_OPTIONS", "A valid file or directory path is required.");
}
return resolve(path);
}
function optionsFor(options: VideoLibraryOptions): ResolvedOptions {
const timeoutMs = options.timeoutMs ?? 30_000;
const maxOutputBytes = options.maxOutputBytes ?? 16 * 1024 * 1024;
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3_600_000) {
throw new VideoLibraryError("INVALID_OPTIONS", "The video deadline must be between 1 ms and one hour.");
}
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 64 * 1024 * 1024) {
throw new VideoLibraryError("INVALID_OPTIONS", "The video output limit must be between 1 byte and 64 MiB.");
}
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
throw new VideoLibraryError("INVALID_OPTIONS", "signal must be an AbortSignal.");
}
return {
timeoutMs,
maxOutputBytes,
signal: options.signal,
binary: options.binary === undefined ? undefined : pathArgument(options.binary),
dataDir: pathArgument(options.dataDir ?? join(repositoryRoot, ".local", "video", "presets")),
};
}
async function rejectProjectOutput(projectPath: string, output: string): Promise<void> {
const source = pathArgument(projectPath);
const equal = process.platform === "win32" ? source.toLowerCase() === output.toLowerCase() : source === output;
if (equal) {
throw new VideoLibraryError("INVALID_OPTIONS", "Media output must use a separate path from the source project.");
}
let sourceFile;
try {
sourceFile = await stat(source, { bigint: true });
} catch {
throw new VideoLibraryError("INVALID_OPTIONS", "The source project is unavailable.");
}
let outputFile;
try {
outputFile = await stat(output, { bigint: true });
} catch (error) {
if ((error as { code?: string }).code === "ENOENT") return;
throw new VideoLibraryError("INVALID_OPTIONS", "The media destination is unavailable.");
}
if (sourceFile.dev === outputFile.dev && sourceFile.ino === outputFile.ino) {
throw new VideoLibraryError("INVALID_OPTIONS", "Media output must use a separate file from the source project.");
}
}
function commandId(id: string): string {
if (typeof id !== "string" || !/^[A-Za-z][A-Za-z0-9._-]{0,127}$/.test(id)) {
throw new VideoLibraryError("INVALID_OPTIONS", "A native command identifier is required.");
}
return id;
}
function secondsArgument(value: string | number): string {
if (
(typeof value !== "number" && typeof value !== "string") ||
(typeof value === "string" && (value.length > 64 || !/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)))
) {
throw new VideoLibraryError("INVALID_OPTIONS", "Use nonnegative seconds.");
}
const seconds = Number(value);
if (!Number.isFinite(seconds) || seconds < 0 || seconds > 10_000_000) {
throw new VideoLibraryError("INVALID_OPTIONS", "Seconds must be between 0 and 10000000.");
}
return seconds.toString();
}
async function sourceConfig(): Promise<Config> {
let config: Config;
try {
config = await loadConfig();
} catch {
throw new VideoLibraryError("SOURCE_MISMATCH", "The video workspace configuration is unavailable.");
}
try {
assertRuntime(config);
} catch {
throw new VideoLibraryError("RUNTIME_MISMATCH", "Use the pinned Aphrody Bun 1.4.4 runtime.");
}
const source = (await listModuleStatus(config)).find(module => module.id === "filmcraft");
if (!source || source.error || !source.initialized || !source.pointerMatches || !source.revisionMatches) {
throw new VideoLibraryError("SOURCE_MISMATCH", "The video library differs from its pinned source.");
}
return config;
}
async function validatedBinary(path: string, options: ResolvedOptions): Promise<string> {
const expected = process.platform === "win32" ? "filmcraft-cli.exe" : "filmcraft-cli";
if (basename(path).toLowerCase() !== expected.toLowerCase()) {
throw new VideoLibraryError("INVALID_OPTIONS", "Select the native video library CLI artifact.");
}
if (!(await Bun.file(path).exists())) throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
const version = await runBounded([path, "--version"], repositoryRoot, {
timeoutMs: Math.min(options.timeoutMs, 5_000),
maxOutputBytes: Math.min(options.maxOutputBytes, 4_096),
signal: options.signal,
});
if (version.code !== 0 || version.stdout.trim() !== "filmcraft-cli 0.7.2") {
throw new VideoLibraryError("SOURCE_MISMATCH", "The native video artifact differs from its pinned version.");
}
return path;
}
async function resolveBinary(config: Config, options: ResolvedOptions): Promise<string> {
if (options.binary) return validatedBinary(options.binary, options);
const selected = (await localBinaries(config)).filmcraft;
if (selected) return validatedBinary(selected, options);
const module = await resolveCargoModule(findModule(config, "filmcraft"));
const cargo = Bun.which("cargo");
if (!cargo) throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
const metadata = await runBounded(
[cargo, "metadata", "--offline", "--no-deps", "--format-version", "1", "--manifest-path", module.manifest],
module.directory,
{ timeoutMs: Math.min(options.timeoutMs, 20_000), maxOutputBytes: options.maxOutputBytes, signal: options.signal },
);
if (metadata.code !== 0) throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
const parsed = parseNativeJSON<NativeObject>(metadata.stdout);
if (typeof parsed.target_directory !== "string") {
throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
}
const filename = process.platform === "win32" ? "filmcraft-cli.exe" : "filmcraft-cli";
for (const profile of ["release", "debug"]) {
const candidate = join(parsed.target_directory, profile, filename);
if (await Bun.file(candidate).exists()) return validatedBinary(candidate, options);
}
throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
}
function nativeResult(result: ProcessResult, failureKind?: "json" | "jsonl" | null): void {
if (result.code !== 0) {
let receipt: NativeJSON | undefined;
if (failureKind && result.stdout.trim()) {
try {
receipt =
failureKind === "json"
? parseNativeJSON(result.stdout)
: result.stdout
.split(/\r?\n/)
.filter(line => line.trim())
.map(line => parseNativeJSON<NativeJSON>(line));
} catch {
// Invalid native output does not replace the original failure status.
}
}
throw new VideoLibraryError("NATIVE_FAILED", "The native video operation failed.", result.code, receipt);
}
}
/** Resolve an existing native artifact. This API never builds or reconstructs project formats. */
export async function createVideoLibrary(options: VideoLibraryOptions = {}): Promise<VideoLibrary> {
const resolved = optionsFor(options);
const config = await sourceConfig();
let binary: string;
try {
binary = await resolveBinary(config, resolved);
} catch (error) {
if (error instanceof VideoLibraryError) throw error;
throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
}
const invoke = async (
args: string[],
projectPath?: string,
input?: Uint8Array,
saveAs?: string,
failureKind: "json" | "jsonl" | null = "json",
) => {
try {
await mkdir(resolved.dataDir, { recursive: true });
} catch {
throw new VideoLibraryError("NATIVE_START_FAILED", "The isolated video resource directory is unavailable.");
}
const argv = [binary, "--compact", "--data-dir", resolved.dataDir];
if (projectPath !== undefined) argv.push("--project", pathArgument(projectPath));
if (saveAs !== undefined) argv.push("--save-as", pathArgument(saveAs));
const result = await runBounded([...argv, ...args], repositoryRoot, resolved, input);
nativeResult(result, failureKind);
return result.stdout;
};
const library: VideoLibrary = {
async commands(filter) {
if (filter !== undefined && (typeof filter !== "string" || filter.length > 128 || filter.includes("\0"))) {
throw new VideoLibraryError("INVALID_OPTIONS", "The command filter must be at most 128 characters.");
}
const args = ["commands", "--json"];
if (filter !== undefined) args.push("--", filter);
return parseNativeJSON<CommandDescription[]>(await invoke(args));
},
async describe(id) {
return parseNativeJSON<CommandDescription>(await invoke(["describe", "--", commandId(id)]));
},
async inspect(projectPath: string, kind: "project" | "sequence" = "sequence") {
if (kind !== "project" && kind !== "sequence") {
throw new VideoLibraryError("INVALID_OPTIONS", "Inspect project or sequence.");
}
return parseNativeJSON(await invoke(["inspect", kind], projectPath)) as ProjectInspection & SequenceInspection;
},
async probe(mediaPath) {
return parseNativeJSON<NativeObject>(await invoke(["probe", "--", pathArgument(mediaPath)]));
},
async run(projectPath, commands, runOptions = {}) {
if (!Array.isArray(commands) || commands.length < 1 || commands.length > maxCommands) {
throw new VideoLibraryError("INVALID_OPTIONS", "Supply between 1 and 10000 video commands.");
}
const encoder = new TextEncoder();
const lines: string[] = [];
let inputBytes = 0;
for (const command of commands) {
if (!command || typeof command !== "object") {
throw new VideoLibraryError("INVALID_OPTIONS", "Each video command requires an identifier and parameters.");
}
const id = commandId(command.id);
if (id.startsWith("file.save")) {
throw new VideoLibraryError("INVALID_OPTIONS", "Persist project edits with the saveAs option.");
}
const params = command.params ?? {};
if (typeof params !== "object" || params === null || Array.isArray(params)) {
throw new VideoLibraryError("INVALID_OPTIONS", "Command parameters must be a JSON object.");
}
const line = `${stringifyNativeJSON({ id, params })}\n`;
inputBytes += encoder.encode(line).byteLength;
if (inputBytes > maxInputBytes) {
throw new VideoLibraryError("INVALID_OPTIONS", "Video commands must fit within 16 MiB.");
}
lines.push(line);
}
const stdout = await invoke(
["run", "-"],
projectPath,
encoder.encode(lines.join("")),
runOptions.saveAs,
"jsonl",
);
return stdout
.split(/\r?\n/)
.filter(line => line.trim())
.map(line => parseNativeJSON<CommandOutcome>(line));
},
async render(projectPath, renderOptions) {
const output = pathArgument(renderOptions.output);
await rejectProjectOutput(projectPath, output);
if (!output.toLowerCase().endsWith(".png")) {
throw new VideoLibraryError("INVALID_OPTIONS", "Render output must be a PNG path.");
}
const args = ["render", "--seconds", secondsArgument(renderOptions.seconds), "--out", output];
if (renderOptions.scale !== undefined) {
if (!Number.isFinite(renderOptions.scale) || renderOptions.scale <= 0 || renderOptions.scale > 1) {
throw new VideoLibraryError("INVALID_OPTIONS", "Render scale must be greater than 0 and at most 1.");
}
args.push("--scale", renderOptions.scale.toString());
}
await invoke(args, projectPath, undefined, undefined, null);
return { output };
},
async export(projectPath, exportOptions) {
const output = pathArgument(exportOptions.output);
await rejectProjectOutput(projectPath, output);
const args = ["export"];
if (exportOptions.format !== undefined) {
if (!nativeFormats.has(exportOptions.format)) {
throw new VideoLibraryError("INVALID_OPTIONS", "Select a supported native export format.");
}
args.push("--format", exportOptions.format);
}
if (exportOptions.start !== undefined) args.push("--start", secondsArgument(exportOptions.start));
if (exportOptions.end !== undefined) args.push("--end", secondsArgument(exportOptions.end));
if (
exportOptions.start !== undefined &&
exportOptions.end !== undefined &&
Number(exportOptions.end) <= Number(exportOptions.start)
) {
throw new VideoLibraryError("INVALID_OPTIONS", "Export end must follow its start.");
}
if (exportOptions.allowOffline !== undefined && typeof exportOptions.allowOffline !== "boolean") {
throw new VideoLibraryError("INVALID_OPTIONS", "allowOffline must be a boolean.");
}
if (exportOptions.allowOffline) args.push("--allow-offline");
args.push("--", output);
// Native formats derive sibling filenames; a fresh directory protects every destination.
try {
await mkdir(dirname(output));
} catch {
throw new VideoLibraryError("INVALID_OPTIONS", "Export into a new directory inside an existing parent.");
}
return parseNativeJSON<ExportReceipt>(await invoke(args, projectPath));
},
};
return library;
}
/** Report source/runtime pins and artifact discovery without starting a media operation. */
export async function videoLibraryDoctor(options: VideoLibraryOptions = {}) {
const resolved = optionsFor(options);
const config = await loadConfig();
const runtime = inspectRuntime(config);
const source = (await listModuleStatus(config)).find(module => module.id === "filmcraft");
let engine: { available: boolean; reason?: string };
try {
await resolveBinary(config, resolved);
engine = { available: true };
} catch (error) {
engine = { available: false, reason: error instanceof VideoLibraryError ? error.code : "ENGINE_MISSING" };
}
return {
identity: "Aphrody / Bun",
runtime: {
version: runtime.version,
revision: runtime.revision,
recognizedFork: runtime.recognizedFork,
versionMatches: runtime.versionMatches,
revisionMatches: runtime.revisionMatches,
},
source: {
revision: source?.revision ?? null,
expectedRevision: findModule(config, "filmcraft").revision,
initialized: source?.initialized ?? false,
pointerMatches: source?.pointerMatches ?? false,
revisionMatches: source?.revisionMatches ?? false,
modified: source?.clean === false,
},
engine,
nativeVideo: await nativeVideoCapabilities(),
limits: { timeoutMs: resolved.timeoutMs, maxOutputBytes: resolved.maxOutputBytes },
};
}
+44
View File
@@ -0,0 +1,44 @@
export interface NativeVideoMetadata {
width: number;
height: number;
duration: number | null;
codec: string;
}
export interface NativeVideo {
frame(seconds: number): NativeVideo;
resize(width: number, height?: number): NativeVideo;
png(): NativeVideo;
bytes(): Promise<Uint8Array>;
metadata(): Promise<NativeVideoMetadata>;
}
interface NativeVideoConstructor {
new (input: string | ArrayBuffer | ArrayBufferView | Blob, options?: { maxPixels?: number }): NativeVideo;
readonly available: boolean;
}
function videoConstructor(): NativeVideoConstructor | undefined {
return (Bun as unknown as { Video?: NativeVideoConstructor }).Video;
}
export function nativeVideoCapabilities() {
const video = videoConstructor();
if (typeof video !== "function") {
return { owner: "Bun.Video", available: false, reason: "This Bun binary has no native video API." };
}
if (video.available !== true) {
return { owner: "Bun.Video", available: false, reason: "The native video provider is disabled." };
}
return { owner: "Bun.Video", available: true };
}
/** Use the runtime owner directly; availability does not qualify codecs or encoding. */
export function openNativeVideo(
input: string | ArrayBuffer | ArrayBufferView | Blob,
options?: { maxPixels?: number },
): NativeVideo {
const capability = nativeVideoCapabilities();
if (!capability.available) throw new Error(capability.reason);
return new (videoConstructor()!)(input, options);
}
+224
View File
@@ -0,0 +1,224 @@
import { VideoLibraryError, type VideoErrorCode } from "./errors.ts";
export interface ProcessLimits {
timeoutMs: number;
maxOutputBytes: number;
signal?: AbortSignal;
}
interface OwnedJob {
assign(process: { pid: number }): void;
terminate(code?: number): void;
close(): void;
}
interface WindowsJobs {
isSupported: boolean;
Job?: new (options: { killOnClose: boolean }) => OwnedJob;
}
export interface ProcessResult {
code: number;
stdout: string;
}
async function reap(child: Bun.ReadableSubprocess): Promise<void> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
child.exited.catch(() => {}),
new Promise<void>(resolve => {
timer = setTimeout(resolve, 5_000);
}),
]);
} finally {
if (timer !== undefined) clearTimeout(timer);
}
}
async function createJob(): Promise<OwnedJob | undefined> {
if (process.platform !== "win32") return;
try {
const windows = (await import("bun:windows")) as unknown as WindowsJobs;
if (windows.isSupported && windows.Job) return new windows.Job({ killOnClose: true });
} catch {
// Hosts without the optional owner API use taskkill for the same owned process tree.
}
}
/** Bound both streams, including discarded diagnostics, and own the child's entire lifetime. */
export async function runBounded(
argv: string[],
cwd: string,
limits: ProcessLimits,
input?: Uint8Array,
): Promise<ProcessResult> {
if (limits.signal?.aborted) {
throw new VideoLibraryError("NATIVE_INTERRUPTED", "The native video operation was interrupted.");
}
let job = await createJob();
if (limits.signal?.aborted) {
job?.close();
throw new VideoLibraryError("NATIVE_INTERRUPTED", "The native video operation was interrupted.");
}
const taskkill = process.platform === "win32" ? Bun.which("taskkill") : null;
if (process.platform === "win32" && !job && !taskkill) {
throw new VideoLibraryError("NATIVE_START_FAILED", "Native process tree cancellation is unavailable.");
}
const controller = new AbortController();
const reads = new AbortController();
let child: Bun.ReadableSubprocess;
try {
child = Bun.spawn(argv, {
cwd,
stdin: input ?? "ignore",
stdout: "pipe",
stderr: "pipe",
signal: controller.signal,
killSignal: "SIGKILL",
windowsHide: true,
detached: process.platform !== "win32",
});
} catch {
job?.close();
throw new VideoLibraryError("NATIVE_START_FAILED", "The native video process could not start.");
}
if (job) {
try {
job.assign(child);
} catch {
job.close();
job = undefined;
if (!taskkill) {
controller.abort();
await Promise.allSettled([reap(child), child.stdout.cancel(), child.stderr.cancel()]);
throw new VideoLibraryError("NATIVE_START_FAILED", "The native video process could not be isolated.");
}
}
}
let failure: VideoLibraryError | undefined;
let stopping: Promise<void> | undefined;
let outputBytes = 0;
const stopTree = (): Promise<void> => {
if (stopping) return stopping;
stopping = (async () => {
reads.abort();
try {
if (job) {
job.terminate();
} else if (process.platform !== "win32") {
try {
process.kill(-child.pid, "SIGKILL");
} catch {
// The owned process group may already be gone.
}
} else if (taskkill && child.exitCode === null) {
const killer = Bun.spawn([taskkill, "/PID", String(child.pid), "/T", "/F"], {
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
windowsHide: true,
signal: AbortSignal.timeout(5_000),
killSignal: "SIGKILL",
});
await killer.exited;
}
} catch {
// Abort still terminates the direct child if the host tree operation fails.
} finally {
controller.abort();
await reap(child);
}
})();
return stopping;
};
const fail = (code: VideoErrorCode, message: string) => {
failure ??= new VideoLibraryError(code, message);
void stopTree();
};
const interrupt = () => fail("NATIVE_INTERRUPTED", "The native video operation was interrupted.");
const timer = setTimeout(
() => fail("NATIVE_TIMEOUT", "The native video operation exceeded its deadline."),
limits.timeoutMs,
);
process.once("SIGINT", interrupt);
process.once("SIGTERM", interrupt);
limits.signal?.addEventListener("abort", interrupt, { once: true });
if (limits.signal?.aborted) interrupt();
const collect = async (stream: ReadableStream<Uint8Array>, retain: boolean): Promise<string> => {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];
let retainedBytes = 0;
try {
while (true) {
if (reads.signal.aborted) throw failure;
let interrupted: (() => void) | undefined;
const cancelled = new Promise<never>((_resolve, reject) => {
interrupted = () => reject(failure);
reads.signal.addEventListener("abort", interrupted, { once: true });
});
let chunk: Awaited<ReturnType<typeof reader.read>>;
try {
chunk = await Promise.race([reader.read(), cancelled]);
} finally {
if (interrupted) reads.signal.removeEventListener("abort", interrupted);
}
if (chunk.done) break;
outputBytes += chunk.value.byteLength;
if (outputBytes > limits.maxOutputBytes) {
fail("OUTPUT_LIMIT", "The native video operation exceeded its output limit.");
throw failure;
}
if (retain) {
chunks.push(chunk.value);
retainedBytes += chunk.value.byteLength;
}
}
if (!retain) return "";
const bytes = new Uint8Array(retainedBytes);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
} finally {
if (reads.signal.aborted) void reader.cancel().catch(() => {});
try {
reader.releaseLock();
} catch {
// A cancelled read can finish after its consumer has stopped.
}
}
};
try {
const [code, stdout] = await Promise.all([
child.exited.then(code => {
// Closing a Windows job also closes pipes inherited by its remaining children.
job?.close();
job = undefined;
return code;
}),
collect(child.stdout, true),
collect(child.stderr, false),
]);
if (failure) throw failure;
return { code, stdout };
} catch (error) {
if (!failure) failure = new VideoLibraryError("NATIVE_FAILED", "The native video operation failed.");
await stopTree();
if (error instanceof VideoLibraryError) throw error;
throw failure;
} finally {
clearTimeout(timer);
process.removeListener("SIGINT", interrupt);
process.removeListener("SIGTERM", interrupt);
limits.signal?.removeEventListener("abort", interrupt);
job?.close();
}
}
+41
View File
@@ -0,0 +1,41 @@
import { VideoLibraryError } from "./errors.ts";
export const TICKS_PER_SECOND = 254_016_000_000n;
const minTick = -(1n << 63n);
const maxTick = (1n << 63n) - 1n;
function checkedTick(ticks: bigint): bigint {
if (typeof ticks !== "bigint" || ticks < minTick || ticks > maxTick) {
throw new VideoLibraryError("INVALID_TIME", "Ticks must fit a signed 64-bit integer.");
}
return ticks;
}
/** Convert plain decimal seconds exactly. Reject values between native ticks. */
export function ticksFromDecimalSeconds(seconds: string): bigint {
if (typeof seconds !== "string" || seconds.length > 64 || !/^-?(?:0|[1-9]\d*)(?:\.\d{1,12})?$/.test(seconds)) {
throw new VideoLibraryError("INVALID_TIME", "Use plain decimal seconds with at most 12 fractional digits.");
}
const negative = seconds.startsWith("-");
const [whole, fraction = ""] = (negative ? seconds.slice(1) : seconds).split(".");
const denominator = 10n ** BigInt(fraction.length);
const numerator = BigInt(`${whole}${fraction}`) * TICKS_PER_SECOND;
if (numerator % denominator !== 0n) {
throw new VideoLibraryError("INVALID_TIME", "The supplied seconds do not fall on an exact native tick.");
}
return checkedTick((negative ? -numerator : numerator) / denominator);
}
/** Display seconds with a fixed precision, truncated toward zero; this is not a round trip. */
export function ticksToDecimalSeconds(ticks: bigint, precision = 12): string {
checkedTick(ticks);
if (!Number.isInteger(precision) || precision < 0 || precision > 12) {
throw new VideoLibraryError("INVALID_TIME", "Decimal precision must be between 0 and 12.");
}
const scale = 10n ** BigInt(precision);
const scaled = (ticks * scale) / TICKS_PER_SECOND;
const magnitude = scaled < 0n ? -scaled : scaled;
const sign = scaled < 0n ? "-" : "";
if (precision === 0) return `${sign}${magnitude}`;
return `${sign}${magnitude / scale}.${(magnitude % scale).toString().padStart(precision, "0")}`;
}
+8 -1
View File
@@ -1,7 +1,7 @@
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
/** Resolve from this checkout; no workstation or host path is embedded. */
export const repositoryRoot = Bun.fileURLToPath(new URL("../../../", import.meta.url));
export const repositoryRoot = resolve(import.meta.dir, "../../..");
export interface Module {
id: string;
@@ -24,6 +24,7 @@ export interface Config {
schemaVersion: 1;
runtime: { version: string; repository: string; revision: string };
modules: Module[];
priorities?: string[];
desktop: Record<string, unknown> & { source?: Source };
}
@@ -170,6 +171,12 @@ export async function loadConfig(root = repositoryRoot): Promise<Config> {
checkoutPath(modulePath, entry.manifestPath);
}
}
if (
value.priorities !== undefined &&
(!Array.isArray(value.priorities) || value.priorities.some(id => typeof id !== "string" || !ids.has(id)))
) {
throw new Error("Priorities must reference catalog libraries.");
}
return value as unknown as Config;
}
+2 -1
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env bun
import { resolve } from "node:path";
const checks = ["doctor", "typecheck", "lint", "format:check"];
for (const check of checks) {
const controller = new AbortController();
const child = Bun.spawn([process.execPath, "run", "--bun", check], {
cwd: Bun.fileURLToPath(new URL("../", import.meta.url)),
cwd: resolve(import.meta.dir, ".."),
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
+9 -2
View File
@@ -9,8 +9,15 @@
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"noEmit": true
"noEmit": true,
"moduleDetection": "force",
"verbatimModuleSyntax": true
},
"include": ["packages/workspace/src/**/*.ts", "apps/desktop/**/*.ts", "scripts/**/*.ts"],
"include": [
"packages/workspace/src/**/*.ts",
"apps/desktop/**/*.ts",
"scripts/**/*.ts",
"packages/video/src/**/*.ts"
],
"exclude": ["node_modules", "toolchains", "apps/desktop/dist", "apps/desktop/src-tauri"]
}