skin-tokens

This commit is contained in:
Aggtaa
2026-09-30 07:38:21 +03:00
parent 8db94e5c31
commit 9cad7452bb
16 changed files with 1314 additions and 78 deletions
+85
View File
@@ -0,0 +1,85 @@
// Риггинг 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');
}