Claude 7ff93f08bb
feat(media): add media contracts, schema and provider interfaces
Groundwork for image and video generation, video editing and transcription.
Nothing calls it yet; the media runner, providers, tools and UI build on it.

- Contracts: video, audio and subtitle MIME types, per-kind size caps for
  streamed and generated artifacts, video/audio message blocks and the live
  media card block, media operations, job statuses and output presets
  (9:16, 16:9, 1:1, 4:5, 4:3, 21:9).
- Core: attachment kinds include video and audio, artifacts become video or
  audio blocks, and agent history keeps media artifact ids so later turns
  can refer to them.
- Schema: artifacts record width, height, duration and the artifact they
  were made from; media_jobs tracks generations, edits and transcriptions.
- Adapter kit: provider-neutral image generation, video generation and
  transcription interfaces, the media job context operations run in, and
  the MediaRuntime that tools use to start jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:04:16 +00:00

230 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type {
MediaJobStatus,
MediaOperation,
MediaOutput,
MediaOutputKind,
MediaPresetId,
} from "@rakazo/contracts";
import type { AdapterDescriptor } from "./types.js";
/** Bytes handed to or returned by a media backend. */
export interface MediaFile {
bytes: Uint8Array;
mimeType: string;
name?: string;
}
export interface GeneratedMedia extends MediaFile {
width?: number;
height?: number;
durationMs?: number;
}
/** Reports progress as a fraction from 0 to 1. */
export type MediaProgressListener = (fraction: number) => void;
/** Free and total memory of the backend's GPU, when the backend can tell. */
export interface GpuStatus {
vramFreeMb: number;
vramTotalMb: number;
}
export interface ImageGenerationRequest {
prompt: string;
negativePrompt?: string;
width: number;
height: number;
/** Number of images, default 1. */
count?: number;
seed?: number;
/** Source image for image-to-image and inpainting. */
source?: MediaFile;
/** 0–1: how far the result may move away from the source. */
strength?: number;
/** Inpainting mask, same size as the source; white areas are repainted. */
mask?: MediaFile;
/** Workflow template id; the provider's default when omitted. */
workflow?: string;
signal?: AbortSignal;
onProgress?: MediaProgressListener;
}
export interface VideoGenerationRequest {
prompt: string;
negativePrompt?: string;
width: number;
height: number;
durationSeconds: number;
fps?: number;
seed?: number;
/** Start frame for image-to-video. */
source?: MediaFile;
workflow?: string;
signal?: AbortSignal;
onProgress?: MediaProgressListener;
}
export interface ImageGenerationCapabilities {
textToImage: boolean;
imageToImage: boolean;
inpaint: boolean;
maxWidth: number;
maxHeight: number;
}
export interface VideoGenerationCapabilities {
textToVideo: boolean;
imageToVideo: boolean;
maxDurationSeconds: number;
}
export interface ImageGenerationProvider {
describe(): AdapterDescriptor<ImageGenerationCapabilities>;
generateImage(request: ImageGenerationRequest): Promise<GeneratedMedia[]>;
/** Lets the media runner wait for free GPU memory instead of failing mid-generation. */
gpuStatus?(signal?: AbortSignal): Promise<GpuStatus | undefined>;
}
export interface VideoGenerationProvider {
describe(): AdapterDescriptor<VideoGenerationCapabilities>;
generateVideo(request: VideoGenerationRequest): Promise<GeneratedMedia[]>;
gpuStatus?(signal?: AbortSignal): Promise<GpuStatus | undefined>;
}
export interface TranscriptSegment {
startMs: number;
endMs: number;
text: string;
}
export interface Transcript {
text: string;
/** Detected or requested language (BCP 47), when the backend reports it. */
language?: string;
/** Timed segments; empty when the backend returns plain text only. */
segments: TranscriptSegment[];
}
export interface TranscriptionRequest {
audio: Uint8Array;
mimeType: string;
fileName?: string;
/** Language hint (BCP 47); detected when omitted. */
language?: string;
signal?: AbortSignal;
}
export interface TranscriptionCapabilities {
segments: boolean;
languageDetection: boolean;
maxBytes: number;
}
export interface TranscriptionProvider {
describe(): AdapterDescriptor<TranscriptionCapabilities>;
transcribe(request: TranscriptionRequest): Promise<Transcript>;
}
/** A file an operation produced in its temp directory, to be stored as an artifact. */
export interface MediaJobOutputFile {
path: string;
name: string;
mimeType: string;
width?: number;
height?: number;
durationMs?: number;
/** Poster frame for a video, as a file in the temp directory. */
posterPath?: string;
parentArtifactId?: string;
preset?: MediaPresetId;
}
/** An artifact the job may read, copied into the job's temp directory. */
export interface MediaJobInputFile {
artifactId: string;
path: string;
name: string;
mimeType: string;
kind: MediaOutputKind;
size: number;
width?: number;
height?: number;
durationMs?: number;
}
/** What the media runner hands an operation while a job runs. */
export interface MediaJobContext {
jobId: string;
spaceId: string;
userId: string;
botId: string | null;
/** The input stored with the job; each operation validates it with its own schema. */
input: unknown;
/** Private scratch directory, removed when the job ends. */
tempDir: string;
signal: AbortSignal;
progress(fraction: number): Promise<void>;
/** Copies an artifact of the job's space that the job's user may read into tempDir. */
fetchArtifact(artifactId: string): Promise<MediaJobInputFile>;
/** Stores a produced file as an artifact and adds it to the media card. */
addOutput(file: MediaJobOutputFile): Promise<MediaOutput>;
}
export interface MediaOperationResult {
/** One or two sentences for the bot that started the job. */
summary: string;
}
export interface MediaOperationHandler {
operation: MediaOperation;
run(context: MediaJobContext): Promise<MediaOperationResult>;
}
/** Thrown by an operation that cannot start yet, e.g. while the GPU lacks free memory. */
export class MediaJobDeferredError extends Error {
constructor(
message: string,
readonly retryAfterMs: number,
) {
super(message);
this.name = "MediaJobDeferredError";
}
}
export interface StartMediaJobInput {
operation: MediaOperation;
/** Validated by the operation's input schema before the job is created. */
input: unknown;
title: string;
spaceId: string;
userId: string;
botId: string | null;
groupId: string | null;
threadId: string;
runId: string | null;
}
export interface MediaJobSnapshot {
id: string;
operation: MediaOperation;
status: MediaJobStatus;
title: string;
progress: number | null;
error: string | null;
outputs: MediaOutput[];
}
/** Which media operations the space can run right now. */
export type MediaCapabilities = Record<MediaOperation, boolean>;
/**
* Starts media jobs and answers which operations are available. Tools call this; the job runs
* in a background worker and keeps its media card current.
*/
export interface MediaRuntime {
capabilities(spaceId: string): Promise<MediaCapabilities>;
start(input: StartMediaJobInput): Promise<{ jobId: string; messageId: string }>;
get(jobId: string): Promise<MediaJobSnapshot | null>;
cancel(jobId: string): Promise<void>;
}