File size: 4,048 Bytes
7ac6163
 
 
 
1c79dfc
 
 
7ac6163
95c22a6
 
7ac6163
 
 
 
 
 
 
 
1c79dfc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7ac6163
 
8ea6326
7ac6163
 
 
 
8ea6326
7ac6163
c3a06a0
7ac6163
c3a06a0
95c22a6
8ea6326
 
 
95c22a6
8ea6326
95c22a6
 
 
 
 
 
c3a06a0
e8b1db1
7ac6163
 
1c79dfc
 
 
 
 
 
 
 
 
 
 
 
 
e8b1db1
7ac6163
95c22a6
e8b1db1
7ac6163
 
 
 
 
 
 
 
 
e8b1db1
7ac6163
 
e8b1db1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""
Routes for data processing and manipulation (Audio/Image).
"""

import time
from collections import defaultdict
from fastapi import APIRouter, File, UploadFile, Form, HTTPException, Request, Depends, status
from fastapi.responses import StreamingResponse
from pydantic import ValidationError
import json
import logging

from app.schemas import AudioAugmentationOptions
from app.services.audio_processor import process_audio

router = APIRouter(prefix="/api/process", tags=["Data Processing"])
logger = logging.getLogger(__name__)

# Rate limiter for heavy processing endpoints
_process_rate_store: dict[str, list[float]] = defaultdict(list)
_PROCESS_RATE_WINDOW = 60
_PROCESS_RATE_MAX = 10
_PROCESS_MAX_IPS = 10_000


async def _process_rate_limit(request: Request) -> None:
    client_ip = request.client.host if request.client else "unknown"
    now = time.time()
    cutoff = now - _PROCESS_RATE_WINDOW

    if len(_process_rate_store) > _PROCESS_MAX_IPS:
        stale = [ip for ip, ts in _process_rate_store.items() if all(t <= cutoff for t in ts)]
        for ip in stale:
            del _process_rate_store[ip]

    hits = [t for t in _process_rate_store[client_ip] if t > cutoff]
    if len(hits) >= _PROCESS_RATE_MAX:
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail={"code": "rate_limit_exceeded", "message": "Too many processing requests. Please wait."}
        )
    hits.append(now)
    _process_rate_store[client_ip] = hits


@router.post("/audio", dependencies=[Depends(_process_rate_limit)])
async def process_audio_endpoint(
    file: UploadFile = File(...),
    options: str = Form(default="{}")
):
    """
    Process an audio file with the given augmentation options.
    Returns the processed WAV file.
    options is a JSON string; if missing or empty, defaults to all-off.
    """
    MAX_PAYLOAD_BYTES = 30 * 1024 * 1024  # 30 MB
    logger.info(f"Received audio processing request for file: {file.filename}")

    # Parse options JSON string into validated Pydantic model
    raw_options = options.strip() if options else "{}"
    if not raw_options:
        raw_options = "{}"
    try:
        parsed_options = AudioAugmentationOptions.model_validate_json(raw_options)
    except (ValidationError, json.JSONDecodeError) as e:
        raise HTTPException(
            status_code=422,
            detail={"code": "invalid_options", "message": f"Invalid options format: {e}"}
        )

    if not file.content_type or not file.content_type.startswith("audio/"):
        raise HTTPException(status_code=400, detail={"code": "invalid_file_type", "message": "Invalid file type. Must be audio."})

    try:
        # Read file in chunks to enforce size limit before full allocation
        chunks = []
        total_read = 0
        chunk_size = 1024 * 1024  # 1 MB chunks
        while True:
            chunk = await file.read(chunk_size)
            if not chunk:
                break
            total_read += len(chunk)
            if total_read > MAX_PAYLOAD_BYTES:
                raise HTTPException(status_code=413, detail={"code": "file_too_large", "message": f"File too large. Maximum size is {MAX_PAYLOAD_BYTES // (1024*1024)} MB."})
            chunks.append(chunk)
        content = b"".join(chunks)

        # Process audio
        processed_audio = process_audio(content, parsed_options)

        # Return as downloadable file
        filename = f"processed_{file.filename}.wav"
        return StreamingResponse(
            processed_audio,
            media_type="audio/wav",
            headers={"Content-Disposition": f"attachment; filename={filename}"}
        )

    except ValueError as e:
        raise HTTPException(status_code=400, detail={"code": "validation_error", "message": str(e)})
    except Exception as e:
        logger.error(f"Unexpected error in audio processing: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail={"code": "internal_error", "message": "Internal server error during audio processing"})