Files
NianAIGC/lib/server/video-generation-service.ts
2026-05-29 10:26:02 +08:00

171 lines
5.4 KiB
TypeScript

import { assemblePrompt, type PromptAssemblyInput, type PromptMaterial } from "@/lib/prompt/assembler";
import {
createAsset,
createGenerationJob,
getGenerationJob,
recordUsageEvent,
updateGenerationJob
} from "@/lib/server/data-store";
import { DEFAULT_OWNER_ID } from "@/lib/server/runtime";
import { importRemoteAssetAsAsset } from "@/lib/server/storage";
import { createSeedanceTask, getSeedanceConfig, querySeedanceTask, shouldMockSeedance, type SeedanceSettings } from "@/lib/seedance/client";
import type { GenerationJob } from "@/lib/types";
import { normalizeVideoDuration, normalizeVideoRatio, normalizeVideoResolution } from "@/lib/video-settings";
export type SubmitVideoJobInput = PromptAssemblyInput & {
ownerId?: string;
prompt?: string;
settings?: SeedanceSettings;
materials?: PromptMaterial[];
retryOf?: string;
};
export async function submitVideoJob(input: SubmitVideoJobInput, origin: string): Promise<GenerationJob> {
const ownerId = input.ownerId || DEFAULT_OWNER_ID;
const config = getSeedanceConfig();
const assembled = assemblePrompt({
...input,
mode: "video",
materials: input.materials || []
});
const finalPrompt = input.prompt?.trim() || assembled.prompt;
const settings: SeedanceSettings = { ...(input.settings || {}) };
settings.ratio = normalizeVideoRatio(settings.ratio, config.ratio);
settings.duration = normalizeVideoDuration(settings.duration) ?? config.duration;
settings.resolution = normalizeVideoResolution(settings.resolution, config.model, config.resolution);
const mock = shouldMockSeedance();
let job = await createGenerationJob({
ownerId,
capability: "video.generate",
provider: mock ? "mock" : "seedance",
reqKey: config.model,
status: "queued",
prompt: finalPrompt,
inputAssetIds: input.materials?.map((material) => material.id).filter(Boolean) as string[] || [],
inputUrls: assembled.materials.map((material) => material.url),
outputAssetIds: [],
requestPayload: {
input,
assembled,
settings
},
retryOf: input.retryOf
});
if (mock) return completeMockVideoJob(job);
try {
const response = await createSeedanceTask({
prompt: finalPrompt,
settings,
materials: assembled.materials,
origin
});
return updateGenerationJob(job.id, {
status: "running",
providerTaskId: response.providerTaskId,
responsePayload: response.raw
});
} catch (error) {
job = await updateGenerationJob(job.id, {
status: "failed",
error: {
message: error instanceof Error ? error.message : String(error),
retryable: false
}
});
return job;
}
}
export async function syncVideoJob(jobId: string, origin: string): Promise<GenerationJob> {
const job = await getGenerationJob(jobId);
if (!job) throw new Error(`Generation job not found: ${jobId}`);
if (["succeeded", "failed", "cancelled", "expired"].includes(job.status)) return job;
if (job.provider === "mock") return completeMockVideoJob(job);
if (!job.providerTaskId) return job;
try {
const result = await querySeedanceTask(job.providerTaskId);
if (result.status !== "succeeded" || !result.resultUrl) {
return updateGenerationJob(job.id, {
status: result.status,
responsePayload: result.raw,
error: result.errorMessage ? { message: result.errorMessage, retryable: result.status === "failed" } : undefined
});
}
const asset = await importRemoteAssetAsAsset({
ownerId: job.ownerId,
url: result.resultUrl,
origin,
source: "generated",
capability: "video.generate",
jobId: job.id,
index: 0,
fallbackContentType: "video/mp4"
});
await recordUsageEvent({
ownerId: job.ownerId,
jobId: job.id,
capability: "video.generate",
quantity: 1,
estimatedUnit: "job"
});
return updateGenerationJob(job.id, {
status: "succeeded",
outputAssetIds: [asset.id],
responsePayload: result.raw
});
} catch (error) {
return updateGenerationJob(job.id, {
status: "failed",
error: {
message: error instanceof Error ? error.message : String(error),
retryable: false
}
});
}
}
export async function retryVideoJob(jobId: string, origin: string): Promise<GenerationJob> {
const job = await getGenerationJob(jobId);
if (!job) throw new Error(`Generation job not found: ${jobId}`);
const input = (job.requestPayload.input || {}) as SubmitVideoJobInput;
return submitVideoJob({ ...input, retryOf: job.id }, origin);
}
async function completeMockVideoJob(job: GenerationJob): Promise<GenerationJob> {
if (job.status === "succeeded" && job.outputAssetIds.length > 0) return job;
const asset = await createAsset({
ownerId: job.ownerId,
kind: "video",
name: `mock-video-${job.id}.mp4`,
url: "/seedance-starter-assets/music_sync_ad/2-3-9-1/result.mp4",
source: "generated",
tags: ["video.generate", "mock"],
metadata: {
mock: true,
capability: "video.generate",
jobId: job.id
}
});
await recordUsageEvent({
ownerId: job.ownerId,
jobId: job.id,
capability: "video.generate",
quantity: 1,
estimatedUnit: "job"
});
return updateGenerationJob(job.id, {
status: "succeeded",
outputAssetIds: [asset.id],
responsePayload: {
mock: true,
data: {
status: "done",
video_url: asset.url
}
}
});
}