diff --git a/README.md b/README.md index 4c0c4fc..464e042 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ ScrAIbe is a transcription and summarization service that: - Provides: - A web GUI for uploading audio and receiving transcripts via email. - A CLI and Python API for direct integration. - - An MCP-style HTTP API (OpenAPI) for LLMs and external systems. - A watch-folder mode for automatic transcription, summarization, and email delivery. No local speech models or heavy dependencies are required. ScrAIbe is designed as a thin client in front of your own AI services. @@ -35,13 +34,6 @@ For more information: https://apstrom.ca - Final transcript (MD + DOCX + JSON) when ready. - Summary as MD + DOCX (if requested). - Error notification if processing fails. -- MCP-style HTTP API (optional): - - Exposes an OpenAPI-compliant REST endpoint for external LLMs or services. - - Allows: - - Audio upload for transcription. - - Job status checks. - - Retrieval of transcript JSON (no summary). - - Enabled via MCP_SERVER_ENABLED=true. - Watch-folder mode (optional): - Monitors a directory for audio files. - For each file: @@ -81,7 +73,6 @@ For more information: https://apstrom.ca - Output formatting (e.g., .md with transcript + summary) - Runs: - Web GUI (Gradio) – always enabled - - MCP-style HTTP API (FastAPI) – optional - Watch-folder mode – optional - Celery worker (async processing) - Redis (in-container by default) @@ -233,19 +224,6 @@ Accent color (UI and emails): - Email headings, links, and email addresses - Default: #7C6DA0 -MCP-style HTTP API: - -- MCP_SERVER_ENABLED: - - Enable MCP-style HTTP API (default: false). - - Values: true/false. -- MCP_SERVER_HOST: - - Bind address (default: 0.0.0.0). -- MCP_SERVER_PORT: - - Port (default: 8000). -- MCP_USE_CELERY: - - Use Celery for async transcription (default: true). - - If false, transcription runs in-process. - Watch-folder mode: - WATCH_ENABLED: @@ -355,8 +333,6 @@ Core runtime dependencies: - celery[redis] - redis - python-docx -- fastapi -- uvicorn - ffmpeg (for audio preprocessing) No local Whisper, PyTorch, or Pyannote models are required. diff --git a/scraibe/__main__.py b/scraibe/__main__.py index eff65ab..d0f3d3d 100644 --- a/scraibe/__main__.py +++ b/scraibe/__main__.py @@ -5,12 +5,9 @@ Entrypoint for running ScrAIbe as a module: Always launches the Web GUI (Gradio). Optionally launches: -- MCP-style API server - Watch-folder mode """ -import os -import threading import logging logger = logging.getLogger("scraibe.__main__") @@ -18,35 +15,7 @@ logger = logging.getLogger("scraibe.__main__") from .webui import create_app -def _run_mcp_server(): - """ - Run MCP server in a separate thread. - """ - import uvicorn - from . import mcp_server - - host = os.getenv("MCP_SERVER_HOST", "0.0.0.0") - port = int(os.getenv("MCP_SERVER_PORT", "8000")) - - uvicorn.run( - mcp_server.app, - host=host, - port=port, - log_level="info", - ) - - if __name__ == "__main__": - # Optionally start MCP server in background (non-blocking) - mcp_enabled = os.getenv("MCP_SERVER_ENABLED", "false").strip().lower() in ("true", "1", "yes") - if mcp_enabled: - try: - t = threading.Thread(target=_run_mcp_server, daemon=True) - t.start() - logger.info("MCP server started in background.") - except Exception as e: - logger.warning("Failed to start MCP server (WebUI will continue): %s", e) - # Optionally start watch-folder mode (non-blocking) try: from .watcher import start_watcher diff --git a/scraibe/mcp_server.py b/scraibe/mcp_server.py deleted file mode 100644 index e7ff206..0000000 --- a/scraibe/mcp_server.py +++ /dev/null @@ -1,304 +0,0 @@ -""" -MCP-style HTTP server for ScrAIbe. - -- Exposes an OpenAPI-compliant endpoint for external LLMs to: - - Submit audio (as base64, URL, or internal path) via JSON. - - Receive transcript JSON (no summary). -- WebUI remains always enabled; this is additive. - -Configuration (env): -- MCP_SERVER_ENABLED: "true"/"false" (default: false) -- MCP_SERVER_HOST: bind address (default: 0.0.0.0) -- MCP_SERVER_PORT: port (default: 8000) -- MCP_USE_CELERY: "true"/"false" (default: true) - - If true, uses Celery tasks; if false, runs synchronously. -""" - -import os -import time -import uuid -import base64 -import logging -from typing import Optional - -import httpx -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel, Field - -from .autotranscript import Scraibe - -logger = logging.getLogger("scraibe.mcp_server") - -app = FastAPI( - title="ScrAIbe MCP Transcription API", - version="0.1.0", - description=( - "MCP-style HTTP API for ScrAIbe. " - "Allows external LLMs to submit audio and receive transcript JSON." - ), - openapi_tags=[ - {"name": "transcription", "description": "Transcription endpoints"} - ], -) - -# In-memory job store for MCP (simple; can be replaced with Redis later) -_mcp_jobs: dict = {} - - -class TranscribeRequest(BaseModel): - """ - Input for transcription. - - Exactly one of audio_base64, audio_url, or audio_path must be provided. - """ - audio_base64: Optional[str] = Field( - None, - description="Base64-encoded audio file content." - ) - audio_url: Optional[str] = Field( - None, - description="Public URL to the audio file." - ) - audio_path: Optional[str] = Field( - None, - description="Internal file path on the server (for internal use only)." - ) - - -class TranscribeResponse(BaseModel): - job_id: str - status: str - message: str - - -class JobStatusResponse(BaseModel): - job_id: str - status: str - message: str - - -class TranscriptJSONResponse(BaseModel): - job_id: str - transcript: str - segments: list - - -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: - 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: - 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: - 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 exactly one of: audio_base64, audio_url, or audio_path", - ) - - -@app.get("/health", tags=["transcription"]) -async def health(): - return {"status": "ok"} - - -@app.post( - "/transcribe", - tags=["transcription"], - operation_id="transcribe", - response_model=TranscribeResponse, -) -async def transcribe(req: TranscribeRequest): - """ - 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) - - Returns: - { - "job_id": "", - "status": "queued" | "processing", - "message": "..." - } - - Use GET /transcribe/{job_id}/status and /json to retrieve results. - """ - use_celery = os.getenv("MCP_USE_CELERY", "true").strip().lower() in ("true", "1", "yes") - - # Save audio to a temporary file - try: - 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}") - - job_id = _job_id() - - if use_celery: - try: - from .tasks import process_mcp_transcribe_task - except ImportError: - # Fallback: run synchronously - use_celery = False - - if use_celery: - try: - process_mcp_transcribe_task.delay( - audio_path=file_path, - job_id=job_id, - language=None, - num_speakers=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": file_path, - } - return TranscribeResponse( - job_id=job_id, - status="error", - message=_mcp_jobs[job_id]["message"], - ) - - _mcp_jobs[job_id] = { - "status": "queued", - "message": "Job queued for processing.", - "file_path": file_path, - } - return TranscribeResponse( - job_id=job_id, - status="queued", - message=_mcp_jobs[job_id]["message"], - ) - - # Synchronous path - _mcp_jobs[job_id] = { - "status": "processing", - "message": "Transcription started (synchronous).", - "file_path": file_path, - } - - def _run_sync(): - try: - scraibe = Scraibe(verbose=False) - result = scraibe.transcribe( - audio_file=file_path, - language=None, - num_speakers=None, - verbose=False, - for_export=True, - ) - transcript_text = result.get("transcript", "") - segments = result.get("segments", []) - _mcp_jobs[job_id]["status"] = "completed" - _mcp_jobs[job_id]["transcript"] = transcript_text - _mcp_jobs[job_id]["segments"] = segments - _mcp_jobs[job_id]["message"] = "Transcription completed." - except Exception as e: - logger.error("MCP sync transcription error: %s", e) - _mcp_jobs[job_id]["status"] = "error" - _mcp_jobs[job_id]["message"] = f"Transcription error: {e}" - - import threading - t = threading.Thread(target=_run_sync, daemon=True) - t.start() - - return TranscribeResponse( - job_id=job_id, - status="processing", - message=_mcp_jobs[job_id]["message"], - ) - - -@app.get( - "/transcribe/{job_id}/status", - tags=["transcription"], - operation_id="get_status", - response_model=JobStatusResponse, -) -async def get_status(job_id: str): - job = _mcp_jobs.get(job_id) - if not job: - raise HTTPException(status_code=404, detail="Job not found") - return JobStatusResponse( - job_id=job_id, - status=job["status"], - message=job.get("message", ""), - ) - - -@app.get( - "/transcribe/{job_id}/json", - tags=["transcription"], - operation_id="get_json", - response_model=TranscriptJSONResponse, -) -async def get_json(job_id: str): - job = _mcp_jobs.get(job_id) - if not job: - raise HTTPException(status_code=404, detail="Job not found") - - if job["status"] != "completed": - raise HTTPException( - status_code=400, - detail=f"Job not completed. Current status: {job['status']}", - ) - - transcript_text = job.get("transcript", "") - segments = job.get("segments", []) - - return TranscriptJSONResponse( - job_id=job_id, - transcript=transcript_text, - segments=segments, - ) diff --git a/scraibe/tasks.py b/scraibe/tasks.py index 091cde3..1c713d1 100644 --- a/scraibe/tasks.py +++ b/scraibe/tasks.py @@ -506,71 +506,6 @@ def process_transcription_task( logger.info("Cleanup completed for job %s.", task_id) -@celery_app.task( - name="scraibe.tasks.process_mcp_transcribe_task", - bind=True, - max_retries=1, - task_time_limit=14400, - task_soft_time_limit=13500, -) -def process_mcp_transcribe_task( - self, - audio_path: str, - job_id: str, - language: str, - num_speakers: int, -): - """ - Async task used by MCP-style API: - - Transcribe audio - - Store transcript + segments in shared MCP job store - - Clean up temporary file - """ - from .mcp_server import _mcp_jobs - - log_level = os.getenv("LOG_LEVEL", "INFO") - setup_logging(level=log_level) - - # Initialize status - _mcp_jobs.setdefault( - job_id, - { - "status": "processing", - "message": "Transcription started (async).", - "file_path": audio_path, - }, - ) - - try: - scraibe = Scraibe(verbose=True) - result = scraibe.transcribe( - audio_file=audio_path, - language=language or None, - num_speakers=int(num_speakers) if num_speakers else None, - verbose=True, - for_export=True, - ) - - transcript_text = result.get("transcript", "") - segments = result.get("segments", []) - - _mcp_jobs[job_id]["status"] = "completed" - _mcp_jobs[job_id]["transcript"] = transcript_text - _mcp_jobs[job_id]["segments"] = segments - _mcp_jobs[job_id]["message"] = "Transcription completed." - - logger.info("MCP job %s completed.", job_id) - - except Exception as e: - logger.error("MCP job %s failed: %s", job_id, e, exc_info=True) - _mcp_jobs[job_id]["status"] = "error" - _mcp_jobs[job_id]["message"] = f"Transcription error: {e}" - - finally: - _remove_file(audio_path) - logger.info("MCP job %s cleanup completed.", job_id) - - @celery_app.task( name="scraibe.tasks.process_watch_file_task", bind=True,