# Project export: DialAI: From any phone, to expert answers

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Your voices are heard: call from any phone to get instant AI help in your language, even without internet or a smartphone.
- Devpost: https://devpost.com/software/dialai-from-any-phone-to-expert-answers
- GitHub: https://github.com/SummitStha/lm4sd
- Video: https://www.youtube.com/embed/uukA76Xq2L0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Summit Shrestha (3 commits)

## Devpost submission (written by the team)

### Inspiration

Millions of people still rely on basic phones or have unreliable/expensive internet. That creates a “voice gap”: not all people can access high-quality information when they need it (health, farming, education, legal aid, disaster help). This project is inspired by the idea that a normal phone call should be enough to access an AI assistant, without requiring a smartphone, apps, or data plans. Also, brainstormed the fairness angle: if AI access is expensive, communities should be able to share access responsibly through quotas and shared plans, similar to how public utilities are managed.

### What it does

DialAI lets anyone dial a phone number and talk to an AI assistant. Works through a simple voice call (no smartphone required). Converts speech → text, the text serves as prompts with added prompt engineering efforts, sends it to a locally at the edge or remotely cloud deployed language model, response text → speech, and speaks the answer back. Supports local languages (depending on speech + text-to-speech (TTS) support). Includes an access control + quota layer so communities can share limited resources fairly. At a high level, we optimize for low connectivity and cost. You can think of it as minimizing the barrier: Access Barrier≈Phone Requirement+Data Requirement+App Requirement DialAI drives “data requirement” and “app requirement” close to zero by making the interface voice-only over GSM/VoIP at the end user's side.

### How we built it

Our system is a simple pipeline: Call Ingestion (Telephony) We used a telephony layer (e.g., Twilio or equivalent) to receive incoming calls and stream audio. Call Ingestion (Telephony) We used a telephony layer (e.g., Twilio or equivalent) to receive incoming calls and stream audio. Speech-to-Text (STT) Incoming audio is transcribed into text using an STT model (e.g., Faster Whisper). This gives us the user’s intent in text form. Speech-to-Text (STT) Incoming audio is transcribed into text using an STT model (e.g., Faster Whisper). This gives us the user’s intent in text form. Prompting + Safety Layer We add lightweight prompt formatting to keep responses clear, short, and voice-friendly (e.g., structured Q&As, ask one follow-up question if needed). We also apply basic safety rules (avoid harmful instructions, keep medical advice cautious, etc.). Prompting + Safety Layer We add lightweight prompt formatting to keep responses clear, short, and voice-friendly (e.g., structured Q&As, ask one follow-up question if needed). We also apply basic safety rules (avoid harmful instructions, keep medical advice cautious, etc.). LLM Response (Locally Deployed Models at Telecoms or the Cloud-based Model APIs) The transcribed text is sent to an LLM API to generate an answer. LLM Response (Locally Deployed Models at Telecoms or the Cloud-based Model APIs) The transcribed text is sent to an LLM API to generate an answer. Text-to-Speech (TTS) The answer is converted back into speech and played to the caller. Text-to-Speech (TTS) The answer is converted back into speech and played to the caller. Identity + Quotas (IAM) [Optional] We maintain simple identity (caller ID / PIN) and policy enforcement (daily minutes, per-user limits) in a small database. Identity + Quotas (IAM) [Optional] We maintain simple identity (caller ID / PIN) and policy enforcement (daily minutes, per-user limits) in a small database.

### Challenges we ran into

1. Latency: A voice pipeline can feel slow (STT + LLM + TTS). We had to keep answers concise and tune the flow with optimization at each component so the user doesn’t feel “stuck waiting.” 2. Noisy audio + accents: Call audio quality varies a lot. Transcription errors increase with background noise, weak signals, and code-switching between languages. 3. Conversation design for voice: Voice UX is different than chat UX. Long answers are painful. We had to learn how to produce short, structured, “spoken” answers. And, if requested, be able to easily interrupt the responses, and initiate new responses. 4. Abuse + safety: Even a simple voice agent needs guardrails (prompt injection attempts, unsafe requests, spam calls). We added basic filtering and logging hooks, which further requires much more policies and enforcement based on community policies. 5. Fair usage: If usage is shared, quotas and policy enforcement become essential so one caller can’t drain the system for everyone. This will be explored as a co-design study with members from the communities.

### Accomplishments we're proud of

Explored various open-sourced models (Gemma, Qwen, Llama) and their variants to better map the tradeoff spaces in terms of the multilingual support, model resource needs (memory, compute), model response quality and accuracy. Built an end-to-end (E2E) prototype where a user can call, speak naturally, and hear an AI response, all without having to be connected to the Internet or have an advanced smartphone. Designed answers to be voice-first: short, readable, and helpful even on low-quality audio calls. Created a foundation that can be deployed as a community/shared service (schools, clinics, municipalities, co-ops).

### What we learned

Voice is the real UI: The best model is not enough, factors such as response length, pacing, and clarity matter more than fancy features when trying to design for resource and infra-constrained minority underrepresented communities. Voice is the real UI: The best model is not enough, factors such as response length, pacing, and clarity matter more than fancy features when trying to design for resource and infra-constrained minority underrepresented communities. Reliability beats complexity: Simple safeguards (timeouts, retries, fallback prompts) make the experience feel much more “real.” Reliability beats complexity: Simple safeguards (timeouts, retries, fallback prompts) make the experience feel much more “real.” Fairness is a product feature: Quotas and identity are not “extra add-ons”, they are all required for long-term service sustainability. Fairness is a product feature: Quotas and identity are not “extra add-ons”, they are all required for long-term service sustainability. Practical lessons about integrating telephony, streaming audio (encoding and decoding with phone audio calls), STT/TTS, and handling edge cases like silence, interruptions, and dropped calls. Practical lessons about integrating telephony, streaming audio (encoding and decoding with phone audio calls), STT/TTS, and handling edge cases like silence, interruptions, and dropped calls.

### What's next

Reduce latency with streaming STT/TTS and partial-response playback (start speaking while generation continues). Reduce latency with streaming STT/TTS and partial-response playback (start speaking while generation continues). Offline / edge options: run smaller local models at the edge on SBCs (Raspberry Pis, Jetson boards) deployed at Telecoms (the other side of edge computing) when backhaul is unreliable, and fall back to cloud when available. Offline / edge options: run smaller local models at the edge on SBCs (Raspberry Pis, Jetson boards) deployed at Telecoms (the other side of edge computing) when backhaul is unreliable, and fall back to cloud when available. Stronger trust & safety: better moderation, abuse detection, and privacy controls to be co-designed with the communities (clear retention policy, anonymization options). Stronger trust & safety: better moderation, abuse detection, and privacy controls to be co-designed with the communities (clear retention policy, anonymization options). Better personalization and privacy mechanisms: caller-ID binding + optional one-time PINs; explore voiceprint only if privacy constraints allow. Better personalization and privacy mechanisms: caller-ID binding + optional one-time PINs; explore voiceprint only if privacy constraints allow. Domain packs with expert fine-tuned models: specialized modes (health triage, agriculture, education) with curated prompts and vetted information sources. Domain packs with expert fine-tuned models: specialized modes (health triage, agriculture, education) with curated prompts and vetted information sources. Pilot deployment: partner with a community organization to test real usage, gather feedback, and measure impact (call success rate, average latency, satisfaction). Pilot deployment: partner with a community organization to test real usage, gather feedback, and measure impact (call success rate, average latency, satisfaction).

## README (from the GitHub repository)

# DialAI: From any phone, to expert answers
Language Model for Sustainable Development (LM4SD)

## Inspiration
Millions of people still rely on basic phones or have unreliable/expensive internet. That creates a **“voice gap”**: not all people can access high-quality information when they need it (health, farming, education, legal aid, disaster help). This project is inspired by the idea that a normal phone call should be enough to access an AI assistant, without requiring a smartphone, apps, or data plans.

We also liked the fairness angle: if AI access is expensive, communities should be able to share access responsibly through quotas and shared plans, similar to how public utilities are managed.

## What it does
DialAI lets anyone dial a phone number and talk to an AI assistant.
- Works through a simple voice call (no smartphone required).
- Converts speech → text, the text serves as prompts with added prompt engineering efforts, sends it to a locally at the edge or remotely cloud deployed language model, response text → speech, and speaks the answer back.
- Supports local languages (depending on speech + text-to-speech (TTS) support).
- Includes an access control + quota layer so communities can share limited resources fairly.

At a high level, we optimize for low connectivity and cost. You can think of it as minimizing the barrier:
Access Barrier≈Phone Requirement+Data Requirement+App Requirement

DialAI drives “data requirement” and “app requirement” close to zero by making the interface voice-only over GSM/VoIP at the end user's side.

## How we built it
Our system is a simple pipeline:
1. Call Ingestion (Telephony)
We used a telephony layer (e.g., Twilio or equivalent) to receive incoming calls and stream audio.

2. Speech-to-Text (STT)
Incoming audio is transcribed into text using an STT model (e.g., Faster Whisper). This gives us the user’s intent in text form.

3. Prompting + Safety Layer
We add lightweight prompt formatting to keep responses clear, short, and voice-friendly (e.g., structured Q&As, ask one follow-up question if needed). We also apply basic safety rules (avoid harmful instructions, keep medical advice cautious, etc.).

4. LLM Response (Locally Deployed Models at Telecoms or the Cloud-based Model APIs)
The transcribed text is sent to an LLM API to generate an answer.

5. Text-to-Speech (TTS)
The answer is converted back into speech and played to the caller.

6. Identity + Quotas (IAM) [Optional]
We maintain simple identity (caller ID / PIN) and policy enforcement (daily minutes, per-user limits) in a small database.

## Challenges we ran into
**Latency**: A voice pipeline can feel slow (STT + LLM + TTS). We had to keep answers concise and tune the flow with optimization at each component so the user doesn’t feel “stuck waiting.”

**Noisy audio + accents:** Call audio quality varies a lot. Transcription errors increase with background noise, weak signals, and code-switching between languages.

**Conversation design for voice:** Voice UX is different than chat UX. Long answers are painful. We had to learn how to produce short, structured, “spoken” answers. And, if requested, be able to easily interrupt the responses, and initiate new responses.

**Abuse + safety:** Even a simple voice agent needs guardrails (prompt injection attempts, unsafe requests, spam calls). We added basic filtering and logging hooks, which further requires much more policies and enforcement based on community policies.

**Fair usage:** If usage is shared, quotas and policy enforcement become essential so one caller can’t drain the system for everyone. This will be explored as a co-design study with members from the communities.

## Accomplishments that we're proud of
- Explored various open-sourced models (Gemma, Qwen, Llama) and their variants to better map the tradeoff spaces in terms of the multilingual support, model resource needs (memory, compute), model response quality and accuracy.
- Built an end-to-end (E2E) prototype where a user can call, speak naturally, and hear an AI response, all without having to be connected to the Internet or have an advanced smartphone.
- Designed answers to be voice-first: short, readable, and helpful even on low-quality audio calls.
- Created a foundation that can be deployed as a community/shared service (schools, clinics, municipalities, co-ops).

## What we learned
**Voice is the real UI:** The best model is not enough, factors such as response length, pacing, and clarity matter more than fancy features when trying to design for resource and infra-constrained minority underrepresented communities.

**Reliability beats complexity:** Simple safeguards (timeouts, retries, fallback prompts) make the experience feel much more “real.”

**Fairness is a product feature:** Quotas and identity are not “extra add-ons”, they are all required for long-term service sustainability.

I also learned practical lessons about integrating telephony, streaming audio (encoding and decoding with phone audio calls), STT/TTS, and handling edge cases like silence, interruptions, and dropped calls.

## What's next for DialAI: From any phone, to expert answers
**Reduce latency** with streaming STT/TTS and partial-response playback (start speaking while generation continues).

**Offline / edge options:** run smaller local models at the edge on SBCs (Raspberry Pis, Jetson boards) deployed at Telecoms (the other side of edge computing) when backhaul is unreliable, and fall back to cloud when available.

**Stronger trust & safety:** better moderation, abuse detection, and privacy controls to be co-designed with the communities (clear retention policy, anonymization options).

**Better personalization and privacy mechanisms:** caller-ID binding + optional one-time PINs; explore voiceprint only if privacy constraints allow.

**Domain packs with expert fine-tuned models:** specialized modes (health triage, agriculture, education) with curated prompts and vetted information sources.

**Pilot deployment:** partner with a community organization to test real usage, gather feedback, and measure impact (call success rate, average latency, satisfaction).

## Detected evidence (automated analysis)

Indexed codebase: 14 recognized source files, 157 KB.
- Flask (technology) — detected in the code
- Python (language) — detected in the code
- Ollama (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (22 of 22)

```
.gitignore
app/__init__.py
app/config.py
app/install_piper_voice.py
app/requirements.txt
app/server_patched.py
app/server_ptt_mandatory.py
app/server_ptt_required.py
app/server_vosk.py
app/server_whisper.py
app/server.py
app/tts_worker.py
app/utils.py
docker-compose.yml
docker-compose.yml.bak
Dockerfile
Dockerfile.ollama
Dockerfile.voice
LICENSE
README.md
requirements.txt
run.sh
```

### Dependencies

- app/requirements.txt: faster-whisper, flask, flask-sock, httpx, noisereduce, numpy, piper-tts, requests, soundfile, vosk, webrtcvad
- requirements.txt: flask, flask-sock, httpx, noisereduce, numpy, piper-tts, requests, soundfile, vosk, webrtcvad

### Recent commits (newest first)

- dev -> Project docs added.
- dev -> optimizations on the pipeline with concurrency integrations and much smoother STT
- dev -> initial working version of the pipeline added
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
flask
flask-sock
vosk
numpy
soundfile
requests
httpx
webrtcvad
noisereduce
piper-tts
```

### Dockerfile

```
FROM python:3.10-slim

RUN apt-get update && \
    apt-get install -y ffmpeg sox libsox-fmt-all wget unzip curl && \
    pip install --no-cache-dir flask flask-sock vosk soundfile requests piper-tts twilio

# === Install Piper voice files ===
RUN mkdir -p /root/.local/share/piper/voices/en_US-amy-low && \
    cd /root/.local/share/piper/voices/en_US-amy-low && \
    wget https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/low/en_US-amy-low.onnx && \
    wget https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/low/en_US-amy-low.onnx.json

WORKDIR /app
COPY . /app

EXPOSE 5000
CMD ["bash", "run.sh"]

```

### docker-compose.yml

```yaml
version: "3.9"

services:
  # === Local LLaMA model service ===
  # ollama:
  #   image: ollama/ollama:latest
  #   container_name: ollama
  #   environment:
  #     - OLLAMA_MODELS=llama3
  #   restart: unless-stopped
  #   ports:
  #     - "11434:11434"
  #   volumes:
  #     - ollama_data:/root/.ollama
  #   networks:
  #     - llm_net

  # === Flask + Vosk + Piper voice server ===
  voice_server:
    build:
      context: .
      dockerfile: Dockerfile.voice
    container_name: voice-server
    restart: unless-stopped
    # depends_on:
    #   - ollama
    networks:
      - llm_net
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - OLLAMA_URL=http://host.docker.internal:11434/api/generate
      - NGROK_DOMAIN=${NGROK_DOMAIN}
      - REPLY_LANG=auto
      - WHISPER_MODEL=large
      - WHISPER_DEVICE=cpu
      - WHISPER_COMPUTE=int16
      - PIPER_PATH=/root/.local/share/piper/voices/ne_NP-chitwan-medium
      - PIPER_MODEL=ne_NP-chitwan-medium
      - PIPER_VOICES_DIR=/root/.local/share/piper/voices
      - PIPER_NE_MODEL=ne_NP-chitwan-medium
      - PIPER_HI_MODEL=hi_IN-rohan-medium
      - PIPER_EN_MODEL=en_US-amy-low
      - TTS_MIN_CHARS=20
      - TWILIO_SEND_FRAME_SLEEP=0.02
    ports:
      - "5000:5000"
    volumes:
      - ./app:/app
    command: ["python3", "-u", "server_ptt_required.py"]

  # === Ngrok tunnel (for Twilio webhook) ===
  ngrok:
    image: ngrok/ngrok:latest
    container_name: ngrok
    restart: unless-stopped
    command:
    - "http"
    - "voice-server:5000"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - NGROK_AUTHTOKEN=${NGROK_AUTHTOKEN}
    depends_on:
      - voice_server
    ports:
      - "4040:4040"
    networks:
      - llm_net

# volumes:
#   ollama_data:

networks:
  llm_net:
    driver: bridge

```

### app/requirements.txt

```
flask
flask-sock
vosk
numpy
soundfile
requests
httpx
webrtcvad
noisereduce
piper-tts
# Whisper STT (multilingual)
faster-whisper
```

### app/server.py

```python
import os
import json
import time
import base64
import audioop
import re
from typing import Optional, Tuple, List, Dict
import noisereduce as nr

import numpy as np
import webrtcvad
import requests
from flask import Flask, request, Response
from flask_sock import Sock
from piper.voice import PiperVoice
from faster_whisper import WhisperModel

# ===========================================
# Flask + WebSocket setup
# ===========================================
app = Flask(__name__)
sock = Sock(app)

# ===========================================
# Env config
# ===========================================
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://host.docker.internal:11434/api/generate")
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3.2:3b")
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "gemma3:4b")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen3:8b")

# Reply language policy:
# - "auto" => reply in detected language (ne/hi/en) and TTS in same language
# - "ne"/"hi"/"en" => force reply language (TTS will also follow forced lang)
REPLY_LANG = os.getenv("REPLY_LANG", "auto").strip().lower()

# Whisper config
WHISPER_MODEL_NAME = os.getenv("WHISPER_MODEL", "large")
WHISPER_DEVICE = os.getenv("WHISPER_DEVICE", "cpu")
WHISPER_COMPUTE = os.getenv("WHISPER_COMPUTE", "int16")

# Piper multi-voice config:
# Put all voices under one directory, each voice has its own subdir named by model id:
#   /root/.local/share/piper/voices/ne_NP-chitwan-medium/ne_NP-chitwan-medium.onnx(.json)
#   /root/.local/share/piper/voices/hi_IN-rohan-medium/hi_IN-rohan-medium.onnx(.json)
#   /root/.local/share/piper/voices/en_US-amy-low/en_US-amy-low.onnx(.json)
PIPER_VOICES_DIR = os.getenv("PIPER_VOICES_DIR", "/root/.local/share/piper/voices").strip()
PIPER_NE_MODEL = os.getenv("PIPER_NE_MODEL", "ne_NP-chitwan-medium").strip()
PIPER_HI_MODEL = os.getenv("PIPER_HI_MODEL", "hi_IN-rohan-medium").strip()
PIPER_EN_MODEL = os.getenv("PIPER_EN_MODEL", "en_US-amy-low").strip()

# Backward compatibility (if you still export single-voice vars)
PIPER_PATH = os.getenv("PIPER_PATH", "").strip()
PIPER_MODEL = os.getenv("PIPER_MODEL", "").strip()

# Twilio media stream audio details:
# - inbound audio: 8kHz mu-law payload, 20ms frames
TWILIO_SR = 8000
FRAME_MS = 20
SAMPLES_PER_FRAME_8K = int(TWILIO_SR * FRAME_MS / 1000)  # 160
PCM16_BYTES_PER_FRAME = SAMPLES_PER_FRAME_8K * 2         # 320 bytes (16-bit)

# ===========================================
# Initialize Whisper
# ===========================================
print(f"[INIT] Loading Whisper model: {WHISPER_MODEL_NAME} ({WHISPER_DEVICE}, {WHISPER_COMPUTE})")
whisper = WhisperModel(
    WHISPER_MODEL_NAME,
    device=WHISPER_DEVICE,
    compute_type=WHISPER_COMPUTE,
)
print("[INIT] ✅ Whisper loaded.")

# ===========================================
# Initialize Piper voices
# ===========================================
def _piper_files_exist(onnx_path: str, json_path: str) -> bool:
    return os.path.exists(onnx_path) and os.path.exists(json_path)

def _load_piper_from_dir(model_dir: str, model_name: str) -> PiperVoice:
    onnx_path = os.path.join(model_dir, f"{model_name}.onnx")
    json_path = os.path.join(model_dir, f"{model_name}.onnx.json")
    if not _piper_files_exist(onnx_path, json_path):
        raise RuntimeError(
            f"❌ Piper files missing for model '{model_name}'.\n"
            f"Expected:\n  {onnx_path}\n  {json_path}\n"
        )
    return PiperVoice.load(onnx_path, config_path=json_path)

def _load_piper_voice(model_name: str) -> PiperVoice:
    # Prefer multi-voice layout: {PIPER_VOICES_DIR}/{model_name}/{model_name}.onnx(.json)
    model_dir = os.path.join(PIPER_VOICES_DIR, model_name)
    if os.path.isdir(model_dir):
        return _load_piper_from_dir(model_dir, model_name)

    # Back-compat: single voice layout using PIPER_PATH + PIPER_MODEL
    if PIPER_PATH and PIPER_MODEL and model_name == PIPER_MODEL:
        return _load_piper_from_dir(PIPER_PATH, PIPER_MODEL)

    # Last resort: try PIPER_VOICES_DIR directly (flat) if someone put files there
    onnx_flat = os.path.join(PIPER_VOICES_DIR, f"{model_name}.onnx")
    json_flat = os.path.join(PIPER_VOICES_DIR, f"{model_name}.onnx.json")
    if _piper_files_exist(onnx_flat, json_flat):
        return PiperVoice.load(onnx_flat, config_path=json_flat)

    raise RuntimeError(
        f"❌ Could not locate Piper model '{model_name}'.\n"
        f"Tried:\n  {model_dir}/...\n  {PIPER_PATH} (if configured)\n  flat: {onnx_flat}\n"
    )

print("[INIT] Loading Piper voices (ne/hi/en)...")
PIPER_VOICES: Dict[str, PiperVoice] = {
    "ne": _load_piper_voice(PIPER_NE_MODEL),
    "hi": _load_piper_voice(PIPER_HI_MODEL),
    "en": _load_piper_voice(PIPER_EN_MODEL),
}
print("[INIT] ✅ Piper voices loaded.")

# ===========================================
# Helper: build Twilio-compliant outbound audio
# ===========================================
def speak_text_to_mulaw_8k(text: str, tts_voice: PiperVoice) -> bytes:
    """
    Piper -> PCM16 (native sr) -> resample to 8k -> mu-law bytes for Twilio.
    Robust against piper returning a generator of AudioChunk.
    """
    text = (text or "").strip()
    if not text:
        return b""

    try:
        result = tts_voice.synthesize(text)
        pcm_bytes = bytearray()
        sr = 22050

        # Piper commonly returns a generator of AudioChunk
        if hasattr(result, "__iter__") and not isinstance(result, (bytes, bytearray, np.ndarray, str)):
            for chunk in result:
                if hasattr(chunk, "audio_int16_bytes"):
                    pcm_bytes.extend(chunk.audio_int16_bytes)
                    sr = int(getattr(chunk, "sample_rate", sr))
                elif hasattr(chunk, "audio_int16_array"):
                    arr = chunk.audio_int16_array
                    pcm_bytes.extend(np.asarray(arr, dtype=np.int16).tobytes())
                    sr = int(getattr(chunk, "sample_rate", sr))
                elif hasattr(chunk, "audio_float_array"):
      
[truncated — 17337 more characters]
```

### run.sh

```shell
#!/bin/bash
set -e

echo "=== Checking Ollama availability... ==="
until curl -s http://host.docker.internal:11434 > /dev/null; do
  echo "Waiting for Ollama..."
  sleep 2
done
echo "=== Ollama is available ==="

echo "=== Launching Flask ==="
# Run Flask in the foreground and log errors
python3 -u -m app.server || { 
    echo "Flask exited unexpectedly with code $?"; 
    sleep 10; 
}

```

### app/utils.py

```python
import base64, json, audioop

def decode_twilio_audio(payload_b64: str) -> bytes:
    mulaw_audio = base64.b64decode(payload_b64)
    return audioop.ulaw2lin(mulaw_audio, 2)

```

### app/install_piper_voice.py

```python
from piper.download import download_voice_model

# Download the "en_US-amy-low" voice and keep it cached under
# /root/.local/share/piper/voices/en_US-amy-low
print("Downloading Piper voice model (en_US-amy-low)...")
download_voice_model("en_US-amy-low")
print("Piper voice model installed successfully.")

```

### app/config.py

```python
# config.py
import os

# Twilio credentials (used only if needed for outbound calls or signature verification)
# TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID", "")
# TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN", "")

# Ollama endpoint
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://host.docker.internal:11434/api/generate")

# Ngrok public domain (used in TwiML)
NGROK_DOMAIN = os.getenv("NGROK_DOMAIN", "anna-bushier-noncensoriously.ngrok-free.dev")

```

### app/tts_worker.py

```python
import os, json, sys, base64, audioop
import numpy as np
from typing import Dict
from piper.voice import PiperVoice

TWILIO_SR = 8000

PIPER_VOICES_DIR = os.getenv("PIPER_VOICES_DIR", "/root/.local/share/piper/voices").strip()
PIPER_NE_MODEL = os.getenv("PIPER_NE_MODEL", "ne_NP-chitwan-medium").strip()
PIPER_HI_MODEL = os.getenv("PIPER_HI_MODEL", "hi_IN-rohan-medium").strip()
PIPER_EN_MODEL = os.getenv("PIPER_EN_MODEL", "en_US-amy-low").strip()
PIPER_PATH = os.getenv("PIPER_PATH", "").strip()
PIPER_MODEL = os.getenv("PIPER_MODEL", "").strip()

def _piper_files_exist(onnx_path: str, json_path: str) -> bool:
    return os.path.exists(onnx_path) and os.path.exists(json_path)

def _load_piper_from_dir(model_dir: str, model_name: str) -> PiperVoice:
    onnx_path = os.path.join(model_dir, f"{model_name}.onnx")
    json_path = os.path.join(model_dir, f"{model_name}.onnx.json")
    if not _piper_files_exist(onnx_path, json_path):
        raise RuntimeError(f"Piper files missing for model '{model_name}' at {onnx_path} / {json_path}")
    return PiperVoice.load(onnx_path, config_path=json_path)

def _load_piper_voice(model_name: str) -> PiperVoice:
    model_dir = os.path.join(PIPER_VOICES_DIR, model_name)
    if os.path.isdir(model_dir):
        return _load_piper_from_dir(model_dir, model_name)

    if PIPER_PATH and PIPER_MODEL and model_name == PIPER_MODEL:
        return _load_piper_from_dir(PIPER_PATH, PIPER_MODEL)

    onnx_flat = os.path.join(PIPER_VOICES_DIR, f"{model_name}.onnx")
    json_flat = os.path.join(PIPER_VOICES_DIR, f"{model_name}.onnx.json")
    if _piper_files_exist(onnx_flat, json_flat):
        return PiperVoice.load(onnx_flat, config_path=json_flat)

    raise RuntimeError(f"Could not locate Piper model '{model_name}'")

print("[TTS_WORKER] Loading Piper voices (ne/hi/en)...", file=sys.stderr, flush=True)
VOICES: Dict[str, PiperVoice] = {
    "ne": _load_piper_voice(PIPER_NE_MODEL),
    "hi": _load_piper_voice(PIPER_HI_MODEL),
    "en": _load_piper_voice(PIPER_EN_MODEL),
}
print("[TTS_WORKER] Ready.", file=sys.stderr, flush=True)

def synth_to_mulaw_8k(text: str, voice: PiperVoice) -> bytes:
    text = (text or "").strip()
    if not text:
        return b""
    result = voice.synthesize(text)

    pcm_bytes = bytearray()
    sr = 22050

    if hasattr(result, "__iter__") and not isinstance(result, (bytes, bytearray, np.ndarray, str)):
        for chunk in result:
            if hasattr(chunk, "audio_int16_bytes"):
                pcm_bytes.extend(chunk.audio_int16_bytes)
                sr = int(getattr(chunk, "sample_rate", sr))
            elif hasattr(chunk, "audio_int16_array"):
                arr = np.asarray(chunk.audio_int16_array, dtype=np.int16)
                pcm_bytes.extend(arr.tobytes())
                sr = int(getattr(chunk, "sample_rate", sr))
            elif hasattr(chunk, "audio_float_array"):
                arr = np.asarray(chunk.audio_float_array, dtype=np.float32)
                arr = np.clip(arr, -1.0, 1.0)
                pcm16 = (arr * 32767.0).astype(np.int16)
                pcm_bytes.extend(pcm16.tobytes())
                sr = int(getattr(chunk, "sample_rate", sr))
            else:
                arr = np.asarray(chunk, dtype=np.float32)
                arr = np.clip(arr, -1.0, 1.0)
                pcm16 = (arr * 32767.0).astype(np.int16)
                pcm_bytes.extend(pcm16.tobytes())
    else:
        arr = np.asarray(result, dtype=np.float32)
        arr = np.clip(arr, -1.0, 1.0)
        pcm16 = (arr * 32767.0).astype(np.int16)
        pcm_bytes.extend(pcm16.tobytes())
        sr = int(getattr(voice, "sample_rate", sr))

    if not pcm_bytes:
        return b""

    pcm_8k = audioop.ratecv(bytes(pcm_bytes), 2, 1, sr, TWILIO_SR, None)[0]
    mulaw = audioop.lin2ulaw(pcm_8k, 2)
    return mulaw

def write_json(obj):
    sys.stdout.write(json.dumps(obj) + "\n")
    sys.stdout.flush()

for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    try:
        req = json.loads(line)
    except Exception:
        continue

    if req.get("cmd") != "synth":
        continue

    lang = (req.get("lang") or "hi").lower().strip()
    if lang not in ("ne", "hi", "en"):
        lang = "hi"
    text = req.get("text") or ""

    try:
        mulaw = synth_to_mulaw_8k(text, VOICES[lang])
        # stream in chunks (arbitrary, e.g. 8000 bytes)
        CHUNK = 8000
        for i in range(0, len(mulaw), CHUNK):
            b64 = base64.b64encode(mulaw[i:i+CHUNK]).decode("ascii")
            write_json({"type": "chunk", "b64": b64})
        write_json({"type": "done"})
    except Exception as e:
        write_json({"type": "err", "msg": str(e)})
```

[5 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]