45 lines
1.8 KiB
TypeScript

import { type EditPlan, editPlanSchema, planTimeline, SECOND } from "./contract";
import { materialize, probeMedia } from "./local";
import { renderEditPlan } from "./render";
// Shared by the local compatibility path and the isolated render service.
export async function renderJob(
value: EditPlan,
userId: string,
dir: string,
progress: (stage: string) => Promise<void>,
) {
const plan = editPlanSchema.parse(value);
planTimeline(plan);
if (plan.assets.reduce((n, a) => n + a.duration, 0) > 1800 * SECOND)
throw Error("素材与分离音轨总时长不能超过 30 分钟");
const files: Record<string, string> = {};
for (const asset of plan.assets) {
await progress("读取原始素材");
const media = await materialize(asset.url, userId),
actual = await probeMedia(media.file);
if (
media.hash !== asset.hash ||
Math.abs(actual.duration - asset.duration) > 100000 ||
actual.hasAudio !== asset.hasAudio ||
actual.width !== asset.width ||
actual.height !== asset.height
)
throw Error("素材内容或参数已变化,请重新生成剪辑方案");
files[asset.id] = media.file;
}
let musicFile: string | undefined;
if (plan.music) {
const media = await materialize(plan.music.url, userId),
meta = await probeMedia(media.file);
if (!meta.hasAudio || plan.music.start >= meta.duration)
throw Error("背景音乐或起始时间无效");
musicFile = media.file;
}
const rendered = await renderEditPlan(plan, files, musicFile, dir, progress);
const actual = await probeMedia(rendered.file, 300.2);
if (Math.abs(actual.duration / SECOND - rendered.duration) > 0.15)
throw Error("导出时长与剪辑方案不一致,未发布结果");
return { ...rendered, duration: actual.duration / SECOND };
}