Files
2026-09-30 07:38:21 +03:00

86 lines
4.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Риггинг 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');
}