Ava2lon commited on
Commit
98fa87c
·
verified ·
1 Parent(s): 48ded4d

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +0 -1
  2. api.py +74 -1
  3. app.py +2 -2
Dockerfile CHANGED
@@ -12,7 +12,6 @@ ENV PYTHONUNBUFFERED=1 \
12
  RUN apt-get update && apt-get install -y --no-install-recommends \
13
  ffmpeg \
14
  fonts-dejavu-core \
15
- libmagic1 \
16
  ca-certificates \
17
  && rm -rf /var/lib/apt/lists/*
18
 
 
12
  RUN apt-get update && apt-get install -y --no-install-recommends \
13
  ffmpeg \
14
  fonts-dejavu-core \
 
15
  ca-certificates \
16
  && rm -rf /var/lib/apt/lists/*
17
 
api.py CHANGED
@@ -1,14 +1,19 @@
1
  from __future__ import annotations
2
 
 
 
 
3
  from pathlib import Path
4
  from typing import Any
5
 
6
- from fastapi import FastAPI, HTTPException
7
  from fastapi.responses import FileResponse
8
  from pydantic import BaseModel, Field
9
 
10
  from renderer.core.config import Settings
 
11
  from renderer.core.models import AIReelsRequest, RenderRequest, Scene
 
12
  from renderer.jobs import JobManager
13
  from renderer.scenes import Timeline
14
 
@@ -51,6 +56,12 @@ class BatchPayload(BaseModel):
51
  jobs: list[RenderPayload]
52
 
53
 
 
 
 
 
 
 
54
  @api.get("/health")
55
  def health() -> dict[str, str]:
56
  return {"status": "ok"}
@@ -77,6 +88,33 @@ def render_batch(payload: BatchPayload) -> dict[str, list[str]]:
77
  return {"job_ids": job_ids}
78
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  @api.get("/status/{job_id}")
81
  def status(job_id: str) -> dict:
82
  try:
@@ -122,3 +160,38 @@ def _render_request(payload: RenderPayload) -> RenderRequest:
122
  )
123
  Timeline(request.scenes)
124
  return request
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ import json
4
+ import shutil
5
+ import uuid
6
  from pathlib import Path
7
  from typing import Any
8
 
9
+ from fastapi import FastAPI, File, Form, HTTPException, UploadFile
10
  from fastapi.responses import FileResponse
11
  from pydantic import BaseModel, Field
12
 
13
  from renderer.core.config import Settings
14
+ from renderer.core.ingest import stage_upload
15
  from renderer.core.models import AIReelsRequest, RenderRequest, Scene
16
+ from renderer.core.utils import safe_filename
17
  from renderer.jobs import JobManager
18
  from renderer.scenes import Timeline
19
 
 
56
  jobs: list[RenderPayload]
57
 
58
 
59
+ class UploadedAsset(BaseModel):
60
+ filename: str
61
+ path: str
62
+ reference: str
63
+
64
+
65
  @api.get("/health")
66
  def health() -> dict[str, str]:
67
  return {"status": "ok"}
 
88
  return {"job_ids": job_ids}
89
 
90
 
91
+ @api.post("/render/upload")
92
+ async def render_upload(request_json: str = Form(...), files: list[UploadFile] = File(default=[])) -> dict[str, str]:
93
+ uploads = await _stage_uploads(files)
94
+ payload_data = _replace_upload_refs(json.loads(request_json), uploads)
95
+ payload = RenderPayload.model_validate(payload_data)
96
+ job_id = job_manager.submit_render(_render_request(payload))
97
+ return {"job_id": job_id, "status_url": f"/status/{job_id}", "download_url": f"/download/{job_id}"}
98
+
99
+
100
+ @api.post("/render/ai-reels/upload")
101
+ async def render_ai_reels_upload(request_json: str = Form(...), files: list[UploadFile] = File(default=[])) -> dict[str, str]:
102
+ uploads = await _stage_uploads(files)
103
+ payload_data = _replace_upload_refs(json.loads(request_json), uploads)
104
+ payload = AIReelsPayload.model_validate(payload_data)
105
+ job_id = job_manager.submit_ai_reels(AIReelsRequest(**payload.model_dump()))
106
+ return {"job_id": job_id, "status_url": f"/status/{job_id}", "download_url": f"/download/{job_id}"}
107
+
108
+
109
+ @api.post("/assets/upload")
110
+ async def upload_assets(files: list[UploadFile] = File(...)) -> dict[str, list[UploadedAsset]]:
111
+ uploads = await _stage_uploads(files)
112
+ assets = [
113
+ UploadedAsset(filename=filename, path=path, reference=f"upload://{filename}") for filename, path in uploads.items()
114
+ ]
115
+ return {"assets": [asset.model_dump() for asset in assets]}
116
+
117
+
118
  @api.get("/status/{job_id}")
119
  def status(job_id: str) -> dict:
120
  try:
 
160
  )
161
  Timeline(request.scenes)
162
  return request
163
+
164
+
165
+ async def _stage_uploads(files: list[UploadFile]) -> dict[str, str]:
166
+ upload_dir = settings.temp_dir / "uploads" / uuid.uuid4().hex
167
+ staged: dict[str, str] = {}
168
+ total_bytes = 0
169
+ for upload in files:
170
+ filename = safe_filename(upload.filename or f"asset_{len(staged)}")
171
+ target = upload_dir / filename
172
+ target.parent.mkdir(parents=True, exist_ok=True)
173
+ with target.open("wb") as output:
174
+ while True:
175
+ chunk = await upload.read(1024 * 1024)
176
+ if not chunk:
177
+ break
178
+ total_bytes += len(chunk)
179
+ if total_bytes > settings.max_download_bytes:
180
+ shutil.rmtree(upload_dir, ignore_errors=True)
181
+ raise HTTPException(status_code=413, detail="Uploaded assets exceed MAX_DOWNLOAD_BYTES")
182
+ output.write(chunk)
183
+ staged[filename] = str(stage_upload(target, upload_dir, filename))
184
+ return staged
185
+
186
+
187
+ def _replace_upload_refs(value: Any, uploads: dict[str, str]) -> Any:
188
+ if isinstance(value, dict):
189
+ return {key: _replace_upload_refs(item, uploads) for key, item in value.items()}
190
+ if isinstance(value, list):
191
+ return [_replace_upload_refs(item, uploads) for item in value]
192
+ if isinstance(value, str) and value.startswith("upload://"):
193
+ name = safe_filename(value.removeprefix("upload://"))
194
+ if name not in uploads:
195
+ raise HTTPException(status_code=400, detail=f"Missing uploaded file for reference: upload://{name}")
196
+ return uploads[name]
197
+ return value
app.py CHANGED
@@ -15,7 +15,7 @@ from renderer.templates import list_templates
15
 
16
 
17
  def create_dashboard() -> gr.Blocks:
18
- with gr.Blocks(title="Basyx FFmpeg Rendering Engine") as demo:
19
  gr.Markdown(
20
  "# Basyx FFmpeg Rendering Engine\n"
21
  "CPU-first rendering backend for reels, shorts, audiograms, captions, slideshows, and batch jobs."
@@ -70,7 +70,7 @@ def create_dashboard() -> gr.Blocks:
70
  inspect_output = gr.JSON(label="Metadata")
71
  inspect_button.click(fn=_inspect_asset, inputs=asset_path, outputs=inspect_output)
72
 
73
- return demo
74
 
75
 
76
  def _submit_render_json(payload: str) -> dict[str, Any]:
 
15
 
16
 
17
  def create_dashboard() -> gr.Blocks:
18
+ with gr.Blocks(title="Basyx FFmpeg Rendering Engine") as dashboard:
19
  gr.Markdown(
20
  "# Basyx FFmpeg Rendering Engine\n"
21
  "CPU-first rendering backend for reels, shorts, audiograms, captions, slideshows, and batch jobs."
 
70
  inspect_output = gr.JSON(label="Metadata")
71
  inspect_button.click(fn=_inspect_asset, inputs=asset_path, outputs=inspect_output)
72
 
73
+ return dashboard
74
 
75
 
76
  def _submit_render_json(payload: str) -> dict[str, Any]: