Upload 45 files
Browse files- .gitattributes +0 -3
- .gitignore +3 -1
- Dockerfile +4 -3
- Dockerfile.renderer +2 -0
- N8N_ENDPOINTS.md +62 -69
- README.md +4 -0
- README.renderer.md +13 -3
- access_log_filter.py +81 -0
- api.py +132 -7
- app.py +28 -1
- musicgen_app.py +7 -3
- requirements.txt +1 -6
- requirements.whisper.txt +1 -16
- whisper_app.py +1 -44
.gitattributes
CHANGED
|
@@ -35,6 +35,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
fonts/TikTok-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
gradio_demo.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
-
assets/edu_note.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
-
assets/fun_fact.wav filter=lfs diff=lfs merge=lfs -text
|
| 40 |
-
assets/thanks.wav filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
fonts/TikTok-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
gradio_demo.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
.gitignore
CHANGED
|
@@ -19,7 +19,9 @@ jobs/*
|
|
| 19 |
storage/*
|
| 20 |
!storage/.gitkeep
|
| 21 |
|
|
|
|
|
|
|
|
|
|
| 22 |
*.db
|
| 23 |
*.sqlite
|
| 24 |
*.sqlite3
|
| 25 |
-
|
|
|
|
| 19 |
storage/*
|
| 20 |
!storage/.gitkeep
|
| 21 |
|
| 22 |
+
ingestion/cookies/*
|
| 23 |
+
!ingestion/cookies/.gitkeep
|
| 24 |
+
|
| 25 |
*.db
|
| 26 |
*.sqlite
|
| 27 |
*.sqlite3
|
|
|
Dockerfile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
ENV PYTHONUNBUFFERED=1 \
|
| 4 |
PIP_NO_CACHE_DIR=1 \
|
|
@@ -16,7 +16,9 @@ ENV PYTHONUNBUFFERED=1 \
|
|
| 16 |
FFMPEG_TIMEOUT_SECONDS=900 \
|
| 17 |
DOWNLOAD_TIMEOUT_SECONDS=60 \
|
| 18 |
MAX_DOWNLOAD_BYTES=524288000 \
|
| 19 |
-
ALLOW_PRIVATE_ASSET_URLS=false
|
|
|
|
|
|
|
| 20 |
|
| 21 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 22 |
build-essential \
|
|
@@ -53,4 +55,3 @@ RUN mkdir -p /app/temp /app/exports /app/jobs /app/models /app/storage \
|
|
| 53 |
EXPOSE 7860
|
| 54 |
|
| 55 |
CMD ["bash", "start.sh"]
|
| 56 |
-
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
|
| 3 |
ENV PYTHONUNBUFFERED=1 \
|
| 4 |
PIP_NO_CACHE_DIR=1 \
|
|
|
|
| 16 |
FFMPEG_TIMEOUT_SECONDS=900 \
|
| 17 |
DOWNLOAD_TIMEOUT_SECONDS=60 \
|
| 18 |
MAX_DOWNLOAD_BYTES=524288000 \
|
| 19 |
+
ALLOW_PRIVATE_ASSET_URLS=false \
|
| 20 |
+
ENABLE_API_DOCS=false \
|
| 21 |
+
SUPPRESS_SCANNER_ACCESS_LOGS=true
|
| 22 |
|
| 23 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 24 |
build-essential \
|
|
|
|
| 55 |
EXPOSE 7860
|
| 56 |
|
| 57 |
CMD ["bash", "start.sh"]
|
|
|
Dockerfile.renderer
CHANGED
|
@@ -19,6 +19,8 @@ ENV PYTHONUNBUFFERED=1 \
|
|
| 19 |
WHISPER_DEVICE=cpu \
|
| 20 |
WHISPER_MODEL_DIR=/app/models \
|
| 21 |
MAX_RETRIES=3 \
|
|
|
|
|
|
|
| 22 |
JOB_RETENTION_SECONDS=86400
|
| 23 |
|
| 24 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
|
| 19 |
WHISPER_DEVICE=cpu \
|
| 20 |
WHISPER_MODEL_DIR=/app/models \
|
| 21 |
MAX_RETRIES=3 \
|
| 22 |
+
CALLBACK_MAX_RETRIES=3 \
|
| 23 |
+
CALLBACK_RETRY_SECONDS=1 \
|
| 24 |
JOB_RETENTION_SECONDS=86400
|
| 25 |
|
| 26 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
N8N_ENDPOINTS.md
CHANGED
|
@@ -91,6 +91,10 @@ Recommended n8n flow:
|
|
| 91 |
For payloads with `callback_url`, use an n8n Webhook node's production URL to
|
| 92 |
avoid polling.
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
## Root and Discovery
|
| 95 |
|
| 96 |
| Method | Path | n8n input | Response |
|
|
@@ -105,6 +109,7 @@ These routes use the root base URL.
|
|
| 105 |
| Method | Path | n8n input | Response |
|
| 106 |
| --- | --- | --- | --- |
|
| 107 |
| GET | `/health` | None | Renderer health. |
|
|
|
|
| 108 |
| GET | `/monitor` | None | Runtime, queue, storage, and capability summary. |
|
| 109 |
| GET | `/queue` | None | Job queue summary. |
|
| 110 |
| GET | `/workers` | None | Worker and runtime settings. |
|
|
@@ -190,6 +195,8 @@ Example:
|
|
| 190 |
| Method | Path | n8n input | Auth | Response |
|
| 191 |
| --- | --- | --- | --- | --- |
|
| 192 |
| POST | `/render` | JSON `RenderPayload` or `AIReelsPayload` | None | Async job. |
|
|
|
|
|
|
|
| 193 |
| POST | `/render/ai-reels` | JSON `AIReelsPayload` | None | Async job. |
|
| 194 |
| POST | `/render/batch` | JSON `BatchPayload` | None | Array of job IDs. |
|
| 195 |
| POST | `/automation/batch` | JSON `BatchPayload` | API key | Array of job IDs. |
|
|
@@ -198,6 +205,8 @@ Example:
|
|
| 198 |
| POST | `/render/ai-reels/upload` | Multipart `request_json` plus zero or more `files` | None | Async job. |
|
| 199 |
| POST | `/assets/upload` | Multipart; one or more `files` | None | Uploaded asset references. |
|
| 200 |
| POST | `/upload` | Multipart `files`; optional `expand_zip` | None | Uploaded or expanded assets. |
|
|
|
|
|
|
|
| 201 |
|
| 202 |
Basic render body:
|
| 203 |
|
|
@@ -244,6 +253,36 @@ For `/render/upload`, configure n8n Form-Data as:
|
|
| 244 |
|
| 245 |
The filename in `upload://clip.mp4` must match the uploaded binary filename.
|
| 246 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
## Platform Processing and Toolkit
|
| 248 |
|
| 249 |
All routes in this table require `X-API-Key` and return asynchronous job
|
|
@@ -275,6 +314,21 @@ Toolkit example:
|
|
| 275 |
}
|
| 276 |
```
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
## Transcription, Subtitles, Jobs, and Downloads
|
| 279 |
|
| 280 |
| Method | Path | n8n input | Response |
|
|
@@ -283,13 +337,14 @@ Toolkit example:
|
|
| 283 |
| POST | `/subtitles` | JSON `SubtitlePayload` | File response (`.srt` or `.ass`). |
|
| 284 |
| POST | `/scene-builder` | JSON `SceneBuildPayload` | Generated scene array. |
|
| 285 |
| POST | `/transcribe/upload` | Multipart `file` plus optional transcription fields | Transcription JSON. |
|
| 286 |
-
| GET | `/status` |
|
| 287 |
| GET | `/status/{job_id}` | Replace `{job_id}` | Job record. |
|
| 288 |
| POST | `/cancel/{job_id}` | Replace `{job_id}` | Updated job record. |
|
| 289 |
| POST | `/admin/cleanup` | Optional query `older_than_seconds` | Cleanup counts. |
|
| 290 |
| GET | `/download` | Query `job_id`; optional `token` | File response. |
|
| 291 |
| GET | `/download/{job_id}` | Replace `{job_id}`; optional query `token` | File response. |
|
| 292 |
| POST | `/inspect` | Query `path` | Media metadata JSON. |
|
|
|
|
| 293 |
|
| 294 |
Transcription body:
|
| 295 |
|
|
@@ -392,45 +447,6 @@ strategy
|
|
| 392 |
batch
|
| 393 |
```
|
| 394 |
|
| 395 |
-
## Whisper Authentication
|
| 396 |
-
|
| 397 |
-
These routes exist only when the Whisper authentication subsystem starts
|
| 398 |
-
successfully.
|
| 399 |
-
|
| 400 |
-
| Method | Path | n8n input |
|
| 401 |
-
| --- | --- | --- |
|
| 402 |
-
| POST | `/whisper/api/auth/signup` | JSON `SignupSchema` |
|
| 403 |
-
| POST | `/whisper/api/auth/login` | JSON `LoginSchema` |
|
| 404 |
-
| POST | `/whisper/api/auth/verify` | JSON `TokenSchema` |
|
| 405 |
-
| POST | `/whisper/api/auth/refresh` | JSON `TokenSchema` |
|
| 406 |
-
| POST | `/whisper/api/auth/logout` | No body |
|
| 407 |
-
|
| 408 |
-
Examples:
|
| 409 |
-
|
| 410 |
-
```json
|
| 411 |
-
{
|
| 412 |
-
"email": "user@example.com",
|
| 413 |
-
"username": "studio_user",
|
| 414 |
-
"password": "strong-password"
|
| 415 |
-
}
|
| 416 |
-
```
|
| 417 |
-
|
| 418 |
-
```json
|
| 419 |
-
{
|
| 420 |
-
"email": "user@example.com",
|
| 421 |
-
"password": "strong-password"
|
| 422 |
-
}
|
| 423 |
-
```
|
| 424 |
-
|
| 425 |
-
```json
|
| 426 |
-
{
|
| 427 |
-
"token": "<JWT returned by signup or login>"
|
| 428 |
-
}
|
| 429 |
-
```
|
| 430 |
-
|
| 431 |
-
Logout returns success but does not currently maintain a server-side token
|
| 432 |
-
revocation list.
|
| 433 |
-
|
| 434 |
## FFmpeg Editor
|
| 435 |
|
| 436 |
| Method | Path | n8n input | Response |
|
|
@@ -610,6 +626,7 @@ music_volume: number = 0.316
|
|
| 610 |
music_fade_in, music_fade_out, music_start: number = 0
|
| 611 |
music_loop, music_ducking: boolean = true
|
| 612 |
voice_volume: number = 1
|
|
|
|
| 613 |
```
|
| 614 |
|
| 615 |
`BatchPayload`
|
|
@@ -837,29 +854,6 @@ transition: string = "fade"
|
|
| 837 |
creative_style: string | null
|
| 838 |
```
|
| 839 |
|
| 840 |
-
### Authentication
|
| 841 |
-
|
| 842 |
-
`SignupSchema`
|
| 843 |
-
|
| 844 |
-
```text
|
| 845 |
-
email*: valid email
|
| 846 |
-
username*: 3-32 letters, numbers, or underscores
|
| 847 |
-
password*: 8-72 characters
|
| 848 |
-
```
|
| 849 |
-
|
| 850 |
-
`LoginSchema`
|
| 851 |
-
|
| 852 |
-
```text
|
| 853 |
-
email*: valid email
|
| 854 |
-
password*: 1-72 characters
|
| 855 |
-
```
|
| 856 |
-
|
| 857 |
-
`TokenSchema`
|
| 858 |
-
|
| 859 |
-
```text
|
| 860 |
-
token*: 10-2048 characters
|
| 861 |
-
```
|
| 862 |
-
|
| 863 |
## Framework and UI Endpoints
|
| 864 |
|
| 865 |
These are not business API operations:
|
|
@@ -872,9 +866,8 @@ These are not business API operations:
|
|
| 872 |
| `/whisper/` | Whisper Gradio interface. |
|
| 873 |
| `/editor/` | FFmpeg editor Gradio interface. |
|
| 874 |
| `/mcp/` | MCP Streamable HTTP transport; use an MCP client rather than a normal REST request. |
|
| 875 |
-
| `/docs` | Root FastAPI Swagger UI. |
|
| 876 |
-
| `/redoc` | Root FastAPI ReDoc UI. |
|
| 877 |
-
| `/openapi.json` | Root renderer OpenAPI schema. |
|
| 878 |
-
| `/<service>/docs` | Mounted service Swagger UI when
|
| 879 |
-
| `/<service>/openapi.json` | Mounted service OpenAPI schema. |
|
| 880 |
-
|
|
|
|
| 91 |
For payloads with `callback_url`, use an n8n Webhook node's production URL to
|
| 92 |
avoid polling.
|
| 93 |
|
| 94 |
+
Render callbacks use `render.completed`, `render.failed`, and
|
| 95 |
+
`render.cancelled`; toolkit and analysis callbacks use `task.*`. The event is
|
| 96 |
+
available in both the JSON `event` property and `X-Ava2lon-Event` header.
|
| 97 |
+
|
| 98 |
## Root and Discovery
|
| 99 |
|
| 100 |
| Method | Path | n8n input | Response |
|
|
|
|
| 109 |
| Method | Path | n8n input | Response |
|
| 110 |
| --- | --- | --- | --- |
|
| 111 |
| GET | `/health` | None | Renderer health. |
|
| 112 |
+
| GET | `/healthz` | None | Health-check alias for hosting probes. |
|
| 113 |
| GET | `/monitor` | None | Runtime, queue, storage, and capability summary. |
|
| 114 |
| GET | `/queue` | None | Job queue summary. |
|
| 115 |
| GET | `/workers` | None | Worker and runtime settings. |
|
|
|
|
| 195 |
| Method | Path | n8n input | Auth | Response |
|
| 196 |
| --- | --- | --- | --- | --- |
|
| 197 |
| POST | `/render` | JSON `RenderPayload` or `AIReelsPayload` | None | Async job. |
|
| 198 |
+
| POST | `/render/variants` | JSON `RenderVariantsPayload` | None | Render group plus child jobs. |
|
| 199 |
+
| POST | `/render/template` | JSON `TemplateRenderPayload` | None | Async job. |
|
| 200 |
| POST | `/render/ai-reels` | JSON `AIReelsPayload` | None | Async job. |
|
| 201 |
| POST | `/render/batch` | JSON `BatchPayload` | None | Array of job IDs. |
|
| 202 |
| POST | `/automation/batch` | JSON `BatchPayload` | API key | Array of job IDs. |
|
|
|
|
| 205 |
| POST | `/render/ai-reels/upload` | Multipart `request_json` plus zero or more `files` | None | Async job. |
|
| 206 |
| POST | `/assets/upload` | Multipart; one or more `files` | None | Uploaded asset references. |
|
| 207 |
| POST | `/upload` | Multipart `files`; optional `expand_zip` | None | Uploaded or expanded assets. |
|
| 208 |
+
| GET | `/render/groups/{group_id}` | Replace `{group_id}` | None | Aggregate state and child jobs. |
|
| 209 |
+
| GET | `/templates/automation` | None | None | Data-driven render templates. |
|
| 210 |
|
| 211 |
Basic render body:
|
| 212 |
|
|
|
|
| 253 |
|
| 254 |
The filename in `upload://clip.mp4` must match the uploaded binary filename.
|
| 255 |
|
| 256 |
+
Render several platform outputs from one timeline with `/render/variants`:
|
| 257 |
+
|
| 258 |
+
```json
|
| 259 |
+
{
|
| 260 |
+
"scenes": [{"start": 0, "duration": 12, "media": "https://example.com/input.mp4"}],
|
| 261 |
+
"callback_url": "https://n8n.example/webhook/render-events",
|
| 262 |
+
"variants": [
|
| 263 |
+
{"name": "shorts", "platform": "youtube_shorts", "output_name": "shorts.mp4"},
|
| 264 |
+
{"name": "reels", "platform": "instagram_reels", "output_name": "reels.mp4"},
|
| 265 |
+
{"name": "youtube", "platform": "youtube_1080p", "output_name": "youtube.mp4"}
|
| 266 |
+
]
|
| 267 |
+
}
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
Use `/render/template` for spreadsheet, CRM, and database-driven renders. An
|
| 271 |
+
exact placeholder preserves arrays, objects, numbers, and booleans instead of
|
| 272 |
+
converting them to strings.
|
| 273 |
+
|
| 274 |
+
```json
|
| 275 |
+
{
|
| 276 |
+
"template": "vertical-captioned-clip",
|
| 277 |
+
"variables": {
|
| 278 |
+
"output_name": "customer-42.mp4",
|
| 279 |
+
"scenes": [{"start": 0, "duration": 8, "media": "https://example.com/42.mp4"}],
|
| 280 |
+
"voiceover": null,
|
| 281 |
+
"background_music": null
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
## Platform Processing and Toolkit
|
| 287 |
|
| 288 |
All routes in this table require `X-API-Key` and return asynchronous job
|
|
|
|
| 314 |
}
|
| 315 |
```
|
| 316 |
|
| 317 |
+
FFmpeg automation tasks added for n8n:
|
| 318 |
+
|
| 319 |
+
| Task | Important `params` | Download artifact |
|
| 320 |
+
| --- | --- | --- |
|
| 321 |
+
| `inspect` | None | FFprobe metadata JSON |
|
| 322 |
+
| `loudness_analyze` | `target_lufs` | LUFS/LRA/true-peak JSON |
|
| 323 |
+
| `silence_detect` | `noise_db`, `duration_seconds` | Timed silence events JSON |
|
| 324 |
+
| `black_detect` | `pixel_threshold`, `duration_seconds` | Timed black-frame events JSON |
|
| 325 |
+
| `scene_detect` | `threshold` | Scene-change timestamps JSON |
|
| 326 |
+
| `contact_sheet` | `columns`, `rows`, `interval_seconds`, `thumbnail_width` | JPEG/PNG |
|
| 327 |
+
| `hls` | `segment_seconds`, `playlist_type`, `crf` | ZIP with M3U8 and TS segments |
|
| 328 |
+
|
| 329 |
+
All use the normal `POST /toolkit` body with `task`, `media`, optional `params`,
|
| 330 |
+
and optional `callback_url`.
|
| 331 |
+
|
| 332 |
## Transcription, Subtitles, Jobs, and Downloads
|
| 333 |
|
| 334 |
| Method | Path | n8n input | Response |
|
|
|
|
| 337 |
| POST | `/subtitles` | JSON `SubtitlePayload` | File response (`.srt` or `.ass`). |
|
| 338 |
| POST | `/scene-builder` | JSON `SceneBuildPayload` | Generated scene array. |
|
| 339 |
| POST | `/transcribe/upload` | Multipart `file` plus optional transcription fields | Transcription JSON. |
|
| 340 |
+
| GET | `/status` | Optional query `job_id` | Service status without `job_id`; job record with it. |
|
| 341 |
| GET | `/status/{job_id}` | Replace `{job_id}` | Job record. |
|
| 342 |
| POST | `/cancel/{job_id}` | Replace `{job_id}` | Updated job record. |
|
| 343 |
| POST | `/admin/cleanup` | Optional query `older_than_seconds` | Cleanup counts. |
|
| 344 |
| GET | `/download` | Query `job_id`; optional `token` | File response. |
|
| 345 |
| GET | `/download/{job_id}` | Replace `{job_id}`; optional query `token` | File response. |
|
| 346 |
| POST | `/inspect` | Query `path` | Media metadata JSON. |
|
| 347 |
+
| POST | `/media/inspect` | JSON `{ "media": "...", "callback_url": "..." }` | Async remote-safe inspection. |
|
| 348 |
|
| 349 |
Transcription body:
|
| 350 |
|
|
|
|
| 447 |
batch
|
| 448 |
```
|
| 449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
## FFmpeg Editor
|
| 451 |
|
| 452 |
| Method | Path | n8n input | Response |
|
|
|
|
| 626 |
music_fade_in, music_fade_out, music_start: number = 0
|
| 627 |
music_loop, music_ducking: boolean = true
|
| 628 |
voice_volume: number = 1
|
| 629 |
+
callback_url, export_target: string | null
|
| 630 |
```
|
| 631 |
|
| 632 |
`BatchPayload`
|
|
|
|
| 854 |
creative_style: string | null
|
| 855 |
```
|
| 856 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 857 |
## Framework and UI Endpoints
|
| 858 |
|
| 859 |
These are not business API operations:
|
|
|
|
| 866 |
| `/whisper/` | Whisper Gradio interface. |
|
| 867 |
| `/editor/` | FFmpeg editor Gradio interface. |
|
| 868 |
| `/mcp/` | MCP Streamable HTTP transport; use an MCP client rather than a normal REST request. |
|
| 869 |
+
| `/docs` | Root FastAPI Swagger UI when `ENABLE_API_DOCS=true`. |
|
| 870 |
+
| `/redoc` | Root FastAPI ReDoc UI when `ENABLE_API_DOCS=true`. |
|
| 871 |
+
| `/openapi.json` | Root renderer OpenAPI schema when `ENABLE_API_DOCS=true`. |
|
| 872 |
+
| `/<service>/docs` | Mounted service Swagger UI when `ENABLE_API_DOCS=true`. |
|
| 873 |
+
| `/<service>/openapi.json` | Mounted service OpenAPI schema when `ENABLE_API_DOCS=true`. |
|
|
|
README.md
CHANGED
|
@@ -31,6 +31,10 @@ See [N8N_ENDPOINTS.md](N8N_ENDPOINTS.md) for all API endpoints, payload
|
|
| 31 |
schemas, editor task IDs, file upload settings, asynchronous job handling, and
|
| 32 |
HTTP Request node examples.
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
## MCP
|
| 35 |
|
| 36 |
Connect an MCP client to:
|
|
|
|
| 31 |
schemas, editor task IDs, file upload settings, asynchronous job handling, and
|
| 32 |
HTTP Request node examples.
|
| 33 |
|
| 34 |
+
Public Swagger, ReDoc, and OpenAPI routes are disabled by default. Set
|
| 35 |
+
`ENABLE_API_DOCS=true` in the Space environment when interactive API
|
| 36 |
+
documentation is required.
|
| 37 |
+
|
| 38 |
## MCP
|
| 39 |
|
| 40 |
Connect an MCP client to:
|
README.renderer.md
CHANGED
|
@@ -261,6 +261,14 @@ Deletes expired job records, old exports, and staged upload folders. Default ret
|
|
| 261 |
|
| 262 |
Probe one local asset path and return MIME type, duration, codecs, bitrate, resolution, FPS, stream list, and cache metadata.
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
## n8n Workflow Integration
|
| 265 |
|
| 266 |
Recommended URL-based flow:
|
|
@@ -268,7 +276,7 @@ Recommended URL-based flow:
|
|
| 268 |
1. Use an HTTP Request node with `POST https://YOUR-SPACE.hf.space/render`.
|
| 269 |
2. Pass public file URLs from S3, Supabase Storage, Cloudinary, Google Drive direct-download links, or another CDN in `scenes[].media`, `voiceover`, and `background_music`.
|
| 270 |
3. Set `auto_subtitles` to `true` when you want faster-whisper captions generated from `voiceover`.
|
| 271 |
-
4. Add `callback_url` to receive
|
| 272 |
5. Poll `GET https://YOUR-SPACE.hf.space/status/{{$json.job_id}}` only if you do not use callbacks.
|
| 273 |
6. Download the final MP4 from the signed `download_url`.
|
| 274 |
|
|
@@ -323,12 +331,14 @@ Templates are selected per request and do not require code changes.
|
|
| 323 |
- Remote asset downloads are killed after `DOWNLOAD_TIMEOUT_SECONDS`.
|
| 324 |
- Remote and multipart input size is capped by `MAX_DOWNLOAD_BYTES`.
|
| 325 |
- faster-whisper models are loaded lazily and cached under `WHISPER_MODEL_DIR`.
|
| 326 |
-
- Webhook callbacks
|
|
|
|
|
|
|
| 327 |
- `export_target=local` copies completed renders to `STORAGE_DIR`.
|
| 328 |
- `export_target=https://...` uploads the completed MP4 with HTTP `PUT`, which works with presigned URLs from S3, Supabase Storage, and similar providers.
|
| 329 |
- Jobs retry up to `MAX_RETRIES`.
|
| 330 |
- Intermediate files are created under `TEMP_DIR` and removed after export.
|
| 331 |
-
- Job records retain command history, logs, render time, output size, and failure reasons.
|
| 332 |
|
| 333 |
## Docker Deployment
|
| 334 |
|
|
|
|
| 261 |
|
| 262 |
Probe one local asset path and return MIME type, duration, codecs, bitrate, resolution, FPS, stream list, and cache metadata.
|
| 263 |
|
| 264 |
+
### n8n automation endpoints
|
| 265 |
+
|
| 266 |
+
- `POST /media/inspect` safely downloads a local or remote asset and returns FFprobe metadata as an asynchronous JSON artifact.
|
| 267 |
+
- `POST /render/template` substitutes validated JSON variables into a named or inline render template.
|
| 268 |
+
- `POST /render/variants` creates up to 12 platform outputs and returns a render group ID.
|
| 269 |
+
- `GET /render/groups/{group_id}` returns aggregate and per-variant status.
|
| 270 |
+
- `POST /toolkit` supports `loudness_analyze`, `silence_detect`, `black_detect`, `scene_detect`, `contact_sheet`, and `hls` tasks.
|
| 271 |
+
|
| 272 |
## n8n Workflow Integration
|
| 273 |
|
| 274 |
Recommended URL-based flow:
|
|
|
|
| 276 |
1. Use an HTTP Request node with `POST https://YOUR-SPACE.hf.space/render`.
|
| 277 |
2. Pass public file URLs from S3, Supabase Storage, Cloudinary, Google Drive direct-download links, or another CDN in `scenes[].media`, `voiceover`, and `background_music`.
|
| 278 |
3. Set `auto_subtitles` to `true` when you want faster-whisper captions generated from `voiceover`.
|
| 279 |
+
4. Add `callback_url` to receive `render.completed`, `render.failed`, or `render.cancelled` events. Toolkit and analysis jobs use the corresponding `task.*` event names.
|
| 280 |
5. Poll `GET https://YOUR-SPACE.hf.space/status/{{$json.job_id}}` only if you do not use callbacks.
|
| 281 |
6. Download the final MP4 from the signed `download_url`.
|
| 282 |
|
|
|
|
| 331 |
- Remote asset downloads are killed after `DOWNLOAD_TIMEOUT_SECONDS`.
|
| 332 |
- Remote and multipart input size is capped by `MAX_DOWNLOAD_BYTES`.
|
| 333 |
- faster-whisper models are loaded lazily and cached under `WHISPER_MODEL_DIR`.
|
| 334 |
+
- Webhook callbacks retry with exponential backoff. `CALLBACK_MAX_RETRIES` and `CALLBACK_RETRY_SECONDS` control delivery.
|
| 335 |
+
- Webhooks include `X-Ava2lon-Event` and an HMAC `X-Ava2lon-Signature` computed with the signing secret.
|
| 336 |
+
- Set `PUBLIC_BASE_URL` so submissions, status payloads, and callbacks contain absolute URLs.
|
| 337 |
- `export_target=local` copies completed renders to `STORAGE_DIR`.
|
| 338 |
- `export_target=https://...` uploads the completed MP4 with HTTP `PUT`, which works with presigned URLs from S3, Supabase Storage, and similar providers.
|
| 339 |
- Jobs retry up to `MAX_RETRIES`.
|
| 340 |
- Intermediate files are created under `TEMP_DIR` and removed after export.
|
| 341 |
+
- Job records retain command history, logs, render time, output size, SHA-256 artifact checksums, callback delivery state, and failure reasons.
|
| 342 |
|
| 343 |
## Docker Deployment
|
| 344 |
|
access_log_filter.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
SCANNER_PATH_PREFIXES = (
|
| 8 |
+
"/.env",
|
| 9 |
+
"/.git",
|
| 10 |
+
"/.streamlit",
|
| 11 |
+
"/_debug",
|
| 12 |
+
"/_profiler",
|
| 13 |
+
"/_stcore",
|
| 14 |
+
"/actuator",
|
| 15 |
+
"/admin",
|
| 16 |
+
"/api/config",
|
| 17 |
+
"/api/credentials",
|
| 18 |
+
"/api/env",
|
| 19 |
+
"/api/keys",
|
| 20 |
+
"/api/predict",
|
| 21 |
+
"/api/queue/status",
|
| 22 |
+
"/api/secrets",
|
| 23 |
+
"/api/settings",
|
| 24 |
+
"/api/v1/",
|
| 25 |
+
"/api-docs",
|
| 26 |
+
"/app.py",
|
| 27 |
+
"/backup/",
|
| 28 |
+
"/config",
|
| 29 |
+
"/debug",
|
| 30 |
+
"/docs",
|
| 31 |
+
"/elmah",
|
| 32 |
+
"/file",
|
| 33 |
+
"/graphql",
|
| 34 |
+
"/horizon",
|
| 35 |
+
"/info",
|
| 36 |
+
"/internal",
|
| 37 |
+
"/main.py",
|
| 38 |
+
"/manifest.json",
|
| 39 |
+
"/metrics",
|
| 40 |
+
"/openapi.json",
|
| 41 |
+
"/phpinfo",
|
| 42 |
+
"/proc/",
|
| 43 |
+
"/prometheus",
|
| 44 |
+
"/redoc",
|
| 45 |
+
"/run/predict",
|
| 46 |
+
"/server-",
|
| 47 |
+
"/settings",
|
| 48 |
+
"/swagger",
|
| 49 |
+
"/telescope",
|
| 50 |
+
"/trace",
|
| 51 |
+
"/upload?upload_id=",
|
| 52 |
+
"/wp-config",
|
| 53 |
+
"/__phpinfo",
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class ScannerNoiseFilter(logging.Filter):
|
| 58 |
+
def filter(self, record: logging.LogRecord) -> bool:
|
| 59 |
+
try:
|
| 60 |
+
path = str(record.args[2]).lower()
|
| 61 |
+
status_code = int(record.args[4])
|
| 62 |
+
except (IndexError, TypeError, ValueError):
|
| 63 |
+
return True
|
| 64 |
+
|
| 65 |
+
if status_code not in {404, 405}:
|
| 66 |
+
return True
|
| 67 |
+
return not path.startswith(SCANNER_PATH_PREFIXES)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def install_access_log_filter() -> None:
|
| 71 |
+
enabled = os.getenv("SUPPRESS_SCANNER_ACCESS_LOGS", "true").lower() in {
|
| 72 |
+
"1",
|
| 73 |
+
"true",
|
| 74 |
+
"yes",
|
| 75 |
+
}
|
| 76 |
+
if not enabled:
|
| 77 |
+
return
|
| 78 |
+
|
| 79 |
+
logger = logging.getLogger("uvicorn.access")
|
| 80 |
+
if not any(isinstance(item, ScannerNoiseFilter) for item in logger.filters):
|
| 81 |
+
logger.addFilter(ScannerNoiseFilter())
|
api.py
CHANGED
|
@@ -13,9 +13,9 @@ from pydantic import BaseModel, Field
|
|
| 13 |
|
| 14 |
from renderer.core.config import Settings
|
| 15 |
from renderer.core.ingest import stage_upload
|
| 16 |
-
from renderer.core.models import AIReelsRequest, RenderRequest, Scene
|
| 17 |
from renderer.core.security import verify_download_token
|
| 18 |
-
from renderer.core.utils import safe_filename
|
| 19 |
from renderer.jobs import JobManager
|
| 20 |
from renderer.platform import PlatformProcessor, supported_toolkit_tasks
|
| 21 |
from renderer.scenes import Timeline
|
|
@@ -44,6 +44,7 @@ from renderer.templates import (
|
|
| 44 |
scene_effect_metadata,
|
| 45 |
)
|
| 46 |
from renderer.transitions import TransitionBuilder
|
|
|
|
| 47 |
|
| 48 |
settings = Settings()
|
| 49 |
settings.ensure_dirs()
|
|
@@ -60,7 +61,7 @@ def _verify_api_key(api_key: str | None = Header(default=None, alias=API_KEY_HEA
|
|
| 60 |
raise HTTPException(status_code=401, detail="Invalid or missing API key")
|
| 61 |
|
| 62 |
def _submission_response(job_id: str) -> dict[str, str]:
|
| 63 |
-
download_url = f"/download/{job_id}"
|
| 64 |
try:
|
| 65 |
record = job_manager.get(job_id)
|
| 66 |
except KeyError:
|
|
@@ -69,7 +70,21 @@ def _submission_response(job_id: str) -> dict[str, str]:
|
|
| 69 |
token = record.download_token
|
| 70 |
if token:
|
| 71 |
download_url = f"{download_url}?token={token}"
|
| 72 |
-
return {"job_id": job_id, "status_url": f"/status/{job_id}", "download_url": download_url}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
|
| 75 |
class ScenePayload(BaseModel):
|
|
@@ -117,6 +132,35 @@ class RenderPayload(BaseModel):
|
|
| 117 |
metadata: dict[str, Any] = Field(default_factory=dict)
|
| 118 |
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
class AIReelsPayload(BaseModel):
|
| 121 |
script: str
|
| 122 |
voiceover: str
|
|
@@ -133,6 +177,8 @@ class AIReelsPayload(BaseModel):
|
|
| 133 |
music_start: float = Field(default=0.0, ge=0)
|
| 134 |
music_ducking: bool = True
|
| 135 |
voice_volume: float = Field(default=1.0, ge=0, le=2)
|
|
|
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
class BatchPayload(BaseModel):
|
|
@@ -361,6 +407,11 @@ def health() -> dict[str, str]:
|
|
| 361 |
return {"status": "ok"}
|
| 362 |
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
@api.get("/monitor")
|
| 365 |
def monitor() -> dict[str, Any]:
|
| 366 |
return _monitor_payload()
|
|
@@ -444,9 +495,15 @@ def templates_catalog() -> dict[str, Any]:
|
|
| 444 |
"caption_templates": list_templates(),
|
| 445 |
"render_presets": list_presets(),
|
| 446 |
"creative_styles": creative_style_metadata(),
|
|
|
|
| 447 |
}
|
| 448 |
|
| 449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
@api.get("/projects")
|
| 451 |
def list_projects() -> dict[str, list[dict[str, Any]]]:
|
| 452 |
return {"projects": project_manager.list()}
|
|
@@ -719,6 +776,38 @@ def render(payload: RenderPayload | AIReelsPayload) -> dict[str, str]:
|
|
| 719 |
return _submission_response(job_id)
|
| 720 |
|
| 721 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
|
| 723 |
@api.post("/render/ai-reels")
|
| 724 |
def render_ai_reels(payload: AIReelsPayload) -> dict[str, str]:
|
|
@@ -881,6 +970,16 @@ def toolkit(payload: ToolkitPayload) -> dict[str, str]:
|
|
| 881 |
return _submission_response(job_id)
|
| 882 |
|
| 883 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
@api.post("/edit", dependencies=[Depends(_verify_api_key)])
|
| 885 |
def edit(payload: ToolkitPayload) -> dict[str, str]:
|
| 886 |
return toolkit(payload)
|
|
@@ -976,14 +1075,20 @@ async def transcribe_upload(
|
|
| 976 |
|
| 977 |
|
| 978 |
@api.get("/status")
|
| 979 |
-
def status_query(job_id: str = Query(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
return status(job_id)
|
| 981 |
|
| 982 |
|
| 983 |
@api.get("/status/{job_id}")
|
| 984 |
def status(job_id: str) -> dict:
|
| 985 |
try:
|
| 986 |
-
|
|
|
|
| 987 |
except KeyError as exc:
|
| 988 |
raise HTTPException(status_code=404, detail="Job not found") from exc
|
| 989 |
|
|
@@ -991,7 +1096,7 @@ def status(job_id: str) -> dict:
|
|
| 991 |
@api.post("/cancel/{job_id}")
|
| 992 |
def cancel(job_id: str) -> dict:
|
| 993 |
try:
|
| 994 |
-
return job_manager.cancel(job_id)
|
| 995 |
except KeyError as exc:
|
| 996 |
raise HTTPException(status_code=404, detail="Job not found") from exc
|
| 997 |
|
|
@@ -1030,6 +1135,26 @@ def inspect_asset(path: str) -> dict:
|
|
| 1030 |
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 1031 |
|
| 1032 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1033 |
def _render_request(payload: RenderPayload) -> RenderRequest:
|
| 1034 |
payload = RenderPayload.model_validate(apply_creative_style(apply_preset(payload.model_dump(exclude_unset=True))))
|
| 1035 |
request = RenderRequest(
|
|
|
|
| 13 |
|
| 14 |
from renderer.core.config import Settings
|
| 15 |
from renderer.core.ingest import stage_upload
|
| 16 |
+
from renderer.core.models import AIReelsRequest, JobRecord, RenderRequest, Scene
|
| 17 |
from renderer.core.security import verify_download_token
|
| 18 |
+
from renderer.core.utils import new_id, safe_filename
|
| 19 |
from renderer.jobs import JobManager
|
| 20 |
from renderer.platform import PlatformProcessor, supported_toolkit_tasks
|
| 21 |
from renderer.scenes import Timeline
|
|
|
|
| 44 |
scene_effect_metadata,
|
| 45 |
)
|
| 46 |
from renderer.transitions import TransitionBuilder
|
| 47 |
+
from renderer.automation import automation_template_catalog, list_automation_templates, resolve_render_template
|
| 48 |
|
| 49 |
settings = Settings()
|
| 50 |
settings.ensure_dirs()
|
|
|
|
| 61 |
raise HTTPException(status_code=401, detail="Invalid or missing API key")
|
| 62 |
|
| 63 |
def _submission_response(job_id: str) -> dict[str, str]:
|
| 64 |
+
download_url = _public_endpoint(f"/download/{job_id}")
|
| 65 |
try:
|
| 66 |
record = job_manager.get(job_id)
|
| 67 |
except KeyError:
|
|
|
|
| 70 |
token = record.download_token
|
| 71 |
if token:
|
| 72 |
download_url = f"{download_url}?token={token}"
|
| 73 |
+
return {"job_id": job_id, "status_url": _public_endpoint(f"/status/{job_id}"), "download_url": download_url}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _public_endpoint(path: str) -> str:
|
| 77 |
+
return f"{settings.public_base_url}{path}" if settings.public_base_url else path
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _status_payload(record: JobRecord) -> dict[str, Any]:
|
| 81 |
+
data = record.__dict__.copy()
|
| 82 |
+
data["status_url"] = _public_endpoint(f"/status/{record.job_id}")
|
| 83 |
+
data["download_url"] = _public_endpoint(f"/download/{record.job_id}?token={record.download_token}")
|
| 84 |
+
artifact = record.metrics.get("artifact") if isinstance(record.metrics, dict) else None
|
| 85 |
+
if artifact:
|
| 86 |
+
data["artifact"] = artifact
|
| 87 |
+
return data
|
| 88 |
|
| 89 |
|
| 90 |
class ScenePayload(BaseModel):
|
|
|
|
| 132 |
metadata: dict[str, Any] = Field(default_factory=dict)
|
| 133 |
|
| 134 |
|
| 135 |
+
class RenderVariantPayload(BaseModel):
|
| 136 |
+
name: str = "variant"
|
| 137 |
+
preset: str | None = None
|
| 138 |
+
template: str | None = None
|
| 139 |
+
creative_style: str | None = None
|
| 140 |
+
platform: str | None = None
|
| 141 |
+
output_name: str | None = None
|
| 142 |
+
preview: bool | None = None
|
| 143 |
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class RenderVariantsPayload(RenderPayload):
|
| 147 |
+
variants: list[RenderVariantPayload] = Field(min_length=1, max_length=12)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class TemplateRenderPayload(BaseModel):
|
| 151 |
+
template: str | dict[str, Any]
|
| 152 |
+
variables: dict[str, Any] = Field(default_factory=dict)
|
| 153 |
+
overrides: dict[str, Any] = Field(default_factory=dict)
|
| 154 |
+
callback_url: str | None = None
|
| 155 |
+
export_target: str | None = None
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class MediaInspectPayload(BaseModel):
|
| 159 |
+
media: str
|
| 160 |
+
callback_url: str | None = None
|
| 161 |
+
export_target: str | None = None
|
| 162 |
+
|
| 163 |
+
|
| 164 |
class AIReelsPayload(BaseModel):
|
| 165 |
script: str
|
| 166 |
voiceover: str
|
|
|
|
| 177 |
music_start: float = Field(default=0.0, ge=0)
|
| 178 |
music_ducking: bool = True
|
| 179 |
voice_volume: float = Field(default=1.0, ge=0, le=2)
|
| 180 |
+
callback_url: str | None = None
|
| 181 |
+
export_target: str | None = None
|
| 182 |
|
| 183 |
|
| 184 |
class BatchPayload(BaseModel):
|
|
|
|
| 407 |
return {"status": "ok"}
|
| 408 |
|
| 409 |
|
| 410 |
+
@api.get("/healthz", include_in_schema=False)
|
| 411 |
+
def healthz() -> dict[str, str]:
|
| 412 |
+
return health()
|
| 413 |
+
|
| 414 |
+
|
| 415 |
@api.get("/monitor")
|
| 416 |
def monitor() -> dict[str, Any]:
|
| 417 |
return _monitor_payload()
|
|
|
|
| 495 |
"caption_templates": list_templates(),
|
| 496 |
"render_presets": list_presets(),
|
| 497 |
"creative_styles": creative_style_metadata(),
|
| 498 |
+
"automation_templates": automation_template_catalog(),
|
| 499 |
}
|
| 500 |
|
| 501 |
|
| 502 |
+
@api.get("/templates/automation")
|
| 503 |
+
def automation_templates() -> dict[str, Any]:
|
| 504 |
+
return {"templates": list_automation_templates(), "catalog": automation_template_catalog()}
|
| 505 |
+
|
| 506 |
+
|
| 507 |
@api.get("/projects")
|
| 508 |
def list_projects() -> dict[str, list[dict[str, Any]]]:
|
| 509 |
return {"projects": project_manager.list()}
|
|
|
|
| 776 |
return _submission_response(job_id)
|
| 777 |
|
| 778 |
|
| 779 |
+
@api.post("/render/variants")
|
| 780 |
+
def render_variants(payload: RenderVariantsPayload) -> dict[str, Any]:
|
| 781 |
+
group_id = new_id("render_group")
|
| 782 |
+
jobs: list[dict[str, str]] = []
|
| 783 |
+
common = payload.model_dump(exclude={"variants"})
|
| 784 |
+
for variant in payload.variants:
|
| 785 |
+
data = dict(common)
|
| 786 |
+
selected = variant.model_dump(exclude_none=True)
|
| 787 |
+
variant_name = selected.pop("name", "variant")
|
| 788 |
+
variant_metadata = dict(data.get("metadata") or {})
|
| 789 |
+
variant_metadata.update(selected.pop("metadata", {}) or {})
|
| 790 |
+
variant_metadata.update({"group_id": group_id, "variant_name": variant_name})
|
| 791 |
+
data.update(selected)
|
| 792 |
+
data["metadata"] = variant_metadata
|
| 793 |
+
request = _render_request(RenderPayload.model_validate(data))
|
| 794 |
+
job_id = job_manager.submit_render(request)
|
| 795 |
+
jobs.append({"variant": variant_name, **_submission_response(job_id)})
|
| 796 |
+
return {"group_id": group_id, "group_status_url": _public_endpoint(f"/render/groups/{group_id}"), "jobs": jobs}
|
| 797 |
+
|
| 798 |
+
|
| 799 |
+
@api.post("/render/template")
|
| 800 |
+
def render_template(payload: TemplateRenderPayload) -> dict[str, str]:
|
| 801 |
+
try:
|
| 802 |
+
data = resolve_render_template(payload.template, payload.variables, payload.overrides)
|
| 803 |
+
data["callback_url"] = payload.callback_url or data.get("callback_url")
|
| 804 |
+
data["export_target"] = payload.export_target or data.get("export_target")
|
| 805 |
+
request = _render_request(RenderPayload.model_validate(data))
|
| 806 |
+
return _submission_response(job_manager.submit_render(request))
|
| 807 |
+
except (KeyError, TypeError, ValueError) as exc:
|
| 808 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 809 |
+
|
| 810 |
+
|
| 811 |
|
| 812 |
@api.post("/render/ai-reels")
|
| 813 |
def render_ai_reels(payload: AIReelsPayload) -> dict[str, str]:
|
|
|
|
| 970 |
return _submission_response(job_id)
|
| 971 |
|
| 972 |
|
| 973 |
+
@api.post("/media/inspect")
|
| 974 |
+
def media_inspect(payload: MediaInspectPayload) -> dict[str, str]:
|
| 975 |
+
job_id = job_manager.submit_task(
|
| 976 |
+
lambda task_id, log: PlatformProcessor(settings, log=log).inspect_media(payload.media, task_id),
|
| 977 |
+
callback_url=payload.callback_url,
|
| 978 |
+
export_target=payload.export_target,
|
| 979 |
+
)
|
| 980 |
+
return _submission_response(job_id)
|
| 981 |
+
|
| 982 |
+
|
| 983 |
@api.post("/edit", dependencies=[Depends(_verify_api_key)])
|
| 984 |
def edit(payload: ToolkitPayload) -> dict[str, str]:
|
| 985 |
return toolkit(payload)
|
|
|
|
| 1075 |
|
| 1076 |
|
| 1077 |
@api.get("/status")
|
| 1078 |
+
def status_query(job_id: str | None = Query(default=None)) -> dict:
|
| 1079 |
+
if job_id is None:
|
| 1080 |
+
return {
|
| 1081 |
+
"status": "ok",
|
| 1082 |
+
"queue": job_manager.summary(),
|
| 1083 |
+
}
|
| 1084 |
return status(job_id)
|
| 1085 |
|
| 1086 |
|
| 1087 |
@api.get("/status/{job_id}")
|
| 1088 |
def status(job_id: str) -> dict:
|
| 1089 |
try:
|
| 1090 |
+
record = job_manager.get(job_id)
|
| 1091 |
+
return _status_payload(record)
|
| 1092 |
except KeyError as exc:
|
| 1093 |
raise HTTPException(status_code=404, detail="Job not found") from exc
|
| 1094 |
|
|
|
|
| 1096 |
@api.post("/cancel/{job_id}")
|
| 1097 |
def cancel(job_id: str) -> dict:
|
| 1098 |
try:
|
| 1099 |
+
return _status_payload(job_manager.cancel(job_id))
|
| 1100 |
except KeyError as exc:
|
| 1101 |
raise HTTPException(status_code=404, detail="Job not found") from exc
|
| 1102 |
|
|
|
|
| 1135 |
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 1136 |
|
| 1137 |
|
| 1138 |
+
@api.get("/render/groups/{group_id}")
|
| 1139 |
+
def render_group_status(group_id: str) -> dict[str, Any]:
|
| 1140 |
+
records = job_manager.group(group_id)
|
| 1141 |
+
if not records:
|
| 1142 |
+
raise HTTPException(status_code=404, detail="Render group not found")
|
| 1143 |
+
jobs = [_status_payload(record) for record in records]
|
| 1144 |
+
states = [record.state for record in records]
|
| 1145 |
+
if any(state in {"PENDING", "RUNNING", "CANCEL_REQUESTED"} for state in states):
|
| 1146 |
+
group_state = "RUNNING"
|
| 1147 |
+
elif all(state == "COMPLETED" for state in states):
|
| 1148 |
+
group_state = "COMPLETED"
|
| 1149 |
+
elif any(state == "FAILED" for state in states):
|
| 1150 |
+
group_state = "FAILED"
|
| 1151 |
+
elif any(state == "CANCELLED" for state in states):
|
| 1152 |
+
group_state = "CANCELLED"
|
| 1153 |
+
else:
|
| 1154 |
+
group_state = "RUNNING"
|
| 1155 |
+
return {"group_id": group_id, "state": group_state, "total": len(jobs), "completed": states.count("COMPLETED"), "jobs": jobs}
|
| 1156 |
+
|
| 1157 |
+
|
| 1158 |
def _render_request(payload: RenderPayload) -> RenderRequest:
|
| 1159 |
payload = RenderPayload.model_validate(apply_creative_style(apply_preset(payload.model_dump(exclude_unset=True))))
|
| 1160 |
request = RenderRequest(
|
app.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
|
|
| 1 |
from contextlib import AsyncExitStack, asynccontextmanager
|
| 2 |
|
| 3 |
-
from fastapi
|
|
|
|
| 4 |
|
|
|
|
| 5 |
from editor_app import app as editor_app
|
| 6 |
from ktts_app import app as tts_app
|
| 7 |
from musicgen_app import app as music_app
|
|
@@ -10,6 +13,30 @@ from renderer_app import app
|
|
| 10 |
from whisper_app import app as whisper_app
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
CHILD_APPS = (
|
| 14 |
("/tts", tts_app, "tts"),
|
| 15 |
("/music", music_app, "music"),
|
|
|
|
| 1 |
+
import os
|
| 2 |
from contextlib import AsyncExitStack, asynccontextmanager
|
| 3 |
|
| 4 |
+
from fastapi import Request
|
| 5 |
+
from fastapi.responses import RedirectResponse, Response
|
| 6 |
|
| 7 |
+
from access_log_filter import install_access_log_filter
|
| 8 |
from editor_app import app as editor_app
|
| 9 |
from ktts_app import app as tts_app
|
| 10 |
from musicgen_app import app as music_app
|
|
|
|
| 13 |
from whisper_app import app as whisper_app
|
| 14 |
|
| 15 |
|
| 16 |
+
install_access_log_filter()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _is_api_docs_path(path: str) -> bool:
|
| 20 |
+
return (
|
| 21 |
+
path.endswith("/openapi.json")
|
| 22 |
+
or path.endswith("/docs")
|
| 23 |
+
or "/docs/" in path
|
| 24 |
+
or path.endswith("/redoc")
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@app.middleware("http")
|
| 29 |
+
async def protect_api_docs(request: Request, call_next):
|
| 30 |
+
docs_enabled = os.getenv("ENABLE_API_DOCS", "false").lower() in {
|
| 31 |
+
"1",
|
| 32 |
+
"true",
|
| 33 |
+
"yes",
|
| 34 |
+
}
|
| 35 |
+
if not docs_enabled and _is_api_docs_path(request.url.path):
|
| 36 |
+
return Response(status_code=404)
|
| 37 |
+
return await call_next(request)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
CHILD_APPS = (
|
| 41 |
("/tts", tts_app, "tts"),
|
| 42 |
("/music", music_app, "music"),
|
musicgen_app.py
CHANGED
|
@@ -33,8 +33,7 @@ def ui_generate(prompt, duration):
|
|
| 33 |
|
| 34 |
|
| 35 |
with gr.Blocks(
|
| 36 |
-
title="AI Background Music Generator"
|
| 37 |
-
theme=gr.themes.Soft()
|
| 38 |
) as demo:
|
| 39 |
|
| 40 |
gr.Markdown("""
|
|
@@ -71,4 +70,9 @@ Generate royalty-free background music instantly.
|
|
| 71 |
# ======================
|
| 72 |
# Mount GRADIO as ROOT APP
|
| 73 |
|
| 74 |
-
app = gr.mount_gradio_app(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
with gr.Blocks(
|
| 36 |
+
title="AI Background Music Generator"
|
|
|
|
| 37 |
) as demo:
|
| 38 |
|
| 39 |
gr.Markdown("""
|
|
|
|
| 70 |
# ======================
|
| 71 |
# Mount GRADIO as ROOT APP
|
| 72 |
|
| 73 |
+
app = gr.mount_gradio_app(
|
| 74 |
+
api,
|
| 75 |
+
demo,
|
| 76 |
+
path="/",
|
| 77 |
+
theme=gr.themes.Soft(),
|
| 78 |
+
)
|
requirements.txt
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
|
| 3 |
accelerate
|
| 4 |
aiofiles>=23.2.1
|
| 5 |
-
email-validator
|
| 6 |
fastapi>=0.115.0
|
| 7 |
faster-whisper==1.0.3
|
| 8 |
ffmpeg-python>=0.2.0
|
|
@@ -11,7 +10,7 @@ google-auth>=2.29.0
|
|
| 11 |
google-auth-httplib2>=0.2.0
|
| 12 |
google-auth-oauthlib>=1.2.0
|
| 13 |
google-generativeai>=0.7.2
|
| 14 |
-
gradio>=
|
| 15 |
httplib2>=0.22.0
|
| 16 |
huggingface_hub<1.0
|
| 17 |
httpx>=0.27.0
|
|
@@ -21,21 +20,17 @@ moviepy==1.0.3
|
|
| 21 |
mcp>=1.28.1,<2
|
| 22 |
numpy>=1.26.0
|
| 23 |
onnxruntime
|
| 24 |
-
passlib[bcrypt]>=1.7.4
|
| 25 |
phonemizer
|
| 26 |
psutil>=5.9.8
|
| 27 |
-
psycopg2-binary>=2.9.9
|
| 28 |
pydantic>=2.7.0
|
| 29 |
pytest>=8.2.0
|
| 30 |
pytest-cov>=5.0.0
|
| 31 |
pysrt>=1.1.2
|
| 32 |
-
python-jose[cryptography]>=3.3.0
|
| 33 |
python-magic
|
| 34 |
python-multipart>=0.0.9
|
| 35 |
requests>=2.31.0
|
| 36 |
scipy
|
| 37 |
soundfile
|
| 38 |
-
sqlalchemy>=2.0.25
|
| 39 |
torch
|
| 40 |
torchaudio
|
| 41 |
torchvision
|
|
|
|
| 2 |
|
| 3 |
accelerate
|
| 4 |
aiofiles>=23.2.1
|
|
|
|
| 5 |
fastapi>=0.115.0
|
| 6 |
faster-whisper==1.0.3
|
| 7 |
ffmpeg-python>=0.2.0
|
|
|
|
| 10 |
google-auth-httplib2>=0.2.0
|
| 11 |
google-auth-oauthlib>=1.2.0
|
| 12 |
google-generativeai>=0.7.2
|
| 13 |
+
gradio>=6.0,<7
|
| 14 |
httplib2>=0.22.0
|
| 15 |
huggingface_hub<1.0
|
| 16 |
httpx>=0.27.0
|
|
|
|
| 20 |
mcp>=1.28.1,<2
|
| 21 |
numpy>=1.26.0
|
| 22 |
onnxruntime
|
|
|
|
| 23 |
phonemizer
|
| 24 |
psutil>=5.9.8
|
|
|
|
| 25 |
pydantic>=2.7.0
|
| 26 |
pytest>=8.2.0
|
| 27 |
pytest-cov>=5.0.0
|
| 28 |
pysrt>=1.1.2
|
|
|
|
| 29 |
python-magic
|
| 30 |
python-multipart>=0.0.9
|
| 31 |
requests>=2.31.0
|
| 32 |
scipy
|
| 33 |
soundfile
|
|
|
|
| 34 |
torch
|
| 35 |
torchaudio
|
| 36 |
torchvision
|
requirements.whisper.txt
CHANGED
|
@@ -50,22 +50,7 @@ google-auth-httplib2>=0.2.0
|
|
| 50 |
# =========================
|
| 51 |
google-generativeai>=0.7.2
|
| 52 |
|
| 53 |
-
# =========================
|
| 54 |
-
# AUTH SYSTEM (JWT + SECURITY)
|
| 55 |
-
# =========================
|
| 56 |
-
python-jose[cryptography]>=3.3.0
|
| 57 |
-
|
| 58 |
-
# FIXED BCRYPT STACK (CRITICAL STABILITY FIX)
|
| 59 |
-
passlib[bcrypt]>=1.7.4
|
| 60 |
-
email-validator
|
| 61 |
-
|
| 62 |
-
# =========================
|
| 63 |
-
# DATABASE (SQLALCHEMY + POSTGRES)
|
| 64 |
-
# =========================
|
| 65 |
-
sqlalchemy>=2.0.25
|
| 66 |
-
psycopg2-binary>=2.9.9
|
| 67 |
-
|
| 68 |
# =========================
|
| 69 |
# OPTIONAL STABILITY LAYER
|
| 70 |
# =========================
|
| 71 |
-
aiofiles>=23.2.1
|
|
|
|
| 50 |
# =========================
|
| 51 |
google-generativeai>=0.7.2
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
# =========================
|
| 54 |
# OPTIONAL STABILITY LAYER
|
| 55 |
# =========================
|
| 56 |
+
aiofiles>=23.2.1
|
whisper_app.py
CHANGED
|
@@ -14,17 +14,6 @@ from utils.logger import logger
|
|
| 14 |
from utils.job_queue import start_worker, create_job, get_job
|
| 15 |
from ingestion.resolver import resolve_input
|
| 16 |
|
| 17 |
-
# ==============================
|
| 18 |
-
# AUTH SYSTEM (HARDENED IMPORT)
|
| 19 |
-
# ==============================
|
| 20 |
-
try:
|
| 21 |
-
from auth.routes import router as auth_router
|
| 22 |
-
from auth.database import Base, engine
|
| 23 |
-
AUTH_ENABLED = True
|
| 24 |
-
except Exception as e:
|
| 25 |
-
logger.error(f"[AUTH BOOT FAILED] {e}")
|
| 26 |
-
AUTH_ENABLED = False
|
| 27 |
-
|
| 28 |
# ==============================
|
| 29 |
# CORE PIPELINE
|
| 30 |
# ==============================
|
|
@@ -50,14 +39,6 @@ from publisher.bulk import execute as bulk_execute
|
|
| 50 |
from publisher.metadata_engine import generate_metadata
|
| 51 |
from publisher.thumbnail_engine import generate_thumbnail
|
| 52 |
|
| 53 |
-
# ==============================
|
| 54 |
-
# SAFE bcrypt SHIELD (DO NOT FAIL BOOT)
|
| 55 |
-
# ==============================
|
| 56 |
-
try:
|
| 57 |
-
import bcrypt
|
| 58 |
-
except Exception as e:
|
| 59 |
-
logger.warning(f"[bcrypt warning ignored] {e}")
|
| 60 |
-
|
| 61 |
# ==============================
|
| 62 |
# INIT
|
| 63 |
# ==============================
|
|
@@ -66,21 +47,6 @@ UPLOAD_DIR = str(BASE_DIR / "jobs")
|
|
| 66 |
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 67 |
|
| 68 |
|
| 69 |
-
# =========================================================
|
| 70 |
-
# SAFE DB INITIALIZATION
|
| 71 |
-
# =========================================================
|
| 72 |
-
def init_database_safe():
|
| 73 |
-
if not AUTH_ENABLED:
|
| 74 |
-
logger.warning("Auth disabled - skipping DB init")
|
| 75 |
-
return
|
| 76 |
-
|
| 77 |
-
try:
|
| 78 |
-
Base.metadata.create_all(bind=engine)
|
| 79 |
-
logger.info("Database initialized successfully")
|
| 80 |
-
except Exception as e:
|
| 81 |
-
logger.error(f"Database init failed (non-fatal): {e}")
|
| 82 |
-
|
| 83 |
-
|
| 84 |
# =========================================================
|
| 85 |
# LIFECYCLE
|
| 86 |
# =========================================================
|
|
@@ -89,9 +55,6 @@ async def lifespan(app: FastAPI):
|
|
| 89 |
|
| 90 |
logger.info("Starting Basyx Whisper V10.1")
|
| 91 |
|
| 92 |
-
# DB init (NON-FATAL)
|
| 93 |
-
init_database_safe()
|
| 94 |
-
|
| 95 |
# workers (must not block boot)
|
| 96 |
try:
|
| 97 |
start_worker()
|
|
@@ -123,11 +86,6 @@ app = FastAPI(
|
|
| 123 |
lifespan=lifespan,
|
| 124 |
)
|
| 125 |
|
| 126 |
-
# AUTH ROUTER (only if available)
|
| 127 |
-
if AUTH_ENABLED:
|
| 128 |
-
app.include_router(auth_router)
|
| 129 |
-
|
| 130 |
-
|
| 131 |
# =========================================================
|
| 132 |
# TASKS
|
| 133 |
# =========================================================
|
|
@@ -305,8 +263,7 @@ async def execute_router(
|
|
| 305 |
def health():
|
| 306 |
return {
|
| 307 |
"status": "online",
|
| 308 |
-
"version": "V10.1"
|
| 309 |
-
"auth_enabled": AUTH_ENABLED
|
| 310 |
}
|
| 311 |
|
| 312 |
|
|
|
|
| 14 |
from utils.job_queue import start_worker, create_job, get_job
|
| 15 |
from ingestion.resolver import resolve_input
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# ==============================
|
| 18 |
# CORE PIPELINE
|
| 19 |
# ==============================
|
|
|
|
| 39 |
from publisher.metadata_engine import generate_metadata
|
| 40 |
from publisher.thumbnail_engine import generate_thumbnail
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
# ==============================
|
| 43 |
# INIT
|
| 44 |
# ==============================
|
|
|
|
| 47 |
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 48 |
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
# =========================================================
|
| 51 |
# LIFECYCLE
|
| 52 |
# =========================================================
|
|
|
|
| 55 |
|
| 56 |
logger.info("Starting Basyx Whisper V10.1")
|
| 57 |
|
|
|
|
|
|
|
|
|
|
| 58 |
# workers (must not block boot)
|
| 59 |
try:
|
| 60 |
start_worker()
|
|
|
|
| 86 |
lifespan=lifespan,
|
| 87 |
)
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
# =========================================================
|
| 90 |
# TASKS
|
| 91 |
# =========================================================
|
|
|
|
| 263 |
def health():
|
| 264 |
return {
|
| 265 |
"status": "online",
|
| 266 |
+
"version": "V10.1"
|
|
|
|
| 267 |
}
|
| 268 |
|
| 269 |
|