trellis.2. unified 3d workflows
This commit is contained in:
+68
-35
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user