Initial commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
/export/pdf - MusicXML to PDF via music21 + LilyPond (or Audiveris)
|
||||
/export/mp3 - WAV buffer to MP3 via FFmpeg
|
||||
"""
|
||||
import base64
|
||||
import binascii
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# C2 - size caps
|
||||
MAX_B64_BYTES = 300 * 1024 * 1024 # 300 MB base64 string (≈ 200 MB decoded)
|
||||
MAX_WAV_BYTES = 200 * 1024 * 1024 # 200 MB decoded WAV
|
||||
|
||||
|
||||
MAX_MUSICXML_BYTES = 10 * 1024 * 1024 # 10 MB
|
||||
|
||||
# M5 - WAV magic bytes: "RIFF....WAVE"
|
||||
_WAV_RIFF = b"RIFF"
|
||||
_WAV_WAVE = b"WAVE"
|
||||
|
||||
|
||||
class ExportPdfRequest(BaseModel):
|
||||
musicxml: str
|
||||
|
||||
|
||||
class ExportMp3Request(BaseModel):
|
||||
wav_base64: str # base64-encoded WAV
|
||||
|
||||
|
||||
def _rm(path: str) -> None:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/pdf")
|
||||
async def export_pdf(req: ExportPdfRequest, background_tasks: BackgroundTasks):
|
||||
"""Convert MusicXML to PDF sheet music."""
|
||||
if len(req.musicxml.encode()) > MAX_MUSICXML_BYTES:
|
||||
raise HTTPException(status_code=413, detail="MusicXML payload too large (max 10 MB)")
|
||||
try:
|
||||
from services.music21_svc import musicxml_to_pdf
|
||||
pdf_path = await musicxml_to_pdf(req.musicxml)
|
||||
background_tasks.add_task(_rm, pdf_path) # H5
|
||||
|
||||
return FileResponse(
|
||||
pdf_path,
|
||||
media_type="application/pdf",
|
||||
filename="sheet.pdf",
|
||||
headers={
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Disposition": 'attachment; filename="sheet.pdf"',
|
||||
},
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("export_pdf failed")
|
||||
raise HTTPException(status_code=500, detail="PDF export failed. Please try again.")
|
||||
|
||||
|
||||
@router.post("/mp3")
|
||||
async def export_mp3(req: ExportMp3Request, background_tasks: BackgroundTasks):
|
||||
"""Convert base64 WAV to MP3 using FFmpeg."""
|
||||
# L6 - size-check before decoding; catch malformed base64 explicitly
|
||||
if len(req.wav_base64) > MAX_B64_BYTES:
|
||||
raise HTTPException(status_code=413, detail="WAV payload too large (max 200 MB)")
|
||||
|
||||
try:
|
||||
wav_bytes = base64.b64decode(req.wav_base64, validate=True)
|
||||
except binascii.Error:
|
||||
raise HTTPException(status_code=422, detail="Invalid base64 encoding")
|
||||
|
||||
# C2 - check decoded size too
|
||||
if len(wav_bytes) > MAX_WAV_BYTES:
|
||||
raise HTTPException(status_code=413, detail="WAV data too large (max 200 MB)")
|
||||
|
||||
# M5 - validate WAV magic bytes: "RIFF....WAVE"
|
||||
if len(wav_bytes) < 12 or wav_bytes[:4] != _WAV_RIFF or wav_bytes[8:12] != _WAV_WAVE:
|
||||
raise HTTPException(status_code=422, detail="Not a valid WAV file")
|
||||
|
||||
try:
|
||||
wav_tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
||||
wav_tmp.write(wav_bytes)
|
||||
wav_tmp.close()
|
||||
|
||||
mp3_tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
|
||||
mp3_tmp.close()
|
||||
|
||||
# H5 - register cleanup regardless of success/failure
|
||||
background_tasks.add_task(_rm, wav_tmp.name)
|
||||
background_tasks.add_task(_rm, mp3_tmp.name)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ffmpeg", "-y",
|
||||
"-i", wav_tmp.name,
|
||||
"-codec:a", "libmp3lame",
|
||||
"-q:a", "2",
|
||||
mp3_tmp.name,
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=60,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error("ffmpeg failed: %s", result.stderr.decode(errors="replace")[:500])
|
||||
raise RuntimeError("ffmpeg conversion failed")
|
||||
|
||||
return FileResponse(
|
||||
mp3_tmp.name,
|
||||
media_type="audio/mpeg",
|
||||
filename="export.mp3",
|
||||
headers={
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Disposition": 'attachment; filename="export.mp3"',
|
||||
},
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("export_mp3 failed")
|
||||
raise HTTPException(status_code=500, detail="MP3 export failed. Please try again.")
|
||||
Reference in New Issue
Block a user