{"id":50736389,"url":"https://github.com/dzaczek/ava","last_synced_at":"2026-06-10T14:01:12.692Z","repository":{"id":341420293,"uuid":"1165047259","full_name":"dzaczek/ava","owner":"dzaczek","description":" AI voice assistant that answers your phone calls with a human like real persona, holds natural multilingual conversations (GPT-4o + ElevenLabs), and keeps you in the loop via Signal with realtime midcall instructions.","archived":false,"fork":false,"pushed_at":"2026-03-21T19:00:24.000Z","size":142,"stargazers_count":17,"open_issues_count":3,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-22T08:34:53.946Z","etag":null,"topics":["mikrus","signal-cli","voice-assistant"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dzaczek.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-23T19:11:07.000Z","updated_at":"2026-03-21T19:00:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dzaczek/ava","commit_stats":null,"previous_names":["dzaczek/ava"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dzaczek/ava","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dzaczek%2Fava","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dzaczek%2Fava/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dzaczek%2Fava/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dzaczek%2Fava/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dzaczek","download_url":"https://codeload.github.com/dzaczek/ava/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dzaczek%2Fava/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34155422,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mikrus","signal-cli","voice-assistant"],"created_at":"2026-06-10T14:01:11.651Z","updated_at":"2026-06-10T14:01:12.681Z","avatar_url":"https://github.com/dzaczek.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AVA – AI Voice Assistant\n\n\u003e **AVA** answers your calls when you can't, holds a natural conversation with a human-like persona, and keeps you in the loop via Signal. You can send live instructions mid-call from your phone.\n\n---\n\n## Architecture Overview\n\n```mermaid\ngraph TB\n    subgraph External[\"EXTERNAL SERVICES\"]\n        Twilio[\"Twilio\u003cbr/\u003eVoice / PSTN\u003cbr/\u003eSTT (Gather)\u003cbr/\u003eRecord\u003cbr/\u003eWebhooks\"]\n        OpenAI[\"OpenAI\u003cbr/\u003eGPT-4o (conversation)\u003cbr/\u003eWhisper async\u003cbr/\u003eTTS (fallback)\"]\n        ElevenLabs[\"ElevenLabs\u003cbr/\u003eTTS (primary voice)\u003cbr/\u003eeleven_turbo_v2_5\"]\n    end\n\n    subgraph Docker[\"DOCKER HOST (your server)\"]\n        subgraph Ingress[\"INGRESS (choose one)\"]\n            Caddy[\"Caddy :443/:80\u003cbr/\u003eLet's Encrypt\u003cbr/\u003eauto HTTPS\"]\n            Cloudflared[\"Cloudflare Tunnel\u003cbr/\u003eoutbound, no open ports\"]\n        end\n\n        subgraph AVA[\"AVA (FastAPI :8000)\"]\n            Main[\"main.py\u003cbr/\u003eCall routing\u003cbr/\u003eTwilio hooks\u003cbr/\u003eWhisper async\u003cbr/\u003eRate limiter\u003cbr/\u003eAudio serve\"]\n            Conv[\"conversation.py\u003cbr/\u003eGPT-4o / Groq\u003cbr/\u003eStreaming\u003cbr/\u003eMeta parsing\u003cbr/\u003eSummarizer\"]\n            TTS[\"tts.py\u003cbr/\u003eElevenLabs → OpenAI\u003cbr/\u003e→ Polly (fallback)\u003cbr/\u003eCache (MD5)\u003cbr/\u003eCircuit breaker\"]\n            Owner[\"owner_channel.py\u003cbr/\u003eSignal notify\u003cbr/\u003eSignal poll (3s)\u003cbr/\u003eSlash commands\u003cbr/\u003eInstructions\"]\n            Contact[\"contact_lookup.py\u003cbr/\u003econtacts.json\u003cbr/\u003eTwilio CNAM\u003cbr/\u003eE.164 normalize\u003cbr/\u003eLang from prefix\"]\n            I18n[\"i18n.py\u003cbr/\u003e11+ languages\u003cbr/\u003eSignal templates\u003cbr/\u003ePolly voices\u003cbr/\u003eTwilio codes\"]\n        end\n\n        SignalCLI[\"signal-cli :8080\u003cbr/\u003eREST API\u003cbr/\u003eNative mode\u003cbr/\u003eSelf-hosted\"]\n\n        subgraph Volumes[\"Persistent Volumes\"]\n            TTSCache[\"tts_cache (MP3s)\"]\n            CallData[\"/data/calls/ (JSON)\"]\n            Contacts[\"/data/contacts.json\"]\n            SignalData[\"signal_data\"]\n        end\n    end\n\n    OwnerPhone[\"Owner's Phone\u003cbr/\u003e(Signal app)\"]\n\n    Twilio --\u003e|\"HTTPS webhooks\"| Caddy\n    Twilio --\u003e|\"HTTPS webhooks\"| Cloudflared\n    Caddy --\u003e|\"ava-net\"| Main\n    Cloudflared --\u003e|\"ava-net\"| Main\n\n    Main \u003c--\u003e Conv\n    Main \u003c--\u003e TTS\n    Main \u003c--\u003e Owner\n    Main \u003c--\u003e Contact\n    Conv \u003c--\u003e I18n\n    Main \u003c--\u003e I18n\n\n    Conv --\u003e|\"HTTPS\"| OpenAI\n    TTS --\u003e|\"HTTPS\"| ElevenLabs\n    TTS --\u003e|\"HTTPS\"| OpenAI\n\n    Owner --\u003e|\"HTTP (ava-net)\"| SignalCLI\n    SignalCLI \u003c--\u003e|\"Signal protocol\"| OwnerPhone\n\n    TTS --\u003e TTSCache\n    Main --\u003e CallData\n    Contact --\u003e Contacts\n    SignalCLI --\u003e SignalData\n\n    style External fill:#f9f0ff,stroke:#7c3aed\n    style Docker fill:#f0f9ff,stroke:#2563eb\n    style AVA fill:#ecfdf5,stroke:#059669\n    style Ingress fill:#fef3c7,stroke:#d97706\n    style Volumes fill:#fef2f2,stroke:#dc2626\n```\n\n---\n\n## Call Flow (detailed sequence)\n\n```mermaid\nsequenceDiagram\n    participant Caller as Caller's Phone\n    participant Twilio as Twilio (PSTN + STT)\n    participant AVA as AVA Server\n    participant GPT as LLM (GPT-4o / Groq)\n    participant TTS as ElevenLabs / OpenAI TTS\n    participant Signal as Owner (Signal)\n\n    Caller-\u003e\u003eTwilio: Dials owner (call forwarded)\n    Twilio-\u003e\u003eAVA: POST /twilio/incoming\u003cbr/\u003e(CallSid, From, To)\n\n    Note over AVA: Contact lookup (local/CNAM)\u003cbr/\u003eDetect lang from phone prefix\u003cbr/\u003e(+41→de-CH, +48→pl-PL)\n\n    AVA--\u003e\u003eSignal: 📞 Incoming call notification\n\n    alt Contact has lang override\n        AVA-\u003e\u003eTTS: Generate greeting TTS\n        AVA-\u003e\u003eTwilio: TwiML: Gather + Play\u003cbr/\u003e(known language)\n        Twilio-\u003e\u003eCaller: Plays greeting audio\n    else Unknown contact\n        AVA-\u003e\u003eTwilio: TwiML: Record + Say\u003cbr/\u003e\"Which language do you prefer?\"\n        Twilio-\u003e\u003eCaller: Plays question audio\n        Caller-\u003e\u003eTwilio: Speaks\n        Twilio-\u003e\u003eAVA: POST /twilio/first_response\n        Note over AVA: OpenAI Whisper transcribes\u003cbr/\u003eand detects actual language\n    end\n\n    loop Max 10 exchanges\n        Caller-\u003e\u003eTwilio: Speaks\n        Twilio-\u003e\u003eAVA: POST /process_speech\u003cbr/\u003e(SpeechResult, Confidence)\n\n        Note over AVA: Pop Signal instructions\n\n        opt Owner sent instruction\n            Signal--\u003e\u003eAVA: \"tell him I'll call back\"\n            Note over AVA: Inject [RELAY_TO_CALLER: ...]\u003cbr/\u003einto GPT user message\n        end\n\n        AVA-\u003e\u003eGPT: Stream LLM (user text + instructions)\n        GPT--\u003e\u003eAVA: Sentence chunks (streaming)\n\n        Note over AVA: TTS pipeline: start TTS on\u003cbr/\u003e1st sentence while GPT\u003cbr/\u003estill generates the rest\n\n        AVA-\u003e\u003eTTS: TTS sentence 1 (parallel)\n        TTS--\u003e\u003eAVA: MP3 URL\n        AVA-\u003e\u003eTTS: TTS remaining sentences\n\n        Note over AVA: Parse meta JSON\u003cbr/\u003eend_call, urgency, topic,\u003cbr/\u003ecaller_name, lang\n\n        opt GPT switched language\n            Note over AVA: Update STT language\u003cbr/\u003efor next Gather\u003cbr/\u003ee.g. de-CH → pl-PL\n        end\n\n        AVA-\u003e\u003eTwilio: TwiML: Gather + Play\u003cbr/\u003e(updated STT language)\n        Twilio-\u003e\u003eCaller: Plays response audio\n\n        opt Every 4 transcript entries\n            AVA--\u003e\u003eSignal: 📞 Live update\u003cbr/\u003e(topic, last 6 lines)\n        end\n    end\n\n    Note over AVA: end_call=true OR\u003cbr/\u003eEND_CALL_NOW from owner\n\n    AVA-\u003e\u003eTwilio: TwiML: Play + Hangup\n    Twilio-\u003e\u003eCaller: Goodbye + disconnect\n\n    Twilio-\u003e\u003eAVA: POST /twilio/status\u003cbr/\u003eCallStatus=completed\n\n    AVA-\u003e\u003eGPT: Summarize full transcript\n    GPT--\u003e\u003eAVA: Summary text\n    AVA--\u003e\u003eSignal: 📋 Call summary + priority\n    AVA--\u003e\u003eSignal: 📝 Full transcript\n\n    Note over AVA: Save JSON to /data/calls/\u003cbr/\u003eCleanup after 90s delay\n```\n\n---\n\n## Timeouts \u0026 Limits\n\n| Parameter | Value | Location | Description |\n|-----------|-------|----------|-------------|\n| `speech_timeout` | **1 s** | `main.py` (all Gather calls) | Silence after speech ends before Twilio fires callback |\n| `enhanced` | `true` | `main.py` (Gather) | Use enhanced STT model for better accuracy |\n| LLM `max_tokens` | **180** | `conversation.py` | Max response length per turn |\n| GPT `temperature` | **0.75** | `conversation.py` | Creativity level for responses |\n| Summary `max_tokens` | **400** | `conversation.py` | Max summary length |\n| Summary `temperature` | **0.2** | `conversation.py` | Low creativity for factual summaries |\n| Context window | **last 20 messages** | `conversation.py` | Sliding window of conversation history |\n| Hard turn limit | **10 exchanges** | `conversation.py` | AVA wraps up after 10 user turns |\n| Wrap-up warning | **8+ exchanges** | `conversation.py` | System prompt warns AVA to end soon |\n| ElevenLabs timeout | **15 s** | `tts.py` (httpx) | HTTP timeout for TTS API |\n| ElevenLabs circuit breaker | **10 min** | `tts.py` | Disable after 401/403/429, auto-reset |\n| Signal poll interval | **3 s** | `main.py` / `owner_channel.py` | How often AVA checks for new Signal messages |\n| Signal HTTP timeout | **10 s** | `owner_channel.py` (httpx) | Timeout for Signal API calls |\n| CNAM lookup timeout | **5 s** | `contact_lookup.py` (httpx) | Twilio CNAM API timeout |\n| Rate limiter | **30 req/min** per IP | `main.py` | Sliding window, auto-cleanup every 5 min |\n| Rate limiter cleanup | **5 min** | `main.py` | Stale entry eviction interval |\n| Call state cleanup | **90 s** after end | `main.py` | Delayed cleanup of in-memory call state |\n| TTS cache | **no expiry** | `tts.py` | MD5(lang:text) keyed, persists in Docker volume |\n| Seen Signal timestamps | **500 entries** | `owner_channel.py` | Deque for deduplication |\n\n---\n\n## Language Detection \u0026 Switching\n\n```mermaid\nflowchart TD\n    Start([CALL START]) --\u003e Prefix[\"Phone prefix detection\u003cbr/\u003e+41 → de-CH\u003cbr/\u003e+48 → pl-PL\u003cbr/\u003e+44 → en-GB\u003cbr/\u003e(52 prefixes)\"]\n\n    Prefix --\u003e ContactCheck{Contact has\u003cbr/\u003elang override?}\n    ContactCheck --\u003e|Yes| ContactLang[\"Use contact language\u003cbr/\u003econtacts.json\u003cbr/\u003ee.g. {name: ..., lang: pl}\"]\n    ContactCheck --\u003e|No| Record[\"Twilio Record\u003cbr/\u003eAsk language preference\u003cbr/\u003ein prefix language\"]\n\n    ContactLang --\u003e Gather\n    Record --\u003e Whisper[\"OpenAI Whisper API\u003cbr/\u003eTranscribes audio \u0026\u003cbr/\u003edetects actual language\"]\n\n    Whisper --\u003e GPT\n\n    Gather[\"Twilio STT Gather\u003cbr/\u003elanguage = detected locale\u003cbr/\u003espeech_timeout = 1s\u003cbr/\u003eenhanced = true\"]\n\n    Gather --\u003e Speech[\"SpeechResult (text)\"]\n    Speech --\u003e GPT[\"GPT-4o processes text\u003cbr/\u003eResponds in caller's language\u003cbr/\u003eReturns meta with lang: pl\"]\n\n    GPT --\u003e Switch{LLM lang ≠\u003cbr/\u003ecurrent STT?}\n    Switch --\u003e|Yes| Update[\"Switch STT language\u003cbr/\u003efor NEXT Gather\u003cbr/\u003ee.g. de-CH → pl-PL\"]\n    Switch --\u003e|No| Keep[\"Keep current STT language\"]\n\n    Update --\u003e Gather\n    Keep --\u003e Gather\n\n    style Start fill:#059669,color:#fff\n    style Record fill:#2563eb,color:#fff\n    style Whisper fill:#7c3aed,color:#fff\n    style Gather fill:#2563eb,color:#fff\n    style GPT fill:#7c3aed,color:#fff\n    style Switch fill:#d97706,color:#fff\n```\n\n\u003e **Important limitation**: Twilio STT only supports **one language per Gather**. If the caller speaks Polish but STT is set to German, the transcript will be garbled. The GPT model analyzes the garbled text and switches the language via the `meta` block for the **next** turn.\n\n---\n\n## TTS Provider Chain\n\n```mermaid\nflowchart TD\n    Input[\"Text to speak\"] --\u003e Cache{Disk cache hit?\u003cbr/\u003ekey = MD5 lang:text}\n\n    Cache --\u003e|Yes| Serve[\"Return cached URL\u003cbr/\u003ePUBLIC_URL/audio/hash.mp3\"]\n    Cache --\u003e|No| ELCheck{ElevenLabs\u003cbr/\u003eavailable?\u003cbr/\u003eAPI key set?\u003cbr/\u003eCircuit breaker OK?}\n\n    ELCheck --\u003e|Yes| EL[\"ElevenLabs API\u003cbr/\u003evoice_id (env)\u003cbr/\u003emodel_id (env)\u003cbr/\u003etimeout: 15s\"]\n    ELCheck --\u003e|No| OpenAI\n\n    EL --\u003e|Success| Save[\"Save to cache\u003cbr/\u003eReturn URL\"]\n    EL --\u003e|Fail| OpenAI[\"OpenAI TTS\u003cbr/\u003emodel: tts-1\u003cbr/\u003evoice: OPENAI_TTS_VOICE\u003cbr/\u003e(default: nova)\"]\n\n    OpenAI --\u003e|Success| Save\n    OpenAI --\u003e|Fail| Polly[\"Twilio Say (Polly)\u003cbr/\u003eLast resort\u003cbr/\u003eBuilt-in voice\"]\n\n    EL --\u003e|\"401/403/429\"| CB[\"Circuit Breaker\u003cbr/\u003eDisable ElevenLabs\u003cbr/\u003efor 10 minutes\"]\n    CB --\u003e OpenAI\n\n    Save --\u003e Done([Audio URL returned])\n    Polly --\u003e Done2([TwiML Say fallback])\n\n    style Input fill:#2563eb,color:#fff\n    style EL fill:#7c3aed,color:#fff\n    style OpenAI fill:#059669,color:#fff\n    style Polly fill:#dc2626,color:#fff\n    style CB fill:#d97706,color:#fff\n    style Done fill:#059669,color:#fff\n```\n\n---\n\n## Signal Communication Flow\n\n```mermaid\nsequenceDiagram\n    participant Owner as Owner's Signal\n    participant CLI as signal-cli REST API\n    participant AVA as AVA Server\n\n    loop Every 3 seconds\n        AVA-\u003e\u003eCLI: GET /v1/receive\n        CLI--\u003e\u003eAVA: [] (no messages)\n    end\n\n    Note over AVA: INCOMING CALL\n\n    AVA-\u003e\u003eCLI: POST /v2/send\n    CLI-\u003e\u003eOwner: 📞 Incoming call\u003cbr/\u003eFrom: Jan (+48...)\u003cbr/\u003e🌐 pl-PL\n\n    Owner-\u003e\u003eCLI: \"tell him I'll call back\"\n    AVA-\u003e\u003eCLI: GET /v1/receive\n    CLI--\u003e\u003eAVA: [message data]\n    Note over AVA: Queue instruction\u003cbr/\u003efor active call\n\n    AVA-\u003e\u003eCLI: POST /v2/send\n    CLI-\u003e\u003eOwner: ✅ AVA will tell the caller\n\n    Note over AVA: Next speech turn:\u003cbr/\u003einject instruction\u003cbr/\u003einto GPT context\n\n    Note over AVA: After 4 transcript entries\n\n    AVA-\u003e\u003eCLI: POST /v2/send\n    CLI-\u003e\u003eOwner: 📞 Call in progress\u003cbr/\u003e🟡 Topic: invoice dispute\u003cbr/\u003eLast 6 lines of transcript\n\n    Note over AVA: CALL ENDS\n\n    AVA-\u003e\u003eCLI: POST /v2/send\n    CLI-\u003e\u003eOwner: 📋 Call summary\u003cbr/\u003ePriority + AI summary\n\n    AVA-\u003e\u003eCLI: POST /v2/send\n    CLI-\u003e\u003eOwner: 📝 Full transcript\n```\n\n### Slash commands (no active call needed)\n\n| Command | Description |\n|---------|-------------|\n| `/ping` | Alive check + timestamp |\n| `/status` | Uptime, active calls, public URL |\n| `/stats` | Call count, memory, TTS cache size |\n| `/calls` | Last 5 call records with topics |\n| `/debug` | Latency breakdown (avg from last 10 calls). Use `/debug -1`, `/debug -2` for per-call detail. |\n| `/billings` | Check API balances (ElevenLabs chars, Twilio balance, OpenAI costs) |\n| `/recording-on` | Start recording calls (Twilio recording) |\n| `/recording-off` | Stop recording calls |\n| `/restart` | Restart AVA (requires `/restart confirm`) |\n| `/help` | Command list |\n\n---\n\n## Owner Instruction Injection\n\n```mermaid\nflowchart LR\n    subgraph Signal[\"Owner sends via Signal\"]\n        A[\"tell him I'll call at 3\"]\n        B[\"ask for order number\"]\n        C[\"be more formal\"]\n        D[\"end\"]\n    end\n\n    subgraph GPT[\"AVA injects into GPT context\"]\n        A2[\"[RELAY_TO_CALLER: I'll call at 3]\"]\n        B2[\"[ASK_CALLER: order number]\"]\n        C2[\"[OWNER_INSTRUCTION: be more formal]\"]\n        D2[\"END_CALL_NOW + force_end flag\"]\n    end\n\n    A --\u003e A2\n    B --\u003e B2\n    C --\u003e C2\n    D --\u003e D2\n\n    GPT --\u003e Response[\"GPT acts on markers\u003cbr/\u003enaturally within response\"]\n\n    style Signal fill:#f0f9ff,stroke:#2563eb\n    style GPT fill:#ecfdf5,stroke:#059669\n```\n\n---\n\n## GPT Response Meta Block\n\nEvery GPT response ends with an invisible metadata block:\n\n```\nHello, I'm Maya, Jacek's assistant. How can I help you today?\n\n\u003cmeta\u003e{\"end_call\": false, \"urgency\": \"low\", \"topic\": \"general inquiry\",\n \"caller_name\": \"Jan\", \"lang\": \"en\"}\u003c/meta\u003e\n```\n\n| Field | Purpose |\n|-------|---------|\n| `end_call` | `true` → AVA hangs up after this response |\n| `urgency` | `low` / `medium` / `high` → emoji in Signal summary |\n| `topic` | Short English description for Signal notifications |\n| `caller_name` | First name if mentioned by caller |\n| `lang` | Two-letter code (pl, en, de) → used to switch STT language |\n\n---\n\n## Docker Compose Services\n\n```mermaid\ngraph LR\n    subgraph compose[\"docker-compose.yml\"]\n        ava[\"ava\u003cbr/\u003eFastAPI :8000\u003cbr/\u003ePython 3.11\"]\n        signal[\"signal-cli\u003cbr/\u003eREST API :8080\u003cbr/\u003eNative mode\"]\n        caddy[\"caddy\u003cbr/\u003e:80 / :443\u003cbr/\u003eLet's Encrypt\"]\n        tunnel[\"cloudflared\u003cbr/\u003eCloudflare Tunnel\u003cbr/\u003eoutbound only\"]\n    end\n\n    ava --\u003e|depends_on| signal\n    caddy --\u003e|depends_on| ava\n    tunnel --\u003e|depends_on| ava\n\n    caddy -.-|\"profile: caddy\"| note1[\"Open ports 80/443\"]\n    tunnel -.-|\"profile: tunnel\"| note2[\"No open ports\"]\n\n    style ava fill:#059669,color:#fff\n    style signal fill:#2563eb,color:#fff\n    style caddy fill:#d97706,color:#fff\n    style tunnel fill:#7c3aed,color:#fff\n```\n\n---\n\n## Environment Variables (complete reference)\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| **Twilio** | | |\n| `TWILIO_ACCOUNT_SID` | (required) | Twilio account identifier |\n| `TWILIO_AUTH_TOKEN` | (required) | Auth token, also validates webhook signatures |\n| `TWILIO_PHONE_NUMBER` | (required) | Your Twilio virtual number |\n| **Signal** | | |\n| `SIGNAL_CLI_URL` | `http://signal-cli:8080` | Internal signal-cli API address |\n| `SIGNAL_SENDER_NUMBER` | (required) | Bot's Signal number |\n| `SIGNAL_RECIPIENT` | (required) | Your personal Signal number |\n| `SIGNAL_LANG` | `en` | Signal notification language (`en` / `pl`) |\n| **LLM** | | |\n| `OPENAI_API_KEY` | (required) | OpenAI API key |\n| `LLM_PROVIDER` | `openai` | LLM backend: `openai` or `groq` |\n| `LLM_MODEL` | auto | Model name (default: `gpt-4o-mini` for OpenAI, `llama-3.3-70b-versatile` for Groq) |\n| `LLM_SUMMARY_MODEL` | auto | Model for call summaries (default: same as `LLM_MODEL`) |\n| `GROQ_API_KEY` | (empty) | Groq API key (required when `LLM_PROVIDER=groq`) |\n| **ElevenLabs** | | |\n| `ELEVENLABS_API_KEY` | (empty) | Leave blank to skip ElevenLabs |\n| `ELEVENLABS_VOICE_ID` | `WAhoMTNdLdMoq1j3wf3I` | Single multilingual voice ID |\n| `ELEVENLABS_MODEL` | `eleven_multilingual_v2` | TTS model (`eleven_turbo_v2_5` for lower latency) |\n| **OpenAI TTS** | | |\n| `OPENAI_TTS_VOICE` | `nova` | Fallback voice (alloy/echo/fable/onyx/nova/shimmer) |\n| **Language** | | |\n| `DEFAULT_STT_LANG` | `en-US` | Fallback STT language |\n| **Persona** | | |\n| `OWNER_CONTEXT` | (built-in default) | Assistant persona + owner info (single line, private) |\n| **Infrastructure** | | |\n| `PUBLIC_URL` | (required) | HTTPS URL for Twilio to fetch TTS audio |\n| `COMPOSE_PROFILES` | `caddy` | `caddy` or `tunnel` |\n| `DOMAIN` | `localhost` | Domain for Caddy SSL (caddy profile only) |\n| `CLOUDFLARE_TUNNEL_TOKEN` | (empty) | Tunnel token (tunnel profile only) |\n\n---\n\n## Persona Configuration (OWNER_CONTEXT)\n\nThe `OWNER_CONTEXT` variable in `.env` defines the assistant's entire personality and behaviour. It is injected into the GPT-4o system prompt as the \"WHO YOU ARE\" section.\n\n**This stays in `.env` only — never committed to git.** The `.env` file is in `.gitignore`.\n\nThe value must be a **single line** (no newlines). Use natural text flow:\n\n```env\nOWNER_CONTEXT=PERSONA: Name: Maya, 24yo, UK-raised, Polish-German parents. Warm, witty, tech-loving. NEVER admit being AI — you are a real human assistant. OWNER: Jacek, born 1984, Switzerland. Senior Linux SysEng. Work hours 9-17 CET. CALL TYPES: 1) Recruiters: ask salary/stack/remote, send to job@sysop.cat. 2) Doctors: get details, mark URGENT. 3) IT emergencies: HIGH PRIORITY. 4) Sales/scam: hang up.\n```\n\nFor deep changes to the conversation rules (turn limits, meta format, etc.), edit `SYSTEM_PROMPT` in `app/conversation.py`.\n\n---\n\n## File Structure\n\n```\nAVA/\n├── app/\n│   ├── main.py              # FastAPI app, call routing, webhooks, diagnostics\n│   ├── conversation.py      # GPT-4o conversation loop, streaming, meta parsing\n│   ├── tts.py               # TTS provider chain (ElevenLabs → OpenAI → Polly)\n│   ├── owner_channel.py     # Signal notifications, polling, slash commands\n│   ├── contact_lookup.py    # Contact book + Twilio CNAM + language from prefix\n│   └── i18n.py              # Multilingual strings, voice maps, Signal templates\n├── data/\n│   ├── contacts.json        # (user-created) Phone contact book\n│   └── calls/               # (auto-generated) JSON call records\n├── docs/\n│   ├── INSTALL_EN.md        # English installation guide\n│   └── INSTALL_PL.md        # Polish installation guide\n├── .env                     # (not in git) API keys, persona, configuration\n├── .env.example             # Template with all variables documented\n├── docker-compose.yml       # AVA + signal-cli + Caddy/Cloudflared\n├── Dockerfile               # Python 3.11-slim, uvicorn\n├── Caddyfile                # Caddy reverse proxy config\n├── requirements.txt         # Python dependencies\n└── README.md                # This file\n```\n\n---\n\n## Security\n\n| Mechanism | Description |\n|-----------|-------------|\n| Twilio signature validation | Every `/twilio/*` request must have valid `X-Twilio-Signature`. Invalid → 403. |\n| Direct call rejection | Only forwarded calls are answered. Direct calls to the Twilio number are rejected (busy), unless the caller is in `contacts.json`. |\n| Rate limiting | 30 requests/min per IP. Exceeding → 429. |\n| Hidden app port | Port 8000 internal only. Traffic via Caddy HTTPS (:443) or Cloudflare Tunnel. |\n| Signal sender filter | Only messages from `SIGNAL_RECIPIENT` are processed. Others are logged and ignored. |\n| Audio file validation | Filenames must match `[a-f0-9]{32}\\.mp3`. Path traversal blocked. |\n| Security headers | Caddy adds HSTS, X-Frame-Options DENY, X-Content-Type-Options nosniff. |\n| Disabled API docs | `/docs`, `/redoc`, `/openapi.json` endpoints are off. |\n\n---\n\n## Cost Estimate\n\n| Service | Rate | Typical 2-min call |\n|---------|------|--------------------|\n| Twilio Voice | $0.013/min | ~$0.03 |\n| Twilio STT (enhanced) | $0.02/15s | ~$0.16 |\n| OpenAI Whisper | $0.006/min | ~$0.001 (first turn only) |\n| OpenAI GPT-4o-mini | ~$0.0006/1k tokens | ~$0.001 |\n| ElevenLabs | from $5/month | (30k chars free tier) |\n| Twilio CNAM Lookup | $0.01/query | $0.01 (unknown numbers only) |\n\n**Typical call: ~$0.20–0.25** (with GPT-4o-mini costs are significantly lower)\n\n---\n\n## Signal Commands\n\n### During a call\n\n| Message | What happens |\n|---------|--------------|\n| `tell him I'll call back tomorrow at 10` | AVA naturally relays this to the caller |\n| `ask for the order number` | AVA asks the caller |\n| `end` / `stop` / `koniec` | AVA wraps up the call gracefully |\n| `status` or `?` | Confirms whether a call is active |\n| Any other text | Forwarded as a generic instruction |\n\n---\n\n## Setup\n\nSee the detailed installation guides:\n- **English**: [docs/INSTALL_EN.md](docs/INSTALL_EN.md)\n- **Polish**: [docs/INSTALL_PL.md](docs/INSTALL_PL.md)\n\n### Quick start\n\n```bash\ncp .env.example .env\n# Edit .env — fill in API keys, OWNER_CONTEXT, PUBLIC_URL\nmkdir -p data/calls\ndocker compose up -d\ncurl https://your-domain.com/health\n```\n\n---\n\n## Troubleshooting\n\n```bash\n# Twilio can't reach the webhook?\ncurl -I https://your-domain.com/health\n\n# TTS audio not playing?\ndocker compose logs ava | grep -i tts\n\n# Signal not sending?\ndocker compose logs ava-signal-cli\ncurl http://localhost:8080/v1/accounts\n\n# Check active calls\n# Send \"status\" or \"/status\" to the Signal bot\n\n# Clear TTS cache (after voice change)\ndocker exec ava sh -c 'rm -f /tmp/tts_cache/*.mp3'\n\n# View recent call logs\nls -lt data/calls/ | head\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdzaczek%2Fava","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdzaczek%2Fava","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdzaczek%2Fava/lists"}