packages/ai/src/video.ts
Rama dev · réplica de lectura del repositorio original.
import { Effect, Schema, Stream } from "effect"
import { Generation, ProgressEvent, QueuedEvent, type AwaitOptions } from "./generation.js"
import { Media } from "./media.js"
import { MediaModel, composeRoute, tryRequest } from "./media-model.js"
import { MediaRoute } from "./route/media.js"
import {
AIError,
MediaUsage,
ProviderMetadata,
ProviderOptions,
type OpenString,
type BaseRequest,
} from "./schema/index.js"
import { VideoClient, Service } from "./video-client.js"
// ---------------------------------------------------------------------------
// Model
// ---------------------------------------------------------------------------
export type VideoOptions = MediaModel.Options
export type VideoRoute = MediaRoute.AnyRoute<VideoRequest, VideoEvent, VideoResponse>
export class VideoModel<Options extends VideoOptions = VideoOptions> extends MediaModel<VideoRoute, Options> {
declare protected readonly _VideoModel: void
/** The number of type arguments selects the kind: `<Options>`, `<Options, Frame, State>`, or `<Options, Token>`. */
static fromRoute<Options extends VideoOptions>(
route: MediaModel.InlineRouteInput<VideoRequest<VideoModel<Options>>, VideoResponse>,
input: MediaRoute.ModelInput,
): VideoModel<Options>
static fromRoute<Options extends VideoOptions, Frame, State>(
route: MediaModel.StreamRouteInput<VideoRequest<VideoModel<Options>>, VideoEvent, Frame, State>,
input: MediaRoute.ModelInput,
): VideoModel<Options>
static fromRoute<Options extends VideoOptions, Token>(
route: MediaModel.QueuedRouteInput<VideoRequest<VideoModel<Options>>, VideoResponse, Token>,
input: MediaRoute.ModelInput,
): VideoModel<Options>
static fromRoute<Options extends VideoOptions, Frame, State, Token>(
route: MediaModel.AnyRouteInput<VideoRequest<VideoModel<Options>>, VideoEvent, VideoResponse, Frame, State, Token>,
input: MediaRoute.ModelInput,
) {
return new VideoModel<Options>({
id: input.id,
provider: route.protocol.provider,
http: input.http,
providerOptions: input.providerOptions,
route: composeRoute(route, input, collectResponse) as VideoRoute,
})
}
}
export const VideoModelSchema = Schema.declare((value): value is VideoModel => value instanceof VideoModel, {
expected: "Video.Model",
})
// ---------------------------------------------------------------------------
// Request
// ---------------------------------------------------------------------------
export type VideoAspectRatio = Media.AspectRatio
export const VideoAspectRatio = Media.AspectRatio
export type VideoResolution = OpenString<"480p" | "720p" | "1080p" | "4k">
/** Pinned frames. Routes that accept only a first frame fail typed when `last` is present. */
export const VideoFrames = Schema.Struct({
first: Schema.optional(Media.AssetSchema),
last: Schema.optional(Media.AssetSchema),
}).annotate({ identifier: "Video.Frames" })
export type VideoFrames = Schema.Schema.Type<typeof VideoFrames>
const VideoRequestSchema = Schema.Struct({
model: VideoModelSchema,
prompt: Schema.String,
frames: Schema.optional(VideoFrames),
/** Style or subject references that guide the output without pinning a frame. */
references: Schema.optional(Schema.Array(Media.AssetSchema)),
/** Source video for edit or extension routes. */
video: Schema.optional(Media.AssetSchema),
durationSeconds: Schema.optional(Schema.Number),
aspect: Schema.optional(VideoAspectRatio),
resolution: Schema.optional(Schema.String),
/** Whether to generate an audio track; routes whose audio is always on fail typed on `false`. */
audio: Schema.optional(Schema.Boolean),
n: Schema.optional(Schema.Int),
seed: Schema.optional(Schema.Number),
negativePrompt: Schema.optional(Schema.String),
providerOptions: Schema.optional(ProviderOptions),
})
/** A normalized video request, the plain object `Video.request(...)` returns. */
export type VideoRequest<Model extends VideoModel = VideoModel> = Omit<
typeof VideoRequestSchema.Type,
"model" | "providerOptions" | "resolution"
> &
BaseRequest<Model, VideoModelOptions<Model>> & { readonly resolution?: VideoResolution }
export type VideoModelOptions<Model> = Model extends VideoModel<infer Options> ? Options : never
/** Input accepted by `Video.request` and the calls; a `VideoRequest` is also a valid input. */
export type VideoRequestInput<Model extends VideoModel = VideoModel> = Omit<VideoRequest<Model>, "providerOptions"> & {
readonly providerOptions?: NoInfer<VideoModelOptions<Model>>
}
// ---------------------------------------------------------------------------
// Response and events
// ---------------------------------------------------------------------------
export class VideoResponse extends Schema.Class<VideoResponse>("Video.Response")({
videos: Schema.Array(Media.AssetSchema),
usage: Schema.optional(MediaUsage),
providerMetadata: Schema.optional(ProviderMetadata),
}) {
get video() {
return this.videos[0]
}
}
export const VideoOutputEvent = Schema.Struct({
type: Schema.tag("video"),
index: Schema.Number,
video: Media.AssetSchema,
}).annotate({ identifier: "Video.Event.Video" })
export const VideoFinishEvent = Schema.Struct({
type: Schema.tag("finish"),
usage: Schema.optional(MediaUsage),
providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "Video.Event.Finish" })
const videoEventTagged = Schema.Union([QueuedEvent, ProgressEvent, VideoOutputEvent, VideoFinishEvent]).pipe(
Schema.toTaggedUnion("type"),
)
export const VideoEvent = Object.assign(videoEventTagged, {
is: {
generationQueued: videoEventTagged.guards["generation-queued"],
generationProgress: videoEventTagged.guards["generation-progress"],
video: videoEventTagged.guards.video,
finish: videoEventTagged.guards.finish,
},
})
export type VideoEvent = Schema.Schema.Type<typeof videoEventTagged>
const collectResponse = (events: ReadonlyArray<VideoEvent>): Effect.Effect<VideoResponse> => {
const finish = events.find(VideoEvent.is.finish)
// A streaming video protocol's `finish` emits the terminal event or fails, so a completed stream always has one.
if (finish === undefined) return Effect.die(new Error("The video stream completed without a finish event"))
return Effect.succeed(
new VideoResponse({
videos: events.filter(VideoEvent.is.video).map((event) => event.video),
usage: finish.usage,
providerMetadata: finish.providerMetadata,
}),
)
}
// ---------------------------------------------------------------------------
// Request-shaped call API
// ---------------------------------------------------------------------------
/** Normalize video request input into a plain `VideoRequest`. */
export const request = <const Model extends VideoModel>(input: VideoRequestInput<Model>): VideoRequest<Model> => ({
...input,
})
const validate = Schema.decodeUnknownSync(VideoRequestSchema)
const requestEffect = (input: VideoRequestInput) =>
tryRequest(() => {
const normalized = request(input)
validate(normalized)
return normalized
})
export function start<const Model extends VideoModel>(
input: VideoRequestInput<Model>,
): Effect.Effect<Generation<VideoResponse>, AIError, Service>
export function start(input: VideoRequestInput) {
return requestEffect(input).pipe(Effect.flatMap((request) => VideoClient.start(request)))
}
export function generate<const Model extends VideoModel>(
input: VideoRequestInput<Model>,
options?: AwaitOptions,
): Effect.Effect<VideoResponse, AIError, Service>
export function generate(input: VideoRequestInput, options?: AwaitOptions) {
return requestEffect(input).pipe(Effect.flatMap((request) => VideoClient.generate(request, options)))
}
/** Rebuild a generation handle from a persisted `Generation.token`, refreshing its status once. */
export const resume = (model: VideoModel, token: unknown): Effect.Effect<Generation<VideoResponse>, AIError, Service> =>
VideoClient.resume(model, token)
export function stream<const Model extends VideoModel>(
input: VideoRequestInput<Model>,
options?: AwaitOptions,
): Stream.Stream<VideoEvent, AIError, Service>
export function stream(input: VideoRequestInput, options?: AwaitOptions) {
return Stream.unwrap(requestEffect(input).pipe(Effect.map((request) => VideoClient.stream(request, options))))
}
export const Video = {
request,
start,
generate,
resume,
stream,
} as const