86 lines
4.6 KiB
TypeScript
86 lines
4.6 KiB
TypeScript
// Риггинг 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<void> {
|
||
return new Promise<void>((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<void> {
|
||
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<void> {
|
||
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');
|
||
}
|