@frank/video-tools (0.1.0)
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 |