import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const here = path.dirname(fileURLToPath(import.meta.url)); const source = fs.readFileSync(path.join(here, "..", "web", "js", "o1keyVideoGenerator.js"), "utf8"); assert.match(source, /const GENERATOR = "O1keyVideoGenerator"/); assert.match(source, /const RESULT = "O1keyVideoResult"/); assert.match(source, /const SAVE_VIDEO = "SaveVideo"/); assert.match(source, /const SAVE_IMAGE = "SaveImage"/); assert.match(source, /import \{ openReferenceImageEditor \} from "\.\/o1keyReferenceImageEditor\.js"/); assert.match(source, /import "\.\/o1keyImageGenerator\.js"/); assert.doesNotMatch(source, /LiteGraph\.createNode\(RESULT\)/); assert.match(source, /LiteGraph\.createNode\(SAVE_VIDEO\)/); assert.match(source, /request\.return_last_frame \? LiteGraph\.createNode\(SAVE_IMAGE\) : null/); assert.match(source, /source\.connect\(0, saveVideo, 0\)/); assert.match(source, /source\.connect\(1, saveImage, 0\)/); assert.match(source, /function createNativeOutputNodes\(source, request, batchId\)/); assert.match(source, /function dispatchNativePreview\(node, descriptor, animated = false\)/); assert.match(source, /api\.dispatchCustomEvent\("executed"/); assert.match(source, /output\.animated = \[true\]/); assert.match(source, /async function submitNativeJob\(source, targets, request, batchId\)/); assert.match(source, /result_node_id: Number\(targets\.saveVideo\.id\)/); assert.match(source, /createNativeOutputNodes\(node, request, batchId\)/); assert.match(source, /function recoverNativeSaveVideo\(node\)/); assert.match(source, /recoverNativeSaveVideo\(node\)/); assert.match(source, /String\(result\.properties\?\.o1keyVideoBatchId \|\| ""\) === batchId/); assert.match(source, /requestJson\("\/o1key\/video\/jobs"/); assert.match(source, /api\.addEventListener\("o1key\.video_job"/); assert.match(source, /run\.disabled = false/); assert.doesNotMatch(source, /独立任务 · 可连续提交 · 提交即运行|o1vg-summary/); assert.doesNotMatch(source, /最多并行|等待后台槽位/); assert.match(source, /value: "overseas_hc", label: "海外"/); assert.doesNotMatch(source, /label: "海外 HC"/); assert.match(source, /const ASSET_CREATION_OPTIONS/); assert.match(source, /value: "auto", label: "自动创建"/); assert.match(source, /value: "manual", label: "手动"/); assert.match(source, /makeField\("素材创建", assetCreation\)/); assert.match(source, /assetCreationField\.style\.display = isSeedance \? "grid" : "none"/); assert.match(source, /assetCreation\._trigger\.disabled = !assetModeAvailable/); assert.match(source, /imageIdField\.style\.display = manualAssets \? "grid" : "none"/); assert.match(source, /videoIdField\.style\.display = manualAssets && activeMode === "multimodal"/); assert.match(source, /asset_creation_mode: manualAssets \? "manual" : "auto"/); assert.match(source, /assets: manualAssets \? manualRequestAssets : \{\}/); assert.match(source, /function requestForRetry\(node\)/); assert.match(source, /request\.asset_creation_mode !== "auto"/); assert.match(source, /return \{ \.\.\.request, asset_creation_mode: "manual", media: \{\}, assets \}/); assert.match(source, /resolved_assets: detail\.resolved_assets \|\| null/); assert.match(source, /const STAGE_LABELS = \{ starting: "正在启动"/); assert.match(source, /STAGE_LABELS\[detail\.stage\] \|\| detail\.stage/); assert.match(source, /const SEEDANCE_REFERENCE_LIMITS = Object\.freeze/); assert.match(source, /minDimension: 300/); assert.match(source, /maxDimension: 6000/); assert.match(source, /minVideoPixels: 407696/); assert.match(source, /maxVideoPixels: 8295044/); assert.match(source, /async function validateReferenceImageFile\(file\)/); assert.match(source, /async function validateReferenceVideoFile\(file\)/); assert.match(source, /if \(kind === "image"\) await validateReferenceImageFile\(file\); else if \(kind === "video"\) await validateReferenceVideoFile\(file\)/); assert.match(source, /宽高 300~6000px · 比例 0\.4~2\.5/); assert.match(source, /title: "参考视频", shortTitle: "视频", hint: "40\.77万~829\.50万像素"/); assert.doesNotMatch(source, /参考图至少需要 480p|≥ 41万像素/); assert.match(source, /window\.o1keyCanvasImagePicker/); assert.match(source, /canvasPick\.append\(el\("b", "", "▣"\), el\("span", "", "画布取图"\)\)/); assert.match(source, /actions\.append\(canvasPick\)/); assert.match(source, /const file = await picker\.readFile\(descriptor\)/); assert.match(source, /media\[key\] = multiple \? \[\.\.\.\(media\[key\] \|\| \[\]\), uploaded\] : uploaded/); assert.doesNotMatch(source, /真人素材 ID/); // Keep the video node on the same visual component language as the image node. assert.match(source, /\.o1vg-field/); assert.match(source, /\.o1vg-select-trigger/); assert.match(source, /\.o1vg-thumb/); assert.match(source, /\.o1vg-generate/); assert.match(source, /\.o1vg-prompt-write/); assert.match(source, /\.o1vg-prompt-wrap\{[^\n]*height:auto;min-height:142px;flex:1 1 142px/); assert.match(source, /\.o1vg-prompt\{[\s\S]*?min-height:142px;max-height:none;resize:none/); assert.match(source, /promptWrite\.title = "AI帮写"/); assert.match(source, /promptWrite\.append\(promptWriteIcon, el\("span", "", "AI帮写"\)\)/); assert.match(source, /requestJson\("\/o1key\/video\/prompt-write"/); assert.match(source, /setPromptWriteStatus\(node, "AI帮写中…", "busy"\)/); assert.match(source, /role: "首帧"/); assert.match(source, /role: index === 0 \? "首帧" : "尾帧"/); assert.match(source, /reference_video_count:/); assert.match(source, /reference_audio_count:/); assert.match(source, /function makeVideoPreview\(item, title\)/); assert.match(source, /video\.preload = "metadata"/); assert.match(source, /video\.currentTime = previewTime/); assert.match(source, /video\.src = descriptorUrl\(item\)/); assert.match(source, /releaseVideoPreviews\(track\)/); assert.match(source, /video\.removeAttribute\("src"\)/); assert.match(source, /if \(kind === "video"\) tile\.append\(makeVideoPreview\(item, title\)\)/); assert.doesNotMatch(source, /drawImage\([^)]*video|canvas\.toDataURL/); assert.match(source, /function makeSelect/); assert.match(source, /function mediaDropDestination/); assert.match(source, /function moveMediaItem/); assert.match(source, /function editMediaImage/); assert.match(source, /openReferenceImageEditor\(\{/); assert.match(source, /addEventListener\("dragstart"/); assert.match(source, /addEventListener\("dragover"/); assert.match(source, /addEventListener\("drop"/); assert.match(source, /\.o1vg-thumb-edit/); assert.match(source, /\.o1vg-thumb-index/); assert.match(source, /function makeMultimodalMediaSection\(node\)/); assert.match(source, /meta\.append\(el\("strong", "", "参考素材"\), hintElement\)/); assert.match(source, /upload\.append\(el\("b", "", "+"\), el\("span", "", "上传"\)\)/); assert.match(source, /input\.accept = sources\.map\(\(source\) => source\.accept\)\.join\(","\)/); assert.match(source, /const sourceForFile = \(file\) =>/); assert.match(source, /addFiles\(files\)/); const multimodalSource = source.slice(source.indexOf("function makeMultimodalMediaSection"), source.indexOf("function nativeSavePrefix")); assert.equal((multimodalSource.match(/actions\.append\(/g) || []).length, 1); assert.doesNotMatch(multimodalSource, /canvasPick/); assert.doesNotMatch(multimodalSource, /o1vg-empty/); assert.doesNotMatch(multimodalSource, /添加\$\{source\.shortTitle\}/); assert.doesNotMatch(multimodalSource, /sourceInput|source\.input/); assert.match(source, /\.o1vg-multimodal-assets \.o1vg-add\{min-width:58px;height:29px;gap:4px;padding:0 10px;[^}]*font-size:11px;font-weight:600/); assert.match(source, /mediaHost\.append\(firstSection, lastSection, multimodalSection\)/); assert.match(source, /multimodalSection\.style\.display = !manualAssets && activeMode === "multimodal" \? "flex" : "none"/); assert.match(source, /automaticHeights = \{ text: 679, first_frame: 824, first_last_frame: 964, multimodal: 824 \}/); assert.match(source, /widget\(node, "generation_mode"\)\?\.value \|\| "multimodal"/); assert.match(source, /const restore = \(\) =>/); assert.match(source, /mode\.value = value\("generation_mode", "multimodal"\)/); assert.match(source, /firstSection\._render\?\.\(\)/); assert.match(source, /multimodalSection\._render\?\.\(\)/); assert.match(source, /node\._o1vgRestore = restore/); assert.match(source, /nodeType\.prototype\.onConfigure = function \(\)/); assert.match(source, /this\._o1vgRestore\?\.\(\)/); assert.match(source, /loadedGraphNode\(node\).*node\._o1vgRestore\?\.\(\)/s); assert.doesNotMatch(source, /const imagesSection = makeMediaSection/); assert.doesNotMatch(source, /const videosSection = makeMediaSection/); assert.doesNotMatch(source, /const audiosSection = makeMediaSection/); assert.match(source, /function applyGeneratorDefaultSize/); assert.match(source, /node\.setSize\?\.\(\[560, 679\]\)/); assert.match(source, /target\.hidden = true/); assert.match(source, /target\.options\.hidden = true/); assert.doesNotMatch(source, /document\.createElement\("select"\)/); // A click creates an independent O1Key job; it must never enter ComfyUI's native queue. assert.doesNotMatch(source, /queuePrompt|app\.queuePrompt|fetchApi\("\/prompt"/); console.log("o1key video generator frontend checks passed");