Update MCP server to accept JSON body with audio_base64/audio_url
- 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:
+95
-38
@@ -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,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user