@frank/video-tools (0.1.0)

Published 2026-10-04 23:32:05 +11:00 by steve

Installation

@frank:registry=
npm install @frank/video-tools@0.1.0
"@frank/video-tools": "0.1.0"

About this package

@frank/video-tools

One project-scoped video render contract and execution boundary for thin Frank and Blockwise adapters. It drives the existing Diffusion host renderer through an injected port; it contains no encoder, browser, provider, store or job loop. Hermes remains the sole authoring runtime. Inputs and outputs belong to projects.

Public contract

parseVideoRenderRequest(input) validates frank.creative.video-render.v1: { schema, scope, requestId, composition, assets, options? }. Scope and opaque asset refs reuse @frank/ad-template; composition is a text asset. Every ref must match both workspace and project. Paths, URLs, traversal and unknown fields are refused. Apps authorize scope and resolve immutable versions from their own ledger; refs never grant permission by themselves.

Host defaults/limits are retained: fps 30 (finite 1..120, including 29.97), resolution 1080 (finite 144..2160), codec avc (avc/hevc/vp9/av1/vp8), format mp4 (mp4/webm/mov), audio false. Host encoder capability checks still apply; a supported contract option does not promise codec availability.

createVideoRenderJob(input) and planVideoRenderOutputs(input) are async. The job is an existing queue input with kind: creative.video.render, workspace fence, normalized payload, three attempts and scoped SHA-256 dedupe. The plan is { scope, requestId, objectKey, mimeType, format } under <workspace>/projects/<project>/video-renders/<requestId>/<payload-sha256>-main.<format>. Mov uses video/quicktime. Changed versions/options produce distinct paths; object-key insertion order does not affect hashes. Asset array order is retained.

import { createVideoRenderJob, executeVideoRender } from "@frank/video-tools";
import { enqueueQueuedJob } from "@frank/jobs";

const descriptor = await createVideoRenderJob(authorizedRequest);
const job = await enqueueQueuedJob(service, descriptor);
// Existing worker handler, after checking the claimed workspace fence:
const result = await executeVideoRender(payload, {
  render: renderWithExistingDiffusionService,
}, context.signal);

render(request, plannedOutput, signal?) -> Promise<unknown> is the injected engine/storage port. The result must be a valid scoped asset ref matching the request and plan MIME. Execution returns { scope, requestId, output, plan }. Invalid requests never call the engine. Abort signals are forwarded and checked before/after work; cancellation from a rejecting port gets a named abort error, while genuine engine failures retain their original error.

Exact app adapter proposal

Frank apps/frank/lib/runs/tools.ts keeps its Hermes Video Studio authoring adapter. In apps/frank/lib/video-studio/store.ts, resolve the authorized composition ref to the existing store-relative project directory, materialize only authorized asset versions, and forward all parsed options to the existing host service at infra/frank/infra/video_studio/render_service.mjs. The adapter maps the planned output to a project-owned export/artifact and returns the ledger ref; untrusted callers never supply host paths. Persist and reuse the underlying host job handle by dedupe key on durable-queue retry; do not submit a second host render on every poll/retry. Losing a lease must stop publication even if the existing host render finishes later.

Blockwise may use the same job/engine boundary after mapping its authorized video project and assets. Its offer, payment, fulfillment, review and publishing workflow stay in the app. In both apps, compare claimed job workspace with the payload scope, enforce project permission, verify actual asset MIME/version, and perform create-only output writes with content verification on retries.

The survivor is this shared option/scope/job contract, with the existing Diffusion engine retained. App option/path/queue glue is the duplicate candidate; host transport validation stays as a separate trust-boundary check. No app code or engine was deleted. Fixture tests prove exact host-option forwarding, project/workspace isolation, content-specific outputs and cancellation. The opt-in installed-encoder fixture also verifies actual MP4 metadata, decoded pixels, real process cancellation and template PNGs. The installed page currently omits media without an asset Library; that acceptance gate fails explicitly and remains app/host integration work.

Dependency handoff

New package version 0.1.0; dependencies @frank/ad-template 0.1.0 (current workspace version), zod ^3.24.0, dev @types/node ^22.0.0. Root packages/* already discovers it. Coordinator owns root lockfile entries, new published/local pins and release packaging; update the contract dependency to the newly chosen version before product consumption. No install, publish, deployment, schema migration, new credentials or service is part of this slice.

Installed encoder acceptance (opt-in, Linux)

tests/acceptance/installed-encoder.mjs invokes the existing installed pipeline/render.mjs, never a copied engine or provider. It requires explicit CREATIVE_ACCEPTANCE_DIR under /tmp/frank-refactor-20261003/creative-acceptance- and FRANK_VIDEO_PIPELINE ending in /pipeline/render.mjs. Set DIFFUSION_EDITOR_ROOT to the installed editor checkout. Put the already built installed page.js in <acceptance-dir>/runtime/page.js; the existing compiler source cache must already exist. Chrome, ffprobe, ffmpeg and installed package dependencies are prerequisites. The fixture creates local synthetic assets; it never opens customer projects or downloads media. Run under the coordinator's heavy-build mutex and an external timeout. It writes acceptance-report.json.

Actual fixture: 640x360 H.264 MP4, 24 fps, 48 frames, two seconds, no audio. Video pre-abort calls no engine; abort during Chrome and after file creation both refuse completion. Template cancellation after one write also refuses completion; an orphan file remains unpublished. Valid file metadata and vector pixels pass, but the standard <image> does not paint in the installed page. The fixture reports imageAssetBinding: false, overall: false, and exits 1 instead of silently treating that omission as accepted output.

Required app/host binding: authenticate project ownership, resolve the text composition ref to an immutable authored project source, and materialize only assets authorized for that same scope/version. Attach the existing runtime Library with an allowlisted resolver for those asset IDs; unresolved or foreign sources must fail instead of completing without media. The injected VideoRenderEnginePorts.render(request, plan, signal) forwards every normalized option to the existing renderer, retains/reuses its job handle for retries, stops or quarantines results after lease cancellation, verifies the encoded artifact and stores it create-only under plan.objectKey. Return a validated ScopedAssetRef matching plan.mimeType; only the app commits publication pointers.

Blockwise's uploaded-video resize/transcode worker supplies no authenticated text-composition binding or Diffusion render port. It is outside this v1 boundary; keep it until those app-owned capabilities exist. Do not reinterpret an uploaded clip as an authored text composition. No app caller or engine is modified here.

Dependencies

Dependencies

ID Version
@frank/ad-template 0.2.0
zod ^3.24.0

Development dependencies

ID Version
@types/node ^22.0.0
Details
npm
2026-10-04 23:32:05 +11:00
15
latest
9.3 KiB
Assets (1)
Versions (1) View all
0.1.0 2026-10-04