Update MCP server to accept JSON body with audio_base64/audio_url
Mirror and run GitLab CI / build (push) Has been cancelled
Ruff / ruff (push) Has been cancelled

- Remove multipart form; use JSON with audio_base64 or audio_url.
- Add internal audio_path field for internal use.
- Keep OpenAPI spec consistent with new schema.
This commit is contained in:
admin
2026-06-20 01:15:50 +00:00
parent cd0c730abe
commit 099fb30e6c
+95 -38
View File
@@ -2,8 +2,8 @@
MCP-style HTTP server for ScrAIbe. MCP-style HTTP server for ScrAIbe.
- Exposes an OpenAPI-compliant endpoint for external LLMs to: - Exposes an OpenAPI-compliant endpoint for external LLMs to:
- Upload audio - Upload audio (as base64 or via URL) in a JSON body.
- Receive transcript JSON (no summary) - Receive transcript JSON (no summary).
- WebUI remains always enabled; this is additive. - WebUI remains always enabled; this is additive.
Configuration (env): Configuration (env):
@@ -17,12 +17,14 @@ Configuration (env):
import os import os
import time import time
import uuid import uuid
import json import base64
import tempfile
import logging import logging
from typing import Optional from typing import Optional
from fastapi import FastAPI, UploadFile, File, Form, HTTPException import httpx
from fastapi.responses import JSONResponse from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from .autotranscript import Scraibe from .autotranscript import Scraibe
@@ -41,28 +43,95 @@ app = FastAPI(
_mcp_jobs: dict = {} _mcp_jobs: dict = {}
class TranscribeRequest(BaseModel):
audio_base64: Optional[str] = None
audio_url: Optional[str] = None
audio_path: Optional[str] = None
language: Optional[str] = None
num_speakers: Optional[int] = None
def _job_id() -> str: def _job_id() -> str:
return str(uuid.uuid4()) return str(uuid.uuid4())
def _save_audio_from_request(req: TranscribeRequest) -> str:
"""
Save audio to a temporary file from base64, URL, or path.
Returns the local file path.
"""
upload_dir = os.getenv("SCRAIBE_UPLOAD_DIR", "/tmp/scraibe_uploads")
os.makedirs(upload_dir, exist_ok=True)
if req.audio_base64:
# base64 upload
try:
data = base64.b64decode(req.audio_base64)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid base64 audio: {e}")
ts = time.strftime("%Y%m%d%H%M%S")
tmp_name = f"mcp_upload_{ts}_{uuid.uuid4().hex[:8]}.wav"
file_path = os.path.join(upload_dir, tmp_name)
with open(file_path, "wb") as f:
f.write(data)
return file_path
if req.audio_url:
# download from URL
try:
with httpx.stream("GET", req.audio_url, timeout=60) as resp:
if resp.status_code != 200:
raise HTTPException(
status_code=400,
detail=f"Failed to download audio from URL: {resp.status_code}",
)
ts = time.strftime("%Y%m%d%H%M%S")
tmp_name = f"mcp_url_{ts}_{uuid.uuid4().hex[:8]}.wav"
file_path = os.path.join(upload_dir, tmp_name)
with open(file_path, "wb") as f:
for chunk in resp.iter_bytes():
f.write(chunk)
return file_path
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error downloading audio: {e}")
if req.audio_path:
# use provided path (for internal use)
path = req.audio_path
if not os.path.isfile(path):
raise HTTPException(status_code=400, detail="audio_path does not exist")
return path
raise HTTPException(
status_code=400,
detail="Provide one of: audio_base64, audio_url, or audio_path",
)
@app.get("/health") @app.get("/health")
async def health(): async def health():
return {"status": "ok"} return {"status": "ok"}
@app.post("/transcribe") @app.post("/transcribe")
async def transcribe( async def transcribe(req: TranscribeRequest):
file: UploadFile = File(...),
language: Optional[str] = Form(None),
num_speakers: Optional[int] = Form(None),
):
""" """
Upload audio and start transcription. Submit an audio file for transcription.
Input (JSON body):
- audio_base64: base64-encoded audio file
- audio_url: URL to audio file
- audio_path: local file path (for internal use)
- language: (optional)
- num_speakers: (optional)
Returns: Returns:
{ {
"job_id": "<id>", "job_id": "<id>",
"status": "queued" | "processing" | "completed" | "error", "status": "queued" | "processing",
"message": "..." "message": "..."
} }
@@ -70,21 +139,11 @@ async def transcribe(
""" """
use_celery = os.getenv("MCP_USE_CELERY", "true").strip().lower() in ("true", "1", "yes") use_celery = os.getenv("MCP_USE_CELERY", "true").strip().lower() in ("true", "1", "yes")
# Save uploaded file temporarily # Save audio to a temporary file
try: try:
import tempfile file_path = _save_audio_from_request(req)
from pathlib import Path except HTTPException:
raise
upload_dir = Path(os.getenv("SCRAIBE_UPLOAD_DIR", "/tmp/scraibe_uploads"))
upload_dir.mkdir(parents=True, exist_ok=True)
ext = Path(file.filename or "file").suffix or ".wav"
ts = time.strftime("%Y%m%d%H%M%S")
tmp_name = f"mcp_upload_{ts}_{uuid.uuid4().hex[:8]}{ext}"
file_path = upload_dir / tmp_name
content = await file.read()
file_path.write_bytes(content)
except Exception as e: except Exception as e:
logger.error("Error saving MCP upload: %s", e) logger.error("Error saving MCP upload: %s", e)
raise HTTPException(status_code=500, detail=f"Error saving file: {e}") raise HTTPException(status_code=500, detail=f"Error saving file: {e}")
@@ -101,17 +160,17 @@ async def transcribe(
if use_celery: if use_celery:
try: try:
process_mcp_transcribe_task.delay( process_mcp_transcribe_task.delay(
audio_path=str(file_path), audio_path=file_path,
job_id=job_id, job_id=job_id,
language=language or None, language=req.language or None,
num_speakers=int(num_speakers) if num_speakers else None, num_speakers=int(req.num_speakers) if req.num_speakers else None,
) )
except Exception as e: except Exception as e:
logger.error("Error enqueuing MCP job: %s", e) logger.error("Error enqueuing MCP job: %s", e)
_mcp_jobs[job_id] = { _mcp_jobs[job_id] = {
"status": "error", "status": "error",
"message": f"Error enqueuing job: {e}", "message": f"Error enqueuing job: {e}",
"file_path": str(file_path), "file_path": file_path,
} }
return { return {
"job_id": job_id, "job_id": job_id,
@@ -122,7 +181,7 @@ async def transcribe(
_mcp_jobs[job_id] = { _mcp_jobs[job_id] = {
"status": "queued", "status": "queued",
"message": "Job queued for processing.", "message": "Job queued for processing.",
"file_path": str(file_path), "file_path": file_path,
} }
return { return {
"job_id": job_id, "job_id": job_id,
@@ -134,16 +193,16 @@ async def transcribe(
_mcp_jobs[job_id] = { _mcp_jobs[job_id] = {
"status": "processing", "status": "processing",
"message": "Transcription started (synchronous).", "message": "Transcription started (synchronous).",
"file_path": str(file_path), "file_path": file_path,
} }
def _run_sync(): def _run_sync():
try: try:
scraibe = Scraibe(verbose=False) scraibe = Scraibe(verbose=False)
result = scraibe.transcribe( result = scraibe.transcribe(
audio_file=str(file_path), audio_file=file_path,
language=language or None, language=req.language or None,
num_speakers=int(num_speakers) if num_speakers else None, num_speakers=int(req.num_speakers) if req.num_speakers else None,
verbose=False, verbose=False,
for_export=True, for_export=True,
) )
@@ -196,10 +255,8 @@ async def get_json(job_id: str):
transcript_text = job.get("transcript", "") transcript_text = job.get("transcript", "")
segments = job.get("segments", []) segments = job.get("segments", [])
return JSONResponse( return {
content={
"job_id": job_id, "job_id": job_id,
"transcript": transcript_text, "transcript": transcript_text,
"segments": segments, "segments": segments,
} }
)