trellis.2. unified 3d workflows

This commit is contained in:
Aggtaa
2026-09-24 13:34:06 +03:00
parent a47d23603c
commit 8db94e5c31
11 changed files with 690 additions and 177 deletions
+68 -35
View File
@@ -3,10 +3,10 @@ import fs from 'node:fs';
import path from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import type {
BatchImage, BatchState, FileRef, ImageBatch, Job, JobState, Project, SettingsPage, Text2ImgInput,
BatchImage, BatchState, FileRef, ImageBatch, Job, JobState, ModelArtifacts, Project, SettingsPage, Text2ImgInput,
TextureArtifacts,
} from '../shared/api.ts';
import { DB_PATH } from './config.ts';
import { glbTriangles } from './glb.ts';
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
const db = new DatabaseSync(DB_PATH);
@@ -104,17 +104,30 @@ addColumns('Projects', {
cleanJobId: 'TEXT', // prompt_id идущей очистки
cleanError: 'TEXT', // ошибка последней очистки
});
// последняя 3D-модель проекта из очищенной картинки (история не хранится)
// 3D-артефакты проекта (история не хранится): модель без текстур и текстурная группа (TRELLIS.2 или Paint).
// v2: прежние раздельные столбцы model*/texture*/trellis*/paint* убраны, 3D-результаты начаты с нуля.
if (!(db.prepare("SELECT 1 FROM meta WHERE key = 'projects.artifacts.v2'").get())) {
const have = new Set((db.prepare('PRAGMA table_info(Projects)').all() as { name: string }[]).map(c => c.name));
for (const c of ['model', 'modelPreview', 'modelUpdated', 'modelJobId', 'modelError', 'modelFaces',
'texture', 'textureUpdated', 'textureFaces', 'textureJobId', 'textureError',
'trellis', 'trellisUpdated', 'trellisFaces', 'trellisJobId', 'trellisError',
'paint', 'paintUpdated', 'paintFaces', 'paintJobId', 'paintError']) {
if (have.has(c)) db.exec(`ALTER TABLE Projects DROP COLUMN ${c}`);
}
db.exec(`DELETE FROM jobs WHERE type IN ('model', 'texture', 'trellis', 'paint')`);
db.exec(`DELETE FROM settings WHERE page IN ('trellis', 'paint')`);
db.exec(`INSERT INTO meta (key, value) VALUES ('projects.artifacts.v2', '1')`);
}
addColumns('Projects', {
model: 'TEXT', // JSON FileRef GLB
modelPreview: 'TEXT', // JSON FileRef PNG-превью
modelUpdated: 'INTEGER', // мс
modelJobId: 'TEXT', // prompt_id идущей генерации
modelError: 'TEXT', // ошибка последней генерации
modelFaces: 'INTEGER', // число треугольников в GLB (из файла)
model: 'TEXT', // JSON ModelArtifacts: модель без текстур
modelJobId: 'TEXT', // prompt_id идущей генерации
modelError: 'TEXT', // ошибка последней генерации
texture: 'TEXT', // JSON TextureArtifacts: текстурированная модель, текстуры, развёртка, виды
textureJobId: 'TEXT', // prompt_id идущего текстурирования
textureError: 'TEXT', // ошибка последнего текстурирования
});
// истории очисток и 3D-моделей не хранятся (результат — в проекте) — завершённые задачи удаляются
db.exec(`DELETE FROM jobs WHERE type IN ('cleanup', 'model') AND state <> 'pending'`);
// истории очисток, 3D-моделей и текстур не хранятся (результат — в проекте) — завершённые задачи удаляются
db.exec(`DELETE FROM jobs WHERE type IN ('cleanup', 'model', 'texture') AND state <> 'pending'`);
/** Несколько записей одной транзакцией. */
export function transaction<T>(fn: () => T): T {
@@ -340,9 +353,9 @@ export function deleteBatch(b: ImageBatch) {
const SELECT_PROJECT = `
SELECT p.id, p.created, p.clean, p.cleanUpdated, p.cleanJobId, p.cleanError,
p.model, p.modelPreview, p.modelUpdated, p.modelJobId, p.modelError, p.modelFaces,
i.id AS imageId, i.batchId, i.filename, i.subfolder, i.type
FROM Projects p JOIN Images i ON i.id = p.imageId`;
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
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 (?, ?)'),
@@ -355,11 +368,15 @@ const pq = {
clearClean: db.prepare('UPDATE Projects SET clean = NULL, cleanUpdated = NULL, cleanError = NULL WHERE id = ?'),
byModelJob: db.prepare('SELECT id FROM Projects WHERE modelJobId = ?'),
startModel: db.prepare('UPDATE Projects SET modelJobId = ?, modelError = NULL WHERE id = ?'),
finishModel: db.prepare(`UPDATE Projects SET model = COALESCE(?, model), modelPreview = COALESCE(?, modelPreview),
modelFaces = COALESCE(?, modelFaces), modelUpdated = ?, modelError = ?, modelJobId = NULL WHERE id = ?`),
clearModel: db.prepare(`UPDATE Projects SET model = NULL, modelPreview = NULL, modelFaces = NULL, modelUpdated = NULL, modelError = NULL
WHERE id = ?`),
setModelFaces: db.prepare('UPDATE Projects SET modelFaces = ? WHERE id = ?'),
// новая модель заменяет прежнюю вместе с текстурами (они от прежней модели); при ошибке всё остаётся
finishModel: db.prepare(`UPDATE Projects SET model = COALESCE(?1, model),
texture = CASE WHEN ?1 IS NULL THEN texture ELSE ?2 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 = ?'),
};
/** Генерация 3D-модели проекта поставлена в очередь. */
@@ -371,17 +388,38 @@ export function projectByModelJob(jobId: string): number | null {
return (pq.byModelJob.get(jobId) as { id: number } | undefined)?.id ?? null;
}
/** Генерация завершена: новая модель заменяет прежнюю (при ошибке прежняя остаётся). */
export function finishProjectModel(id: number, model: FileRef | null, preview: FileRef | null, error?: string) {
pq.finishModel.run(model ? JSON.stringify(model) : null, preview ? JSON.stringify(preview) : null,
model ? glbTriangles(model) : null, Date.now(), error ?? null, id);
/**
* Генерация завершена: новая модель заменяет прежнюю, прежние текстуры убираются (TRELLIS.2 сразу даёт свои).
* При ошибке (model = null) всё остаётся как было.
*/
export function finishProjectModel(id: number, model: ModelArtifacts | null, texture: TextureArtifacts | null, error?: string) {
pq.finishModel.run(model ? JSON.stringify(model) : null, texture ? JSON.stringify(texture) : null, error ?? null, id);
}
/** Убрать 3D-модель из проекта (файлы в ComfyUI/output остаются). */
/** Текстурирование проекта поставлено в очередь. */
export function startProjectTexture(id: number, jobId: string) {
pq.startTexture.run(jobId, id);
}
export function projectByTextureJob(jobId: string): number | null {
return (pq.byTextureJob.get(jobId) as { id: number } | undefined)?.id ?? null;
}
/** Текстурирование завершено: новый результат заменяет прежний (при ошибке прежний остаётся). */
export function finishProjectTexture(id: number, texture: TextureArtifacts | null, error?: string) {
pq.finishTexture.run(texture ? JSON.stringify(texture) : null, error ?? null, id);
}
/** Убрать модель из проекта вместе с текстурами — они сделаны для неё (файлы в ComfyUI/output остаются). */
export function clearProjectModel(id: number) {
pq.clearModel.run(id);
}
/** Убрать текстурную группу (файлы в ComfyUI/output остаются). */
export function clearProjectTexture(id: number) {
pq.clearTexture.run(id);
}
/** Убрать очищенную картинку из проекта (файл в ComfyUI/output остаётся). */
export function clearProjectClean(id: number) {
pq.clearClean.run(id);
@@ -389,26 +427,21 @@ export function clearProjectClean(id: number) {
interface ProjectRow {
id: number; created: number; clean: string | null; cleanUpdated: number | null; cleanJobId: string | null; cleanError: string | null;
model: string | null; modelPreview: string | null; modelUpdated: number | null; modelJobId: string | null; modelError: string | null;
modelFaces: number | null;
imageId: number; batchId: number; filename: string; subfolder: string; type: string;
model: string | null; modelJobId: string | null; modelError: string | null;
texture: string | null; textureJobId: string | null; textureError: string | null;
imageId: number; batchId: number; filename: string; subfolder: string; type: string; sourceType: string;
}
const json = (s: string | null) => (s ? JSON.parse(s) : null);
function toProject(r: ProjectRow): Project {
// модели, прикреплённые до появления modelFaces, — число полигонов считается из файла один раз
if (r.model && r.modelFaces == null) {
r.modelFaces = glbTriangles(JSON.parse(r.model));
if (r.modelFaces != null) pq.setModelFaces.run(r.modelFaces, r.id);
}
return {
id: r.id, created: r.created, batchId: r.batchId,
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 },
clean: json(r.clean), cleanUpdated: r.cleanUpdated,
cleanJobId: r.cleanJobId, ...(r.cleanError ? { cleanError: r.cleanError } : {}),
model: json(r.model), modelPreview: json(r.modelPreview), modelUpdated: r.modelUpdated,
modelFaces: r.modelFaces, modelJobId: r.modelJobId, ...(r.modelError ? { modelError: r.modelError } : {}),
model: json(r.model), modelJobId: r.modelJobId, ...(r.modelError ? { modelError: r.modelError } : {}),
texture: json(r.texture), textureJobId: r.textureJobId, ...(r.textureError ? { textureError: r.textureError } : {}),
};
}