Merge remote-tracking branch 'origin/main' into feat/provisional-vad

# Conflicts:
#	api/routes/main.py
#	api/tests/test_active_calls.py
#	pipecat
#	scripts/rolling_update.sh
This commit is contained in:
Abhishek Kumar 2026-06-29 13:43:57 +05:30
commit cc658131aa
28 changed files with 1101 additions and 31 deletions

101
.conductor/README.md Normal file
View file

@ -0,0 +1,101 @@
# Conductor workspace setup
This directory makes [Conductor](https://conductor.build) workspaces (git
worktrees) self-contained: each one installs its own deps and runs its own
backend + UI on a dedicated port range, so you can develop several branches at
once without collisions.
## How it works
Conductor gives every workspace a block of **10 ports** starting at
`$CONDUCTOR_PORT`. We use:
| Port | Service | Script |
| --------------- | ------------------ | ----------------------- |
| `CONDUCTOR_PORT` | UI (Next.js) | `run-ui.sh` |
| `CONDUCTOR_PORT + 1` | Backend (uvicorn) | `run-backend.sh` |
| `+2 .. +9` | reserved | — |
The UI sits on the base port so Conductor's **Open** button (`preview_urls`)
lands on it directly — preview templates only substitute `$CONDUCTOR_PORT`, with
no `+1` arithmetic. The UI is wired to its own workspace's backend
(`NEXT_PUBLIC_BACKEND_URL`, `BACKEND_URL`) and the backend's CORS / `UI_APP_URL`
point back at its own UI — all derived from `$CONDUCTOR_PORT`, so no two
workspaces interfere.
### Shared, NOT per-workspace
Postgres, Redis, and MinIO run as a **single shared Docker stack** (compose
project `dograh`). `setup.sh` brings it up idempotently with
`COMPOSE_PROJECT_NAME=dograh` so every workspace reuses the same one instead of
fighting over the fixed 5432/6379/9000 ports. All workspaces therefore share one
database — fine for app servers, but it means the **Arq worker should run in only
one workspace** (`run-worker.sh`), since a single worker drains the shared queue
for everyone.
## The Run menu
> **Important:** a Conductor workspace runs **one run script at a time**.
> `run_mode = "concurrent"` lets *different workspaces* run simultaneously — it
> does **not** let you click two run buttons in the *same* workspace. That's why
> **dev** starts the UI and backend together instead of relying on two buttons.
The Run dropdown offers:
- **dev** (default) — UI (`$CONDUCTOR_PORT`) **and** backend (`$CONDUCTOR_PORT+1`)
together, via `concurrently`. **Use this day to day.**
- **ui** — Next.js only, for debugging the frontend alone.
- **backend** — uvicorn only, for debugging the API alone.
- **worker** — Arq worker; start in just one workspace.
Hit **dev** and both servers come up; click Conductor's **Open** button to launch
the UI. Start **worker** once (e.g. in your primary workspace) when you need
background jobs — the same way you used to run a single arq worker in VS Code.
## Creating a new workspace
In Conductor: **New Workspace** → pick a branch. `setup.sh` then runs
automatically and:
1. copies the gitignored env files from your main checkout (see
[Environment files](#environment-files)),
2. checks out the `pipecat` submodule,
3. builds `venv` (Python 3.13) + installs backend/pipecat deps,
4. `npm install` for the UI,
5. ensures the shared Docker stack is up,
6. runs `alembic upgrade head`.
The first setup is slow (deps); afterwards the Run buttons are instant.
## Environment files
The app's env files hold real secrets, so they're **gitignored** and never
committed — a fresh worktree won't have them. `setup.sh` copies them from your
main checkout (`$CONDUCTOR_ROOT_PATH`) into each new workspace:
| File | Used by |
| ----------------------------- | -------------------------------- |
| `api/.env` | backend (DB/Redis URLs, secrets) |
| `api/.env.test` | backend test runs |
| `ui/.env` | UI (backend URL, public config) |
| `ui/.env.local` | UI secrets (Stack/PostHog/etc.) |
| `ui/.env.sentry-build-plugin` | UI Sentry source-map upload |
The copy is idempotent (only fills in what's missing), so re-running setup won't
clobber a workspace-local edit. **Add a new env file?** List it in the loop near
the top of `setup.sh`.
## Files
| File | Purpose |
| --------------------- | -------------------------------------------------- |
| `settings.toml` | Conductor config: setup + run scripts, preview_urls |
| `setup.sh` | One-time workspace bootstrap |
| `run-dev.sh` | Default run: UI + backend together (`concurrently`) |
| `run-ui.sh` | Foreground Next.js on `$CONDUCTOR_PORT` |
| `run-backend.sh` | Foreground uvicorn on `$CONDUCTOR_PORT + 1` |
| `run-worker.sh` | Foreground Arq worker (run in one workspace only) |
> Need machine-local tweaks (e.g. a different port base or skipping the worker)?
> Put them in `.conductor/settings.local.toml`, which is personal and not
> committed.

30
.conductor/run-backend.sh Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Conductor run script — Backend (FastAPI/uvicorn) for THIS workspace.
#
# Binds $CONDUCTOR_PORT + 1 and points CORS / UI_APP_URL at this workspace's UI
# (which runs on $CONDUCTOR_PORT). Runs in the FOREGROUND with exec (no &) so
# Conductor's SIGHUP cleanly tears it down.
set -euo pipefail
cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}"
UI_PORT="${CONDUCTOR_PORT:-8000}"
BACKEND_PORT="$((UI_PORT + 1))"
# Load the workspace's api/.env, then override the port-specific bits. The
# backend reads config via os.getenv, and these exports happen after the source,
# so they win over the values copied from the main checkout (which assume 8000/3000).
if [[ -f api/.env ]]; then set -a; # shellcheck disable=SC1091
source api/.env; set +a; fi
export FASTAPI_PORT="$BACKEND_PORT"
export UI_APP_URL="http://localhost:${UI_PORT}"
export CORS_ALLOWED_ORIGINS="http://localhost:${UI_PORT},http://127.0.0.1:${UI_PORT}"
if [[ ! -d venv ]]; then
echo "ERROR: venv missing. Re-run workspace setup (.conductor/setup.sh)." >&2
exit 1
fi
# shellcheck disable=SC1091
source venv/bin/activate
echo "[backend] workspace=${CONDUCTOR_WORKSPACE_NAME:-?} port=${BACKEND_PORT} ui=${UI_PORT}"
exec uvicorn api.app:app --host 0.0.0.0 --port "$BACKEND_PORT" --reload --reload-dir api

59
.conductor/run-dev.sh Executable file
View file

@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Conductor run script — full dev stack (UI + backend) for THIS workspace.
#
# A Conductor workspace runs ONE run script at a time (run_mode governs
# concurrency ACROSS workspaces, not multiple run buttons within one). So to get
# the UI AND the backend up together, we launch both with `concurrently`.
#
# Conductor stops a run with SIGHUP (then SIGKILL after 200ms). Neither npx nor
# concurrently reliably tears down their child tree on SIGHUP, so we supervise:
# trap the signal and recursively kill the whole tree ourselves. We do NOT exec,
# so this shell stays alive to handle the trap.
#
# Ports: UI on $CONDUCTOR_PORT, backend on $CONDUCTOR_PORT + 1.
set -uo pipefail
cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}"
UI_PORT="${CONDUCTOR_PORT:-8000}"
BACKEND_PORT="$((UI_PORT + 1))"
# Ensure node/npx is on PATH (load nvm + honor .nvmrc if needed).
if ! command -v npx >/dev/null 2>&1; then
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
# shellcheck disable=SC1091
[[ -s "$NVM_DIR/nvm.sh" ]] && . "$NVM_DIR/nvm.sh"
command -v nvm >/dev/null 2>&1 && nvm use >/dev/null 2>&1 || true
fi
# Prefer a locally-installed concurrently (fast); fall back to npx, which fetches
# it once then caches — so this also works in workspaces created before it existed.
if [[ -x ui/node_modules/.bin/concurrently ]]; then
RUNNER=(ui/node_modules/.bin/concurrently)
else
RUNNER=(npx --yes concurrently)
fi
# Recursively SIGTERM a process and every descendant (children first). npm/npx/
# next don't reliably forward signals, so we signal each PID in the tree directly.
kill_tree() {
local pid=$1 child
for child in $(pgrep -P "$pid" 2>/dev/null); do kill_tree "$child"; done
kill -TERM "$pid" 2>/dev/null || true
}
shutdown() {
trap - HUP INT TERM EXIT
[[ -n "${CHILD:-}" ]] && kill_tree "$CHILD"
exit 0
}
trap shutdown HUP INT TERM EXIT
echo "[dev] workspace=${CONDUCTOR_WORKSPACE_NAME:-?} ui=:${UI_PORT} backend=:${BACKEND_PORT}"
"${RUNNER[@]}" \
--names "ui,backend" \
--prefix-colors "magenta,cyan" \
--kill-others \
"bash .conductor/run-ui.sh" \
"bash .conductor/run-backend.sh" &
CHILD=$!
wait "$CHILD"

29
.conductor/run-ui.sh Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Conductor run script — UI (Next.js) for THIS workspace.
#
# Binds $CONDUCTOR_PORT (so Conductor's Open button / preview_urls land here) and
# talks to this workspace's backend on $CONDUCTOR_PORT + 1. Foreground exec (no &)
# so Conductor can stop it cleanly.
set -euo pipefail
cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}"
UI_PORT="${CONDUCTOR_PORT:-8000}"
BACKEND_PORT="$((UI_PORT + 1))"
BACKEND="http://localhost:${BACKEND_PORT}"
# Ensure node is on PATH (load nvm + honor .nvmrc if needed).
if ! command -v node >/dev/null 2>&1; then
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
# shellcheck disable=SC1091
[[ -s "$NVM_DIR/nvm.sh" ]] && . "$NVM_DIR/nvm.sh"
command -v nvm >/dev/null 2>&1 && nvm use >/dev/null 2>&1 || true
fi
# Shell env overrides .env files in Next.js, so this points the UI at the right
# backend for this workspace.
export BACKEND_URL="$BACKEND"
export NEXT_PUBLIC_BACKEND_URL="$BACKEND"
cd ui
echo "[ui] workspace=${CONDUCTOR_WORKSPACE_NAME:-?} port=${UI_PORT} backend=${BACKEND}"
exec npm run dev -- --port "$UI_PORT"

21
.conductor/run-worker.sh Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Conductor run script — Arq background worker.
#
# Run this in ONE workspace only. Every workspace shares the same Redis/Postgres,
# so a single arq worker drains the task queue for all of them — multiple workers
# would just fight over the same jobs. Foreground exec so Conductor stops it cleanly.
set -euo pipefail
cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}"
if [[ -f api/.env ]]; then set -a; # shellcheck disable=SC1091
source api/.env; set +a; fi
if [[ ! -d venv ]]; then
echo "ERROR: venv missing. Re-run workspace setup (.conductor/setup.sh)." >&2
exit 1
fi
# shellcheck disable=SC1091
source venv/bin/activate
echo "[worker] arq worker on shared Redis — workspace=${CONDUCTOR_WORKSPACE_NAME:-?}"
exec python -m arq api.tasks.arq.WorkerSettings --custom-log-dict api.tasks.arq.LOG_CONFIG

54
.conductor/settings.toml Normal file
View file

@ -0,0 +1,54 @@
# Conductor project config — shared, committed so every contributor (and every
# new workspace) gets the same per-worktree dev workflow.
#
# Each Conductor workspace is its own git worktree and gets its own 10-port range
# starting at $CONDUCTOR_PORT. We use:
# $CONDUCTOR_PORT -> ui (Next.js) <- Conductor's Open button lands here
# $CONDUCTOR_PORT + 1 -> backend (FastAPI/uvicorn)
# +2 .. +9 -> reserved for future per-workspace services
#
# Postgres/Redis/MinIO are a single SHARED Docker stack (project name "dograh"),
# not per-workspace — see .conductor/setup.sh and .conductor/README.md.
"$schema" = "https://conductor.build/schemas/settings.repo.schema.json"
# Conductor's Open button. The UI runs on $CONDUCTOR_PORT, so this opens it.
# (Preview URL templates only substitute $CONDUCTOR_PORT — no "+1" arithmetic —
# which is why the UI, the thing you actually open, sits on the base port.)
[[preview_urls]]
name = "UI"
url = "http://localhost:$CONDUCTOR_PORT"
[scripts]
# Runs once when a workspace is created: copies gitignored env files, inits the
# pipecat submodule, builds the venv + node_modules, ensures the shared Docker
# stack, and runs migrations.
setup = "bash .conductor/setup.sh"
# concurrent => multiple WORKSPACES can run their dev servers at the same time
# (each on its own CONDUCTOR_PORT range). NOTE: within a single workspace,
# Conductor runs ONE run script at a time — that's why the default "dev" script
# below starts the UI and backend together rather than relying on two buttons.
run_mode = "concurrent"
# Full dev stack: UI ($CONDUCTOR_PORT) + backend ($CONDUCTOR_PORT+1) together.
# Default Run button — this is the one to use day to day.
[scripts.run.dev]
command = "bash .conductor/run-dev.sh"
icon = "play"
default = true
# UI only (Next.js) on $CONDUCTOR_PORT — handy for debugging the frontend alone.
[scripts.run.ui]
command = "bash .conductor/run-ui.sh"
icon = "play"
# Backend only (FastAPI/uvicorn) on $CONDUCTOR_PORT + 1 — debugging the API alone.
[scripts.run.backend]
command = "bash .conductor/run-backend.sh"
icon = "server"
# Arq background worker. Start in ONE workspace only — all workspaces share the
# same Redis/Postgres, so a single worker drains the queue for everyone.
[scripts.run.worker]
command = "bash .conductor/run-worker.sh"
icon = "server"

91
.conductor/setup.sh Executable file
View file

@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Conductor setup script — runs ONCE when a workspace (git worktree) is created.
#
# A fresh worktree only has git-tracked files, so this recreates the rest: the
# gitignored env files, the pipecat submodule checkout, a Python venv,
# ui/node_modules, the shared local Docker stack, and the DB schema.
#
# Conductor injects: CONDUCTOR_ROOT_PATH (main checkout), CONDUCTOR_WORKSPACE_PATH,
# CONDUCTOR_WORKSPACE_NAME, CONDUCTOR_PORT (first of 10), CONDUCTOR_IS_LOCAL.
set -euo pipefail
ROOT="${CONDUCTOR_ROOT_PATH:-}"
WS="${CONDUCTOR_WORKSPACE_PATH:-$PWD}"
cd "$WS"
log() { printf '\n\033[1;36m[conductor-setup]\033[0m %s\n' "$*"; }
# 1) Copy the gitignored env files from the main checkout. These hold real
# secrets, so they're never committed — a fresh worktree won't have them. We copy
# only what's missing (idempotent), so re-running setup won't clobber local edits.
# (See README "Environment files" for the canonical list.)
if [[ -n "$ROOT" && "$ROOT" != "$WS" ]]; then
log "Copying gitignored env files from $ROOT"
for f in api/.env api/.env.test ui/.env ui/.env.local ui/.env.sentry-build-plugin; do
if [[ ! -f "$f" && -f "$ROOT/$f" ]]; then
mkdir -p "$(dirname "$f")"
cp "$ROOT/$f" "$f"
echo " copied $f"
fi
done
fi
# 2) pipecat submodule — REQUIRED here, NOT redundant with step 3.
# A fresh git worktree has an empty pipecat/. setup_requirements.sh --dev
# (step 3) deliberately SKIPS `git submodule update` (it assumes CI already
# checked out submodules) but still runs `uv pip install -e ./pipecat`, which
# fails unless the checkout is already on disk. So we populate it first.
log "Initializing git submodules (pipecat)"
git submodule update --init --recursive
# 3) Python venv with 3.13 + backend/pipecat deps ----------------------------
# Bare `python3` may be 3.14 here; setup_requirements.sh requires 3.12/3.13.
PY313="$(command -v python3.13 || true)"
if [[ -z "$PY313" ]]; then
echo "ERROR: python3.13 not found. Install it (e.g. brew install python@3.13)." >&2
exit 1
fi
if [[ ! -d venv ]]; then
log "Creating venv with $PY313 ($("$PY313" --version))"
"$PY313" -m venv venv
fi
# shellcheck disable=SC1091
source venv/bin/activate
log "Installing backend + pipecat deps (uv) — this is the slow step"
bash scripts/setup_requirements.sh --dev
# 4) UI deps -----------------------------------------------------------------
ensure_node() {
if ! command -v node >/dev/null 2>&1; then
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
# shellcheck disable=SC1091
[[ -s "$NVM_DIR/nvm.sh" ]] && . "$NVM_DIR/nvm.sh"
command -v nvm >/dev/null 2>&1 && nvm use >/dev/null 2>&1 || true
fi
}
ensure_node
log "Installing UI deps (npm install)"
( cd ui && npm install )
# 5) Shared local Docker stack (idempotent, pinned project name) -------------
# All workspaces share ONE stack named "dograh" so they never collide on the
# fixed Postgres/Redis/MinIO ports. If it's already up this is a no-op.
if command -v docker >/dev/null 2>&1; then
log "Ensuring shared Docker stack (COMPOSE_PROJECT_NAME=dograh)"
COMPOSE_PROJECT_NAME=dograh docker compose -f docker-compose-local.yaml up -d \
|| echo " (warning: could not start docker stack; start it manually)"
else
echo " (docker not found; start Postgres/Redis/MinIO yourself)"
fi
# 6) DB migrations (best-effort; shared DB, alembic is idempotent) -----------
if [[ -f api/.env ]]; then
log "Running DB migrations (alembic upgrade head)"
set -a; # shellcheck disable=SC1091
source api/.env; set +a
alembic -c api/alembic.ini upgrade head || echo " (warning: migrations skipped/failed — is the DB up?)"
fi
PORT="${CONDUCTOR_PORT:-8000}"
log "Setup complete for '${CONDUCTOR_WORKSPACE_NAME:-?}'."
echo " Run menu: backend -> :${PORT} ui -> :$((PORT + 1)) worker -> shared arq"

3
.gitignore vendored
View file

@ -4,6 +4,9 @@ __pycache__
.env.prod
.env.test
# Conductor personal/per-machine overrides (settings.toml IS committed)
.conductor/settings.local.toml
# logs and run directory on production
/logs/
/run/

View file

@ -12,6 +12,11 @@ UI_APP_URL="http://localhost:3000"
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/postgres"
REDIS_URL="redis://:redissecret@localhost:6379"
# Internal devops secret for deployment scripts and lifecycle hooks.
# scripts/rolling_update.sh sends this to protected operational endpoints via
# X-Dograh-Devops-Secret. Use a unique random value in production.
DOGRAH_DEVOPS_SECRET="change-me-dograh-devops-secret"
# AWS S3 Configuration
ENABLE_AWS_S3="false"
# AWS_ACCESS_KEY_ID=""

View file

@ -14,4 +14,6 @@ UI_APP_URL=http://localhost:3000
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/test_db"
REDIS_URL="redis://:redissecret@localhost:6379/0"
DOGRAH_DEVOPS_SECRET="test-dograh-devops-secret"
MINIO_PUBLIC_ENDPOINT=http://localhost:9000

View file

@ -54,6 +54,7 @@ STACK_AUTH_PROJECT_ID = os.getenv("STACK_AUTH_PROJECT_ID")
STACK_PUBLISHABLE_CLIENT_KEY = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY")
DOGRAH_MPS_SECRET_KEY = os.getenv("DOGRAH_MPS_SECRET_KEY", None)
MPS_API_URL = os.getenv("MPS_API_URL", "https://services.dograh.com")
DOGRAH_DEVOPS_SECRET = os.getenv("DOGRAH_DEVOPS_SECRET") or None
# Storage Configuration
ENABLE_AWS_S3 = os.getenv("ENABLE_AWS_S3", "false").lower() == "true"

View file

@ -1,4 +1,7 @@
from fastapi import APIRouter
import secrets
from typing import Annotated
from fastapi import APIRouter, Header, HTTPException, status
from loguru import logger
from pydantic import BaseModel
@ -131,8 +134,35 @@ class ActiveCallsResponse(BaseModel):
active_calls: int
DOGRAH_DEVOPS_SECRET_HEADER = "X-Dograh-Devops-Secret"
def _verify_devops_secret(
configured_secret: str | None,
provided_secret: str | None,
) -> None:
if not configured_secret:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Devops secret is not configured",
)
if not provided_secret or not secrets.compare_digest(
provided_secret,
configured_secret,
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden",
)
@router.get("/health/active-calls", response_model=ActiveCallsResponse)
async def active_calls() -> ActiveCallsResponse:
async def active_calls(
x_dograh_devops_secret: Annotated[
str | None,
Header(alias=DOGRAH_DEVOPS_SECRET_HEADER),
] = None,
) -> ActiveCallsResponse:
"""In-flight call count for THIS worker — the drain signal for deploys.
A deploy orchestrator polls this per worker and waits for zero before
@ -141,6 +171,8 @@ async def active_calls() -> ActiveCallsResponse:
count is per-process: one uvicorn per VM port (scripts/rolling_update.sh)
or per Kubernetes pod (preStop hook). See api/services/pipecat/active_calls.py.
"""
from api.constants import DOGRAH_DEVOPS_SECRET
from api.services.pipecat.active_calls import active_call_count
_verify_devops_secret(DOGRAH_DEVOPS_SECRET, x_dograh_devops_secret)
return ActiveCallsResponse(active_calls=active_call_count())

View file

@ -9,6 +9,12 @@ from .azure import (
AZURE_SPEECH_TTS_LANGUAGES,
AZURE_SPEECH_TTS_VOICES,
)
from .cartesia import (
CARTESIA_INK_2_STT_LANGUAGES,
CARTESIA_INK_WHISPER_STT_LANGUAGES,
CARTESIA_STT_LANGUAGES,
CARTESIA_STT_MODELS,
)
from .deepgram import (
DEEPGRAM_FLUX_MODELS,
DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS,
@ -59,6 +65,10 @@ __all__ = [
"AZURE_SPEECH_STT_LANGUAGES",
"AZURE_SPEECH_TTS_LANGUAGES",
"AZURE_SPEECH_TTS_VOICES",
"CARTESIA_INK_2_STT_LANGUAGES",
"CARTESIA_INK_WHISPER_STT_LANGUAGES",
"CARTESIA_STT_LANGUAGES",
"CARTESIA_STT_MODELS",
"DEEPGRAM_FLUX_MODELS",
"DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGES",
"DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS",

View file

@ -0,0 +1,105 @@
CARTESIA_STT_MODELS = ["ink-2", "ink-whisper"]
CARTESIA_INK_2_STT_LANGUAGES = ("en",)
CARTESIA_INK_WHISPER_STT_LANGUAGES = (
"en",
"zh",
"de",
"es",
"ru",
"ko",
"fr",
"ja",
"pt",
"tr",
"pl",
"ca",
"nl",
"ar",
"sv",
"it",
"id",
"hi",
"fi",
"vi",
"he",
"uk",
"el",
"ms",
"cs",
"ro",
"da",
"hu",
"ta",
"no",
"th",
"ur",
"hr",
"bg",
"lt",
"la",
"mi",
"ml",
"cy",
"sk",
"te",
"fa",
"lv",
"bn",
"sr",
"az",
"sl",
"kn",
"et",
"mk",
"br",
"eu",
"is",
"hy",
"ne",
"mn",
"bs",
"kk",
"sq",
"sw",
"gl",
"mr",
"pa",
"si",
"km",
"sn",
"yo",
"so",
"af",
"oc",
"ka",
"be",
"tg",
"sd",
"gu",
"am",
"yi",
"lo",
"uz",
"fo",
"ht",
"ps",
"tk",
"nn",
"mt",
"sa",
"lb",
"my",
"bo",
"tl",
"mg",
"as",
"tt",
"haw",
"ln",
"ha",
"ba",
"jw",
"su",
"yue",
)
CARTESIA_STT_LANGUAGES = CARTESIA_INK_WHISPER_STT_LANGUAGES

View file

@ -14,6 +14,10 @@ from api.services.configuration.options import (
AZURE_SPEECH_STT_LANGUAGES,
AZURE_SPEECH_TTS_LANGUAGES,
AZURE_SPEECH_TTS_VOICES,
CARTESIA_INK_2_STT_LANGUAGES,
CARTESIA_INK_WHISPER_STT_LANGUAGES,
CARTESIA_STT_LANGUAGES,
CARTESIA_STT_MODELS,
DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS,
DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGES,
DEEPGRAM_LANGUAGES,
@ -1323,9 +1327,6 @@ class DeepgramSTTConfiguration(BaseSTTConfiguration):
)
CARTESIA_STT_MODELS = ["ink-whisper"]
@register_stt
class CartesiaSTTConfiguration(BaseSTTConfiguration):
model_config = CARTESIA_PROVIDER_MODEL_CONFIG
@ -1335,6 +1336,17 @@ class CartesiaSTTConfiguration(BaseSTTConfiguration):
description="Cartesia STT model.",
json_schema_extra={"examples": CARTESIA_STT_MODELS},
)
language: str = Field(
default="en",
description="ISO 639-1 language code. ink-2 currently supports English only.",
json_schema_extra={
"examples": CARTESIA_STT_LANGUAGES,
"model_options": {
"ink-2": CARTESIA_INK_2_STT_LANGUAGES,
"ink-whisper": CARTESIA_INK_WHISPER_STT_LANGUAGES,
},
},
)
OPENAI_STT_MODELS = ["gpt-4o-transcribe"]

View file

@ -50,7 +50,7 @@ from api.services.pipecat.service_factory import (
create_realtime_llm_service,
create_stt_service,
create_tts_service,
stt_uses_flux_turns,
stt_uses_external_turns,
)
from api.services.pipecat.tracing_config import (
ensure_tracing,
@ -97,6 +97,19 @@ from pipecat.utils.run_context import set_current_org_id, set_current_run_id
# Setup tracing if enabled
ensure_tracing()
DEFAULT_USER_TURN_STOP_TIMEOUT = 5.0
EXTERNAL_TURN_USER_STOP_TIMEOUT = 30.0
def _resolve_user_turn_stop_timeout(
run_configs: dict, *, uses_external_turns: bool
) -> float:
if "user_turn_stop_timeout" in run_configs:
return float(run_configs["user_turn_stop_timeout"])
if uses_external_turns:
return EXTERNAL_TURN_USER_STOP_TIMEOUT
return DEFAULT_USER_TURN_STOP_TIMEOUT
def _create_realtime_user_turn_config(provider: str):
"""Return user turn strategies and optional local VAD for realtime providers."""
@ -154,6 +167,34 @@ async def run_pipeline_telephony(
user_id: int,
call_id: str,
transport_kwargs: dict,
) -> None:
"""Run a pipeline for any telephony provider."""
# Register before any async setup so deploy drains see calls that are still
# resolving DB/config/transport state.
register_active_call(workflow_run_id)
try:
await _run_pipeline_telephony_impl(
websocket,
provider_name=provider_name,
workflow_id=workflow_id,
workflow_run_id=workflow_run_id,
user_id=user_id,
call_id=call_id,
transport_kwargs=transport_kwargs,
)
finally:
unregister_active_call(workflow_run_id)
async def _run_pipeline_telephony_impl(
websocket,
*,
provider_name: str,
workflow_id: int,
workflow_run_id: int,
user_id: int,
call_id: str,
transport_kwargs: dict,
) -> None:
"""Run a pipeline for any telephony provider.
@ -227,7 +268,7 @@ async def run_pipeline_telephony(
)
try:
await _run_pipeline(
await _run_pipeline_impl(
transport,
workflow_id,
workflow_run_id,
@ -251,6 +292,31 @@ async def run_pipeline_smallwebrtc(
user_id: int,
call_context_vars: dict = {},
user_provider_id: str | None = None,
) -> None:
"""Run pipeline for WebRTC connections."""
# Register before any async setup so deploy drains see calls that are still
# resolving DB/config/transport state.
register_active_call(workflow_run_id)
try:
await _run_pipeline_smallwebrtc_impl(
webrtc_connection,
workflow_id,
workflow_run_id,
user_id,
call_context_vars=call_context_vars,
user_provider_id=user_provider_id,
)
finally:
unregister_active_call(workflow_run_id)
async def _run_pipeline_smallwebrtc_impl(
webrtc_connection: SmallWebRTCConnection,
workflow_id: int,
workflow_run_id: int,
user_id: int,
call_context_vars: dict = {},
user_provider_id: str | None = None,
) -> None:
"""Run pipeline for WebRTC connections"""
logger.debug(
@ -300,7 +366,7 @@ async def run_pipeline_smallwebrtc(
ambient_noise_config,
is_realtime=is_realtime,
)
await _run_pipeline(
await _run_pipeline_impl(
transport,
workflow_id,
workflow_run_id,
@ -323,6 +389,35 @@ async def _run_pipeline(
user_provider_id: str | None = None,
workflow_run=None,
resolved_user_config=None,
) -> None:
"""Run the pipeline with active-call drain accounting."""
register_active_call(workflow_run_id)
try:
await _run_pipeline_impl(
transport,
workflow_id,
workflow_run_id,
user_id,
call_context_vars=call_context_vars,
audio_config=audio_config,
user_provider_id=user_provider_id,
workflow_run=workflow_run,
resolved_user_config=resolved_user_config,
)
finally:
unregister_active_call(workflow_run_id)
async def _run_pipeline_impl(
transport,
workflow_id: int,
workflow_run_id: int,
user_id: int,
call_context_vars: dict = {},
audio_config: AudioConfig = None,
user_provider_id: str | None = None,
workflow_run=None,
resolved_user_config=None,
) -> None:
"""
Run the pipeline with the given transport and configuration
@ -624,16 +719,18 @@ async def _run_pipeline(
# Configure turn strategies based on STT provider, model, and workflow configuration
if is_realtime:
uses_external_turns = False
# Realtime services still need user-turn tracking even when the model
# itself owns speech generation and interruption behavior.
user_turn_strategies, user_vad_analyzer = _create_realtime_user_turn_config(
user_config.realtime.provider
)
else:
# Deepgram Flux and supported Dograh managed Flux languages emit their
# own turn boundaries, so the aggregator follows those external signals.
# Other models use configurable turn detection.
if stt_uses_flux_turns(user_config):
# Some STT services emit their own turn boundaries, so the aggregator
# follows those external signals. Other models use configurable turn
# detection.
uses_external_turns = stt_uses_external_turns(user_config)
if uses_external_turns:
user_turn_strategies = UserTurnStrategies(
start=[
VADUserTurnStartStrategy(),
@ -663,9 +760,15 @@ async def _run_pipeline(
stop=[SpeechTimeoutUserTurnStopStrategy()],
)
user_turn_stop_timeout = _resolve_user_turn_stop_timeout(
run_configs,
uses_external_turns=uses_external_turns,
)
user_params = LLMUserAggregatorParams(
user_turn_strategies=user_turn_strategies,
user_mute_strategies=user_mute_strategies,
user_turn_stop_timeout=user_turn_stop_timeout,
user_idle_timeout=max_user_idle_timeout,
vad_analyzer=user_vad_analyzer,
)

View file

@ -21,12 +21,13 @@ from pipecat.services.aws.llm import AWSBedrockLLMService, AWSBedrockLLMSettings
from pipecat.services.azure.llm import AzureLLMService, AzureLLMSettings
from pipecat.services.azure.stt import AzureSTTService, AzureSTTSettings
from pipecat.services.azure.tts import AzureTTSService, AzureTTSSettings
from pipecat.services.cartesia.stt import CartesiaSTTService
from pipecat.services.cartesia.stt import CartesiaSTTService, CartesiaSTTSettings
from pipecat.services.cartesia.tts import (
CartesiaTTSService,
CartesiaTTSSettings,
GenerationConfig,
)
from pipecat.services.cartesia.turns.stt import CartesiaTurnsSTTService
from pipecat.services.deepgram.flux.stt import (
DeepgramFluxSTTService,
DeepgramFluxSTTSettings,
@ -106,11 +107,13 @@ def dograh_stt_uses_flux_language(language: str | None) -> bool:
return language in DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS
def stt_uses_flux_turns(user_config) -> bool:
def stt_uses_external_turns(user_config) -> bool:
if user_config.stt.provider == ServiceProviders.DEEPGRAM.value:
return user_config.stt.model in DEEPGRAM_FLUX_MODELS
if user_config.stt.provider == ServiceProviders.DOGRAH.value:
return dograh_stt_uses_flux_language(getattr(user_config.stt, "language", None))
if user_config.stt.provider == ServiceProviders.CARTESIA.value:
return user_config.stt.model == "ink-2"
return False
@ -215,8 +218,20 @@ def create_stt_service(
sample_rate=audio_config.transport_in_sample_rate,
)
elif user_config.stt.provider == ServiceProviders.CARTESIA.value:
if user_config.stt.model == "ink-2":
return CartesiaTurnsSTTService(
api_key=user_config.stt.api_key,
should_interrupt=False, # Let UserAggregator emit interruption frames.
sample_rate=audio_config.transport_in_sample_rate,
)
language = getattr(user_config.stt, "language", None) or "en"
return CartesiaSTTService(
api_key=user_config.stt.api_key,
settings=CartesiaSTTSettings(
model=user_config.stt.model,
language=language,
),
sample_rate=audio_config.transport_in_sample_rate,
)
elif user_config.stt.provider == ServiceProviders.DOGRAH.value:

View file

@ -6,7 +6,15 @@ worker. The guarantees that matter for draining: register/unregister are
idempotent, and the count only reaches zero when every registered run is gone.
"""
import asyncio
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from api.routes import main as main_routes
from api.services.pipecat import active_calls
from api.services.pipecat import run_pipeline as run_pipeline_module
def setup_function():
@ -14,6 +22,21 @@ def setup_function():
active_calls._active_run_ids.clear()
def _make_active_calls_client(
monkeypatch,
configured_secret: str | None = "test-dograh-devops-secret",
) -> TestClient:
monkeypatch.setattr("api.constants.DOGRAH_DEVOPS_SECRET", configured_secret)
app = FastAPI()
app.add_api_route(
"/api/v1/health/active-calls",
main_routes.active_calls,
methods=["GET"],
response_model=main_routes.ActiveCallsResponse,
)
return TestClient(app)
def test_starts_empty():
assert active_calls.active_call_count() == 0
@ -51,3 +74,147 @@ def test_full_lifecycle_drains_to_zero():
assert active_calls.active_call_count() == 1
active_calls.unregister_active_call(42)
assert active_calls.active_call_count() == 0
@pytest.mark.asyncio
async def test_run_pipeline_counts_call_during_setup(monkeypatch):
entered_setup = asyncio.Event()
release_setup = asyncio.Event()
async def fake_get_workflow_run(*args, **kwargs):
entered_setup.set()
await release_setup.wait()
raise RuntimeError("setup failed")
monkeypatch.setattr(
run_pipeline_module.db_client,
"get_workflow_run",
fake_get_workflow_run,
)
task = asyncio.create_task(
run_pipeline_module._run_pipeline(
transport=object(),
workflow_id=1,
workflow_run_id=42,
user_id=7,
)
)
await asyncio.wait_for(entered_setup.wait(), timeout=1.0)
assert active_calls.active_call_count() == 1
release_setup.set()
with pytest.raises(RuntimeError, match="setup failed"):
await asyncio.wait_for(task, timeout=1.0)
assert active_calls.active_call_count() == 0
@pytest.mark.asyncio
async def test_webrtc_entrypoint_counts_call_during_setup(monkeypatch):
entered_setup = asyncio.Event()
release_setup = asyncio.Event()
async def fake_get_workflow(*args, **kwargs):
entered_setup.set()
await release_setup.wait()
raise RuntimeError("setup failed")
monkeypatch.setattr(
run_pipeline_module.db_client, "get_workflow", fake_get_workflow
)
task = asyncio.create_task(
run_pipeline_module.run_pipeline_smallwebrtc(
webrtc_connection=object(),
workflow_id=1,
workflow_run_id=43,
user_id=7,
)
)
await asyncio.wait_for(entered_setup.wait(), timeout=1.0)
assert active_calls.active_call_count() == 1
release_setup.set()
with pytest.raises(RuntimeError, match="setup failed"):
await asyncio.wait_for(task, timeout=1.0)
assert active_calls.active_call_count() == 0
@pytest.mark.asyncio
async def test_telephony_entrypoint_counts_call_during_setup(monkeypatch):
entered_setup = asyncio.Event()
release_setup = asyncio.Event()
async def fake_get_workflow(*args, **kwargs):
entered_setup.set()
await release_setup.wait()
raise RuntimeError("setup failed")
monkeypatch.setattr(
run_pipeline_module.db_client, "get_workflow", fake_get_workflow
)
task = asyncio.create_task(
run_pipeline_module.run_pipeline_telephony(
websocket=object(),
provider_name="twilio",
workflow_id=1,
workflow_run_id=44,
user_id=7,
call_id="call-1",
transport_kwargs={},
)
)
await asyncio.wait_for(entered_setup.wait(), timeout=1.0)
assert active_calls.active_call_count() == 1
release_setup.set()
with pytest.raises(RuntimeError, match="setup failed"):
await asyncio.wait_for(task, timeout=1.0)
assert active_calls.active_call_count() == 0
def test_active_calls_route_requires_configured_secret(monkeypatch):
client = _make_active_calls_client(monkeypatch, configured_secret=None)
response = client.get(
"/api/v1/health/active-calls",
headers={"X-Dograh-Devops-Secret": "test-dograh-devops-secret"},
)
assert response.status_code == 503
def test_active_calls_route_rejects_missing_secret_header(monkeypatch):
client = _make_active_calls_client(monkeypatch)
response = client.get("/api/v1/health/active-calls")
assert response.status_code == 403
def test_active_calls_route_rejects_wrong_secret(monkeypatch):
client = _make_active_calls_client(monkeypatch)
response = client.get(
"/api/v1/health/active-calls",
headers={"X-Dograh-Devops-Secret": "wrong"},
)
assert response.status_code == 403
def test_active_calls_route_returns_count_with_secret(monkeypatch):
active_calls.register_active_call(42)
client = _make_active_calls_client(monkeypatch)
response = client.get(
"/api/v1/health/active-calls",
headers={"X-Dograh-Devops-Secret": "test-dograh-devops-secret"},
)
assert response.status_code == 200
assert response.json() == {"active_calls": 1}

View file

@ -0,0 +1,94 @@
from types import SimpleNamespace
from unittest.mock import patch
from api.services.configuration.options import (
CARTESIA_INK_2_STT_LANGUAGES,
CARTESIA_INK_WHISPER_STT_LANGUAGES,
CARTESIA_STT_MODELS,
)
from api.services.configuration.registry import (
CartesiaSTTConfiguration,
ServiceProviders,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.service_factory import (
create_stt_service,
stt_uses_external_turns,
)
def _audio_config() -> AudioConfig:
return AudioConfig(
transport_in_sample_rate=16000,
transport_out_sample_rate=16000,
)
def _cartesia_config(model: str, language: str = "en") -> SimpleNamespace:
return SimpleNamespace(
stt=SimpleNamespace(
provider=ServiceProviders.CARTESIA.value,
api_key="test-key",
model=model,
language=language,
)
)
def test_cartesia_stt_configuration_exposes_ink_2_and_ink_whisper_languages():
config = CartesiaSTTConfiguration(api_key="test-key")
language_schema = CartesiaSTTConfiguration.model_json_schema()["properties"][
"language"
]
assert config.provider == ServiceProviders.CARTESIA
assert config.model == "ink-whisper"
assert config.language == "en"
assert CARTESIA_STT_MODELS == ["ink-2", "ink-whisper"]
assert CARTESIA_INK_2_STT_LANGUAGES == ("en",)
assert "es" in CARTESIA_INK_WHISPER_STT_LANGUAGES
assert language_schema["model_options"]["ink-2"] == ["en"]
assert "es" in language_schema["model_options"]["ink-whisper"]
def test_cartesia_ink_2_uses_external_turns_and_turns_service():
user_config = _cartesia_config("ink-2")
assert stt_uses_external_turns(user_config)
with (
patch(
"api.services.pipecat.service_factory.CartesiaTurnsSTTService"
) as turns_service,
patch("api.services.pipecat.service_factory.CartesiaSTTService") as stt_service,
):
create_stt_service(user_config, _audio_config())
turns_service.assert_called_once()
stt_service.assert_not_called()
kwargs = turns_service.call_args.kwargs
assert kwargs["api_key"] == "test-key"
assert kwargs["sample_rate"] == 16000
assert kwargs["should_interrupt"] is False
def test_cartesia_ink_whisper_uses_manual_stt_service_with_model_and_language():
user_config = _cartesia_config("ink-whisper", language="es")
assert not stt_uses_external_turns(user_config)
with (
patch(
"api.services.pipecat.service_factory.CartesiaTurnsSTTService"
) as turns_service,
patch("api.services.pipecat.service_factory.CartesiaSTTService") as stt_service,
):
create_stt_service(user_config, _audio_config())
turns_service.assert_not_called()
stt_service.assert_called_once()
kwargs = stt_service.call_args.kwargs
assert kwargs["api_key"] == "test-key"
assert kwargs["sample_rate"] == 16000
assert kwargs["settings"].model == "ink-whisper"
assert kwargs["settings"].language == "es"

View file

@ -9,7 +9,7 @@ from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.service_factory import (
create_stt_service,
dograh_stt_uses_flux_language,
stt_uses_flux_turns,
stt_uses_external_turns,
)
@ -38,10 +38,10 @@ def test_dograh_flux_language_predicate_matches_multilingual_support():
assert not dograh_stt_uses_flux_language("ar")
def test_stt_uses_flux_turns_only_for_dograh_flux_supported_languages():
assert stt_uses_flux_turns(_dograh_config("multi"))
assert stt_uses_flux_turns(_dograh_config("es"))
assert not stt_uses_flux_turns(_dograh_config("ar"))
def test_stt_uses_external_turns_only_for_dograh_flux_supported_languages():
assert stt_uses_external_turns(_dograh_config("multi"))
assert stt_uses_external_turns(_dograh_config("es"))
assert not stt_uses_external_turns(_dograh_config("ar"))
def test_create_dograh_multi_uses_flux_service_without_language_hint():

View file

@ -11,7 +11,12 @@ from pipecat.turns.user_stop import (
)
from api.services.configuration.registry import ServiceProviders
from api.services.pipecat.run_pipeline import _create_realtime_user_turn_config
from api.services.pipecat.run_pipeline import (
DEFAULT_USER_TURN_STOP_TIMEOUT,
EXTERNAL_TURN_USER_STOP_TIMEOUT,
_create_realtime_user_turn_config,
_resolve_user_turn_stop_timeout,
)
def test_gemini_realtime_uses_local_vad_without_local_interruptions():
@ -72,3 +77,27 @@ def test_unknown_realtime_providers_keep_local_vad():
assert isinstance(strategies.start[0], VADUserTurnStartStrategy)
assert len(strategies.stop) == 1
assert isinstance(strategies.stop[0], SpeechTimeoutUserTurnStopStrategy)
def test_external_turn_stt_uses_longer_stop_timeout():
assert (
_resolve_user_turn_stop_timeout({}, uses_external_turns=True)
== EXTERNAL_TURN_USER_STOP_TIMEOUT
)
def test_standard_stt_keeps_default_stop_timeout():
assert (
_resolve_user_turn_stop_timeout({}, uses_external_turns=False)
== DEFAULT_USER_TURN_STOP_TIMEOUT
)
def test_workflow_config_can_override_user_turn_stop_timeout():
assert (
_resolve_user_turn_stop_timeout(
{"user_turn_stop_timeout": "12.5"},
uses_external_turns=True,
)
== 12.5
)

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit d0ed1ca7ead5935cf3622e668f3d424f273bf086
Subproject commit 5a4fa626cfba4f4509f7744b13bb83adae0992fe

View file

@ -29,6 +29,7 @@ NGINX_UPSTREAM_CONF="/etc/nginx/conf.d/dograh_upstream.conf"
HEALTH_CHECK_ENDPOINT="/api/v1/health"
ACTIVE_CALLS_ENDPOINT="/api/v1/health/active-calls"
DOGRAH_DEVOPS_SECRET_HEADER="X-Dograh-Devops-Secret"
# Load environment
if [[ -f "$ENV_FILE" ]]; then
@ -57,6 +58,15 @@ log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $*"; }
log_warn() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARN: $*"; }
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2; }
if [[ -z "${DOGRAH_DEVOPS_SECRET:-}" ]]; then
log_error "DOGRAH_DEVOPS_SECRET is not set. Add it to $ENV_FILE before running rolling_update.sh."
exit 1
fi
if [[ "$DOGRAH_DEVOPS_SECRET" == "change-me-dograh-devops-secret" ]]; then
log_error "DOGRAH_DEVOPS_SECRET still has the example placeholder value. Replace it in $ENV_FILE."
exit 1
fi
# Band port calculation: band A = base, band B = base + 100
band_base_port() {
local band=$1
@ -100,17 +110,38 @@ kill_process_tree() {
}
# Active in-progress call count for a single worker, via its health endpoint.
# A worker that is unreachable (already exited) or returns no/garbage body
# reports 0, so it never blocks the drain.
# A worker that is unreachable (already exited) reports 0, so it never blocks the
# drain. Non-200 responses or malformed bodies are hard failures: otherwise an
# auth/configuration error could be mistaken for a fully drained worker.
count_active_calls_on_port() {
local port=$1
local body n
body=$(curl -s --max-time 3 \
local response http_code body n
response=$(curl -sS --max-time 3 \
-H "${DOGRAH_DEVOPS_SECRET_HEADER}: ${DOGRAH_DEVOPS_SECRET}" \
-w $'\n%{http_code}' \
"http://127.0.0.1:${port}${ACTIVE_CALLS_ENDPOINT}" 2>/dev/null || true)
http_code="${response##*$'\n'}"
body="${response%$'\n'*}"
if [[ "$http_code" == "000" ]]; then
printf '0'
return 0
fi
if [[ "$http_code" != "200" ]]; then
log_error "uvicorn_${port} active-calls endpoint returned HTTP ${http_code}. Check DOGRAH_DEVOPS_SECRET in $ENV_FILE."
return 1
fi
n=$(printf '%s' "$body" \
| grep -o '"active_calls"[[:space:]]*:[[:space:]]*[0-9]\+' \
| grep -o '[0-9]\+$' || true)
printf '%s' "${n:-0}"
if [[ -z "$n" ]]; then
log_error "uvicorn_${port} active-calls endpoint returned an invalid response body."
return 1
fi
printf '%s' "$n"
}
###############################################################################
@ -399,7 +430,10 @@ while true; do
# Only poll workers still alive; an exited worker holds no calls.
pidfile="$RUN_DIR/uvicorn_${port}.pid"
if [[ -f "$pidfile" ]] && kill -0 "$(<"$pidfile")" 2>/dev/null; then
active=$((active + $(count_active_calls_on_port "$port")))
if ! call_count=$(count_active_calls_on_port "$port"); then
exit 1
fi
active=$((active + call_count))
fi
done

View file

@ -33,6 +33,15 @@ if [[ -f "$ENV_FILE" ]]; then
set -a && . "$ENV_FILE" && set +a
fi
if [[ -z "${DOGRAH_DEVOPS_SECRET:-}" ]]; then
echo "ERROR: DOGRAH_DEVOPS_SECRET is not set. Add it to $ENV_FILE before starting production services."
exit 1
fi
if [[ "$DOGRAH_DEVOPS_SECRET" == "change-me-dograh-devops-secret" ]]; then
echo "ERROR: DOGRAH_DEVOPS_SECRET still has the example placeholder value. Replace it in $ENV_FILE."
exit 1
fi
UVICORN_BASE_PORT=${UVICORN_BASE_PORT:-8000}
CPU_CORES=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1)
FASTAPI_WORKERS=${FASTAPI_WORKERS:-$CPU_CORES}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -176,6 +176,16 @@ export type AwsBedrockLlmConfiguration = {
aws_region?: string;
};
/**
* ActiveCallsResponse
*/
export type ActiveCallsResponse = {
/**
* Active Calls
*/
active_calls: number;
};
/**
* AmbientNoiseUploadRequest
*/
@ -1042,6 +1052,12 @@ export type CartesiaSttConfiguration = {
* Cartesia STT model.
*/
model?: string;
/**
* Language
*
* ISO 639-1 language code. ink-2 currently supports English only.
*/
language?: string;
};
/**
@ -13838,3 +13854,38 @@ export type HealthApiV1HealthGetResponses = {
};
export type HealthApiV1HealthGetResponse = HealthApiV1HealthGetResponses[keyof HealthApiV1HealthGetResponses];
export type ActiveCallsApiV1HealthActiveCallsGetData = {
body?: never;
headers?: {
/**
* X-Dograh-Devops-Secret
*/
'X-Dograh-Devops-Secret'?: string | null;
};
path?: never;
query?: never;
url: '/api/v1/health/active-calls';
};
export type ActiveCallsApiV1HealthActiveCallsGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ActiveCallsApiV1HealthActiveCallsGetError = ActiveCallsApiV1HealthActiveCallsGetErrors[keyof ActiveCallsApiV1HealthActiveCallsGetErrors];
export type ActiveCallsApiV1HealthActiveCallsGetResponses = {
/**
* Successful Response
*/
200: ActiveCallsResponse;
};
export type ActiveCallsApiV1HealthActiveCallsGetResponse = ActiveCallsApiV1HealthActiveCallsGetResponses[keyof ActiveCallsApiV1HealthActiveCallsGetResponses];