diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..738e6776 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,108 @@ +name: Build and Push Docker Images + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + API_IMAGE_NAME: ${{ github.repository }}-api + WORKER_IMAGE_NAME: ${{ github.repository }}-worker + +jobs: + build-api: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for API image + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.API_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push API image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.api + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64 + + build-worker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Worker image + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.WORKER_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Worker image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.worker + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64 + build-args: | + INSTALL_EXTRAS=qualitative-voice,reports + PRELOAD_MODELS=true diff --git a/docker-compose.yml b/docker-compose.yml index 109592d0..7c3b2fdd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,12 +28,14 @@ services: retries: 5 api: - build: - context: . - dockerfile: docker/Dockerfile.api + # Pre-built image from GitHub Container Registry + image: ghcr.io/efficientai-tech/efficientai-api:latest + # For local development, uncomment below to build instead of pull: + # build: + # context: . + # dockerfile: docker/Dockerfile.api container_name: efficientai_api environment: - # These can be overridden by config.yml, but provide defaults for Docker networking DATABASE_URL: postgresql://${POSTGRES_USER:-efficientai}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-efficientai} REDIS_URL: redis://redis:6379/0 CELERY_BROKER_URL: redis://redis:6379/0 @@ -44,11 +46,9 @@ services: FRONTEND_DIR: /app/frontend/dist ENCRYPTION_KEY: ${ENCRYPTION_KEY:-} volumes: - - ./app:/app/app - ./uploads:/app/uploads - ./.encryption_key:/app/.encryption_key:ro - ./config.docker.yml:/app/config.yml:ro - # Note: Frontend is built into the image, not mounted as volume ports: - "8000:8000" depends_on: @@ -59,24 +59,25 @@ services: command: eai start --config /app/config.yml --host 0.0.0.0 --port 8000 --no-build-frontend worker: - build: - context: . - dockerfile: docker/Dockerfile.worker + # Pre-built image from GitHub Container Registry + image: ghcr.io/efficientai-tech/efficientai-worker:latest + # For local development, uncomment below to build instead of pull: + # build: + # context: . + # dockerfile: docker/Dockerfile.worker + # args: + # INSTALL_EXTRAS: "qualitative-voice,reports" + # PRELOAD_MODELS: "true" container_name: efficientai_worker # Use host network mode for WebRTC connectivity (Retell/Vapi calls) network_mode: host environment: - # When using host network, config.yml with localhost URLs works directly ENCRYPTION_KEY: ${ENCRYPTION_KEY:-} volumes: - - ./app:/app/app - ./uploads:/app/uploads - ./.encryption_key:/app/.encryption_key:ro - # Worker uses host network, so regular config.yml with localhost URLs works - ./config.yml:/app/config.yml:ro - # Note: depends_on doesn't work with network_mode: host, ensure db/redis are running command: eai worker --config /app/config.yml --loglevel info volumes: postgres_data: - diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api index fb85de8f..9badfd4e 100644 --- a/docker/Dockerfile.api +++ b/docker/Dockerfile.api @@ -16,31 +16,48 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv # Set working directory WORKDIR /app -# Copy dependency files and application code (needed for editable install) +# ============================================================ +# LAYER 1: Python dependencies (cached unless pyproject.toml changes) +# ============================================================ COPY pyproject.toml README.md ./ -COPY src/ ./src/ -COPY app/ ./app/ -COPY scripts/ ./scripts/ + +# Create minimal src structure for editable install +RUN mkdir -p src/efficientai && touch src/efficientai/__init__.py # Install Python dependencies -RUN uv pip install --system -e . +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=cache,target=/root/.cache/pip \ + uv pip install --system -e . -# Build frontend -# Copy package files first for better layer caching +# ============================================================ +# LAYER 2: Frontend dependencies (cached unless package.json changes) +# ============================================================ COPY frontend/package.json frontend/package-lock.json* ./frontend/ WORKDIR /app/frontend + # Install all dependencies (including dev dependencies needed for build) # Use --legacy-peer-deps to resolve peer dependency conflicts with HeroUI and Tailwind -RUN npm ci --legacy-peer-deps || npm install --legacy-peer-deps +RUN --mount=type=cache,target=/root/.npm \ + npm ci --legacy-peer-deps || npm install --legacy-peer-deps -# Copy frontend source files +# ============================================================ +# LAYER 3: Frontend build (rebuilds on frontend code changes) +# ============================================================ COPY frontend/ ./ # Build frontend for production RUN npm run build -# Return to app directory +# ============================================================ +# LAYER 4: Backend code (rebuilds on backend code changes) +# ============================================================ WORKDIR /app +COPY src/ ./src/ +COPY app/ ./app/ +COPY scripts/ ./scripts/ + +# Re-run editable install to link the actual code +RUN uv pip install --system -e . # Create uploads directory RUN mkdir -p /app/uploads @@ -50,4 +67,3 @@ EXPOSE 8000 # Run the application CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] - diff --git a/docker/Dockerfile.worker b/docker/Dockerfile.worker index 75c7947d..35339d4f 100644 --- a/docker/Dockerfile.worker +++ b/docker/Dockerfile.worker @@ -1,5 +1,9 @@ FROM python:3.11-slim +# Build arguments for configurable installs +ARG INSTALL_EXTRAS="qualitative-voice,reports" +ARG PRELOAD_MODELS="true" + # Install system dependencies (git needed for torch.hub clone, build-essential + # cmake needed for packages like praat-parselmouth on arm64/Apple Silicon) RUN apt-get update && apt-get install -y \ @@ -14,48 +18,55 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv # Set working directory WORKDIR /app -# Copy dependency files and application code (needed for editable install) +# ============================================================ +# LAYER 1: Dependencies (cached unless pyproject.toml changes) +# ============================================================ COPY pyproject.toml README.md ./ -COPY src/ ./src/ -COPY app/ ./app/ -COPY scripts/ ./scripts/ -# Install dependencies (qualitative-voice for evaluation metrics, nemo-asr for TTS hallucination detection) -RUN uv pip install --system -e ".[qualitative-voice,nemo-asr]" +# Create minimal src structure for editable install +RUN mkdir -p src/efficientai && touch src/efficientai/__init__.py + +# Install dependencies based on INSTALL_EXTRAS arg +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=cache,target=/root/.cache/pip \ + if [ -n "$INSTALL_EXTRAS" ]; then \ + uv pip install --system -e ".[$INSTALL_EXTRAS]"; \ + else \ + uv pip install --system -e .; \ + fi + +# ============================================================ +# LAYER 2: Pre-download ML models (cached, conditional) +# ============================================================ + +# Download qualitative-voice models if that extra is installed +RUN if [ "$PRELOAD_MODELS" = "true" ] && echo "$INSTALL_EXTRAS" | grep -q "qualitative-voice"; then \ + echo "Downloading UTMOS MOS predictor..." && \ + python -c "import torch; torch.hub.load('tarepan/SpeechMOS:v1.2.0', 'utmos22_strong', trust_repo=True); print('UTMOS cached')" && \ + echo "Downloading emotion classifier..." && \ + python -c "from transformers import pipeline; pipeline('audio-classification', model='ehcalabres/wav2vec2-lg-xlsr-en-speech-emotion-recognition'); print('Emotion classifier cached')" && \ + echo "Downloading valence/arousal model..." && \ + python -c "from transformers import AutoProcessor, AutoModelForAudioClassification; AutoProcessor.from_pretrained('audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim'); AutoModelForAudioClassification.from_pretrained('audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim'); print('Valence/arousal cached')"; \ + fi + +# Download NeMo ASR model if that extra is installed +RUN if [ "$PRELOAD_MODELS" = "true" ] && echo "$INSTALL_EXTRAS" | grep -q "nemo-asr"; then \ + echo "Downloading NeMo ASR model..." && \ + python -c "import nemo.collections.asr as nemo_asr; nemo_asr.models.ASRModel.from_pretrained('stt_en_conformer_ctc_large'); print('NeMo ASR cached')"; \ + fi # ============================================================ -# Pre-download qualitative metric models into the image so -# they are available immediately and not fetched at runtime. +# LAYER 3: Application code (rebuilds on code changes only) # ============================================================ +COPY src/ ./src/ +COPY app/ ./app/ +COPY scripts/ ./scripts/ -# 1. UTMOS MOS predictor (torch.hub -> /root/.cache/torch/hub/) -RUN python -c "\ -import torch; \ -model = torch.hub.load('tarepan/SpeechMOS:v1.2.0', 'utmos22_strong', trust_repo=True); \ -print('UTMOS MOS predictor cached successfully'); \ -" - -# 2. Emotion classifier + Valence/Arousal model (HuggingFace -> /root/.cache/huggingface/hub/) -RUN python -c "\ -from transformers import pipeline, AutoProcessor, AutoModelForAudioClassification; \ -pipe = pipeline('audio-classification', model='ehcalabres/wav2vec2-lg-xlsr-en-speech-emotion-recognition'); \ -print('Emotion classifier cached successfully'); \ -proc = AutoProcessor.from_pretrained('audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim'); \ -mdl = AutoModelForAudioClassification.from_pretrained('audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim'); \ -print('Valence/arousal model cached successfully'); \ -" - -# 3. NeMo Conformer CTC ASR model for TTS hallucination detection (WER/CER) -# Downloads from NVIDIA NGC -> /root/.cache/torch/NeMo/ -RUN python -c "\ -import nemo.collections.asr as nemo_asr; \ -model = nemo_asr.models.ASRModel.from_pretrained('stt_en_conformer_ctc_large'); \ -print('NeMo ASR model cached successfully'); \ -" +# Re-run editable install to link the actual code +RUN uv pip install --system -e . # Create uploads directory RUN mkdir -p /app/uploads # Run Celery worker CMD ["celery", "-A", "app.workers.celery_app", "worker", "--loglevel=info"] - diff --git a/pyproject.toml b/pyproject.toml index 75f980c6..714feb47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ dependencies = [ "redis>=5.0.0", "librosa>=0.10.0", "soundfile>=0.12.0", - "openai-whisper>=20231117", "pydub>=0.25.1", "python-dotenv>=1.0.0", "httpx>=0.25.0", @@ -28,7 +27,6 @@ dependencies = [ "pyyaml>=6.0.0", "aiofiles>=23.2.0", "boto3>=1.34.0", - "pipecat-ai[silero,websocket,google]>=0.0.82", "cartesia>=0.1.0", "deepgram-sdk>=3.0.0,<4.0.0", "loguru>=0.7.0", @@ -43,7 +41,6 @@ dependencies = [ "jiwer>=3.0.0", "litellm>=1.60.0", "jinja2>=3.1.0", - "weasyprint>=62.0", ] [project.optional-dependencies] @@ -67,6 +64,14 @@ cartesia = [ deepgram = [ "deepgram-sdk>=3.0.0,<4.0.0", ] +# Local Whisper transcription (requires PyTorch) - use OpenAI API instead for lighter install +local-whisper = [ + "openai-whisper>=20231117", +] +# PDF report generation +reports = [ + "weasyprint>=62.0", +] # Qualitative Voice AI Metrics (MOS, Emotion, Prosody) + Speaker Diarization qualitative-voice = [ "torch>=2.0.0",