Key Points 6 min read
  • Three decoupled layers — React + LiveKit frontend, FastAPI backend, and AI Agent — wired together over WebRTC for real-time voice
  • The AI teacher Emma runs directly on Gemini 2.5 Flash Native Audio, bundling VAD/STT/LLM/TTS into one, eliminating a multi-stage pipeline
  • LiveKit runs in Self-hosted Docker mode, paired with Firebase Auth and an internal secret to protect the Agent callback API
Table of Contents

Live English Tutor is an AI English tutoring system built around real-time voice. Students practice conversations with the AI teacher Emma through their microphone (and optionally camera or screen sharing); the system corrects mistakes in real time during the session and generates a Chinese-language learning report afterward.

Unlike most text-first language-learning tools, this project puts the emphasis on “natural conversation”: letting students speak and practice just like they would with a human tutor. The biggest engineering challenge in pulling this off is the latency and architectural integration of real-time voice — and that’s the central axis of the whole system design.

Three Decoupled Layers

The system deliberately splits the “media layer,” the “API layer,” and the “AI Agent layer” apart, each with its own independent responsibility:

  • Frontend (browser): React + Vite, using Firebase Auth for Google Sign-In, going through the REST API via axios, and building the WebRTC voice/video connection with the LiveKit JS SDK.
  • FastAPI backend: Handles Firebase token verification, course and message management, issuing LiveKit tokens, and persisting data into PostgreSQL.
  • LiveKit Agent Worker (Emma): The actual AI teacher, running in a standalone worker backed by Google Gemini 2.5 Flash Native Audio.

External services include LiveKit (Self-hosted Docker), Firebase (auth), the Google Gemini API (conversation + voice), and Ollama (post-class report generation, running on an external server).

graph TD
  FE["Frontend<br/>React + Vite + LiveKit SDK"]
  API["Backend API<br/>FastAPI + PostgreSQL"]
  Agent["AI Teacher Emma<br/>LiveKit Agent + Gemini Native Audio"]
  LK["LiveKit Server<br/>Self-hosted WebRTC"]
  Firebase["Firebase Auth"]
  Ollama["Post-class Report Generation<br/>Ollama"]

  FE -->|"Google Sign-In"| Firebase
  FE -->|"REST API"| API
  FE -->|"WebRTC voice/video"| LK
  API -->|"Issue LiveKit token<br/>Create Room + dispatch Agent"| LK
  LK -->|"Audio/video stream"| Agent
  Agent -->|"Internal HTTP (x-internal-secret)<br/>messages/corrections/end session"| API
  API -->|"Trigger report generation"| Ollama

Why Gemini Native Audio

Traditional voice-conversation systems usually chain together a long pipeline: VAD (voice activity detection) → STT (speech-to-text) → LLM (generate response) → TTS (text-to-speech). Each stage adds latency, and stacked up they break the fluency of a conversation.

This project instead uses Google Gemini 2.5 Flash Native Audio — a native audio model that integrates VAD, STT, LLM, and TTS together. The Agent is configured with video_enabled=True, so beyond voice it can also receive the student’s camera feed or screen share. This design dramatically reduces the latency and complexity of stitching multiple services together.

Emma’s Four-Stage State Machine

The AI teacher Emma isn’t a single fixed conversation mode — she’s driven by a state machine that switches through four stages in order, each corresponding to a different System Prompt:

graph LR
  WARMUP["WARMUP<br/>Warm-up"] --> PRACTICE["PRACTICE<br/>Conversation Practice"]
  PRACTICE --> CORRECTION["CORRECTION<br/>Real-time Correction"]
  CORRECTION --> PRACTICE
  PRACTICE --> SUMMARY["SUMMARY<br/>Summary"]

Conversation messages and correction records are persisted via the Agent’s callback mechanism, which calls the backend’s internal API — these internal endpoints require an x-internal-secret header, are for Agent use only, and are isolated from the user-facing API.

The Full Flow of One Class

sequenceDiagram
  participant S as Student
  participant FE as Frontend
  participant API as Backend API
  participant LK as LiveKit
  participant E as Emma
  participant O as Ollama

  S->>FE: Google Sign-In
  FE->>API: Create session / get LiveKit token
  API->>LK: Create Room + dispatch Agent
  LK->>E: Start Emma
  FE->>LK: Join Room (WebRTC)
  loop Conversation Practice
    S->>E: Voice input
    E->>S: Response + real-time correction
    E->>API: Persist message / correction
  end
  S->>FE: End session
  FE->>API: Notify session end
  API->>O: Trigger post-class report generation
  API->>FE: Return post-class Chinese report

Post-class report generation is handled by Ollama (OpenAI-compatible API), which is independent of the main conversation flow, and it’s disabled by default — you need to set ENABLE_REPORT_GENERATION to true and confirm the Ollama server is reachable for it to be enabled. The frontend can query the status via GET /sessions/{id}/report (disabled / pending / ready).

The Fine Points of Self-hosted LiveKit and LAN Connectivity

LiveKit runs in Self-hosted mode (Docker), requiring no LiveKit Cloud account; the whole stack is brought up locally via docker-compose.livekit.yml.

A classic WebRTC problem I hit in practice: when accessing from another device on the LAN, the connection would fail (could not establish pc connection). The cause is that the IP broadcast in the ICE candidates is wrong. The fix is to set LIVEKIT_NODE_IP to the host’s LAN IP (e.g. 192.168.15.116) so LiveKit broadcasts the correct address, then restart the LiveKit server. These “works locally but drops when switching devices” problems almost always originate in the ICE/network layer rather than application logic.

Auth and Security Boundaries

The whole system has two clear trust boundaries:

  • External: The user APIs (/auth/*, /sessions/*) all require a Firebase ID Token. The frontend does Google Sign-In first to obtain the token, and the backend verifies it with the Firebase Admin SDK.
  • Internal: The Agent → Backend callbacks (/internal/agent/*) are protected by an x-internal-secret shared secret, isolated from the external API.

After deploying to Cloudflare Pages, there are two more must-dos: add the *.pages.dev domain to the Authorized domains in the Firebase Console (otherwise Google Sign-In returns auth/unauthorized-domain), and add the frontend domain to ALLOWED_ORIGINS in the backend’s main.py to avoid CORS errors.

Tech Stack at a Glance

LayerTechnology
FrontendReact 18, TypeScript, Vite, React Router v6, Zustand, Axios, LiveKit JS SDK
BackendFastAPI, SQLAlchemy 2.0, PostgreSQL 16, Firebase Admin SDK, LiveKit API SDK
AI AgentLiveKit Agents SDK 1.x, Google Gemini 2.5 Flash Native Audio (Realtime)
Report GenerationOllama (OpenAI-compatible API, external server)
AuthFirebase Authentication (Google Sign-In)
Real-time Voice/VideoLiveKit Self-hosted (WebRTC)
DeploymentDocker Compose (backend + Agent), Cloudflare Pages (frontend)

Wrap-up

The design focus of Live English Tutor is splitting a “real-time voice AI tutor” into three independently operable layers: media (LiveKit WebRTC), API (FastAPI + PostgreSQL), and AI Agent (Gemini Native Audio + state machine). Among these, using a native audio model in place of a traditional STT/TTS pipeline is the key decision for reducing conversation latency; while Self-hosted LiveKit, Firebase auth, and the internal secret together form the operational and security foundation for running this system in both local and production environments.

References

Ask this article

Answers come from this article only. Click any prompt below or open the chat at the bottom right.

🇺🇸 English

Picture this: instead of typing into a chatbot to practice English, you just... talk. Out loud, through your microphone, the same way you'd talk to a real tutor sitting across the table. That's the whole idea behind Live English Tutor — a real-time voice AI system where students have actual spoken conversations with an AI teacher named Emma. She listens, she talks back, she corrects your mistakes on the fly, and when the session's over, she hands you a written learning report in Chinese.

Now, most language-learning apps out there are text-first. You read, you tap, you fill in blanks. This project deliberately went the other direction — natural spoken conversation as the core experience. And here's the thing: the moment you commit to real-time voice, you've signed up for the single hardest engineering problem in the whole system, which is latency. How do you make the AI respond fast enough that it feels like a conversation and not a walkie-talkie exchange with awkward pauses? That question drives basically every design decision here.

Let me walk you through how it's built, because the architecture is genuinely clean. The system is split into three decoupled layers, each minding its own business.

First, the frontend — that's the browser. Built with React and Vite. It handles Google Sign-In through Firebase, talks to the backend over a normal REST API, and crucially, it establishes the live voice and video connection using the LiveKit JavaScript SDK. That's your media pipe.

Second, the backend — a FastAPI service. This is the traffic controller. It verifies your login token from Firebase, manages your courses and messages, issues the tokens that let you into a LiveKit room, and saves everything into a PostgreSQL database.

And third, the star of the show: the LiveKit Agent Worker, which *is* Emma. She runs as her own standalone worker, powered by Google's Gemini 2.5 Flash Native Audio model. When you're in a session, the audio and video stream flows from LiveKit straight into Emma, and Emma streams her voice right back.

Around all this sit a few external services — LiveKit itself running self-hosted in Docker, Firebase for auth, the Google Gemini API for the actual conversation and voice, and Ollama running on a separate server to generate those post-class reports.

So why Gemini Native Audio? This is the key decision, so let me slow down here. The traditional way to build a talking AI is to chain together a whole assembly line: first voice activity detection to figure out when you're speaking, then speech-to-text to transcribe what you said, then a language model to think up a response, then text-to-speech to say it out loud. Four separate stages. And every single one adds a little delay. Stack them all up, and the conversation starts to feel sluggish — that fluency just falls apart.

Gemini 2.5 Flash Native Audio collapses all four of those stages into one integrated model. Voice goes in, voice comes out, and all the detection, understanding, and speaking happen natively inside. That's a dramatic cut in both latency and integration complexity. And because it's configured with video enabled, Emma can also see your camera feed or your shared screen — not just hear you.

Now, Emma isn't just one static personality reading from a single script. She runs on a four-stage state machine, and she moves through the stages in order, with a different system prompt for each. She starts in WARMUP — easing you in, warming up the conversation. Then she moves to PRACTICE, the main conversation drills. From PRACTICE she can dip into CORRECTION when you make a mistake — real-time correction — and then loop right back to PRACTICE to keep going. And finally, when the lesson's winding down, she transitions to SUMMARY. Throughout all of this, every message and every correction gets saved. Emma's worker calls back to the backend's internal API to persist them — and those internal endpoints are locked down with a special secret header, meant only for the Agent, completely walled off from the user-facing API.

Let me trace one full class start to finish, because it ties everything together. You sign in with Google. The frontend asks the backend to create a session and hand over a LiveKit token. The backend spins up a room on LiveKit and dispatches the Agent — that boots Emma. Your browser joins the room over WebRTC. And now you're in the loop: you speak, Emma responds and corrects in real time, and behind the scenes she's saving each exchange to the backend. When you're done, you end the session, the frontend notifies the backend, and the backend triggers Ollama to generate your post-class report. That report comes back in Chinese.

One detail worth knowing: report generation is deliberately kept independent from the main conversation, and it's actually turned off by default. You have to flip an environment flag to true and confirm your Ollama server is reachable before it'll run. The frontend can poll for status — is the report disabled, pending, or ready — and display accordingly.

Now I want to talk about the self-hosting, because there's a war story here that anyone who's touched WebRTC will recognize. LiveKit runs fully self-hosted in Docker — no LiveKit Cloud account needed, the whole stack comes up locally with one compose file. Great. But then the classic bug hit: everything worked perfectly on the local machine, and the moment you tried to connect from another device on the same network, the connection just... failed. "Could not establish PC connection."

Here's what was going on. WebRTC uses these things called ICE candidates to figure out how two devices should reach each other — essentially a list of "here's where you can find me." And LiveKit was broadcasting the *wrong* IP address in that list. The fix was to explicitly set the node's IP to the host's actual LAN address and restart the server. Once LiveKit advertised the correct address, cross-device connections worked. And the broader lesson — file this away — when something works locally but drops the instant you switch devices, the culprit is almost always the ICE and networking layer, not your application code. Don't waste hours debugging your business logic on that one.

A quick word on security, because the system has two crystal-clear trust boundaries. On the outside, all the user-facing APIs require a valid Firebase ID token — you sign in with Google first, get your token, and the backend verifies it with the Firebase Admin SDK. On the inside, the Agent-to-backend callbacks are guarded by that shared secret header I mentioned, totally isolated from the external API. And two easy-to-forget deployment gotchas: after you push the frontend to Cloudflare Pages, you have to add that pages.dev domain to Firebase's authorized domains, or Google Sign-In throws an unauthorized-domain error. And you have to add the frontend's domain to the backend's allowed origins list, or CORS will block you.

So let me leave you with the three things that really matter here.

One: the architecture wins by decoupling. Media, API, and AI Agent are three independent layers, each swappable and operable on its own. That separation is what makes the whole thing tractable.

Two: the native audio model is the make-or-break decision. Replacing that traditional four-stage speech pipeline with a single integrated Gemini model is what buys you low enough latency for the conversation to actually feel natural. If you take one technical idea away, take that one.

And three: the boring operational stuff is what keeps it alive. Self-hosted LiveKit, Firebase auth, the internal secret, and that ICE networking fix — none of it is glamorous, but together it's the foundation that lets this run in both your local setup and production without falling over. Real-time voice AI is exciting, but it's the plumbing that determines whether it ships.

🇹🇼 中文

Live English Tutor 是一套把「即時語音」放在正中央的 AI 英文家教系統。它的概念很簡單:學生打開麥克風,需要的話還能開攝影機或分享螢幕,然後就跟一位叫 Emma 的 AI 老師直接對話練習。過程中 Emma 會即時幫你糾錯,等課上完,系統還會生成一份中文的學習報告。

大部分語言學習工具都是以文字為主,你打字、它回你。但這個專案刻意反其道而行,重心放在「自然對話」,讓你真的像跟真人家教一樣開口說。而要做到這件事,最難的工程問題就是:即時語音的延遲,還有整套架構怎麼整合。這也是整個系統設計的主軸。

先講架構。系統刻意分成三層,彼此解耦、各管各的。

第一層是前端,也就是瀏覽器。技術上是 React 加 Vite,用 Firebase 做 Google 登入,透過 REST API 跟後端溝通,然後用 LiveKit 的 JS SDK 建立 WebRTC 的語音跟視訊連線。

第二層是 FastAPI 後端,負責驗證 Firebase token、管理課程跟訊息、簽發 LiveKit token,資料通通存進 PostgreSQL。

第三層是 AI 老師 Emma 本人。她跑在一個獨立的 worker 上,背後接的是 Google Gemini 2.5 Flash Native Audio。

外部服務則有幾個:Self-hosted 的 LiveKit、負責認證的 Firebase、負責對話跟語音的 Gemini API,還有跑在外部伺服器、專門生成課後報告的 Ollama。

接下來這個決策很關鍵——為什麼選 Gemini 的原生音訊模型。

傳統的語音對話系統,通常要串一長串 pipeline。先做語音活動偵測,判斷你有沒有在講話;然後語音轉文字;再把文字丟給大語言模型生成回應;最後再把回應的文字轉回語音。四個階段,每一段都有自己的延遲,疊加起來,對話的流暢感就毀了,你會覺得對方反應很慢,像在跟機器人講話。

這個專案改用 Gemini 2.5 Flash Native Audio,一個原生音訊模型,直接把剛剛那四段——偵測、轉文字、生成、轉語音——整合在同一個模型裡。而且 Agent 端有開 video enabled,所以除了聲音,它也能看到你的攝影機畫面或螢幕分享。這樣一來,把多個服務串接起來的延遲跟複雜度,就大幅降下來了。這是降低對話延遲最核心的一步。

再來聊 Emma 這個老師怎麼運作。她不是一個固定不變的對話模式,而是用一個狀態機在驅動,會依序在四個階段之間切換,每個階段對應不同的 System Prompt。第一階段是暖身,先讓你進入狀況;接著進入對話練習;練習中如果你講錯,就切到即時糾錯,糾完再回到練習;最後練習結束,進入總結。對話的訊息跟糾錯紀錄,會透過 Agent 的回呼機制,呼叫後端的內部 API 存下來。這些內部端點要帶一個叫 x-internal-secret 的 header,只給 Agent 用,跟對外的使用者 API 完全隔離。

那一堂課實際上是怎麼跑的?學生先做 Google 登入,前端跟後端要一個 LiveKit token 並建立課程,後端就去 LiveKit 建立一個房間、把 Agent 派進去,Emma 就啟動了。學生用 WebRTC 加入房間,開始對話。這個過程會不斷循環:你語音輸入,Emma 回應加即時糾錯,同時把訊息跟糾錯持久化到後端。等你結束課程,前端通知後端,後端就去觸發 Ollama 生成課後報告,最後把中文報告回傳給你。

值得注意的是,課後報告這塊是獨立於主對話流程的,由 Ollama 負責,而且預設是關閉的。你得把 ENABLE_REPORT_GENERATION 設成 true,並確認 Ollama 伺服器連得上,它才會啟用。前端可以查報告的狀態,會有三種:關閉、生成中、或已完成。

然後講一個很實務的坑——Self-hosted LiveKit 跟區網連線。

這個專案的 LiveKit 是自己架的 Docker 模式,不需要 LiveKit Cloud 帳號,整套用一個 docker-compose 檔在本地拉起來就好。但實務上會踩到一個很典型的 WebRTC 問題:你在自己這台機器上測都好好的,可是換成區網裡的另一台裝置去連,就失敗了,錯誤訊息是 could not establish pc connection。

原因在哪?在於 ICE candidate 廣播出去的 IP 不對。解法是把 LIVEKIT_NODE_IP 這個設定,設成主機的區網 IP,比如 192.168 那一串,讓 LiveKit 廣播正確的位址,設完重啟 server 就好。這邊給大家一個經驗法則:這種「本地能跑、換裝置就斷線」的問題,十之八九出在 ICE 跟網路層,而不是你的應用邏輯。別在應用程式碼裡瞎找。

安全這塊,整個系統有兩道很清楚的信任邊界。對外的部分,所有使用者 API 都需要 Firebase 的 ID Token,前端先登入拿 token,後端用 Firebase Admin SDK 驗證。對內的部分,就是 Agent 打回後端的那些回呼,用剛剛提到的 x-internal-secret 共享密鑰保護,跟對外 API 隔開。

另外,如果你要部署到 Cloudflare Pages,有兩件事一定要做。第一,去 Firebase Console 的 Authorized domains 把 pages.dev 這個網域加進去,不然 Google 登入會直接回你 unauthorized-domain 的錯誤。第二,去後端的 ALLOWED_ORIGINS 把前端網域加進去,避免 CORS 錯誤。這兩個都是很容易漏掉、然後卡半天的地方。

好,做個收尾。這套 Live English Tutor,我覺得有三個重點值得記住。

第一,架構上把即時語音家教拆成三層——媒體層用 LiveKit 的 WebRTC、API 層用 FastAPI 加 PostgreSQL、AI Agent 層用 Gemini 加狀態機,三層獨立運作、各司其職,這種解耦讓系統好維護也好擴充。

第二,也是最關鍵的技術決策——用原生音訊模型取代傳統的語音轉文字、文字轉語音那一整套 pipeline,這是壓低對話延遲、讓對話變自然的核心。

第三,Self-hosted 的 LiveKit、Firebase 認證,加上內部密鑰這三樣,一起撐起了這套系統在本地跟生產環境的運行基礎跟安全邊界,而且那個 ICE 廣播 IP 的坑,是自架 WebRTC 幾乎人人都會遇到的,先記起來能省你不少時間。

即時語音 AI 的門檻,正在被原生音訊模型快速拉低,這個專案就是一個很好的起手式範例。

Tags

Related Articles