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
58 changes: 58 additions & 0 deletions api/routers/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,64 @@ async def generate_from_image(



@router.post("/from-text")
async def generate_from_text(
background_tasks: BackgroundTasks,
prompt: str = Form(...),
model_id: str = Form("sf3d"),
collection: str = Form("Default"),
remesh: str = Form("quad"),
enable_texture: bool = Form(False),
texture_resolution: int = Form(1024),
params: str = Form("{}"),
):
if not prompt or not prompt.strip():
raise HTTPException(400, "Prompt is required")

if remesh not in VALID_REMESH_MODES:
raise HTTPException(400, "remesh must be 'quad', 'triangle', or 'none'")

collection = sanitize_collection(collection)

# Verify the requested model exists in the registry
try:
generator_registry.get_generator(model_id)
except ValueError as e:
raise HTTPException(400, str(e))

generator_registry.switch_model(model_id)

# Parse model-specific params from JSON and merge with common fields
try:
model_params = json.loads(params)
except (json.JSONDecodeError, TypeError):
model_params = {}

job_id = str(uuid.uuid4())
# Use a 1x1 transparent PNG as placeholder for text-to-image
import base64
placeholder_b64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='
image_bytes = base64.b64decode(placeholder_b64)
full_params = {
"remesh": remesh,
"enable_texture": enable_texture,
"texture_resolution": texture_resolution,
"prompt": prompt,
"text": prompt,
**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()

background_tasks.add_task(_run_generation, job_id, image_bytes, full_params, collection)

return {"job_id": job_id}


@router.get("/status/{job_id}")
async def job_status(job_id: str):
job = _jobs.get(job_id)
Expand Down
53 changes: 52 additions & 1 deletion api/routers/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,53 @@ async def model_params(model_id: Optional[str] = None):
raise HTTPException(404, f"Unknown model ID: {model_id}")


@router.get("/readiness/{model_id}")
async def model_readiness(model_id: str):
"""
Check if a model's runtime is ready (weights downloaded, venv set up, etc.).
Returns: { ready: boolean, status: string, details?: object }
"""
try:
gen = generator_registry.get_generator(model_id)
except ValueError:
raise HTTPException(404, f"Unknown model ID: {model_id}")

manifest = generator_registry.get_manifest(model_id)
ext_id = manifest.get("ext_id", model_id.split("/")[0])

# Check if weights are downloaded
weights_downloaded = gen.is_downloaded()

# Check if setup is needed (for subprocess extensions)
setup_needed = False
if hasattr(gen, '_proc') and gen._proc is None:
# ExtensionProcess - check if venv exists
import os
from pathlib import Path
ext_dir = Path(os.environ.get("EXTENSIONS_DIR", "")) / ext_id
if ext_dir.exists():
venv_python = ext_dir / "venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
setup_needed = not venv_python.exists()

# Determine readiness
ready = weights_downloaded and not setup_needed

status = "ready"
if not weights_downloaded:
status = "weights_missing"
elif setup_needed:
status = "setup_required"

return {
"model_id": model_id,
"ready": ready,
"status": status,
"weights_downloaded": weights_downloaded,
"setup_needed": setup_needed,
"loaded": gen.is_loaded(),
}


@router.post("/switch")
async def switch_model(model_id: str):
"""Switch the active model."""
Expand Down Expand Up @@ -129,6 +176,7 @@ async def hf_download(
skip_prefixes: Optional[str] = None,
include_prefixes: Optional[str] = None,
token: Optional[str] = None,
weight_owner_id: Optional[str] = None,
):
"""
Streams a HuggingFace Hub model download via SSE.
Expand All @@ -138,13 +186,16 @@ async def hf_download(
skip_prefixes: JSON-encoded list of path prefixes to exclude.
include_prefixes: JSON-encoded list of path prefixes to include (whitelist).
token: HuggingFace access token for gated repos (from Electron settings).
weight_owner_id: If set, download into the owner's directory instead of model_id's.
All three fall back to the extension's manifest / environment when not supplied.

SSE format: data: {"percent": 0-100, "file": "...", "status": "..."}
"""
import json as _json
import os
dest_dir = str(MODELS_DIR / model_id)
# Use weight_owner_id if provided, otherwise use model_id
effective_model_id = weight_owner_id or model_id
dest_dir = str(MODELS_DIR / effective_model_id)
# Prefer skip_prefixes passed directly from the client (authoritative, no registry dep)
if skip_prefixes:
try:
Expand Down
1 change: 1 addition & 0 deletions api/services/generator_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ def _discover_extensions(
"download_check": node.get("download_check", ""),
"hf_skip_prefixes": node.get("hf_skip_prefixes", []),
"hf_include_prefixes": node.get("hf_include_prefixes", []),
"weight_owner_id": node.get("weight_owner_id"),
"params_schema": node.get("params_schema", manifest.get("params_schema", [])),
"input": node.get("input", "image"),
"output": node.get("output", "mesh"),
Expand Down
Loading