Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5a9c033
feat: add GitHub Actions workflow for advanced Docker Compose build a…
0xrushi Oct 29, 2025
2d211f6
fix: improve .env creation logic and ensure correct image tagging in …
0xrushi Oct 29, 2025
c0186da
secrets
0xrushi Oct 29, 2025
4a65381
env
0xrushi Oct 29, 2025
5f8f498
space
0xrushi Oct 29, 2025
43c1f67
dea
0xrushi Oct 29, 2025
0eeea39
docker b
0xrushi Oct 29, 2025
0c386b2
fix: enhance error handling and JSON validation in Docker Compose wor…
0xrushi Oct 29, 2025
5db1410
refactor: update image retrieval method in Docker Compose workflow to…
0xrushi Oct 29, 2025
7bdc71c
refactor: update service names and tagging logic in Docker Compose wo…
0xrushi Oct 29, 2025
777b87c
refactor: enhance .env copying logic and add Docker Compose builds fo…
0xrushi Oct 29, 2025
d47a5e1
refactor: implement dynamic runner selection in GitHub Actions workflow
0xrushi Oct 29, 2025
ed4d667
fix: improve error handling for self-hosted runner selection in GitHu…
0xrushi Oct 29, 2025
da92136
fix: set working directory for runner display step in GitHub Actions …
0xrushi Oct 29, 2025
33355ad
fix: update error logging for self-hosted runner fallback in GitHub A…
0xrushi Oct 29, 2025
3ad9280
refactor: streamline GitHub Actions workflow by consolidating runner …
0xrushi Oct 29, 2025
ecbd16d
fix: correct loop termination in GitHub Actions workflow for Docker i…
0xrushi Oct 29, 2025
c6c631c
refactor: consolidate Docker Compose build steps and improve image ha…
0xrushi Oct 29, 2025
cd903f7
refactor: simplify service build configuration in GitHub Actions work…
0xrushi Oct 29, 2025
b58b781
fix: update image tag removal logging and adjust build condition in G…
0xrushi Oct 29, 2025
c17a3a1
Merge branch 'main' of github.com:0xrushi/friend-lite into feat/actions
0xrushi Nov 8, 2025
65ddb49
Update service URLs and enhance speaker-recognition setup
0xrushi Nov 8, 2025
257c7c1
cuda variants
0xrushi Nov 9, 2025
4d968e0
Refactor GitHub Actions workflow and enhance audio stream worker shut…
0xrushi Nov 9, 2025
e0ef7f4
Update service URLs for speaker recognition and Nginx configuration
0xrushi Nov 9, 2025
7077964
Add health status checker and update setup instructions
AnkushMalaker Nov 18, 2025
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
248 changes: 248 additions & 0 deletions .github/workflows/advanced-docker-compose-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
name: Build and Deploy Advanced (Docker Compose)

on:
workflow_dispatch:
inputs:
version:
description: Optional version tag override (e.g. v1.2.3)
required: false
push:
branches: [ "main" ]
paths:
- "*"
- "backends/advanced/**"
- "extras/asr-services/**"
- "extras/speaker-recognition/**"
- "extras/openmemory-mcp/**"
- ".github/workflows/advanced-docker-compose-build.yml"
tags:
- "v*"


permissions:
contents: read
packages: write
actions: read

env:
REGISTRY: ghcr.io

jobs:
build-default:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
ADVANCED_ENV: ${{ secrets.ADVANCED_ENV }}
RUNNER_FLAVOUR: ubuntu-latest
defaults:
run:
shell: bash
working-directory: backends/advanced

steps:
- name: Show selected runner
run: echo "Workflow running on ${RUNNER_FLAVOUR} runner"
working-directory: .

- name: Checkout
uses: actions/checkout@v4

- name: Print commit details
run: |
echo "Event: ${{ github.event_name }}"
echo "Ref: $GITHUB_REF"
echo "Ref name: ${{ github.ref_name }}"
echo "Repository: $GITHUB_REPOSITORY"
echo "Actor: $GITHUB_ACTOR"
echo "SHA: $GITHUB_SHA"
echo "Short SHA: ${GITHUB_SHA::7}"
echo "Commit info:"
git log -1 --pretty=format:'Author: %an <%ae>%nDate: %ad%nSubject: %s' || true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Copy .env.template to .env
run: |
set -euo pipefail
copy_env() {
local dir="$1"
local template="${dir}/.env.template"
local target="${dir}/.env"
if [ -f "$template" ]; then
echo "Copying $template to $target"
cp "$template" "$target"
else
echo "$template not found; skipping"
fi
}

copy_env .
copy_env ../../extras/asr-services
copy_env ../../extras/speaker-recognition
copy_env ../../extras/openmemory-mcp

- name: Create .env from secret (if provided)
if: env.ADVANCED_ENV != ''
run: |
echo "Writing .env from ADVANCED_ENV secret"
printf "%s\n" "${ADVANCED_ENV}" > .env

- name: Source .env (if present)
run: |
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
else
echo ".env not found; continuing"
fi

- name: Determine version
id: version
run: |
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
else
VERSION="sha-${GITHUB_SHA::7}"
fi
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"

- name: Build, tag, and push services sequentially with version
env:
OWNER: ${{ github.repository_owner }}
VERSION: ${{ steps.version.outputs.VERSION }}
run: |
set -euo pipefail
docker compose version
OWNER_LC=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')

# CUDA variants from pyproject.toml
CUDA_VARIANTS=("cpu" "cu121" "cu126" "cu128")

# Base services (no CUDA variants, no profiles)
base_service_specs=(
"friend-backend|advanced-friend-backend|docker-compose.yml|."
"workers|advanced-workers|docker-compose.yml|."
"webui|advanced-webui|docker-compose.yml|."
"openmemory-mcp|openmemory-mcp|../../extras/openmemory-mcp/docker-compose.yml|../../extras/openmemory-mcp"
)

# Build and push base services
for spec in "${base_service_specs[@]}"; do
IFS='|' read -r svc svc_repo compose_file project_dir <<< "$spec"

echo "::group::Building and pushing $svc_repo"
if [ "$compose_file" = "docker-compose.yml" ] && [ "$project_dir" = "." ]; then
docker compose build --pull "$svc"
else
docker compose -f "$compose_file" --project-directory "$project_dir" build "$svc"
fi
# Resolve the built image ID via compose (avoids name mismatches)
if [ "$compose_file" = "docker-compose.yml" ] && [ "$project_dir" = "." ]; then
img_id=$(docker compose images -q "$svc" | head -n1)
else
img_id=$(docker compose -f "$compose_file" --project-directory "$project_dir" images -q "$svc" | head -n1)
fi
if [ -z "${img_id:-}" ]; then
echo "Skipping $svc_repo (no built image found after build)"
echo "::endgroup::"
continue
fi

# Tag and push with version
target_image="$REGISTRY/$OWNER_LC/$svc_repo:$VERSION"
latest_image="$REGISTRY/$OWNER_LC/$svc_repo:latest"
echo "Tagging $img_id as $target_image"
docker tag "$img_id" "$target_image"
echo "Tagging $img_id as $latest_image"
docker tag "$img_id" "$latest_image"

echo "Pushing $target_image"
docker push "$target_image"
echo "Pushing $latest_image"
docker push "$latest_image"

# Clean up local tags
docker image rm -f "$target_image" || true
docker image rm -f "$latest_image" || true
echo "::endgroup::"
done

# Build and push parakeet-asr with CUDA variants (cu121, cu126, cu128)
echo "::group::Building and pushing parakeet-asr CUDA variants"
cd ../../extras/asr-services
for cuda_variant in cu121 cu126 cu128; do
echo "Building parakeet-asr-${cuda_variant}"
export CUDA_VERSION="${cuda_variant}"
docker compose build parakeet-asr

img_id=$(docker compose images -q parakeet-asr | head -n1)
if [ -n "${img_id:-}" ]; then
target_image="$REGISTRY/$OWNER_LC/parakeet-asr-${cuda_variant}:$VERSION"
latest_image="$REGISTRY/$OWNER_LC/parakeet-asr-${cuda_variant}:latest"
echo "Tagging $img_id as $target_image"
docker tag "$img_id" "$target_image"
echo "Tagging $img_id as $latest_image"
docker tag "$img_id" "$latest_image"

echo "Pushing $target_image"
docker push "$target_image"
echo "Pushing $latest_image"
docker push "$latest_image"

# Clean up local tags
docker image rm -f "$target_image" || true
docker image rm -f "$latest_image" || true
fi
done
cd - > /dev/null
echo "::endgroup::"

# Build and push speaker-recognition with all CUDA variants (including CPU)
# Note: speaker-service has profiles, but we can build it directly by setting PYTORCH_CUDA_VERSION
echo "::group::Building and pushing speaker-recognition variants"
cd ../../extras/speaker-recognition
for cuda_variant in "${CUDA_VARIANTS[@]}"; do
echo "Building speaker-recognition-${cuda_variant}"
export PYTORCH_CUDA_VERSION="${cuda_variant}"
# Build speaker-service directly (profiles only affect 'up', not 'build')
docker compose build speaker-service

img_id=$(docker compose images -q speaker-service | head -n1)
if [ -n "${img_id:-}" ]; then
target_image="$REGISTRY/$OWNER_LC/speaker-recognition-${cuda_variant}:$VERSION"
latest_image="$REGISTRY/$OWNER_LC/speaker-recognition-${cuda_variant}:latest"
echo "Tagging $img_id as $target_image"
docker tag "$img_id" "$target_image"
echo "Tagging $img_id as $latest_image"
docker tag "$img_id" "$latest_image"

echo "Pushing $target_image"
docker push "$target_image"
echo "Pushing $latest_image"
docker push "$latest_image"

# Clean up local tags
docker image rm -f "$target_image" || true
docker image rm -f "$latest_image" || true
fi
done
cd - > /dev/null
echo "::endgroup::"

# Summary
echo "::group::Build Summary"
echo "Built and pushed images with version tag: ${VERSION}"
echo "Images pushed to: $REGISTRY/$OWNER_LC/"
echo "::endgroup::"
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ uv run pytest tests/test_memory_service.py # Single test file
# Environment setup
cp .env.template .env # Configure environment variables

# Setup test environment (optional, for running integration tests)
uv run --with-requirements setup-requirements.txt python setup_test_env.py # Creates .env.test

# Reset data (development)
sudo rm -rf backends/advanced/data/
```
Expand Down Expand Up @@ -68,6 +71,10 @@ cd backends/advanced
# Requires .env file with DEEPGRAM_API_KEY and OPENAI_API_KEY
cp .env.template .env # Configure API keys

# Optional: Setup test environment with test-specific credentials
# (wizard.py prompts for this, or run manually)
uv run --with-requirements setup-requirements.txt python setup_test_env.py

# Run full integration test suite
./run-test.sh

Expand Down
2 changes: 1 addition & 1 deletion Docs/init-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Note (Linux): If `host.docker.internal` is unavailable, add `extra_hosts: - "hos

### Container-to-Container Communication
Services use `host.docker.internal` for inter-container communication:
- `http://host.docker.internal:8085` - Speaker Recognition
- `http://127.0.0.1:8085` - Speaker Recognition
- `http://host.docker.internal:8767` - Parakeet ASR
- `http://host.docker.internal:8765` - OpenMemory MCP

Expand Down
3 changes: 2 additions & 1 deletion backends/advanced/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
!nginx.conf
!nginx.conf.template
!start.sh
!start-workers.sh
!start-workers.sh
!Caddyfile
5 changes: 4 additions & 1 deletion backends/advanced/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ services:
- MISTRAL_API_KEY=${MISTRAL_API_KEY}
- MISTRAL_MODEL=${MISTRAL_MODEL}
- TRANSCRIPTION_PROVIDER=${TRANSCRIPTION_PROVIDER}
- PARAKEET_ASR_URL=${PARAKEET_ASR_URL}
- OFFLINE_ASR_TCP_URI=${OFFLINE_ASR_TCP_URI}
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL}
- HF_TOKEN=${HF_TOKEN}
Expand All @@ -31,7 +32,7 @@ services:
- NEO4J_HOST=${NEO4J_HOST}
- NEO4J_USER=${NEO4J_USER}
- NEO4J_PASSWORD=${NEO4J_PASSWORD}
- CORS_ORIGINS=http://localhost:3010,http://localhost:8000,https://localhost:3010,https://localhost:8000,https://100.105.225.45,https://localhost
- CORS_ORIGINS=http://localhost:3010,http://localhost:8000,http://192.168.1.153:3010,http://192.168.1.153:8000,https://localhost:3010,https://localhost:8000,https://100.105.225.45,https://localhost
- REDIS_URL=redis://redis:6379/0
depends_on:
qdrant:
Expand All @@ -51,6 +52,7 @@ services:
restart: unless-stopped

# Unified Worker Container
# No CUDA needed for friend-backend and workers, workers only orchestrate jobs and call external services
# Runs all workers in a single container for efficiency:
# - 3 RQ workers (transcription, memory, default queues)
# - 1 Audio stream worker (Redis Streams consumer - must be single to maintain sequential chunks)
Expand All @@ -63,6 +65,7 @@ services:
- .env
volumes:
- ./src:/app/src
- ./start-workers.sh:/app/start-workers.sh
- ./data/audio_chunks:/app/audio_chunks
- ./data:/app/data
environment:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,13 @@ async def get_conversations(user: User):
# Convert conversations to API format
conversations = []
for conv in user_conversations:
# Format conversation for list - use model_dump with exclusions
# Ensure legacy fields are populated from active transcript version
conv._update_legacy_transcript_fields()

# Format conversation for list - include segments but exclude large nested fields
conv_dict = conv.model_dump(
mode='json', # Automatically converts datetime to ISO strings
exclude={'id', 'transcript', 'segments', 'transcript_versions', 'memory_versions'} # Exclude large fields for list view
exclude={'id', 'transcript_versions', 'memory_versions'} # Include segments for UI display
)

# Add computed/external fields
Expand Down Expand Up @@ -349,7 +352,7 @@ async def reprocess_transcript(conversation_id: str, user: User):
from advanced_omi_backend.workers.speaker_jobs import recognise_speakers_job
from advanced_omi_backend.workers.audio_jobs import process_cropping_job
from advanced_omi_backend.workers.memory_jobs import process_memory_job
from advanced_omi_backend.controllers.queue_controller import transcription_queue, memory_queue, default_queue, JOB_RESULT_TTL, redis_conn
from advanced_omi_backend.controllers.queue_controller import transcription_queue, memory_queue, default_queue, JOB_RESULT_TTL

# Job 1: Transcribe audio to text
transcript_job = transcription_queue.enqueue(
Expand Down Expand Up @@ -400,10 +403,10 @@ async def reprocess_transcript(conversation_id: str, user: User):
logger.info(f"📥 RQ: Enqueued audio cropping job {cropping_job.id} (depends on {speaker_job.id})")

# Job 4: Extract memories (depends on cropping)
# Note: redis_client is injected by @async_job decorator, don't pass it directly
memory_job = memory_queue.enqueue(
process_memory_job,
conversation_id,
redis_conn,
depends_on=cropping_job,
job_timeout=1800,
result_ttl=JOB_RESULT_TTL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,30 @@ async def _initialize_streaming_session(
client_state.stream_audio_format = audio_format
application_logger.info(f"🆔 Created stream session: {client_state.stream_session_id}")

# Determine transcription provider from environment
transcription_provider = os.getenv("TRANSCRIPTION_PROVIDER", "").lower()
if transcription_provider in ["offline", "parakeet"]:
provider = "parakeet"
elif transcription_provider == "deepgram":
provider = "deepgram"
else:
# Auto-detect: prefer Parakeet if URL is set, otherwise Deepgram
parakeet_url = os.getenv("PARAKEET_ASR_URL") or os.getenv("OFFLINE_ASR_TCP_URI")
deepgram_key = os.getenv("DEEPGRAM_API_KEY")
if parakeet_url:
provider = "parakeet"
elif deepgram_key:
provider = "deepgram"
else:
raise ValueError("No transcription provider configured (DEEPGRAM_API_KEY or PARAKEET_ASR_URL required)")

# Initialize session tracking in Redis
await audio_stream_producer.init_session(
session_id=client_state.stream_session_id,
user_id=user_id,
client_id=client_id,
mode="streaming",
provider="deepgram"
provider=provider
)

# Enqueue streaming jobs (speech detection + audio persistence)
Expand Down
Loading
Loading