2d50679ec0
- FastAPI with job polling: POST /generate, GET /jobs/{id}, /download, /health
- SQLite job persistence (aiosqlite), background task processing
- LTX-2.3 DistilledPipeline via native ltx-pipelines (editable install from submodule)
- Configurable model paths, LoRA support, FP8 quantization via env vars
- Single-concurrency GPU lock for safe inference
- LTX-2 as git submodule under libs/
119 lines
3.6 KiB
Python
119 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
import aiosqlite
|
|
from pydantic import BaseModel
|
|
|
|
|
|
VALID_FRAME_COUNTS = {8 * n + 1 for n in range(1, 129)}
|
|
VALID_STATUSES = {"pending", "processing", "completed", "failed"}
|
|
|
|
|
|
class JobRecord(BaseModel):
|
|
id: str
|
|
status: str = "pending"
|
|
prompt: Optional[str] = None
|
|
params: dict | None = None
|
|
error: Optional[str] = None
|
|
created_at: str
|
|
completed_at: Optional[str] = None
|
|
|
|
|
|
class JobDB:
|
|
def __init__(self, db_path: str) -> None:
|
|
self.db_path = db_path
|
|
|
|
async def init(self) -> None:
|
|
os.makedirs(os.path.dirname(self.db_path) or ".", exist_ok=True)
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
await db.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
id TEXT PRIMARY KEY,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
prompt TEXT,
|
|
params TEXT,
|
|
error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
completed_at TEXT
|
|
)
|
|
"""
|
|
)
|
|
await db.commit()
|
|
|
|
async def create_job(self, job: JobRecord) -> None:
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
await db.execute(
|
|
"INSERT INTO jobs (id, status, prompt, params, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
(
|
|
job.id,
|
|
job.status,
|
|
job.prompt,
|
|
_json_encode(job.params),
|
|
job.created_at,
|
|
),
|
|
)
|
|
await db.commit()
|
|
|
|
async def get_job(self, job_id: str) -> JobRecord | None:
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
db.row_factory = aiosqlite.Row
|
|
async with db.execute("SELECT * FROM jobs WHERE id = ?", (job_id,)) as cursor:
|
|
row = await cursor.fetchone()
|
|
if row is None:
|
|
return None
|
|
return JobRecord(
|
|
id=row["id"],
|
|
status=row["status"],
|
|
prompt=row["prompt"],
|
|
params=_json_decode(row["params"]),
|
|
error=row["error"],
|
|
created_at=row["created_at"],
|
|
completed_at=row["completed_at"],
|
|
)
|
|
|
|
async def update_status(
|
|
self,
|
|
job_id: str,
|
|
status: str,
|
|
error: Optional[str] = None,
|
|
) -> None:
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
if status in {"completed", "failed"}:
|
|
await db.execute(
|
|
"UPDATE jobs SET status = ?, error = ?, completed_at = ? WHERE id = ?",
|
|
(status, error, now, job_id),
|
|
)
|
|
else:
|
|
await db.execute(
|
|
"UPDATE jobs SET status = ?, error = ? WHERE id = ?",
|
|
(status, error, job_id),
|
|
)
|
|
await db.commit()
|
|
|
|
async def delete_job(self, job_id: str) -> bool:
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
cur = await db.execute("DELETE FROM jobs WHERE id = ?", (job_id,))
|
|
await db.commit()
|
|
return cur.rowcount > 0
|
|
|
|
|
|
def _json_encode(obj: dict | None) -> str | None:
|
|
if obj is None:
|
|
return None
|
|
import json
|
|
|
|
return json.dumps(obj)
|
|
|
|
|
|
def _json_decode(s: str | None) -> dict | None:
|
|
if s is None:
|
|
return None
|
|
import json
|
|
|
|
return json.loads(s)
|