// Риггинг SkinTokens: skin-tokens.cpp (CPU), собранный в WSL2, запускается как отдельный процесс wsl.exe. // Задача не проходит через ComfyUI: одна за раз, состояние — в проекте (rigJobId), результат пишется по завершении. import { spawn } from 'node:child_process'; import path from 'node:path'; /** Дистрибутив WSL и папка skin-tokens.cpp в нём (dist — установка cmake, models — веса GGUF F16). */ const DISTRO = process.env.SKINTOKENS_DISTRO ?? 'Ubuntu'; const ROOT = process.env.SKINTOKENS_ROOT ?? '/home/anton/skin-tokens.cpp'; const CLI = ROOT + '/dist/bin/skintokens-cli'; const MODEL_DIR = ROOT + '/models/SkinTokens-GGUF/F16'; /** Windows-путь → путь в WSL: D:\a\b.glb → /mnt/d/a/b.glb. */ export function toWsl(p: string): string { const m = /^([A-Za-z]):[\\/](.*)$/.exec(p); if (!m) throw new Error('не абсолютный путь Windows: ' + p); return `/mnt/${m[1].toLowerCase()}/${m[2].replace(/\\/g, '/')}`; } export type Fit = 'none' | 'global' | 'articulated'; export interface RigRun { /** меш (Windows-путь) */ mesh: string; /** готовый скелет (GLB) — тогда только веса под него (skin); без него скелет генерируется (rig) */ skeleton: string | null; fit: Fit; postprocess: boolean; output: string; } let busy = false; /** Идёт риггинг (любого проекта): CPU занят полностью, поэтому по одному. */ export const rigBusy = () => busy; /** Процесс до завершения; ошибка — с последними строками его вывода. */ function run(cmd: string, args: string[], what: string): Promise { return new Promise((resolve, reject) => { // PYTHONIOENCODING: без него print() конвертера падает на кириллице и «→» в кодировке консоли Windows (cp1251) const child = spawn(cmd, args, { windowsHide: true, env: { ...process.env, PYTHONIOENCODING: 'utf-8' } }); let log = ''; const keep = (d: Buffer) => { log = (log + d.toString('utf8')).slice(-4000); }; child.stdout.on('data', keep); child.stderr.on('data', keep); child.on('error', reject); child.on('close', code => { if (code === 0) resolve(); else reject(new Error(`${what}: код ${code}: ${log.replace(/\0/g, '').trim().split('\n').slice(-3).join(' ')}`)); }); }); } const cli = (args: string[]) => run('wsl.exe', ['-d', DISTRO, '--', CLI, ...args], 'skintokens-cli'); /** skintokens-cli rig|skin … --device cpu. */ export async function runSkinTokens(r: RigRun): Promise { const args = r.skeleton ? ['skin', MODEL_DIR, toWsl(r.mesh), toWsl(r.skeleton), toWsl(r.output), '--fit', r.fit] : ['rig', MODEL_DIR, toWsl(r.mesh), toWsl(r.output)]; args.push('--device', 'cpu'); if (r.postprocess) args.push('--postprocess'); busy = true; try { await cli(args); } finally { busy = false; } } /** Python с numpy и pygltflib (встроенный в ComfyUI) и конвертер Mixamo → SOMA30 (img2glb/tools). */ const PYTHON = process.env.IMG2GLB_PYTHON ?? 'D:\\AI\\ComfyUI-0.37.0\\python_embeded\\python.exe'; const MIXAMO2SOMA = path.join(import.meta.dirname, '..', '..', 'tools', 'mixamo2soma30.py'); /** * Анимации Mixamo на сгенерированный SkinTokens скелет (без повторного риггинга, секунды): каждое движение → SOMA30 * с позой покоя под руки этого скелета (mixamo2soma30.py --match), затем все skintokens-cli retarget-generated * одним запуском WSL (сам запуск WSL — основная часть времени). */ export async function runRetarget(o: { rig: string; jobs: { motion: string; soma: string; output: string }[] }): Promise { for (const j of o.jobs) await run(PYTHON, [MIXAMO2SOMA, j.motion, j.soma, '--match', o.rig], 'mixamo2soma30 ' + path.basename(j.motion)); const script = 'set -e; cli="$1"; rig="$2"; shift 2; while [ $# -gt 0 ]; do "$cli" retarget-generated "$rig" "$1" "$2"; shift 2; done'; // --exec: без промежуточной оболочки — иначе wsl.exe склеивает аргументы в строку и $1… раскрываются заранее await run('wsl.exe', ['-d', DISTRO, '--exec', 'bash', '-c', script, 'retarget', CLI, toWsl(o.rig), ...o.jobs.flatMap(j => [toWsl(j.soma), toWsl(j.output)])], 'skintokens-cli retarget-generated'); }