Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,39 @@ Modly supports external AI model extensions. Each extension is a GitHub reposito

Join the [Discord server](https://discord.gg/BvjDCvS3yr) to stay up to date with the latest news, report bugs, and share feedback.

Follow Modly and its development on X:

- [Modly on X](https://x.com/modly3d)
- [Lightning Pixel on X](https://x.com/lightningpiixel)

---

## Sponsors

<p align="center">
Thanks to our early sponsors for believing in Modly and helping make local AI 3D generation more accessible.
</p>

<p align="center">
<kbd>
<img src="https://images.weserv.nl/?url=github.com/DrHepa.png&w=96&h=96&fit=cover&mask=circle" width="40" height="40" alt="DrHepa" />
<br />
<sub><a href="https://github.com/DrHepa">DrHepa</a></sub>
</kbd>
&nbsp;&nbsp;
<kbd>
<img src="https://images.weserv.nl/?url=github.com/benjapenjamin.png&w=96&h=96&fit=cover&mask=circle" width="40" height="40" alt="benjapenjamin" />
<br />
<sub><a href="https://github.com/benjapenjamin">benjapenjamin</a></sub>
</kbd>
&nbsp;&nbsp;
<kbd>
<img src="https://images.weserv.nl/?url=github.com/iammojogo-sudo.png&w=96&h=96&fit=cover&mask=circle" width="40" height="40" alt="iammojogo-sudo" />
<br />
<sub><a href="https://github.com/iammojogo-sudo">iammojogo-sudo</a></sub>
</kbd>
</p>

---

## License
Expand Down
2 changes: 1 addition & 1 deletion api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def filter(self, record):

app = FastAPI(
title="Modly API",
version="0.3.5",
version="0.3.6",
lifespan=lifespan,
)

Expand Down
20 changes: 20 additions & 0 deletions api/routers/generation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import json
import threading
import time
import traceback
import uuid
from typing import Dict
Expand All @@ -16,6 +17,19 @@
_jobs: Dict[str, JobStatus] = {}
_cancelled: set = set()
_cancel_events: Dict[str, threading.Event] = {}
_completed_at: Dict[str, float] = {}

_JOB_TTL = 1800 # purge terminal jobs after 30 minutes


def _purge_old_jobs() -> None:
cutoff = time.monotonic() - _JOB_TTL
stale = [jid for jid, t in _completed_at.items() if t < cutoff]
for jid in stale:
_jobs.pop(jid, None)
_cancelled.discard(jid)
_cancel_events.pop(jid, None)
_completed_at.pop(jid, None)


@router.post("/from-image")
Expand Down Expand Up @@ -63,6 +77,8 @@ async def generate_from_image(
**model_params,
}

_purge_old_jobs()

job = JobStatus(job_id=job_id, status="pending", progress=0)
_jobs[job_id] = job
_cancel_events[job_id] = threading.Event()
Expand Down Expand Up @@ -91,6 +107,7 @@ async def cancel_job(job_id: str):
_cancel_events[job_id].set()
if job.status in ("pending", "running"):
job.status = "cancelled"
_completed_at[job_id] = time.monotonic()
# Kill the active generator subprocess immediately so inference stops now.
# _run_generation will catch the resulting exception, see job_id in _cancelled,
# and return cleanly without setting an error status.
Expand Down Expand Up @@ -162,6 +179,7 @@ def progress_cb(pct: int, step: str = "") -> None:

job.status = "done"
job.progress = 100
_completed_at[job_id] = time.monotonic()
try:
rel = output_path.relative_to(WORKSPACE_DIR)
job.output_url = f"/workspace/{rel.as_posix()}"
Expand All @@ -170,10 +188,12 @@ def progress_cb(pct: int, step: str = "") -> None:

except GenerationCancelled:
job.status = "cancelled"
_completed_at[job_id] = time.monotonic()
except Exception as exc:
if job_id in _cancelled:
return
tb = traceback.format_exc()
print(f"[Generation ERROR] {exc}\n{tb}")
job.status = "error"
job.error = tb.strip()
_completed_at[job_id] = time.monotonic()
Loading