StepAudio-3-Music / server.py
yanchaomars's picture
Upload 147 files
86020e3 verified
Raw History Blame Contribute Delete
3.69 kB
"""Serve the original music studio and proxy StepFun using a Space Secret."""
import asyncio
import os
import time
from collections import deque
from pathlib import Path
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
UPSTREAM = 'https://api.stepfun.com'
MAX_BODY = 30 * 1024 * 1024
submissions = deque()
submit_lock = asyncio.Lock()
def error(status: int, kind: str, message: str) -> JSONResponse:
return JSONResponse({'error': {'type': kind, 'message': message}}, status_code=status)
@app.get('/')
async def studio():
return RedirectResponse('/studio/app.html')
@app.get('/healthz')
async def health() -> dict:
return {'status': 'ok', 'configured': bool(os.environ.get('STEPFUN_API_KEY'))}
@app.post('/v1/audio/music/{action}')
async def music(action: str, request: Request):
"""Forward only music submission and polling; never return the API key."""
if action not in ('submit', 'query'):
return error(404, 'not_found', 'Unknown endpoint')
key = os.environ.get('STEPFUN_API_KEY', '').strip()
if not key:
return error(503, 'service_unavailable', 'Music service is not configured yet')
raw = bytearray()
async for chunk in request.stream():
raw.extend(chunk)
if len(raw) > MAX_BODY:
return error(413, 'request_params_invalid', 'Payload too large')
import json
try:
body = json.loads(raw)
if not isinstance(body, dict):
raise ValueError()
except (ValueError, UnicodeDecodeError):
return error(400, 'request_params_invalid', 'Expected a JSON object')
if action == 'submit':
if body.get('task') not in ('text_to_music', 'vocal_to_music', 'music_cover'):
return error(400, 'request_params_invalid', 'Unsupported task')
if not isinstance(body.get('caption'), str) or not body['caption'].strip():
return error(400, 'request_params_invalid', 'Caption is required')
body['model_id'] = 'step-music'
async with submit_lock:
now = time.monotonic()
while submissions and submissions[0] < now - 60:
submissions.popleft()
if len(submissions) >= int(os.environ.get('SUBMITS_PER_MINUTE', '6')):
return error(429, 'rate_limited', 'Please wait before generating again')
submissions.append(now)
elif not isinstance(body.get('task_id'), str) or not body['task_id']:
return error(400, 'request_params_invalid', 'Task ID is required')
try:
async with httpx.AsyncClient(timeout=httpx.Timeout(230, connect=15)) as client:
res = await client.post(f'{UPSTREAM}/v1/audio/music/{action}', json=body,
headers={'Authorization': f'Bearer {key}'})
payload = res.json()
# Never echo upstream error messages or arbitrary response headers.
if res.is_error:
kind = payload.get('error', {}).get('type', 'service_unavailable')
if kind not in ('not_allowed', 'request_params_invalid', 'model_invalid', 'rate_limited', 'service_unavailable'):
kind = 'service_unavailable'
return error(res.status_code, kind, 'Music API request failed')
return JSONResponse(payload, headers={'Cache-Control': 'no-store'})
except (httpx.HTTPError, ValueError):
return error(502, 'service_unavailable', 'Music API is temporarily unavailable')
app.mount('/studio', StaticFiles(directory=Path(__file__).parent / 'static', html=True), name='studio')