From fb1e5dcebadde0c0bea759f85f66043a84b8fe0d Mon Sep 17 00:00:00 2001 From: Alihan Date: Mon, 27 Oct 2025 23:01:22 +0300 Subject: [PATCH] Upgrade to PyTorch 2.6.0 and enhance GPU reset script with Ollama management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade PyTorch and torchaudio to 2.6.0 with CUDA 12.4 support - Update GPU reset script to gracefully stop/start Ollama via supervisorctl - Add Docker Compose configuration for both API and MCP server modes - Implement comprehensive Docker entrypoint for multi-mode deployment - Add GPU health check cleanup to prevent memory leaks - Fix transcription memory management with proper resource cleanup - Add filename security validation to prevent path traversal attacks - Include .dockerignore for optimized Docker builds - Remove deprecated supervisor configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .dockerignore | 60 +++++ Dockerfile | 93 +++++-- TRANSCRIPTOR_API_FIX.md | 403 +++++++++++++++++++++++++++++++ docker-build.sh | 19 ++ docker-compose.yml | 106 ++++++++ docker-entrypoint.sh | 67 +++++ docker-run-api.sh | 62 +++++ docker-run-mcp.sh | 40 +++ reset_gpu.sh | 28 +++ src/core/gpu_health.py | 32 ++- src/core/transcriber.py | 16 ++ src/servers/api_server.py | 9 +- src/utils/input_validation.py | 66 +++++ supervisor/transcriptor-api.conf | 26 -- test.mp3 | Bin 0 -> 56270 bytes test_filename_fix.py | 60 +++++ tests/test_input_validation.py | 281 +++++++++++++++++++++ 17 files changed, 1313 insertions(+), 55 deletions(-) create mode 100644 .dockerignore create mode 100644 TRANSCRIPTOR_API_FIX.md create mode 100755 docker-build.sh create mode 100644 docker-compose.yml create mode 100755 docker-entrypoint.sh create mode 100755 docker-run-api.sh create mode 100755 docker-run-mcp.sh delete mode 100644 supervisor/transcriptor-api.conf create mode 100644 test.mp3 create mode 100644 test_filename_fix.py create mode 100644 tests/test_input_validation.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..43dd7b0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,60 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ + +# Virtual environments +venv/ +env/ +ENV/ +.venv + +# Project specific +logs/ +outputs/ +models/ +*.log +*.logs +mcp.logs +api.logs + +# Git +.git/ +.gitignore +.github/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Docker +.dockerignore +docker-compose.yml +docker-compose.*.yml + +# Temporary files +*.tmp +*.temp +.DS_Store +Thumbs.db + +# Documentation (optional - uncomment if you want to exclude) +# README.md +# CLAUDE.md +# IMPLEMENTATION_PLAN.md + +# Scripts (already in container) +# reset_gpu.sh - NEEDED for GPU health checks +run_api_server.sh +run_mcp_server.sh + +# Supervisor config (not needed in container) +supervisor/ diff --git a/Dockerfile b/Dockerfile index f99104a..88e6b64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,34 @@ -# Use NVIDIA CUDA base image with Python -FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04 +# Multi-purpose Whisper Transcriptor Docker Image +# Supports both MCP Server and REST API Server modes +# Use SERVER_MODE environment variable to select: "mcp" or "api" -# Install Python 3.12 +FROM nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04 + +# Prevent interactive prompts during installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install system dependencies RUN apt-get update && apt-get install -y \ software-properties-common \ + curl \ && add-apt-repository ppa:deadsnakes/ppa \ && apt-get update && apt-get install -y \ python3.12 \ python3.12-venv \ python3.12-dev \ - python3-pip \ ffmpeg \ git \ + nginx \ + supervisor \ && rm -rf /var/lib/apt/lists/* # Make python3.12 the default -RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.12 1 -RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 +RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.12 1 && \ + update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 -# Upgrade pip -RUN python -m pip install --upgrade pip +# Install pip using ensurepip (Python 3.12+ doesn't have distutils) +RUN python -m ensurepip --upgrade && \ + python -m pip install --upgrade pip # Set working directory WORKDIR /app @@ -27,30 +36,68 @@ WORKDIR /app # Copy requirements first for better caching COPY requirements.txt . -# Install Python dependencies with CUDA support +# Install Python dependencies with CUDA 12.4 support RUN pip install --no-cache-dir \ + torch==2.6.0 --index-url https://download.pytorch.org/whl/cu124 && \ + pip install --no-cache-dir \ + torchaudio==2.6.0 --index-url https://download.pytorch.org/whl/cu124 && \ + pip install --no-cache-dir \ faster-whisper \ - torch==2.5.1 --index-url https://download.pytorch.org/whl/cu121 \ - torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu121 \ - mcp[cli] + fastapi>=0.115.0 \ + uvicorn[standard]>=0.32.0 \ + python-multipart>=0.0.9 \ + aiofiles>=23.0.0 \ + mcp[cli]>=1.2.0 \ + gTTS>=2.3.0 \ + pyttsx3>=2.90 \ + scipy>=1.10.0 \ + numpy>=1.24.0 # Copy application code COPY src/ ./src/ COPY pyproject.toml . -COPY README.md . -# Create directories for models and outputs -RUN mkdir -p /models /outputs +# Copy test audio file for GPU health checks +COPY test.mp3 . + +# Copy nginx configuration +COPY nginx/transcriptor.conf /etc/nginx/sites-available/transcriptor.conf + +# Copy entrypoint script and GPU reset script +COPY docker-entrypoint.sh /docker-entrypoint.sh +COPY reset_gpu.sh /app/reset_gpu.sh +RUN chmod +x /docker-entrypoint.sh /app/reset_gpu.sh + +# Create directories for models, outputs, and logs +RUN mkdir -p /models /outputs /logs /app/outputs/uploads /app/outputs/batch /app/outputs/jobs # Set Python path ENV PYTHONPATH=/app/src -# Set environment variables for GPU -ENV WHISPER_MODEL_DIR=/models -ENV TRANSCRIPTION_OUTPUT_DIR=/outputs -ENV TRANSCRIPTION_MODEL=large-v3 -ENV TRANSCRIPTION_DEVICE=cuda -ENV TRANSCRIPTION_COMPUTE_TYPE=float16 +# Default environment variables (can be overridden) +ENV WHISPER_MODEL_DIR=/models \ + TRANSCRIPTION_OUTPUT_DIR=/outputs \ + TRANSCRIPTION_BATCH_OUTPUT_DIR=/outputs/batch \ + TRANSCRIPTION_MODEL=large-v3 \ + TRANSCRIPTION_DEVICE=auto \ + TRANSCRIPTION_COMPUTE_TYPE=auto \ + TRANSCRIPTION_OUTPUT_FORMAT=txt \ + TRANSCRIPTION_BEAM_SIZE=5 \ + TRANSCRIPTION_TEMPERATURE=0.0 \ + API_HOST=127.0.0.1 \ + API_PORT=33767 \ + JOB_QUEUE_MAX_SIZE=5 \ + JOB_METADATA_DIR=/outputs/jobs \ + JOB_RETENTION_DAYS=7 \ + GPU_HEALTH_CHECK_ENABLED=true \ + GPU_HEALTH_CHECK_INTERVAL_MINUTES=10 \ + GPU_HEALTH_TEST_MODEL=tiny \ + GPU_HEALTH_TEST_AUDIO=/test-audio/test.mp3 \ + GPU_RESET_COOLDOWN_MINUTES=5 \ + SERVER_MODE=api -# Run the server -CMD ["python", "src/servers/whisper_server.py"] \ No newline at end of file +# Expose port 80 for nginx (API mode only) +EXPOSE 80 + +# Use entrypoint script to handle different server modes +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/TRANSCRIPTOR_API_FIX.md b/TRANSCRIPTOR_API_FIX.md new file mode 100644 index 0000000..4229562 --- /dev/null +++ b/TRANSCRIPTOR_API_FIX.md @@ -0,0 +1,403 @@ +# Transcriptor API - Filename Validation Bug Fix + +## Issue Summary + +The transcriptor API is rejecting valid audio files due to overly strict path validation. Files with `..` (double periods) anywhere in the filename are being rejected as potential path traversal attacks, even when they appear naturally in legitimate filenames. + +## Current Behavior + +### Error Observed +```json +{ + "detail": { + "error": "Upload failed", + "message": "Audio file validation failed: Path traversal (..) is not allowed" + } +} +``` + +### HTTP Response +- **Status Code**: 500 +- **Endpoint**: `POST /transcribe` +- **Request**: File upload with filename containing `..` + +### Example Failing Filename +``` +This Weird FPV Drone only takes one kind of Battery... Rekon 35 V2.m4a + ^^^ + (Three dots, parsed as "..") +``` + +## Root Cause Analysis + +### Current Validation Logic (Problematic) +The API is likely checking for `..` anywhere in the filename string, which creates false positives: + +```python +# CURRENT (WRONG) +if ".." in filename: + raise ValidationError("Path traversal (..) is not allowed") +``` + +This rejects legitimate filenames like: +- `"video...mp4"` (ellipsis in title) +- `"Part 1... Part 2.m4a"` (ellipsis separator) +- `"Wait... what.mp4"` (dramatic pause) + +### Actual Security Concern +Path traversal attacks use `..` as **directory separators** to navigate up the filesystem: +- `../../etc/passwd` (DANGEROUS) +- `../../../secrets.txt` (DANGEROUS) +- `video...mp4` (SAFE - just a filename) + +## Recommended Fix + +### Option 1: Path Component Validation (Recommended) + +Check for `..` only when it appears as a **complete path component**, not as part of the filename text. + +```python +import os +from pathlib import Path + +def validate_filename(filename: str) -> bool: + """ + Validate filename for path traversal attacks. + + Returns True if safe, raises ValidationError if dangerous. + """ + # Normalize the path + normalized = os.path.normpath(filename) + + # Check if normalization changed the path (indicates traversal) + if normalized != filename: + raise ValidationError(f"Path traversal detected: {filename}") + + # Check for absolute paths + if os.path.isabs(filename): + raise ValidationError(f"Absolute paths not allowed: {filename}") + + # Split into components and check for parent directory references + parts = Path(filename).parts + if ".." in parts: + raise ValidationError(f"Parent directory references not allowed: {filename}") + + # Check for any path separators (should be basename only) + if os.sep in filename or (os.altsep and os.altsep in filename): + raise ValidationError(f"Path separators not allowed: {filename}") + + return True + +# Examples: +validate_filename("video.mp4") # ✓ PASS +validate_filename("video...mp4") # ✓ PASS (ellipsis) +validate_filename("This is... a video.m4a") # ✓ PASS +validate_filename("../../../etc/passwd") # ✗ FAIL (traversal) +validate_filename("dir/../file.mp4") # ✗ FAIL (traversal) +validate_filename("/etc/passwd") # ✗ FAIL (absolute) +``` + +### Option 2: Basename-Only Validation (Simpler) + +Only accept basenames (no directory components at all): + +```python +import os + +def validate_filename(filename: str) -> bool: + """ + Ensure filename contains no path components. + """ + # Extract basename + basename = os.path.basename(filename) + + # Must match original (no path components) + if basename != filename: + raise ValidationError(f"Filename must not contain path components: {filename}") + + # Additional check: no path separators + if "/" in filename or "\\" in filename: + raise ValidationError(f"Path separators not allowed: {filename}") + + return True + +# Examples: +validate_filename("video.mp4") # ✓ PASS +validate_filename("video...mp4") # ✓ PASS +validate_filename("../file.mp4") # ✗ FAIL +validate_filename("dir/file.mp4") # ✗ FAIL +``` + +### Option 3: Regex Pattern Matching (Most Strict) + +Use a whitelist approach for allowed characters: + +```python +import re + +def validate_filename(filename: str) -> bool: + """ + Validate filename using whitelist of safe characters. + """ + # Allow: letters, numbers, spaces, dots, hyphens, underscores + # Length: 1-255 characters + pattern = r'^[a-zA-Z0-9 .\-_]{1,255}\.[a-zA-Z0-9]{2,10}$' + + if not re.match(pattern, filename): + raise ValidationError(f"Invalid filename format: {filename}") + + # Additional safety: reject if starts/ends with dot + if filename.startswith('.') or filename.endswith('.'): + raise ValidationError(f"Filename cannot start or end with dot: {filename}") + + return True + +# Examples: +validate_filename("video.mp4") # ✓ PASS +validate_filename("video...mp4") # ✓ PASS +validate_filename("My Video... Part 2.m4a") # ✓ PASS +validate_filename("../file.mp4") # ✗ FAIL (starts with ..) +validate_filename("file<>.mp4") # ✗ FAIL (invalid chars) +``` + +## Implementation Steps + +### 1. Locate Current Validation Code + +Search for files containing the validation logic: + +```bash +grep -r "Path traversal" /path/to/transcriptor-api +grep -r '".."' /path/to/transcriptor-api +grep -r "normpath\|basename" /path/to/transcriptor-api +``` + +### 2. Update Validation Function + +Replace the current naive check with one of the recommended solutions above. + +**Priority Order:** +1. **Option 1** (Path Component Validation) - Best security/usability balance +2. **Option 2** (Basename-Only) - Simplest, very secure +3. **Option 3** (Regex) - Most restrictive, may reject valid files + +### 3. Test Cases + +Create comprehensive test suite: + +```python +import pytest + +def test_valid_filenames(): + """Test filenames that should be accepted.""" + valid_names = [ + "video.mp4", + "audio.m4a", + "This is... a test.mp4", + "Part 1... Part 2.wav", + "video...multiple...dots.mp3", + "My-Video_2024.mp4", + "song (remix).m4a", + ] + + for filename in valid_names: + assert validate_filename(filename), f"Should accept: {filename}" + +def test_dangerous_filenames(): + """Test filenames that should be rejected.""" + dangerous_names = [ + "../../../etc/passwd", + "../../secrets.txt", + "../file.mp4", + "/etc/passwd", + "C:\\Windows\\System32\\file.txt", + "dir/../file.mp4", + "file/../../etc/passwd", + ] + + for filename in dangerous_names: + with pytest.raises(ValidationError): + validate_filename(filename) + +def test_edge_cases(): + """Test edge cases.""" + edge_cases = [ + (".", False), # Current directory + ("..", False), # Parent directory + ("...", True), # Just dots (valid) + ("....", True), # Multiple dots (valid) + (".hidden.mp4", True), # Hidden file (valid on Unix) + ("", False), # Empty string + ("a" * 256, False), # Too long + ] + + for filename, should_pass in edge_cases: + if should_pass: + assert validate_filename(filename) + else: + with pytest.raises(ValidationError): + validate_filename(filename) +``` + +### 4. Update Error Response + +Provide clearer error messages: + +```python +# BAD (current) +{"detail": {"error": "Upload failed", "message": "Audio file validation failed: Path traversal (..) is not allowed"}} + +# GOOD (improved) +{ + "detail": { + "error": "Invalid filename", + "message": "Filename contains path traversal characters. Please use only the filename without directory paths.", + "filename": "../../etc/passwd", + "suggestion": "Use: passwd.txt" + } +} +``` + +## Testing the Fix + +### Manual Testing + +1. **Test with problematic filename from bug report:** + ```bash + curl -X POST http://192.168.1.210:33767/transcribe \ + -F "file=@/path/to/This Weird FPV Drone only takes one kind of Battery... Rekon 35 V2.m4a" \ + -F "model=medium" + ``` + Expected: HTTP 200 (success) + +2. **Test with actual path traversal:** + ```bash + curl -X POST http://192.168.1.210:33767/transcribe \ + -F "file=@/tmp/test.m4a;filename=../../etc/passwd" \ + -F "model=medium" + ``` + Expected: HTTP 400 (validation error) + +3. **Test with various ellipsis patterns:** + - `"video...mp4"` → Should pass + - `"Part 1... Part 2.m4a"` → Should pass + - `"Wait... what!.mp4"` → Should pass + +### Automated Testing + +```python +# integration_test.py +import requests + +def test_ellipsis_filenames(): + """Test files with ellipsis in names.""" + test_cases = [ + "video...mp4", + "This is... a test.m4a", + "Wait... what.mp3", + ] + + for filename in test_cases: + response = requests.post( + "http://192.168.1.210:33767/transcribe", + files={"file": (filename, open("test_audio.m4a", "rb"))}, + data={"model": "medium"} + ) + assert response.status_code == 200, f"Failed for: {filename}" +``` + +## Security Considerations + +### What We're Protecting Against + +1. **Path Traversal**: `../../../sensitive/file` +2. **Absolute Paths**: `/etc/passwd` or `C:\Windows\System32\` +3. **Hidden Paths**: `./.git/config` + +### What We're NOT Breaking + +1. **Ellipsis in titles**: `"Wait... what.mp4"` +2. **Multiple extensions**: `"file.tar.gz"` +3. **Special characters**: `"My Video (2024).mp4"` + +### Additional Hardening (Optional) + +```python +def sanitize_and_validate_filename(filename: str) -> str: + """ + Sanitize filename and validate for safety. + Returns cleaned filename or raises error. + """ + # Remove null bytes + filename = filename.replace("\0", "") + + # Extract basename (strips any path components) + filename = os.path.basename(filename) + + # Limit length + max_length = 255 + if len(filename) > max_length: + name, ext = os.path.splitext(filename) + filename = name[:max_length-len(ext)] + ext + + # Validate + validate_filename(filename) + + return filename +``` + +## Deployment Checklist + +- [ ] Update validation function with recommended fix +- [ ] Add comprehensive test suite +- [ ] Test with real-world filenames (including bug report case) +- [ ] Test security: attempt path traversal attacks +- [ ] Update API documentation +- [ ] Review error messages for clarity +- [ ] Deploy to staging environment +- [ ] Run integration tests +- [ ] Monitor logs for validation failures +- [ ] Deploy to production +- [ ] Verify bug reporter's file now works + +## Contact & Context + +**Bug Report Date**: 2025-10-26 +**Affected Endpoint**: `POST /transcribe` +**Error Code**: HTTP 500 +**Client Application**: yt-dlp-webui v3 + +**Example Failing Request:** +``` +POST http://192.168.1.210:33767/transcribe +Content-Type: multipart/form-data + +file: "This Weird FPV Drone only takes one kind of Battery... Rekon 35 V2.m4a" +model: "medium" +``` + +**Current Behavior**: Returns 500 error with path traversal message +**Expected Behavior**: Accepts file and processes transcription + +--- + +## Quick Reference + +### Files to Check +- `/path/to/api/validators.py` or similar +- `/path/to/api/upload_handler.py` +- `/path/to/api/routes/transcribe.py` + +### Search Commands +```bash +# Find validation code +rg "Path traversal" --type py +rg '"\.\."' --type py +rg "ValidationError.*filename" --type py + +# Find upload handlers +rg "def.*upload|def.*transcribe" --type py +``` + +### Priority Fix +Use **Option 1 (Path Component Validation)** - it provides the best balance of security and usability. diff --git a/docker-build.sh b/docker-build.sh new file mode 100755 index 0000000..45421ab --- /dev/null +++ b/docker-build.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +datetime_prefix() { + date "+[%Y-%m-%d %H:%M:%S]" +} + +echo "$(datetime_prefix) Building Whisper Transcriptor Docker image..." + +# Build the Docker image +docker build -t transcriptor-apimcp:latest . + +echo "$(datetime_prefix) Build complete!" +echo "$(datetime_prefix) Image: transcriptor-apimcp:latest" +echo "" +echo "Usage:" +echo " API mode: ./docker-run-api.sh" +echo " MCP mode: ./docker-run-mcp.sh" +echo " Or use: docker-compose up transcriptor-api" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..43dbf68 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,106 @@ +version: '3.8' + +services: + # API Server mode with nginx reverse proxy + transcriptor-api: + build: + context: . + dockerfile: Dockerfile + image: transcriptor-apimcp:latest + container_name: transcriptor-api + runtime: nvidia + environment: + NVIDIA_VISIBLE_DEVICES: "0" + NVIDIA_DRIVER_CAPABILITIES: compute,utility + SERVER_MODE: api + API_HOST: 127.0.0.1 + API_PORT: 33767 + WHISPER_MODEL_DIR: /models + TRANSCRIPTION_OUTPUT_DIR: /outputs + TRANSCRIPTION_BATCH_OUTPUT_DIR: /outputs/batch + TRANSCRIPTION_MODEL: large-v3 + TRANSCRIPTION_DEVICE: auto + TRANSCRIPTION_COMPUTE_TYPE: auto + TRANSCRIPTION_OUTPUT_FORMAT: txt + TRANSCRIPTION_BEAM_SIZE: 5 + TRANSCRIPTION_TEMPERATURE: 0.0 + JOB_QUEUE_MAX_SIZE: 5 + JOB_METADATA_DIR: /outputs/jobs + JOB_RETENTION_DAYS: 7 + GPU_HEALTH_CHECK_ENABLED: "true" + GPU_HEALTH_CHECK_INTERVAL_MINUTES: 10 + GPU_HEALTH_TEST_MODEL: tiny + GPU_HEALTH_TEST_AUDIO: /test-audio/test.mp3 + GPU_RESET_COOLDOWN_MINUTES: 5 + # Optional proxy settings (uncomment if needed) + # HTTP_PROXY: http://192.168.1.212:8080 + # HTTPS_PROXY: http://192.168.1.212:8080 + ports: + - "33767:80" # Map host:33767 to container nginx:80 + volumes: + - /home/uad/agents/tools/mcp-transcriptor/models:/models + - /home/uad/agents/tools/mcp-transcriptor/outputs:/outputs + - /home/uad/agents/tools/mcp-transcriptor/logs:/logs + - /home/uad/agents/tools/mcp-transcriptor/data/test.mp3:/test-audio/test.mp3:ro + - /etc/localtime:/etc/localtime:ro # Sync container time with host + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + restart: unless-stopped + networks: + - transcriptor-network + + # MCP Server mode (stdio based) + transcriptor-mcp: + build: + context: . + dockerfile: Dockerfile + image: transcriptor-apimcp:latest + container_name: transcriptor-mcp + environment: + SERVER_MODE: mcp + WHISPER_MODEL_DIR: /models + TRANSCRIPTION_OUTPUT_DIR: /outputs + TRANSCRIPTION_BATCH_OUTPUT_DIR: /outputs/batch + TRANSCRIPTION_MODEL: large-v3 + TRANSCRIPTION_DEVICE: auto + TRANSCRIPTION_COMPUTE_TYPE: auto + TRANSCRIPTION_OUTPUT_FORMAT: txt + TRANSCRIPTION_BEAM_SIZE: 5 + TRANSCRIPTION_TEMPERATURE: 0.0 + JOB_QUEUE_MAX_SIZE: 100 + JOB_METADATA_DIR: /outputs/jobs + JOB_RETENTION_DAYS: 7 + GPU_HEALTH_CHECK_ENABLED: "true" + GPU_HEALTH_CHECK_INTERVAL_MINUTES: 10 + GPU_HEALTH_TEST_MODEL: tiny + GPU_RESET_COOLDOWN_MINUTES: 5 + # Optional proxy settings (uncomment if needed) + # HTTP_PROXY: http://192.168.1.212:8080 + # HTTPS_PROXY: http://192.168.1.212:8080 + volumes: + - /home/uad/agents/tools/mcp-transcriptor/models:/models + - /home/uad/agents/tools/mcp-transcriptor/outputs:/outputs + - /home/uad/agents/tools/mcp-transcriptor/logs:/logs + - /etc/localtime:/etc/localtime:ro + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + stdin_open: true # Enable stdin for MCP stdio mode + tty: true + restart: unless-stopped + networks: + - transcriptor-network + profiles: + - mcp # Only start when explicitly requested + +networks: + transcriptor-network: + driver: bridge diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..039169c --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -e + +# Docker Entrypoint Script for Whisper Transcriptor +# Supports both MCP and API server modes + +datetime_prefix() { + date "+[%Y-%m-%d %H:%M:%S]" +} + +echo "$(datetime_prefix) Starting Whisper Transcriptor in ${SERVER_MODE} mode..." + +# Ensure required directories exist +mkdir -p "$WHISPER_MODEL_DIR" +mkdir -p "$TRANSCRIPTION_OUTPUT_DIR" +mkdir -p "$TRANSCRIPTION_BATCH_OUTPUT_DIR" +mkdir -p "$JOB_METADATA_DIR" +mkdir -p /app/outputs/uploads + +# Display GPU information +if command -v nvidia-smi &> /dev/null; then + echo "$(datetime_prefix) GPU Information:" + nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader +else + echo "$(datetime_prefix) Warning: nvidia-smi not found. GPU may not be available." +fi + +# Check server mode and start appropriate service +case "${SERVER_MODE}" in + "api") + echo "$(datetime_prefix) Starting API Server mode with nginx reverse proxy" + + # Update nginx configuration to use correct backend + sed -i "s/server 127.0.0.1:33767;/server ${API_HOST}:${API_PORT};/" /etc/nginx/sites-available/transcriptor.conf + + # Enable nginx site + ln -sf /etc/nginx/sites-available/transcriptor.conf /etc/nginx/sites-enabled/ + rm -f /etc/nginx/sites-enabled/default + + # Test nginx configuration + echo "$(datetime_prefix) Testing nginx configuration..." + nginx -t + + # Start nginx in background + echo "$(datetime_prefix) Starting nginx..." + nginx + + # Start API server (foreground - this keeps container running) + echo "$(datetime_prefix) Starting API server on ${API_HOST}:${API_PORT}" + echo "$(datetime_prefix) API accessible via nginx on port 80" + exec python -u /app/src/servers/api_server.py + ;; + + "mcp") + echo "$(datetime_prefix) Starting MCP Server mode (stdio)" + echo "$(datetime_prefix) Model directory: $WHISPER_MODEL_DIR" + + # Start MCP server in stdio mode + exec python -u /app/src/servers/whisper_server.py + ;; + + *) + echo "$(datetime_prefix) ERROR: Invalid SERVER_MODE: ${SERVER_MODE}" + echo "$(datetime_prefix) Valid modes: 'api' or 'mcp'" + exit 1 + ;; +esac diff --git a/docker-run-api.sh b/docker-run-api.sh new file mode 100755 index 0000000..77cbb08 --- /dev/null +++ b/docker-run-api.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -e + +datetime_prefix() { + date "+[%Y-%m-%d %H:%M:%S]" +} + +echo "$(datetime_prefix) Starting Whisper Transcriptor in API mode with nginx..." + +# Check if image exists +if ! docker image inspect transcriptor-apimcp:latest &> /dev/null; then + echo "$(datetime_prefix) Image not found. Building first..." + ./docker-build.sh +fi + +# Stop and remove existing container if running +if docker ps -a --format '{{.Names}}' | grep -q '^transcriptor-api$'; then + echo "$(datetime_prefix) Stopping existing container..." + docker stop transcriptor-api || true + docker rm transcriptor-api || true +fi + +# Run the container in API mode +docker run -d \ + --name transcriptor-api \ + --gpus all \ + -p 33767:80 \ + -e SERVER_MODE=api \ + -e API_HOST=127.0.0.1 \ + -e API_PORT=33767 \ + -e CUDA_VISIBLE_DEVICES=0 \ + -e TRANSCRIPTION_MODEL=large-v3 \ + -e TRANSCRIPTION_DEVICE=auto \ + -e TRANSCRIPTION_COMPUTE_TYPE=auto \ + -e JOB_QUEUE_MAX_SIZE=5 \ + -v "$(pwd)/models:/models" \ + -v "$(pwd)/outputs:/outputs" \ + -v "$(pwd)/logs:/logs" \ + --restart unless-stopped \ + transcriptor-apimcp:latest + +echo "$(datetime_prefix) Container started!" +echo "" +echo "API Server running at: http://localhost:33767" +echo "" +echo "Useful commands:" +echo " Check logs: docker logs -f transcriptor-api" +echo " Check status: docker ps | grep transcriptor-api" +echo " Test health: curl http://localhost:33767/health" +echo " Test GPU: curl http://localhost:33767/health/gpu" +echo " Stop container: docker stop transcriptor-api" +echo " Restart: docker restart transcriptor-api" +echo "" +echo "$(datetime_prefix) Waiting for service to start..." +sleep 5 + +# Test health endpoint +if curl -s http://localhost:33767/health > /dev/null 2>&1; then + echo "$(datetime_prefix) ✓ Service is healthy!" +else + echo "$(datetime_prefix) ⚠ Service not responding yet. Check logs with: docker logs transcriptor-api" +fi diff --git a/docker-run-mcp.sh b/docker-run-mcp.sh new file mode 100755 index 0000000..dd51cf0 --- /dev/null +++ b/docker-run-mcp.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -e + +datetime_prefix() { + date "+[%Y-%m-%d %H:%M:%S]" +} + +echo "$(datetime_prefix) Starting Whisper Transcriptor in MCP mode..." + +# Check if image exists +if ! docker image inspect transcriptor-apimcp:latest &> /dev/null; then + echo "$(datetime_prefix) Image not found. Building first..." + ./docker-build.sh +fi + +# Stop and remove existing container if running +if docker ps -a --format '{{.Names}}' | grep -q '^transcriptor-mcp$'; then + echo "$(datetime_prefix) Stopping existing container..." + docker stop transcriptor-mcp || true + docker rm transcriptor-mcp || true +fi + +# Run the container in MCP mode (interactive stdio) +echo "$(datetime_prefix) Starting MCP server in stdio mode..." +echo "$(datetime_prefix) Press Ctrl+C to stop" +echo "" + +docker run -it --rm \ + --name transcriptor-mcp \ + --gpus all \ + -e SERVER_MODE=mcp \ + -e CUDA_VISIBLE_DEVICES=0 \ + -e TRANSCRIPTION_MODEL=large-v3 \ + -e TRANSCRIPTION_DEVICE=auto \ + -e TRANSCRIPTION_COMPUTE_TYPE=auto \ + -e JOB_QUEUE_MAX_SIZE=100 \ + -v "$(pwd)/models:/models" \ + -v "$(pwd)/outputs:/outputs" \ + -v "$(pwd)/logs:/logs" \ + transcriptor-apimcp:latest diff --git a/reset_gpu.sh b/reset_gpu.sh index d366361..9ddd33e 100755 --- a/reset_gpu.sh +++ b/reset_gpu.sh @@ -2,12 +2,28 @@ # Script to reset NVIDIA GPU drivers without rebooting # This reloads kernel modules and restarts nvidia-persistenced service +# Also handles stopping/starting Ollama to release GPU resources echo "============================================================" echo "NVIDIA GPU Driver Reset Script" echo "============================================================" echo "" +# Stop Ollama via supervisorctl +echo "Stopping Ollama service..." +sudo supervisorctl stop ollama 2>/dev/null +if [ $? -eq 0 ]; then + echo "✓ Ollama stopped via supervisorctl" + OLLAMA_WAS_RUNNING=true +else + echo " Ollama not running or supervisorctl not available" + OLLAMA_WAS_RUNNING=false +fi +echo "" + +# Give Ollama time to release GPU resources +sleep 2 + # Stop nvidia-persistenced service echo "Stopping nvidia-persistenced service..." sudo systemctl stop nvidia-persistenced @@ -65,6 +81,18 @@ else fi echo "" +# Restart Ollama if it was running +if [ "$OLLAMA_WAS_RUNNING" = true ]; then + echo "Restarting Ollama service..." + sudo supervisorctl start ollama + if [ $? -eq 0 ]; then + echo "✓ Ollama restarted" + else + echo "✗ Failed to restart Ollama" + fi + echo "" +fi + echo "============================================================" echo "GPU driver reset completed successfully" echo "============================================================" diff --git a/src/core/gpu_health.py b/src/core/gpu_health.py index 4cbe00d..67cacc2 100644 --- a/src/core/gpu_health.py +++ b/src/core/gpu_health.py @@ -6,6 +6,7 @@ with strict failure handling to prevent silent CPU fallbacks. Includes circuit breaker pattern to prevent repeated failed checks. """ +import os import time import logging import threading @@ -14,7 +15,6 @@ from datetime import datetime from typing import Optional, List import torch -from utils.test_audio_generator import generate_test_audio from utils.circuit_breaker import CircuitBreaker, CircuitBreakerOpen logger = logging.getLogger(__name__) @@ -109,8 +109,18 @@ def _check_gpu_health_internal(expected_device: str = "auto") -> GPUHealthStatus logger.warning(f"Failed to get GPU info: {e}") try: - # Generate test audio - test_audio_path = generate_test_audio(duration_seconds=1.0) + # Get test audio path from environment variable + test_audio_path = os.getenv("GPU_HEALTH_TEST_AUDIO") + + if not test_audio_path: + raise ValueError("GPU_HEALTH_TEST_AUDIO environment variable not set") + + # Verify test audio file exists + if not os.path.exists(test_audio_path): + raise FileNotFoundError( + f"Test audio file not found: {test_audio_path}. " + f"Please ensure test audio exists before running GPU health checks." + ) # Import here to avoid circular dependencies from faster_whisper import WhisperModel @@ -129,6 +139,7 @@ def _check_gpu_health_internal(expected_device: str = "auto") -> GPUHealthStatus gpu_memory_before = torch.cuda.memory_allocated(0) # Load tiny model and transcribe + model = None try: model = WhisperModel( "tiny", @@ -140,7 +151,7 @@ def _check_gpu_health_internal(expected_device: str = "auto") -> GPUHealthStatus segments, info = model.transcribe(test_audio_path, beam_size=1) # Consume segments (needed to actually run inference) - list(segments) + segments_list = list(segments) # Check if GPU was actually used # faster-whisper uses CTranslate2 which manages GPU memory separately @@ -156,6 +167,19 @@ def _check_gpu_health_internal(expected_device: str = "auto") -> GPUHealthStatus actual_device = "cpu" gpu_working = False logger.error(f"GPU health check failed: {error_msg}") + finally: + # Clean up model resources to prevent GPU memory leak + if model is not None: + try: + del model + segments_list = None + # Force garbage collection and empty CUDA cache + import gc + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + except Exception as cleanup_error: + logger.warning(f"Error cleaning up GPU health check model: {cleanup_error}") except Exception as e: error_msg = f"Health check setup failed: {str(e)}" diff --git a/src/core/transcriber.py b/src/core/transcriber.py index c0d96d7..c7d3d7e 100644 --- a/src/core/transcriber.py +++ b/src/core/transcriber.py @@ -129,6 +129,9 @@ def transcribe_audio( logger.info("Using standard model for transcription...") segments, info = model_instance['model'].transcribe(audio_source, **options) + # Convert segments generator to list to release model resources + segments = list(segments) + # Determine output directory and path early audio_dir = os.path.dirname(audio_path) audio_filename = os.path.splitext(os.path.basename(audio_path))[0] @@ -253,6 +256,19 @@ def transcribe_audio( logger.error(f"Transcription failed: {str(e)}") return f"Error occurred during transcription: {str(e)}" + finally: + # Force GPU memory cleanup after transcription to prevent accumulation + if device == "cuda": + import torch + import gc + # Clear segments list to free memory + segments = None + # Force garbage collection + gc.collect() + # Empty CUDA cache + torch.cuda.empty_cache() + logger.debug("GPU memory cleaned up after transcription") + def batch_transcribe( audio_folder: str, diff --git a/src/servers/api_server.py b/src/servers/api_server.py index 3259892..d8d8522 100644 --- a/src/servers/api_server.py +++ b/src/servers/api_server.py @@ -33,7 +33,8 @@ from utils.input_validation import ( validate_model_name, validate_device, validate_compute_type, - validate_output_format + validate_output_format, + validate_filename_safe ) # Logging configuration @@ -218,6 +219,8 @@ async def transcribe_upload( try: # Validate form parameters early try: + # Validate filename for security (basename-only, no path traversal) + validate_filename_safe(file.filename) model = validate_model_name(model) output_format = validate_output_format(output_format) beam_size = validate_beam_size(beam_size) @@ -663,9 +666,11 @@ if __name__ == "__main__": # Perform startup GPU health check from utils.startup import perform_startup_gpu_check + # Disable auto_reset in Docker (sudo not available, GPU reset won't work) + in_docker = os.path.exists('/.dockerenv') perform_startup_gpu_check( required_device="cuda", - auto_reset=True, + auto_reset=not in_docker, exit_on_failure=True ) diff --git a/src/utils/input_validation.py b/src/utils/input_validation.py index 5daabfd..f4577cf 100644 --- a/src/utils/input_validation.py +++ b/src/utils/input_validation.py @@ -88,6 +88,72 @@ def sanitize_error_message(error_msg: str, sanitize_paths: bool = True) -> str: return sanitized +def validate_filename_safe(filename: str) -> str: + """ + Validate uploaded filename for security (basename-only validation). + + This function is specifically for validating uploaded filenames to ensure + they don't contain path traversal attempts. It enforces that the filename: + - Contains no directory separators (/, \) + - Has no path components (must be basename only) + - Contains no null bytes + - Has a valid audio file extension + + Args: + filename: Filename to validate (should be basename only, not full path) + + Returns: + Validated filename (unchanged if valid) + + Raises: + ValidationError: If filename is invalid or empty + PathTraversalError: If filename contains path components or traversal attempts + InvalidFileTypeError: If file extension is not allowed + + Examples: + validate_filename_safe("video.mp4") # ✓ PASS + validate_filename_safe("audio...mp3") # ✓ PASS (ellipsis OK) + validate_filename_safe("Wait... what.m4a") # ✓ PASS + validate_filename_safe("../../../etc/passwd") # ✗ FAIL (traversal) + validate_filename_safe("dir/file.mp4") # ✗ FAIL (path separator) + validate_filename_safe("/etc/passwd") # ✗ FAIL (absolute path) + """ + if not filename: + raise ValidationError("Filename cannot be empty") + + # Check for null bytes + if "\x00" in filename: + logger.warning(f"Null byte in filename detected: {filename}") + raise PathTraversalError("Null bytes in filename are not allowed") + + # Extract basename - if it differs from original, filename contained path components + basename = os.path.basename(filename) + if basename != filename: + logger.warning(f"Filename contains path components: {filename}") + raise PathTraversalError( + "Filename must not contain path components. " + f"Use only the filename: {basename}" + ) + + # Additional check: explicitly reject any path separators + if "/" in filename or "\\" in filename: + logger.warning(f"Path separators in filename: {filename}") + raise PathTraversalError("Path separators (/ or \\) are not allowed in filename") + + # Check file extension (case-insensitive) + file_ext = Path(filename).suffix.lower() + if not file_ext: + raise InvalidFileTypeError("Filename must have a file extension") + + if file_ext not in ALLOWED_AUDIO_EXTENSIONS: + raise InvalidFileTypeError( + f"Unsupported audio format: {file_ext}. " + f"Supported: {', '.join(sorted(ALLOWED_AUDIO_EXTENSIONS))}" + ) + + return filename + + def validate_path_safe(file_path: str, allowed_dirs: Optional[List[str]] = None) -> Path: """ Validate and sanitize a file path to prevent directory traversal attacks. diff --git a/supervisor/transcriptor-api.conf b/supervisor/transcriptor-api.conf deleted file mode 100644 index 577e3c3..0000000 --- a/supervisor/transcriptor-api.conf +++ /dev/null @@ -1,26 +0,0 @@ -[program:transcriptor-api] -command=/home/uad/agents/tools/mcp-transcriptor/venv/bin/python /home/uad/agents/tools/mcp-transcriptor/src/servers/api_server.py -directory=/home/uad/agents/tools/mcp-transcriptor -user=uad -autostart=true -autorestart=true -redirect_stderr=true -stdout_logfile=/home/uad/agents/tools/mcp-transcriptor/logs/transcriptor-api.log -stdout_logfile_maxbytes=50MB -stdout_logfile_backups=10 -environment= - PYTHONPATH="/home/uad/agents/tools/mcp-transcriptor/src", - CUDA_VISIBLE_DEVICES="0", - API_HOST="0.0.0.0", - API_PORT="33767", - WHISPER_MODEL_DIR="/home/uad/agents/tools/mcp-transcriptor/models", - TRANSCRIPTION_OUTPUT_DIR="/home/uad/agents/tools/mcp-transcriptor/outputs", - TRANSCRIPTION_BATCH_OUTPUT_DIR="/home/uad/agents/tools/mcp-transcriptor/outputs/batch", - TRANSCRIPTION_MODEL="large-v3", - TRANSCRIPTION_DEVICE="auto", - TRANSCRIPTION_COMPUTE_TYPE="auto", - TRANSCRIPTION_OUTPUT_FORMAT="txt", - HTTP_PROXY=http://192.168.1.212:8080, - HTTPS_PROXY=http://192.168.1.212:8080 -stopwaitsecs=10 -stopsignal=TERM diff --git a/test.mp3 b/test.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0688ef7548f25a91e4317fc08ca3f346960ef3a6 GIT binary patch literal 56270 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 i3cS_-n{%}PdGu(s(ZON