314 lines
10 KiB
JavaScript
314 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawn, execFileSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import http from 'node:http';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const ROOT = process.cwd();
|
|
const PORT = Number(process.env.TRUEGROWTH_LOCAL_API_PORT || 48177);
|
|
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
const SMOKE_VERSION = 'v1.1.0-smoke';
|
|
const SMOKE_TITLE = 'LatentSync 自动识别烟测模型';
|
|
const TIMEOUT_MS = 30_000;
|
|
const RUNTIME_MODEL_DOWNLOAD_DIR = path.join(ROOT, '.truegrowth-runtime', 'aigcpanel-model-downloads');
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
function delay(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function safeJson(text) {
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function requestJson(url, options = {}) {
|
|
const response = await fetch(url, options);
|
|
const text = await response.text();
|
|
const json = safeJson(text);
|
|
if (!response.ok) {
|
|
throw new Error(`${options.method || 'GET'} ${url} failed ${response.status}: ${text}`);
|
|
}
|
|
return json ?? text;
|
|
}
|
|
|
|
async function waitForApi() {
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < TIMEOUT_MS) {
|
|
try {
|
|
await requestJson(`${BASE_URL}/local-api/health`);
|
|
return;
|
|
} catch {
|
|
await delay(300);
|
|
}
|
|
}
|
|
throw new Error('Local API did not become ready in time.');
|
|
}
|
|
|
|
async function apiIsReady() {
|
|
try {
|
|
await requestJson(`${BASE_URL}/local-api/health`);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function makeSmokePackage() {
|
|
const smokeRoot = path.join(os.tmpdir(), `truegrowth-aigcpanel-flow-${Date.now()}`);
|
|
const modelRoot = path.join(smokeRoot, 'LatentSyncPackage');
|
|
fs.mkdirSync(modelRoot, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(modelRoot, 'config.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: 'LatentSync',
|
|
title: SMOKE_TITLE,
|
|
version: SMOKE_VERSION,
|
|
entry: '__EasyServer__',
|
|
functions: {
|
|
videoGen: { param: [] },
|
|
videoGenFlow: { param: [] },
|
|
},
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
fs.writeFileSync(path.join(modelRoot, 'server.js'), 'module.exports = {};\n');
|
|
const archivePath = path.join(smokeRoot, 'LatentSync-official-auto-smoke.zip');
|
|
execFileSync('zip', ['-qr', archivePath, 'LatentSyncPackage'], {
|
|
cwd: smokeRoot,
|
|
stdio: 'ignore',
|
|
});
|
|
return { smokeRoot, archivePath };
|
|
}
|
|
|
|
function smokeModelDir() {
|
|
return path.join(
|
|
os.homedir(),
|
|
'Library',
|
|
'Application Support',
|
|
'aigcpanel',
|
|
'data',
|
|
'model',
|
|
'latentsync'
|
|
);
|
|
}
|
|
|
|
function storagePath() {
|
|
return path.join(
|
|
os.homedir(),
|
|
'Library',
|
|
'Application Support',
|
|
'aigcpanel',
|
|
'data',
|
|
'storage',
|
|
'server.json'
|
|
);
|
|
}
|
|
|
|
function cleanupSmoke(smokeRoot) {
|
|
const modelDir = smokeModelDir();
|
|
const configPath = path.join(modelDir, 'config.json');
|
|
if (fs.existsSync(configPath)) {
|
|
const config = safeJson(fs.readFileSync(configPath, 'utf8')) || {};
|
|
if (config.version === SMOKE_VERSION || String(config.title || '').includes('烟测')) {
|
|
fs.rmSync(modelDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
const registryPath = storagePath();
|
|
if (fs.existsSync(registryPath)) {
|
|
const registry = safeJson(fs.readFileSync(registryPath, 'utf8')) || {};
|
|
for (const key of ['records', 'items', 'servers', 'models']) {
|
|
if (Array.isArray(registry[key])) {
|
|
registry[key] = registry[key].filter((item) => {
|
|
return !(
|
|
item?.version === SMOKE_VERSION ||
|
|
item?.config?.version === SMOKE_VERSION ||
|
|
String(item?.title || '').includes('烟测')
|
|
);
|
|
});
|
|
}
|
|
}
|
|
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
}
|
|
|
|
const downloadsPath = path.join(ROOT, '.truegrowth-runtime', 'aigcpanel-model-downloads.json');
|
|
if (fs.existsSync(downloadsPath)) {
|
|
const downloads = safeJson(fs.readFileSync(downloadsPath, 'utf8')) || {};
|
|
downloads.items = (Array.isArray(downloads.items) ? downloads.items : []).filter((item) => {
|
|
return !(
|
|
String(item?.filePath || '').includes('LatentSync-official-auto-smoke') ||
|
|
String(item?.finalUrl || '').includes('LatentSync-official-auto-smoke') ||
|
|
item?.version === SMOKE_VERSION
|
|
);
|
|
});
|
|
fs.writeFileSync(downloadsPath, JSON.stringify(downloads, null, 2));
|
|
}
|
|
|
|
if (fs.existsSync(RUNTIME_MODEL_DOWNLOAD_DIR)) {
|
|
for (const entry of fs.readdirSync(RUNTIME_MODEL_DOWNLOAD_DIR, { withFileTypes: true })) {
|
|
if (entry.isFile() && entry.name.includes('LatentSync-official-auto-smoke')) {
|
|
fs.rmSync(path.join(RUNTIME_MODEL_DOWNLOAD_DIR, entry.name), { force: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (smokeRoot) {
|
|
fs.rmSync(smokeRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const { smokeRoot, archivePath } = makeSmokePackage();
|
|
const existingModelDir = smokeModelDir();
|
|
const backupModelDir = fs.existsSync(existingModelDir)
|
|
? path.join(os.tmpdir(), `truegrowth-latentsync-backup-${Date.now()}`)
|
|
: '';
|
|
let child = null;
|
|
try {
|
|
if (backupModelDir) {
|
|
fs.renameSync(existingModelDir, backupModelDir);
|
|
}
|
|
cleanupSmoke(null);
|
|
let stderr = '';
|
|
if (!(await apiIsReady())) {
|
|
child = spawn(process.execPath, ['scripts/truegrowth-local-api.mjs'], {
|
|
cwd: ROOT,
|
|
env: { ...process.env, TRUEGROWTH_LOCAL_API_PORT: String(PORT) },
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
child.stderr.on('data', (chunk) => {
|
|
stderr += String(chunk);
|
|
});
|
|
}
|
|
await waitForApi();
|
|
|
|
fs.mkdirSync(RUNTIME_MODEL_DOWNLOAD_DIR, { recursive: true });
|
|
const scannedArchivePath = path.join(
|
|
RUNTIME_MODEL_DOWNLOAD_DIR,
|
|
`LatentSync-official-auto-smoke-${Date.now()}.zip`
|
|
);
|
|
fs.copyFileSync(archivePath, scannedArchivePath);
|
|
|
|
const scannedBeforeInstall = await requestJson(`${BASE_URL}/local-api/aigcpanel/model-assets`);
|
|
const scannedPackage = scannedBeforeInstall.localPackages?.find(
|
|
(item) => item.path === scannedArchivePath
|
|
);
|
|
assert(scannedPackage, 'Downloaded model package was not discovered by local package scan.');
|
|
assert(scannedPackage.assetId === 'latentsync', 'Scanned package did not infer LatentSync.');
|
|
|
|
const install = await requestJson(
|
|
`${BASE_URL}/local-api/aigcpanel/model-assets/__auto__/install-local`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ localPath: scannedArchivePath }),
|
|
}
|
|
);
|
|
assert(install.ok, 'Auto install response was not ok.');
|
|
assert(install.resolvedAsset?.id === 'latentsync', 'Auto import did not resolve to LatentSync.');
|
|
assert(install.installedModel?.key === `LatentSync|${SMOKE_VERSION}`, 'Installed model key was not returned.');
|
|
assert(install.registration?.registered, 'Installed model was not registered.');
|
|
|
|
const assets = await requestJson(`${BASE_URL}/local-api/aigcpanel/model-assets`);
|
|
const latentsync = assets.items?.find((item) => item.id === 'latentsync');
|
|
assert(latentsync?.installed, 'Model asset list did not show LatentSync as installed.');
|
|
assert(
|
|
latentsync.installedModel?.service?.status === 'registered_pending_reload',
|
|
'Installed model status did not reflect pending local-service refresh.'
|
|
);
|
|
assert(
|
|
Array.isArray(assets.categories) && assets.categories.includes('视频换口型'),
|
|
'Official category list did not include 视频换口型.'
|
|
);
|
|
|
|
const modelsResult = await requestJson(`${BASE_URL}/local-api/aigcpanel/models`);
|
|
const models = Array.isArray(modelsResult)
|
|
? modelsResult
|
|
: modelsResult.items || modelsResult.models || [];
|
|
const avatarModel = models.find((model) => model.key === `LatentSync|${SMOKE_VERSION}`);
|
|
assert(avatarModel, 'Imported model was not exposed through /models for form selection.');
|
|
assert(
|
|
avatarModel.functionNames?.includes('videoGenFlow'),
|
|
'Imported model was missing videoGenFlow in form model list.'
|
|
);
|
|
|
|
const task = await requestJson(`${BASE_URL}/local-api/aigcpanel/tasks`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
capability: 'avatar.render',
|
|
title: '数字人自动模型脚本烟测',
|
|
payload: {
|
|
biz: 'VideoGenFlow',
|
|
model: `LatentSync|${SMOKE_VERSION}`,
|
|
videoUrl: 'local://smoke/avatar.mp4',
|
|
audioUrl: 'local://smoke/voice.wav',
|
|
script: '自动导入模型后生成数字人口播。',
|
|
format: '9:16 竖屏',
|
|
},
|
|
}),
|
|
});
|
|
assert(task.ok && task.task?.id, 'Digital-human task was not accepted.');
|
|
|
|
let finalTask = null;
|
|
for (let i = 0; i < 10; i += 1) {
|
|
const polled = await requestJson(`${BASE_URL}/local-api/tasks/${task.task.id}`);
|
|
finalTask = polled.item;
|
|
if (finalTask?.status === 'completed' || finalTask?.status === 'failed') {
|
|
assert(polled.assets?.some((asset) => asset.type === 'avatar'), 'Completed task did not create avatar asset.');
|
|
break;
|
|
}
|
|
await delay(500);
|
|
}
|
|
assert(finalTask?.status === 'completed', `Digital-human task did not complete: ${finalTask?.status || 'unknown'}`);
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
scannedPackage: scannedPackage.name,
|
|
resolvedAsset: scannedPackage.assetId,
|
|
installedModel: install.installedModel.key,
|
|
modelAssetStatus: latentsync.installedModel.service.status,
|
|
formModelCount: models.length,
|
|
taskStatus: finalTask.status,
|
|
taskAssetIds: finalTask.assetIds,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
if (stderr.trim()) {
|
|
console.error(stderr.trim());
|
|
}
|
|
} finally {
|
|
if (child && !child.killed) {
|
|
child.kill('SIGTERM');
|
|
}
|
|
cleanupSmoke(smokeRoot);
|
|
if (backupModelDir && fs.existsSync(backupModelDir)) {
|
|
fs.rmSync(existingModelDir, { recursive: true, force: true });
|
|
fs.renameSync(backupModelDir, existingModelDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
|
process.exit(1);
|
|
});
|