diff --git a/scraibe/mcp_server.py b/scraibe/mcp_server.py index bc52ea9..0b8d6be 100644 --- a/scraibe/mcp_server.py +++ b/scraibe/mcp_server.py @@ -2,8 +2,8 @@ MCP-style HTTP server for ScrAIbe. - Exposes an OpenAPI-compliant endpoint for external LLMs to: - - Upload audio - - Receive transcript JSON (no summary) + - Upload audio (as base64 or via URL) in a JSON body. + - Receive transcript JSON (no summary). - WebUI remains always enabled; this is additive. Configuration (env): @@ -17,12 +17,14 @@ Configuration (env): import os import time import uuid -import json +import base64 +import tempfile import logging from typing import Optional -from fastapi import FastAPI, UploadFile, File, Form, HTTPException -from fastapi.responses import JSONResponse +import httpx +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel from .autotranscript import Scraibe @@ -41,28 +43,95 @@ app = FastAPI( _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: 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") async def health(): return {"status": "ok"} @app.post("/transcribe") -async def transcribe( - file: UploadFile = File(...), - language: Optional[str] = Form(None), - num_speakers: Optional[int] = Form(None), -): +async def transcribe(req: TranscribeRequest): """ - 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: { "job_id": "", - "status": "queued" | "processing" | "completed" | "error", + "status": "queued" | "processing", "message": "..." } @@ -70,21 +139,11 @@ async def transcribe( """ 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: - import tempfile - from pathlib import Path - - 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) + file_path = _save_audio_from_request(req) + except HTTPException: + raise except Exception as e: logger.error("Error saving MCP upload: %s", e) raise HTTPException(status_code=500, detail=f"Error saving file: {e}") @@ -101,17 +160,17 @@ async def transcribe( if use_celery: try: process_mcp_transcribe_task.delay( - audio_path=str(file_path), + audio_path=file_path, job_id=job_id, - language=language or None, - num_speakers=int(num_speakers) if num_speakers else None, + language=req.language or None, + num_speakers=int(req.num_speakers) if req.num_speakers else None, ) except Exception as e: logger.error("Error enqueuing MCP job: %s", e) _mcp_jobs[job_id] = { "status": "error", "message": f"Error enqueuing job: {e}", - "file_path": str(file_path), + "file_path": file_path, } return { "job_id": job_id, @@ -122,7 +181,7 @@ async def transcribe( _mcp_jobs[job_id] = { "status": "queued", "message": "Job queued for processing.", - "file_path": str(file_path), + "file_path": file_path, } return { "job_id": job_id, @@ -134,16 +193,16 @@ async def transcribe( _mcp_jobs[job_id] = { "status": "processing", "message": "Transcription started (synchronous).", - "file_path": str(file_path), + "file_path": file_path, } def _run_sync(): try: scraibe = Scraibe(verbose=False) result = scraibe.transcribe( - audio_file=str(file_path), - language=language or None, - num_speakers=int(num_speakers) if num_speakers else None, + audio_file=file_path, + language=req.language or None, + num_speakers=int(req.num_speakers) if req.num_speakers else None, verbose=False, for_export=True, ) @@ -196,10 +255,8 @@ async def get_json(job_id: str): transcript_text = job.get("transcript", "") segments = job.get("segments", []) - return JSONResponse( - content={ - "job_id": job_id, - "transcript": transcript_text, - "segments": segments, - } - ) + return { + "job_id": job_id, + "transcript": transcript_text, + "segments": segments, + }