비디오 생성(실험적)
⚠️ 실험적 기능 경고
비디오 생성은 실험적 기능이며 중대한 변경이 발생할 수 있습니다. 이 기능을 사용하기 전에 아래 주의 사항을 주의 깊게 읽어 보세요.
주요 주의 사항:
- 향후 버전에서 API가 예고 없이 변경될 수 있습니다.
- OpenAI의 Sora API는 제한적으로 제공되며 조직 인증이 필요할 수 있습니다.
- 비디오 생성은 작업/폴링 아키텍처를 사용하며, 다른 동기식 작업과 다릅니다.
- 가격, 속도 제한 및 할당량은 달라질 수 있으며 변경될 수 있습니다.
- 여기에 설명된 모든 기능을 OpenAI 계정에서 사용하지 못할 수 있습니다.
개요
TanStack AI는 전용 비디오 어댑터를 통해 비디오 생성을 실험적으로 지원합니다. 이미지 생성과 달리 비디오 생성은 작업/폴링 패턴을 사용하는 비동기 작업입니다.
- 작업 생성 - 프롬프트를 제출하고 작업 ID를 받습니다.
- 상태 폴링 - 완료될 때까지 작업 상태를 확인합니다.
- 비디오 가져오기 - 생성된 비디오를 다운로드하거나 볼 수 있는 URL을 가져옵니다.
현재 지원되는 항목:
- OpenAI: Sora-2 및 Sora-2-Pro 모델(사용 가능한 경우)
- Google Gemini: Veo 3.1 모델(장기 실행 작업 API 사용) 및 Gemini Omni Flash(Interactions API 사용)
- Grok(xAI): grok-imagine-video 및 grok-imagine-video-1.5(텍스트-비디오, 이미지-비디오; 1.5는 레퍼런스-비디오를 추가하고, v1.0은 편집 및 확장을 추가합니다.)
- BytePlus: Seedance 2.0, 1.5-pro 및 1.0-pro 모델(텍스트-비디오, 첫/마지막 프레임, 2.0의 멀티모달 레퍼런스)
- fal.ai: MiniMax, Luma, Kling, Hunyuan 및 기타 호스팅 비디오 모델
- OpenRouter: 전용 비동기 비디오 API를 통한 Seedance, Veo 3.1, Wan, Kling, Sora 2 Pro 등(
POST /api/v1/videos)
비디오 실행에는 몇 분이 걸립니다. 새로 고침으로 실행을 잃지 마세요. 이것이 Generation Persistence가 가장 유용한 경우입니다: 각 실행 기록을 유지하므로 새로 고침 후 훅에 빈 양식 대신 해당 실행의 마지막 알려진 상태와 결과가 표시됩니다. 내구성 있는 서버 측 스트림에서 계속 스트리밍 중인 실행은 다시 연결되어 해당 위치에서 완료됩니다. 그렇지 않으면 제공자 작업이 아니라 기록이 복원됩니다. 또한 제공자의 비디오 URL은 만료되므로, 자체 저장소에 바이트를 저장하여 완료된 클립을 보관하세요.
기본 사용법
비디오 작업 생성
import { generateVideo } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
// Start a video generation job (the adapter uses OPENAI_API_KEY from environment)
const { jobId, model } = await generateVideo({
adapter: openaiVideo("sora-2"),
prompt: "A golden retriever puppy playing in a field of sunflowers",
});
console.log("Job started:", jobId);
상태 폴링
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
const { jobId } = await generateVideo({
adapter: openaiVideo("sora-2"),
prompt: "A golden retriever puppy playing in a field of sunflowers",
});
// Check the status of the job
const status = await getVideoJobStatus({
adapter: openaiVideo("sora-2"),
jobId,
});
console.log("Status:", status.status); // 'pending' | 'processing' | 'completed' | 'failed'
console.log("Progress:", status.progress); // 0-100 (if available)
if (status.status === "failed") {
console.error("Error:", status.error);
}
비디오 URL 가져오기
import { getVideoJobStatus } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
import { jobId } from "./job";
// Only call this after status is 'completed'
const result = await getVideoJobStatus({
adapter: openaiVideo("sora-2"),
jobId,
});
if (result.status === "completed" && result.url) {
console.log("Video URL:", result.url);
}
폴링 루프를 포함한 완전한 예제
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
async function createAndAwaitVideo(prompt: string) {
// 1. Create the job
const { jobId } = await generateVideo({
adapter: openaiVideo("sora-2"),
prompt,
size: "1280x720",
duration: 8, // 4, 8, or 12 seconds
});
console.log("Job created:", jobId);
// 2. Poll for completion
let status = "pending";
while (status !== "completed" && status !== "failed") {
// Wait 5 seconds between polls
await new Promise((resolve) => setTimeout(resolve, 5000));
const result = await getVideoJobStatus({
adapter: openaiVideo("sora-2"),
jobId,
});
status = result.status;
console.log(
`Status: ${status}${result.progress ? ` (${result.progress}%)` : ""}`,
);
if (result.status === "failed") {
throw new Error(result.error || "Video generation failed");
}
}
// 3. Get the video URL
const result = await getVideoJobStatus({
adapter: openaiVideo("sora-2"),
jobId,
});
if (result.status === "completed" && result.url) {
return result.url;
}
throw new Error("Video generation failed or URL not available");
}
// Usage
const videoUrl = await createAndAwaitVideo("A cat playing piano in a jazz bar");
console.log("Video ready:", videoUrl);
풀스택 사용법
TanStack AI의 generateVideo 함수는 서버 측에서 작업 생성과 폴링 루프를 처리하고 상태 업데이트를 클라이언트에 실시간으로 스트리밍하는 stream: true 플래그를 지원합니다.
스트리밍 모드(서버 라우트 + 클라이언트 훅)
서버 — 서버가 전체 폴링 수명 주기를 처리하고 이벤트를 클라이언트로 스트리밍합니다:
// routes/api/generate/video.ts
import { generateVideo, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/api/generate/video")({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json();
const { prompt, size, duration, model } = body.data;
const stream = generateVideo({
adapter: openaiVideo(model ?? "sora-2"),
prompt,
size,
duration,
stream: true,
pollingInterval: 3000, // Check status every 3 seconds
maxDuration: 600_000, // Timeout after 10 minutes
});
return toServerSentEventsResponse(stream);
},
},
},
});
클라이언트 — 작업 상태를 자동으로 추적하는 useGenerateVideo 훅을 사용합니다:
import { useGenerateVideo, fetchServerSentEvents } from "@tanstack/ai-react";
function VideoGenerator() {
const {
generate,
result,
jobId,
videoStatus,
isLoading,
error,
stop,
reset,
} = useGenerateVideo({
connection: fetchServerSentEvents("/api/generate/video"),
onJobCreated: (id) => console.log("Job created:", id),
onStatusUpdate: (status) => console.log("Status:", status.status),
});
return (
<div>
<button
onClick={() =>
generate({ prompt: "A golden retriever playing in sunflowers" })
}
disabled={isLoading}
>
{isLoading ? "Generating..." : "Generate Video"}
</button>
{isLoading && (
<div>
{jobId && <p>Job: {jobId}</p>}
{videoStatus?.progress != null && (
<progress value={videoStatus.progress} max={100} />
)}
<p>Status: {videoStatus?.status ?? "starting..."}</p>
<button onClick={stop}>Cancel</button>
</div>
)}
{error && <p>Error: {error.message}</p>}
{result && (
<div>
<video src={result.url} controls width={640} />
<button onClick={reset}>Clear</button>
</div>
)}
</div>
);
}
다른 두 전송 방식(JSON을 반환하는 서버 함수 또는 SSE Response를 반환하는 함수)도 여기서 동일하게 작동합니다. 이 방식은
고급: 다른 전송 방식에 있으며, Generations에서 한 번 설명합니다.
훅 API
useGenerateVideo 훅은 모든 공통 옵션과 비디오 전용 콜백을 허용합니다:
| 옵션 | 타입 | 설명 |
|---|---|---|
connection | ConnectionAdapter | 스트리밍 전송 방식(SSE, HTTP 스트림, 사용자 지정) |
fetcher | (input) => Promise<VideoGenerateResult | Response> | 직접 비동기 함수 또는 SSE Response를 반환하는 서버 함수 |
onResult | (result) => TOutput | null | void | 비디오가 준비되었을 때 호출됩니다. 선택적으로 변환된 값을 반환하여 result로 저장할 수 있습니다. |
onError | (error) => void | 오류 발생 시 호출됩니다. |
onProgress | (progress, message?) => void | 진행률 업데이트(0~100) |
onJobCreated | (jobId: string) => void | 작업이 생성되었을 때 호출됩니다. |
onStatusUpdate | (status: VideoStatusInfo) => void | 각 폴링 업데이트마다 호출됩니다. |
그리고 다음을 반환합니다:
| 속성 | 타입 | 설명 |
|---|---|---|
generate | (input: VideoGenerateInput) => Promise<void> | 생성 트리거 |
result | VideoGenerateResult | null | 비디오 URL이 포함된 결과 또는 null |
jobId | string | null | 현재 작업 ID |
videoStatus | VideoStatusInfo | null | 최신 폴링 상태(진행률, 상태) |
isLoading | boolean | 생성이 진행 중인지 여부 |
error | Error | undefined | 현재 오류(있는 경우) |
status | GenerationClientState | 'idle' | 'generating' | 'success' | 'error' |
stop | () => void | 현재 생성을 중단합니다. |
reset | () => void | 모든 상태를 지우고 대기 상태로 돌아갑니다. |
옵션
작업 생성 옵션
| 옵션 | 타입 | 설명 |
|---|---|---|
adapter | VideoAdapter | 모델이 포함된 비디오 어댑터 인스턴스(필수) |
prompt | string | MediaPromptPart[] | 생성할 비디오에 대한 설명(필수)입니다. 일반 문자열 또는 조건부 생성을 지원하는 모델에서 텍스트와 이미지/비디오/오디오 입력을 교차 배치한 정렬된 콘텐츠 부분 배열입니다. 아래 Image-to-Video를 참조하세요. |
size | string | WIDTHxHEIGHT 형식의 비디오 해상도 |
duration | number | 초 단위 비디오 길이(API의 seconds 매개변수에 매핑됨) |
modelOptions? | object | 모델별 옵션(과거 providerOptions에서 이름 변경됨) |
이미지-투-비디오
시작 프레임, 종료 프레임 및 참조 이미지 조건부 비디오
generation을 시작하려면 prompt을 콘텐츠 파트 배열로 전달합니다:
import { generateVideo } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
import { base64Image } from "./assets";
const { jobId } = await generateVideo({
adapter: openaiVideo("sora-2"),
prompt: [
{
type: "text",
content:
"Animate this still into a slow cinematic push-in with subtle motion",
},
{
type: "image",
source: {
type: "data",
value: base64Image,
mimeType: "image/png",
},
},
],
});
허용되는 파트 유형은 컴파일 시 모델별로 좁혀집니다. 예를 들어 fal 엔드포인트는 SDK 입력 유형에 실제로 선언된 필드를 가진 이미지 / 비디오 / 오디오 파트만 허용합니다.
프롬프트 텍스트는 항상 있는 그대로 전송됩니다. SDK는 절대로 주입하거나 다시 작성하지 않습니다.
프롬프트 내 참조 마커를 사용합니다. 일부 fal 비디오 엔드포인트에는 자체적인
참조 구문이 있으므로 텍스트에 직접 작성할 수 있습니다(예: Kling v3
요소는 @Element1, Seedance 2.0의 reference-to-video는 @Image1 /
@Video1 / @Audio1(입력 순서 기준 1부터 시작); Veo와 Sora는
참조 이미지를 일반 입력으로 받고 자연스럽게 작성된 프롬프트를 사용합니다. 자세한 내용은
프롬프트에서 이미지 참조하기
provider별 표를 참조하세요.
역할 힌트
각 ImagePart에는 선택적인 metadata.role 힌트를 포함할 수 있으며,
어댑터는 이를 사용해 입력을 provider별 필드로 라우팅합니다:
| 역할 | 매핑 대상 |
|---|---|
'start_frame' | fal start_image_url, Veo 입력 image(첫 번째 입력의 위치 기반 기본값), Seedance first_frame, OpenRouter frame_images[] 및 frame_type: 'first_frame' 사용 |
'end_frame' | fal end_image_url, Veo lastFrame, Seedance last_frame, OpenRouter frame_images[] 및 frame_type: 'last_frame' 사용 |
'reference' | fal reference_image_urls, Veo referenceImages, Seedance reference_image, OpenRouter input_references[] |
'character' | 'reference'과 동일 — 캐릭터 일관성 이미지 |
import { generateVideo } from "@tanstack/ai";
import { falVideo } from "@tanstack/ai-fal";
import { firstFrameUrl, lastFrameUrl } from "./assets";
await generateVideo({
adapter: falVideo("fal-ai/kling-video/v3/pro/image-to-video"),
prompt: [
{ type: "image", source: { type: "url", value: firstFrameUrl } },
{ type: "text", content: "Slow cinematic push-in then a hard cut" },
{
type: "image",
source: { type: "url", value: lastFrameUrl },
metadata: { role: "end_frame" },
},
],
});
프로바이더 지원
| provider | 이미지-투-비디오 동작 |
|---|---|
| OpenAI | Sora-2 / Sora-2-Pro → 이미지 파트는 input_reference로 전달되고, 평탄화된 텍스트가 프롬프트가 됩니다. 이미지 하나만 허용되며 둘 이상이면 예외가 발생합니다. |
| fal.ai | 필드 이름은 fal SDK 엔드포인트 유형에서 생성된 맵을 기반으로 엔드포인트별로 결정됩니다. 예를 들어 role: 'start_frame'은 Kling/Veo 이미지-투-비디오에서 image_url로, 첫-마지막-프레임 엔드포인트에서 first_frame_url로, 그 외에는 start_image_url로 전달됩니다. 기본값: 단일 입력 → image_url(시작 프레임); role: 'end_frame' → end_image_url; role: 'reference' / 'character' → reference_image_urls. modelOptions을 통해 엔드포인트별로 재정의할 수 있습니다. 미디어 조건 필드는 일반적으로 프롬프트 파트로 전달되므로 해당 위치에서는 엔드포인트가 이를 요구하더라도 타입상 선택 사항입니다. |
| Gemini | Veo → 역할이 지정되지 않은 첫 번째 이미지 또는 'start_frame' 이미지가 입력 이미지가 됩니다. 'end_frame' → lastFrame; 'reference' / 'character' → referenceImages(에셋 참조, Veo 3.1). 시작 이미지가 여러 개이면 예외가 발생합니다. |
| BytePlus | Seedance → 역할이 지정되지 않은 단일 이미지 또는 'start_frame' 이미지가 first_frame가 됩니다. 'end_frame' → last_frame(첫 번째 프레임이 함께 필요하며 seedance-1-0-pro-fast-251015에서는 거부됨); 'reference' / 'character' → reference_image, 비디오 파트 → reference_video, 오디오 파트 → reference_audio(Seedance 2.5 및 2.0 제품군; 2.5는 오디오만 포함된 참조 입력도 허용). 프레임 역할과 참조 역할은 상호 배타적인 모드이므로 혼합하면 예외가 발생합니다. |
| OpenRouter | role: 'start_frame' / 'end_frame' → frame_images[] 및 frame_type: 'first_frame' / 'last_frame' 사용; role: 'reference' / 'character' → input_references[]; 역할이 지정되지 않은 이미지는 기본적으로 시작 프레임이 됩니다. 시작 프레임과 종료 프레임은 각각 최대 하나만 허용됩니다. 프레임 역할은 모델의 supported_frame_images 메타데이터에 대해 검증됩니다(예: Hailuo는 첫 번째 프레임만 허용). 프레임 이미지와 참조가 모두 있으면 OpenRouter는 요청을 이미지-투-비디오로 처리하고 참조의 우선순위를 낮춥니다. URL 이미지 소스는 있는 그대로 전달되고 data 소스는 data URI가 됩니다. OpenRouter는 리디렉션이나 봇 확인 뒤의 URL을 가져오지 않으므로 직접 접근 가능한 URL을 사용해야 합니다. |
기반 API가 이미지 입력을 허용하지 않는 어댑터는 명확한 런타임 오류를 발생시키므로 호출이 즉시 실패합니다.
지원되는 크기
OpenAI API 문서를 기준으로 합니다:
| 크기 | 설명 |
|---|---|
1280x720 | 720p 가로 방향(16:9) - 기본값 |
720x1280 | 720p 세로 방향(9:16) |
1792x1024 | 와이드 가로 방향 |
1024x1792 | 긴 세로 방향 |
지원되는 길이
API는 seconds 매개변수를 사용합니다. 허용되는 값:
4초8초(기본값)12초
고급
이 기능을 작동시키는 데 필요하지 않은 참조 세부 정보입니다.
기타 전송 방식
직접 모드(서버 함수 + Fetcher)
서버가 전체 폴링 루프를 처리하고 완료된 결과를 반환하는 경우:
// lib/server-functions.ts
import { createServerFn } from "@tanstack/react-start";
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
export const generateVideoFn = createServerFn({ method: "POST" })
.inputValidator((data: { prompt: string }) => data)
.handler(async ({ data }) => {
const adapter = openaiVideo("sora-2");
// Create the job
const { jobId } = await generateVideo({
adapter,
prompt: data.prompt,
});
// Poll until complete
let status = await getVideoJobStatus({ adapter, jobId });
while (status.status !== "completed" && status.status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
status = await getVideoJobStatus({ adapter, jobId });
}
if (status.status === "failed") {
throw new Error(status.error || "Video generation failed");
}
return {
jobId,
status: "completed" as const,
url: status.url!,
};
});
import { useGenerateVideo } from "@tanstack/ai-react";
import { generateVideoFn } from "../lib/server-functions";
function VideoGenerator() {
const { generate, result, isLoading } = useGenerateVideo({
fetcher: (input) => generateVideoFn({ data: input }),
});
// ... same UI as above (note: jobId and videoStatus won't update in fetcher mode)
}
참고: 직접 fetcher 모드에서는 스트리밍이 없으므로
jobId및videoStatus이 실시간 업데이트를 받지 못합니다. 진행 상황을 추적하려면 스트리밍 연결 모드 또는 server function 스트리밍을 사용하세요.
서버 함수 스트리밍(Fetcher + Response)
결과를 스트리밍하는 TanStack Start server function의 경우입니다. fetcher는 타입 안전한 입력을 받고 SSE Response을 반환하며, 클라이언트가 이를 자동으로 파싱합니다. 이를 통해 타입 안전성과 실시간 jobId/videoStatus 업데이트를 모두 얻을 수 있습니다:
// lib/server-functions.ts
import { createServerFn } from "@tanstack/react-start";
import { generateVideo, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
export const generateVideoStreamFn = createServerFn({ method: "POST" })
.inputValidator(
(data: { prompt: string; size?: string; duration?: number }) => data,
)
.handler(({ data }) => {
return toServerSentEventsResponse(
generateVideo({
adapter: openaiVideo("sora-2"),
prompt: data.prompt,
size: data.size as any,
duration: data.duration,
stream: true,
}),
);
});
import { useGenerateVideo } from "@tanstack/ai-react";
import { generateVideoStreamFn } from "../lib/server-functions";
function VideoGenerator() {
const { generate, result, jobId, videoStatus, isLoading } = useGenerateVideo({
fetcher: (input) => generateVideoStreamFn({ data: input }),
});
// ... same UI as streaming mode (jobId and videoStatus update in real-time)
}
모델 옵션
OpenAI 모델 옵션
OpenAI Sora API를 기반으로 합니다:
import { generateVideo } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
const { jobId } = await generateVideo({
adapter: openaiVideo("sora-2"),
prompt: "A beautiful sunset over the ocean",
size: "1280x720", // '1280x720', '720x1280', '1792x1024', '1024x1792'
duration: 8, // 4, 8, or 12 seconds
modelOptions: {
size: "1280x720", // Alternative way to specify size
seconds: "8", // Alternative way to specify duration ('4' | '8' | '12')
},
});
Google Veo(Gemini) 모델 옵션
Veo는 Google의 장기 실행 작업 API에서 실행됩니다. 어댑터가 작업을 시작하고
getVideoJobStatus을(를) 비디오가 준비될 때까지 폴링합니다:
import { generateVideo } from "@tanstack/ai";
import { geminiVideo } from "@tanstack/ai-gemini";
const { jobId } = await generateVideo({
adapter: geminiVideo("veo-3.1-generate-preview"),
prompt: "A close-up of a luthier carving a guitar neck",
size: "16:9", // aspect ratio: '16:9' or '9:16'
duration: 8, // typed per model — see below
modelOptions: {
resolution: "1080p", // '720p' (default), '1080p', '4k' (Veo 3.1 only)
negativePrompt: "cartoon, low quality",
generateAudio: true, // Veo 3+ generates synchronized audio
},
});
타입이 지정된 기간
각 Veo 모델은 고정된 기간 집합을 허용하며, duration 옵션에서
컴파일 시간에 적용됩니다:
| 모델 | duration 값(초) |
|---|---|
veo-3.1-generate-preview | 4, 6, 8 |
veo-3.1-fast-generate-preview | 4, 6, 8 |
veo-3.1-lite-generate-preview | 4, 6, 8 |
원시 초 값(예: UI 슬라이더에서 가져온 값)이 있다면 snapDuration를 사용해 변환하거나, availableDurations를 사용해 전체 집합을 확인합니다:
import { generateVideo } from "@tanstack/ai";
import { geminiVideo } from "@tanstack/ai-gemini";
const adapter = geminiVideo("veo-3.1-lite-generate-preview");
adapter.availableDurations(); // { kind: 'discrete', values: [4, 6, 8] }
adapter.snapDuration(7); // 6 — closest valid duration
await generateVideo({
adapter,
prompt: "A timelapse of a city skyline at dusk",
duration: adapter.snapDuration(7),
});
모델별 기간 맵을 선언하지 않은 어댑터는 일반적인
duration?: number 입력 중, 반환 { kind: 'none' }에서
availableDurations(), 그리고 undefined을 snapDuration()에서 반환합니다.
fal은 예외입니다: duration은 @fal-ai/client의
EndpointTypeMap 런타임 맵에 항목이 없는 경우에도 적용됩니다.
참고: Veo 작업에 대해 반환되는 동영상 URL은 Gemini Files API에서 제공되며, 다운로드하려면 API 키가 필요합니다(키를
x-goog-api-key헤더 또는key쿼리 매개변수로 전송합니다).
Gemini Omni Flash (Interactions API) 모델 옵션
Gemini Omni Flash(gemini-omni-flash-preview)는 대화형 편집을 지원하는 Google의 멀티모달
동영상 생성 모델입니다. Interactions API만
제공하며, 동일한 geminiVideo() 어댑터가 자동으로 라우팅합니다:
generateVideo는 백그라운드 인터랙션을 생성합니다.getVideoJobStatus는 ID로 폴링합니다.- 완료된 클립은
data:video/mp4;base64,…URL로 인라인 반환됩니다. Google이 대신 참조로 제공하는 경우 Files API URI가 그대로 전달되며, Veo와 마찬가지로 다운로드하려면 API 키가 필요합니다.
클립은 720p, 24 FPS입니다. duration는 3~10초 범위의 모든 값(소수 초 포함)을
허용하며, 생략하면 기본값은 10초입니다:
availableDurations()는{ kind: 'range', min: 3, max: 10, unit: 'seconds' }를 보고합니다.- 범위를 벗어난
duration값은 작업 생성 시 거부됩니다. snapDuration(n)는 원시 초 값을 범위 내로 조정하고, 양 끝값으로 제한한 다음 정수 초로 반올림합니다.
size 옵션은 인터랙션의 출력 가로세로 비율에 매핑됩니다:
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { geminiVideo } from "@tanstack/ai-gemini";
const adapter = geminiVideo("gemini-omni-flash-preview");
const { jobId } = await generateVideo({
adapter,
prompt: "A woman playing violin outdoors at golden hour",
size: "9:16", // aspect ratio: '16:9' (default) or '9:16'
duration: 6, // 3-10 seconds; omit for the 10s default
});
const status = await getVideoJobStatus({ adapter, jobId });
// status.url → 'data:video/mp4;base64,…' once completed
이미지 및 동영상 프롬프트 부분은 콘텐츠 블록으로 인터랙션에 전송되며,
이미지, 동영상, 텍스트 프롬프트 순으로 그룹화됩니다(Omni는 Veo의
metadata.role 라우팅을 사용하지 않음). 따라서 정지 이미지나 짧은
참조 클립을 조건으로 생성할 수 있습니다. 각 소스가 전송되는 방식은 다음과 같습니다:
data소스는 base64로 인라인 전송됩니다.url소스는 있는 그대로 전달됩니다. 어댑터는 이를 다운로드하지 않으므로 Gemini Files API URI를 사용해야 합니다(대용량 미디어는 먼저 Files API를 통해 업로드합니다).
대화형 동영상 편집
Omni의 핵심 기능은 반복적인 개선입니다. 이전 생성의 인터랙션 ID
(해당 생성의 jobId)를 modelOptions.previous_interaction_id로
전달하고 변경 사항을 설명하면, 모델은 언급하지 않은 모든 내용을 유지하면서
동영상을 편집합니다:
import { generateVideo } from "@tanstack/ai";
import { geminiVideo } from "@tanstack/ai-gemini";
const adapter = geminiVideo("gemini-omni-flash-preview");
// Turn 1: generate
const first = await generateVideo({
adapter,
prompt: "A woman playing violin outdoors at golden hour",
});
// …poll first.jobId to completion, then…
// Turn 2: edit the result conversationally
const second = await generateVideo({
adapter,
prompt: "Make the violin invisible",
modelOptions: { previous_interaction_id: first.jobId },
});
modelOptions은 Interactions API의 요청 필드도 전달합니다
(예: 작업 모드를 모델이 추론하도록 하는 대신 generation_config.video_config.task을(를) 고정하기 위한
'text_to_video' | 'image_to_video' | 'reference_to_video' | 'edit').
Grok (xAI Imagine) 모델 옵션
xAI 동영상 생성 API를 기반으로 합니다. 두 가지 모델을 사용할 수 있습니다: grok-imagine-video(v1.0)과 grok-imagine-video-1.5(xAI가 권장하는 기본 모델이며 네이티브 1080p 텍스트-투-비디오를 지원)입니다. 두 모델 모두 텍스트-투-비디오 및 이미지-투-비디오를 지원하며, 1.5에서는 참조-투-비디오가 추가됩니다. 동영상 편집 및 확장은 grok-imagine-video에서만 지원되며, 1.5에는 동영상 입력이 없습니다. 두 모델 모두 화면 비율에 따라 크기가 결정됩니다. 일반적인 size 옵션은 aspectRatio_resolution 템플릿을 사용하며(Grok Imagine 이미지 모델과 동일), 클립 길이는 1~15초로 설정할 수 있습니다.
텍스트-투-비디오:
import { generateVideo } from "@tanstack/ai";
import { grokVideo } from "@tanstack/ai-grok";
const { jobId } = await generateVideo({
adapter: grokVideo("grok-imagine-video-1.5"),
prompt: "A beautiful sunset over the ocean",
size: "16:9_720p", // aspect ratio: '1:1' | '16:9' | '9:16' | '4:3' | '3:4' | '3:2' | '2:3'
// resolution (optional suffix): '480p' | '720p' | '1080p'
duration: 5, // integer seconds, 1-15
modelOptions: {
aspect_ratio: "16:9", // Alternative way to specify the aspect ratio
resolution: "720p", // Alternative way to specify the resolution
duration: 5, // Alternative way to specify the duration
},
});
이미지-투-비디오 — 시작 프레임으로 사용할 image 프롬프트 부분을 포함합니다. URL 소스는 xAI 서버에서 가져오므로 공개적으로 접근 가능해야 합니다. base64 시작 프레임에는 data 소스를 사용합니다:
import { generateVideo } from "@tanstack/ai";
import { grokVideo } from "@tanstack/ai-grok";
const { jobId } = await generateVideo({
adapter: grokVideo("grok-imagine-video-1.5"),
prompt: [
{ type: "text", content: "Slowly pan out as the waves roll in" },
{
type: "image",
source: { type: "url", value: "https://example.com/still.png" },
},
],
size: "16:9_720p",
duration: 5,
});
참조-투-비디오(grok-imagine-video-1.5만 해당, 출력은 720p로 제한됨) — metadata.role: 'reference' 또는 'character'를 포함한 이미지 프롬프트 부분은 reference_images이 됩니다(프롬프트에서는 <IMAGE_0>, <IMAGE_1> 등으로 지정합니다). 최대 3개의 사전 설정 TTS 음성은 modelOptions.reference_audios를 통해 참조할 수 있습니다(각각 <AUDIO_0> 등으로 지정합니다):
import { generateVideo } from "@tanstack/ai";
import { grokVideo } from "@tanstack/ai-grok";
const { jobId } = await generateVideo({
adapter: grokVideo("grok-imagine-video-1.5"),
prompt: [
{ type: "text", content: "<IMAGE_0> waves at the camera while <AUDIO_0> says hello" },
{
type: "image",
source: { type: "url", value: "https://example.com/character.png" },
metadata: { role: "reference" },
},
],
size: "16:9_720p",
modelOptions: { reference_audios: [{ voice_id: "eve" }] },
});
비디오 편집 및 확장 (grok-imagine-video 만) — 소스 클립을 video 프롬프트 부분으로 전달하고 modelOptions.mode 로 모드를 선택하세요. 'edit' (/v1/videos/edits) 는 프롬프트가 요청하는 내용만 수정하며 소스 클립의 지속 시간/화면 비율/해상도를 상속받습니다 (720p 로 제한됨). 'extend' (/v1/videos/extensions) 는 클립을 계속하며 duration 은 추가된 꼬리의 길이를 의미합니다. 출력은 소스 클립의 속성을 상속받으므로 어댑터는 두 모드 모두에서 size / aspect_ratio / resolution 를 거부하며 (편집 모드에서는 duration 도 거부) API 가 무시하는 필드를 전송하지 않습니다. 어댑터는 소스 비디오 부분이나 mode 을 grok-imagine-video-1.5 에서 거부합니다.
import { generateVideo } from "@tanstack/ai";
import { grokVideo } from "@tanstack/ai-grok";
const { jobId } = await generateVideo({
adapter: grokVideo("grok-imagine-video"),
prompt: [
{ type: "text", content: "The camera keeps panning right across the bay" },
{
type: "video",
source: { type: "url", value: "https://example.com/clip.mp4" },
},
],
duration: 5, // 'extend' mode: seconds added to the clip, not the total
modelOptions: { mode: "extend" },
});
두 모델 모두 1–15 범위 내의 전체 초를 허용합니다. 원본 duration 는 거부되지 않고 해당 범위로 강제 변환되며 값은 [1, 15] 로 클램핑되어 가장 가까운 초로 반올림됩니다. Veo 와 동일한 방식으로 범위를 확인하거나 미리 스냅하세요.
import { grokVideo } from "@tanstack/ai-grok";
const adapter = grokVideo("grok-imagine-video-1.5");
adapter.availableDurations(); // { kind: 'range', min: 1, max: 15, step: 1, unit: 'seconds' }
adapter.snapDuration(2.5); // 3 — clamped/rounded into range
adapter.snapDuration(99); // 15
생성된 클립에는 오디오 트랙이 포함됩니다. 작업이 완료되면 어댑터는 결과에 usage.billed ({ quantity, unit: 'seconds' } — 비디오의 과금된 초수) 및 usage.cost (API 가 반환한 정확한 USD 비용) 을 보고합니다.
BytePlus (Seedance) 모델 옵션
Seedance 는 Grok Imagine 과 동일한 비율 크기를 갖습니다. size 은 ratio 또는 ratio_resolution 템플릿을 사용합니다. 비율은 16:9, 9:16, 4:3, 3:4, 1:1, 21:9 및 adaptive이며, 해상도는 480p, 720p, 1080p 및 (dreamina-seedance-2-0-260128 에서만) 4k 입니다. Seedance 2.5 (dreamina-seedance-2-5-260628) 는 480p/720p/1080p 를 지원하며 최대 30 초까지 실행됩니다. 모든 Seedance 모델에는 2K 계층이 없습니다.
import { generateVideo } from "@tanstack/ai";
import { byteplusVideo } from "@tanstack/ai-byteplus";
const { jobId } = await generateVideo({
adapter: byteplusVideo("dreamina-seedance-2-0-260128"),
prompt: "A beautiful sunset over the ocean",
size: "16:9_720p",
duration: 5,
modelOptions: {
seed: 42,
generate_audio: true,
priority: 5, // Seedance 2.5 / 2.0 family — queue priority, 0-9
},
});
옵션은 모델별이며 서버 측에서 유효성 검사됩니다: Ark 는 해당 필드를 무시하는 대신 400 으로 거부합니다. service_tier 과 camera_fixed 은 Seedance 1.x 전용이며, frames 는 1.0-pro 모델에서 작동하고, draft 은 1.5-pro 에서, priority 은 Seedance 2.5 와 2.0 계열에서, duration: -1 (모델이 선택하도록 함) 은 2.5, 2.0 및 1.5-pro 에서 작동합니다. 지속 시간은 Seedance 2.5 에서 4–30 초, 2.0 계열에서 4–15 초, 1.5-pro 에서 4–12 초, 1.0-pro 모델에서 2–12 초입니다.
Seedance 비디오 URL 은 작업이 완료된 후 24 시간 동안만 유효합니다(작업 기록은 7 일간 유지됩니다), 따라서 링크 대신 바이트를 영속화하세요. 전체 옵션 표는 BytePlus 어댑터 를 참조하세요. 완료된 작업은 usage.billed 을 { quantity, unit: 'tokens' } 로 보고합니다(Seedance 는 출력 토큰만 과금합니다).
Seedance 호출을 제공자 간에 이전하는 방법
Seedance 는 여러 어댑터를 통해 접근할 수 있습니다 — 이 패키지는 BytePlus 로 직접 연결되는 경로이며, fal 어댑터 는 동일한 모델을 프록시합니다. metadata.role 어휘는 모두 동일하지만(size 은 동일하지 않습니다), 각 제공자가 엔드포인트 크기를 다르게 설정하기 때문입니다:
| 어댑터 | size 형상 | 예시 |
|---|---|---|
@tanstack/ai-byteplus | ratio 또는 ratio_resolution (필요한 비율) | '16:9_720p', '16:9' |
@tanstack/ai-fal | ratio_resolution, ratio, 또는 비율 없이 명시된 해결책 | '16:9_720p', '16:9', '720p' |
비율을 명시하지 않은 size: '720p' 는 fal 에서는 유효하지만 BytePlus 에서는 예외를 발생시키며, BytePlus 는 Grok Imagine 템플릿을 따르고 항상 비율을 요구합니다. 비율을 명시적으로 전달하면 ('16:9_720p') 동일한 문자열이 양쪽 모두에서 작동합니다.
모드 또한 변경됩니다: fal 은 엔드포인트 id 에 이를 인코딩하며 (fal-ai/bytedance/seedance/v1/pro/image-to-video 대 .../reference-to-video), BytePlus 는 하나의 모델 id 를 취하고 프롬프트에 첨부한 부분에서 모드를 추론합니다. 둘 다 구성할 수 없습니다 — 각 제공업체의 자체 API 를 따릅니다.
OpenRouter 모델 옵션
OpenRouter 의 비디오 생성 API
Seedance, Veo, Wan, Kling, Sora 2 Pro 등 여러 모델을 하나의 비동기 작업
API 를 통해 실행합니다. size, duration 및 아래 모델별 옵션은 OpenRouter 의
공식 모델 기능에서 유형 지정되고 모델별로 유효성 검사됩니다 (모델이
지원하지 않는 크기 또는 지속 시간은 요청이 전송되기 전에 예외를 발생시킵니다):
import { generateVideo } from "@tanstack/ai";
import { openRouterVideo } from "@tanstack/ai-openrouter";
const { jobId } = await generateVideo({
adapter: openRouterVideo("bytedance/seedance-2.0"),
prompt: "A beautiful sunset over the ocean",
size: "1280x720", // per-model union from OpenRouter's model metadata
duration: 8, // validated against the model's supported durations
modelOptions: {
resolution: "720p", // alternative to size: resolution + aspectRatio
aspectRatio: "16:9",
generateAudio: true, // omitted from the type for models that can't
seed: 42, // omitted from the type for models that can't
callbackUrl: "https://your-app.com/webhooks/openrouter-video",
provider: { options: { byteplus: { watermark: false } } }, // passthrough
},
});
Veo 어댑터와 마찬가지로 OpenRouter 의 duration는 모델별로 유형 지정됩니다 —
각 모델은 메타데이터에 게시된 전체 초 단위 유니온으로 duration를 제한하며,
어댑터는 동일한 availableDurations() / snapDuration()
인테르스펙션 도구를 구현합니다:
import { generateVideo } from "@tanstack/ai";
import { openRouterVideo } from "@tanstack/ai-openrouter";
const adapter = openRouterVideo("bytedance/seedance-2.0");
adapter.availableDurations();
// { kind: 'discrete', values: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] }
adapter.snapDuration(7.4); // 7 — closest valid duration
const sliderSeconds = 7; // raw seconds from a UI control
await generateVideo({
adapter,
prompt: "A timelapse of clouds",
duration: adapter.snapDuration(sliderSeconds), // coerce to a valid duration
});
알아야 할 OpenRouter 전용 동작 두 가지:
- 완성된 동영상은
data:URL로 도착합니다. OpenRouter의 다운로드 URL에는Authorization헤더에 API 키가 필요하므로, 어댑터가 서버 측에서 콘텐츠를 다운로드하고<video>태그에 바로 전달할 수 있는 base64 데이터 URL을 반환합니다. 약 10MiB를 초과하는 동영상은 경고를 기록하므로, 큰 데이터 URL을 전달하는 대신 자체 스토리지/CDN에 다시 업로드하는 방식을 권장합니다. - 비용은 완료 시 보고됩니다. 게이트웨이는 작업에 실제로 청구된
비용을 보고하며, 완료된 결과에서
usage.cost로 표시됩니다.
fal.ai 모델 옵션
duration 는 @fal-ai/client 에서 엔드포인트별로 타입이 정의됩니다. 인기 있는 모델도
availableDurations() / snapDuration() (Kling 2.6/Pika '5' | '10',
Kling 3 '3'…'15', Luma '5s' | '9s', Veo 3.1 '4s' | '6s' | '8s', WAN
'2'…'15') 를 구현합니다. 지속 시간 필드가 없는 모델 (Minimax, Hunyuan) 은
duration 을 undefined 로 타입화하므로, 하나를 전달하면 컴파일 오류가 발생합니다.
전체 표는 fal adapter 를 참조하세요.
import { generateVideo } from '@tanstack/ai'
import { falVideo } from '@tanstack/ai-fal'
const adapter = falVideo('fal-ai/veo3.1')
adapter.availableDurations() // { kind: 'discrete', values: ['4s', '6s', '8s'] }
await generateVideo({
adapter,
prompt: 'A timelapse of a city skyline at dusk',
duration: adapter.snapDuration(7), // '6s'
})
응답 타입
참고: 아래 인터페이스는 기본 어댑터 수준 타입입니다.
getVideoJobStatus()헬퍼는 단일 병합 객체{ status, progress?, url?, error?, usage? }를 반환하며,jobId또는expiresAt를 반환하지 않습니다.
VideoJobResult(create에서 반환)
interface VideoJobResult {
jobId: string; // Unique job identifier for polling
model: string; // Model used for generation
}
VideoStatusResult(status에서 반환)
interface VideoStatusResult {
jobId: string;
status: "pending" | "processing" | "completed" | "failed";
progress?: number; // 0-100, if available
error?: string; // Error message if failed
}
VideoUrlResult(url에서 반환)
import type { TokenUsage } from "@tanstack/ai";
interface VideoUrlResult {
jobId: string;
url: string; // URL to download/stream the video
expiresAt?: Date; // When the URL expires
// Usage for the completed generation, when the adapter reports it. The
// billed quantity is self-describing: fal reports
// `usage.billed = { quantity, unit: 'units' }` (from its
// `x-fal-billable-units` header), Grok Imagine reports
// `{ quantity, unit: 'seconds' }`.
usage?: TokenUsage;
}
비용 추적(fal): fal은 미디어 생성을 토큰이 아닌 사용량 기반 단위로 청구합니다. fal 어댑터는 실제 청구 수량을 다음과 같이 표시합니다.
usage.billed—{ quantity, unit: 'units' }이며, 여기서'units'는 fal의 엔드포인트에서 정의한 요금 단위를 나타냅니다. 수량을 엔드포인트의 단위 가격(GET https://api.fal.ai/v1/models/pricing?endpoint_id=…)과 결합하여 정확한 비용(billed.quantity * unitPrice)을 계산합니다. 동일한usage.billed이(가) 이미지, 오디오, 음성 및 전사 결과에도 표시됩니다. (지원 중단된 기본 수량usage.unitsBilled도 하위 호환성을 위해 계속 채워집니다. 역방향 호환성을 위해.)
모델 변형
| 모델 | 설명 | 사용 사례 |
|---|---|---|
sora-2 | 더 빠른 생성, 우수한 품질 | 신속한 반복 작업, 프로토타이핑 |
sora-2-pro | 더 높은 품질, 더 느린 생성 | 프로덕션 품질의 출력 |
오류 처리
동영상 생성은 다양한 이유로 실패할 수 있습니다. 항상 적절한 오류 처리를 구현해야 합니다:
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { openaiVideo } from "@tanstack/ai-openai";
try {
const { jobId } = await generateVideo({
adapter: openaiVideo("sora-2"),
prompt: "A scene",
});
// Poll for status...
const status = await getVideoJobStatus({
adapter: openaiVideo("sora-2"),
jobId,
});
if (status.status === "failed") {
console.error("Generation failed:", status.error);
// Handle failure (e.g., retry, notify user)
}
} catch (error) {
if (error instanceof Error) {
if (error.message.includes("Video generation API is not available")) {
console.error(
"Sora API access may be required. Check your OpenAI account.",
);
} else if (error.message.includes("rate limit")) {
console.error("Rate limited. Please wait before trying again.");
} else {
console.error("Unexpected error:", error);
}
}
}
속도 제한 및 할당량
⚠️ 참고: 동영상 생성의 속도 제한 및 할당량은 변경될 수 있으며 계정 등급에 따라 달라질 수 있습니다.
일반적으로 고려할 사항:
- 동영상 생성에는 많은 컴퓨팅 리소스가 필요합니다
- 동시 작업 제한이 적용될 수 있습니다
- 월별 생성 할당량이 있을 수 있습니다
- 더 길거나 품질이 높은 동영상은 더 많은 할당량을 소비합니다
현재 제한은 OpenAI 문서에서 확인하세요.
환경 변수
동영상 어댑터는 각 공급자의 다른 어댑터와 동일한 환경 변수를 사용합니다.
OPENAI_API_KEY: OpenAI API 키(Sora)GOOGLE_API_KEY또는GEMINI_API_KEY: Google API 키(Veo)ARK_API_KEY(또는BYTEPLUS_API_KEY): BytePlus ModelArk 키(Seedance)OPENROUTER_API_KEY: OpenRouter API 키(openRouterVideo)FAL_KEY: fal.ai API 키(falVideo)XAI_API_KEY: xAI API 키(grokVideo)
명시적 API 키
프로덕션 환경에서 사용하거나 명시적인 제어가 필요한 경우:
import { createOpenaiVideo } from "@tanstack/ai-openai";
const adapter = createOpenaiVideo("sora-2", "your-openai-api-key");
이미지 생성과의 차이점
| 측면 | 이미지 생성 | 동영상 생성 |
|---|---|---|
| API 유형 | 동기식 | 작업/폴링 |
| 반환 타입 | ImageGenerationResult | VideoJobResult → VideoStatusResult → VideoUrlResult |
| 대기 시간 | 초 | 분 |
| 여러 출력 | numberOfImages 옵션 | 지원되지 않음 |
| 옵션 필드 | prompt, size, numberOfImages | prompt, size, duration |
알려진 제한 사항
⚠️ 이 제한 사항은 기능이 발전함에 따라 변경될 수 있습니다.
- API 사용 가능 여부: Sora API는 모든 OpenAI 계정에서 사용 가능하지 않을 수 있습니다
- 생성 시간: 동영상 생성에는 몇 분이 걸릴 수 있습니다
- URL 만료: 생성된 동영상 URL은 일정 시간이 지나면 만료될 수 있습니다
- 실시간 진행 상황 없음: 진행 상황 업데이트가 제한되거나 지연될 수 있습니다
- 오디오 제한 사항: 오디오 생성 지원이 제한될 수 있습니다
- 프롬프트 길이: 긴 프롬프트는 잘릴 수 있습니다
모범 사례
- 타임아웃 구현: 폴링 루프에 합리적인 타임아웃을 설정합니다
- 실패를 우아하게 처리: 생성에 실패했을 때의 대체 동작을 마련합니다
- URL 캐시: 동영상 URL을 저장하고 다시 가져오기 전에 만료 여부를 확인합니다
- 사용자 피드백: 생성 중에 명확한 진행 상황 표시기를 보여 줍니다
- 프롬프트 검증: 제출하기 전에 프롬프트의 길이와 내용을 확인합니다
- 사용량 모니터링: 할당량 초과를 방지하도록 생성 사용량을 추적합니다
향후 고려 사항
이 기능은 실험적입니다. 향후 버전에는 다음이 포함될 수 있습니다:
- 추가 동영상 모델 및 provider
- 스트리밍 진행 상황 업데이트
- 동영상 편집 및 조작
- 오디오 트랙 생성
- 일괄 동영상 생성
- 사용자 지정 스타일/미적 요소 제어
업데이트 소식은 TanStack AI 변경 로그에서 확인하시기 바랍니다.