arena-learning / studyArena /lib /media /adapters /seedance-adapter.ts
Nitish kumar
Upload folder using huggingface_hub
c20f20c verified
/**
* Seedance (ByteDance / Doubao / Ark) Video Generation Adapter
*
* Uses async task pattern: submit task → poll until succeeded → get video URL.
* Endpoint: https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks
*
* Request format (text-to-video):
* POST /api/v3/contents/generations/tasks
* {
* "model": "doubao-seedance-1-5-pro-251215",
* "content": [{ "type": "text", "text": "prompt here" }],
* "ratio": "16:9",
* "duration": 5,
* "resolution": "1080p",
* "watermark": false
* }
*
* Supported models:
* - doubao-seedance-1-5-pro-251215 (latest, 4~12s)
* - doubao-seedance-1-0-pro-250528 (stable, 2~12s)
* - doubao-seedance-1-0-pro-fast-251015 (faster, 2~12s)
* - doubao-seedance-1-0-lite-t2v-250428 (lightweight, 2~12s)
*
* API docs: https://www.volcengine.com/docs/6492/2165104
*/
import type {
VideoGenerationConfig,
VideoGenerationOptions,
VideoGenerationResult,
} from '../types';
const DEFAULT_MODEL = 'doubao-seedance-1-5-pro-251215';
const DEFAULT_BASE_URL = 'https://ark.cn-beijing.volces.com';
const POLL_INTERVAL_MS = 5000;
const MAX_POLL_ATTEMPTS = 60; // 5 minutes max
/** Response shape for task creation (only returns id) */
interface SeedanceSubmitResponse {
id: string;
}
/** Response shape for task polling */
interface SeedancePollResponse {
id: string;
model: string;
status: 'queued' | 'running' | 'succeeded' | 'failed' | string;
content?: {
video_url?: string;
};
resolution?: string;
ratio?: string;
duration?: number;
framespersecond?: number;
error?: {
message: string;
code?: string;
};
}
/**
* Map aspect ratio to Seedance ratio format.
* Seedance uses the same "W:H" format we already have.
*/
function toSeedanceRatio(aspectRatio?: string): string | undefined {
if (!aspectRatio) return undefined;
return aspectRatio; // Already in "16:9" format
}
/**
* Map resolution to Seedance format.
* Seedance expects "480p", "720p", "1080p".
*/
function toSeedanceResolution(resolution?: string): string | undefined {
if (!resolution) return undefined;
return resolution; // Already in "720p" format
}
/**
* Estimate video dimensions from ratio and resolution for the result.
*/
function estimateDimensions(
ratio?: string,
resolution?: string,
): { width: number; height: number } {
const resMap: Record<string, number> = {
'480p': 480,
'720p': 720,
'1080p': 1080,
};
const h = resMap[resolution || '720p'] || 720;
if (!ratio) return { width: Math.round((h * 16) / 9), height: h };
const [w, hRatio] = ratio.split(':').map(Number);
if (!w || !hRatio) return { width: Math.round((h * 16) / 9), height: h };
return { width: Math.round((h * w) / hRatio), height: h };
}
/**
* Submit a video generation task to Seedance API.
* Returns the task ID for polling.
*/
/**
* Lightweight connectivity test — validates API key by making a GET request
* to poll a non-existent task. If auth fails we get 401/403; if auth succeeds
* we get 404 (task not found), confirming the key is valid.
*/
export async function testSeedanceConnectivity(
config: VideoGenerationConfig,
): Promise<{ success: boolean; message: string }> {
const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
try {
const response = await fetch(
`${baseUrl}/api/v3/contents/generations/tasks/connectivity-test-nonexistent`,
{
method: 'GET',
headers: { Authorization: `Bearer ${config.apiKey}` },
},
);
// 401/403 means key invalid; anything else (404, 400, 200) means key works
if (response.status === 401 || response.status === 403) {
const text = await response.text();
return {
success: false,
message: `Seedance auth failed (${response.status}): ${text}`,
};
}
return { success: true, message: 'Connected to Seedance' };
} catch (err) {
return { success: false, message: `Seedance connectivity error: ${err}` };
}
}
export async function submitSeedanceTask(
config: VideoGenerationConfig,
options: VideoGenerationOptions,
): Promise<string> {
const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
const body: Record<string, unknown> = {
model: config.model || DEFAULT_MODEL,
content: [
{
type: 'text',
text: options.prompt,
},
],
watermark: false,
};
const ratio = toSeedanceRatio(options.aspectRatio);
if (ratio) body.ratio = ratio;
if (options.duration) body.duration = options.duration;
const resolution = toSeedanceResolution(options.resolution);
if (resolution) body.resolution = resolution;
const response = await fetch(`${baseUrl}/api/v3/contents/generations/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Seedance task submission failed (${response.status}): ${text}`);
}
const data = (await response.json()) as SeedanceSubmitResponse;
if (!data.id) {
throw new Error('Seedance returned empty task ID');
}
return data.id;
}
/**
* Poll the status of a Seedance video generation task.
* Returns the result if complete, null if still running.
* Throws on failure.
*/
export async function pollSeedanceTask(
config: VideoGenerationConfig,
taskId: string,
): Promise<VideoGenerationResult | null> {
const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
const response = await fetch(`${baseUrl}/api/v3/contents/generations/tasks/${taskId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${config.apiKey}`,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Seedance poll failed (${response.status}): ${text}`);
}
const data = (await response.json()) as SeedancePollResponse;
if (data.status === 'succeeded') {
if (!data.content?.video_url) {
throw new Error('Seedance task succeeded but no video URL returned');
}
const dims = estimateDimensions(data.ratio, data.resolution);
return {
url: data.content.video_url,
duration: data.duration || 5,
width: dims.width,
height: dims.height,
};
}
if (data.status === 'failed') {
throw new Error(`Seedance video generation failed: ${data.error?.message || 'Unknown error'}`);
}
// queued or running
return null;
}
/**
* Generate a video using Seedance: submit task + poll until complete.
*/
export async function generateWithSeedance(
config: VideoGenerationConfig,
options: VideoGenerationOptions,
): Promise<VideoGenerationResult> {
const taskId = await submitSeedanceTask(config, options);
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
const result = await pollSeedanceTask(config, taskId);
if (result) return result;
}
throw new Error(
`Seedance video generation timed out after ${(MAX_POLL_ATTEMPTS * POLL_INTERVAL_MS) / 1000}s (task: ${taskId})`,
);
}