AIVideo/workflows/novel_to_seedance/scripts/generate_upload_assets.mjs
luoqian 2eb629fe4b 新增图片生成与上传执行管线:nano-banana-2真实出图→上传→公网URL回填→上传后二次生成API调用稿
- 新增 image_generation_upload_rules.md、post_upload_call_draft_rules.md 规则文件,scripts/ 脚本目录
  - 工作流新增阶段 5.1 图片生成与上传、5.2 上传后调用稿分支,调整原有步骤编号
  - 所有模板和规则强制要求:API 图片输入使用上传后的公网 URL / asset:// URI,禁止本地路径或占位符
  - 官网 Prompt 模板强制要求使用 @官网上传名,绑定已上传资产
  - 附件/API 资产清单新增状态追踪列(pending_generation / pending_upload / generated_uploaded_ready 等)和 uploaded_assets.json 回填规则
  - api_payload_rules.md 新增"上传后二次生成硬约束",要求 JSONL 必须在图片上传完成后基于真实 URL 重新生成
  - image_model_provider_rules.md 明确 nano-banana-2 为默认执行资源(非仅提示词名称),新增图片生成/上传执行标记

  ---
  核心变更概括:工作流从"只写提示词"升级为"提示词→真实出图→上传→获取公网 URL→回填→二次生成调用稿"的完整资产管线。
2026-05-26 15:45:46 +08:00

209 lines
6.0 KiB
JavaScript

#!/usr/bin/env node
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const DEFAULT_UPLOAD_URL =
"https://lms.laitool.cn/lms/FileUpload/FileUpload/01564ca6c907eab817058e5356bb40b57cd3f3171adbbc9ea49be0de448cc68e";
function argValue(name, fallback = undefined) {
const index = process.argv.indexOf(name);
if (index === -1 || index + 1 >= process.argv.length) return fallback;
return process.argv[index + 1];
}
function requireArg(name) {
const value = argValue(name);
if (!value) {
throw new Error(`Missing required argument: ${name}`);
}
return value;
}
async function readJson(filePath) {
return JSON.parse(await readFile(filePath, "utf8"));
}
function sanitizeName(value) {
return String(value)
.trim()
.replace(/[\\/:*?"<>|]+/g, "_")
.replace(/\s+/g, "_")
.slice(0, 120);
}
function extensionFromMime(contentType) {
if (contentType === "image/png") return ".png";
if (contentType === "image/webp") return ".webp";
return ".jpg";
}
function extractInlineImage(response) {
const candidates = response?.candidates ?? [];
for (const candidate of candidates) {
const parts = candidate?.content?.parts ?? [];
for (const part of parts) {
const inlineData = part.inlineData ?? part.inline_data;
if (inlineData?.data) {
return {
base64: inlineData.data,
contentType: inlineData.mimeType ?? inlineData.mime_type ?? "image/jpeg",
};
}
}
}
throw new Error("Image model response did not contain inline image data.");
}
function extractUploadUrl(resultText) {
try {
const json = JSON.parse(resultText);
return (
json.url ??
json.fileUrl ??
json.fileURL ??
json.data?.url ??
json.data?.fileUrl ??
json.data?.fileURL ??
json.data?.path ??
json.result?.url ??
json.result?.fileUrl ??
""
);
} catch {
const match = resultText.match(/https?:\/\/[^\s"'<>]+/);
return match?.[0] ?? "";
}
}
async function generateImage({ prompt, model, apiKey, aspectRatio, contentType }) {
const endpoint = `https://magic666.top/v1beta/models/${encodeURIComponent(
model,
)}:generateContent?key=${encodeURIComponent(apiKey)}`;
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contents: [{ role: "user", parts: [{ text: prompt }] }],
generationConfig: {
responseModalities: ["IMAGE"],
...(aspectRatio ? { aspectRatio } : {}),
},
}),
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(`Image generation failed: ${response.status} ${responseText}`);
}
const image = extractInlineImage(JSON.parse(responseText));
return {
base64: image.base64,
contentType: contentType ?? image.contentType,
};
}
async function uploadImage({ base64, fileName, contentType, uploadUrl }) {
const originalSize = Buffer.byteLength(Buffer.from(base64, "base64")).toString();
const response = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
redirect: "follow",
body: JSON.stringify({
file: base64,
fileName,
contentType,
metadata: {
uploadTime: new Date().toISOString(),
originalSize,
},
}),
});
const resultText = await response.text();
if (!response.ok) {
throw new Error(`Image upload failed: ${response.status} ${resultText}`);
}
return {
raw: resultText,
url: extractUploadUrl(resultText),
};
}
async function main() {
const jobsPath = requireArg("--jobs");
const outDir = path.resolve(argValue("--out-dir", "outputs/generated_assets"));
const manifestPath = path.resolve(argValue("--manifest", path.join(outDir, "uploaded_assets.json")));
// const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
const apiKey = "sk-QLEqdjgAE457aFsxmvUHdoj0EnyBcFaX23I4DxCJ8bgnrJNw"
const model = argValue("--model", process.env.NANO_BANANA_MODEL ?? "nano-banana-2");
const uploadUrl = argValue("--upload-url", process.env.IMAGE_UPLOAD_URL ?? DEFAULT_UPLOAD_URL);
if (!apiKey) {
throw new Error("Set GEMINI_API_KEY or GOOGLE_API_KEY before running image generation.");
}
const jobs = await readJson(jobsPath);
if (!Array.isArray(jobs)) {
throw new Error("Jobs file must be a JSON array.");
}
await mkdir(outDir, { recursive: true });
const results = [];
for (const [index, job] of jobs.entries()) {
if (!job.prompt) {
throw new Error(`Job ${index + 1} is missing prompt.`);
}
const generated = await generateImage({
prompt: job.prompt,
model: "gemini-3-pro-image-preview",
apiKey,
aspectRatio: job.aspectRatio,
contentType: job.contentType,
});
const fileName =
job.fileName ??
`${String(index + 1).padStart(3, "0")}_${sanitizeName(job.internalRef ?? job.name ?? "asset")}${extensionFromMime(
generated.contentType,
)}`;
const localPath = path.join(outDir, fileName);
await writeFile(localPath, Buffer.from(generated.base64, "base64"));
const uploaded = await uploadImage({
base64: generated.base64,
fileName,
contentType: generated.contentType,
uploadUrl,
});
results.push({
internalRef: job.internalRef ?? "",
type: job.type ?? "",
promptName: job.promptName ?? job.name ?? "",
fileName,
localPath,
contentType: generated.contentType,
publicUrl: uploaded.url,
uploadRawResponse: uploaded.raw,
jimengUploadName: job.jimengUploadName ?? path.parse(fileName).name,
referenceDuty: job.referenceDuty ?? "",
status: uploaded.url ? "uploaded" : "uploaded_url_unparsed",
});
}
await writeFile(manifestPath, `${JSON.stringify(results, null, 2)}\n`, "utf8");
console.log(`Wrote ${results.length} uploaded asset records to ${manifestPath}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});