#!/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); });