Spaces:
Running
Running
Commit ·
5f78436
1
Parent(s): 722c296
whatever
Browse files
app/api/v1/media_convert.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import time
|
| 5 |
-
from pathlib import Path
|
| 6 |
from typing import Annotated, Any, Dict, Optional, Tuple
|
| 7 |
from urllib.parse import urlparse
|
| 8 |
|
|
@@ -14,7 +13,7 @@ from fastapi import (
|
|
| 14 |
HTTPException,
|
| 15 |
UploadFile,
|
| 16 |
)
|
| 17 |
-
from fastapi.responses import
|
| 18 |
from pydantic import ValidationError
|
| 19 |
|
| 20 |
from app.config import get_settings
|
|
@@ -42,12 +41,6 @@ _logger = get_logger(__name__)
|
|
| 42 |
_settings = get_settings()
|
| 43 |
_MAX_UPLOAD_BYTES = _settings.max_upload_bytes
|
| 44 |
|
| 45 |
-
_MIME_BY_EXT = {
|
| 46 |
-
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
| 47 |
-
".png": "image/png", ".webp": "image/webp",
|
| 48 |
-
".bmp": "image/bmp", ".gif": "image/gif", ".tiff": "image/tiff",
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
|
| 52 |
# ---------------------------------------------------------------------------
|
| 53 |
# Helpers
|
|
@@ -279,22 +272,6 @@ async def convert_image_url(body: MediaImageUrlRequest):
|
|
| 279 |
return _ok_response(start, data)
|
| 280 |
|
| 281 |
|
| 282 |
-
@router.get(
|
| 283 |
-
"/media-convert/files/{job_id}/{filename}",
|
| 284 |
-
summary="Download a converted file (local mode)",
|
| 285 |
-
responses={404: {"description": "File not found"}},
|
| 286 |
-
)
|
| 287 |
-
async def download_file(job_id: str, filename: str):
|
| 288 |
-
root = Path(_settings.media_output_dir).resolve()
|
| 289 |
-
if job_id != Path(job_id).name or filename != Path(filename).name:
|
| 290 |
-
raise HTTPException(status_code=404, detail={"success": False, "message": "File not found."})
|
| 291 |
-
path = (root / job_id / "out" / filename).resolve()
|
| 292 |
-
if not str(path).startswith(str(root)) or not path.is_file():
|
| 293 |
-
raise HTTPException(status_code=404, detail={"success": False, "message": "File not found."})
|
| 294 |
-
media_type = _MIME_BY_EXT.get(path.suffix.lower(), "application/octet-stream")
|
| 295 |
-
return FileResponse(path, media_type=media_type, filename=filename)
|
| 296 |
-
|
| 297 |
-
|
| 298 |
@router.get(
|
| 299 |
"/media-convert/formats",
|
| 300 |
response_model=MediaFormatsResponse,
|
|
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import time
|
|
|
|
| 5 |
from typing import Annotated, Any, Dict, Optional, Tuple
|
| 6 |
from urllib.parse import urlparse
|
| 7 |
|
|
|
|
| 13 |
HTTPException,
|
| 14 |
UploadFile,
|
| 15 |
)
|
| 16 |
+
from fastapi.responses import JSONResponse
|
| 17 |
from pydantic import ValidationError
|
| 18 |
|
| 19 |
from app.config import get_settings
|
|
|
|
| 41 |
_settings = get_settings()
|
| 42 |
_MAX_UPLOAD_BYTES = _settings.max_upload_bytes
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
# ---------------------------------------------------------------------------
|
| 46 |
# Helpers
|
|
|
|
| 272 |
return _ok_response(start, data)
|
| 273 |
|
| 274 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
@router.get(
|
| 276 |
"/media-convert/formats",
|
| 277 |
response_model=MediaFormatsResponse,
|
app/models/schemas.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import re
|
| 4 |
from enum import Enum
|
| 5 |
-
from typing import Any, Dict, List, Literal, Optional
|
| 6 |
|
| 7 |
from pydantic import BaseModel, Field, field_validator, model_validator
|
| 8 |
|
|
@@ -1208,8 +1208,11 @@ class GCSBaseCredentialsRequest(BaseModel):
|
|
| 1208 |
credentials: Optional[Dict[str, Any]] = Field(
|
| 1209 |
None, description="Service account key as an inline JSON object"
|
| 1210 |
)
|
| 1211 |
-
credentials_json: Optional[str] = Field(
|
| 1212 |
-
None, description=
|
|
|
|
|
|
|
|
|
|
| 1213 |
)
|
| 1214 |
credentials_url: Optional[str] = Field(
|
| 1215 |
None, description="Publicly reachable URL that returns the service account JSON"
|
|
@@ -1560,8 +1563,7 @@ class MediaOutputFile(BaseModel):
|
|
| 1560 |
size_bytes: int = Field(0, description="File size in bytes")
|
| 1561 |
format: str = Field(..., description="Output format (e.g. JPEG, PNG)")
|
| 1562 |
content_type: str = Field("application/octet-stream", description="MIME type of the output")
|
| 1563 |
-
url: str = Field(..., description="Download URL
|
| 1564 |
-
base64: Optional[str] = Field(None, description="Base64-encoded file bytes (local mode only)")
|
| 1565 |
|
| 1566 |
|
| 1567 |
class MediaUploadSummary(BaseModel):
|
|
|
|
| 2 |
|
| 3 |
import re
|
| 4 |
from enum import Enum
|
| 5 |
+
from typing import Any, Dict, List, Literal, Optional, Union
|
| 6 |
|
| 7 |
from pydantic import BaseModel, Field, field_validator, model_validator
|
| 8 |
|
|
|
|
| 1208 |
credentials: Optional[Dict[str, Any]] = Field(
|
| 1209 |
None, description="Service account key as an inline JSON object"
|
| 1210 |
)
|
| 1211 |
+
credentials_json: Optional[Union[str, Dict[str, Any]]] = Field(
|
| 1212 |
+
None, description=(
|
| 1213 |
+
"Service account key as a JSON object OR a raw JSON string. "
|
| 1214 |
+
"Passing it as a JSON object avoids escaping newlines/quotes."
|
| 1215 |
+
)
|
| 1216 |
)
|
| 1217 |
credentials_url: Optional[str] = Field(
|
| 1218 |
None, description="Publicly reachable URL that returns the service account JSON"
|
|
|
|
| 1563 |
size_bytes: int = Field(0, description="File size in bytes")
|
| 1564 |
format: str = Field(..., description="Output format (e.g. JPEG, PNG)")
|
| 1565 |
content_type: str = Field("application/octet-stream", description="MIME type of the output")
|
| 1566 |
+
url: str = Field(..., description="Download URL: Supabase signed URL (storage mode) or data URL (local/fallback mode)")
|
|
|
|
| 1567 |
|
| 1568 |
|
| 1569 |
class MediaUploadSummary(BaseModel):
|
app/services/media_conversion_service.py
CHANGED
|
@@ -11,10 +11,12 @@ converter and the reference PDF-conversion service.
|
|
| 11 |
|
| 12 |
Output files are written to a per-job directory and then either:
|
| 13 |
|
| 14 |
-
*
|
| 15 |
or
|
| 16 |
* uploaded to Supabase Storage with 24-hour signed URLs
|
| 17 |
(``SUPABASE_UPLOAD_ENABLED=true``) and a warning stating the expiry.
|
|
|
|
|
|
|
| 18 |
"""
|
| 19 |
|
| 20 |
from __future__ import annotations
|
|
@@ -564,7 +566,7 @@ class MediaConversionService:
|
|
| 564 |
if isinstance(outcome, Exception):
|
| 565 |
failed += 1
|
| 566 |
_logger.error("storage_upload_failed job=%s file=%s error=%s", job_id, f.filename, outcome)
|
| 567 |
-
f.url = f"
|
| 568 |
|
| 569 |
expires_at = iso_expiry(ttl)
|
| 570 |
warning = (
|
|
@@ -585,11 +587,7 @@ class MediaConversionService:
|
|
| 585 |
)
|
| 586 |
|
| 587 |
for f in files:
|
| 588 |
-
f.url = f"
|
| 589 |
-
try:
|
| 590 |
-
f.base64 = base64.b64encode((out_dir / f.filename).read_bytes()).decode("ascii")
|
| 591 |
-
except OSError:
|
| 592 |
-
f.base64 = None
|
| 593 |
return MediaUploadSummary(mode="local", total_files=len(files), failed_uploads=0), None
|
| 594 |
|
| 595 |
|
|
|
|
| 11 |
|
| 12 |
Output files are written to a per-job directory and then either:
|
| 13 |
|
| 14 |
+
* returned as data URLs (``SUPABASE_UPLOAD_ENABLED=false``),
|
| 15 |
or
|
| 16 |
* uploaded to Supabase Storage with 24-hour signed URLs
|
| 17 |
(``SUPABASE_UPLOAD_ENABLED=true``) and a warning stating the expiry.
|
| 18 |
+
|
| 19 |
+
If a Supabase upload fails, the file falls back to a data URL.
|
| 20 |
"""
|
| 21 |
|
| 22 |
from __future__ import annotations
|
|
|
|
| 566 |
if isinstance(outcome, Exception):
|
| 567 |
failed += 1
|
| 568 |
_logger.error("storage_upload_failed job=%s file=%s error=%s", job_id, f.filename, outcome)
|
| 569 |
+
f.url = f"data:{f.content_type};base64," + base64.b64encode((out_dir / f.filename).read_bytes()).decode("ascii")
|
| 570 |
|
| 571 |
expires_at = iso_expiry(ttl)
|
| 572 |
warning = (
|
|
|
|
| 587 |
)
|
| 588 |
|
| 589 |
for f in files:
|
| 590 |
+
f.url = f"data:{f.content_type};base64," + base64.b64encode((out_dir / f.filename).read_bytes()).decode("ascii")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
return MediaUploadSummary(mode="local", total_files=len(files), failed_uploads=0), None
|
| 592 |
|
| 593 |
|