Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Demos with a **Live** link run in your browser at [demos.speechify.ai](https://d
| [`demos/mastra-agent-speechify/`](./demos/mastra-agent-speechify) | TypeScript (Mastra) | | Text-in, speech-out Mastra Agent using an OpenAI LLM for replies and Speechify's simba-3.2 model for TTS via `@mastra/voice-speechify`. |
| [`demos/voice-agent-showcase/`](./demos/voice-agent-showcase) | Cloudflare Workers | | One page, ten live Voice Agents API demos: calendar booking, policy-bound support, a page copilot, form intake, US outbound calls with a 5-minute cap, a voice gallery, mid-call language handoff, cross-call memory, a grounded knowledge base, and dual-control troubleshooting. |
| [`demos/vercel-ai-sdk/`](./demos/vercel-ai-sdk) | TypeScript (Vercel AI SDK) | | Speechify TTS through the Vercel AI SDK's unified `generateSpeech` interface via the official `@speechify/vercel` provider — one-line swap from OpenAI/ElevenLabs, plus word-level speech marks from `providerMetadata`. |
| [`demos/agent-events-inspector/`](./demos/agent-events-inspector) | Next.js | [Open](https://demos.speechify.ai/agent-events-inspector) | A debug timeline for Voice Agent calls: replay a sample realtime event stream, or inspect a live conversation by id. The workspace key stays server-side. |
<!-- DEMOS:END -->

## Get an API key
Expand Down
1 change: 1 addition & 0 deletions demos/agent-events-inspector/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SPEECHIFY_API_KEY=your_api_key_here
9 changes: 9 additions & 0 deletions demos/agent-events-inspector/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
.next/
.env
next-env.d.ts
*.tsbuildinfo
test-results/
playwright-report/
/.playwright/
.last-run.json
58 changes: 58 additions & 0 deletions demos/agent-events-inspector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Voice agent events inspector

A tiny debug view for a Speechify Voice Agent call. Watch a call unfold as a
timeline of realtime events — session start, user transcripts, agent replies,
tool calls and results, session end — either by **replaying a bundled sample
stream** or by **inspecting one of your own conversations by id**. The workspace
API key stays server-side.

Pairs with the Speechify post *Realtime agent events: debugging a live voice
agent call*.

## What you get

- A **timeline** that colour-codes events by source (session / user / agent /
tool) and lets you expand any event's raw JSON payload.
- **Replay mode** — plays the sample event stream in
[`app/sample-events.ts`](./app/sample-events.ts) against a scrubber, at
0.5×–4× speed, so you can see how a call reads as a stream.
- **Inspect mode** — enter a `conv_…` id (or leave blank to list recent calls);
the server proxies `GET /v1/agents/conversations/{id}` on the Voice Agents API
and renders what comes back.
- **[`app/api/conversation/route.ts`](./app/api/conversation/route.ts)** — the
one server route that holds the key and talks to the API.

## Run it yourself

```bash
cp .env.example .env # paste your Speechify workspace API key
pnpm install
pnpm dev # http://localhost:8774/agent-events-inspector
```

Get a workspace key at [platform.speechify.ai/api-keys](https://platform.speechify.ai/api-keys).
Replay mode needs no key; inspect mode needs a key with a Voice Agents workspace.

## On the event schema

The confirmed conversation record fields are `id`, `agent_id`, `status`,
`started_at`, `ended_at`, and `duration_ms` — the same ones the
[voice-agent-showcase](../voice-agent-showcase) reads. The per-event stream shape
in `app/sample-events.ts` is **illustrative**: it shows how you'd render a live
event feed, and the inspector renders whichever event/transcript array a live
conversation returns. Confirm the exact live event field names against the
[Voice Agents API docs](https://docs.speechify.ai) before wiring this to a
production audit tool.

## Abuse protection (hosted)

The hosted build gates `/api/conversation` with Cloudflare Turnstile via the
shared [`app/lib/turnstile.ts`](./app/lib/turnstile.ts) helper. It fail-opens
when `TURNSTILE_SECRET_KEY` is unset, so local dev and forks work with zero
config.

## Prerequisites

- Node 20+.
- For inspect mode: a Speechify workspace with the Voice Agents API and at least
one conversation to look at.
53 changes: 53 additions & 0 deletions demos/agent-events-inspector/app/api/conversation/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextResponse } from "next/server";
import { verifyTurnstile } from "../../lib/turnstile";

export const runtime = "nodejs";

const BASE = "https://api.speechify.ai";

// Proxies the Voice Agents conversations API so the workspace key never reaches
// the browser. A single id returns that conversation; no id lists recent ones.
export async function POST(req: Request) {
if (!(await verifyTurnstile(req))) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

const key = process.env.SPEECHIFY_API_KEY;
if (!key) {
return NextResponse.json(
{ error: "SPEECHIFY_API_KEY is not set on the server." },
{ status: 503 },
);
}

const { id } = (await req.json().catch(() => ({}))) as { id?: string };

// conv_ ids only — don't proxy arbitrary paths.
if (id && !/^conv_[a-z0-9]+$/i.test(id)) {
return NextResponse.json({ error: "That doesn't look like a conversation id (conv_…)." }, { status: 400 });
}

const path = id ? `/v1/agents/conversations/${id}` : `/v1/agents/conversations?limit=20`;

try {
const upstream = await fetch(`${BASE}${path}`, {
headers: { Authorization: `Bearer ${key}` },
});
const text = await upstream.text();
let data: unknown = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
/* non-JSON upstream error */
}
if (!upstream.ok) {
return NextResponse.json(
{ error: `Upstream ${upstream.status}`, detail: data ?? text.slice(0, 300) },
{ status: upstream.status },
);
}
return NextResponse.json(data);
} catch (err) {
return NextResponse.json({ error: (err as Error).message }, { status: 502 });
}
}
Loading