From 5a9c033063335749cfb688485dc2816ee366465c Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:16:54 -0400 Subject: [PATCH 01/25] feat: add GitHub Actions workflow for advanced Docker Compose build and deployment --- .../advanced-docker-compose-build.yml | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/workflows/advanced-docker-compose-build.yml diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml new file mode 100644 index 00000000..1d59f0dd --- /dev/null +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -0,0 +1,113 @@ +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/**" + - ".github/workflows/advanced-docker-compose-build.yml" + tags: + - "v*" + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + timeout-minutes: 60 + defaults: + run: + shell: bash + working-directory: backends/advanced + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - 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: Create .env from secret (if provided) + if: ${{ secrets.ADVANCED_ENV != '' }} + run: | + echo "Writing .env from ADVANCED_ENV secret" + printf "%s\n" "${{ secrets.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_type }}" = "tag" ]; then + VERSION="${{ github.ref_name }}" + else + VERSION="sha-${GITHUB_SHA::7}" + fi + echo "VERSION=$VERSION" | tee -a "$GITHUB_OUTPUT" + + - name: Docker Compose build (only services with local Dockerfiles) + run: | + docker compose version + docker compose build --pull + + - name: Install jq + run: | + sudo apt-get update + sudo apt-get install -y jq + + - name: Tag and push built images to GHCR + env: + OWNER: ${{ github.repository_owner }} + VERSION: ${{ steps.version.outputs.VERSION }} + run: | + set -euo pipefail + # Only services with local Dockerfiles (skip redis/mongo/qdrant/caddy etc.) + services=("friend-backend" "workers" "webui") + + images_json=$(docker compose images --format json || echo "[]") + echo "$images_json" | jq '.' >/dev/null + + for svc in "${services[@]}"; do + img_id=$(echo "$images_json" | jq -r ".[] | select(.Name==\"$svc\") | .ID" | head -n1) + if [ -z "${img_id:-}" ] || [ "$img_id" = "null" ]; then + echo "Skipping $svc (no built image found)" + continue + fi + + target_image="$REGISTRY/$OWNER/$svc:$VERSION" + echo "Tagging $img_id as $target_image" + docker tag "$img_id" "$target_image" + + echo "Pushing $target_image" + docker push "$target_image" + done + + From 2d211f6c9981046a47ce84ddd6106758b61aaa56 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:19:30 -0400 Subject: [PATCH 02/25] fix: improve .env creation logic and ensure correct image tagging in Docker workflow --- .../workflows/advanced-docker-compose-build.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 1d59f0dd..9d3756eb 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -45,10 +45,15 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create .env from secret (if provided) - if: ${{ secrets.ADVANCED_ENV != '' }} + env: + ADVANCED_ENV: ${{ secrets.ADVANCED_ENV }} run: | - echo "Writing .env from ADVANCED_ENV secret" - printf "%s\n" "${{ secrets.ADVANCED_ENV }}" > .env + if [ -n "${ADVANCED_ENV}" ]; then + echo "Writing .env from ADVANCED_ENV secret" + printf "%s\n" "${ADVANCED_ENV}" > .env + else + echo "ADVANCED_ENV secret not set; skipping .env creation" + fi - name: Source .env (if present) run: | @@ -89,6 +94,7 @@ jobs: VERSION: ${{ steps.version.outputs.VERSION }} run: | set -euo pipefail + OWNER_LC=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]') # Only services with local Dockerfiles (skip redis/mongo/qdrant/caddy etc.) services=("friend-backend" "workers" "webui") @@ -102,7 +108,7 @@ jobs: continue fi - target_image="$REGISTRY/$OWNER/$svc:$VERSION" + target_image="$REGISTRY/$OWNER_LC/$svc:$VERSION" echo "Tagging $img_id as $target_image" docker tag "$img_id" "$target_image" From c0186da48fb13b2ef69bebe961bee642741887c2 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:35:48 -0400 Subject: [PATCH 03/25] secrets --- .../workflows/advanced-docker-compose-build.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 9d3756eb..ce34a696 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -25,6 +25,8 @@ jobs: build-and-deploy: runs-on: ubuntu-latest timeout-minutes: 60 + env: + ADVANCED_ENV: ${{ secrets.ADVANCED_ENV }} defaults: run: shell: bash @@ -45,15 +47,10 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create .env from secret (if provided) - env: - ADVANCED_ENV: ${{ secrets.ADVANCED_ENV }} + if: ${{ env.ADVANCED_ENV != '' }} run: | - if [ -n "${ADVANCED_ENV}" ]; then - echo "Writing .env from ADVANCED_ENV secret" - printf "%s\n" "${ADVANCED_ENV}" > .env - else - echo "ADVANCED_ENV secret not set; skipping .env creation" - fi + echo "Writing .env from ADVANCED_ENV secret" + printf "%s\n" "${ADVANCED_ENV}" > .env - name: Source .env (if present) run: | @@ -116,4 +113,3 @@ jobs: docker push "$target_image" done - From 4a6538127905c99ec93fee5a75fd697a5ab0c13c Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:37:08 -0400 Subject: [PATCH 04/25] env --- .github/workflows/advanced-docker-compose-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index ce34a696..f32eeae0 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -47,7 +47,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create .env from secret (if provided) - if: ${{ env.ADVANCED_ENV != '' }} + if: env.ADVANCED_ENV != '' run: | echo "Writing .env from ADVANCED_ENV secret" printf "%s\n" "${ADVANCED_ENV}" > .env From 5f8f49866525aedbf022941f7c8a677e69c23a5b Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:38:55 -0400 Subject: [PATCH 05/25] space --- .github/workflows/advanced-docker-compose-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index f32eeae0..9037278f 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -14,6 +14,7 @@ on: tags: - "v*" + permissions: contents: read packages: write From 43c1f67faf29291e83507d94b6f6a2790babd80d Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:39:54 -0400 Subject: [PATCH 06/25] dea --- .github/workflows/advanced-docker-compose-build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 9037278f..97ab918d 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -7,8 +7,9 @@ on: description: Optional version tag override (e.g. v1.2.3) required: false push: - branches: [ "main" ] + branches: [ "main", "feat/*" ] paths: + - "*" - "backends/advanced/**" - ".github/workflows/advanced-docker-compose-build.yml" tags: From 0eeea390793877ae1b50f82bb5757420b156b501 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:09:29 -0400 Subject: [PATCH 07/25] docker b --- .github/workflows/advanced-docker-compose-build.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 97ab918d..bccf6117 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -48,6 +48,15 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Copy .env.template to .env + run: | + if [ -f .env.template ]; then + echo "Copying .env.template to .env" + cp .env.template .env + else + echo ".env.template not found; skipping" + fi + - name: Create .env from secret (if provided) if: env.ADVANCED_ENV != '' run: | From 0c386b27592c88f823744eda56e60e07a598b8cb Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:17:07 -0400 Subject: [PATCH 08/25] fix: enhance error handling and JSON validation in Docker Compose workflow --- .../workflows/advanced-docker-compose-build.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index bccf6117..ff2d3432 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -106,8 +106,20 @@ jobs: # Only services with local Dockerfiles (skip redis/mongo/qdrant/caddy etc.) services=("friend-backend" "workers" "webui") - images_json=$(docker compose images --format json || echo "[]") - echo "$images_json" | jq '.' >/dev/null + # Get built images, handle empty/null response + images_output=$(docker compose images --format json 2>/dev/null || echo "") + if [ -z "$images_output" ] || [ "$images_output" = "null" ]; then + echo "No images found from docker compose images" + images_json="[]" + else + images_json="$images_output" + fi + + # Validate JSON + if ! echo "$images_json" | jq empty 2>/dev/null; then + echo "Invalid JSON from docker compose images, using empty array" + images_json="[]" + fi for svc in "${services[@]}"; do img_id=$(echo "$images_json" | jq -r ".[] | select(.Name==\"$svc\") | .ID" | head -n1) From 5db141089833d0fe9acda272ac3c5b094bf5e697 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:27:32 -0400 Subject: [PATCH 09/25] refactor: update image retrieval method in Docker Compose workflow to use 'docker images' --- .../advanced-docker-compose-build.yml | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index ff2d3432..9bba4e25 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -106,24 +106,14 @@ jobs: # Only services with local Dockerfiles (skip redis/mongo/qdrant/caddy etc.) services=("friend-backend" "workers" "webui") - # Get built images, handle empty/null response - images_output=$(docker compose images --format json 2>/dev/null || echo "") - if [ -z "$images_output" ] || [ "$images_output" = "null" ]; then - echo "No images found from docker compose images" - images_json="[]" - else - images_json="$images_output" - fi - - # Validate JSON - if ! echo "$images_json" | jq empty 2>/dev/null; then - echo "Invalid JSON from docker compose images, using empty array" - images_json="[]" - fi + # Get built images using docker images instead of docker compose images + echo "Looking for built images..." + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}" | grep -E "(friend-backend|workers|webui)" || echo "No matching images found" for svc in "${services[@]}"; do - img_id=$(echo "$images_json" | jq -r ".[] | select(.Name==\"$svc\") | .ID" | head -n1) - if [ -z "${img_id:-}" ] || [ "$img_id" = "null" ]; then + # Look for images with the service name as repository + img_id=$(docker images --format "{{.ID}}" --filter "reference=${svc}:*" | head -n1) + if [ -z "${img_id:-}" ]; then echo "Skipping $svc (no built image found)" continue fi From 7bdc71c89a98c65b8e99d7203487c1a94025fbef Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:38:34 -0400 Subject: [PATCH 10/25] refactor: update service names and tagging logic in Docker Compose workflow --- .../workflows/advanced-docker-compose-build.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 9bba4e25..ac9a5c25 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -104,21 +104,26 @@ jobs: set -euo pipefail OWNER_LC=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]') # Only services with local Dockerfiles (skip redis/mongo/qdrant/caddy etc.) - services=("friend-backend" "workers" "webui") + # Docker Compose creates images with "advanced-" prefix + services=("advanced-friend-backend" "advanced-workers" "advanced-webui") + service_names=("friend-backend" "workers" "webui") # Get built images using docker images instead of docker compose images echo "Looking for built images..." - docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}" | grep -E "(friend-backend|workers|webui)" || echo "No matching images found" + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}" | grep -E "(advanced-friend-backend|advanced-workers|advanced-webui)" || echo "No matching images found" - for svc in "${services[@]}"; do + for i in "${!services[@]}"; do + svc="${services[$i]}" + svc_name="${service_names[$i]}" + # Look for images with the service name as repository img_id=$(docker images --format "{{.ID}}" --filter "reference=${svc}:*" | head -n1) if [ -z "${img_id:-}" ]; then - echo "Skipping $svc (no built image found)" + echo "Skipping $svc_name (no built image found)" continue fi - target_image="$REGISTRY/$OWNER_LC/$svc:$VERSION" + target_image="$REGISTRY/$OWNER_LC/$svc_name:$VERSION" echo "Tagging $img_id as $target_image" docker tag "$img_id" "$target_image" From 777b87c4c65f5b1df0b3f38d2ebbca8eec753fac Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:23:18 -0400 Subject: [PATCH 11/25] refactor: enhance .env copying logic and add Docker Compose builds for extras services --- .../advanced-docker-compose-build.yml | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index ac9a5c25..0ab15ea4 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -50,12 +50,22 @@ jobs: - name: Copy .env.template to .env run: | - if [ -f .env.template ]; then - echo "Copying .env.template to .env" - cp .env.template .env - else - echo ".env.template not found; skipping" - fi + 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 - name: Create .env from secret (if provided) if: env.ADVANCED_ENV != '' @@ -91,6 +101,16 @@ jobs: docker compose version docker compose build --pull + - name: Docker Compose build (extras/asr-services parakeet-asr) + run: | + docker compose version + docker compose -f ../../extras/asr-services/docker-compose.yml --project-directory ../../extras/asr-services build parakeet-asr + + - name: Docker Compose build (extras/speaker-recognition speaker-service) + run: | + docker compose version + docker compose -f ../../extras/speaker-recognition/docker-compose.yml --project-directory ../../extras/speaker-recognition build speaker-service + - name: Install jq run: | sudo apt-get update @@ -104,13 +124,13 @@ jobs: set -euo pipefail OWNER_LC=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]') # Only services with local Dockerfiles (skip redis/mongo/qdrant/caddy etc.) - # Docker Compose creates images with "advanced-" prefix - services=("advanced-friend-backend" "advanced-workers" "advanced-webui") - service_names=("friend-backend" "workers" "webui") + # Advanced project builds use the "advanced-" prefix; extras use explicit names + services=("advanced-friend-backend" "advanced-workers" "advanced-webui" "parakeet-asr" "speaker-recognition") + service_names=("friend-backend" "workers" "webui" "parakeet-asr" "speaker-recognition") # Get built images using docker images instead of docker compose images echo "Looking for built images..." - docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}" | grep -E "(advanced-friend-backend|advanced-workers|advanced-webui)" || echo "No matching images found" + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}" | grep -E "(advanced-friend-backend|advanced-workers|advanced-webui|parakeet-asr|speaker-recognition)" || echo "No matching images found" for i in "${!services[@]}"; do svc="${services[$i]}" @@ -130,4 +150,3 @@ jobs: echo "Pushing $target_image" docker push "$target_image" done - From d47a5e1952212813615dd632c89eec3953e938bb Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:40:38 -0400 Subject: [PATCH 12/25] refactor: implement dynamic runner selection in GitHub Actions workflow --- .../advanced-docker-compose-build.yml | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 0ab15ea4..3427eb5e 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -19,13 +19,44 @@ on: permissions: contents: read packages: write + actions: read env: REGISTRY: ghcr.io jobs: - build-and-deploy: + detect-runner: runs-on: ubuntu-latest + outputs: + runner: ${{ steps.determine.outputs.runner }} + label: ${{ steps.determine.outputs.label }} + steps: + - name: Determine runner target + id: determine + uses: actions/github-script@v7 + with: + script: | + const {owner, repo} = context.repo; + const response = await github.rest.actions.listSelfHostedRunnersForRepo({ + owner, + repo, + per_page: 100 + }); + const available = response.data.runners.find((runner) => runner.status === 'online' && !runner.busy); + + if (available) { + core.info(`Using self-hosted runner "${available.name}" with labels: ${available.labels.map((label) => label.name).join(', ')}`); + core.setOutput('runner', '["self-hosted","linux","x64"]'); + core.setOutput('label', 'self-hosted'); + } else { + core.info('No idle self-hosted runners available; falling back to "ubuntu-latest".'); + core.setOutput('runner', '"ubuntu-latest"'); + core.setOutput('label', 'ubuntu-latest'); + } + + build-and-deploy: + needs: detect-runner + runs-on: ${{ fromJson(needs.detect-runner.outputs.runner) }} timeout-minutes: 60 env: ADVANCED_ENV: ${{ secrets.ADVANCED_ENV }} @@ -35,6 +66,9 @@ jobs: working-directory: backends/advanced steps: + - name: Show selected runner + run: echo "Workflow running on ${{ needs.detect-runner.outputs.label }} runner" + - name: Checkout uses: actions/checkout@v4 From ed4d6677acb0ef94bbcc82fbb0183eb640dc2ec5 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:42:00 -0400 Subject: [PATCH 13/25] fix: improve error handling for self-hosted runner selection in GitHub Actions workflow --- .../advanced-docker-compose-build.yml | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 3427eb5e..13a8c52f 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -37,23 +37,29 @@ jobs: with: script: | const {owner, repo} = context.repo; - const response = await github.rest.actions.listSelfHostedRunnersForRepo({ - owner, - repo, - per_page: 100 - }); - const available = response.data.runners.find((runner) => runner.status === 'online' && !runner.busy); - - if (available) { - core.info(`Using self-hosted runner "${available.name}" with labels: ${available.labels.map((label) => label.name).join(', ')}`); - core.setOutput('runner', '["self-hosted","linux","x64"]'); - core.setOutput('label', 'self-hosted'); - } else { + try { + const response = await github.rest.actions.listSelfHostedRunnersForRepo({ + owner, + repo, + per_page: 100 + }); + const available = response.data.runners.find((runner) => runner.status === 'online' && !runner.busy); + + if (available) { + core.info(`Using self-hosted runner "${available.name}" with labels: ${available.labels.map((label) => label.name).join(', ')}`); + core.setOutput('runner', '["self-hosted","linux","x64"]'); + core.setOutput('label', 'self-hosted'); + return; + } + core.info('No idle self-hosted runners available; falling back to "ubuntu-latest".'); - core.setOutput('runner', '"ubuntu-latest"'); - core.setOutput('label', 'ubuntu-latest'); + } catch (error) { + core.warning(`Unable to query self-hosted runners (${error.message}); falling back to "ubuntu-latest".`); } + core.setOutput('runner', '"ubuntu-latest"'); + core.setOutput('label', 'ubuntu-latest'); + build-and-deploy: needs: detect-runner runs-on: ${{ fromJson(needs.detect-runner.outputs.runner) }} From da92136edb8f7b350ee7fff1ca06d2a9ae07252a Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:43:59 -0400 Subject: [PATCH 14/25] fix: set working directory for runner display step in GitHub Actions workflow --- .github/workflows/advanced-docker-compose-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 13a8c52f..1cb890cc 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -74,6 +74,7 @@ jobs: steps: - name: Show selected runner run: echo "Workflow running on ${{ needs.detect-runner.outputs.label }} runner" + working-directory: . - name: Checkout uses: actions/checkout@v4 From 33355adddfd83a549bdf3e2567d82d478dfea60b Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:45:08 -0400 Subject: [PATCH 15/25] fix: update error logging for self-hosted runner fallback in GitHub Actions workflow --- .github/workflows/advanced-docker-compose-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 1cb890cc..6a86002b 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -54,7 +54,7 @@ jobs: core.info('No idle self-hosted runners available; falling back to "ubuntu-latest".'); } catch (error) { - core.warning(`Unable to query self-hosted runners (${error.message}); falling back to "ubuntu-latest".`); + core.info(`Unable to query self-hosted runners (${error.message}); falling back to "ubuntu-latest".`); } core.setOutput('runner', '"ubuntu-latest"'); From 3ad92808d221eda0ef7a080bb8494ec4de9e1711 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:47:07 -0400 Subject: [PATCH 16/25] refactor: streamline GitHub Actions workflow by consolidating runner selection and adding fallback to default runner --- .../advanced-docker-compose-build.yml | 60 +++++++------------ 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 6a86002b..0a25c698 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -25,55 +25,21 @@ env: REGISTRY: ghcr.io jobs: - detect-runner: - runs-on: ubuntu-latest - outputs: - runner: ${{ steps.determine.outputs.runner }} - label: ${{ steps.determine.outputs.label }} - steps: - - name: Determine runner target - id: determine - uses: actions/github-script@v7 - with: - script: | - const {owner, repo} = context.repo; - try { - const response = await github.rest.actions.listSelfHostedRunnersForRepo({ - owner, - repo, - per_page: 100 - }); - const available = response.data.runners.find((runner) => runner.status === 'online' && !runner.busy); - - if (available) { - core.info(`Using self-hosted runner "${available.name}" with labels: ${available.labels.map((label) => label.name).join(', ')}`); - core.setOutput('runner', '["self-hosted","linux","x64"]'); - core.setOutput('label', 'self-hosted'); - return; - } - - core.info('No idle self-hosted runners available; falling back to "ubuntu-latest".'); - } catch (error) { - core.info(`Unable to query self-hosted runners (${error.message}); falling back to "ubuntu-latest".`); - } - - core.setOutput('runner', '"ubuntu-latest"'); - core.setOutput('label', 'ubuntu-latest'); - - build-and-deploy: - needs: detect-runner - runs-on: ${{ fromJson(needs.detect-runner.outputs.runner) }} + build-self-hosted: + runs-on: [self-hosted, linux, x64] timeout-minutes: 60 + continue-on-error: true env: ADVANCED_ENV: ${{ secrets.ADVANCED_ENV }} + RUNNER_FLAVOUR: self-hosted defaults: run: shell: bash working-directory: backends/advanced - steps: + steps: &advanced_build_steps - name: Show selected runner - run: echo "Workflow running on ${{ needs.detect-runner.outputs.label }} runner" + run: echo "Workflow running on ${RUNNER_FLAVOUR} runner" working-directory: . - name: Checkout @@ -190,4 +156,18 @@ jobs: echo "Pushing $target_image" docker push "$target_image" + + build-default: + needs: build-self-hosted + if: needs.build-self-hosted.result != 'success' + 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: *advanced_build_steps done From ecbd16d35faa2eac949ebfc0e966716e9e6017db Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:49:46 -0400 Subject: [PATCH 17/25] fix: correct loop termination in GitHub Actions workflow for Docker image pushing --- .github/workflows/advanced-docker-compose-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 0a25c698..78e95700 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -156,6 +156,7 @@ jobs: echo "Pushing $target_image" docker push "$target_image" + done build-default: needs: build-self-hosted @@ -170,4 +171,3 @@ jobs: shell: bash working-directory: backends/advanced steps: *advanced_build_steps - done From c6c631c32339ae419e6aacfde9f17efef0397b08 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:06:28 -0400 Subject: [PATCH 18/25] refactor: consolidate Docker Compose build steps and improve image handling in GitHub Actions workflow --- .../advanced-docker-compose-build.yml | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 78e95700..66e8650e 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -103,50 +103,44 @@ jobs: fi echo "VERSION=$VERSION" | tee -a "$GITHUB_OUTPUT" - - name: Docker Compose build (only services with local Dockerfiles) - run: | - docker compose version - docker compose build --pull - - - name: Docker Compose build (extras/asr-services parakeet-asr) - run: | - docker compose version - docker compose -f ../../extras/asr-services/docker-compose.yml --project-directory ../../extras/asr-services build parakeet-asr - - - name: Docker Compose build (extras/speaker-recognition speaker-service) - run: | - docker compose version - docker compose -f ../../extras/speaker-recognition/docker-compose.yml --project-directory ../../extras/speaker-recognition build speaker-service - - - name: Install jq - run: | - sudo apt-get update - sudo apt-get install -y jq - - - name: Tag and push built images to GHCR + - name: Build, push, and prune services sequentially env: OWNER: ${{ github.repository_owner }} VERSION: ${{ steps.version.outputs.VERSION }} run: | set -euo pipefail + docker compose version OWNER_LC=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]') # Only services with local Dockerfiles (skip redis/mongo/qdrant/caddy etc.) # Advanced project builds use the "advanced-" prefix; extras use explicit names services=("advanced-friend-backend" "advanced-workers" "advanced-webui" "parakeet-asr" "speaker-recognition") service_names=("friend-backend" "workers" "webui" "parakeet-asr" "speaker-recognition") - # Get built images using docker images instead of docker compose images - echo "Looking for built images..." - docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}" | grep -E "(advanced-friend-backend|advanced-workers|advanced-webui|parakeet-asr|speaker-recognition)" || echo "No matching images found" - for i in "${!services[@]}"; do svc="${services[$i]}" svc_name="${service_names[$i]}" - - # Look for images with the service name as repository - img_id=$(docker images --format "{{.ID}}" --filter "reference=${svc}:*" | head -n1) + + echo "::group::Building $svc_name" + case "$svc" in + advanced-friend-backend|advanced-workers|advanced-webui) + docker compose build --pull "$svc" + ;; + parakeet-asr) + docker compose -f ../../extras/asr-services/docker-compose.yml --project-directory ../../extras/asr-services build parakeet-asr + ;; + speaker-recognition) + docker compose -f ../../extras/speaker-recognition/docker-compose.yml --project-directory ../../extras/speaker-recognition build speaker-service + ;; + *) + echo "Unknown service '$svc'; aborting." + exit 1 + ;; + esac + + img_id=$(docker images --format "{{.Repository}} {{.Tag}} {{.ID}}" | awk -v repo="$svc" '$1 == repo {print $3; exit}') if [ -z "${img_id:-}" ]; then - echo "Skipping $svc_name (no built image found)" + echo "Skipping $svc_name (no built image found after build)" + echo "::endgroup::" continue fi @@ -156,11 +150,18 @@ jobs: echo "Pushing $target_image" docker push "$target_image" + echo "Removing local image tags for $svc_name" + docker image rm -f "$target_image" || true + docker image rm -f "$img_id" || true + echo "Pruning Docker builder cache and unused data" + docker builder prune -af || true + docker system prune -af || true + echo "::endgroup::" done build-default: needs: build-self-hosted - if: needs.build-self-hosted.result != 'success' + if: needs.build-self-hosted.result == 'success' runs-on: ubuntu-latest timeout-minutes: 60 env: From cd903f79d97be440145d65ca572e4c936ac8f434 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:10:30 -0400 Subject: [PATCH 19/25] refactor: simplify service build configuration in GitHub Actions workflow by using a structured array for Docker Compose services --- .../advanced-docker-compose-build.yml | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 66e8650e..6c3ca27b 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -111,40 +111,32 @@ jobs: set -euo pipefail docker compose version OWNER_LC=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]') - # Only services with local Dockerfiles (skip redis/mongo/qdrant/caddy etc.) - # Advanced project builds use the "advanced-" prefix; extras use explicit names - services=("advanced-friend-backend" "advanced-workers" "advanced-webui" "parakeet-asr" "speaker-recognition") - service_names=("friend-backend" "workers" "webui" "parakeet-asr" "speaker-recognition") - - for i in "${!services[@]}"; do - svc="${services[$i]}" - svc_name="${service_names[$i]}" - - echo "::group::Building $svc_name" - case "$svc" in - advanced-friend-backend|advanced-workers|advanced-webui) - docker compose build --pull "$svc" - ;; - parakeet-asr) - docker compose -f ../../extras/asr-services/docker-compose.yml --project-directory ../../extras/asr-services build parakeet-asr - ;; - speaker-recognition) - docker compose -f ../../extras/speaker-recognition/docker-compose.yml --project-directory ../../extras/speaker-recognition build speaker-service - ;; - *) - echo "Unknown service '$svc'; aborting." - exit 1 - ;; - esac - - img_id=$(docker images --format "{{.Repository}} {{.Tag}} {{.ID}}" | awk -v repo="$svc" '$1 == repo {print $3; exit}') + service_specs=( + "friend-backend|advanced-friend-backend|docker-compose.yml|." + "workers|advanced-workers|docker-compose.yml|." + "webui|advanced-webui|docker-compose.yml|." + "parakeet-asr|parakeet-asr|../../extras/asr-services/docker-compose.yml|../../extras/asr-services" + "speaker-service|speaker-recognition|../../extras/speaker-recognition/docker-compose.yml|../../extras/speaker-recognition" + ) + + for spec in "${service_specs[@]}"; do + IFS='|' read -r svc svc_repo compose_file project_dir <<< "$spec" + + echo "::group::Building $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 + + img_id=$(docker images --format "{{.ID}}" --filter "reference=${svc_repo}:*" | head -n1) if [ -z "${img_id:-}" ]; then - echo "Skipping $svc_name (no built image found after build)" + echo "Skipping $svc_repo (no built image found after build)" echo "::endgroup::" continue fi - target_image="$REGISTRY/$OWNER_LC/$svc_name:$VERSION" + target_image="$REGISTRY/$OWNER_LC/$svc_repo:$VERSION" echo "Tagging $img_id as $target_image" docker tag "$img_id" "$target_image" From b58b781e0cba6bb066cab9424577999161e6554e Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:18:09 -0400 Subject: [PATCH 20/25] fix: update image tag removal logging and adjust build condition in GitHub Actions workflow --- .github/workflows/advanced-docker-compose-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 6c3ca27b..19643430 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -142,7 +142,7 @@ jobs: echo "Pushing $target_image" docker push "$target_image" - echo "Removing local image tags for $svc_name" + echo "Removing local image tags for $svc_repo" docker image rm -f "$target_image" || true docker image rm -f "$img_id" || true echo "Pruning Docker builder cache and unused data" @@ -153,7 +153,7 @@ jobs: build-default: needs: build-self-hosted - if: needs.build-self-hosted.result == 'success' + if: needs.build-self-hosted.result != 'success' runs-on: ubuntu-latest timeout-minutes: 60 env: From 65ddb494cce8cd538e9d6e695d006a3eba00990b Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Sat, 8 Nov 2025 18:59:39 -0500 Subject: [PATCH 21/25] Update service URLs and enhance speaker-recognition setup - Changed speaker service URL from `http://host.docker.internal:8085` to `http://127.0.0.1:8085` in `wizard.py` and updated related documentation. - Added validation for `HF_TOKEN` in the speaker-recognition setup, prompting the user if it's missing or invalid. - Introduced a new `ParakeetStreamConsumer` for handling audio streams with Parakeet, including graceful shutdown handling. - Updated `docker-compose.yml` and related files to ensure proper environment variable usage and service health checks. - Enhanced error handling in audio stream workers for better logging and user feedback. --- Docs/init-system.md | 4 +- backends/advanced/docker-compose.yml | 5 +- .../controllers/conversation_controller.py | 11 ++- .../controllers/websocket_controller.py | 19 +++- .../models/conversation.py | 13 +++ .../services/transcription/parakeet.py | 1 + .../transcription/parakeet_stream_consumer.py | 91 +++++++++++++++++++ .../workers/audio_stream_deepgram_worker.py | 5 +- .../workers/audio_stream_parakeet_worker.py | 80 ++++++++++++++++ .../workers/transcription_jobs.py | 51 +++++++++-- backends/advanced/start-workers.sh | 37 ++++++-- extras/speaker-recognition/.env.template | 4 +- extras/speaker-recognition/README.md | 39 +++++++- extras/speaker-recognition/docker-compose.yml | 5 +- .../speaker-recognition/nginx.conf.template | 5 +- wizard.py | 37 ++++++-- 16 files changed, 364 insertions(+), 43 deletions(-) create mode 100644 backends/advanced/src/advanced_omi_backend/services/transcription/parakeet_stream_consumer.py create mode 100644 backends/advanced/src/advanced_omi_backend/workers/audio_stream_parakeet_worker.py diff --git a/Docs/init-system.md b/Docs/init-system.md index 0688f8bf..4890ea27 100644 --- a/Docs/init-system.md +++ b/Docs/init-system.md @@ -101,7 +101,7 @@ When using the orchestrated setup, service URLs are automatically configured: | Service Selected | Backend Gets Configured With | |----------------------|-------------------------------------------------------------------| -| Speaker Recognition | `SPEAKER_SERVICE_URL=http://host.docker.internal:8085` | +| Speaker Recognition | `SPEAKER_SERVICE_URL=http://127.0.0.1:8085` | | ASR Services | `PARAKEET_ASR_URL=http://host.docker.internal:8767` | This eliminates the need to manually configure service URLs when running services on the same machine. @@ -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 diff --git a/backends/advanced/docker-compose.yml b/backends/advanced/docker-compose.yml index 2dcc11bb..d9d58dca 100644 --- a/backends/advanced/docker-compose.yml +++ b/backends/advanced/docker-compose.yml @@ -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} @@ -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: @@ -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) @@ -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: diff --git a/backends/advanced/src/advanced_omi_backend/controllers/conversation_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/conversation_controller.py index 86e00ad3..4b852dae 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/conversation_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/conversation_controller.py @@ -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 @@ -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( @@ -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, diff --git a/backends/advanced/src/advanced_omi_backend/controllers/websocket_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/websocket_controller.py index 98e96734..53a580a7 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/websocket_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/websocket_controller.py @@ -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) diff --git a/backends/advanced/src/advanced_omi_backend/models/conversation.py b/backends/advanced/src/advanced_omi_backend/models/conversation.py index cba23c41..367c0daf 100644 --- a/backends/advanced/src/advanced_omi_backend/models/conversation.py +++ b/backends/advanced/src/advanced_omi_backend/models/conversation.py @@ -159,6 +159,19 @@ def clean_legacy_data(cls, data: Any) -> Any: elif not isinstance(segment['speaker'], str): segment['speaker'] = "unknown" + # Populate legacy fields from active transcript version if they're empty + active_version_id = data.get('active_transcript_version') + if active_version_id and 'transcript_versions' in data and isinstance(data['transcript_versions'], list): + for version in data['transcript_versions']: + if isinstance(version, dict) and version.get('version_id') == active_version_id: + # Populate transcript if missing + if not data.get('transcript') and version.get('transcript'): + data['transcript'] = version['transcript'] + # Populate segments if missing or empty + if (not data.get('segments') or len(data.get('segments', [])) == 0) and version.get('segments'): + data['segments'] = version['segments'] + break + return data @property diff --git a/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet.py b/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet.py index 10da0058..5b11e094 100644 --- a/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet.py +++ b/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet.py @@ -7,6 +7,7 @@ import asyncio import json import logging +import os import tempfile from typing import Dict, Optional diff --git a/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet_stream_consumer.py b/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet_stream_consumer.py new file mode 100644 index 00000000..75974fed --- /dev/null +++ b/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet_stream_consumer.py @@ -0,0 +1,91 @@ +""" +Parakeet stream consumer for Redis Streams architecture. + +Reads from: audio:stream:* streams +Writes to: transcription:results:{session_id} +""" + +import logging +import os + +from advanced_omi_backend.services.audio_stream.consumer import BaseAudioStreamConsumer +from advanced_omi_backend.services.transcription.parakeet import ParakeetProvider + +logger = logging.getLogger(__name__) + + +class ParakeetStreamConsumer: + """ + Parakeet consumer for Redis Streams architecture. + + Reads from: specified stream (client-specific or provider-specific) + Writes to: transcription:results:{session_id} + + This inherits from BaseAudioStreamConsumer and implements transcribe_audio(). + """ + + def __init__(self, redis_client, service_url: str = None, buffer_chunks: int = 30): + """ + Initialize Parakeet consumer. + + Dynamically discovers all audio:stream:* streams and claims them using Redis locks. + + Args: + redis_client: Connected Redis client + service_url: Parakeet service URL (defaults to PARAKEET_ASR_URL env var) + buffer_chunks: Number of chunks to buffer before transcribing (default: 30 = ~7.5s) + """ + self.service_url = service_url or os.getenv("PARAKEET_ASR_URL") or os.getenv("OFFLINE_ASR_TCP_URI") + if not self.service_url: + raise ValueError("PARAKEET_ASR_URL or OFFLINE_ASR_TCP_URI is required") + + # Initialize Parakeet provider + self.provider = ParakeetProvider(service_url=self.service_url) + + # Create a concrete subclass that implements transcribe_audio + class _ConcreteConsumer(BaseAudioStreamConsumer): + def __init__(inner_self, provider_name: str, redis_client, buffer_chunks: int): + super().__init__(provider_name, redis_client, buffer_chunks) + inner_self._parakeet_provider = self.provider + + async def transcribe_audio(inner_self, audio_data: bytes, sample_rate: int) -> dict: + """Transcribe using ParakeetProvider.""" + try: + result = await inner_self._parakeet_provider.transcribe( + audio_data=audio_data, + sample_rate=sample_rate + ) + + # Calculate confidence (Parakeet may not provide confidence, default to 0.9) + confidence = 0.9 + if result.get("words"): + confidences = [ + w.get("confidence", 0.9) + for w in result["words"] + if "confidence" in w + ] + if confidences: + confidence = sum(confidences) / len(confidences) + + return { + "text": result.get("text", ""), + "words": result.get("words", []), + "segments": result.get("segments", []), + "confidence": confidence + } + + except Exception as e: + logger.error(f"Parakeet transcription failed: {e}", exc_info=True) + raise + + # Instantiate the concrete consumer + self._consumer = _ConcreteConsumer("parakeet", redis_client, buffer_chunks) + + async def start_consuming(self): + """Delegate to base consumer.""" + return await self._consumer.start_consuming() + + async def stop(self): + """Delegate to base consumer.""" + return await self._consumer.stop() + diff --git a/backends/advanced/src/advanced_omi_backend/workers/audio_stream_deepgram_worker.py b/backends/advanced/src/advanced_omi_backend/workers/audio_stream_deepgram_worker.py index 80203677..c8866eed 100644 --- a/backends/advanced/src/advanced_omi_backend/workers/audio_stream_deepgram_worker.py +++ b/backends/advanced/src/advanced_omi_backend/workers/audio_stream_deepgram_worker.py @@ -30,8 +30,9 @@ async def main(): # Get configuration from environment api_key = os.getenv("DEEPGRAM_API_KEY") if not api_key: - logger.error("DEEPGRAM_API_KEY environment variable is required") - sys.exit(1) + logger.warning("DEEPGRAM_API_KEY environment variable not set - Deepgram audio stream worker will not start") + logger.warning("Audio transcription will use alternative providers if configured") + return redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0") diff --git a/backends/advanced/src/advanced_omi_backend/workers/audio_stream_parakeet_worker.py b/backends/advanced/src/advanced_omi_backend/workers/audio_stream_parakeet_worker.py new file mode 100644 index 00000000..aa8b9c6a --- /dev/null +++ b/backends/advanced/src/advanced_omi_backend/workers/audio_stream_parakeet_worker.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +Parakeet audio stream worker. + +Starts a consumer that reads from audio:stream:* and transcribes audio using Parakeet. +""" + +import asyncio +import logging +import os +import signal +import sys + +import redis.asyncio as redis + +from advanced_omi_backend.services.transcription.parakeet_stream_consumer import ParakeetStreamConsumer + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" +) + +logger = logging.getLogger(__name__) + + +async def main(): + """Main worker entry point.""" + logger.info("šŸš€ Starting Parakeet audio stream worker") + + # Get configuration from environment + service_url = os.getenv("PARAKEET_ASR_URL") or os.getenv("OFFLINE_ASR_TCP_URI") + if not service_url: + logger.warning("PARAKEET_ASR_URL or OFFLINE_ASR_TCP_URI environment variable not set - Parakeet audio stream worker will not start") + logger.warning("Audio transcription will use alternative providers if configured") + return + + redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0") + + # Create Redis client + redis_client = await redis.from_url( + redis_url, + encoding="utf-8", + decode_responses=False + ) + logger.info("Connected to Redis") + + # Create consumer with balanced buffer size + # 20 chunks = ~5 seconds of audio + # Balance between transcription accuracy and latency + consumer = ParakeetStreamConsumer( + redis_client=redis_client, + service_url=service_url, + buffer_chunks=20 # 5 seconds - good context without excessive delay + ) + + # Setup signal handlers for graceful shutdown + def signal_handler(signum, frame): + logger.info(f"Received signal {signum}, shutting down...") + asyncio.create_task(consumer.stop()) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + try: + logger.info("āœ… Parakeet worker ready") + + # This blocks until consumer is stopped + await consumer.start_consuming() + + except Exception as e: + logger.error(f"Worker error: {e}", exc_info=True) + sys.exit(1) + finally: + await redis_client.aclose() + logger.info("šŸ‘‹ Parakeet worker stopped") + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py b/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py index e081786a..664f621f 100644 --- a/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py +++ b/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py @@ -197,19 +197,52 @@ async def transcribe_full_audio_job( # Convert segments to SpeakerSegment objects speaker_segments = [] - for seg in segments: - # Use identified_as if available (from speaker recognition), otherwise use speaker label - speaker_name = seg.get("identified_as") or seg.get("speaker", "Unknown") - + + if segments: + # Use provided segments + for seg in segments: + # Use identified_as if available (from speaker recognition), otherwise use speaker label + speaker_name = seg.get("identified_as") or seg.get("speaker", "Unknown") + + speaker_segments.append( + Conversation.SpeakerSegment( + start=seg.get("start", 0), + end=seg.get("end", 0), + text=seg.get("text", ""), + speaker=speaker_name, + confidence=seg.get("confidence") + ) + ) + elif transcript_text: + # NOTE: Parakeet falls here. + # If no segments but we have text, create a single segment from the full transcript + # Calculate duration from words if available, otherwise estimate from audio + start_time_seg = 0.0 + end_time_seg = 0.0 + + if words: + # Use word timestamps if available + start_times = [w.get("start", 0) for w in words if "start" in w] + end_times = [w.get("end", 0) for w in words if "end" in w] + if start_times: + start_time_seg = min(start_times) + if end_times: + end_time_seg = max(end_times) + else: + # Estimate duration: assume ~150 words per minute, or use audio file duration + # For now, use a default duration if we can't calculate it + end_time_seg = len(transcript_text.split()) * 0.4 # Rough estimate: 0.4s per word + speaker_segments.append( Conversation.SpeakerSegment( - start=seg.get("start", 0), - end=seg.get("end", 0), - text=seg.get("text", ""), - speaker=speaker_name, - confidence=seg.get("confidence") + start=start_time_seg, + end=end_time_seg if end_time_seg > start_time_seg else start_time_seg + 1.0, + text=transcript_text, + speaker="Unknown", + confidence=None ) ) + logger.info(f"šŸ“Š Created single segment from transcript text (no segments returned by provider)") logger.info(f"šŸ“Š Created {len(speaker_segments)} speaker segments") diff --git a/backends/advanced/start-workers.sh b/backends/advanced/start-workers.sh index 0f39cb09..d9386d37 100755 --- a/backends/advanced/start-workers.sh +++ b/backends/advanced/start-workers.sh @@ -41,7 +41,8 @@ shutdown() { kill $RQ_WORKER_5_PID 2>/dev/null || true kill $RQ_WORKER_6_PID 2>/dev/null || true kill $AUDIO_PERSISTENCE_WORKER_PID 2>/dev/null || true - kill $AUDIO_STREAM_WORKER_PID 2>/dev/null || true + [ -n "$AUDIO_STREAM_WORKER_PID" ] && kill $AUDIO_STREAM_WORKER_PID 2>/dev/null || true + [ -n "$PARAKEET_STREAM_WORKER_PID" ] && kill $PARAKEET_STREAM_WORKER_PID 2>/dev/null || true wait echo "āœ… All workers stopped" exit 0 @@ -74,11 +75,28 @@ echo "šŸ’¾ Starting audio persistence worker (1 worker for audio queue)..." uv run python -m advanced_omi_backend.workers.rq_worker_entry audio & AUDIO_PERSISTENCE_WORKER_PID=$! -# Start 1 audio stream worker for Deepgram +# Start 1 audio stream worker for Deepgram (only if DEEPGRAM_API_KEY is set) # Single worker ensures sequential processing of audio chunks -echo "šŸŽµ Starting audio stream Deepgram worker (1 worker for sequential processing)..." -uv run python -m advanced_omi_backend.workers.audio_stream_deepgram_worker & -AUDIO_STREAM_WORKER_PID=$! +if [ -n "$DEEPGRAM_API_KEY" ]; then + echo "šŸŽµ Starting audio stream Deepgram worker (1 worker for sequential processing)..." + uv run python -m advanced_omi_backend.workers.audio_stream_deepgram_worker & + AUDIO_STREAM_WORKER_PID=$! +else + echo "ā­ļø Skipping Deepgram audio stream worker (DEEPGRAM_API_KEY not set)" + AUDIO_STREAM_WORKER_PID="" +fi + +# Start 1 audio stream worker for Parakeet (only if PARAKEET_ASR_URL or OFFLINE_ASR_TCP_URI is set) +# Single worker ensures sequential processing of audio chunks +PARAKEET_URL="${PARAKEET_ASR_URL:-${OFFLINE_ASR_TCP_URI:-}}" +if [ -n "$PARAKEET_URL" ]; then + echo "šŸŽ¤ Starting audio stream Parakeet worker (1 worker for sequential processing)..." + uv run python -m advanced_omi_backend.workers.audio_stream_parakeet_worker & + PARAKEET_STREAM_WORKER_PID=$! +else + echo "ā­ļø Skipping Parakeet audio stream worker (PARAKEET_ASR_URL or OFFLINE_ASR_TCP_URI not set)" + PARAKEET_STREAM_WORKER_PID="" +fi echo "āœ… All workers started:" echo " - RQ worker 1: PID $RQ_WORKER_1_PID (transcription, memory, default)" @@ -88,7 +106,12 @@ echo " - RQ worker 4: PID $RQ_WORKER_4_PID (transcription, memory, default)" echo " - RQ worker 5: PID $RQ_WORKER_5_PID (transcription, memory, default)" echo " - RQ worker 6: PID $RQ_WORKER_6_PID (transcription, memory, default)" echo " - Audio persistence worker: PID $AUDIO_PERSISTENCE_WORKER_PID (audio queue - file rotation)" -echo " - Audio stream worker: PID $AUDIO_STREAM_WORKER_PID (Redis Streams consumer - sequential processing)" +if [ -n "$AUDIO_STREAM_WORKER_PID" ]; then + echo " - Audio stream Deepgram worker: PID $AUDIO_STREAM_WORKER_PID (Redis Streams consumer - sequential processing)" +fi +if [ -n "$PARAKEET_STREAM_WORKER_PID" ]; then + echo " - Audio stream Parakeet worker: PID $PARAKEET_STREAM_WORKER_PID (Redis Streams consumer - sequential processing)" +fi # Wait for any process to exit wait -n @@ -102,7 +125,7 @@ kill $RQ_WORKER_4_PID 2>/dev/null || true kill $RQ_WORKER_5_PID 2>/dev/null || true kill $RQ_WORKER_6_PID 2>/dev/null || true kill $AUDIO_PERSISTENCE_WORKER_PID 2>/dev/null || true -kill $AUDIO_STREAM_WORKER_PID 2>/dev/null || true +[ -n "$AUDIO_STREAM_WORKER_PID" ] && kill $AUDIO_STREAM_WORKER_PID 2>/dev/null || true wait echo "šŸ”„ All workers stopped" diff --git a/extras/speaker-recognition/.env.template b/extras/speaker-recognition/.env.template index 2ba3a1c7..3d653c62 100644 --- a/extras/speaker-recognition/.env.template +++ b/extras/speaker-recognition/.env.template @@ -27,7 +27,9 @@ SIMILARITY_THRESHOLD=0.15 # SPEAKER_SERVICE_HOST: Interface to bind to (0.0.0.0 = all interfaces, allows cross-network access) SPEAKER_SERVICE_HOST=0.0.0.0 SPEAKER_SERVICE_PORT=8085 -SPEAKER_SERVICE_URL=http://speaker-service:8085 + +# should be 127.0.0.1 here because speaker-service and speaker-service gpu are different +SPEAKER_SERVICE_URL=http://127.0.0.1:8085 # React Web UI Configuration REACT_UI_HOST=0.0.0.0 diff --git a/extras/speaker-recognition/README.md b/extras/speaker-recognition/README.md index 3196e349..4bfbc810 100644 --- a/extras/speaker-recognition/README.md +++ b/extras/speaker-recognition/README.md @@ -50,7 +50,29 @@ For non-interactive setup: ./init.sh --hf-token YOUR_TOKEN --compute-mode gpu --enable-https --server-ip 100.83.66.30 ``` -### 4. Start the system +### 4. Generate SSL Certificates (Required for Nginx) + +**āš ļø Important**: The nginx proxy requires SSL certificates to start. SSL certificates are optional through `wizard.sh`. If you haven't generated them during setup, you must create them manually: + +```bash +cd extras/speaker-recognition +# Generate certificates for localhost (default) +bash ssl/generate-ssl.sh localhost + +# Or generate for a specific IP/domain (e.g., Tailscale IP) +bash ssl/generate-ssl.sh 100.83.66.30 +``` + +This creates: +- `ssl/server.crt` - SSL certificate +- `ssl/server.key` - Private key + +**Note**: If SSL certificates are missing, nginx will fail to start with errors like: +``` +cannot load certificate "/etc/nginx/ssl/server.crt": BIO_new_file() failed +``` + +### 5. Start the system ```bash # For CPU-only docker compose --profile cpu up --build -d @@ -73,7 +95,7 @@ docker compose --profile cpu down docker compose --profile gpu down ``` -### 5. Access the Web UI +### 6. Access the Web UI **HTTPS Mode (Recommended for microphone access):** - **Secure Access**: https://localhost:8444/ or https://your-ip:8444/ @@ -85,7 +107,7 @@ docker compose --profile gpu down **Microphone access requires HTTPS for network connections (not just localhost).** -### 6. Get Started +### 7. Get Started 1. **Create a user** using the sidebar 2. **Upload audio** in the "Audio Viewer" page 3. **Annotate segments** in the "Annotation" page @@ -393,9 +415,20 @@ The React UI is configured with HTTPS enabled by default (`REACT_UI_HTTPS=true`) ## 🚨 Troubleshooting +**Nginx failing to start with SSL certificate errors?** +- Error: `cannot load certificate "/etc/nginx/ssl/server.crt": BIO_new_file() failed` +- **Solution**: Generate SSL certificates (see step 4 in Quick Start): + ```bash + cd extras/speaker-recognition + bash ssl/generate-ssl.sh localhost + ``` +- Verify certificates exist: `ls -la ssl/server.crt ssl/server.key` +- Restart nginx: `docker compose --profile cpu restart nginx` (or `--profile gpu`) + **Can't access the web UI?** - Check if services are running: `docker compose --profile cpu ps` (or `--profile gpu`) - View logs: `docker compose --profile cpu logs web-ui` +- Check nginx logs: `docker compose --profile cpu logs nginx` **Speaker service not responding?** - Check backend logs: `docker compose --profile cpu logs speaker-service` diff --git a/extras/speaker-recognition/docker-compose.yml b/extras/speaker-recognition/docker-compose.yml index c0821de5..364e81c5 100644 --- a/extras/speaker-recognition/docker-compose.yml +++ b/extras/speaker-recognition/docker-compose.yml @@ -29,7 +29,7 @@ services: - DEEPGRAM_API_KEY=${DEEPGRAM_API_KEY} restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://speaker-service:8085/health"] + test: ["CMD", "curl", "-f", "http://localhost:8085/health"] interval: 30s timeout: 10s retries: 3 @@ -96,7 +96,8 @@ services: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./ssl:/etc/nginx/ssl:ro depends_on: - - web-ui + web-ui: + condition: service_healthy restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "-k", "https://localhost/health"] diff --git a/extras/speaker-recognition/nginx.conf.template b/extras/speaker-recognition/nginx.conf.template index 38a8604a..68a40658 100644 --- a/extras/speaker-recognition/nginx.conf.template +++ b/extras/speaker-recognition/nginx.conf.template @@ -42,7 +42,7 @@ http { # Upstream services upstream speaker_service { - server speaker-service:8085; + server http://127.0.0.1:8085; } upstream web_ui { @@ -51,7 +51,8 @@ http { # HTTPS Server server { - listen 443 ssl http2; + listen 443 ssl; + http2 on; server_name localhost TAILSCALE_IP; # SSL Configuration diff --git a/wizard.py b/wizard.py index cfc5b861..6bb82696 100755 --- a/wizard.py +++ b/wizard.py @@ -158,7 +158,7 @@ def run_service_setup(service_name, selected_services, https_enabled=False, serv # For advanced backend, pass URLs of other selected services and HTTPS config cmd = service['cmd'].copy() if 'speaker-recognition' in selected_services: - cmd.extend(['--speaker-service-url', 'http://host.docker.internal:8085']) + cmd.extend(['--speaker-service-url', 'http://127.0.0.1:8085']) if 'asr-services' in selected_services: cmd.extend(['--parakeet-asr-url', 'http://host.docker.internal:8767']) @@ -174,8 +174,34 @@ def run_service_setup(service_name, selected_services, https_enabled=False, serv if service_name == 'speaker-recognition' and https_enabled and server_ip: cmd.extend(['--enable-https', '--server-ip', server_ip]) - # For speaker-recognition, try to pass API keys and config if available + # For speaker-recognition, validate HF_TOKEN is required if service_name == 'speaker-recognition': + # HF_TOKEN is required for speaker-recognition + speaker_env_path = 'extras/speaker-recognition/.env' + hf_token = read_env_value(speaker_env_path, 'HF_TOKEN') + + # Check if HF_TOKEN is missing or is a placeholder + if not hf_token or is_placeholder(hf_token, 'your_huggingface_token_here', 'your-huggingface-token-here', 'hf_xxxxx'): + console.print("\n[red][ERROR][/red] HF_TOKEN is required for speaker-recognition service") + console.print("[yellow]Speaker recognition requires a Hugging Face token to download models[/yellow]") + console.print("Get your token from: https://huggingface.co/settings/tokens") + console.print() + + # Prompt for HF_TOKEN + try: + hf_token_input = console.input("[cyan]Enter your HF_TOKEN[/cyan]: ").strip() + if not hf_token_input or is_placeholder(hf_token_input, 'your_huggingface_token_here', 'your-huggingface-token-here', 'hf_xxxxx'): + console.print("[red][ERROR][/red] Invalid HF_TOKEN provided. Speaker-recognition setup cancelled.") + return False + hf_token = hf_token_input + except EOFError: + console.print("[red][ERROR][/red] HF_TOKEN is required. Speaker-recognition setup cancelled.") + return False + + # Pass HF Token to init script + cmd.extend(['--hf-token', hf_token]) + console.print("[green][SUCCESS][/green] HF_TOKEN configured") + # Pass Deepgram API key from backend if available backend_env_path = 'backends/advanced/.env' deepgram_key = read_env_value(backend_env_path, 'DEEPGRAM_API_KEY') @@ -183,13 +209,6 @@ def run_service_setup(service_name, selected_services, https_enabled=False, serv cmd.extend(['--deepgram-api-key', deepgram_key]) console.print("[blue][INFO][/blue] Found existing DEEPGRAM_API_KEY from backend config, reusing") - # Pass HF Token from existing speaker recognition .env if available - speaker_env_path = 'extras/speaker-recognition/.env' - hf_token = read_env_value(speaker_env_path, 'HF_TOKEN') - if hf_token and not is_placeholder(hf_token, 'your_huggingface_token_here', 'your-huggingface-token-here'): - cmd.extend(['--hf-token', hf_token]) - console.print("[blue][INFO][/blue] Found existing HF_TOKEN, reusing") - # Pass compute mode from existing .env if available compute_mode = read_env_value(speaker_env_path, 'COMPUTE_MODE') if compute_mode in ['cpu', 'gpu']: From 257c7c16b8746f989a2837c187fdd71a7ead5f88 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:34:24 -0500 Subject: [PATCH 22/25] cuda variants --- .../advanced-docker-compose-build.yml | 150 +++++++++++++++--- 1 file changed, 126 insertions(+), 24 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 19643430..96c6ec02 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -6,11 +6,18 @@ on: version: description: Optional version tag override (e.g. v1.2.3) required: false + use_self_hosted: + description: Run on self-hosted runner instead of default + type: boolean + default: false push: - branches: [ "main", "feat/*" ] + branches: [ "main" ] paths: - "*" - "backends/advanced/**" + - "extras/asr-services/**" + - "extras/speaker-recognition/**" + - "extras/openmemory-mcp/**" - ".github/workflows/advanced-docker-compose-build.yml" tags: - "v*" @@ -26,9 +33,10 @@ env: jobs: build-self-hosted: + if: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.use_self_hosted == 'true') || vars.USE_SELF_HOSTED == 'true' }} runs-on: [self-hosted, linux, x64] timeout-minutes: 60 - continue-on-error: true + continue-on-error: false env: ADVANCED_ENV: ${{ secrets.ADVANCED_ENV }} RUNNER_FLAVOUR: self-hosted @@ -45,6 +53,18 @@ jobs: - 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 @@ -73,6 +93,7 @@ jobs: 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 != '' @@ -96,14 +117,14 @@ jobs: run: | if [ -n "${{ github.event.inputs.version }}" ]; then VERSION="${{ github.event.inputs.version }}" - elif [ "${{ github.ref_type }}" = "tag" ]; then - VERSION="${{ github.ref_name }}" + elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then + VERSION="${GITHUB_REF#refs/tags/}" else VERSION="sha-${GITHUB_SHA::7}" fi - echo "VERSION=$VERSION" | tee -a "$GITHUB_OUTPUT" + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - - name: Build, push, and prune services sequentially + - name: Build, tag, and push services sequentially with version env: OWNER: ${{ github.repository_owner }} VERSION: ${{ steps.version.outputs.VERSION }} @@ -111,49 +132,130 @@ jobs: set -euo pipefail docker compose version OWNER_LC=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]') - service_specs=( + + # 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|." - "parakeet-asr|parakeet-asr|../../extras/asr-services/docker-compose.yml|../../extras/asr-services" - "speaker-service|speaker-recognition|../../extras/speaker-recognition/docker-compose.yml|../../extras/speaker-recognition" + "openmemory-mcp|openmemory-mcp|../../extras/openmemory-mcp/docker-compose.yml|../../extras/openmemory-mcp" ) - - for spec in "${service_specs[@]}"; do + + # 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 $svc_repo" + + 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 - - img_id=$(docker images --format "{{.ID}}" --filter "reference=${svc_repo}:*" | head -n1) + # 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 "Removing local image tags for $svc_repo" + echo "Pushing $latest_image" + docker push "$latest_image" + + # Clean up local tags docker image rm -f "$target_image" || true - docker image rm -f "$img_id" || true - echo "Pruning Docker builder cache and unused data" - docker builder prune -af || true - docker system prune -af || 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::" + # Default runner build (primary): runs when self-hosted flag is not set build-default: - needs: build-self-hosted - if: needs.build-self-hosted.result != 'success' + if: ${{ !(github.event_name == 'workflow_dispatch' && github.event.inputs.use_self_hosted == 'true') && vars.USE_SELF_HOSTED != 'true' }} runs-on: ubuntu-latest timeout-minutes: 60 env: From 4d968e0e8fee84568288e38b196f49434505f480 Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:15:39 -0500 Subject: [PATCH 23/25] Refactor GitHub Actions workflow and enhance audio stream worker shutdown handling --- .../advanced-docker-compose-build.yml | 28 +++---------------- .../workers/audio_stream_parakeet_worker.py | 22 ++++++++++++--- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/.github/workflows/advanced-docker-compose-build.yml b/.github/workflows/advanced-docker-compose-build.yml index 96c6ec02..5acc717e 100644 --- a/.github/workflows/advanced-docker-compose-build.yml +++ b/.github/workflows/advanced-docker-compose-build.yml @@ -6,10 +6,6 @@ on: version: description: Optional version tag override (e.g. v1.2.3) required: false - use_self_hosted: - description: Run on self-hosted runner instead of default - type: boolean - default: false push: branches: [ "main" ] paths: @@ -32,20 +28,18 @@ env: REGISTRY: ghcr.io jobs: - build-self-hosted: - if: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.use_self_hosted == 'true') || vars.USE_SELF_HOSTED == 'true' }} - runs-on: [self-hosted, linux, x64] + build-default: + runs-on: ubuntu-latest timeout-minutes: 60 - continue-on-error: false env: ADVANCED_ENV: ${{ secrets.ADVANCED_ENV }} - RUNNER_FLAVOUR: self-hosted + RUNNER_FLAVOUR: ubuntu-latest defaults: run: shell: bash working-directory: backends/advanced - steps: &advanced_build_steps + steps: - name: Show selected runner run: echo "Workflow running on ${RUNNER_FLAVOUR} runner" working-directory: . @@ -252,17 +246,3 @@ jobs: echo "Built and pushed images with version tag: ${VERSION}" echo "Images pushed to: $REGISTRY/$OWNER_LC/" echo "::endgroup::" - - # Default runner build (primary): runs when self-hosted flag is not set - build-default: - if: ${{ !(github.event_name == 'workflow_dispatch' && github.event.inputs.use_self_hosted == 'true') && vars.USE_SELF_HOSTED != 'true' }} - 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: *advanced_build_steps diff --git a/backends/advanced/src/advanced_omi_backend/workers/audio_stream_parakeet_worker.py b/backends/advanced/src/advanced_omi_backend/workers/audio_stream_parakeet_worker.py index aa8b9c6a..1ffae49c 100644 --- a/backends/advanced/src/advanced_omi_backend/workers/audio_stream_parakeet_worker.py +++ b/backends/advanced/src/advanced_omi_backend/workers/audio_stream_parakeet_worker.py @@ -54,9 +54,11 @@ async def main(): ) # Setup signal handlers for graceful shutdown - def signal_handler(signum, frame): + shutdown_event = asyncio.Event() + + def signal_handler(signum, _frame): logger.info(f"Received signal {signum}, shutting down...") - asyncio.create_task(consumer.stop()) + shutdown_event.set() signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) @@ -64,8 +66,20 @@ def signal_handler(signum, frame): try: logger.info("āœ… Parakeet worker ready") - # This blocks until consumer is stopped - await consumer.start_consuming() + # This blocks until consumer is stopped or shutdown signaled + consume_task = asyncio.create_task(consumer.start_consuming()) + shutdown_task = asyncio.create_task(shutdown_event.wait()) + + done, pending = await asyncio.wait( + [consume_task, shutdown_task], + return_when=asyncio.FIRST_COMPLETED + ) + + # Cancel pending tasks + for task in pending: + task.cancel() + + await consumer.stop() except Exception as e: logger.error(f"Worker error: {e}", exc_info=True) From e0ef7f463f08d7cbd33188e29a211702f776bafe Mon Sep 17 00:00:00 2001 From: 0xrushi <6279035+0xrushi@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:09:52 -0500 Subject: [PATCH 24/25] Update service URLs for speaker recognition and Nginx configuration --- Docs/init-system.md | 2 +- extras/speaker-recognition/nginx.conf.template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Docs/init-system.md b/Docs/init-system.md index 4890ea27..fb9c1763 100644 --- a/Docs/init-system.md +++ b/Docs/init-system.md @@ -101,7 +101,7 @@ When using the orchestrated setup, service URLs are automatically configured: | Service Selected | Backend Gets Configured With | |----------------------|-------------------------------------------------------------------| -| Speaker Recognition | `SPEAKER_SERVICE_URL=http://127.0.0.1:8085` | +| Speaker Recognition | `SPEAKER_SERVICE_URL=http://host.docker.internal:8085` | | ASR Services | `PARAKEET_ASR_URL=http://host.docker.internal:8767` | This eliminates the need to manually configure service URLs when running services on the same machine. diff --git a/extras/speaker-recognition/nginx.conf.template b/extras/speaker-recognition/nginx.conf.template index 68a40658..da22ab28 100644 --- a/extras/speaker-recognition/nginx.conf.template +++ b/extras/speaker-recognition/nginx.conf.template @@ -42,7 +42,7 @@ http { # Upstream services upstream speaker_service { - server http://127.0.0.1:8085; + server speaker-service:8085; } upstream web_ui { From 707796493ef9f43de1a9396f62c53210f0a81999 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:18:00 +0000 Subject: [PATCH 25/25] Add health status checker and update setup instructions - Introduced a new `status.py` script for checking the health status of services, including container and HTTP health checks. - Added a `status.sh` script for easier execution of the health checker. - Updated `CLAUDE.md` to include instructions for setting up the test environment and running the health status checker. - Enhanced `setup-requirements.txt` by adding `requests` as a dependency. - Modified `.dockerignore` to include `Caddyfile` for better Docker management. - Updated service URL for speaker recognition in `wizard.py` to use Docker service name. --- CLAUDE.md | 7 + backends/advanced/.dockerignore | 3 +- setup-requirements.txt | 3 +- status.py | 358 ++++++++++++++++++++++++++++++++ status.sh | 2 + wizard.py | 2 +- 6 files changed, 372 insertions(+), 3 deletions(-) create mode 100644 status.py create mode 100755 status.sh diff --git a/CLAUDE.md b/CLAUDE.md index 9c06c934..1efb4a2e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/ ``` @@ -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 diff --git a/backends/advanced/.dockerignore b/backends/advanced/.dockerignore index 2f10e879..38c6284e 100644 --- a/backends/advanced/.dockerignore +++ b/backends/advanced/.dockerignore @@ -16,4 +16,5 @@ !nginx.conf !nginx.conf.template !start.sh -!start-workers.sh \ No newline at end of file +!start-workers.sh +!Caddyfile \ No newline at end of file diff --git a/setup-requirements.txt b/setup-requirements.txt index de5610c1..284ce5bb 100644 --- a/setup-requirements.txt +++ b/setup-requirements.txt @@ -1,3 +1,4 @@ # Dependencies for Friend-Lite root setup orchestrator rich>=13.0.0 -python-dotenv \ No newline at end of file +python-dotenv +requests>=2.31.0 \ No newline at end of file diff --git a/status.py b/status.py new file mode 100644 index 00000000..1ae9a353 --- /dev/null +++ b/status.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +""" +Friend-Lite Health Status Checker +Show runtime health status of all services +""" + +import argparse +import subprocess +import sys +import json +import requests +from pathlib import Path +from typing import Dict, List, Any, Optional + +from rich import print as rprint +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +from rich.live import Live +from rich.layout import Layout + +# Import service definitions from services.py +from services import SERVICES, check_service_configured + +console = Console() + +# Health check endpoints +HEALTH_ENDPOINTS = { + 'backend': 'http://localhost:8000/health', + 'speaker-recognition': 'http://localhost:8085/health', + 'openmemory-mcp': 'http://localhost:8765/docs', # No health endpoint, check docs +} + + +def get_container_status(service_name: str) -> Dict[str, Any]: + """Get Docker container status for a service""" + service = SERVICES[service_name] + service_path = Path(service['path']) + + if not service_path.exists(): + return {'status': 'not_found', 'containers': []} + + try: + # Get container status using docker compose ps + cmd = ['docker', 'compose', 'ps', '--format', 'json'] + + # Handle special profiles for backend (HTTPS) + if service_name == 'backend': + caddyfile_path = service_path / 'Caddyfile' + if caddyfile_path.exists(): + cmd = ['docker', 'compose', '--profile', 'https', 'ps', '--format', 'json'] + + # Handle speaker-recognition profiles + if service_name == 'speaker-recognition': + from dotenv import dotenv_values + env_file = service_path / '.env' + if env_file.exists(): + env_values = dotenv_values(env_file) + compute_mode = env_values.get('COMPUTE_MODE', 'cpu') + if compute_mode == 'gpu': + cmd = ['docker', 'compose', '--profile', 'gpu', 'ps', '--format', 'json'] + else: + cmd = ['docker', 'compose', '--profile', 'cpu', 'ps', '--format', 'json'] + + result = subprocess.run( + cmd, + cwd=service_path, + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode != 0: + return {'status': 'error', 'containers': [], 'error': result.stderr} + + # Parse JSON output (one JSON object per line) + containers = [] + for line in result.stdout.strip().split('\n'): + if line: + try: + container = json.loads(line) + containers.append({ + 'name': container.get('Name', 'unknown'), + 'state': container.get('State', 'unknown'), + 'status': container.get('Status', 'unknown'), + 'health': container.get('Health', 'none') + }) + except json.JSONDecodeError: + continue + + if not containers: + return {'status': 'stopped', 'containers': []} + + # Determine overall status + all_running = all(c['state'] == 'running' for c in containers) + any_running = any(c['state'] == 'running' for c in containers) + + if all_running: + status = 'running' + elif any_running: + status = 'partial' + else: + status = 'stopped' + + return {'status': status, 'containers': containers} + + except subprocess.TimeoutExpired: + return {'status': 'timeout', 'containers': []} + except Exception as e: + return {'status': 'error', 'containers': [], 'error': str(e)} + + +def check_http_health(url: str, timeout: int = 5) -> Dict[str, Any]: + """Check HTTP health endpoint""" + try: + response = requests.get(url, timeout=timeout) + + if response.status_code == 200: + # Try to parse JSON response + try: + data = response.json() + return {'healthy': True, 'status_code': 200, 'data': data} + except json.JSONDecodeError: + return {'healthy': True, 'status_code': 200, 'data': None} + else: + return {'healthy': False, 'status_code': response.status_code, 'data': None} + + except requests.exceptions.ConnectionError: + return {'healthy': False, 'error': 'Connection refused'} + except requests.exceptions.Timeout: + return {'healthy': False, 'error': 'Timeout'} + except Exception as e: + return {'healthy': False, 'error': str(e)} + + +def get_service_health(service_name: str) -> Dict[str, Any]: + """Get comprehensive health status for a service""" + # Check if configured + if not check_service_configured(service_name): + return { + 'configured': False, + 'container_status': 'not_configured', + 'health': None + } + + # Get container status + container_info = get_container_status(service_name) + + # Check HTTP health endpoint if available + health_check = None + if service_name in HEALTH_ENDPOINTS: + url = HEALTH_ENDPOINTS[service_name] + health_check = check_http_health(url) + + return { + 'configured': True, + 'container_status': container_info['status'], + 'containers': container_info.get('containers', []), + 'health': health_check + } + + +def show_quick_status(): + """Show quick status overview""" + console.print("\nšŸ„ [bold]Friend-Lite Health Status[/bold]\n") + + table = Table(title="Service Status Overview") + table.add_column("Service", style="cyan", no_wrap=True) + table.add_column("Config", justify="center") + table.add_column("Containers", justify="center") + table.add_column("Health", justify="center") + table.add_column("Description", style="dim") + + for service_name, service_info in SERVICES.items(): + status = get_service_health(service_name) + + # Config status + config_icon = "āœ…" if status['configured'] else "āŒ" + + # Container status + if not status['configured']: + container_icon = "⚪" + elif status['container_status'] == 'running': + container_icon = "🟢" + elif status['container_status'] == 'partial': + container_icon = "🟔" + elif status['container_status'] == 'stopped': + container_icon = "šŸ”“" + else: + container_icon = "⚫" + + # Health status + if status['health'] is None: + health_icon = "⚪" + elif status['health'].get('healthy'): + health_icon = "āœ…" + else: + health_icon = "āŒ" + + table.add_row( + service_name, + config_icon, + container_icon, + health_icon, + service_info['description'] + ) + + console.print(table) + + # Legend + console.print("\n[dim]Legend:[/dim]") + console.print("[dim] Containers: 🟢 Running | 🟔 Partial | šŸ”“ Stopped | ⚪ Not Configured | ⚫ Error[/dim]") + console.print("[dim] Health: āœ… Healthy | āŒ Unhealthy | ⚪ No Endpoint[/dim]") + + +def show_detailed_status(): + """Show detailed status with backend health breakdown""" + console.print("\nšŸ„ [bold]Friend-Lite Detailed Health Status[/bold]\n") + + # Get all service statuses + for service_name, service_info in SERVICES.items(): + status = get_service_health(service_name) + + # Service header + if status['configured']: + header = f"šŸ“¦ {service_name.upper()}" + else: + header = f"šŸ“¦ {service_name.upper()} (Not Configured)" + + console.print(f"\n[bold cyan]{header}[/bold cyan]") + console.print(f"[dim]{service_info['description']}[/dim]") + + if not status['configured']: + console.print("[yellow] āš ļø Not configured (no .env file)[/yellow]") + continue + + # Container status + console.print(f"\n [bold]Containers:[/bold]") + if status['container_status'] == 'running': + console.print(f" [green]🟢 All containers running[/green]") + elif status['container_status'] == 'partial': + console.print(f" [yellow]🟔 Some containers running[/yellow]") + elif status['container_status'] == 'stopped': + console.print(f" [red]šŸ”“ All containers stopped[/red]") + else: + console.print(f" [red]⚫ Error checking containers[/red]") + + # Show container details + for container in status.get('containers', []): + state_icon = "🟢" if container['state'] == 'running' else "šŸ”“" + health_status = f" ({container['health']})" if container['health'] != 'none' else "" + console.print(f" {state_icon} {container['name']}: {container['status']}{health_status}") + + # HTTP Health check + if status['health'] is not None: + console.print(f"\n [bold]HTTP Health:[/bold]") + + if status['health'].get('healthy'): + console.print(f" [green]āœ… Healthy[/green]") + + # For backend, show detailed health data + if service_name == 'backend' and status['health'].get('data'): + health_data = status['health']['data'] + + # Overall status + overall_status = health_data.get('status', 'unknown') + if overall_status == 'healthy': + console.print(f" Overall: [green]{overall_status}[/green]") + elif overall_status == 'degraded': + console.print(f" Overall: [yellow]{overall_status}[/yellow]") + else: + console.print(f" Overall: [red]{overall_status}[/red]") + + # Critical services + services = health_data.get('services', {}) + console.print(f"\n [bold]Critical Services:[/bold]") + + for svc_name in ['mongodb', 'redis']: + if svc_name in services: + svc = services[svc_name] + if svc.get('healthy'): + console.print(f" [green]āœ… {svc_name}: {svc.get('status', 'ok')}[/green]") + else: + console.print(f" [red]āŒ {svc_name}: {svc.get('status', 'error')}[/red]") + + # Optional services + console.print(f"\n [bold]Optional Services:[/bold]") + optional_services = ['audioai', 'memory_service', 'speech_to_text', 'speaker_recognition', 'openmemory_mcp'] + for svc_name in optional_services: + if svc_name in services: + svc = services[svc_name] + if svc.get('healthy'): + console.print(f" [green]āœ… {svc_name}: {svc.get('status', 'ok')}[/green]") + else: + console.print(f" [yellow]āš ļø {svc_name}: {svc.get('status', 'degraded')}[/yellow]") + + # Configuration info + config = health_data.get('config', {}) + if config: + console.print(f"\n [bold]Configuration:[/bold]") + console.print(f" LLM: {config.get('llm_provider', 'unknown')} ({config.get('llm_model', 'unknown')})") + console.print(f" Transcription: {config.get('transcription_service', 'unknown')}") + console.print(f" Active Clients: {config.get('active_clients', 0)}") + else: + error = status['health'].get('error', 'Unknown error') + console.print(f" [red]āŒ Unhealthy: {error}[/red]") + + console.print("") # Spacing + + +def show_json_status(): + """Show status in JSON format for programmatic consumption""" + status_data = {} + + for service_name in SERVICES.keys(): + status_data[service_name] = get_service_health(service_name) + + print(json.dumps(status_data, indent=2)) + + +def main(): + parser = argparse.ArgumentParser( + description="Friend-Lite Health Status Checker", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + ./status.sh Show quick status overview + ./status.sh --detailed Show detailed health information + ./status.sh --json Output status in JSON format + """ + ) + + parser.add_argument( + '--detailed', '-d', + action='store_true', + help='Show detailed health information including backend service breakdown' + ) + + parser.add_argument( + '--json', '-j', + action='store_true', + help='Output status in JSON format' + ) + + args = parser.parse_args() + + if args.json: + show_json_status() + elif args.detailed: + show_detailed_status() + else: + show_quick_status() + + console.print("\nšŸ’” [dim]Tip: Use './status.sh --detailed' for comprehensive health checks[/dim]\n") + + +if __name__ == "__main__": + main() diff --git a/status.sh b/status.sh new file mode 100755 index 00000000..a66fe459 --- /dev/null +++ b/status.sh @@ -0,0 +1,2 @@ +#!/bin/bash +uv run --with-requirements setup-requirements.txt python status.py "$@" diff --git a/wizard.py b/wizard.py index 6bb82696..a1875028 100755 --- a/wizard.py +++ b/wizard.py @@ -158,7 +158,7 @@ def run_service_setup(service_name, selected_services, https_enabled=False, serv # For advanced backend, pass URLs of other selected services and HTTPS config cmd = service['cmd'].copy() if 'speaker-recognition' in selected_services: - cmd.extend(['--speaker-service-url', 'http://127.0.0.1:8085']) + cmd.extend(['--speaker-service-url', 'http://speaker-service:8085']) if 'asr-services' in selected_services: cmd.extend(['--parakeet-asr-url', 'http://host.docker.internal:8767'])