221 lines
7.4 KiB
TypeScript
221 lines
7.4 KiB
TypeScript
import { resolveGroupInputs } from "./group-execution";
|
|
import { groupTaskInputs } from "./task-store";
|
|
import { executeVideoUpscale } from "./video-upscale";
|
|
import { getUpscaleCheckpoint } from "./task-recovery";
|
|
import { executeSmartEdit } from "./smart-edit/execute";
|
|
import { archiveCanvasVideo } from "./archive-video";
|
|
import { executeCanvasAudio } from "./audio-generation";
|
|
import { ProviderTaskUncertainError } from "./video-task-lifecycle";
|
|
import {
|
|
getEnhanceCheckpoint,
|
|
getVideoCheckpoint,
|
|
getAudioCheckpoint,
|
|
getSmartEditCheckpoint,
|
|
} from "./task-recovery";
|
|
import { requireStoryPromptResult } from "./story-task-attachment";
|
|
import {
|
|
executeImageOperation,
|
|
ImageOperationUncertainError,
|
|
CanvasTaskLeaseLostError,
|
|
} from "./image-operation";
|
|
import {
|
|
executeEnhancePOST,
|
|
executeEnhanceGET,
|
|
} from "@/lib/canvas/operations/enhance";
|
|
import { executeExtendPOST } from "@/lib/canvas/operations/extend";
|
|
import { executeRemoveBackgroundPOST } from "@/lib/canvas/operations/rmbg";
|
|
import { executeAngleEditPOST } from "@/lib/canvas/operations/angle-edit";
|
|
import {
|
|
checkpointCanvasGenerationTask,
|
|
interruptCanvasGenerationTask,
|
|
ownsCanvasTaskLease,
|
|
} from "./task-store";
|
|
import "server-only";
|
|
import { executeDirectVideoGeneration } from "@/lib/ai/tools/generate-video";
|
|
import { executeCanvasImageResponse } from "@/lib/canvas/generate-image-response";
|
|
import { executeWorkflowAnalysisResponse } from "./workflow-analysis-response";
|
|
import {
|
|
requireAnalysisResult,
|
|
type CanvasGenerationTask,
|
|
} from "./task-contract";
|
|
import { validateVideoReferences } from "./reference-input";
|
|
import { probeRemoteUrl } from "@/lib/net/url-access";
|
|
import {
|
|
claimCanvasGenerationTask,
|
|
finishCanvasGenerationTask,
|
|
heartbeatCanvasGenerationTask,
|
|
} from "./task-store";
|
|
|
|
async function execute(task: CanvasGenerationTask, userId: string) {
|
|
const request = task.request;
|
|
const assertLease = async () => {
|
|
if (!(await ownsCanvasTaskLease(task.id, task.attempts!)))
|
|
throw new CanvasTaskLeaseLostError("任务已由新的执行进程接管");
|
|
};
|
|
await assertLease();
|
|
const resume = getEnhanceCheckpoint(task);
|
|
const videoResume = getVideoCheckpoint(task);
|
|
const audioResume = getAudioCheckpoint(task);
|
|
const editResume = getSmartEditCheckpoint(task);
|
|
const upscaleResume = getUpscaleCheckpoint(task);
|
|
if (
|
|
(task.attempts || 1) > 1 &&
|
|
!resume &&
|
|
!videoResume &&
|
|
!audioResume &&
|
|
!editResume &&
|
|
!upscaleResume
|
|
)
|
|
throw Error("缺少上游任务编号,禁止重新提交");
|
|
const params = request.workflow
|
|
? resolveGroupInputs(request, await groupTaskInputs(userId, request))
|
|
: request.params;
|
|
if (request.kind === "video-upscale")
|
|
return executeVideoUpscale(params, userId, task.id, {
|
|
resume: upscaleResume || undefined,
|
|
active: assertLease,
|
|
save: (value) =>
|
|
checkpointCanvasGenerationTask(task.id, value, task.attempts),
|
|
});
|
|
if (request.kind === "smart-edit")
|
|
return executeSmartEdit(params, userId, {
|
|
taskId: task.id,
|
|
resume: editResume,
|
|
assertActive: assertLease,
|
|
checkpoint: (value) =>
|
|
checkpointCanvasGenerationTask(task.id, value, task.attempts),
|
|
});
|
|
if (request.kind === "audio")
|
|
return executeCanvasAudio(params, {
|
|
resume: audioResume,
|
|
assertActive: assertLease,
|
|
checkpoint: (value) =>
|
|
checkpointCanvasGenerationTask(task.id, value, task.attempts),
|
|
});
|
|
if (request.kind === "image-operation")
|
|
return executeImageOperation(
|
|
params,
|
|
userId,
|
|
{
|
|
enhance: executeEnhancePOST,
|
|
extend: executeExtendPOST,
|
|
removeBackground: executeRemoveBackgroundPOST,
|
|
angleEdit: executeAngleEditPOST,
|
|
pollEnhance: executeEnhanceGET,
|
|
},
|
|
{
|
|
resume,
|
|
assertLease,
|
|
checkpoint: (result) =>
|
|
checkpointCanvasGenerationTask(task.id, result, task.attempts),
|
|
}
|
|
);
|
|
if (request.kind === "video") {
|
|
const checked = videoResume
|
|
? { images: params.images as string[] | undefined, error: undefined }
|
|
: await validateVideoReferences(params.images, (url) =>
|
|
probeRemoteUrl(url, { timeoutMs: 8000 })
|
|
);
|
|
if (checked.error) throw Error(checked.error);
|
|
const result = await executeDirectVideoGeneration({
|
|
...params,
|
|
prompt: String(params.prompt || ""),
|
|
mode: params.mode as any,
|
|
images: checked.images,
|
|
taskLifecycle: {
|
|
resume: videoResume,
|
|
assertActive: assertLease,
|
|
save: (checkpoint) =>
|
|
checkpointCanvasGenerationTask(
|
|
task.id,
|
|
{ upstreamTask: checkpoint },
|
|
task.attempts
|
|
),
|
|
},
|
|
});
|
|
if (!result.success || !result.videoUrl) throw Error("视频生成未返回结果");
|
|
// A TVstar group can wait several minutes before editing. Preserve each
|
|
// generated clip before marking its dependency ready for the next node.
|
|
const videoUrl = request.workflow && result.metadata?.permanentVideoUrl !== result.videoUrl
|
|
? await archiveCanvasVideo(result.videoUrl) : result.videoUrl;
|
|
return {
|
|
url: videoUrl,
|
|
metadata: {
|
|
...result.metadata,
|
|
temporaryVideoUrl: result.videoUrl,
|
|
permanentVideoUrl: videoUrl !== result.videoUrl ? videoUrl : result.metadata?.permanentVideoUrl,
|
|
storageStatus: videoUrl !== result.videoUrl || result.metadata?.permanentVideoUrl === result.videoUrl
|
|
? "archived" : "temporary",
|
|
},
|
|
modelId: result.modelId,
|
|
modelRef: result.modelRef,
|
|
};
|
|
}
|
|
const response =
|
|
request.kind === "image"
|
|
? await executeCanvasImageResponse(
|
|
{ ...params, prompt: String(params.prompt || ""), async: false },
|
|
userId
|
|
)
|
|
: await executeWorkflowAnalysisResponse(params);
|
|
const body = await response.json();
|
|
if (!response.ok || !body.success) throw Error(body.error || "任务执行失败");
|
|
if (
|
|
request.kind === "image" &&
|
|
!body.data?.images?.some(
|
|
(image: any) => image.originalUrl || image.thumbnailUrl
|
|
)
|
|
)
|
|
throw Error("图片生成没有返回结果");
|
|
if (request.kind === "analysis")
|
|
requireAnalysisResult(body.data, String(params.mode));
|
|
requireStoryPromptResult(body.data, request.attachment);
|
|
return body.data;
|
|
}
|
|
export async function runCanvasGenerationQueue(
|
|
userId: string,
|
|
projectId: string
|
|
) {
|
|
// Work is independent of the browser request. Atomic DB claiming also covers multiple processes.
|
|
for (let i = 0; i < 40; i++) {
|
|
const task = await claimCanvasGenerationTask(userId, projectId);
|
|
if (!task) return;
|
|
const timer = setInterval(
|
|
() =>
|
|
void heartbeatCanvasGenerationTask(task.id, task.attempts).catch(
|
|
() => {}
|
|
),
|
|
20000
|
|
);
|
|
try {
|
|
await finishCanvasGenerationTask(
|
|
task.id,
|
|
await execute(task, userId),
|
|
undefined,
|
|
task.attempts
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof CanvasTaskLeaseLostError) return;
|
|
if (
|
|
error instanceof ImageOperationUncertainError ||
|
|
error instanceof ProviderTaskUncertainError
|
|
) {
|
|
await interruptCanvasGenerationTask(
|
|
task.id,
|
|
error.message,
|
|
task.attempts
|
|
);
|
|
continue;
|
|
}
|
|
await finishCanvasGenerationTask(
|
|
task.id,
|
|
undefined,
|
|
error instanceof Error ? error.message : "任务执行失败",
|
|
task.attempts
|
|
);
|
|
} finally {
|
|
clearInterval(timer);
|
|
}
|
|
}
|
|
}
|