validops-east-1 commited on
Commit
5f78436
·
1 Parent(s): 722c296
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 FileResponse, JSONResponse
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="Service account key as a raw JSON string"
 
 
 
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 (Supabase signed URL or local endpoint)")
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
- * exposed through local download endpoints (``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
 
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"/api/v1/media-convert/files/{job_id}/{f.filename}"
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"/api/v1/media-convert/files/{job_id}/{f.filename}"
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