skin-tokens
This commit is contained in:
+101
-20
@@ -4,7 +4,7 @@ import path from 'node:path';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import type {
|
||||
BatchImage, BatchState, FileRef, ImageBatch, Job, JobState, ModelArtifacts, Project, SettingsPage, Text2ImgInput,
|
||||
TextureArtifacts,
|
||||
AnimArtifacts, RigArtifacts, TextureArtifacts,
|
||||
} from '../shared/api.ts';
|
||||
import { DB_PATH } from './config.ts';
|
||||
|
||||
@@ -79,12 +79,13 @@ db.exec(`
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS Images_batchId ON Images (batchId);
|
||||
|
||||
-- проект: работа над одной картинкой из батча (из одной картинки — только один проект)
|
||||
-- проект: работа над одной картинкой из батча (из одной картинки — сколько угодно проектов)
|
||||
CREATE TABLE IF NOT EXISTS Projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created INTEGER NOT NULL, -- мс
|
||||
imageId INTEGER NOT NULL UNIQUE REFERENCES Images(id) ON DELETE CASCADE
|
||||
imageId INTEGER NOT NULL REFERENCES Images(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS Projects_imageId ON Projects (imageId);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
@@ -125,7 +126,35 @@ addColumns('Projects', {
|
||||
texture: 'TEXT', // JSON TextureArtifacts: текстурированная модель, текстуры, развёртка, виды
|
||||
textureJobId: 'TEXT', // prompt_id идущего текстурирования
|
||||
textureError: 'TEXT', // ошибка последнего текстурирования
|
||||
rig: 'TEXT', // JSON RigArtifacts: риг SkinTokens
|
||||
rigJobId: 'TEXT', // 'skintokens:<мс>' — идёт риггинг (процесс сервера)
|
||||
rigError: 'TEXT', // ошибка последнего риггинга
|
||||
anim: 'TEXT', // JSON AnimArtifacts[] (раньше — одна): движения на сгенерированном скелете рига
|
||||
animError: 'TEXT', // ошибка последнего ретаргета
|
||||
});
|
||||
// риггинг — процесс самого сервера: после перезапуска незавершённый риггинг потерян
|
||||
db.exec(`UPDATE Projects SET rigError = 'прервано: сервер перезапускался', rigJobId = NULL WHERE rigJobId IS NOT NULL`);
|
||||
// раньше из картинки можно было сделать только один проект (imageId UNIQUE) — таблица пересобирается без ограничения
|
||||
if ((db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'Projects'").get() as { sql: string }).sql
|
||||
.match(/imageId\s+INTEGER\s+NOT\s+NULL\s+UNIQUE/i)) {
|
||||
const cols = (db.prepare('PRAGMA table_info(Projects)').all() as { name: string; type: string }[]);
|
||||
const extra = cols.filter(c => !['id', 'created', 'imageId'].includes(c.name));
|
||||
const names = cols.map(c => c.name).join(', ');
|
||||
db.exec('PRAGMA foreign_keys = OFF');
|
||||
db.exec('BEGIN');
|
||||
db.exec(`CREATE TABLE Projects_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created INTEGER NOT NULL,
|
||||
imageId INTEGER NOT NULL REFERENCES Images(id) ON DELETE CASCADE
|
||||
${extra.map(c => `, ${c.name} ${c.type}`).join('')}
|
||||
)`);
|
||||
db.exec(`INSERT INTO Projects_new (${names}) SELECT ${names} FROM Projects`);
|
||||
db.exec('DROP TABLE Projects');
|
||||
db.exec('ALTER TABLE Projects_new RENAME TO Projects');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS Projects_imageId ON Projects (imageId)');
|
||||
db.exec('COMMIT');
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
// истории очисток, 3D-моделей и текстур не хранятся (результат — в проекте) — завершённые задачи удаляются
|
||||
db.exec(`DELETE FROM jobs WHERE type IN ('cleanup', 'model', 'texture') AND state <> 'pending'`);
|
||||
|
||||
@@ -215,12 +244,12 @@ interface BatchRow {
|
||||
files: string | null;
|
||||
}
|
||||
|
||||
interface ImageRow { id: number; batchId: number; filename: string; subfolder: string; type: string; projectId: number | null }
|
||||
interface ImageRow { id: number; batchId: number; filename: string; subfolder: string; type: string; projectIds: string | null }
|
||||
|
||||
/** Картинки с id и проектом (если из картинки создан проект). */
|
||||
/** Картинки с id и проектами, созданными из них (через запятую). */
|
||||
const SELECT_IMAGES = `
|
||||
SELECT i.id, i.batchId, i.filename, i.subfolder, i.type, p.id AS projectId
|
||||
FROM Images i LEFT JOIN Projects p ON p.imageId = i.id`;
|
||||
SELECT i.id, i.batchId, i.filename, i.subfolder, i.type, (SELECT GROUP_CONCAT(id) FROM Projects WHERE imageId = i.id) AS projectIds
|
||||
FROM Images i`;
|
||||
|
||||
const SELECT_BATCH = `
|
||||
SELECT b.*, t.shape, t.colors, t.template, t.negative, t.batchSize, t.seed, u.files
|
||||
@@ -253,8 +282,10 @@ const bq = {
|
||||
imageBatch: db.prepare('SELECT batchId FROM Images WHERE type = ? AND subfolder = ? AND filename = ?'),
|
||||
};
|
||||
|
||||
const ids = (s: string | null) => (s ? s.split(',').map(Number).sort((a, b) => a - b) : []);
|
||||
|
||||
const ref = (i: ImageRow): BatchImage => ({
|
||||
id: i.id, filename: i.filename, subfolder: i.subfolder, type: i.type, projectId: i.projectId,
|
||||
id: i.id, filename: i.filename, subfolder: i.subfolder, type: i.type, projectIds: ids(i.projectIds),
|
||||
});
|
||||
|
||||
function toBatch(row: BatchRow, images: BatchImage[]): ImageBatch {
|
||||
@@ -353,13 +384,13 @@ export function deleteBatch(b: ImageBatch) {
|
||||
|
||||
const SELECT_PROJECT = `
|
||||
SELECT p.id, p.created, p.clean, p.cleanUpdated, p.cleanJobId, p.cleanError,
|
||||
p.model, p.modelJobId, p.modelError, p.texture, p.textureJobId, p.textureError,
|
||||
i.id AS imageId, i.batchId, i.filename, i.subfolder, i.type, b.sourceType
|
||||
p.model, p.modelJobId, p.modelError, p.texture, p.textureJobId, p.textureError, p.rig, p.rigJobId, p.rigError, p.anim, p.animError,
|
||||
i.id AS imageId, i.batchId, i.filename, i.subfolder, i.type, b.sourceType,
|
||||
(SELECT GROUP_CONCAT(id) FROM Projects WHERE imageId = i.id) AS projectIds
|
||||
FROM Projects p JOIN Images i ON i.id = p.imageId JOIN ImageBatches b ON b.id = i.batchId`;
|
||||
|
||||
const pq = {
|
||||
insert: db.prepare('INSERT OR IGNORE INTO Projects (created, imageId) VALUES (?, ?)'),
|
||||
byImage: db.prepare('SELECT id FROM Projects WHERE imageId = ?'),
|
||||
insert: db.prepare('INSERT INTO Projects (created, imageId) VALUES (?, ?)'),
|
||||
get: db.prepare(SELECT_PROJECT + ' WHERE p.id = ?'),
|
||||
list: db.prepare(SELECT_PROJECT + ' ORDER BY p.created DESC, p.id DESC LIMIT ?'),
|
||||
byCleanJob: db.prepare('SELECT id FROM Projects WHERE cleanJobId = ?'),
|
||||
@@ -371,12 +402,25 @@ const pq = {
|
||||
// новая модель заменяет прежнюю вместе с текстурами (они от прежней модели); при ошибке всё остаётся
|
||||
finishModel: db.prepare(`UPDATE Projects SET model = COALESCE(?1, model),
|
||||
texture = CASE WHEN ?1 IS NULL THEN texture ELSE ?2 END,
|
||||
rig = CASE WHEN ?1 IS NULL THEN rig ELSE NULL END,
|
||||
anim = CASE WHEN ?1 IS NULL THEN anim ELSE NULL END,
|
||||
modelError = ?3, modelJobId = NULL WHERE id = ?4`),
|
||||
byTextureJob: db.prepare('SELECT id FROM Projects WHERE textureJobId = ?'),
|
||||
startTexture: db.prepare('UPDATE Projects SET textureJobId = ?, textureError = NULL WHERE id = ?'),
|
||||
finishTexture: db.prepare('UPDATE Projects SET texture = COALESCE(?, texture), textureError = ?, textureJobId = NULL WHERE id = ?'),
|
||||
clearModel: db.prepare('UPDATE Projects SET model = NULL, modelError = NULL, texture = NULL, textureError = NULL WHERE id = ?'),
|
||||
clearTexture: db.prepare('UPDATE Projects SET texture = NULL, textureError = NULL WHERE id = ?'),
|
||||
finishTexture: db.prepare(`UPDATE Projects SET texture = COALESCE(?1, texture), rig = CASE WHEN ?1 IS NULL THEN rig ELSE NULL END,
|
||||
anim = CASE WHEN ?1 IS NULL THEN anim ELSE NULL END,
|
||||
textureError = ?2, textureJobId = NULL WHERE id = ?3`),
|
||||
clearModel: db.prepare(`UPDATE Projects SET model = NULL, modelError = NULL, texture = NULL, textureError = NULL, rig = NULL,
|
||||
rigError = NULL, anim = NULL, animError = NULL WHERE id = ?`),
|
||||
clearTexture: db.prepare(`UPDATE Projects SET texture = NULL, textureError = NULL, rig = NULL, rigError = NULL, anim = NULL,
|
||||
animError = NULL WHERE id = ?`),
|
||||
startRig: db.prepare('UPDATE Projects SET rigJobId = ?, rigError = NULL WHERE id = ?'),
|
||||
finishRig: db.prepare(`UPDATE Projects SET rig = COALESCE(?1, rig), anim = CASE WHEN ?1 IS NULL THEN anim ELSE NULL END,
|
||||
rigError = ?2, rigJobId = NULL WHERE id = ?3`),
|
||||
clearRig: db.prepare('UPDATE Projects SET rig = NULL, rigError = NULL, anim = NULL, animError = NULL WHERE id = ?'),
|
||||
getAnims: db.prepare('SELECT anim FROM Projects WHERE id = ?'),
|
||||
setAnims: db.prepare('UPDATE Projects SET anim = ?, animError = ? WHERE id = ?'),
|
||||
setAnimError: db.prepare('UPDATE Projects SET animError = ? WHERE id = ?'),
|
||||
};
|
||||
|
||||
/** Генерация 3D-модели проекта поставлена в очередь. */
|
||||
@@ -420,6 +464,40 @@ export function clearProjectTexture(id: number) {
|
||||
pq.clearTexture.run(id);
|
||||
}
|
||||
|
||||
/** Риггинг проекта начат (процесс сервера). */
|
||||
export function startProjectRig(id: number, jobId: string) {
|
||||
pq.startRig.run(jobId, id);
|
||||
}
|
||||
|
||||
/** Риггинг завершён: новый риг заменяет прежний (при ошибке прежний остаётся). */
|
||||
export function finishProjectRig(id: number, rig: RigArtifacts | null, error?: string) {
|
||||
pq.finishRig.run(rig ? JSON.stringify(rig) : null, error ?? null, id);
|
||||
}
|
||||
|
||||
/** Список анимаций из столбца anim (раньше там хранилась одна — объект). */
|
||||
function animsOf(value: string | null): AnimArtifacts[] {
|
||||
const v = value ? JSON.parse(value) : null;
|
||||
return Array.isArray(v) ? v : v ? [v] : [];
|
||||
}
|
||||
|
||||
/** Ретаргет завершён: новая анимация добавляется к прежним; при ошибке — только текст ошибки. */
|
||||
export function addProjectAnim(id: number, anim: AnimArtifacts | null, error?: string) {
|
||||
if (!anim) { pq.setAnimError.run(error ?? null, id); return; }
|
||||
const list = animsOf((pq.getAnims.get(id) as { anim: string | null }).anim);
|
||||
pq.setAnims.run(JSON.stringify([...list, anim]), null, id);
|
||||
}
|
||||
|
||||
/** Убрать одну анимацию (по имени файла) из проекта; файл в ComfyUI/output остаётся. */
|
||||
export function removeProjectAnim(id: number, filename: string) {
|
||||
const list = animsOf((pq.getAnims.get(id) as { anim: string | null }).anim).filter(a => a.glb.filename !== filename);
|
||||
pq.setAnims.run(list.length ? JSON.stringify(list) : null, null, id);
|
||||
}
|
||||
|
||||
/** Убрать риг из проекта вместе с анимацией на нём (файлы в ComfyUI/output остаются). */
|
||||
export function clearProjectRig(id: number) {
|
||||
pq.clearRig.run(id);
|
||||
}
|
||||
|
||||
/** Убрать очищенную картинку из проекта (файл в ComfyUI/output остаётся). */
|
||||
export function clearProjectClean(id: number) {
|
||||
pq.clearClean.run(id);
|
||||
@@ -429,7 +507,9 @@ interface ProjectRow {
|
||||
id: number; created: number; clean: string | null; cleanUpdated: number | null; cleanJobId: string | null; cleanError: string | null;
|
||||
model: string | null; modelJobId: string | null; modelError: string | null;
|
||||
texture: string | null; textureJobId: string | null; textureError: string | null;
|
||||
rig: string | null; rigJobId: string | null; rigError: string | null; anim: string | null; animError: string | null;
|
||||
imageId: number; batchId: number; filename: string; subfolder: string; type: string; sourceType: string;
|
||||
projectIds: string | null;
|
||||
}
|
||||
|
||||
const json = (s: string | null) => (s ? JSON.parse(s) : null);
|
||||
@@ -437,11 +517,13 @@ const json = (s: string | null) => (s ? JSON.parse(s) : null);
|
||||
function toProject(r: ProjectRow): Project {
|
||||
return {
|
||||
id: r.id, created: r.created, batchId: r.batchId, imageSource: r.sourceType === 'upload' ? 'upload' : 'text',
|
||||
image: { id: r.imageId, filename: r.filename, subfolder: r.subfolder, type: r.type, projectId: r.id },
|
||||
image: { id: r.imageId, filename: r.filename, subfolder: r.subfolder, type: r.type, projectIds: ids(r.projectIds) },
|
||||
clean: json(r.clean), cleanUpdated: r.cleanUpdated,
|
||||
cleanJobId: r.cleanJobId, ...(r.cleanError ? { cleanError: r.cleanError } : {}),
|
||||
model: json(r.model), modelJobId: r.modelJobId, ...(r.modelError ? { modelError: r.modelError } : {}),
|
||||
texture: json(r.texture), textureJobId: r.textureJobId, ...(r.textureError ? { textureError: r.textureError } : {}),
|
||||
rig: json(r.rig), rigJobId: r.rigJobId, ...(r.rigError ? { rigError: r.rigError } : {}),
|
||||
anims: animsOf(r.anim), ...(r.animError ? { animError: r.animError } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -468,10 +550,9 @@ export function getImage(id: number): (BatchImage & { batchId: number }) | null
|
||||
return row ? { ...ref(row), batchId: row.batchId } : null;
|
||||
}
|
||||
|
||||
/** Проект из картинки: из одной картинки — только один проект; если уже есть, возвращается он. */
|
||||
export function projectForImage(imageId: number): number {
|
||||
pq.insert.run(Date.now(), imageId);
|
||||
return (pq.byImage.get(imageId) as { id: number }).id;
|
||||
/** Новый проект из картинки (из одной картинки — сколько угодно проектов). */
|
||||
export function createProjectFor(imageId: number): number {
|
||||
return Number(pq.insert.run(Date.now(), imageId).lastInsertRowid);
|
||||
}
|
||||
|
||||
export function getProject(id: number): Project | null {
|
||||
|
||||
Reference in New Issue
Block a user