Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -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
29 changes: 15 additions & 14 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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:

38 changes: 27 additions & 11 deletions docker/Dockerfile.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -50,4 +67,3 @@ EXPOSE 8000

# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

77 changes: 44 additions & 33 deletions docker/Dockerfile.worker
Original file line number Diff line number Diff line change
@@ -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 \
Expand All @@ -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"]

11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,13 @@ 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",
"click>=8.1.0",
"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",
Expand All @@ -43,7 +41,6 @@ dependencies = [
"jiwer>=3.0.0",
"litellm>=1.60.0",
"jinja2>=3.1.0",
"weasyprint>=62.0",
]

[project.optional-dependencies]
Expand All @@ -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",
Expand Down
Loading