Initial commit: Revids REST API for LTX-2.3 image-to-video generation
- 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/
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user