initial commit
This commit is contained in:
+452
@@ -0,0 +1,452 @@
|
||||
// SQLite (встроенный node:sqlite): задачи ComfyUI, батчи картинок и их источники, значения полей форм.
|
||||
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,
|
||||
} 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);
|
||||
db.exec(`
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- задачи ComfyUI (все типы); отслеживаются трекером
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id TEXT PRIMARY KEY, -- prompt_id ComfyUI
|
||||
type TEXT NOT NULL, -- text2img | cleanup | model
|
||||
created INTEGER NOT NULL, -- мс
|
||||
state TEXT NOT NULL, -- pending | done | error
|
||||
error TEXT,
|
||||
source TEXT, -- JSON FileRef исходника
|
||||
params TEXT NOT NULL, -- JSON параметров
|
||||
images TEXT NOT NULL DEFAULT '[]', -- JSON FileRef[]
|
||||
models TEXT NOT NULL DEFAULT '[]' -- JSON FileRef[]
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS jobs_type_created ON jobs (type, created DESC);
|
||||
CREATE INDEX IF NOT EXISTS jobs_state ON jobs (state);
|
||||
|
||||
-- значения полей форм страниц
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
page TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- батч картинок: результат одного запуска text2img или одной загрузки файлов
|
||||
CREATE TABLE IF NOT EXISTS ImageBatches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created INTEGER NOT NULL, -- мс
|
||||
updated INTEGER NOT NULL,
|
||||
sourceType TEXT NOT NULL, -- text | upload
|
||||
sourceId INTEGER NOT NULL, -- ImageBatchTextSources.id | ImageBatchUploadSources.id (по sourceType)
|
||||
state TEXT NOT NULL, -- draft | pending | done | error
|
||||
error TEXT,
|
||||
jobId TEXT, -- text: prompt_id последнего запуска в ComfyUI
|
||||
seedUsed INTEGER, -- text: фактический seed последнего запуска
|
||||
prompt TEXT, -- text: итоговый позитивный промпт последнего запуска
|
||||
clonedFrom INTEGER REFERENCES ImageBatches(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ImageBatches_created ON ImageBatches (created DESC);
|
||||
|
||||
-- пользовательский ввод text2img — всё, что нужно, чтобы повторить запуск
|
||||
CREATE TABLE IF NOT EXISTS ImageBatchTextSources (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
shape TEXT NOT NULL,
|
||||
colors TEXT NOT NULL,
|
||||
template TEXT NOT NULL, -- как ввёл пользователь; пусто — шаблон по умолчанию
|
||||
negative TEXT NOT NULL,
|
||||
batchSize INTEGER NOT NULL,
|
||||
seed TEXT NOT NULL -- как введено; пусто — случайный
|
||||
);
|
||||
|
||||
-- загрузка файлов пользователем (один или несколько файлов за раз)
|
||||
CREATE TABLE IF NOT EXISTS ImageBatchUploadSources (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
files TEXT NOT NULL -- JSON: исходные имена файлов
|
||||
);
|
||||
|
||||
-- картинки батчей (сгенерированные и загруженные)
|
||||
CREATE TABLE IF NOT EXISTS Images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batchId INTEGER NOT NULL REFERENCES ImageBatches(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
subfolder TEXT NOT NULL,
|
||||
type TEXT NOT NULL, -- input | output
|
||||
UNIQUE (type, subfolder, filename)
|
||||
);
|
||||
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
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// Столбцы, добавленные позже: в существующих базах — ALTER TABLE.
|
||||
function addColumns(table: string, columns: Record<string, string>) {
|
||||
const have = new Set((db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[]).map(c => c.name));
|
||||
for (const [name, def] of Object.entries(columns)) if (!have.has(name)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${def}`);
|
||||
}
|
||||
// последняя очищенная картинка проекта (история очисток не хранится)
|
||||
addColumns('Projects', {
|
||||
clean: 'TEXT', // JSON FileRef последней очищенной картинки
|
||||
cleanUpdated: 'INTEGER', // мс
|
||||
cleanJobId: 'TEXT', // prompt_id идущей очистки
|
||||
cleanError: 'TEXT', // ошибка последней очистки
|
||||
});
|
||||
// последняя 3D-модель проекта из очищенной картинки (история не хранится)
|
||||
addColumns('Projects', {
|
||||
model: 'TEXT', // JSON FileRef GLB
|
||||
modelPreview: 'TEXT', // JSON FileRef PNG-превью
|
||||
modelUpdated: 'INTEGER', // мс
|
||||
modelJobId: 'TEXT', // prompt_id идущей генерации
|
||||
modelError: 'TEXT', // ошибка последней генерации
|
||||
modelFaces: 'INTEGER', // число треугольников в GLB (из файла)
|
||||
});
|
||||
// истории очисток и 3D-моделей не хранятся (результат — в проекте) — завершённые задачи удаляются
|
||||
db.exec(`DELETE FROM jobs WHERE type IN ('cleanup', 'model') AND state <> 'pending'`);
|
||||
|
||||
/** Несколько записей одной транзакцией. */
|
||||
export function transaction<T>(fn: () => T): T {
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
const res = fn();
|
||||
db.exec('COMMIT');
|
||||
return res;
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- jobs ----------
|
||||
|
||||
interface JobRow {
|
||||
id: string; type: string; created: number; state: string; error: string | null;
|
||||
source: string | null; params: string; images: string; models: string;
|
||||
}
|
||||
|
||||
function toJob(row: JobRow): Job {
|
||||
return {
|
||||
id: row.id, type: row.type, created: row.created, state: row.state, ...(row.error ? { error: row.error } : {}),
|
||||
source: row.source ? JSON.parse(row.source) : null, params: JSON.parse(row.params),
|
||||
images: JSON.parse(row.images), models: JSON.parse(row.models),
|
||||
} as Job;
|
||||
}
|
||||
|
||||
const jq = {
|
||||
insert: db.prepare(`INSERT OR IGNORE INTO jobs (id, type, created, state, error, source, params, images, models)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
||||
pending: db.prepare(`SELECT * FROM jobs WHERE state = 'pending'`),
|
||||
finish: db.prepare('UPDATE jobs SET state = ?, error = ?, images = ?, models = ? WHERE id = ?'),
|
||||
};
|
||||
|
||||
export function insertJob(j: Job) {
|
||||
jq.insert.run(j.id, j.type, j.created, j.state, j.error ?? null, j.source ? JSON.stringify(j.source) : null,
|
||||
JSON.stringify(j.params), JSON.stringify(j.images), JSON.stringify(j.models));
|
||||
}
|
||||
|
||||
export function pendingJobs(): Job[] {
|
||||
return (jq.pending.all() as unknown as JobRow[]).map(toJob);
|
||||
}
|
||||
|
||||
export function finishJob(id: string, state: Exclude<JobState, 'pending'>, result: { error?: string; images?: FileRef[]; models?: FileRef[] }) {
|
||||
jq.finish.run(state, result.error ?? null, JSON.stringify(result.images ?? []), JSON.stringify(result.models ?? []), id);
|
||||
}
|
||||
|
||||
|
||||
// ---------- settings, meta ----------
|
||||
|
||||
const sq = {
|
||||
get: db.prepare('SELECT value FROM settings WHERE page = ?'),
|
||||
put: db.prepare('INSERT INTO settings (page, value) VALUES (?, ?) ON CONFLICT(page) DO UPDATE SET value = excluded.value'),
|
||||
getMeta: db.prepare('SELECT value FROM meta WHERE key = ?'),
|
||||
setMeta: db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'),
|
||||
};
|
||||
|
||||
export function getSettings(page: SettingsPage): object {
|
||||
const row = sq.get.get(page) as { value: string } | undefined;
|
||||
return row ? JSON.parse(row.value) : {};
|
||||
}
|
||||
|
||||
export function putSettings(page: SettingsPage, value: object) {
|
||||
sq.put.run(page, JSON.stringify(value));
|
||||
}
|
||||
|
||||
export function getMeta(key: string): string | null {
|
||||
return (sq.getMeta.get(key) as { value: string } | undefined)?.value ?? null;
|
||||
}
|
||||
|
||||
export function setMeta(key: string, value: string) {
|
||||
sq.setMeta.run(key, value);
|
||||
}
|
||||
|
||||
// ---------- ImageBatches + источники + Images ----------
|
||||
|
||||
interface BatchRow {
|
||||
id: number; created: number; updated: number; sourceType: string; sourceId: number; state: string; error: string | null;
|
||||
jobId: string | null; seedUsed: number | null; prompt: string | null; clonedFrom: number | null;
|
||||
// из ImageBatchTextSources
|
||||
shape: string | null; colors: string | null; template: string | null; negative: string | null; batchSize: number | null; seed: string | null;
|
||||
// из ImageBatchUploadSources
|
||||
files: string | null;
|
||||
}
|
||||
|
||||
interface ImageRow { id: number; batchId: number; filename: string; subfolder: string; type: string; projectId: number | null }
|
||||
|
||||
/** Картинки с 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`;
|
||||
|
||||
const SELECT_BATCH = `
|
||||
SELECT b.*, t.shape, t.colors, t.template, t.negative, t.batchSize, t.seed, u.files
|
||||
FROM ImageBatches b
|
||||
LEFT JOIN ImageBatchTextSources t ON b.sourceType = 'text' AND t.id = b.sourceId
|
||||
LEFT JOIN ImageBatchUploadSources u ON b.sourceType = 'upload' AND u.id = b.sourceId`;
|
||||
|
||||
const bq = {
|
||||
get: db.prepare(SELECT_BATCH + ' WHERE b.id = ?'),
|
||||
list: db.prepare(SELECT_BATCH + ' ORDER BY b.created DESC, b.id DESC LIMIT ?'),
|
||||
byJob: db.prepare('SELECT id FROM ImageBatches WHERE jobId = ?'),
|
||||
insertBatch: db.prepare(`INSERT INTO ImageBatches (created, updated, sourceType, sourceId, state, seedUsed, prompt, clonedFrom)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`),
|
||||
insertText: db.prepare(`INSERT INTO ImageBatchTextSources (shape, colors, template, negative, batchSize, seed)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`),
|
||||
updateText: db.prepare(`UPDATE ImageBatchTextSources SET shape = ?, colors = ?, template = ?, negative = ?, batchSize = ?, seed = ?
|
||||
WHERE id = ?`),
|
||||
insertUpload: db.prepare('INSERT INTO ImageBatchUploadSources (files) VALUES (?)'),
|
||||
touch: db.prepare('UPDATE ImageBatches SET updated = ? WHERE id = ?'),
|
||||
start: db.prepare(`UPDATE ImageBatches SET state = 'pending', error = NULL, jobId = ?, seedUsed = ?, prompt = ?, updated = ? WHERE id = ?`),
|
||||
finish: db.prepare('UPDATE ImageBatches SET state = ?, error = ?, updated = ? WHERE id = ?'),
|
||||
removeBatch: db.prepare('DELETE FROM ImageBatches WHERE id = ?'),
|
||||
removeText: db.prepare('DELETE FROM ImageBatchTextSources WHERE id = ?'),
|
||||
removeUpload: db.prepare('DELETE FROM ImageBatchUploadSources WHERE id = ?'),
|
||||
images: db.prepare(SELECT_IMAGES + ' WHERE i.batchId = ? ORDER BY i.id'),
|
||||
listImages: db.prepare(SELECT_IMAGES + `
|
||||
WHERE i.batchId IN (SELECT id FROM ImageBatches ORDER BY created DESC, id DESC LIMIT ?) ORDER BY i.id`),
|
||||
image: db.prepare(SELECT_IMAGES + ' WHERE i.id = ?'),
|
||||
attach: db.prepare('INSERT OR IGNORE INTO Images (batchId, filename, subfolder, type) VALUES (?, ?, ?, ?)'),
|
||||
imageBatch: db.prepare('SELECT batchId FROM Images WHERE type = ? AND subfolder = ? AND filename = ?'),
|
||||
};
|
||||
|
||||
const ref = (i: ImageRow): BatchImage => ({
|
||||
id: i.id, filename: i.filename, subfolder: i.subfolder, type: i.type, projectId: i.projectId,
|
||||
});
|
||||
|
||||
function toBatch(row: BatchRow, images: BatchImage[]): ImageBatch {
|
||||
const base = {
|
||||
id: row.id, created: row.created, updated: row.updated, sourceId: row.sourceId, state: row.state as BatchState,
|
||||
...(row.error ? { error: row.error } : {}), clonedFrom: row.clonedFrom, images,
|
||||
};
|
||||
if (row.sourceType === 'upload') {
|
||||
return { ...base, sourceType: 'upload', source: { id: row.sourceId, files: row.files ? JSON.parse(row.files) : [] } };
|
||||
}
|
||||
return {
|
||||
...base, sourceType: 'text', jobId: row.jobId, seedUsed: row.seedUsed, prompt: row.prompt ?? '',
|
||||
source: {
|
||||
id: row.sourceId,
|
||||
input: { shape: row.shape ?? '', colors: row.colors ?? '', template: row.template ?? '', negative: row.negative ?? '',
|
||||
batch: row.batchSize ?? 1, seed: row.seed ?? '' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getBatch(id: number): ImageBatch | null {
|
||||
const row = bq.get.get(id) as BatchRow | undefined;
|
||||
return row ? toBatch(row, (bq.images.all(id) as unknown as ImageRow[]).map(ref)) : null;
|
||||
}
|
||||
|
||||
export function listBatches(limit = 1000): ImageBatch[] {
|
||||
const byBatch = new Map<number, BatchImage[]>();
|
||||
for (const i of bq.listImages.all(limit) as unknown as ImageRow[]) byBatch.set(i.batchId, [...(byBatch.get(i.batchId) ?? []), ref(i)]);
|
||||
return (bq.list.all(limit) as unknown as BatchRow[]).map(row => toBatch(row, byBatch.get(row.id) ?? []));
|
||||
}
|
||||
|
||||
export function batchByJob(jobId: string): number | null {
|
||||
return (bq.byJob.get(jobId) as { id: number } | undefined)?.id ?? null;
|
||||
}
|
||||
|
||||
/** В каком батче картинка (или null). */
|
||||
export function batchOfImage(i: FileRef): number | null {
|
||||
return (bq.imageBatch.get(i.type, i.subfolder, i.filename) as { batchId: number } | undefined)?.batchId ?? null;
|
||||
}
|
||||
|
||||
/** Новый text-батч с источником. Без transaction — вызывать внутри неё, если нужно вместе с другими записями. */
|
||||
export function insertTextBatch(input: Text2ImgInput, o: { created?: number; state?: BatchState; clonedFrom?: number | null;
|
||||
seedUsed?: number | null; prompt?: string | null } = {}): number {
|
||||
const src = bq.insertText.run(input.shape, input.colors, input.template, input.negative, input.batch, input.seed);
|
||||
const now = Date.now();
|
||||
const res = bq.insertBatch.run(o.created ?? now, now, 'text', Number(src.lastInsertRowid), o.state ?? 'draft',
|
||||
o.seedUsed ?? null, o.prompt ?? null, o.clonedFrom ?? null);
|
||||
return Number(res.lastInsertRowid);
|
||||
}
|
||||
|
||||
export function createTextBatch(input: Text2ImgInput, clonedFrom: number | null = null): number {
|
||||
return transaction(() => insertTextBatch(input, { clonedFrom }));
|
||||
}
|
||||
|
||||
/** Новый upload-батч с загруженными картинками (одна загрузка — один батч). */
|
||||
export function createUploadBatch(originalNames: string[], images: FileRef[]): number {
|
||||
return transaction(() => {
|
||||
const src = bq.insertUpload.run(JSON.stringify(originalNames));
|
||||
const now = Date.now();
|
||||
const id = Number(bq.insertBatch.run(now, now, 'upload', Number(src.lastInsertRowid), 'done', null, null, null).lastInsertRowid);
|
||||
attachImages(id, images);
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
export function updateTextInput(batch: ImageBatch, input: Text2ImgInput) {
|
||||
transaction(() => {
|
||||
bq.updateText.run(input.shape, input.colors, input.template, input.negative, input.batch, input.seed, batch.sourceId);
|
||||
bq.touch.run(Date.now(), batch.id);
|
||||
});
|
||||
}
|
||||
|
||||
export function startBatch(id: number, jobId: string, seedUsed: number, prompt: string) {
|
||||
bq.start.run(jobId, seedUsed, prompt, Date.now(), id);
|
||||
}
|
||||
|
||||
/** Прикрепить картинки к батчу (без своей транзакции). */
|
||||
export function attachImages(id: number, images: FileRef[]) {
|
||||
for (const i of images) bq.attach.run(id, i.filename, i.subfolder, i.type);
|
||||
}
|
||||
|
||||
/** Завершение запуска: картинки к батчу + состояние (без своей транзакции — трекер вызывает внутри своей). */
|
||||
export function finishBatch(id: number, state: 'done' | 'error', images: FileRef[], error?: string) {
|
||||
attachImages(id, images);
|
||||
bq.finish.run(state, error ?? null, Date.now(), id);
|
||||
}
|
||||
|
||||
export function deleteBatch(b: ImageBatch) {
|
||||
transaction(() => {
|
||||
bq.removeBatch.run(b.id);
|
||||
(b.sourceType === 'text' ? bq.removeText : bq.removeUpload).run(b.sourceId);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Projects ----------
|
||||
|
||||
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`;
|
||||
|
||||
const pq = {
|
||||
insert: db.prepare('INSERT OR IGNORE INTO Projects (created, imageId) VALUES (?, ?)'),
|
||||
byImage: db.prepare('SELECT id FROM Projects WHERE imageId = ?'),
|
||||
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 = ?'),
|
||||
startClean: db.prepare('UPDATE Projects SET cleanJobId = ?, cleanError = NULL WHERE id = ?'),
|
||||
finishClean: db.prepare('UPDATE Projects SET clean = COALESCE(?, clean), cleanUpdated = ?, cleanError = ?, cleanJobId = NULL WHERE id = ?'),
|
||||
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 = ?'),
|
||||
};
|
||||
|
||||
/** Генерация 3D-модели проекта поставлена в очередь. */
|
||||
export function startProjectModel(id: number, jobId: string) {
|
||||
pq.startModel.run(jobId, id);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/** Убрать 3D-модель из проекта (файлы в ComfyUI/output остаются). */
|
||||
export function clearProjectModel(id: number) {
|
||||
pq.clearModel.run(id);
|
||||
}
|
||||
|
||||
/** Убрать очищенную картинку из проекта (файл в ComfyUI/output остаётся). */
|
||||
export function clearProjectClean(id: number) {
|
||||
pq.clearClean.run(id);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function listProjects(limit = 1000): Project[] {
|
||||
return (pq.list.all(limit) as unknown as ProjectRow[]).map(toProject);
|
||||
}
|
||||
|
||||
/** Очистка исходника проекта поставлена в очередь. */
|
||||
export function startProjectClean(id: number, jobId: string) {
|
||||
pq.startClean.run(jobId, id);
|
||||
}
|
||||
|
||||
export function projectByCleanJob(jobId: string): number | null {
|
||||
return (pq.byCleanJob.get(jobId) as { id: number } | undefined)?.id ?? null;
|
||||
}
|
||||
|
||||
/** Очистка завершена: результат заменяет прежнюю очищенную картинку (при ошибке прежняя остаётся). */
|
||||
export function finishProjectClean(id: number, clean: FileRef | null, error?: string) {
|
||||
pq.finishClean.run(clean ? JSON.stringify(clean) : null, Date.now(), error ?? null, id);
|
||||
}
|
||||
|
||||
export function getImage(id: number): (BatchImage & { batchId: number }) | null {
|
||||
const row = bq.image.get(id) as ImageRow | undefined;
|
||||
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 getProject(id: number): Project | null {
|
||||
const row = pq.get.get(id) as ProjectRow | undefined;
|
||||
return row ? toProject(row) : null;
|
||||
}
|
||||
|
||||
/** Удаление задачи (история очисток не хранится). */
|
||||
export function deleteJob(id: string) {
|
||||
db.prepare('DELETE FROM jobs WHERE id = ?').run(id);
|
||||
}
|
||||
Reference in New Issue
Block a user