Deployment (t4g2): media-worker gains font-dejavu (drawtext lower-thirds) and a dedicated produce-worker compose service running src/produce.ts — claims video_processing_jobs compose|mix by lane, renders assembled/ overlaid/voiced cuts, registers derived library assets. Deliberately its own process so a remote-lane box needs only Postgres + S3 + ffmpeg. Web: compose media attachments UI (t2), Producer panel in the video drawer (t5), collective creation + branding settings + shell branding (brandweb), mobile CSS (mcss: touch targets, icon sizing, safe areas).
79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
/**
|
||
* Produce job store ports (migrations/009_video.sql + 039_producer.sql).
|
||
*
|
||
* The worker owns the same two tables the API's video service owns: it claims
|
||
* `video_processing_jobs` rows by kind, writes the derived `video_assets` row a
|
||
* job produces, and drives the state machine to `complete`/`failed`. Ports, not
|
||
* a concrete client, so tests and the (single-process) e2e path can double them –
|
||
* the same standing as `ObservedStore`, `AssetStore` and `PreviewObjectStore`.
|
||
*/
|
||
import type { Lane, ProduceJobKind, ProduceRequest } from "./command-builder";
|
||
|
||
export type { Lane, ProduceJobKind, ProduceRequest } from "./command-builder";
|
||
|
||
/** The source `video_assets` row a claimed job renders from. */
|
||
export interface ProduceSourceAsset {
|
||
id: string;
|
||
storageKey: string;
|
||
mimeType: string;
|
||
sizeBytes: number;
|
||
width: number | null;
|
||
height: number | null;
|
||
durationMs: number | null;
|
||
frameRate: number | null;
|
||
/** True when the asset is known to carry an audio track (probed by 009's probe job). */
|
||
hasAudio: boolean | null;
|
||
}
|
||
|
||
export interface ClaimedProduceJob {
|
||
jobId: string;
|
||
workspaceId: string;
|
||
kind: ProduceJobKind;
|
||
request: ProduceRequest;
|
||
source: ProduceSourceAsset;
|
||
claimedAt: string;
|
||
}
|
||
|
||
export interface ProduceOutput {
|
||
/** Storage key of the produced mp4. */
|
||
storageKey: string;
|
||
sizeBytes: number;
|
||
durationMs: number | null;
|
||
width: number;
|
||
height: number;
|
||
frameRate: number;
|
||
}
|
||
|
||
export interface ProduceJobStore {
|
||
/**
|
||
* Claim exactly one queued compose/mix row for this worker's lane, with lease
|
||
* semantics: the `queued → transcoding` state bump inside
|
||
* `FOR UPDATE SKIP LOCKED` is the lease, so two workers can never take the
|
||
* same row and a row stays claimed until this worker finishes it.
|
||
*
|
||
* Returns `null` when nothing is claimable — including when a row was claimed
|
||
* but could not be honoured (an undecodable `input_json`, a missing source
|
||
* asset): such a row is failed inside the claim, never thrown, so no row can
|
||
* strand in `transcoding` holding its lease. Store/transport errors still throw
|
||
* (the poll loop's store-error path).
|
||
*/
|
||
claim(lane: Lane): Promise<ClaimedProduceJob | null>;
|
||
/** Report progress on a claimed job (never changes state). */
|
||
progress(jobId: string, percent: number): Promise<void>;
|
||
/** Insert the derived asset and complete the job pointing at it. */
|
||
complete(jobId: string, output: ProduceOutput, derivedFrom: string): Promise<string>;
|
||
/** Fail a claimed job; `reason` must already be redacted/bounded. */
|
||
fail(jobId: string, reason: string): Promise<void>;
|
||
/** Resolve a voiceover asset id to its storage key. */
|
||
asset(workspaceId: string, assetId: string): Promise<{ storageKey: string; mimeType: string } | null>;
|
||
}
|
||
|
||
/**
|
||
* Object storage, i.e. the API's `StorageService` (content-addressed, envelope
|
||
* encrypted): the worker downloads source bytes and publishes the produced mp4
|
||
* through exactly this pair. The adapter lives in `s3-storage.ts`.
|
||
*/
|
||
export interface ProduceObjectStorage {
|
||
getObject(key: string): Promise<Buffer>;
|
||
putContent(body: Buffer, contentType: "video/mp4"): Promise<{ key: string }>;
|
||
}
|