diff --git a/.gitignore b/.gitignore
index 609a81c..4262a08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -144,3 +144,5 @@ dmypy.json
.pyre/
+
+**/.claude/settings.local.json
diff --git a/blogs/telnyx-solaria-python-fastapi/.gitignore b/blogs/telnyx-solaria-python-fastapi/.gitignore
new file mode 100644
index 0000000..85c55eb
--- /dev/null
+++ b/blogs/telnyx-solaria-python-fastapi/.gitignore
@@ -0,0 +1,2 @@
+.env
+.venv
diff --git a/blogs/telnyx-solaria-python-fastapi/blog.md b/blogs/telnyx-solaria-python-fastapi/blog.md
new file mode 100644
index 0000000..59d57d9
--- /dev/null
+++ b/blogs/telnyx-solaria-python-fastapi/blog.md
@@ -0,0 +1,311 @@
+## How to Transcribe Vonage Calls in Real Time with FastAPI and Python & Gladia
+
+Vonage's Voice APIs deliver audio via WebSockets that can be configured for various formats. Gladia's real-time Speech-to-Text (STT) API is flexible enough to handle this audio with minimal processing, enabling you to build real-time transcription with sub-300 ms latency.
+
+Gladia's `/v2/live` endpoint lets you specify multiple audio format configurations to match what Vonage delivers ([Gladia][1]).
+
+---
+
+### Prerequisites
+
+| What you need | Why |
+| ----------------------------------------- | -------------------------------------------------------------------- |
+| **Gladia API key** | Sign up & copy from the dashboard. |
+| **Vonage account + voice-enabled number** | To receive / place calls. |
+| **Python 3.12+** | We'll use `fastapi`, `uvicorn`, `websockets`, and `requests`. |
+| **Public URL** | Expose a WebSocket endpoint with ngrok or a cloud VM. |
+| **Audio format from Vonage** | Typically Linear PCM (L16) but configurable in Vonage's NCCO. |
+
+---
+
+### 1 — Initiate a Gladia live session
+
+```python
+import os
+import requests
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/pcm", # For Vonage's Linear PCM
+ "sample_rate": 8000, # Standard telephony sample rate
+ "channels": 1,
+ }
+
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ print("🛰 Gladia session ID:", data["id"])
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+```
+
+> **Note on audio format:** Vonage WebSockets typically send L16 PCM audio by default. Gladia can process this directly or you can configure Vonage to send other formats.
+
+---
+
+### System Architecture Flow
+
+Here's how data flows through the system:
+
+```mermaid
+sequenceDiagram
+ participant Caller
+ participant Vonage
+ participant FastAPI as FastAPI WebSocket Server
+ participant Gladia as Gladia API
+
+ FastAPI->>Gladia: Initialize session (POST /v2/live)
+ Gladia-->>FastAPI: Return WebSocket URL
+
+ Caller->>Vonage: Make phone call
+ Vonage->>FastAPI: Connect to WebSocket (/media)
+
+ loop Audio Streaming
+ Vonage->>FastAPI: Send audio chunks (base64 encoded)
+ FastAPI->>FastAPI: Decode base64
+ FastAPI->>Gladia: Forward audio bytes
+ Gladia->>Gladia: Process speech
+ Gladia-->>FastAPI: Return partial transcripts
+ Gladia-->>FastAPI: Return final transcripts
+ FastAPI->>FastAPI: Log/process transcripts
+ end
+
+ Caller->>Vonage: End call
+ Vonage->>FastAPI: Close WebSocket
+ FastAPI->>Gladia: Close WebSocket
+```
+
+---
+
+### 2 — Build the Python WebSocket proxy
+
+The proxy does **three** things:
+
+1. Accept Vonage's base64-encoded audio frames.
+2. Base64-decode the audio data.
+3. Pipe the bytes straight to Gladia and print transcripts as they come back.
+
+```python
+# server.py
+import os
+import base64
+import json
+import logging
+import asyncio
+import websockets
+import requests
+from fastapi import FastAPI, WebSocket
+from dotenv import load_dotenv
+import uvicorn
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+# Convert HTTP_PORT to integer
+HTTP_PORT = int(os.getenv("HTTP_PORT", "5000"))
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = FastAPI()
+
+# Store gladia session information
+gladia_session = {
+ "id": None,
+ "url": None,
+}
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/pcm", # For Vonage's Linear PCM
+ "sample_rate": 8000, # Standard telephony sample rate
+ "channels": 1,
+ }
+
+ try:
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ logger.info("🛰 Gladia session ID: %s", data["id"])
+ gladia_session["id"] = data["id"]
+ gladia_session["url"] = data["url"]
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+ except requests.exceptions.RequestException as e:
+ logger.error("Failed to create Gladia session: %s", e)
+ raise
+
+# Create initial Gladia session
+try:
+ create_session()
+except Exception as e:
+ logger.error("Failed to create initial Gladia session: %s", e)
+ raise
+
+@app.get("/health")
+def health_check():
+ return {"status": "ok", "service": "vonage-gladia-transcription"}
+
+@app.websocket("/media")
+async def media(websocket: WebSocket):
+ """Handle incoming WebSocket connections from Vonage."""
+ await websocket.accept()
+ client = websocket.client
+ client_info = f"{client.host}:{client.port}"
+ logger.info(f"🔌 Vonage WebSocket connected from {client_info}")
+
+ await handle_websocket(websocket)
+
+async def process_message(ws_message, gladia_ws):
+ """Process a message asynchronously."""
+ try:
+ data = json.loads(ws_message)
+
+ # Vonage WebSocket sends audio data with a different structure
+ # Check if this is audio data from Vonage
+ if "content" in data and data.get("content", {}).get("encoding") == "audio/l16;rate=8000":
+ # Extract audio data and convert from base64
+ audio_payload = base64.b64decode(data["content"]["data"])
+
+ # Send the audio data to Gladia
+ await gladia_ws.send(audio_payload)
+ else:
+ logger.debug(f"Non-audio event: {json.dumps(data)[:100]}...")
+ return # ignore non-audio events
+
+ # Check for transcripts
+ try:
+ # Try to get transcripts (non-blocking)
+ while True:
+ resp = await asyncio.wait_for(gladia_ws.recv(), 0.001)
+ handle_gladia(resp)
+ except asyncio.TimeoutError:
+ pass # No transcript available yet
+ except Exception as e:
+ logger.error(f"Error processing message: {e}")
+
+def handle_gladia(msg):
+ """Process transcription results from Gladia."""
+ m = json.loads(msg)
+ if m["type"] == "transcript" and m["data"]["is_final"]:
+ transcript = m["data"]["utterance"]["text"]
+ print(f"📝 Transcript: {transcript}")
+ return transcript
+```
+
+With proper asynchronous handling for high performance.
+
+---
+
+### 3 — Configure Vonage to stream audio
+
+Create an NCCO (Nexmo Call Control Object) in your Vonage dashboard like:
+
+```xml
+
+
+
+ wss://jl.mydomain.com/media
+ audio/l16;rate=8000
+
+
+
+ phone
+ 14155551234
+
+
+
+```
+
+Let's examine each element in this NCCO configuration:
+
+- ``: The root element of any Vonage NCCO document. It contains all the instructions for handling the call.
+
+- ``: This element configures a WebSocket connection for streaming audio:
+ - ``: Specifies the WebSocket endpoint where Vonage will send the audio data.
+ - The URL must use secure WebSockets (`wss://`).
+ - The domain should be your public domain (e.g., an ngrok URL or a custom domain).
+ - The path (`/media`) must match the WebSocket route in your FastAPI application.
+ - Each call will create a new WebSocket connection to this endpoint.
+ - ``: Specifies the audio format that Vonage will send (Linear PCM at 8kHz in this case).
+
+- ``: This element connects the caller to another phone number:
+ - During this connection, the media streaming continues in the background.
+ - Audio from both sides of the conversation is sent to your WebSocket endpoint.
+ - The number shown (`14155551234`) is just an example - replace it with your desired destination.
+
+When a call triggers this NCCO, Vonage immediately opens a secure WebSocket connection to your server's `/media` endpoint and begins streaming audio. Each audio chunk is base64-encoded and sent as a JSON message, which your server then decodes and forwards to Gladia.
+
+---
+
+### 4 — Expose & test
+
+```bash
+# Install dependencies
+pip install -r requirements.txt
+
+# Run the proxy (default port 5000)
+python server.py
+
+# Or specify a custom port
+HTTP_PORT=5001 python server.py
+
+# Tunnel it with ngrok
+ngrok http 5000
+
+# Or with a custom domain
+ngrok http --domain=your.domain.com 5000
+```
+
+Call your Vonage number and you should see live text scroll instantly:
+
+```
+🛰 Gladia session ID: 3f65…
+🚀 Starting server on 0.0.0.0:5000
+🔌 Vonage WebSocket connected from 54.174.99.133:12345
+📝 Transcript: Hello and thank you for calling Acme support.
+📝 Transcript: Sure, I'd be happy to help with your order.
+```
+
+---
+
+### 5 — Next steps
+
+* **Add-ons** – enable diarization, sentiment, keywords, etc., by including the flags when you create the session.
+* **Dual-channel** – Vonage can stream separate channels; Gladia preserves them so you always know who's speaking.
+* **Post-call JSON** – store the session `id` and hit `GET /v2/live/:id` for the full, punctuated transcript when the call ends.
+* **Scale it** – FastAPI with Uvicorn is production-ready, but for even higher loads, consider deploying with Gunicorn as a process manager.
+
+---
+
+### Wrap-up
+
+Real-time call transcription with Vonage and Gladia is simple and straightforward. With just a decode and forward approach, you can quickly add speech-to-text capabilities to your Vonage applications. The minimal processing needed means lower CPU usage and lightning-fast latency. Drop this proxy into any Python stack and start surfacing live insights from every call. Happy building! 🎙️📝
+
+[1]: https://docs.gladia.io/api-reference/v2/live/init "Initiate a session - Gladia"
\ No newline at end of file
diff --git a/blogs/telnyx-solaria-python-fastapi/src/README.md b/blogs/telnyx-solaria-python-fastapi/src/README.md
new file mode 100644
index 0000000..6e677d7
--- /dev/null
+++ b/blogs/telnyx-solaria-python-fastapi/src/README.md
@@ -0,0 +1,96 @@
+# Vonage Call Transcription with Gladia
+
+This project implements real-time transcription of Vonage calls using Gladia's Speech-to-Text API, which natively supports audio format conversion from Vonage's WebSocket streams.
+
+## Prerequisites
+
+- **Gladia API key** - Sign up at [app.gladia.io](https://app.gladia.io/)
+- **Vonage account + voice-enabled number**
+- **Python 3.8+**
+- **Public URL** - Use ngrok or a cloud VM to expose your WebSocket endpoint
+
+## Setup
+
+1. **Install dependencies**:
+ ```bash
+ # Install pyenv (if not already installed)
+ # macOS (using Homebrew)
+ brew install pyenv
+
+ # Linux
+ curl https://pyenv.run | bash
+
+ # Setup pyenv
+ pyenv install 3.12
+ pyenv local 3.12
+
+ pyenv virtualenv 3.12 vonage-gladia-python
+
+ # Install Python dependencies
+ pip install -r requirements.txt
+ ```
+
+2. **Set up environment variables**:
+ Create a `.env` file in the same directory as `server.py` with:
+ ```
+ GLADIA_API_KEY=your_gladia_api_key_here
+ ```
+
+3. **Configure Vonage**:
+ - Create a new application in your Vonage dashboard at [https://dashboard.nexmo.com/applications](https://dashboard.nexmo.com/applications)
+ - Create a new voice application or use an existing one
+ - Link your Vonage phone number to this application
+ - In your answer URL configuration, use the content of `vonage_example.xml` as your NCCO (Nexmo Call Control Object)
+ - Replace `jl.mydomain.com` with your actual public domain (the ngrok URL from step 2 in "Running the application" or a custom domain)
+
+## Technical Notes
+
+- The server uses FastAPI with native WebSocket support
+- Uvicorn is used as the ASGI server
+- The application is fully asynchronous
+- Unlike Twilio which uses μ-law, Vonage typically sends linear PCM audio (audio/l16)
+
+## Running the application
+
+1. **Start the server**:
+ ```bash
+ # Default port (5000)
+ python server.py
+
+ # Or specify a custom port
+ HTTP_PORT=5001 python server.py
+ ```
+
+2. **Expose your server publicly**:
+ ```bash
+ # Make sure the port matches HTTP_PORT from step 1
+ ngrok http 5000 # you'll get a ngrok assigned random URL
+
+ # If you used a custom port in step 1, use the same port here
+ ngrok http 5001
+
+ # for a custom domain
+ ngrok http --domain=jl.mydomain.com 5001
+ ```
+
+3. **Update your NCCO**:
+ - Update the `uri` in your NCCO to your ngrok URL (e.g., `wss://jl.mydomain.com/media` or the random URL assigned by ngrok)
+
+4. **Test**:
+ - Call your Vonage number
+ - You should see transcripts appearing in your console
+
+## How it works
+
+1. The server connects to Gladia's real-time STT API
+2. When a call comes in, Vonage connects to your WebSocket endpoint
+3. Vonage streams audio frames (typically linear PCM), which are base64-decoded
+4. The audio data is forwarded to Gladia with minimal processing
+5. Gladia returns real-time transcription results
+
+## Next steps
+
+- **Add-ons** – enable diarization, sentiment, keywords, etc.
+- **Dual-channel** – Transcribe both sides of the conversation separately
+- **Post-call JSON** – Get the full transcript when the call ends
+- **Scale it** – Use Gunicorn as a process manager for even higher loads
\ No newline at end of file
diff --git a/blogs/telnyx-solaria-python-fastapi/src/env_setup.txt b/blogs/telnyx-solaria-python-fastapi/src/env_setup.txt
new file mode 100644
index 0000000..f950bb8
--- /dev/null
+++ b/blogs/telnyx-solaria-python-fastapi/src/env_setup.txt
@@ -0,0 +1,7 @@
+Before running the application, create a .env file in the same directory with the following contents:
+
+GLADIA_API_KEY=your_gladia_api_key_here
+# Optional: Set a custom port (default is 5000)
+# HTTP_PORT=5001
+
+You need to replace 'your_gladia_api_key_here' with your actual Gladia API key from https://app.gladia.io/
\ No newline at end of file
diff --git a/blogs/telnyx-solaria-python-fastapi/src/requirements.txt b/blogs/telnyx-solaria-python-fastapi/src/requirements.txt
new file mode 100644
index 0000000..54fbbba
--- /dev/null
+++ b/blogs/telnyx-solaria-python-fastapi/src/requirements.txt
@@ -0,0 +1,6 @@
+fastapi==0.108.0
+uvicorn==0.25.0
+websockets==15.0.1
+requests==2.31.0
+python-dotenv==1.0.1
+greenlet==3.2.1
\ No newline at end of file
diff --git a/blogs/telnyx-solaria-python-fastapi/src/server.py b/blogs/telnyx-solaria-python-fastapi/src/server.py
new file mode 100644
index 0000000..0051da1
--- /dev/null
+++ b/blogs/telnyx-solaria-python-fastapi/src/server.py
@@ -0,0 +1,174 @@
+import os
+import base64
+import json
+import logging
+import asyncio
+import websockets
+import requests
+from fastapi import FastAPI, WebSocket
+from dotenv import load_dotenv
+import uvicorn
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+# Convert HTTP_PORT to integer
+HTTP_PORT = int(os.getenv("HTTP_PORT", "5000"))
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = FastAPI()
+
+# Store gladia session information
+gladia_session = {
+ "id": None,
+ "url": None,
+}
+
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/ulaw", # μ-law!
+ "bit_depth": 8, # 8-bit μ-law
+ "sample_rate": 8000, # matches Vonage
+ "channels": 1,
+ }
+
+ try:
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ logger.info("🛰 Gladia session ID: %s", data["id"])
+ gladia_session["id"] = data["id"]
+ gladia_session["url"] = data["url"]
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+ except requests.exceptions.RequestException as e:
+ logger.error("Failed to create Gladia session: %s", e)
+ raise
+
+
+# Create initial Gladia session
+try:
+ create_session()
+except Exception as e:
+ logger.error("Failed to create initial Gladia session: %s", e)
+ raise
+
+
+def handle_gladia(msg):
+ """Process transcription results from Gladia."""
+ m = json.loads(msg)
+ if m["type"] == "transcript" and m["data"]["is_final"]:
+ transcript = m["data"]["utterance"]["text"]
+ print(f"📝 Transcript: {transcript}")
+ return transcript
+
+
+# Add a health check endpoint
+@app.get("/health")
+def health_check():
+ return {"status": "ok", "service": "vonage-gladia-transcription"}
+
+
+@app.websocket("/media")
+async def media(websocket: WebSocket):
+ """Handle incoming WebSocket connections from Vonage."""
+ await websocket.accept()
+ client = websocket.client
+ client_info = f"{client.host}:{client.port}"
+ logger.info(f"🔌 Vonage WebSocket connected from {client_info}")
+
+ await handle_websocket(websocket)
+
+
+@app.websocket("/{remaining_path:path}")
+async def catch_all_websocket(websocket: WebSocket, remaining_path: str):
+ """Catch-all handler for WebSocket connections."""
+ await websocket.accept()
+ logger.info(f"🔌 Catch-all WebSocket connected to /{remaining_path}")
+
+ await handle_websocket(websocket)
+
+
+async def process_message(ws_message, gladia_ws):
+ """Process a message asynchronously."""
+ try:
+ data = json.loads(ws_message)
+
+ # Vonage WebSocket sends audio data with a different structure
+ # Check if this is audio data from Vonage
+ if "content" in data and data.get("content", {}).get("encoding") == "audio/l16;rate=8000":
+ # Extract audio data and convert from base64
+ audio_payload = base64.b64decode(data["content"]["data"])
+
+ # Convert L16 PCM to μ-law for Gladia if needed
+ # For simplicity, we'll assume Vonage is already configured to send μ-law
+ # If not, a conversion would be needed here
+
+ # Send the audio data to Gladia
+ await gladia_ws.send(audio_payload)
+ elif data.get("event") == "media":
+ # Legacy format handling (from Twilio-style messages)
+ mulaw = base64.b64decode(data["media"]["payload"])
+ await gladia_ws.send(mulaw)
+ else:
+ logger.debug(f"Non-audio event: {json.dumps(data)[:100]}...")
+ return # Ignore other message types
+
+ # Check for transcripts
+ try:
+ # Try to get transcripts (non-blocking)
+ while True:
+ resp = await asyncio.wait_for(gladia_ws.recv(), 0.001)
+ handle_gladia(resp)
+ except asyncio.TimeoutError:
+ pass # No transcript available yet, continue
+ except Exception as e:
+ logger.error(f"Error processing message: {e}")
+
+
+async def handle_websocket(websocket: WebSocket):
+ """Handle a WebSocket connection asynchronously."""
+ # Connect to Gladia for this connection
+ try:
+ gladia_ws = await websockets.connect(gladia_session["url"])
+ logger.info(f"Connected to Gladia session {gladia_session['id']}")
+
+ while True:
+ try:
+ message = await websocket.receive_text()
+ await process_message(message, gladia_ws)
+ except Exception as e:
+ logger.error(f"Error receiving message: {e}")
+ break
+
+ except websockets.exceptions.ConnectionClosed:
+ logger.info("Gladia WebSocket connection closed")
+ finally:
+ # Close the Gladia connection
+ try:
+ await gladia_ws.close()
+ except:
+ pass
+
+
+if __name__ == "__main__":
+ if not GLADIA_KEY:
+ logger.error("GLADIA_API_KEY environment variable is required")
+ exit(1)
+
+ logger.info(f"🚀 Starting server on 0.0.0.0:{HTTP_PORT}")
+ uvicorn.run(app, host="0.0.0.0", port=HTTP_PORT)
\ No newline at end of file
diff --git a/blogs/telnyx-solaria-python-fastapi/src/vonage_example.xml b/blogs/telnyx-solaria-python-fastapi/src/vonage_example.xml
new file mode 100644
index 0000000..63d71e2
--- /dev/null
+++ b/blogs/telnyx-solaria-python-fastapi/src/vonage_example.xml
@@ -0,0 +1,13 @@
+
+
+
+ wss://jl.mydomain.com/media
+ audio/l16;rate=8000
+
+
+
+ phone
+ 14155551234
+
+
+
\ No newline at end of file
diff --git a/blogs/twilio-solaria-go/.gitignore b/blogs/twilio-solaria-go/.gitignore
new file mode 100644
index 0000000..85c55eb
--- /dev/null
+++ b/blogs/twilio-solaria-go/.gitignore
@@ -0,0 +1,2 @@
+.env
+.venv
diff --git a/blogs/twilio-solaria-go/blog.md b/blogs/twilio-solaria-go/blog.md
new file mode 100644
index 0000000..9f3c7b5
--- /dev/null
+++ b/blogs/twilio-solaria-go/blog.md
@@ -0,0 +1,369 @@
+## How to Transcribe Twilio Calls in Real Time with Go & Gladia (μ-law Native)
+
+Twilio's Voice **Media Streams** deliver 8 kHz, 8-bit μ-law audio. Gladia's real-time Speech-to-Text (STT) API now ingests that exact format out-of-the-box, so you can skip every resample or decode step and still keep sub-300 ms latency.
+
+Gladia's `/v2/live` endpoint lets you specify **`encoding: "wav/ulaw"`** with **`bit_depth: 8`**, matching Twilio 1-for-1 ([Gladia][1]).
+
+---
+
+### Prerequisites
+
+| What you need | Why |
+| ----------------------------------------- | -------------------------------------------------------------------- |
+| **Gladia API key** | Sign up & copy from the dashboard. |
+| **Twilio account + voice-enabled number** | To receive / place calls. |
+| **Go 1.18.1+** | We'll use `gorilla/websocket` for WebSocket handling. |
+| **Public URL** | Expose a WebSocket endpoint with ngrok or a cloud VM. |
+| **8 kHz, 8-bit μ-law audio** | Exactly what Twilio streams – and what Gladia now consumes natively. |
+
+---
+
+### 1 — Initiate a Gladia live session
+
+```go
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "time"
+)
+
+const (
+ gladiaInitURL = "https://api.gladia.io/v2/live"
+)
+
+// GladiaSession stores session information
+type GladiaSession struct {
+ ID string `json:"id"`
+ URL string `json:"url"`
+}
+
+// createSession initializes a Gladia real-time transcription session and returns the WebSocket URL.
+func createSession() (GladiaSession, error) {
+ payload := map[string]interface{}{ // μ-law, 8-bit, 8 kHz, mono
+ "encoding": "wav/ulaw",
+ "bit_depth": 8,
+ "sample_rate": 8000,
+ "channels": 1,
+ }
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return GladiaSession{}, fmt.Errorf("failed to marshal payload: %w", err)
+ }
+ req, err := http.NewRequest("POST", gladiaInitURL, bytes.NewReader(body))
+ if err != nil {
+ return GladiaSession{}, fmt.Errorf("failed to create request: %w", err)
+ }
+ req.Header.Set("X-Gladia-Key", gladiaAPIKey)
+ req.Header.Set("Content-Type", "application/json")
+ client := &http.Client{Timeout: 10 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return GladiaSession{}, fmt.Errorf("session init request failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ return GladiaSession{}, fmt.Errorf("bad status code: %d - %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ var data GladiaSession
+ if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+ return GladiaSession{}, fmt.Errorf("failed to decode response: %w", err)
+ }
+ log.Printf("🛰 Gladia session ID: %s", data.ID)
+ return data, nil
+}
+```
+
+> **Why no resample / decode?** Gladia parses raw μ-law frames directly, so we just forward the bytes Twilio gives us.
+
+---
+
+### System Architecture Flow
+
+Here's how data flows through the system:
+
+```mermaid
+sequenceDiagram
+ participant Caller
+ participant Twilio
+ participant GoServer as Go WebSocket Server
+ participant Gladia as Gladia API
+
+ GoServer->>Gladia: Initialize session (POST /v2/live)
+ Gladia-->>GoServer: Return WebSocket URL
+
+ Caller->>Twilio: Make phone call
+ Twilio->>GoServer: Connect to WebSocket (/media)
+
+ loop Audio Streaming
+ Twilio->>GoServer: Send audio chunks (base64 μ-law)
+ GoServer->>GoServer: Decode base64
+ GoServer->>Gladia: Forward raw μ-law bytes
+ Gladia->>Gladia: Process speech
+ Gladia-->>GoServer: Return partial transcripts
+ Gladia-->>GoServer: Return final transcripts
+ GoServer->>GoServer: Log/process transcripts
+ end
+
+ Caller->>Twilio: End call
+ Twilio->>GoServer: Close WebSocket
+ GoServer->>Gladia: Close WebSocket
+```
+
+---
+
+### 2 — Build the Go WebSocket proxy
+
+The proxy server handles three main tasks:
+
+1. Accept Twilio's base64-encoded μ-law frames.
+2. Base64-decode the payload (the only transformation needed).
+3. Pipe the raw bytes to Gladia and process returned transcripts.
+
+```go
+package main
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "log"
+ "net/http"
+ "os"
+ "sync"
+
+ "github.com/gorilla/websocket"
+ "github.com/joho/godotenv"
+)
+
+// TwilioMessage represents the structure of messages from Twilio
+type TwilioMessage struct {
+ Event string `json:"event"`
+ Media struct {
+ Payload string `json:"payload"`
+ } `json:"media"`
+}
+
+// GladiaMessage represents the structure of messages from Gladia
+type GladiaMessage struct {
+ Type string `json:"type"`
+ Data struct {
+ IsFinal bool `json:"is_final"`
+ Utterance struct {
+ Text string `json:"text"`
+ } `json:"utterance"`
+ } `json:"data"`
+}
+
+// processMessage decodes Twilio media payload and forwards raw μ-law bytes to Gladia.
+func processMessage(message []byte, gladiaConn *websocket.Conn) {
+ var msg TwilioMessage
+ if err := json.Unmarshal(message, &msg); err != nil {
+ log.Printf("Error parsing Twilio message: %v", err)
+ return
+ }
+ if msg.Event != "media" {
+ log.Printf("Ignoring non-media event: %s", msg.Event)
+ return // ignore non-media events
+ }
+ mulaw, err := base64.StdEncoding.DecodeString(msg.Media.Payload)
+ if err != nil {
+ log.Printf("Error decoding payload: %v", err)
+ return
+ }
+ if err := gladiaConn.WriteMessage(websocket.BinaryMessage, mulaw); err != nil {
+ log.Printf("Error sending to Gladia: %v", err)
+ }
+}
+
+// handleGladia processes incoming messages from Gladia and logs final transcripts.
+func handleGladia(message []byte) string {
+ var msg GladiaMessage
+ if err := json.Unmarshal(message, &msg); err != nil {
+ log.Printf("Error parsing Gladia message: %v", err)
+ return ""
+ }
+ if msg.Type == "transcript" && msg.Data.IsFinal {
+ transcript := msg.Data.Utterance.Text
+ log.Printf("📝 Transcript: %s", transcript)
+ return transcript
+ }
+ return ""
+}
+
+// handleWebSocket manages a WebSocket connection between Twilio and Gladia.
+func handleWebSocket(twilioConn *websocket.Conn) {
+ clientInfo := twilioConn.RemoteAddr().String()
+ log.Printf("🔌 Twilio WebSocket connected from %s", clientInfo)
+
+ defer twilioConn.Close()
+
+ // Connect to Gladia
+ dialer := websocket.DefaultDialer
+ gladiaConn, _, err := dialer.Dial(session.URL, nil)
+ if err != nil {
+ log.Printf("Failed to connect to Gladia: %v", err)
+ return
+ }
+ defer gladiaConn.Close()
+ log.Printf("Connected to Gladia session %s", session.ID)
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ // Twilio -> Gladia
+ go func() {
+ defer wg.Done()
+ for {
+ _, msg, err := twilioConn.ReadMessage()
+ if err != nil {
+ log.Printf("Error reading from Twilio: %v", err)
+ return
+ }
+ processMessage(msg, gladiaConn)
+ }
+ }()
+
+ // Gladia -> transcripts
+ go func() {
+ defer wg.Done()
+ for {
+ _, msg, err := gladiaConn.ReadMessage()
+ if err != nil {
+ log.Printf("Error reading from Gladia: %v", err)
+ return
+ }
+ handleGladia(msg)
+ }
+ }()
+
+ wg.Wait()
+}
+
+func main() {
+ // Configure logging
+ log.SetFlags(log.LstdFlags | log.Lshortfile)
+
+ // Load environment variables from .env (optional)
+ if err := godotenv.Load(); err != nil {
+ log.Println("No .env file found. Using environment variables.")
+ }
+
+ gladiaAPIKey = os.Getenv("GLADIA_API_KEY")
+ if gladiaAPIKey == "" {
+ log.Fatal("GLADIA_API_KEY environment variable is required")
+ }
+
+ port := os.Getenv("HTTP_PORT")
+ if port == "" {
+ port = "5000"
+ }
+
+ var err error
+ session, err = createSession()
+ if err != nil {
+ log.Fatalf("Failed to create initial Gladia session: %v", err)
+ }
+
+ // Set up HTTP routes and start server
+ // ...
+}
+```
+
+No audio processing libraries needed, no resampling, no CPU overhead, and with efficient goroutines for concurrent handling.
+
+---
+
+### 3 — Tell Twilio to stream audio
+
+Point your Twilio number (or a Voice Application) to a TwiML endpoint like:
+
+```xml
+
+
+
+
+
+
+
+ +14155551234
+
+```
+
+Let's examine each element in this TwiML configuration:
+
+- ``: The root element of any TwiML document. It contains all the TwiML instructions for handling the call.
+
+- ``: This element initiates Twilio's Media Streams feature, which allows streaming of audio in real-time while the call is in progress. It tells Twilio to begin capturing and streaming media before executing the rest of the call flow.
+
+- ``: A child element of `` that configures the media stream:
+ - `url` attribute: Specifies the WebSocket endpoint where Twilio will send the audio data.
+ - The URL must use secure WebSockets (`wss://`).
+ - The domain should be your public domain (e.g., an ngrok URL or a custom domain).
+ - The path (`/media`) must match the WebSocket route in your Go application.
+ - Each call will create a new WebSocket connection to this endpoint.
+
+- ``: After starting the media stream, this element connects the caller to another phone number. During this connection:
+ - The media streaming continues in the background.
+ - Audio from both sides of the conversation is sent to your WebSocket endpoint.
+ - You can replace this with other TwiML verbs like ``, ``, or `` depending on your use case.
+ - The number shown (`+14155551234`) is just an example - replace it with your desired destination.
+
+When a call triggers this TwiML, Twilio immediately opens a secure WebSocket connection to your server's `/media` endpoint and begins streaming audio as 20 ms μ-law frames. Each frame is base64-encoded and sent as a JSON message, which your server then decodes and forwards to Gladia.
+
+---
+
+### 4 — Expose & test
+
+```bash
+# Build the application
+go build -o twilio-gladia-server
+
+# Run the server (default port 5000)
+./twilio-gladia-server
+
+# Or specify a custom port
+HTTP_PORT=5001 ./twilio-gladia-server
+
+# Alternatively, run directly
+go run main.go
+
+# Tunnel it with ngrok
+ngrok http 5001
+
+# Or with a custom domain
+ngrok http --domain=your.domain.com 5001
+```
+
+Call your Twilio number and you should see live text scroll instantly:
+
+```
+🛰 Gladia session ID: 3f65…
+🚀 Starting server on 0.0.0.0:5000
+🔌 Twilio WebSocket connected from 42.422.42.4242:12345
+Connected to Gladia session 3f65…
+📝 Transcript: Hello and thank you for calling Acme support.
+📝 Transcript: Sure, I'd be happy to help with your order.
+```
+
+---
+
+### 5 — Next steps
+
+* **Add-ons** – enable diarization, sentiment, keywords, etc., by including the flags when you create the session.
+* **Dual-channel** – Twilio can stream separate channels; Gladia preserves them so you always know who's speaking.
+* **Post-call JSON** – store the session `id` and hit `GET /v2/live/:id` for the full, punctuated transcript when the call ends.
+* **Scale it** – Go's concurrency model with goroutines makes it easy to scale for high loads. Consider deploying with a load balancer for horizontal scaling.
+
+---
+
+### Wrap-up
+
+Because Gladia natively accepts Twilio's μ-law stream, **real-time call transcription is now literally "base64-decode and forward."** Fewer steps, lower CPU, and the same lightning-fast latency. With Go's excellent concurrency model, the solution is not only simple but highly performant and scalable. Drop this proxy into any Go stack and start surfacing live insights from every call. Happy building! 🎙️📝
+
+[1]: https://docs.gladia.io/api-reference/v2/live/init "Initiate a session - Gladia"
diff --git a/blogs/twilio-solaria-go/src/README.md b/blogs/twilio-solaria-go/src/README.md
new file mode 100644
index 0000000..29e94c1
--- /dev/null
+++ b/blogs/twilio-solaria-go/src/README.md
@@ -0,0 +1,90 @@
+# Twilio Call Transcription with Gladia (Go Version)
+
+This project implements real-time transcription of Twilio calls using Gladia's Speech-to-Text API and Go, which natively supports Twilio's μ-law audio format.
+
+## Prerequisites
+
+- **Gladia API key** - Sign up at [app.gladia.io](https://app.gladia.io/)
+- **Twilio account + voice-enabled number**
+- **Go 1.21+**
+- **Public URL** - Use ngrok or a cloud VM to expose your WebSocket endpoint
+
+## Setup
+
+1. **Install Go dependencies**:
+ ```bash
+ # install go if needed
+ sudo apt-get update && sudo apt-get -y install golang-go
+ # Download dependencies
+ go mod download
+ ```
+
+2. **Set up environment variables**:
+ Create a `.env` file in the same directory as `main.go` with:
+ ```
+ GLADIA_API_KEY=your_gladia_api_key_here
+ # Optional: HTTP_PORT=5001
+ ```
+
+3. **Configure Twilio**:
+ - Create a TwiML Bin or webhook in your Twilio account at [https://console.twilio.com/us1/develop/twiml-bins](https://console.twilio.com/us1/develop/twiml-bins)
+ - Use the contents of `twiml_example.xml` as your TwiML
+ - Replace `YOUR_PUBLIC_DOMAIN` with your actual public domain (the ngrok URL from step 2 in "Running the application" or a custom domain)
+ - Assign this TwiML Bin to your Twilio phone number at [https://console.twilio.com/us1/develop/phone-numbers/manage/incoming](https://console.twilio.com/us1/develop/phone-numbers/manage/incoming)
+
+## Technical Notes
+
+- The server uses standard Go HTTP server with gorilla/websocket for WebSocket support
+- The application uses goroutines for concurrent processing
+- Error handling follows Go idiomatic patterns
+
+## Running the application
+
+1. **Build and start the server**:
+ ```bash
+ # Build the application
+ go build -o twilio-gladia-server
+
+ # Run with default port (5000)
+ ./twilio-gladia-server
+
+ # Or specify a custom port
+ HTTP_PORT=5001 ./twilio-gladia-server
+
+ # Alternatively, run without building
+ go run main.go
+ ```
+
+2. **Expose your server publicly**:
+ ```bash
+ # Make sure the port matches HTTP_PORT from step 1
+ ngrok http $HTTP_PORT # you'll get a random ngrok URL
+
+ # If you used a custom port in step 1, use the same port here
+ ngrok http 5001
+
+ # For a custom domain
+ ngrok http --domain=your.domain.com 5001
+ ```
+
+3. **Update your TwiML**:
+ - Update the `url` in your TwiML to your ngrok URL (e.g., `wss://your.domain.com/media` or the random ngrok URL)
+
+4. **Test**:
+ - Call your Twilio number
+ - You should see transcripts appearing in your console
+
+## How it works
+
+1. The server connects to Gladia's real-time STT API
+2. When a call comes in, Twilio connects to your WebSocket endpoint
+3. Twilio streams 20ms μ-law frames, which are base64-decoded
+4. The raw μ-law bytes are forwarded to Gladia without any conversion
+5. Gladia returns real-time transcription results
+
+## Next steps
+
+- **Add-ons** – Enable sentiment, keywords, etc.
+- **Dual-channel** – Transcribe both sides of the conversation separately
+- **Post-call JSON** – Get the full transcript when the call ends
+- **Scale it** – Deploy to cloud services with load balancers for high availability
\ No newline at end of file
diff --git a/blogs/twilio-solaria-go/src/env_setup.txt b/blogs/twilio-solaria-go/src/env_setup.txt
new file mode 100644
index 0000000..8056f9c
--- /dev/null
+++ b/blogs/twilio-solaria-go/src/env_setup.txt
@@ -0,0 +1,23 @@
+Before running the Go application, create a .env file in the same directory as main.go with the following contents:
+
+GLADIA_API_KEY=your_gladia_api_key_here
+# Optional: Set a custom port (default is 5000)
+# HTTP_PORT=5001
+
+You need to replace 'your_gladia_api_key_here' with your actual Gladia API key from https://app.gladia.io/
+
+Environment variables can also be set directly in your terminal:
+
+```bash
+# Linux/macOS
+export GLADIA_API_KEY=your_gladia_api_key_here
+export HTTP_PORT=5001
+
+# Windows Command Prompt
+set GLADIA_API_KEY=your_gladia_api_key_here
+set HTTP_PORT=5001
+
+# Windows PowerShell
+$env:GLADIA_API_KEY="your_gladia_api_key_here"
+$env:HTTP_PORT="5001"
+```
\ No newline at end of file
diff --git a/blogs/twilio-solaria-go/src/go.mod b/blogs/twilio-solaria-go/src/go.mod
new file mode 100644
index 0000000..373daff
--- /dev/null
+++ b/blogs/twilio-solaria-go/src/go.mod
@@ -0,0 +1,10 @@
+module twilio-gladia-go
+
+go 1.21
+
+require (
+ github.com/gorilla/websocket v1.5.1
+ github.com/joho/godotenv v1.5.1
+)
+
+require golang.org/x/net v0.17.0 // indirect
diff --git a/blogs/twilio-solaria-go/src/go.sum b/blogs/twilio-solaria-go/src/go.sum
new file mode 100644
index 0000000..737d760
--- /dev/null
+++ b/blogs/twilio-solaria-go/src/go.sum
@@ -0,0 +1,6 @@
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
diff --git a/blogs/twilio-solaria-go/src/main.go b/blogs/twilio-solaria-go/src/main.go
new file mode 100644
index 0000000..f8d99ff
--- /dev/null
+++ b/blogs/twilio-solaria-go/src/main.go
@@ -0,0 +1,250 @@
+package main
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+ "github.com/joho/godotenv"
+)
+
+const (
+ gladiaInitURL = "https://api.gladia.io/v2/live"
+)
+
+// GladiaSession stores session information
+type GladiaSession struct {
+ ID string
+ URL string
+}
+
+var (
+ gladiaAPIKey string
+ session GladiaSession
+)
+
+// createSession initializes a Gladia real-time transcription session and returns the WebSocket URL.
+func createSession() (GladiaSession, error) {
+ payload := map[string]interface{}{ // μ-law, 8-bit, 8 kHz, mono
+ "encoding": "wav/ulaw",
+ "bit_depth": 8,
+ "sample_rate": 8000,
+ "channels": 1,
+ }
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return GladiaSession{}, fmt.Errorf("failed to marshal payload: %w", err)
+ }
+ req, err := http.NewRequest("POST", gladiaInitURL, bytes.NewReader(body))
+ if err != nil {
+ return GladiaSession{}, fmt.Errorf("failed to create request: %w", err)
+ }
+ req.Header.Set("X-Gladia-Key", gladiaAPIKey)
+ req.Header.Set("Content-Type", "application/json")
+ client := &http.Client{Timeout: 10 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return GladiaSession{}, fmt.Errorf("session init request failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ return GladiaSession{}, fmt.Errorf("bad status code: %d - %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ var data GladiaSession
+ if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+ return GladiaSession{}, fmt.Errorf("failed to decode response: %w", err)
+ }
+ log.Printf("🛰 Gladia session ID: %s", data.ID)
+ return data, nil
+}
+
+// healthCheck returns a simple health status.
+func healthCheck(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Write([]byte(`{"status":"ok","service":"twilio-gladia-transcription"}`))
+}
+
+// TwilioMessage represents the structure of messages from Twilio
+type TwilioMessage struct {
+ Event string `json:"event"`
+ Media struct {
+ Payload string `json:"payload"`
+ } `json:"media"`
+}
+
+// GladiaMessage represents the structure of messages from Gladia
+type GladiaMessage struct {
+ Type string `json:"type"`
+ Data struct {
+ IsFinal bool `json:"is_final"`
+ Utterance struct {
+ Text string `json:"text"`
+ } `json:"utterance"`
+ } `json:"data"`
+}
+
+// processMessage decodes Twilio media payload and forwards raw μ-law bytes to Gladia.
+func processMessage(message []byte, gladiaConn *websocket.Conn) {
+ var msg TwilioMessage
+ if err := json.Unmarshal(message, &msg); err != nil {
+ log.Printf("Error parsing Twilio message: %v", err)
+ return
+ }
+ if msg.Event != "media" {
+ log.Printf("Ignoring non-media event: %s", msg.Event)
+ return // ignore non-media events
+ }
+ mulaw, err := base64.StdEncoding.DecodeString(msg.Media.Payload)
+ if err != nil {
+ log.Printf("Error decoding payload: %v", err)
+ return
+ }
+ if err := gladiaConn.WriteMessage(websocket.BinaryMessage, mulaw); err != nil {
+ log.Printf("Error sending to Gladia: %v", err)
+ }
+}
+
+// handleGladia processes incoming messages from Gladia and logs final transcripts.
+func handleGladia(message []byte) string {
+ var msg GladiaMessage
+ if err := json.Unmarshal(message, &msg); err != nil {
+ log.Printf("Error parsing Gladia message: %v", err)
+ return ""
+ }
+ if msg.Type == "transcript" && msg.Data.IsFinal {
+ transcript := msg.Data.Utterance.Text
+ log.Printf("📝 Transcript: %s", transcript)
+ return transcript
+ }
+ return ""
+}
+
+// handleWebSocket manages a WebSocket connection between Twilio and Gladia.
+func handleWebSocket(twilioConn *websocket.Conn) {
+ clientInfo := twilioConn.RemoteAddr().String()
+ log.Printf("🔌 Twilio WebSocket connected from %s", clientInfo)
+
+ defer twilioConn.Close()
+
+ // Connect to Gladia
+ dialer := websocket.DefaultDialer
+ gladiaConn, _, err := dialer.Dial(session.URL, nil)
+ if err != nil {
+ log.Printf("Failed to connect to Gladia: %v", err)
+ return
+ }
+ defer gladiaConn.Close()
+ log.Printf("Connected to Gladia session %s", session.ID)
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ // Twilio -> Gladia
+ go func() {
+ defer wg.Done()
+ for {
+ _, msg, err := twilioConn.ReadMessage()
+ if err != nil {
+ log.Printf("Error reading from Twilio: %v", err)
+ return
+ }
+ processMessage(msg, gladiaConn)
+ }
+ }()
+
+ // Gladia -> transcripts
+ go func() {
+ defer wg.Done()
+ for {
+ _, msg, err := gladiaConn.ReadMessage()
+ if err != nil {
+ log.Printf("Error reading from Gladia: %v", err)
+ return
+ }
+ handleGladia(msg)
+ }
+ }()
+
+ wg.Wait()
+}
+
+func main() {
+ // Configure logging
+ log.SetFlags(log.LstdFlags | log.Lshortfile)
+
+ // Load environment variables from .env (optional)
+ if err := godotenv.Load(); err != nil {
+ log.Println("No .env file found. Using environment variables.")
+ }
+
+ gladiaAPIKey = os.Getenv("GLADIA_API_KEY")
+ if gladiaAPIKey == "" {
+ log.Fatal("GLADIA_API_KEY environment variable is required")
+ }
+
+ port := os.Getenv("HTTP_PORT")
+ if port == "" {
+ port = "5000"
+ }
+
+ var err error
+ session, err = createSession()
+ if err != nil {
+ log.Fatalf("Failed to create initial Gladia session: %v", err)
+ }
+
+ // Set up HTTP routes
+ http.HandleFunc("/health", healthCheck)
+
+ // Configure WebSocket upgrader
+ upgrader := websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool { return true },
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ }
+
+ // Media endpoint
+ http.HandleFunc("/media", func(w http.ResponseWriter, r *http.Request) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Printf("WebSocket upgrade failed: %v", err)
+ return
+ }
+ handleWebSocket(conn)
+ })
+
+ // Catch-all WebSocket handler
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if websocket.IsWebSocketUpgrade(r) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Printf("WebSocket upgrade failed: %v", err)
+ return
+ }
+ log.Printf("🔌 Catch-all WebSocket connected to %s", r.URL.Path)
+ handleWebSocket(conn)
+ } else {
+ // For regular HTTP requests to root, return a simple info page
+ w.Header().Set("Content-Type", "text/plain")
+ w.Write([]byte("Twilio-Gladia Transcription Server\n\nAvailable endpoints:\n- /media (WebSocket): Connect Twilio Media Streams\n- /health (HTTP): Health check endpoint"))
+ }
+ })
+
+ // Start the server
+ addr := fmt.Sprintf(":%s", port)
+ log.Printf("🚀 Starting server on 0.0.0.0:%s", port)
+ if err := http.ListenAndServe(addr, nil); err != nil {
+ log.Fatalf("Server failed: %v", err)
+ }
+}
\ No newline at end of file
diff --git a/blogs/twilio-solaria-go/src/twiml_example.xml b/blogs/twilio-solaria-go/src/twiml_example.xml
new file mode 100644
index 0000000..a8f9196
--- /dev/null
+++ b/blogs/twilio-solaria-go/src/twiml_example.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+ +14155551234
+
\ No newline at end of file
diff --git a/blogs/twilio-solaria-javascript/.gitignore b/blogs/twilio-solaria-javascript/.gitignore
new file mode 100644
index 0000000..d41e285
--- /dev/null
+++ b/blogs/twilio-solaria-javascript/.gitignore
@@ -0,0 +1,22 @@
+# Dependency directories
+node_modules/
+npm-debug.log
+yarn-debug.log
+yarn-error.log
+
+# Environment variables
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Logs
+logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
diff --git a/blogs/twilio-solaria-javascript/blog.md b/blogs/twilio-solaria-javascript/blog.md
new file mode 100644
index 0000000..7a74a1e
--- /dev/null
+++ b/blogs/twilio-solaria-javascript/blog.md
@@ -0,0 +1,291 @@
+## How to Transcribe Twilio Calls in Real Time with JavaScript & Gladia (μ-law Native)
+
+Twilio's Voice **Media Streams** deliver 8 kHz, 8-bit μ-law audio. Gladia's real-time Speech-to-Text (STT) API now ingests that exact format out-of-the-box, so you can skip every resample or decode step and still keep sub-300 ms latency.
+
+Gladia's `/v2/live` endpoint lets you specify **`encoding: "wav/ulaw"`** with **`bit_depth: 8`**, matching Twilio 1-for-1 ([Gladia][1]).
+
+---
+
+### Prerequisites
+
+| What you need | Why |
+| ----------------------------------------- | -------------------------------------------------------------------- |
+| **Gladia API key** | Sign up & copy from the dashboard. |
+| **Twilio account + voice-enabled number** | To receive / place calls. |
+| **Node.js 14+** | We'll use `ws` for WebSocket handling. |
+| **Public URL** | Expose a WebSocket endpoint with ngrok or a cloud VM. |
+| **8 kHz, 8-bit μ-law audio** | Exactly what Twilio streams – and what Gladia now consumes natively. |
+
+---
+
+### 1 — Initiate a Gladia live session
+
+```javascript
+const fetch = require('node-fetch');
+
+const GLADIA_INIT_URL = 'https://api.gladia.io/v2/live';
+
+// Create a Gladia session for real-time transcription
+async function createSession() {
+ // μ-law, 8-bit, 8 kHz, mono
+ const payload = {
+ encoding: 'wav/ulaw',
+ bit_depth: 8,
+ sample_rate: 8000,
+ channels: 1
+ };
+
+ try {
+ const response = await fetch(GLADIA_INIT_URL, {
+ method: 'POST',
+ headers: {
+ 'X-Gladia-Key': gladiaAPIKey,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(payload),
+ timeout: 10000
+ });
+
+ if (!response.ok) {
+ const errorBody = await response.text();
+ throw new Error(`Bad status code: ${response.status} - ${errorBody}`);
+ }
+
+ const data = await response.json();
+ console.log(`🛰 Gladia session ID: ${data.id}`);
+ return data;
+ } catch (error) {
+ throw new Error(`Failed to create session: ${error.message}`);
+ }
+}
+```
+
+> **Why no resample / decode?** Gladia parses raw μ-law frames directly, so we just forward the bytes Twilio gives us.
+
+---
+
+### System Architecture Flow
+
+Here's how data flows through the system:
+
+```mermaid
+sequenceDiagram
+ participant Caller
+ participant Twilio
+ participant JSServer as JavaScript WebSocket Server
+ participant Gladia as Gladia API
+
+ JSServer->>Gladia: Initialize session (POST /v2/live)
+ Gladia-->>JSServer: Return WebSocket URL
+
+ Caller->>Twilio: Make phone call
+ Twilio->>JSServer: Connect to WebSocket (/media)
+
+ loop Audio Streaming
+ Twilio->>JSServer: Send audio chunks (base64 μ-law)
+ JSServer->>JSServer: Decode base64
+ JSServer->>Gladia: Forward raw μ-law bytes
+ Gladia->>Gladia: Process speech
+ Gladia-->>JSServer: Return partial transcripts
+ Gladia-->>JSServer: Return final transcripts
+ JSServer->>JSServer: Log/process transcripts
+ end
+
+ Caller->>Twilio: End call
+ Twilio->>JSServer: Close WebSocket
+ JSServer->>Gladia: Close WebSocket
+```
+
+---
+
+### 2 — Build the JavaScript WebSocket proxy
+
+The proxy server handles three main tasks:
+
+1. Accept Twilio's base64-encoded μ-law frames.
+2. Base64-decode the payload (the only transformation needed).
+3. Pipe the raw bytes to Gladia and process returned transcripts.
+
+```javascript
+const http = require('http');
+const WebSocket = require('ws');
+const dotenv = require('dotenv');
+
+// Load environment variables
+dotenv.config();
+
+// Process Twilio messages, decode μ-law audio and forward to Gladia
+function processMessage(message, gladiaConn) {
+ try {
+ const msg = JSON.parse(message);
+ if (msg.event !== 'media') {
+ console.log(`Ignoring non-media event: ${msg.event}`);
+ return; // Ignore non-media events
+ }
+
+ // Decode base64 payload to get raw μ-law bytes
+ const mulaw = Buffer.from(msg.media.payload, 'base64');
+
+ gladiaConn.send(mulaw, { binary: true }, (err) => {
+ if (err) {
+ console.log(`Error sending to Gladia: ${err}`);
+ }
+ });
+ } catch (error) {
+ console.log(`Error parsing Twilio message: ${error}`);
+ }
+}
+
+// Handle messages from Gladia and extract final transcripts
+function handleGladia(message) {
+ try {
+ const msg = JSON.parse(message);
+ if (msg.type === 'transcript' && msg.data.is_final) {
+ const transcript = msg.data.utterance.text;
+ console.log(`📝 Transcript: ${transcript}`);
+ return transcript;
+ }
+ return '';
+ } catch (error) {
+ console.log(`Error parsing Gladia message: ${error}`);
+ return '';
+ }
+}
+
+// Handle WebSocket connections from Twilio
+function handleWebSocket(twilioConn) {
+ const clientInfo = twilioConn._socket.remoteAddress;
+ console.log(`🔌 Twilio WebSocket connected from ${clientInfo}`);
+
+ // Connect to Gladia
+ const gladiaConn = new WebSocket(session.url);
+
+ gladiaConn.on('open', () => {
+ console.log(`Connected to Gladia session ${session.id}`);
+
+ // Handle incoming messages from Twilio
+ twilioConn.on('message', (msg) => {
+ processMessage(msg, gladiaConn);
+ });
+
+ // Handle errors and connection close events
+ twilioConn.on('error', (error) => {
+ console.log(`Error from Twilio: ${error}`);
+ });
+
+ twilioConn.on('close', () => {
+ console.log('Twilio connection closed');
+ gladiaConn.close();
+ });
+ });
+
+ // Handle messages from Gladia
+ gladiaConn.on('message', (msg) => {
+ handleGladia(msg.toString());
+ });
+
+ // Handle Gladia connection events
+ gladiaConn.on('error', (error) => {
+ console.log(`Error from Gladia: ${error}`);
+ });
+
+ gladiaConn.on('close', () => {
+ console.log('Gladia connection closed');
+ twilioConn.close();
+ });
+}
+```
+
+No audio processing libraries needed, no resampling, no CPU overhead, and with JavaScript's event-driven architecture for efficient handling.
+
+---
+
+### 3 — Tell Twilio to stream audio
+
+Point your Twilio number (or a Voice Application) to a TwiML endpoint like:
+
+```xml
+
+
+
+
+
+
+
+ +14155551234
+
+```
+
+Let's examine each element in this TwiML configuration:
+
+- ``: The root element of any TwiML document. It contains all the TwiML instructions for handling the call.
+
+- ``: This element initiates Twilio's Media Streams feature, which allows streaming of audio in real-time while the call is in progress. It tells Twilio to begin capturing and streaming media before executing the rest of the call flow.
+
+- ``: A child element of `` that configures the media stream:
+ - `url` attribute: Specifies the WebSocket endpoint where Twilio will send the audio data.
+ - The URL must use secure WebSockets (`wss://`).
+ - The domain should be your public domain (e.g., an ngrok URL or a custom domain).
+ - The path (`/media`) must match the WebSocket route in your JavaScript application.
+ - Each call will create a new WebSocket connection to this endpoint.
+
+- ``: After starting the media stream, this element connects the caller to another phone number. During this connection:
+ - The media streaming continues in the background.
+ - Audio from both sides of the conversation is sent to your WebSocket endpoint.
+ - You can replace this with other TwiML verbs like ``, ``, or `` depending on your use case.
+ - The number shown (`+14155551234`) is just an example - replace it with your desired destination.
+
+When a call triggers this TwiML, Twilio immediately opens a secure WebSocket connection to your server's `/media` endpoint and begins streaming audio as 20 ms μ-law frames. Each frame is base64-encoded and sent as a JSON message, which your server then decodes and forwards to Gladia.
+
+---
+
+### 4 — Expose & test
+
+```bash
+# Install dependencies
+npm install
+
+# Run the server (default port 5001)
+npm start
+
+# Or with live reload during development
+npm run dev
+
+# Or specify a custom port
+HTTP_PORT=5001 npm start
+
+# Tunnel it with ngrok
+ngrok http 5001
+
+# Or with a custom domain
+ngrok http --domain=your.domain.com 5001
+```
+
+Call your Twilio number and you should see live text scroll instantly:
+
+```
+🛰 Gladia session ID: 3f65…
+🚀 Starting server on 0.0.0.0:5001
+🔌 WebSocket connected to /media
+🔌 Twilio WebSocket connected from ::ffff:127.0.0.1
+Connected to Gladia session 3f65…
+📝 Transcript: Hello and thank you for calling Acme support.
+📝 Transcript: Sure, I'd be happy to help with your order.
+```
+
+---
+
+### 5 — Next steps
+
+* **Add-ons** – enable diarization, sentiment, keywords, etc., by including the flags when you create the session.
+* **Dual-channel** – Twilio can stream separate channels; Gladia preserves them so you always know who's speaking.
+* **Post-call JSON** – store the session `id` and hit `GET /v2/live/:id` for the full, punctuated transcript when the call ends.
+* **Scale it** – Node.js's event-driven, non-blocking I/O model makes it perfect for scaling WebSocket applications. Consider deploying to a serverless platform or container service with auto-scaling capabilities.
+
+---
+
+### Wrap-up
+
+Because Gladia natively accepts Twilio's μ-law stream, **real-time call transcription is now literally "base64-decode and forward."** Fewer steps, lower CPU, and the same lightning-fast latency. With JavaScript's event-driven architecture, the solution is not only simple but highly performant. Drop this proxy into any Node.js application and start surfacing live insights from every call. Happy building! 🎙️📝
+
+[1]: https://docs.gladia.io/api-reference/v2/live/init "Initiate a session - Gladia"
\ No newline at end of file
diff --git a/blogs/twilio-solaria-javascript/package-lock.json b/blogs/twilio-solaria-javascript/package-lock.json
new file mode 100644
index 0000000..4f9eb55
--- /dev/null
+++ b/blogs/twilio-solaria-javascript/package-lock.json
@@ -0,0 +1,471 @@
+{
+ "name": "twilio-solaria-javascript",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "twilio-solaria-javascript",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "dotenv": "^16.0.3",
+ "node-fetch": "^2.6.7",
+ "ws": "^8.12.0"
+ },
+ "devDependencies": {
+ "nodemon": "^2.0.20"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "2.0.22",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
+ "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^3.2.7",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^5.7.1",
+ "simple-update-notifier": "^1.0.7",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
+ "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "~7.0.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/simple-update-notifier/node_modules/semver": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
+ "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
+ "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/blogs/twilio-solaria-javascript/package.json b/blogs/twilio-solaria-javascript/package.json
new file mode 100644
index 0000000..01c5b86
--- /dev/null
+++ b/blogs/twilio-solaria-javascript/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "twilio-solaria-javascript",
+ "version": "1.0.0",
+ "description": "Real-time transcription of Twilio calls using Gladia's Speech-to-Text API",
+ "main": "src/main.js",
+ "scripts": {
+ "start": "node src/main.js",
+ "dev": "nodemon src/main.js"
+ },
+ "dependencies": {
+ "dotenv": "^16.0.3",
+ "node-fetch": "^2.6.7",
+ "ws": "^8.12.0"
+ },
+ "devDependencies": {
+ "nodemon": "^2.0.20"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "keywords": [
+ "twilio",
+ "gladia",
+ "transcription",
+ "websocket",
+ "speech-to-text"
+ ],
+ "author": "",
+ "license": "MIT"
+}
\ No newline at end of file
diff --git a/blogs/twilio-solaria-javascript/src/README.md b/blogs/twilio-solaria-javascript/src/README.md
new file mode 100644
index 0000000..5918677
--- /dev/null
+++ b/blogs/twilio-solaria-javascript/src/README.md
@@ -0,0 +1,82 @@
+# Twilio Call Transcription with Gladia (JavaScript Version)
+
+This project implements real-time transcription of Twilio calls using Gladia's Speech-to-Text API and JavaScript, which handles Twilio's μ-law audio format natively.
+
+## Prerequisites
+
+- **Gladia API key** - Sign up at [app.gladia.io](https://app.gladia.io/)
+- **Twilio account + voice-enabled number**
+- **Node.js 14+**
+- **Public URL** - Use ngrok or a cloud VM to expose your WebSocket endpoint
+
+## Setup
+
+1. **Install Node.js dependencies**:
+ ```bash
+ # Install dependencies
+ npm install
+ ```
+
+2. **Set up environment variables**:
+ Create a `.env` file in the root directory with:
+ ```
+ GLADIA_API_KEY=your_gladia_api_key_here
+ # Optional: HTTP_PORT=5001
+ ```
+
+3. **Configure Twilio**:
+ - Create a TwiML Bin or webhook in your Twilio account at [https://console.twilio.com/us1/develop/twiml-bins](https://console.twilio.com/us1/develop/twiml-bins)
+ - Use the contents of `twiml_example.xml` as your TwiML
+ - Replace `YOUR_PUBLIC_DOMAIN` with your actual public domain (the ngrok URL from step 2 in "Running the application" or a custom domain)
+ - Assign this TwiML Bin to your Twilio phone number at [https://console.twilio.com/us1/develop/phone-numbers/manage/incoming](https://console.twilio.com/us1/develop/phone-numbers/manage/incoming)
+
+## Technical Notes
+
+- The server uses Node.js HTTP server with the ws library for WebSocket support
+- The application uses asynchronous callbacks for event handling
+- Error handling follows JavaScript idiomatic patterns with try/catch
+
+## Running the application
+
+1. **Start the server**:
+ ```bash
+ # Run with default port (5001)
+ npm start
+
+ # Or with live reload during development
+ npm run dev
+
+ # Or specify a custom port
+ HTTP_PORT=5001 npm start
+ ```
+
+2. **Expose your server publicly**:
+ ```bash
+ # Make sure the port matches HTTP_PORT from step 1
+ ngrok http 5001 # you'll get a random ngrok URL
+
+ # For a custom domain
+ ngrok http --domain=your.domain.com 5001
+ ```
+
+3. **Update your TwiML**:
+ - Update the `url` in your TwiML to your ngrok URL (e.g., `wss://your.domain.com/media` or the random ngrok URL)
+
+4. **Test**:
+ - Call your Twilio number
+ - You should see transcripts appearing in your console
+
+## How it works
+
+1. The server connects to Gladia's real-time STT API
+2. When a call comes in, Twilio connects to your WebSocket endpoint
+3. Twilio streams 20ms μ-law frames, which are base64-decoded
+4. The raw μ-law bytes are forwarded to Gladia without any conversion
+5. Gladia returns real-time transcription results
+
+## Next steps
+
+- **Add-ons** – Enable sentiment, keywords, etc.
+- **Dual-channel** – Transcribe both sides of the conversation separately
+- **Post-call JSON** – Get the full transcript when the call ends
+- **Scale it** – Deploy to cloud services for high availability
\ No newline at end of file
diff --git a/blogs/twilio-solaria-javascript/src/env_setup.txt b/blogs/twilio-solaria-javascript/src/env_setup.txt
new file mode 100644
index 0000000..158d47f
--- /dev/null
+++ b/blogs/twilio-solaria-javascript/src/env_setup.txt
@@ -0,0 +1,23 @@
+Before running the JavaScript application, create a .env file in the root directory with the following contents:
+
+GLADIA_API_KEY=your_gladia_api_key_here
+# Optional: Set a custom port (default is 5001)
+# HTTP_PORT=5001
+
+You need to replace 'your_gladia_api_key_here' with your actual Gladia API key from https://app.gladia.io/
+
+Environment variables can also be set directly in your terminal:
+
+```bash
+# Linux/macOS
+export GLADIA_API_KEY=your_gladia_api_key_here
+export HTTP_PORT=5001
+
+# Windows Command Prompt
+set GLADIA_API_KEY=your_gladia_api_key_here
+set HTTP_PORT=5001
+
+# Windows PowerShell
+$env:GLADIA_API_KEY="your_gladia_api_key_here"
+$env:HTTP_PORT="5001"
+```
\ No newline at end of file
diff --git a/blogs/twilio-solaria-javascript/src/main.js b/blogs/twilio-solaria-javascript/src/main.js
new file mode 100644
index 0000000..bc66bde
--- /dev/null
+++ b/blogs/twilio-solaria-javascript/src/main.js
@@ -0,0 +1,192 @@
+const http = require('http');
+const WebSocket = require('ws');
+const dotenv = require('dotenv');
+const fetch = require('node-fetch');
+const { fileURLToPath } = require('url');
+const path = require('path');
+const fs = require('fs');
+
+// Constants
+const GLADIA_INIT_URL = 'https://api.gladia.io/v2/live';
+
+// Load environment variables
+dotenv.config();
+
+// Global variables
+let gladiaAPIKey = process.env.GLADIA_API_KEY;
+let session = null;
+
+// Create a Gladia session for real-time transcription
+async function createSession() {
+ // μ-law, 8-bit, 8 kHz, mono
+ const payload = {
+ encoding: 'wav/ulaw',
+ bit_depth: 8,
+ sample_rate: 8000,
+ channels: 1
+ };
+
+ try {
+ const response = await fetch(GLADIA_INIT_URL, {
+ method: 'POST',
+ headers: {
+ 'X-Gladia-Key': gladiaAPIKey,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(payload),
+ timeout: 10000
+ });
+
+ if (!response.ok) {
+ const errorBody = await response.text();
+ throw new Error(`Bad status code: ${response.status} - ${errorBody}`);
+ }
+
+ const data = await response.json();
+ console.log(`🛰 Gladia session ID: ${data.id}`);
+ return data;
+ } catch (error) {
+ throw new Error(`Failed to create session: ${error.message}`);
+ }
+}
+
+// Process Twilio messages, decode μ-law audio and forward to Gladia
+function processMessage(message, gladiaConn) {
+ try {
+ const msg = JSON.parse(message);
+ if (msg.event !== 'media') {
+ console.log(`Ignoring non-media event: ${msg.event}`);
+ return; // Ignore non-media events
+ }
+
+ // Decode base64 payload to get raw μ-law bytes
+ const mulaw = Buffer.from(msg.media.payload, 'base64');
+
+ gladiaConn.send(mulaw, { binary: true }, (err) => {
+ if (err) {
+ console.log(`Error sending to Gladia: ${err}`);
+ }
+ });
+ } catch (error) {
+ console.log(`Error parsing Twilio message: ${error}`);
+ }
+}
+
+// Handle messages from Gladia and extract final transcripts
+function handleGladia(message) {
+ try {
+ const msg = JSON.parse(message);
+ if (msg.type === 'transcript' && msg.data.is_final) {
+ const transcript = msg.data.utterance.text;
+ console.log(`📝 Transcript: ${transcript}`);
+ return transcript;
+ }
+ return '';
+ } catch (error) {
+ console.log(`Error parsing Gladia message: ${error}`);
+ return '';
+ }
+}
+
+// Handle WebSocket connections from Twilio
+function handleWebSocket(twilioConn) {
+ const clientInfo = twilioConn._socket.remoteAddress;
+ console.log(`🔌 Twilio WebSocket connected from ${clientInfo}`);
+
+ // Connect to Gladia
+ const gladiaConn = new WebSocket(session.url);
+
+ gladiaConn.on('open', () => {
+ console.log(`Connected to Gladia session ${session.id}`);
+
+ // Handle incoming messages from Twilio
+ twilioConn.on('message', (msg) => {
+ processMessage(msg, gladiaConn);
+ });
+
+ // Handle errors from Twilio connection
+ twilioConn.on('error', (error) => {
+ console.log(`Error from Twilio: ${error}`);
+ });
+
+ // Handle Twilio connection close
+ twilioConn.on('close', () => {
+ console.log('Twilio connection closed');
+ gladiaConn.close();
+ });
+ });
+
+ // Handle messages from Gladia
+ gladiaConn.on('message', (msg) => {
+ handleGladia(msg.toString());
+ });
+
+ // Handle errors from Gladia connection
+ gladiaConn.on('error', (error) => {
+ console.log(`Error from Gladia: ${error}`);
+ });
+
+ // Handle Gladia connection close
+ gladiaConn.on('close', () => {
+ console.log('Gladia connection closed');
+ twilioConn.close();
+ });
+}
+
+// Initialize and start the server
+async function main() {
+ // Check for API key
+ if (!gladiaAPIKey) {
+ console.error('GLADIA_API_KEY environment variable is required');
+ process.exit(1);
+ }
+
+ // Get port from environment or use default
+ const port = process.env.HTTP_PORT || 5001;
+
+ try {
+ // Create initial Gladia session
+ session = await createSession();
+
+ // Create HTTP server
+ const server = http.createServer((req, res) => {
+ // Handle health check endpoint
+ if (req.url === '/health') {
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({
+ status: 'ok',
+ service: 'twilio-gladia-transcription'
+ }));
+ return;
+ }
+
+ // Default message for HTTP requests
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
+ res.end('Twilio-Gladia Transcription Server\n\nAvailable endpoints:\n- /media (WebSocket): Connect Twilio Media Streams\n- /health (HTTP): Health check endpoint');
+ });
+
+ // Create WebSocket server
+ const wss = new WebSocket.Server({ server });
+
+ // Handle WebSocket connections
+ wss.on('connection', (ws, req) => {
+ console.log(`🔌 WebSocket connected to ${req.url}`);
+ handleWebSocket(ws);
+ });
+
+ // Start the server
+ server.listen(port, () => {
+ console.log(`🚀 Starting server on 0.0.0.0:${port}`);
+ });
+
+ } catch (error) {
+ console.error(`Server initialization failed: ${error}`);
+ process.exit(1);
+ }
+}
+
+// Start the application
+main().catch(err => {
+ console.error(`Unhandled error: ${err}`);
+ process.exit(1);
+});
\ No newline at end of file
diff --git a/blogs/twilio-solaria-javascript/src/twiml_example.xml b/blogs/twilio-solaria-javascript/src/twiml_example.xml
new file mode 100644
index 0000000..23d6d67
--- /dev/null
+++ b/blogs/twilio-solaria-javascript/src/twiml_example.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+ +14155551234
+
\ No newline at end of file
diff --git a/blogs/twilio-solaria-python-fastapi/.gitignore b/blogs/twilio-solaria-python-fastapi/.gitignore
new file mode 100644
index 0000000..85c55eb
--- /dev/null
+++ b/blogs/twilio-solaria-python-fastapi/.gitignore
@@ -0,0 +1,2 @@
+.env
+.venv
diff --git a/blogs/twilio-solaria-python-fastapi/blog.md b/blogs/twilio-solaria-python-fastapi/blog.md
new file mode 100644
index 0000000..12473db
--- /dev/null
+++ b/blogs/twilio-solaria-python-fastapi/blog.md
@@ -0,0 +1,308 @@
+## How to Transcribe Twilio Calls in Real Time with FastAPI and Python & Gladia (μ-law Native)
+
+Twilio's Voice **Media Streams** deliver 8 kHz, 8-bit μ-law audio. Gladia's real-time Speech-to-Text (STT) API now ingests that exact format out-of-the-box, so you can skip every resample or decode step and still keep sub-300 ms latency.
+
+Gladia's `/v2/live` endpoint lets you specify **`encoding: "wav/ulaw"`** with **`bit_depth: 8`**, matching Twilio 1-for-1 ([Gladia][1]).
+
+---
+
+### Prerequisites
+
+| What you need | Why |
+| ----------------------------------------- | -------------------------------------------------------------------- |
+| **Gladia API key** | Sign up & copy from the dashboard. |
+| **Twilio account + voice-enabled number** | To receive / place calls. |
+| **Python 3.12+** | We'll use `fastapi`, `uvicorn`, `websockets`, and `requests`. |
+| **Public URL** | Expose a WebSocket endpoint with ngrok or a cloud VM. |
+| **8 kHz, 8-bit μ-law audio** | Exactly what Twilio streams – and what Gladia now consumes natively. |
+
+---
+
+### 1 — Initiate a Gladia live session
+
+```python
+import os
+import requests
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/ulaw", # μ-law!
+ "bit_depth": 8, # 8-bit μ-law
+ "sample_rate": 8000, # matches Twilio
+ "channels": 1,
+ }
+
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ print("🛰 Gladia session ID:", data["id"])
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+```
+
+> **Why no resample / decode?** Gladia parses raw μ-law frames directly, so we just forward the bytes Twilio gives us.
+
+---
+
+### System Architecture Flow
+
+Here's how data flows through the system:
+
+```mermaid
+sequenceDiagram
+ participant Caller
+ participant Twilio
+ participant FastAPI as FastAPI WebSocket Server
+ participant Gladia as Gladia API
+
+ FastAPI->>Gladia: Initialize session (POST /v2/live)
+ Gladia-->>FastAPI: Return WebSocket URL
+
+ Caller->>Twilio: Make phone call
+ Twilio->>FastAPI: Connect to WebSocket (/media)
+
+ loop Audio Streaming
+ Twilio->>FastAPI: Send audio chunks (base64 μ-law)
+ FastAPI->>FastAPI: Decode base64
+ FastAPI->>Gladia: Forward raw μ-law bytes
+ Gladia->>Gladia: Process speech
+ Gladia-->>FastAPI: Return partial transcripts
+ Gladia-->>FastAPI: Return final transcripts
+ FastAPI->>FastAPI: Log/process transcripts
+ end
+
+ Caller->>Twilio: End call
+ Twilio->>FastAPI: Close WebSocket
+ FastAPI->>Gladia: Close WebSocket
+```
+
+---
+
+### 2 — Build the Python WebSocket proxy
+
+The proxy now does **three** things:
+
+1. Accept Twilio's base64-encoded μ-law frames.
+2. Base64-decode (that's the *only* transformation).
+3. Pipe the raw bytes straight to Gladia and print transcripts as they come back.
+
+```python
+# server.py
+import os
+import base64
+import json
+import logging
+import asyncio
+import websockets
+import requests
+from fastapi import FastAPI, WebSocket, Request
+from fastapi.responses import JSONResponse
+from dotenv import load_dotenv
+import uvicorn
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+# Convert HTTP_PORT to integer
+HTTP_PORT = int(os.getenv("HTTP_PORT", "5000"))
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = FastAPI()
+
+# Store gladia session information
+gladia_session = {
+ "id": None,
+ "url": None,
+}
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/ulaw", # μ-law!
+ "bit_depth": 8, # 8-bit μ-law
+ "sample_rate": 8000, # matches Twilio
+ "channels": 1,
+ }
+
+ try:
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ logger.info("🛰 Gladia session ID: %s", data["id"])
+ gladia_session["id"] = data["id"]
+ gladia_session["url"] = data["url"]
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+ except requests.exceptions.RequestException as e:
+ logger.error("Failed to create Gladia session: %s", e)
+ raise
+
+# Create initial Gladia session
+try:
+ create_session()
+except Exception as e:
+ logger.error("Failed to create initial Gladia session: %s", e)
+ raise
+
+@app.get("/health")
+def health_check():
+ return {"status": "ok", "service": "twilio-gladia-transcription"}
+
+@app.websocket("/media")
+async def media(websocket: WebSocket):
+ """Handle incoming WebSocket connections from Twilio."""
+ await websocket.accept()
+ client = websocket.client
+ client_info = f"{client.host}:{client.port}"
+ logger.info(f"🔌 Twilio WebSocket connected from {client_info}")
+
+ await handle_websocket(websocket)
+
+async def process_message(ws_message, gladia_ws):
+ """Process a message asynchronously."""
+ try:
+ data = json.loads(ws_message)
+ if data.get("event") != "media":
+ return # ignore start/stop pings
+
+ # 20 ms μ-law chunk, base64-encoded
+ mulaw = base64.b64decode(data["media"]["payload"])
+
+ # Send raw μ-law bytes to Gladia
+ await gladia_ws.send(mulaw)
+
+ # Check for transcripts
+ try:
+ # Try to get transcripts (non-blocking)
+ while True:
+ resp = await asyncio.wait_for(gladia_ws.recv(), 0.001)
+ handle_gladia(resp)
+ except asyncio.TimeoutError:
+ pass # No transcript available yet
+ except Exception as e:
+ logger.error(f"Error processing message: {e}")
+
+def handle_gladia(msg):
+ """Process transcription results from Gladia."""
+ m = json.loads(msg)
+ if m["type"] == "transcript" and m["data"]["is_final"]:
+ transcript = m["data"]["utterance"]["text"]
+ print(f"📝 Transcript: {transcript}")
+ return transcript
+```
+
+No `audioop`, no resampling, no CPU overhead, and with proper asynchronous handling.
+
+---
+
+### 3 — Tell Twilio to stream audio
+
+Point your Twilio number (or a Voice Application) to a TwiML endpoint like:
+
+```xml
+
+
+
+
+
+
+
+ +14155551234
+
+```
+
+Let's examine each element in this TwiML configuration:
+
+- ``: The root element of any TwiML document. It contains all the TwiML instructions for handling the call.
+
+- ``: This element initiates Twilio's Media Streams feature, which allows streaming of audio in real-time while the call is in progress. It tells Twilio to begin capturing and streaming media before executing the rest of the call flow.
+
+- ``: A child element of `` that configures the media stream:
+ - `url` attribute: Specifies the WebSocket endpoint where Twilio will send the audio data.
+ - The URL must use secure WebSockets (`wss://`).
+ - The domain should be your public domain (e.g., an ngrok URL or a custom domain).
+ - The path (`/media`) must match the WebSocket route in your FastAPI application.
+ - Each call will create a new WebSocket connection to this endpoint.
+
+- ``: After starting the media stream, this element connects the caller to another phone number. During this connection:
+ - The media streaming continues in the background.
+ - Audio from both sides of the conversation is sent to your WebSocket endpoint.
+ - You can replace this with other TwiML verbs like ``, ``, or `` depending on your use case.
+ - The number shown (`+14155551234`) is just an example - replace it with your desired destination.
+
+When a call triggers this TwiML, Twilio immediately opens a secure WebSocket connection to your server's `/media` endpoint and begins streaming audio as 20 ms μ-law frames. Each frame is base64-encoded and sent as a JSON message, which your server then decodes and forwards to Gladia.
+
+---
+
+### 4 — Expose & test
+
+```bash
+# Install dependencies
+pip install -r requirements.txt
+
+# Run the proxy (default port 5000)
+python server.py
+
+# Or specify a custom port
+HTTP_PORT=5001 python server.py
+
+# Tunnel it with ngrok
+ngrok http 5000
+
+# Or with a custom domain
+ngrok http --domain=your.domain.com 5000
+```
+
+Call your Twilio number and you should see live text scroll instantly:
+
+```
+🛰 Gladia session ID: 3f65…
+🚀 Starting server on 0.0.0.0:5000
+🔌 Twilio WebSocket connected from 54.174.99.133:12345
+📝 Transcript: Hello and thank you for calling Acme support.
+📝 Transcript: Sure, I'd be happy to help with your order.
+```
+
+---
+
+### 5 — Next steps
+
+* **Add-ons** – enable diarization, sentiment, keywords, etc., by including the flags when you create the session.
+* **Dual-channel** – Twilio can stream separate channels; Gladia preserves them so you always know who's speaking.
+* **Post-call JSON** – store the session `id` and hit `GET /v2/live/:id` for the full, punctuated transcript when the call ends.
+* **Scale it** – FastAPI with Uvicorn is production-ready, but for even higher loads, consider deploying with Gunicorn as a process manager.
+
+---
+
+### Wrap-up
+
+Because Gladia natively accepts Twilio's μ-law stream, **real-time call transcription is now literally "base64-decode and forward."** Fewer steps, lower CPU, and the same lightning-fast latency. Drop this proxy into any Python stack and start surfacing live insights from every call. Happy building! 🎙️📝
+
+[1]: https://docs.gladia.io/api-reference/v2/live/init "Initiate a session - Gladia"
diff --git a/blogs/twilio-solaria-python-fastapi/src/README.md b/blogs/twilio-solaria-python-fastapi/src/README.md
new file mode 100644
index 0000000..9a164ab
--- /dev/null
+++ b/blogs/twilio-solaria-python-fastapi/src/README.md
@@ -0,0 +1,94 @@
+# Twilio Call Transcription with Gladia
+
+This project implements real-time transcription of Twilio calls using Gladia's Speech-to-Text API, which now natively supports Twilio's μ-law audio format.
+
+## Prerequisites
+
+- **Gladia API key** - Sign up at [app.gladia.io](https://app.gladia.io/)
+- **Twilio account + voice-enabled number**
+- **Python 3.8+**
+- **Public URL** - Use ngrok or a cloud VM to expose your WebSocket endpoint
+
+## Setup
+
+1. **Install dependencies**:
+ ```bash
+ # Install pyenv (if not already installed)
+ # macOS (using Homebrew)
+ brew install pyenv
+
+ # Linux
+ curl https://pyenv.run | bash
+
+ # Setup pyenv
+ pyenv install 3.12
+ pyenv local 3.12
+
+ pyenv virtualenv 3.12 twilio-gladia-python
+
+ # Install Python dependencies
+ pip install -r requirements.txt
+ ```
+
+2. **Set up environment variables**:
+ Create a `.env` file in the same directory as `server.py` with:
+ ```
+ GLADIA_API_KEY=your_gladia_api_key_here
+ ```
+
+3. **Configure Twilio**:
+ - Create a TwiML Bin or webhook in your Twilio account at [https://console.twilio.com/us1/develop/twiml-bins](https://console.twilio.com/us1/develop/twiml-bins)
+ - Use the contents of `twiml_example.xml` as your TwiML
+ - Replace `YOUR_PUBLIC_DOMAIN` with your actual public domain (the ngrok URL from step 2 in "Running the application" or a custom domain https://ngrok.com/docs/guides/other-guides/how-to-set-up-a-custom-domain/)
+ - Assign this TwiML Bin to your Twilio phone number at [https://console.twilio.com/us1/develop/phone-numbers/manage/incoming](https://console.twilio.com/us1/develop/phone-numbers/manage/incoming)
+
+## Technical Notes
+
+- The server uses FastAPI with native WebSocket support
+- Uvicorn is used as the ASGI server
+- The application is fully asynchronous
+
+## Running the application
+
+1. **Start the server**:
+ ```bash
+ # Default port (5000)
+ python server.py
+
+ # Or specify a custom port
+ HTTP_PORT=5001 python server.py
+ ```
+
+2. **Expose your server publicly**:
+ ```bash
+ # Make sure the port matches HTTP_PORT from step 1
+ ngrok http 5000 # you'll get a ngrok assign random url
+
+ # If you used a custom port in step 1, use the same port here
+ ngrok http 5001
+
+ # for a custom domain
+ ngrok http --domain=jl.mydomain.com 5001
+ ```
+
+3. **Update your TwiML**:
+ - Update the `url` in your TwiML to your ngrok URL (e.g., `wss://jl.mydomain.com/media` or the ngrok random url previously attributed by ngrok)
+
+4. **Test**:
+ - Call your Twilio number
+ - You should see transcripts appearing in your console
+
+## How it works
+
+1. The server connects to Gladia's real-time STT API
+2. When a call comes in, Twilio connects to your WebSocket endpoint
+3. Twilio streams 20ms μ-law frames, which are base64-decoded
+4. The raw μ-law bytes are forwarded to Gladia without any conversion
+5. Gladia returns real-time transcription results
+
+## Next steps
+
+- **Add-ons** – enable diarization, sentiment, keywords, etc.
+- **Dual-channel** – Transcribe both sides of the conversation separately
+- **Post-call JSON** – Get the full transcript when the call ends
+- **Scale it** – Use Gunicorn as a process manager for even higher loads
\ No newline at end of file
diff --git a/blogs/twilio-solaria-python-fastapi/src/env_setup.txt b/blogs/twilio-solaria-python-fastapi/src/env_setup.txt
new file mode 100644
index 0000000..cf4bdf9
--- /dev/null
+++ b/blogs/twilio-solaria-python-fastapi/src/env_setup.txt
@@ -0,0 +1,7 @@
+Before running the application, create a .env file in the same directory with the following contents:
+
+GLADIA_API_KEY=your_gladia_api_key_here
+# Optional: Set a custom port (default is 5000)
+# HTTP_PORT=5001
+
+You need to replace 'your_gladia_api_key_here' with your actual Gladia API key from https://app.gladia.io/
\ No newline at end of file
diff --git a/blogs/twilio-solaria-python-fastapi/src/requirements.txt b/blogs/twilio-solaria-python-fastapi/src/requirements.txt
new file mode 100644
index 0000000..54fbbba
--- /dev/null
+++ b/blogs/twilio-solaria-python-fastapi/src/requirements.txt
@@ -0,0 +1,6 @@
+fastapi==0.108.0
+uvicorn==0.25.0
+websockets==15.0.1
+requests==2.31.0
+python-dotenv==1.0.1
+greenlet==3.2.1
\ No newline at end of file
diff --git a/blogs/twilio-solaria-python-fastapi/src/server.py b/blogs/twilio-solaria-python-fastapi/src/server.py
new file mode 100644
index 0000000..2cd4442
--- /dev/null
+++ b/blogs/twilio-solaria-python-fastapi/src/server.py
@@ -0,0 +1,163 @@
+import os
+import base64
+import json
+import logging
+import asyncio
+import websockets
+import requests
+from fastapi import FastAPI, WebSocket
+from dotenv import load_dotenv
+import uvicorn
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+# Convert HTTP_PORT to integer
+HTTP_PORT = int(os.getenv("HTTP_PORT", "5000"))
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = FastAPI()
+
+# Store gladia session information
+gladia_session = {
+ "id": None,
+ "url": None,
+}
+
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/ulaw", # μ-law!
+ "bit_depth": 8, # 8-bit μ-law
+ "sample_rate": 8000, # matches Twilio
+ "channels": 1,
+ }
+
+ try:
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ logger.info("🛰 Gladia session ID: %s", data["id"])
+ gladia_session["id"] = data["id"]
+ gladia_session["url"] = data["url"]
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+ except requests.exceptions.RequestException as e:
+ logger.error("Failed to create Gladia session: %s", e)
+ raise
+
+
+# Create initial Gladia session
+try:
+ create_session()
+except Exception as e:
+ logger.error("Failed to create initial Gladia session: %s", e)
+ raise
+
+
+def handle_gladia(msg):
+ """Process transcription results from Gladia."""
+ m = json.loads(msg)
+ if m["type"] == "transcript" and m["data"]["is_final"]:
+ transcript = m["data"]["utterance"]["text"]
+ print(f"📝 Transcript: {transcript}")
+ return transcript
+
+
+# Add a health check endpoint
+@app.get("/health")
+def health_check():
+ return {"status": "ok", "service": "twilio-gladia-transcription"}
+
+
+@app.websocket("/media")
+async def media(websocket: WebSocket):
+ """Handle incoming WebSocket connections from Twilio."""
+ await websocket.accept()
+ client = websocket.client
+ client_info = f"{client.host}:{client.port}"
+ logger.info(f"🔌 Twilio WebSocket connected from {client_info}")
+
+ await handle_websocket(websocket)
+
+
+@app.websocket("/{remaining_path:path}")
+async def catch_all_websocket(websocket: WebSocket, remaining_path: str):
+ """Catch-all handler for WebSocket connections."""
+ await websocket.accept()
+ logger.info(f"🔌 Catch-all WebSocket connected to /{remaining_path}")
+
+ await handle_websocket(websocket)
+
+
+async def process_message(ws_message, gladia_ws):
+ """Process a message asynchronously."""
+ try:
+ data = json.loads(ws_message)
+ if data.get("event") != "media":
+ logger.debug(f"Non-media event: {data.get('event')}")
+ return # ignore start/stop pings
+
+ # 20 ms μ-law chunk, base64-encoded
+ mulaw = base64.b64decode(data["media"]["payload"])
+
+ # Send raw μ-law bytes to Gladia
+ await gladia_ws.send(mulaw)
+
+ # Check for transcripts
+ try:
+ # Try to get transcripts (non-blocking)
+ while True:
+ resp = await asyncio.wait_for(gladia_ws.recv(), 0.001)
+ handle_gladia(resp)
+ except asyncio.TimeoutError:
+ pass # No transcript available yet, continue
+ except Exception as e:
+ logger.error(f"Error processing message: {e}")
+
+
+async def handle_websocket(websocket: WebSocket):
+ """Handle a WebSocket connection asynchronously."""
+ # Connect to Gladia for this connection
+ try:
+ gladia_ws = await websockets.connect(gladia_session["url"])
+ logger.info(f"Connected to Gladia session {gladia_session['id']}")
+
+ while True:
+ try:
+ message = await websocket.receive_text()
+ await process_message(message, gladia_ws)
+ except Exception as e:
+ logger.error(f"Error receiving message: {e}")
+ break
+
+ except websockets.exceptions.ConnectionClosed:
+ logger.info("Gladia WebSocket connection closed")
+ finally:
+ # Close the Gladia connection
+ try:
+ await gladia_ws.close()
+ except:
+ pass
+
+
+if __name__ == "__main__":
+ if not GLADIA_KEY:
+ logger.error("GLADIA_API_KEY environment variable is required")
+ exit(1)
+
+ logger.info(f"🚀 Starting server on 0.0.0.0:{HTTP_PORT}")
+ uvicorn.run(app, host="0.0.0.0", port=HTTP_PORT)
diff --git a/blogs/twilio-solaria-python-fastapi/src/twiml_example.xml b/blogs/twilio-solaria-python-fastapi/src/twiml_example.xml
new file mode 100644
index 0000000..b18f641
--- /dev/null
+++ b/blogs/twilio-solaria-python-fastapi/src/twiml_example.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+ +14155551234
+
\ No newline at end of file
diff --git a/blogs/twilio-solaria-python-flask/.gitignore b/blogs/twilio-solaria-python-flask/.gitignore
new file mode 100644
index 0000000..85c55eb
--- /dev/null
+++ b/blogs/twilio-solaria-python-flask/.gitignore
@@ -0,0 +1,2 @@
+.env
+.venv
diff --git a/blogs/twilio-solaria-python-flask/blog.md b/blogs/twilio-solaria-python-flask/blog.md
new file mode 100644
index 0000000..51f8387
--- /dev/null
+++ b/blogs/twilio-solaria-python-flask/blog.md
@@ -0,0 +1,273 @@
+## How to Transcribe Twilio Calls in Real Time with Flask and Python & Gladia (μ-law Native)
+
+Twilio's Voice **Media Streams** deliver 8 kHz, 8-bit μ-law audio. Gladia's real-time Speech-to-Text (STT) API now ingests that exact format out-of-the-box, so you can skip every resample or decode step and still keep sub-300 ms latency.
+
+Gladia's `/v2/live` endpoint lets you specify **`encoding: "wav/ulaw"`** with **`bit_depth: 8`**, matching Twilio 1-for-1 ([Gladia][1]).
+
+---
+
+### Prerequisites
+
+| What you need | Why |
+| ----------------------------------------- | -------------------------------------------------------------------- |
+| **Gladia API key** | Sign up & copy from the dashboard. |
+| **Twilio account + voice-enabled number** | To receive / place calls. |
+| **Python 3.12+** | We'll use `flask`, `flask-sock`, `websockets`, and `requests`. |
+| **Public URL** | Expose a WebSocket endpoint with ngrok or a cloud VM. |
+| **8 kHz, 8-bit μ-law audio** | Exactly what Twilio streams – and what Gladia now consumes natively. |
+
+---
+
+### 1 — Initiate a Gladia live session
+
+```python
+import os
+import requests
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/ulaw", # μ-law!
+ "bit_depth": 8, # 8-bit μ-law
+ "sample_rate": 8000, # matches Twilio
+ "channels": 1,
+ }
+
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ print("🛰 Gladia session ID:", data["id"])
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+```
+
+> **Why no resample / decode?** Gladia parses raw μ-law frames directly, so we just forward the bytes Twilio gives us.
+
+---
+
+### System Architecture Flow
+
+Here's how data flows through the system:
+
+```mermaid
+sequenceDiagram
+ participant Caller
+ participant Twilio
+ participant Flask as Flask WebSocket Server
+ participant Gladia as Gladia API
+
+ Flask->>Gladia: Initialize session (POST /v2/live)
+ Gladia-->>Flask: Return WebSocket URL
+
+ Caller->>Twilio: Make phone call
+ Twilio->>Flask: Connect to WebSocket (/media)
+
+ loop Audio Streaming
+ Twilio->>Flask: Send audio chunks (base64 μ-law)
+ Flask->>Flask: Decode base64
+ Flask->>Gladia: Forward raw μ-law bytes
+ Gladia->>Gladia: Process speech
+ Gladia-->>Flask: Return partial transcripts
+ Gladia-->>Flask: Return final transcripts
+ Flask->>Flask: Log/process transcripts
+ end
+
+ Caller->>Twilio: End call
+ Twilio->>Flask: Close WebSocket
+ Flask->>Gladia: Close WebSocket
+```
+
+---
+
+### 2 — Build the Python WebSocket proxy
+
+The proxy now does **three** things:
+
+1. Accept Twilio's base64-encoded μ-law frames.
+2. Base64-decode (that's the *only* transformation).
+3. Pipe the raw bytes straight to Gladia and print transcripts as they come back.
+
+```python
+# server.py
+import os
+import base64
+import json
+import logging
+import asyncio
+import websockets
+import requests
+from flask import Flask, request, jsonify
+from flask_sock import Sock
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+# Convert HTTP_PORT to integer
+HTTP_PORT = int(os.getenv("HTTP_PORT", "5000"))
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = Flask(__name__)
+sock = Sock(app)
+
+# Store gladia session information
+gladia_session = {
+ "id": None,
+ "url": None,
+}
+
+# Create initial Gladia session
+try:
+ create_session()
+except Exception as e:
+ logger.error("Failed to create initial Gladia session: %s", e)
+ raise
+
+@sock.route("/media")
+def media(ws):
+ """Handle incoming WebSocket connections from Twilio."""
+ client_info = f"{request.remote_addr}:{request.environ.get('REMOTE_PORT', '?')}"
+ logger.info(f"🔌 Twilio WebSocket connected from {client_info}")
+
+ handle_websocket(ws)
+
+async def process_message(ws_message, gladia_ws):
+ """Process a message asynchronously."""
+ try:
+ data = json.loads(ws_message)
+ if data.get("event") != "media":
+ return # ignore start/stop pings
+
+ # 20 ms μ-law chunk, base64-encoded
+ mulaw = base64.b64decode(data["media"]["payload"])
+
+ # Send raw μ-law bytes to Gladia
+ await gladia_ws.send(mulaw)
+
+ # Check for transcripts
+ try:
+ # Try to get transcripts (non-blocking)
+ while True:
+ resp = await asyncio.wait_for(gladia_ws.recv(), 0.001)
+ handle_gladia(resp)
+ except asyncio.TimeoutError:
+ pass # No transcript available yet
+ except Exception as e:
+ logger.error(f"Error processing message: {e}")
+
+def handle_gladia(msg):
+ """Process transcription results from Gladia."""
+ m = json.loads(msg)
+ if m["type"] == "transcript" and m["data"]["is_final"]:
+ transcript = m["data"]["utterance"]["text"]
+ print(f"📝 Transcript: {transcript}")
+ return transcript
+```
+
+No `audioop`, no resampling, no CPU overhead, and with proper asynchronous handling.
+
+---
+
+### 3 — Tell Twilio to stream audio
+
+Point your Twilio number (or a Voice Application) to a TwiML endpoint like:
+
+```xml
+
+
+
+
+
+
+
+ +14155551234
+
+```
+
+Let's examine each element in this TwiML configuration:
+
+- ``: The root element of any TwiML document. It contains all the TwiML instructions for handling the call.
+
+- ``: This element initiates Twilio's Media Streams feature, which allows streaming of audio in real-time while the call is in progress. It tells Twilio to begin capturing and streaming media before executing the rest of the call flow.
+
+- ``: A child element of `` that configures the media stream:
+ - `url` attribute: Specifies the WebSocket endpoint where Twilio will send the audio data.
+ - The URL must use secure WebSockets (`wss://`).
+ - The domain should be your public domain (e.g., an ngrok URL or a custom domain).
+ - The path (`/media`) must match the WebSocket route in your Flask application.
+ - Each call will create a new WebSocket connection to this endpoint.
+
+- ``: After starting the media stream, this element connects the caller to another phone number. During this connection:
+ - The media streaming continues in the background.
+ - Audio from both sides of the conversation is sent to your WebSocket endpoint.
+ - You can replace this with other TwiML verbs like ``, ``, or `` depending on your use case.
+ - The number shown (`+14155551234`) is just an example - replace it with your desired destination.
+
+When a call triggers this TwiML, Twilio immediately opens a secure WebSocket connection to your server's `/media` endpoint and begins streaming audio as 20 ms μ-law frames. Each frame is base64-encoded and sent as a JSON message, which your server then decodes and forwards to Gladia.
+
+---
+
+### 4 — Expose & test
+
+```bash
+# Install dependencies
+pip install -r requirements.txt
+
+# Run the proxy (default port 5000)
+python server.py
+
+# Or specify a custom port
+HTTP_PORT=5001 python server.py
+
+# Tunnel it with ngrok
+ngrok http 5000
+
+# Or with a custom domain
+ngrok http --domain=your.domain.com 5000
+```
+
+Call your Twilio number and you should see live text scroll instantly:
+
+```
+🛰 Gladia session ID: 3f65…
+🚀 Listening on ws://0.0.0.0:5000/media
+🔌 Twilio WebSocket connected from 54.174.99.133:?
+📝 Transcript: Hello and thank you for calling Acme support.
+📝 Transcript: Sure, I'd be happy to help with your order.
+```
+
+---
+
+### 5 — Next steps
+
+* **Add-ons** – enable diarization, sentiment, keywords, etc., by including the flags when you create the session.
+* **Dual-channel** – Twilio can stream separate channels; Gladia preserves them so you always know who's speaking.
+* **Post-call JSON** – store the session `id` and hit `GET /v2/live/:id` for the full, punctuated transcript when the call ends.
+* **Scale it** – containerise the proxy for production use with a proper WSGI server that supports WebSockets.
+
+---
+
+### Wrap-up
+
+Because Gladia natively accepts Twilio's μ-law stream, **real-time call transcription is now literally "base64-decode and forward."** Fewer steps, lower CPU, and the same lightning-fast latency. Drop this proxy into any Python stack and start surfacing live insights from every call. Happy building! 🎙️📝
+
+[1]: https://docs.gladia.io/api-reference/v2/live/init "Initiate a session - Gladia"
diff --git a/blogs/twilio-solaria-python-flask/src/README.md b/blogs/twilio-solaria-python-flask/src/README.md
new file mode 100644
index 0000000..9c953fe
--- /dev/null
+++ b/blogs/twilio-solaria-python-flask/src/README.md
@@ -0,0 +1,94 @@
+# Twilio Call Transcription with Gladia
+
+This project implements real-time transcription of Twilio calls using Gladia's Speech-to-Text API, which now natively supports Twilio's μ-law audio format.
+
+## Prerequisites
+
+- **Gladia API key** - Sign up at [app.gladia.io](https://app.gladia.io/)
+- **Twilio account + voice-enabled number**
+- **Python 3.8+**
+- **Public URL** - Use ngrok or a cloud VM to expose your WebSocket endpoint
+
+## Setup
+
+1. **Install dependencies**:
+ ```bash
+ # Install pyenv (if not already installed)
+ # macOS (using Homebrew)
+ brew install pyenv
+
+ # Linux
+ curl https://pyenv.run | bash
+
+ # Setup pyenv
+ pyenv install 3.12
+ pyenv local 3.12
+
+ pyenv virtualenv 3.12 twilio-gladia-python
+
+ # Install Python dependencies
+ pip install -r requirements.txt
+ ```
+
+2. **Set up environment variables**:
+ Create a `.env` file in the same directory as `server.py` with:
+ ```
+ GLADIA_API_KEY=your_gladia_api_key_here
+ ```
+
+3. **Configure Twilio**:
+ - Create a TwiML Bin or webhook in your Twilio account at [https://console.twilio.com/us1/develop/twiml-bins](https://console.twilio.com/us1/develop/twiml-bins)
+ - Use the contents of `twiml_example.xml` as your TwiML
+ - Replace `YOUR_PUBLIC_DOMAIN` with your actual public domain (the ngrok URL from step 2 in "Running the application" or a custom domain https://ngrok.com/docs/guides/other-guides/how-to-set-up-a-custom-domain/)
+ - Assign this TwiML Bin to your Twilio phone number at [https://console.twilio.com/us1/develop/phone-numbers/manage/incoming](https://console.twilio.com/us1/develop/phone-numbers/manage/incoming)
+
+## Technical Notes
+
+- The server uses `flask-sock` for WebSocket support, which works with Flask's built-in development server
+- No gevent or external server is needed for development purposes
+- For production, consider using a WSGI server that supports WebSockets, like uWSGI or Gunicorn with gevent worker
+
+## Running the application
+
+1. **Start the server**:
+ ```bash
+ # Default port (5000)
+ python server.py
+
+ # Or specify a custom port
+ HTTP_PORT=5001 python server.py
+ ```
+
+2. **Expose your server publicly**:
+ ```bash
+ # Make sure the port matches HTTP_PORT from step 1
+ ngrok http 5000 # you'll get a ngrok assign random url
+
+ # If you used a custom port in step 1, use the same port here
+ ngrok http 5001
+
+ # for a custom domain
+ ngrok http --domain=jl.mydomain.com 5001
+ ```
+
+3. **Update your TwiML**:
+ - Update the `url` in your TwiML to your ngrok URL (e.g., `wss://jl.mydomain.com/media` or the ngrok random url previously attributed by ngrok)
+
+4. **Test**:
+ - Call your Twilio number
+ - You should see transcripts appearing in your console
+
+## How it works
+
+1. The server connects to Gladia's real-time STT API
+2. When a call comes in, Twilio connects to your WebSocket endpoint
+3. Twilio streams 20ms μ-law frames, which are base64-decoded
+4. The raw μ-law bytes are forwarded to Gladia without any conversion
+5. Gladia returns real-time transcription results
+
+## Next steps
+
+- **Add-ons** – enable diarization, sentiment, keywords, etc.
+- **Dual-channel** – Transcribe both sides of the conversation separately
+- **Post-call JSON** – Get the full transcript when the call ends
+- **Scale it** – Containerize for production use
\ No newline at end of file
diff --git a/blogs/twilio-solaria-python-flask/src/env_setup.txt b/blogs/twilio-solaria-python-flask/src/env_setup.txt
new file mode 100644
index 0000000..cf4bdf9
--- /dev/null
+++ b/blogs/twilio-solaria-python-flask/src/env_setup.txt
@@ -0,0 +1,7 @@
+Before running the application, create a .env file in the same directory with the following contents:
+
+GLADIA_API_KEY=your_gladia_api_key_here
+# Optional: Set a custom port (default is 5000)
+# HTTP_PORT=5001
+
+You need to replace 'your_gladia_api_key_here' with your actual Gladia API key from https://app.gladia.io/
\ No newline at end of file
diff --git a/blogs/twilio-solaria-python-flask/src/requirements.txt b/blogs/twilio-solaria-python-flask/src/requirements.txt
new file mode 100644
index 0000000..3dadb0a
--- /dev/null
+++ b/blogs/twilio-solaria-python-flask/src/requirements.txt
@@ -0,0 +1,6 @@
+flask==3.1.0
+flask-sock==0.6.0
+websockets==15.0.1
+requests==2.31.0
+python-dotenv==1.0.1
+greenlet==3.2.1
\ No newline at end of file
diff --git a/blogs/twilio-solaria-python-flask/src/server.py b/blogs/twilio-solaria-python-flask/src/server.py
new file mode 100644
index 0000000..952a358
--- /dev/null
+++ b/blogs/twilio-solaria-python-flask/src/server.py
@@ -0,0 +1,184 @@
+import os
+import base64
+import json
+import logging
+import asyncio
+import websockets
+import requests
+from flask import Flask, request, jsonify
+from flask_sock import Sock
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+# Convert HTTP_PORT to integer
+HTTP_PORT = int(os.getenv("HTTP_PORT", "5000"))
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = Flask(__name__)
+sock = Sock(app)
+
+# Store gladia session information
+gladia_session = {
+ "id": None,
+ "url": None,
+}
+
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/ulaw", # μ-law!
+ "bit_depth": 8, # 8-bit μ-law
+ "sample_rate": 8000, # matches Twilio
+ "channels": 1,
+ }
+
+ try:
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ logger.info("🛰 Gladia session ID: %s", data["id"])
+ gladia_session["id"] = data["id"]
+ gladia_session["url"] = data["url"]
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+ except requests.exceptions.RequestException as e:
+ logger.error("Failed to create Gladia session: %s", e)
+ raise
+
+
+# Create initial Gladia session
+try:
+ create_session()
+except Exception as e:
+ logger.error("Failed to create initial Gladia session: %s", e)
+ raise
+
+
+def handle_gladia(msg):
+ """Process transcription results from Gladia."""
+ m = json.loads(msg)
+ if m["type"] == "transcript" and m["data"]["is_final"]:
+ transcript = m["data"]["utterance"]["text"]
+ print(f"📝 Transcript: {transcript}")
+ return transcript
+
+
+# Add a health check endpoint
+@app.route("/health")
+def health_check():
+ return jsonify({"status": "ok", "service": "twilio-gladia-transcription"})
+
+
+# Keep the specific route for /media
+@sock.route("/media")
+def media(ws):
+ """Handle incoming WebSocket connections from Twilio."""
+ client_info = f"{request.remote_addr}:{request.environ.get('REMOTE_PORT', '?')}"
+ logger.info(f"🔌 Twilio WebSocket connected from {client_info}")
+ logger.info(f"WebSocket headers: {request.headers}")
+
+ handle_websocket(ws)
+
+
+# Add a catch-all route for flexibility
+@sock.route("/")
+def catch_all_websocket(ws, remaining_path):
+ """Catch-all handler for WebSocket connections."""
+ logger.info(f"🔌 Catch-all WebSocket connected to /{remaining_path}")
+ logger.info(f"WebSocket headers: {request.headers}")
+
+ handle_websocket(ws)
+
+
+async def process_message(ws_message, gladia_ws):
+ """Process a message asynchronously."""
+ try:
+ data = json.loads(ws_message)
+ if data.get("event") != "media":
+ logger.debug(f"Non-media event: {data.get('event')}")
+ return # ignore start/stop pings
+
+ # 20 ms μ-law chunk, base64-encoded
+ mulaw = base64.b64decode(data["media"]["payload"])
+
+ # Send raw μ-law bytes to Gladia
+ await gladia_ws.send(mulaw)
+
+ # Check for transcripts
+ try:
+ # Try to get transcripts (non-blocking)
+ while True:
+ resp = await asyncio.wait_for(gladia_ws.recv(), 0.001)
+ handle_gladia(resp)
+ except asyncio.TimeoutError:
+ pass # No transcript available yet, continue
+ except Exception as e:
+ logger.error(f"Error processing message: {e}")
+
+
+async def handle_connection(ws):
+ """Handle a WebSocket connection asynchronously."""
+ # Connect to Gladia for this connection
+ try:
+ gladia_ws = await websockets.connect(gladia_session["url"])
+ logger.info(f"Connected to Gladia session {gladia_session['id']}")
+
+ while True:
+ message = ws.receive()
+ if message is None:
+ break
+
+ await process_message(message, gladia_ws)
+
+ except websockets.exceptions.ConnectionClosed:
+ logger.info("Gladia WebSocket connection closed")
+ finally:
+ # Close the Gladia connection
+ try:
+ await gladia_ws.close()
+ except:
+ pass
+
+
+def handle_websocket(ws):
+ """Common WebSocket handling logic."""
+ try:
+ # Create a new event loop for this thread
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+
+ # Run the connection handler in this loop
+ loop.run_until_complete(handle_connection(ws))
+ except Exception as e:
+ logger.error(f"WebSocket connection error: {e}")
+ finally:
+ # Clean up
+ try:
+ loop.close()
+ except:
+ pass
+
+
+if __name__ == "__main__":
+ if not GLADIA_KEY:
+ logger.error("GLADIA_API_KEY environment variable is required")
+ exit(1)
+
+ # flask_sock works with the Flask development server
+ logger.info(f"🚀 Listening on ws://0.0.0.0:{HTTP_PORT}/media")
+ app.run(host="0.0.0.0", port=HTTP_PORT, debug=True)
diff --git a/blogs/twilio-solaria-python-flask/src/twiml_example.xml b/blogs/twilio-solaria-python-flask/src/twiml_example.xml
new file mode 100644
index 0000000..b18f641
--- /dev/null
+++ b/blogs/twilio-solaria-python-flask/src/twiml_example.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+ +14155551234
+
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/.gitignore b/blogs/twilio-solaria-typescript/.gitignore
new file mode 100644
index 0000000..3df08b0
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/.gitignore
@@ -0,0 +1,28 @@
+# Dependency directories
+node_modules/
+dist/
+
+# Environment variables
+.env
+.env.local
+.env.*.local
+
+# Logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+logs
+*.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# OS specific
+.DS_Store
+Thumbs.db
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/blog.md b/blogs/twilio-solaria-typescript/blog.md
new file mode 100644
index 0000000..2ea54c0
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/blog.md
@@ -0,0 +1,392 @@
+## How to Transcribe Twilio Calls in Real Time with TypeScript & Gladia (μ-law Native)
+
+Twilio's Voice **Media Streams** deliver 8 kHz, 8-bit μ-law audio. Gladia's real-time Speech-to-Text (STT) API now ingests that exact format out-of-the-box, so you can skip every resample or decode step and still keep sub-300 ms latency.
+
+Gladia's `/v2/live` endpoint lets you specify **`encoding: "wav/ulaw"`** with **`bit_depth: 8`**, matching Twilio 1-for-1 ([Gladia][1]).
+
+---
+
+### Prerequisites
+
+| What you need | Why |
+| ----------------------------------------- | -------------------------------------------------------------------- |
+| **Gladia API key** | Sign up & copy from the dashboard. |
+| **Twilio account + voice-enabled number** | To receive / place calls. |
+| **Node.js 18+** | We'll use the `ws` library for WebSocket handling. |
+| **Public URL** | Expose a WebSocket endpoint with ngrok or a cloud VM. |
+| **8 kHz, 8-bit μ-law audio** | Exactly what Twilio streams – and what Gladia now consumes natively. |
+
+---
+
+### 1 — Initiate a Gladia live session
+
+```typescript
+import * as https from 'https';
+import { GladiaSession } from './types';
+
+const GLADIA_INIT_URL = 'https://api.gladia.io/v2/live';
+
+/**
+ * Creates a Gladia real-time transcription session
+ * @param apiKey Gladia API key
+ * @returns Promise resolving to a session with ID and WebSocket URL
+ */
+export async function createSession(apiKey: string): Promise {
+ // Define the payload for μ-law, 8-bit, 8 kHz, mono audio
+ const payload = {
+ encoding: 'wav/ulaw',
+ bit_depth: 8,
+ sample_rate: 8000,
+ channels: 1
+ };
+
+ // Convert payload to JSON
+ const body = JSON.stringify(payload);
+
+ // Create and return a promise for the HTTP request
+ return new Promise((resolve, reject) => {
+ // Prepare the request options
+ const options = {
+ method: 'POST',
+ headers: {
+ 'X-Gladia-Key': apiKey,
+ 'Content-Type': 'application/json',
+ 'Content-Length': Buffer.byteLength(body)
+ }
+ };
+
+ // Make the HTTP request
+ const req = https.request(GLADIA_INIT_URL, options, (res) => {
+ // Check for successful status code
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
+ const statusCode = res.statusCode;
+ let responseData = '';
+
+ res.on('data', (chunk) => {
+ responseData += chunk;
+ });
+
+ res.on('end', () => {
+ reject(new Error(`Bad status code: ${statusCode} - ${responseData}`));
+ });
+
+ return;
+ }
+
+ // Collect the response data
+ let responseData = '';
+ res.on('data', (chunk) => {
+ responseData += chunk;
+ });
+
+ // Process the response when it's complete
+ res.on('end', () => {
+ try {
+ const data = JSON.parse(responseData) as GladiaSession;
+ console.log(`🛰 Gladia session ID: ${data.id}`);
+ resolve(data);
+ } catch (error) {
+ reject(new Error(`Failed to decode response: ${error}`));
+ }
+ });
+ });
+
+ // Handle request errors
+ req.on('error', (error) => {
+ reject(new Error(`Session init request failed: ${error}`));
+ });
+
+ // Set timeout (10 seconds)
+ req.setTimeout(10000, () => {
+ req.destroy();
+ reject(new Error('Request timed out'));
+ });
+
+ // Send the request body
+ req.write(body);
+ req.end();
+ });
+}
+```
+
+> **Why no resample / decode?** Gladia parses raw μ-law frames directly, so we just forward the bytes Twilio gives us.
+
+---
+
+### System Architecture Flow
+
+Here's how data flows through the system:
+
+```mermaid
+sequenceDiagram
+ participant Caller
+ participant Twilio
+ participant TSServer as TypeScript WebSocket Server
+ participant Gladia as Gladia API
+
+ TSServer->>Gladia: Initialize session (POST /v2/live)
+ Gladia-->>TSServer: Return WebSocket URL
+
+ Caller->>Twilio: Make phone call
+ Twilio->>TSServer: Connect to WebSocket (/media)
+
+ loop Audio Streaming
+ Twilio->>TSServer: Send audio chunks (base64 μ-law)
+ TSServer->>TSServer: Decode base64
+ TSServer->>Gladia: Forward raw μ-law bytes
+ Gladia->>Gladia: Process speech
+ Gladia-->>TSServer: Return partial transcripts
+ Gladia-->>TSServer: Return final transcripts
+ TSServer->>TSServer: Log/process transcripts
+ end
+
+ Caller->>Twilio: End call
+ Twilio->>TSServer: Close WebSocket
+ TSServer->>Gladia: Close WebSocket
+```
+
+---
+
+### 2 — Build the TypeScript WebSocket proxy
+
+The proxy server handles three main tasks:
+
+1. Accept Twilio's base64-encoded μ-law frames.
+2. Base64-decode the payload (the only transformation needed).
+3. Pipe the raw bytes to Gladia and process returned transcripts.
+
+```typescript
+// Type definitions
+export interface TwilioMessage {
+ event: string;
+ media?: {
+ payload: string;
+ };
+}
+
+export interface GladiaMessage {
+ type: string;
+ data?: {
+ is_final: boolean;
+ utterance: {
+ text: string;
+ };
+ };
+}
+
+/**
+ * Processes messages from Twilio, decodes the base64 payload, and forwards to Gladia
+ * @param message The raw message from Twilio
+ * @param gladiaConn The WebSocket connection to Gladia
+ */
+export function processMessage(message: Buffer, gladiaConn: WebSocket): void {
+ try {
+ // Parse the message from Twilio
+ const msg: TwilioMessage = JSON.parse(message.toString());
+
+ // Ignore non-media events
+ if (msg.event !== 'media') {
+ console.log(`Ignoring non-media event: ${msg.event}`);
+ return;
+ }
+
+ // Ensure we have a payload
+ if (!msg.media || !msg.media.payload) {
+ console.log('Missing media payload');
+ return;
+ }
+
+ // Decode the base64 payload to get raw μ-law bytes
+ const mulaw = Buffer.from(msg.media.payload, 'base64');
+
+ // Forward the raw bytes to Gladia
+ gladiaConn.send(mulaw, { binary: true }, (error) => {
+ if (error) {
+ console.error(`Error sending to Gladia: ${error}`);
+ }
+ });
+ } catch (error) {
+ console.error(`Error parsing Twilio message: ${error}`);
+ }
+}
+
+/**
+ * Processes messages from Gladia and logs final transcripts
+ * @param message The raw message from Gladia
+ * @returns The transcript text if final, empty string otherwise
+ */
+export function handleGladia(message: Buffer): string {
+ try {
+ // Parse the message from Gladia
+ const msg: GladiaMessage = JSON.parse(message.toString());
+
+ // Check if this is a final transcript
+ if (msg.type === 'transcript' && msg.data?.is_final) {
+ const transcript = msg.data.utterance.text;
+ console.log(`📝 Transcript: ${transcript}`);
+ return transcript;
+ }
+
+ return '';
+ } catch (error) {
+ console.error(`Error parsing Gladia message: ${error}`);
+ return '';
+ }
+}
+
+// handleWebSocket manages a WebSocket connection between Twilio and Gladia
+wss.on('connection', async (twilioConn: WebSocket, req: http.IncomingMessage) => {
+ const clientInfo = req.socket.remoteAddress || 'unknown';
+ console.log(`🔌 Twilio WebSocket connected from ${clientInfo} on path ${req.url}`);
+
+ try {
+ // Connect to Gladia
+ const gladiaConn = new WebSocket(session.url);
+
+ // Handle connection errors
+ gladiaConn.on('error', (error) => {
+ console.error(`Error with Gladia connection: ${error}`);
+ twilioConn.close();
+ });
+
+ // Wait for Gladia connection to open
+ await new Promise((resolve, reject) => {
+ gladiaConn.on('open', () => {
+ console.log(`Connected to Gladia session ${session.id}`);
+ resolve();
+ });
+ gladiaConn.on('error', reject);
+ });
+
+ // Handle messages from Twilio
+ twilioConn.on('message', (message: Buffer) => {
+ try {
+ processMessage(message, gladiaConn);
+ } catch (error) {
+ console.error(`Error processing Twilio message: ${error}`);
+ }
+ });
+
+ // Handle messages from Gladia
+ gladiaConn.on('message', (message: Buffer) => {
+ try {
+ handleGladia(message);
+ } catch (error) {
+ console.error(`Error processing Gladia message: ${error}`);
+ }
+ });
+
+ // Handle Twilio connection close
+ twilioConn.on('close', () => {
+ console.log(`Twilio connection closed from ${clientInfo}`);
+ gladiaConn.close();
+ });
+
+ // Handle Gladia connection close
+ gladiaConn.on('close', () => {
+ console.log('Gladia connection closed');
+ twilioConn.close();
+ });
+
+ } catch (error) {
+ console.error(`Failed to establish connection to Gladia: ${error}`);
+ twilioConn.close();
+ }
+});
+```
+
+No audio processing libraries needed, no resampling, no CPU overhead, and with efficient async/await for handling connections.
+
+---
+
+### 3 — Tell Twilio to stream audio
+
+Point your Twilio number (or a Voice Application) to a TwiML endpoint like:
+
+```xml
+
+
+
+
+
+
+
+ +14155551234
+
+```
+
+Let's examine each element in this TwiML configuration:
+
+- ``: The root element of any TwiML document. It contains all the TwiML instructions for handling the call.
+
+- ``: This element initiates Twilio's Media Streams feature, which allows streaming of audio in real-time while the call is in progress. It tells Twilio to begin capturing and streaming media before executing the rest of the call flow.
+
+- ``: A child element of `` that configures the media stream:
+ - `url` attribute: Specifies the WebSocket endpoint where Twilio will send the audio data.
+ - The URL must use secure WebSockets (`wss://`).
+ - The domain should be your public domain (e.g., an ngrok URL or a custom domain).
+ - The path (`/media`) must match the WebSocket route in your TypeScript application.
+ - Each call will create a new WebSocket connection to this endpoint.
+
+- ``: After starting the media stream, this element connects the caller to another phone number. During this connection:
+ - The media streaming continues in the background.
+ - Audio from both sides of the conversation is sent to your WebSocket endpoint.
+ - You can replace this with other TwiML verbs like ``, ``, or `` depending on your use case.
+ - The number shown (`+14155551234`) is just an example - replace it with your desired destination.
+
+When a call triggers this TwiML, Twilio immediately opens a secure WebSocket connection to your server's `/media` endpoint and begins streaming audio as 20 ms μ-law frames. Each frame is base64-encoded and sent as a JSON message, which your server then decodes and forwards to Gladia.
+
+---
+
+### 4 — Expose & test
+
+```bash
+# Install dependencies
+npm install
+
+# Build the application
+npm run build
+
+# Run the server (default port 5001)
+npm start
+
+# Or run with development mode
+npm run dev
+
+# Or specify a custom port
+HTTP_PORT=5001 npm start
+
+# Tunnel it with ngrok
+ngrok http 5001
+
+# Or with a custom domain
+ngrok http --domain=your.domain.com 5001
+```
+
+Call your Twilio number and you should see live text scroll instantly:
+
+```
+🛰 Gladia session ID: 3f65…
+🚀 Starting server on 0.0.0.0:5001
+🔌 Twilio WebSocket connected from 42.422.42.4242:12345
+Connected to Gladia session 3f65…
+📝 Transcript: Hello and thank you for calling Acme support.
+📝 Transcript: Sure, I'd be happy to help with your order.
+```
+
+---
+
+### 5 — Next steps
+
+* **Add-ons** – enable diarization, sentiment, keywords, etc., by including the flags when you create the session.
+* **Dual-channel** – Twilio can stream separate channels; Gladia preserves them so you always know who's speaking.
+* **Post-call JSON** – store the session `id` and hit `GET /v2/live/:id` for the full, punctuated transcript when the call ends.
+* **Scale it** – TypeScript/Node.js's event-driven, non-blocking I/O model makes it easy to scale for high loads. Consider deploying with a load balancer for horizontal scaling.
+
+---
+
+### Wrap-up
+
+Because Gladia natively accepts Twilio's μ-law stream, **real-time call transcription is now literally "base64-decode and forward."** Fewer steps, lower CPU, and the same lightning-fast latency. With TypeScript's type safety and Node.js's event-driven architecture, the solution is not only simple but highly maintainable and scalable. Drop this proxy into any TypeScript/Node.js stack and start surfacing live insights from every call. Happy building! 🎙️📝
+
+[1]: https://docs.gladia.io/api-reference/v2/live/init "Initiate a session - Gladia"
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/package-lock.json b/blogs/twilio-solaria-typescript/package-lock.json
new file mode 100644
index 0000000..66df1d2
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/package-lock.json
@@ -0,0 +1,284 @@
+{
+ "name": "twilio-solaria-typescript",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "twilio-solaria-typescript",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "dotenv": "^16.4.5",
+ "ws": "^8.16.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.11.30",
+ "@types/ws": "^8.5.10",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.4.3"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
+ "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.17.45",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.45.tgz",
+ "integrity": "sha512-vO9+E1smq+149wsmmLdM8SKVW7gRzLjfo0mU7kiykhV6rL+GEUhUmW7VywJNSxJHQzt9QBIHEo+3SG4MrFTqbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.14.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ws": {
+ "version": "8.18.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
+ "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ }
+}
diff --git a/blogs/twilio-solaria-typescript/package.json b/blogs/twilio-solaria-typescript/package.json
new file mode 100644
index 0000000..f608d66
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "twilio-solaria-typescript",
+ "version": "1.0.0",
+ "description": "Real-time transcription of Twilio calls using Gladia's Speech-to-Text API",
+ "main": "dist/app/server.js",
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/app/server.js",
+ "dev": "ts-node src/app/server.ts",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [
+ "twilio",
+ "gladia",
+ "transcription",
+ "speech-to-text",
+ "websocket"
+ ],
+ "author": "",
+ "license": "MIT",
+ "dependencies": {
+ "dotenv": "^16.4.5",
+ "ws": "^8.16.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.11.30",
+ "@types/ws": "^8.5.10",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.4.3"
+ }
+}
diff --git a/blogs/twilio-solaria-typescript/src/README.md b/blogs/twilio-solaria-typescript/src/README.md
new file mode 100644
index 0000000..e400d1b
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/src/README.md
@@ -0,0 +1,85 @@
+# Twilio Call Transcription with Gladia (TypeScript Version)
+
+This project implements real-time transcription of Twilio calls using Gladia's Speech-to-Text API and TypeScript, which supports Twilio's μ-law audio format.
+
+## Prerequisites
+
+- **Gladia API key** - Sign up at [app.gladia.io](https://app.gladia.io/)
+- **Twilio account + voice-enabled number**
+- **Node.js 18+**
+- **Public URL** - Use ngrok or a cloud VM to expose your WebSocket endpoint
+
+## Setup
+
+1. **Install Node.js dependencies**:
+ ```bash
+ # Install dependencies
+ npm install
+ ```
+
+2. **Set up environment variables**:
+ Create a `.env` file in the root directory with:
+ ```
+ GLADIA_API_KEY=your_gladia_api_key_here
+ # Optional: HTTP_PORT=5001
+ ```
+
+3. **Configure Twilio**:
+ - Create a TwiML Bin or webhook in your Twilio account at [https://console.twilio.com/us1/develop/twiml-bins](https://console.twilio.com/us1/develop/twiml-bins)
+ - Use the contents of `src/twiml_example.xml` as your TwiML
+ - Replace `your.domain.com` with your actual public domain (the ngrok URL from step 2 in "Running the application" or a custom domain)
+ - Assign this TwiML Bin to your Twilio phone number at [https://console.twilio.com/us1/develop/phone-numbers/manage/incoming](https://console.twilio.com/us1/develop/phone-numbers/manage/incoming)
+
+## Technical Notes
+
+- The server uses Node.js HTTP server with the `ws` library for WebSocket support
+- The application uses async/await for asynchronous operations
+- Error handling follows TypeScript best practices with proper type definitions
+
+## Running the application
+
+1. **Build and start the server**:
+ ```bash
+ # Build the TypeScript code
+ npm run build
+
+ # Run with default port (5001)
+ npm start
+
+ # Or run with development mode
+ npm run dev
+
+ # To specify a custom port
+ HTTP_PORT=5001 npm start
+ ```
+
+2. **Expose your server publicly**:
+ ```bash
+ # Make sure the port matches HTTP_PORT from step 1
+ ngrok http 5001 # you'll get a random ngrok URL
+
+ # For a custom domain
+ ngrok http --domain=your.domain.com 5001
+ ```
+
+3. **Update your TwiML**:
+ - Update the `url` in your TwiML to your ngrok URL (e.g., `wss://your.domain.com/media` or the random ngrok URL)
+
+4. **Test**:
+ - Call your Twilio number
+ - You should see transcripts appearing in your console
+
+## How it works
+
+1. The server connects to Gladia's real-time STT API
+2. When a call comes in, Twilio connects to your WebSocket endpoint
+3. Twilio streams 20ms μ-law frames, which are base64-decoded
+4. The raw μ-law bytes are forwarded to Gladia without any conversion
+5. Gladia returns real-time transcription results
+
+## Next steps
+
+- **Add-ons** – Enable sentiment, keywords, etc.
+- **Dual-channel** – Transcribe both sides of the conversation separately
+- **Post-call JSON** – Get the full transcript when the call ends
+- **Scale it** – Deploy to cloud services with load balancers for high availability
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/src/app/gladiaClient.ts b/blogs/twilio-solaria-typescript/src/app/gladiaClient.ts
new file mode 100644
index 0000000..d81f1bf
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/src/app/gladiaClient.ts
@@ -0,0 +1,87 @@
+import * as https from 'https';
+import { GladiaSession } from './types';
+
+// Constants
+const GLADIA_INIT_URL = 'https://api.gladia.io/v2/live';
+
+/**
+ * Creates a Gladia real-time transcription session
+ * @param apiKey Gladia API key
+ * @returns Promise resolving to a session with ID and WebSocket URL
+ */
+export async function createSession(apiKey: string): Promise {
+ // Define the payload for μ-law, 8-bit, 8 kHz, mono audio
+ const payload = {
+ encoding: 'wav/ulaw',
+ bit_depth: 8,
+ sample_rate: 8000,
+ channels: 1
+ };
+
+ // Convert payload to JSON
+ const body = JSON.stringify(payload);
+
+ // Create and return a promise for the HTTP request
+ return new Promise((resolve, reject) => {
+ // Prepare the request options
+ const options = {
+ method: 'POST',
+ headers: {
+ 'X-Gladia-Key': apiKey,
+ 'Content-Type': 'application/json',
+ 'Content-Length': Buffer.byteLength(body)
+ }
+ };
+
+ // Make the HTTP request
+ const req = https.request(GLADIA_INIT_URL, options, (res) => {
+ // Check for successful status code
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
+ const statusCode = res.statusCode;
+ let responseData = '';
+
+ res.on('data', (chunk) => {
+ responseData += chunk;
+ });
+
+ res.on('end', () => {
+ reject(new Error(`Bad status code: ${statusCode} - ${responseData}`));
+ });
+
+ return;
+ }
+
+ // Collect the response data
+ let responseData = '';
+ res.on('data', (chunk) => {
+ responseData += chunk;
+ });
+
+ // Process the response when it's complete
+ res.on('end', () => {
+ try {
+ const data = JSON.parse(responseData) as GladiaSession;
+ console.log(`🛰 Gladia session ID: ${data.id}`);
+ resolve(data);
+ } catch (error) {
+ reject(new Error(`Failed to decode response: ${error}`));
+ }
+ });
+ });
+
+ // Handle request errors
+ req.on('error', (error) => {
+ reject(new Error(`Session init request failed: ${error}`));
+ });
+
+ // Set timeout (10 seconds)
+ req.setTimeout(10000, () => {
+ req.destroy();
+ reject(new Error('Request timed out'));
+ });
+
+ // Send the request body
+ req.write(body);
+ req.end();
+ });
+}
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/src/app/handlers.ts b/blogs/twilio-solaria-typescript/src/app/handlers.ts
new file mode 100644
index 0000000..04eb8a9
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/src/app/handlers.ts
@@ -0,0 +1,62 @@
+import { WebSocket } from 'ws';
+import { TwilioMessage, GladiaMessage } from './types';
+
+/**
+ * Processes messages from Twilio, decodes the base64 payload, and forwards to Gladia
+ * @param message The raw message from Twilio
+ * @param gladiaConn The WebSocket connection to Gladia
+ */
+export function processMessage(message: Buffer, gladiaConn: WebSocket): void {
+ try {
+ // Parse the message from Twilio
+ const msg: TwilioMessage = JSON.parse(message.toString());
+
+ // Ignore non-media events
+ if (msg.event !== 'media') {
+ console.log(`Ignoring non-media event: ${msg.event}`);
+ return;
+ }
+
+ // Ensure we have a payload
+ if (!msg.media || !msg.media.payload) {
+ console.log('Missing media payload');
+ return;
+ }
+
+ // Decode the base64 payload to get raw μ-law bytes
+ const mulaw = Buffer.from(msg.media.payload, 'base64');
+
+ // Forward the raw bytes to Gladia
+ gladiaConn.send(mulaw, { binary: true }, (error) => {
+ if (error) {
+ console.error(`Error sending to Gladia: ${error}`);
+ }
+ });
+ } catch (error) {
+ console.error(`Error parsing Twilio message: ${error}`);
+ }
+}
+
+/**
+ * Processes messages from Gladia and logs final transcripts
+ * @param message The raw message from Gladia
+ * @returns The transcript text if final, empty string otherwise
+ */
+export function handleGladia(message: Buffer): string {
+ try {
+ // Parse the message from Gladia
+ const msg: GladiaMessage = JSON.parse(message.toString());
+
+ // Check if this is a final transcript
+ if (msg.type === 'transcript' && msg.data?.is_final) {
+ const transcript = msg.data.utterance.text;
+ console.log(`📝 Transcript: ${transcript}`);
+ return transcript;
+ }
+
+ return '';
+ } catch (error) {
+ console.error(`Error parsing Gladia message: ${error}`);
+ return '';
+ }
+}
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/src/app/server.ts b/blogs/twilio-solaria-typescript/src/app/server.ts
new file mode 100644
index 0000000..8624af4
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/src/app/server.ts
@@ -0,0 +1,140 @@
+import * as http from 'http';
+import * as dotenv from 'dotenv';
+import { WebSocket, WebSocketServer } from 'ws';
+import { createSession } from './gladiaClient';
+import { processMessage, handleGladia } from './handlers';
+import { GladiaSession } from './types';
+
+// Load environment variables
+dotenv.config();
+
+// Constants
+const DEFAULT_PORT = '5001';
+
+// Global variables
+let gladiaAPIKey: string;
+let session: GladiaSession;
+
+async function main() {
+ // Configure logging
+ console.log = (...args) => {
+ const date = new Date().toISOString();
+ process.stdout.write(`${date} ${args.join(' ')}\n`);
+ };
+
+ // Get API key
+ gladiaAPIKey = process.env.GLADIA_API_KEY || '';
+ if (!gladiaAPIKey) {
+ console.error('GLADIA_API_KEY environment variable is required');
+ process.exit(1);
+ }
+
+ // Get port
+ const port = process.env.HTTP_PORT || DEFAULT_PORT;
+
+ try {
+ // Initialize Gladia session
+ session = await createSession(gladiaAPIKey);
+ } catch (error) {
+ console.error(`Failed to create initial Gladia session: ${error}`);
+ process.exit(1);
+ }
+
+ // Create HTTP server
+ const server = http.createServer((req, res) => {
+ if (req.url === '/health') {
+ // Health check endpoint
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({
+ status: 'ok',
+ service: 'twilio-gladia-transcription'
+ }));
+ } else {
+ // For regular HTTP requests to root, return a simple info page
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
+ res.end('Twilio-Gladia Transcription Server\n\nAvailable endpoints:\n- /media (WebSocket): Connect Twilio Media Streams\n- /health (HTTP): Health check endpoint');
+ }
+ });
+
+ // Create WebSocket server
+ const wss = new WebSocketServer({ server });
+
+ // Handle WebSocket connections
+ wss.on('connection', async (twilioConn: WebSocket, req: http.IncomingMessage) => {
+ const clientInfo = req.socket.remoteAddress || 'unknown';
+ console.log(`🔌 Twilio WebSocket connected from ${clientInfo} on path ${req.url}`);
+
+ try {
+ // Connect to Gladia
+ const gladiaConn = new WebSocket(session.url);
+
+ // Handle connection errors
+ gladiaConn.on('error', (error) => {
+ console.error(`Error with Gladia connection: ${error}`);
+ twilioConn.close();
+ });
+
+ // Wait for Gladia connection to open
+ await new Promise((resolve, reject) => {
+ gladiaConn.on('open', () => {
+ console.log(`Connected to Gladia session ${session.id}`);
+ resolve();
+ });
+ gladiaConn.on('error', reject);
+ });
+
+ // Handle messages from Twilio
+ twilioConn.on('message', (message: Buffer) => {
+ try {
+ processMessage(message, gladiaConn);
+ } catch (error) {
+ console.error(`Error processing Twilio message: ${error}`);
+ }
+ });
+
+ // Handle messages from Gladia
+ gladiaConn.on('message', (message: Buffer) => {
+ try {
+ handleGladia(message);
+ } catch (error) {
+ console.error(`Error processing Gladia message: ${error}`);
+ }
+ });
+
+ // Handle Twilio connection close
+ twilioConn.on('close', () => {
+ console.log(`Twilio connection closed from ${clientInfo}`);
+ gladiaConn.close();
+ });
+
+ // Handle Gladia connection close
+ gladiaConn.on('close', () => {
+ console.log('Gladia connection closed');
+ twilioConn.close();
+ });
+
+ } catch (error) {
+ console.error(`Failed to establish connection to Gladia: ${error}`);
+ twilioConn.close();
+ }
+ });
+
+ // Start the server
+ server.listen(parseInt(port), '0.0.0.0', () => {
+ console.log(`🚀 Starting server on 0.0.0.0:${port}`);
+ });
+
+ // Handle graceful shutdown
+ process.on('SIGINT', () => {
+ console.log('Server shutting down...');
+ server.close(() => {
+ console.log('Server stopped');
+ process.exit(0);
+ });
+ });
+}
+
+main().catch(error => {
+ console.error(`Fatal error: ${error}`);
+ process.exit(1);
+});
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/src/app/types.ts b/blogs/twilio-solaria-typescript/src/app/types.ts
new file mode 100644
index 0000000..66f687f
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/src/app/types.ts
@@ -0,0 +1,23 @@
+// Type definitions
+
+export interface GladiaSession {
+ id: string;
+ url: string;
+}
+
+export interface TwilioMessage {
+ event: string;
+ media?: {
+ payload: string;
+ };
+}
+
+export interface GladiaMessage {
+ type: string;
+ data?: {
+ is_final: boolean;
+ utterance: {
+ text: string;
+ };
+ };
+}
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/src/env_setup.txt b/blogs/twilio-solaria-typescript/src/env_setup.txt
new file mode 100644
index 0000000..d7403cb
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/src/env_setup.txt
@@ -0,0 +1,23 @@
+Before running the TypeScript application, create a .env file in the root directory with the following contents:
+
+GLADIA_API_KEY=your_gladia_api_key_here
+# Optional: Set a custom port (default is 5001)
+# HTTP_PORT=5001
+
+You need to replace 'your_gladia_api_key_here' with your actual Gladia API key from https://app.gladia.io/
+
+Environment variables can also be set directly in your terminal:
+
+```bash
+# Linux/macOS
+export GLADIA_API_KEY=your_gladia_api_key_here
+export HTTP_PORT=5001
+
+# Windows Command Prompt
+set GLADIA_API_KEY=your_gladia_api_key_here
+set HTTP_PORT=5001
+
+# Windows PowerShell
+$env:GLADIA_API_KEY="your_gladia_api_key_here"
+$env:HTTP_PORT="5001"
+```
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/src/twiml_example.xml b/blogs/twilio-solaria-typescript/src/twiml_example.xml
new file mode 100644
index 0000000..c3b0b2b
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/src/twiml_example.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+ +14155551234
+
\ No newline at end of file
diff --git a/blogs/twilio-solaria-typescript/tsconfig.json b/blogs/twilio-solaria-typescript/tsconfig.json
new file mode 100644
index 0000000..50f44ac
--- /dev/null
+++ b/blogs/twilio-solaria-typescript/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2022"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/blogs/vonage-solaria-python-fastapi/.gitignore b/blogs/vonage-solaria-python-fastapi/.gitignore
new file mode 100644
index 0000000..85c55eb
--- /dev/null
+++ b/blogs/vonage-solaria-python-fastapi/.gitignore
@@ -0,0 +1,2 @@
+.env
+.venv
diff --git a/blogs/vonage-solaria-python-fastapi/blog.md b/blogs/vonage-solaria-python-fastapi/blog.md
new file mode 100644
index 0000000..59d57d9
--- /dev/null
+++ b/blogs/vonage-solaria-python-fastapi/blog.md
@@ -0,0 +1,311 @@
+## How to Transcribe Vonage Calls in Real Time with FastAPI and Python & Gladia
+
+Vonage's Voice APIs deliver audio via WebSockets that can be configured for various formats. Gladia's real-time Speech-to-Text (STT) API is flexible enough to handle this audio with minimal processing, enabling you to build real-time transcription with sub-300 ms latency.
+
+Gladia's `/v2/live` endpoint lets you specify multiple audio format configurations to match what Vonage delivers ([Gladia][1]).
+
+---
+
+### Prerequisites
+
+| What you need | Why |
+| ----------------------------------------- | -------------------------------------------------------------------- |
+| **Gladia API key** | Sign up & copy from the dashboard. |
+| **Vonage account + voice-enabled number** | To receive / place calls. |
+| **Python 3.12+** | We'll use `fastapi`, `uvicorn`, `websockets`, and `requests`. |
+| **Public URL** | Expose a WebSocket endpoint with ngrok or a cloud VM. |
+| **Audio format from Vonage** | Typically Linear PCM (L16) but configurable in Vonage's NCCO. |
+
+---
+
+### 1 — Initiate a Gladia live session
+
+```python
+import os
+import requests
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/pcm", # For Vonage's Linear PCM
+ "sample_rate": 8000, # Standard telephony sample rate
+ "channels": 1,
+ }
+
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ print("🛰 Gladia session ID:", data["id"])
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+```
+
+> **Note on audio format:** Vonage WebSockets typically send L16 PCM audio by default. Gladia can process this directly or you can configure Vonage to send other formats.
+
+---
+
+### System Architecture Flow
+
+Here's how data flows through the system:
+
+```mermaid
+sequenceDiagram
+ participant Caller
+ participant Vonage
+ participant FastAPI as FastAPI WebSocket Server
+ participant Gladia as Gladia API
+
+ FastAPI->>Gladia: Initialize session (POST /v2/live)
+ Gladia-->>FastAPI: Return WebSocket URL
+
+ Caller->>Vonage: Make phone call
+ Vonage->>FastAPI: Connect to WebSocket (/media)
+
+ loop Audio Streaming
+ Vonage->>FastAPI: Send audio chunks (base64 encoded)
+ FastAPI->>FastAPI: Decode base64
+ FastAPI->>Gladia: Forward audio bytes
+ Gladia->>Gladia: Process speech
+ Gladia-->>FastAPI: Return partial transcripts
+ Gladia-->>FastAPI: Return final transcripts
+ FastAPI->>FastAPI: Log/process transcripts
+ end
+
+ Caller->>Vonage: End call
+ Vonage->>FastAPI: Close WebSocket
+ FastAPI->>Gladia: Close WebSocket
+```
+
+---
+
+### 2 — Build the Python WebSocket proxy
+
+The proxy does **three** things:
+
+1. Accept Vonage's base64-encoded audio frames.
+2. Base64-decode the audio data.
+3. Pipe the bytes straight to Gladia and print transcripts as they come back.
+
+```python
+# server.py
+import os
+import base64
+import json
+import logging
+import asyncio
+import websockets
+import requests
+from fastapi import FastAPI, WebSocket
+from dotenv import load_dotenv
+import uvicorn
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+# Convert HTTP_PORT to integer
+HTTP_PORT = int(os.getenv("HTTP_PORT", "5000"))
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = FastAPI()
+
+# Store gladia session information
+gladia_session = {
+ "id": None,
+ "url": None,
+}
+
+def create_session():
+ """Initialize a Gladia real-time transcription session."""
+ payload = {
+ "encoding": "wav/pcm", # For Vonage's Linear PCM
+ "sample_rate": 8000, # Standard telephony sample rate
+ "channels": 1,
+ }
+
+ try:
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+ r.raise_for_status()
+ data = r.json()
+ logger.info("🛰 Gladia session ID: %s", data["id"])
+ gladia_session["id"] = data["id"]
+ gladia_session["url"] = data["url"]
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+ except requests.exceptions.RequestException as e:
+ logger.error("Failed to create Gladia session: %s", e)
+ raise
+
+# Create initial Gladia session
+try:
+ create_session()
+except Exception as e:
+ logger.error("Failed to create initial Gladia session: %s", e)
+ raise
+
+@app.get("/health")
+def health_check():
+ return {"status": "ok", "service": "vonage-gladia-transcription"}
+
+@app.websocket("/media")
+async def media(websocket: WebSocket):
+ """Handle incoming WebSocket connections from Vonage."""
+ await websocket.accept()
+ client = websocket.client
+ client_info = f"{client.host}:{client.port}"
+ logger.info(f"🔌 Vonage WebSocket connected from {client_info}")
+
+ await handle_websocket(websocket)
+
+async def process_message(ws_message, gladia_ws):
+ """Process a message asynchronously."""
+ try:
+ data = json.loads(ws_message)
+
+ # Vonage WebSocket sends audio data with a different structure
+ # Check if this is audio data from Vonage
+ if "content" in data and data.get("content", {}).get("encoding") == "audio/l16;rate=8000":
+ # Extract audio data and convert from base64
+ audio_payload = base64.b64decode(data["content"]["data"])
+
+ # Send the audio data to Gladia
+ await gladia_ws.send(audio_payload)
+ else:
+ logger.debug(f"Non-audio event: {json.dumps(data)[:100]}...")
+ return # ignore non-audio events
+
+ # Check for transcripts
+ try:
+ # Try to get transcripts (non-blocking)
+ while True:
+ resp = await asyncio.wait_for(gladia_ws.recv(), 0.001)
+ handle_gladia(resp)
+ except asyncio.TimeoutError:
+ pass # No transcript available yet
+ except Exception as e:
+ logger.error(f"Error processing message: {e}")
+
+def handle_gladia(msg):
+ """Process transcription results from Gladia."""
+ m = json.loads(msg)
+ if m["type"] == "transcript" and m["data"]["is_final"]:
+ transcript = m["data"]["utterance"]["text"]
+ print(f"📝 Transcript: {transcript}")
+ return transcript
+```
+
+With proper asynchronous handling for high performance.
+
+---
+
+### 3 — Configure Vonage to stream audio
+
+Create an NCCO (Nexmo Call Control Object) in your Vonage dashboard like:
+
+```xml
+
+
+
+ wss://jl.mydomain.com/media
+ audio/l16;rate=8000
+
+
+
+ phone
+ 14155551234
+
+
+
+```
+
+Let's examine each element in this NCCO configuration:
+
+- ``: The root element of any Vonage NCCO document. It contains all the instructions for handling the call.
+
+- ``: This element configures a WebSocket connection for streaming audio:
+ - ``: Specifies the WebSocket endpoint where Vonage will send the audio data.
+ - The URL must use secure WebSockets (`wss://`).
+ - The domain should be your public domain (e.g., an ngrok URL or a custom domain).
+ - The path (`/media`) must match the WebSocket route in your FastAPI application.
+ - Each call will create a new WebSocket connection to this endpoint.
+ - ``: Specifies the audio format that Vonage will send (Linear PCM at 8kHz in this case).
+
+- ``: This element connects the caller to another phone number:
+ - During this connection, the media streaming continues in the background.
+ - Audio from both sides of the conversation is sent to your WebSocket endpoint.
+ - The number shown (`14155551234`) is just an example - replace it with your desired destination.
+
+When a call triggers this NCCO, Vonage immediately opens a secure WebSocket connection to your server's `/media` endpoint and begins streaming audio. Each audio chunk is base64-encoded and sent as a JSON message, which your server then decodes and forwards to Gladia.
+
+---
+
+### 4 — Expose & test
+
+```bash
+# Install dependencies
+pip install -r requirements.txt
+
+# Run the proxy (default port 5000)
+python server.py
+
+# Or specify a custom port
+HTTP_PORT=5001 python server.py
+
+# Tunnel it with ngrok
+ngrok http 5000
+
+# Or with a custom domain
+ngrok http --domain=your.domain.com 5000
+```
+
+Call your Vonage number and you should see live text scroll instantly:
+
+```
+🛰 Gladia session ID: 3f65…
+🚀 Starting server on 0.0.0.0:5000
+🔌 Vonage WebSocket connected from 54.174.99.133:12345
+📝 Transcript: Hello and thank you for calling Acme support.
+📝 Transcript: Sure, I'd be happy to help with your order.
+```
+
+---
+
+### 5 — Next steps
+
+* **Add-ons** – enable diarization, sentiment, keywords, etc., by including the flags when you create the session.
+* **Dual-channel** – Vonage can stream separate channels; Gladia preserves them so you always know who's speaking.
+* **Post-call JSON** – store the session `id` and hit `GET /v2/live/:id` for the full, punctuated transcript when the call ends.
+* **Scale it** – FastAPI with Uvicorn is production-ready, but for even higher loads, consider deploying with Gunicorn as a process manager.
+
+---
+
+### Wrap-up
+
+Real-time call transcription with Vonage and Gladia is simple and straightforward. With just a decode and forward approach, you can quickly add speech-to-text capabilities to your Vonage applications. The minimal processing needed means lower CPU usage and lightning-fast latency. Drop this proxy into any Python stack and start surfacing live insights from every call. Happy building! 🎙️📝
+
+[1]: https://docs.gladia.io/api-reference/v2/live/init "Initiate a session - Gladia"
\ No newline at end of file
diff --git a/blogs/vonage-solaria-python-fastapi/src/README.md b/blogs/vonage-solaria-python-fastapi/src/README.md
new file mode 100644
index 0000000..d82dff8
--- /dev/null
+++ b/blogs/vonage-solaria-python-fastapi/src/README.md
@@ -0,0 +1,96 @@
+# Vonage Call Transcription with Gladia
+
+This project implements real-time transcription of Vonage calls using Gladia's Speech-to-Text API, which natively supports audio format conversion from Vonage's WebSocket streams.
+
+## Prerequisites
+
+- **Gladia API key** - Sign up at [app.gladia.io](https://app.gladia.io/)
+- **Vonage account + voice-enabled number**
+- **Python 3.8+**
+- **Public URL** - Use ngrok or a cloud VM to expose your WebSocket endpoint
+
+## Setup
+
+1. **Install dependencies**:
+ ```bash
+ # Install pyenv (if not already installed)
+ # macOS (using Homebrew)
+ brew install pyenv
+
+ # Linux
+ curl https://pyenv.run | bash
+
+ # Setup pyenv
+ pyenv install 3.12
+ pyenv local 3.12
+
+ pyenv virtualenv 3.12 vonage-gladia-python
+
+ # Install Python dependencies
+ pip install -r requirements.txt
+ ```
+
+2. **Set up environment variables**:
+ Create a `.env` file in the same directory as `server.py` with:
+ ```
+ GLADIA_API_KEY=your_gladia_api_key_here
+ ```
+
+3. **Configure Vonage**:
+ - Create a new application in your Vonage dashboard at [https://dashboard.nexmo.com/applications](https://dashboard.nexmo.com/applications)
+ - Create a new voice application or use an existing one
+ - Link your Vonage phone number to this application
+ - In your answer URL configuration, use the content of `vonage_example.xml` as your NCCO (Nexmo Call Control Object) (https://jl.gladia.dev/media)
+ - Replace `jl.mydomain.com` with your actual public domain (the ngrok URL from step 2 in "Running the application" or a custom domain)
+
+## Technical Notes
+
+- The server uses FastAPI with native WebSocket support
+- Uvicorn is used as the ASGI server
+- The application is fully asynchronous
+- Unlike Twilio which uses μ-law, Vonage typically sends linear PCM audio (audio/l16)
+
+## Running the application
+
+1. **Start the server**:
+ ```bash
+ # Default port (5000)
+ python server.py
+
+ # Or specify a custom port
+ HTTP_PORT=5001 python server.py
+ ```
+
+2. **Expose your server publicly**:
+ ```bash
+ # Make sure the port matches HTTP_PORT from step 1
+ ngrok http 5000 # you'll get a ngrok assigned random URL
+
+ # If you used a custom port in step 1, use the same port here
+ ngrok http 5001
+
+ # for a custom domain
+ ngrok http --domain=jl.mydomain.com 5001
+ ```
+
+3. **Update your NCCO**:
+ - Update the `uri` in your NCCO to your ngrok URL (e.g., `wss://my.domain.com/media` or the random URL assigned by ngrok)
+
+4. **Test**:
+ - Call your Vonage number
+ - You should see transcripts appearing in your console
+
+## How it works
+
+1. The server connects to Gladia's real-time STT API
+2. When a call comes in, Vonage connects to your WebSocket endpoint
+3. Vonage streams audio frames (typically linear PCM), which are base64-decoded
+4. The audio data is forwarded to Gladia with minimal processing
+5. Gladia returns real-time transcription results
+
+## Next steps
+
+- **Add-ons** – enable diarization, sentiment, keywords, etc.
+- **Dual-channel** – Transcribe both sides of the conversation separately
+- **Post-call JSON** – Get the full transcript when the call ends
+- **Scale it** – Use Gunicorn as a process manager for even higher loads
\ No newline at end of file
diff --git a/blogs/vonage-solaria-python-fastapi/src/env_setup.txt b/blogs/vonage-solaria-python-fastapi/src/env_setup.txt
new file mode 100644
index 0000000..f950bb8
--- /dev/null
+++ b/blogs/vonage-solaria-python-fastapi/src/env_setup.txt
@@ -0,0 +1,7 @@
+Before running the application, create a .env file in the same directory with the following contents:
+
+GLADIA_API_KEY=your_gladia_api_key_here
+# Optional: Set a custom port (default is 5000)
+# HTTP_PORT=5001
+
+You need to replace 'your_gladia_api_key_here' with your actual Gladia API key from https://app.gladia.io/
\ No newline at end of file
diff --git a/blogs/vonage-solaria-python-fastapi/src/requirements.txt b/blogs/vonage-solaria-python-fastapi/src/requirements.txt
new file mode 100644
index 0000000..54fbbba
--- /dev/null
+++ b/blogs/vonage-solaria-python-fastapi/src/requirements.txt
@@ -0,0 +1,6 @@
+fastapi==0.108.0
+uvicorn==0.25.0
+websockets==15.0.1
+requests==2.31.0
+python-dotenv==1.0.1
+greenlet==3.2.1
\ No newline at end of file
diff --git a/blogs/vonage-solaria-python-fastapi/src/server.py b/blogs/vonage-solaria-python-fastapi/src/server.py
new file mode 100644
index 0000000..42e517c
--- /dev/null
+++ b/blogs/vonage-solaria-python-fastapi/src/server.py
@@ -0,0 +1,449 @@
+import os
+import base64
+import json
+import logging
+import asyncio
+import websockets
+import requests
+import time
+import random
+from fastapi import FastAPI, WebSocket, Response, HTTPException, Request
+from dotenv import load_dotenv
+import uvicorn
+
+# Load environment variables
+load_dotenv()
+
+GLADIA_KEY = os.getenv("GLADIA_API_KEY")
+GLADIA_INIT = "https://api.gladia.io/v2/live"
+# Convert HTTP_PORT to integer
+HTTP_PORT = int(os.getenv("HTTP_PORT", "5000"))
+
+# Configure logging
+logging.basicConfig(
+ level=logging.WARNING, # Change to WARNING to reduce standard logs
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger(__name__)
+
+# Create a special logger just for transcription output
+transcript_logger = logging.getLogger("transcription")
+transcript_logger.setLevel(logging.INFO)
+transcript_handler = logging.StreamHandler()
+transcript_handler.setFormatter(logging.Formatter('📝 TRANSCRIPTION: %(message)s'))
+transcript_logger.addHandler(transcript_handler)
+
+# Silence uvicorn access logs
+logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
+logging.getLogger("uvicorn.error").setLevel(logging.ERROR)
+
+app = FastAPI()
+
+# Store gladia session information
+gladia_session = {
+ "id": None,
+ "url": None,
+ "last_init_attempt": 0,
+ "retry_count": 0
+}
+
+# Maximum number of retries
+MAX_RETRIES = 5
+# Initial delay in seconds
+INITIAL_RETRY_DELAY = 1
+# Maximum delay in seconds
+MAX_RETRY_DELAY = 60
+
+def create_session(force=False):
+ """Initialize a Gladia real-time transcription session with exponential backoff retry."""
+ # If we already have a session and not forcing a new one, return it
+ if gladia_session["url"] and not force:
+ return gladia_session["url"]
+
+ # Rate limiting - wait at least 2 seconds between attempts
+ current_time = time.time()
+ time_since_last_attempt = current_time - gladia_session["last_init_attempt"]
+ if time_since_last_attempt < 2 and gladia_session["last_init_attempt"] > 0:
+ logger.debug(f"Rate limiting: waiting before retrying Gladia session creation")
+ time.sleep(2 - time_since_last_attempt)
+
+ gladia_session["last_init_attempt"] = time.time()
+
+ # If we've tried too many times recently, back off
+ if gladia_session["retry_count"] >= MAX_RETRIES:
+ delay = min(MAX_RETRY_DELAY, INITIAL_RETRY_DELAY * (2 ** (gladia_session["retry_count"] - MAX_RETRIES)))
+ # Add jitter
+ delay = delay * (0.5 + random.random())
+ logger.warning(f"Too many Gladia session creation attempts. Backing off for {delay:.2f} seconds")
+ time.sleep(delay)
+
+ # Try all possible API formats that might work with Gladia
+ # Format 1: Current v2 API format
+ payload = {
+ "sample_rate": 8000,
+ "encoding": "wav/pcm",
+ "sample_rate": 8000,
+ "bit_depth": 16,
+ "channels": 1,
+
+ }
+
+ logger.debug(f"Gladia API payload: {json.dumps(payload)}")
+ print(f"Gladia API payload: {json.dumps(payload)}")
+
+ try:
+ logger.debug("Attempting to create Gladia session...")
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+
+ # If the request fails, try alternate format
+ if r.status_code >= 400:
+ print(f"First attempt failed with status {r.status_code}, trying alternate format {r.text}")
+ logger.warning(f"First attempt failed with status {r.status_code}, trying alternate format")
+ # Format 2: Legacy format
+ payload = {
+ "encoding": "wav/pcm",
+ "sample_rate": 8000,
+ "bit_depth": 16,
+ "channels": 1
+ }
+
+ logger.debug(f"Trying alternate payload: {json.dumps(payload)}")
+
+ r = requests.post(
+ GLADIA_INIT,
+ json=payload,
+ headers={
+ "X-Gladia-Key": GLADIA_KEY,
+ "Content-Type": "application/json",
+ },
+ timeout=10,
+ )
+
+ # Log the response for debugging
+ logger.debug(f"Gladia API response status: {r.status_code}")
+
+ try:
+ resp_json = r.json()
+ logger.debug(f"Gladia API response: {json.dumps(resp_json)}")
+ except:
+ logger.warning(f"Could not parse Gladia API response as JSON: {r.text[:200]}")
+
+ r.raise_for_status()
+ data = r.json()
+ session_id = data["id"]
+ logger.debug(f"Created Gladia session ID: {session_id}")
+ gladia_session["id"] = session_id
+ gladia_session["url"] = data["url"]
+ gladia_session["retry_count"] = 0 # Reset retry count on success
+ return data["url"] # wss://api.gladia.io/v2/live?token=…
+ except requests.exceptions.RequestException as e:
+ gladia_session["retry_count"] += 1
+ logger.error(f"Failed to create Gladia session (attempt {gladia_session['retry_count']}): {e}")
+ # Return None to indicate failure
+ return None
+
+
+# Try to create initial Gladia session, but don't fail if it doesn't work
+try:
+ create_session()
+except Exception as e:
+ logger.error(f"Failed to create initial Gladia session: {e}")
+ print("Server will continue to run and attempt to create session when needed.")
+
+
+def handle_gladia(msg):
+ """Process transcription results from Gladia."""
+ try:
+ m = json.loads(msg)
+ if m["type"] == "transcript":
+ is_final = m["data"].get("is_final", False)
+ transcript = m["data"]["utterance"]["text"]
+
+ if is_final and transcript.strip():
+ # Only log final transcriptions with content
+ transcript_logger.info(f"{transcript}")
+ return transcript
+ return None
+ except Exception as e:
+ logger.error(f"Error handling Gladia response: {e}")
+ return None
+
+
+# Add a health check endpoint
+@app.get("/health")
+def health_check():
+ return {"status": "ok", "service": "vonage-gladia-transcription"}
+
+
+# Add a route to serve the NCCO XML
+@app.get("/answer")
+async def answer():
+ try:
+ with open("vonage_example.xml", "r") as file:
+ ncco_content = file.read()
+ logger.debug("Serving NCCO XML")
+ return Response(content=ncco_content, media_type="application/xml")
+ except Exception as e:
+ logger.error(f"Error serving NCCO XML: {e}")
+ return Response(content="Error serving NCCO", status_code=500)
+
+
+# Add a JSON format NCCO endpoint which might be more reliable
+@app.get("/answer.json")
+async def answer_json():
+ try:
+ # Create NCCO in JSON format - proper Vonage format
+ ncco = [
+ {
+ "action": "talk",
+ "text": "You are now being connected for transcription."
+ },
+ {
+ "action": "connect",
+ "from": "12013775364",
+ "endpoint": [
+ {
+ "type": "websocket",
+ "uri": "wss://jl.gladia.dev/media",
+ "content-type": "audio/l16;rate=8000"
+ }
+ ],
+ "eventUrl": ["https://jl.gladia.dev/events"]
+ }
+ ]
+ logger.debug(f"Serving NCCO JSON")
+ return ncco
+ except Exception as e:
+ logger.error(f"Error serving NCCO JSON: {e}")
+ return {"error": "Error serving NCCO"}, 500
+
+
+# Event webhook to receive Vonage call events
+@app.post("/events")
+async def events(request: Request):
+ try:
+ data = await request.json()
+ logger.debug(f"Received Vonage event: {json.dumps(data)}")
+ return {"status": "ok"}
+ except Exception as e:
+ logger.error(f"Error processing Vonage event: {e}")
+ return {"error": "Error processing event"}, 500
+
+
+@app.websocket("/media")
+async def media(websocket: WebSocket):
+ """Handle incoming WebSocket connections from Vonage."""
+ await websocket.accept()
+ client = websocket.client
+ client_info = f"{client.host}:{client.port}"
+ logger.info(f"🔌 Vonage WebSocket connected from {client_info}")
+
+ # Tell the caller we've connected
+ try:
+ await websocket.send_text(json.dumps({
+ "action": "text",
+ "text": "WebSocket connection established. Starting transcription."
+ }))
+ except Exception as e:
+ logger.error(f"Failed to send initial message: {e}")
+
+ await handle_websocket(websocket)
+
+
+@app.websocket("/{remaining_path:path}")
+async def catch_all_websocket(websocket: WebSocket, remaining_path: str):
+ """Catch-all handler for WebSocket connections."""
+ await websocket.accept()
+ logger.info(f"🔌 Catch-all WebSocket connected to /{remaining_path}")
+
+ await handle_websocket(websocket)
+
+
+async def process_message(ws_message, gladia_ws, websocket):
+ """Process a message asynchronously."""
+ try:
+ # Parse the message
+ data = json.loads(ws_message)
+ msg_type = data.get("event", "unknown")
+ logger.debug(f"Received Vonage WebSocket message: {msg_type}")
+
+ # Debug the raw message structure
+ logger.debug(f"Message structure keys: {list(data.keys())}")
+
+ # Handle different message types
+ if msg_type == "websocket:connected":
+ # Connection confirmation message
+ logger.debug("WebSocket connection established with Vonage")
+
+ # Send a response to confirm the connection
+ await websocket.send_text(json.dumps({
+ "action": "speech",
+ "text": "Connection established. You can start speaking."
+ }))
+ return
+
+ elif "content" in data and "data" in data.get("content", {}):
+ # This is audio data - extract and send to Gladia
+ encoding = data.get("content", {}).get("encoding", "")
+ logger.debug(f"Received audio data with encoding: {encoding}")
+ logger.debug(f"Content structure: {list(data['content'].keys())}")
+
+ # Debug the base64 data (first 20 chars)
+ b64_data = data["content"]["data"]
+ logger.debug(f"Base64 data (first 20 chars): {b64_data[:20]}")
+ logger.debug(f"Base64 data length: {len(b64_data)}")
+
+ try:
+ # Decode the base64 data
+ audio_payload = base64.b64decode(b64_data)
+ logger.debug(f"Decoded audio length: {len(audio_payload)} bytes")
+
+ # Send to Gladia and log the result
+ logger.debug(f"Sending {len(audio_payload)} bytes to Gladia WebSocket")
+ await gladia_ws.send(audio_payload)
+ logger.debug("Successfully sent audio data to Gladia")
+ except Exception as e:
+ logger.error(f"Error sending audio to Gladia: {e}")
+
+ # Always send acknowledgment back
+ try:
+ await websocket.send_text(json.dumps({"event": "ack"}))
+ except Exception as e:
+ logger.error(f"Error sending ack: {e}")
+ else:
+ # Log other message types for debugging
+ logger.debug(f"Received non-audio message: {json.dumps(data)[:200]}...")
+
+ except Exception as e:
+ logger.error(f"Error processing message: {e}")
+
+ # Check for transcripts from Gladia - this should happen after each message is processed
+ try:
+ for _ in range(10): # Try up to 10 times to get transcripts
+ try:
+ resp = await asyncio.wait_for(gladia_ws.recv(), 0.05) # Increase timeout slightly
+ logger.debug(f"Got response from Gladia: {resp[:200]}")
+ transcript = handle_gladia(resp)
+ if transcript:
+ # Send transcript back to Vonage
+ logger.debug(f"Sending transcript to caller: {transcript}")
+ await websocket.send_text(json.dumps({
+ "action": "speech",
+ "text": f"I heard: {transcript}"
+ }))
+ except asyncio.TimeoutError:
+ break # No more transcripts available
+ except Exception as e:
+ logger.error(f"Error handling transcripts: {e}")
+
+
+async def handle_websocket(websocket: WebSocket):
+ """Handle a WebSocket connection asynchronously."""
+ gladia_ws = None
+
+ # Try to create or get a Gladia session
+ for attempt in range(3): # Try up to 3 times for this connection
+ session_url = create_session()
+ if session_url:
+ break
+ logger.warning(f"Failed to get Gladia session on attempt {attempt+1}/3, retrying...")
+ await asyncio.sleep(2 * (attempt + 1)) # Incremental backoff
+
+ if not session_url:
+ logger.error("Could not establish Gladia session after multiple attempts")
+ await websocket.close(1011, "Could not establish Gladia session")
+ return
+
+ try:
+ logger.debug("Connecting to Gladia session...")
+ logger.debug(f"Gladia WebSocket URL: {session_url}")
+ gladia_ws = await websockets.connect(session_url)
+ logger.debug(f"Connected to Gladia session {gladia_session['id']}")
+
+ # Send initial message to let Vonage know we're ready
+ await websocket.send_text(json.dumps({
+ "event": "connected",
+ "status": "ready"
+ }))
+ logger.debug("Sent ready message to Vonage")
+
+ while True:
+ try:
+ logger.debug("Waiting for incoming WebSocket message from Vonage...")
+ # Handle both text and binary messages
+ message = await websocket.receive()
+ logger.debug(f"Received message type: {message.get('type', 'unknown')}")
+
+ if "text" in message:
+ # Process text message
+ text_data = message["text"]
+ logger.debug(f"Received text message of length {len(text_data)}")
+ await process_message(text_data, gladia_ws, websocket)
+
+ elif "bytes" in message:
+ # Process binary message - this is likely audio data
+ binary_data = message["bytes"]
+ logger.debug(f"Received binary message of length {len(binary_data)}")
+
+ # Send directly to Gladia
+ logger.debug(f"Sending {len(binary_data)} bytes directly to Gladia")
+ await gladia_ws.send(binary_data)
+
+ # Check for transcripts from Gladia
+ try:
+ for _ in range(3): # Try a few times to get transcripts
+ try:
+ resp = await asyncio.wait_for(gladia_ws.recv(), 0.05)
+ logger.debug(f"Got response from Gladia: {resp[:100]}")
+ transcript = handle_gladia(resp)
+ if transcript:
+ logger.debug(f"Sending transcript to caller: {transcript}")
+ await websocket.send_text(json.dumps({
+ "action": "speech",
+ "text": f"I heard: {transcript}"
+ }))
+ except asyncio.TimeoutError:
+ break # No more transcripts available
+ except Exception as e:
+ logger.error(f"Error handling transcripts: {e}")
+
+ else:
+ logger.debug(f"Received message with unknown format: {message}")
+
+ except websockets.exceptions.ConnectionClosed as e:
+ logger.warning(f"Vonage WebSocket connection closed: {e}")
+ break
+ except Exception as e:
+ logger.error(f"Error receiving message: {e}")
+ break
+
+ except websockets.exceptions.ConnectionClosed as e:
+ logger.warning(f"Gladia WebSocket connection closed: {e}")
+ except Exception as e:
+ logger.error(f"Error connecting to Gladia: {e}")
+ finally:
+ # Close the Gladia connection
+ if gladia_ws:
+ try:
+ logger.debug("Closing Gladia WebSocket connection")
+ await gladia_ws.close()
+ except Exception as e:
+ logger.error(f"Error closing Gladia connection: {e}")
+
+
+if __name__ == "__main__":
+ if not GLADIA_KEY:
+ logger.error("GLADIA_API_KEY environment variable is required")
+ exit(1)
+
+ print(f"🚀 Starting server on 0.0.0.0:{HTTP_PORT}")
+ print("📝 Transcriptions will appear below:")
+ uvicorn.run(app, host="0.0.0.0", port=HTTP_PORT, log_level="error")
\ No newline at end of file
diff --git a/blogs/vonage-solaria-python-fastapi/src/vonage_example.xml b/blogs/vonage-solaria-python-fastapi/src/vonage_example.xml
new file mode 100644
index 0000000..5cac62a
--- /dev/null
+++ b/blogs/vonage-solaria-python-fastapi/src/vonage_example.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ websocket
+ wss://my.domain.com/media
+ audio/l16;rate=8000
+
+
+ Your call is being transcribed in real-time with Gladia
+
\ No newline at end of file