LiveKit 기반 AI 영어 대화 실습 시스템. TBLT(Task-Based Language Teaching) 방식으로, AI 캐릭터 Kate와 과업 기반 영어 대화를 수행하는 실시간 음성 대화 에이전트. 대상: 한국어권 학생.
관리자는 /admin에서 수업 운영 모드를 선택할 수 있다.
- 그룹 대화 모드 (
pipeline): 학생 n명 + 에이전트 1명의 STT → LLM → TTS 파이프라인. 에이전트 이름:pipeline-agent - 개별 대화 모드 (
realtime): 학생 1명 + 에이전트 1명의 OpenAI Realtime speech-to-speech 파이프라인. 에이전트 이름:realtime-agent, role:dominant또는collaborative
CSCL_TBLT/
├── .env # API 키 (직접 입력 필요, git 추적 안 됨)
├── .env.example # 환경변수 템플릿
├── .github/
│ └── workflows/
│ ├── agent-tests.yml # agent/ 테스트 CI
│ └── client-build-and-test.yaml # client/ 빌드/린트 CI
│
├── agent/ # AI 음성 에이전트 (LiveKit Agents Python SDK)
│ ├── main.py # 에이전트 진입점 (CLI 지원)
│ ├── prompt_pipeline.py # 그룹 대화 모드 시스템 프롬프트
│ ├── prompt_realtime.py # 개별 대화 모드 시스템 프롬프트
│ ├── logger.py # 대화 JSON 로거
│ ├── pyproject.toml
│ ├── uv.lock
│ └── tests/
│ └── test_conversation.py # LLM 단독 대화 흐름 테스트
│
├── server/ # legacy static 클라이언트용 토큰 발급 서버 (FastAPI)
│ ├── main.py # GET /token, POST /dispatch
│ ├── pyproject.toml
│ └── uv.lock
│
├── client/ # Next.js 브라우저 클라이언트
│ ├── app/ # Next.js App Router + API routes
│ ├── components/ # 로비, 관리자, LiveKit UI 컴포넌트
│ ├── static/ # legacy 심플 클라이언트 (HTML/JS)
│ │ ├── index.html
│ │ ├── app.js
│ │ └── style.css
│ └── .env.local # Next.js용 환경변수 (git 추적 안 됨)
│
├── config.example.json # 로컬 fallback/import용 운영 설정 예시
├── config.json # 로컬 fallback 운영 설정 (git 추적 안 됨)
└── logs/ # 대화 로그 JSON (자동 생성, git 추적 안 됨)
이 저장소는 main / develop 기준으로 운영한다.
| 브랜치 | 역할 | 운영 원칙 |
|---|---|---|
main |
실제 서버 배포와 연결되는 안정 브랜치 | 배포 가능한 상태만 유지한다. 검증된 변경만 병합하고, 미완성 실험은 올리지 않는다. |
develop |
기능 개발과 실험 통합 브랜치 | 새 기능, UI/프롬프트 실험, 검증 전 변경의 기본 합류 지점으로 사용한다. |
기본 흐름:
- 새 작업은
develop에서feature/...,issue-...,experiment/...브랜치를 만든다. - 일반 변경은 작업 브랜치에서
develop으로 PR을 열어 합친다. - 배포할 묶음이 안정화되면
develop에서main으로 PR을 열어 합친 뒤 실제 서버에 배포한다. - 운영 장애나 긴급 수정은
main에서hotfix/...브랜치를 만들고, 배포 후 같은 수정이develop에도 반영되도록 한다.
브라우저 클라이언트 (client/ Next.js)
│
├─[GET /api/rooms]──────────────► Supabase app_settings + LiveKit room 조회
├─[POST /api/admin/config]──────► Supabase app_settings에 운영 설정 저장
├─[POST /api/token]─────────────► LiveKit 참가자 토큰 발급
│ room_config.agents로 agent 자동 배치
│
└─[WebRTC 연결]────────────────► LiveKit Cloud
│
├─ pipeline-agent: STT → LLM → TTS
└─ realtime-agent: OpenAI Realtime
- Next.js 클라이언트 (
client/): 학생 로비, 관리자 설정, 관리자 대시보드, 토큰 발급 API를 포함한다. - AI 에이전트 (
agent/): 같은 코드에서pipeline-agent,realtime-agentworker를 실행한다. Realtime role은dominant또는collaborative로 선택한다. - legacy 토큰 서버 (
server/):client/staticHTML 클라이언트용이다. 현재 Next.js 앱 실행에는 필요하지 않다.
| 모드 | UX 표시명 | Room 정책 | Agent | 음성 처리 |
|---|---|---|---|---|
pipeline |
그룹 대화 모드 | 12반-1그룹 같은 그룹 room |
pipeline-agent |
STT deepgram/nova-3 → LLM openai/gpt-4.1-mini → TTS cartesia/sonic-3 |
realtime |
개별 대화 모드 | realtime-{반}-{학생명}-{suffix} 자동 생성 |
realtime-agent |
OpenAI Realtime speech-to-speech |
curl -LsSf https://astral.sh/uv/install.sh | shcp .env.example .env
cp config.example.json config.jsonconfig.json은 Supabase가 설정되지 않은 로컬 개발 fallback 및 초기 import 참고용이다. Production 운영 설정은 Supabase app_settings에 저장된다.
.env 파일에 값 입력:
# LiveKit Cloud (https://cloud.livekit.io → 프로젝트 생성 후 발급)
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret
# 채팅방 및 에이전트 이름
ROOM_NAME=english-practice
AGENT_NAME=pipeline-agent
AGENT_WORKER_MODE=pipeline
AGENT_ROLE=dominant
# Realtime 모드에는 OPENAI_API_KEY가 필요
# Pipeline 모드는 LiveKit Inference를 사용하면 OPENAI_API_KEY / DEEPGRAM_API_KEY 없이도 동작
OPENAI_API_KEY=
DEEPGRAM_API_KEY=Next.js 앱의 운영 모드는
.env가 아니라 Supabaseapp_settings.agent_mode로 결정되며,/admin에서 변경한다. Supabase가 없는 로컬 개발 환경에서는config.jsonfallback을 사용할 수 있다. Realtime 상호작용 role은 관리자 전용agentRole설정으로만 노출된다.
cd agent
uv sync
uv run python main.py download-files최초 1회 의존성을 설치한다.
pnpm setup루트에서 다음 명령 하나로 agent worker와 Next.js 클라이언트를 함께 실행한다. pnpm dev의 auto 모드는 로컬 프로세스 선택을 위해 config.json의 agentMode/agentRole을 읽는다. 앱 route의 런타임 운영 설정은 Supabase app_settings를 사용하며, Supabase가 없는 로컬 fallback 환경에서는 config.json 값을 사용한다.
pnpm dev브라우저에서:
- 학생 로비: http://localhost:3000
- 관리자 설정: http://localhost:3000/admin
- 관리자 대시보드: http://localhost:3000/admin/dashboard
Next.js 클라이언트는 자체
/api/token라우트를 가지므로server/FastAPI 토큰 서버가 필요 없다.
개별 대화 모드 worker로 실행하려면 다음 명령을 사용한다.
pnpm dev:realtimecollaborative 조건으로 실행하려면 다음 명령을 사용한다.
pnpm dev:realtime:collaborativepipeline, realtime dominant, realtime collaborative worker를 모두 함께 띄우려면 다음 명령을 사용한다.
pnpm dev:all/admin에서 개별 대화 모드와 상호작용 방식을 선택하면 학생은 조건명을 보지 않고 개별 room으로 입장한다.
기존처럼 터미널을 나눠 실행해야 할 때는 다음 개별 명령을 사용할 수 있다.
pnpm dev:agent:pipeline
pnpm dev:clientclient/static을 사용할 때만 FastAPI 토큰 서버가 필요하다.
# 터미널 1 — legacy 토큰 서버
cd server
uv sync
uv run python -m uvicorn main:app --port 8000 --reload# 터미널 2 — pipeline-agent
cd agent
AGENT_WORKER_MODE=pipeline uv run python main.py dev# 터미널 3 — static HTML 클라이언트
cd client/static
python -m http.server 3000브라우저에서 http://localhost:3000 접속 → 이름 입력 → 입장 → 🤖 에이전트 생성 버튼 클릭.
에이전트를 터미널에서 직접 실행해 백엔드만 빠르게 실험할 수 있습니다. 마이크와 스피커만 있으면 되고, 토큰 서버와 클라이언트는 불필요합니다.
cd agent
# 터미널 음성 대화 모드 (마이크로 말하고 스피커로 듣기)
uv run python main.py console
# 프론트엔드 연결 대기 모드 (토큰 서버·클라이언트와 함께 사용)
uv run python main.py dev
# 프로덕션 모드
uv run python main.py start| 명령 | 설명 | 필요한 것 |
|---|---|---|
console |
터미널 단독 음성 대화 | 마이크, 스피커 |
dev |
프론트엔드 연결 대기 (자동 재시작) | Next.js 클라이언트 |
start |
프로덕션 실행 | Next.js 클라이언트 |
download-files |
VAD 등 ML 모델 다운로드 | — |
client/.env.local에 LiveKit 자격증명이 필요하다.
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret
# Supabase admin auth / persistent state migration
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
SUPABASE_SECRET_KEY=sb_secret_...Next.js API routes:
| Route | 역할 |
|---|---|
POST /api/token |
참가자 토큰 발급, 현재 운영 모드에 맞는 agent 자동 배치 |
GET /api/rooms |
활성 반의 그룹 room과 realtime 개별 room 현황 조회 |
GET/POST /api/admin/config |
Supabase app_settings 읽기/쓰기 |
GET/POST /api/dispatch |
그룹 대화 모드에서 pipeline-agent 수동 배치/존재 확인 |
POST /api/rooms/terminate |
room 강제 종료 |
GET /api/logs, GET /api/logs/stream |
대화 로그 목록/스트리밍 |
주의: Production에서
/admin,/api/admin, 관리자성 API(/api/dispatch,/api/logs,/api/rooms/terminate)는 Supabase Auth session과profiles.role = 'admin'권한으로 보호된다.ADMIN_USERNAME/ADMIN_PASSWORDBasic Auth는 더 이상 사용하지 않는다.
Supabase 도입은 GitHub issue #4의 단계별 작업으로 진행한다. 현재 admin auth와 수업 운영 설정은 Supabase를 사용한다.
client/lib/supabase/client.ts: browser/client component용 Supabase clientclient/lib/supabase/server.ts: server component, route handler, server action용 cookie-aware clientclient/lib/supabase/admin.ts: 서버 전용 secret-key clientclient/lib/supabase/proxy.ts: Auth session refresh와 admin route guard용 proxy/middleware helperclient/lib/settings-store.ts:app_settings기반 운영 설정 store와 local fallback adaptersupabase/config.toml: 로컬 Supabase CLI 실행 포트와 migration 설정supabase/migrations/20260611000000_foundation.sql: 초기 schema와 RLS policysupabase/migrations/20260612000000_realtime_prompt_version_rpc.sql: active prompt version 교체 RPC
초기 schema는 다음 테이블을 만든다.
| Table | 역할 |
|---|---|
profiles |
Supabase Auth 사용자별 앱 권한 및 표시명 |
app_settings |
수업 운영 설정 저장소 |
realtime_prompt_versions |
Realtime prompt override version 저장소 |
최초 admin은 Supabase Auth 사용자 생성 후 SQL 또는 dashboard에서 profiles.role = 'admin'으로 부여한다.
insert into public.profiles (user_id, role, display_name)
values ('<auth-user-id>', 'admin', 'Admin')
on conflict (user_id) do update
set role = 'admin',
display_name = excluded.display_name;SUPABASE_SECRET_KEY는 RLS를 우회할 수 있으므로 server-only 코드에서만 사용한다. Next.js API routes는 client/.env.local에서 이 값을 읽고, Python realtime agent는 root .env에서 같은 URL/secret을 읽어 promptVersionId에 해당하는 realtime_prompt_versions row를 가져온다.
관리자 로그인 화면은 /admin/login이다. 로그인은 Supabase email/password 계정을 사용하며, 계정의 profiles.role이 admin이어야 /admin 및 관리자성 API를 사용할 수 있다.
기존 config.json 값을 Supabase로 옮길 때는 다음 형태로 app_settings 기본 row를 seed한다.
insert into public.app_settings (
id,
num_classes,
num_groups_per_class,
class_start,
active_class,
agent_mode,
agent_role,
feedback_condition_id,
realtime_resetting
)
values (
'default',
4,
12,
9,
9,
'realtime',
'collaborative',
'explicit_correction',
false
)
on conflict (id) do update
set num_classes = excluded.num_classes,
num_groups_per_class = excluded.num_groups_per_class,
class_start = excluded.class_start,
active_class = excluded.active_class,
agent_mode = excluded.agent_mode,
agent_role = excluded.agent_role,
feedback_condition_id = excluded.feedback_condition_id,
realtime_resetting = excluded.realtime_resetting;Production에서는 client/.env.local과 root .env에 Supabase 연결값이 필요하다. Next.js에는 NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY가 필요하고, Python realtime agent에는 NEXT_PUBLIC_SUPABASE_URL 또는 SUPABASE_URL과 SUPABASE_SECRET_KEY 또는 SUPABASE_SERVICE_ROLE_KEY가 필요하다. Supabase가 미설정이거나 DB 장애가 있으면 운영 설정 route는 setup/runtime error를 반환한다. 로컬 개발에서는 Supabase가 없거나 일시적으로 실패할 때 config.json fallback을 사용한다.
기존 prompt_config.json override는 supabase/README.md의 realtime_prompt_versions Migration 절차로 한 번만 active version row로 이관한다. 이관 후 Realtime custom prompt source of truth는 Supabase prompt version row다. /api/token은 active row의 promptVersionId를 LiveKit metadata에 넣고, Python realtime agent는 그 id로 같은 row를 fetch한다. promptVersionId가 없는 default session은 tracked markdown prompt를 사용한다.
로컬 Supabase CLI는 기존 프로젝트와 기본 포트가 충돌하지 않도록 5532x 대역을 사용한다.
supabase start
supabase db reset --no-seed
supabase statusclient/.env.local에서 로컬 Supabase를 사용할 때는 supabase status 출력의 key를 넣는다.
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:55321
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<publishable-key-from-supabase-status>
SUPABASE_SECRET_KEY=<secret-key-from-supabase-status>Studio는 http://127.0.0.1:55323, Postgres는 postgresql://postgres:postgres@127.0.0.1:55322/postgres에서 열린다.
/admin에서 다음 값을 관리한다.
| 설정 | 저장 위치 | 설명 |
|---|---|---|
| 수업 운영 모드 | app_settings.agent_mode |
pipeline 또는 realtime |
| 현재 수업 중인 반 | app_settings.active_class |
학생 로비에 표시되는 반 |
| 반 번호 시작 | app_settings.class_start |
반 번호 범위의 시작 |
| 전체 학급 수 | app_settings.num_classes |
관리자 화면에 표시할 반 개수 |
| 반당 그룹 수 | app_settings.num_groups_per_class |
그룹 대화 모드에서 표시할 그룹 수 |
예시:
{
"numClasses": 4,
"numGroupsPerClass": 12,
"classStart": 9,
"activeClass": 12,
"agentMode": "pipeline"
}마이크·브라우저 없이 LLM만 사용해 대화 흐름을 자동 검증합니다.
cd agent
uv run pytest tests/ -v테스트 내용 (tests/test_conversation.py):
- 5턴 대화 흐름 검증 (주말 약속 만들기 태스크)
- 문법 오류 발화에 대한 Kate의 자연스러운 반응 확인
- 역질문(주말 활동)에 대한 응답 확인
Next.js 클라이언트가 실행 중일 때 브라우저에서 아래 주소에 접속하면 세션 로그를 확인할 수 있습니다.
http://localhost:3000/admin/dashboard
- 저장된 세션 목록 확인
- 세션별 대화 로그 확인
- 참가자 이름, 발화 시각, 역할(User/Agent) 표시
legacy static 클라이언트를 사용할 때는 client/static/admin.html을 사용할 수 있다.
대화가 끝나면 logs/ 폴더에 JSON 파일로 자동 저장됩니다.
root .env에서 LOG_PROMPT_STACK=true를 설정하면 각 Realtime 세션의 전체 resolved prompt stack이 metadata.prompt_stack에 함께 저장되고 /admin/dashboard에서 접어서 확인할 수 있습니다. 기본값은 off입니다.
logs/
└── RM_9GkS7d8BWCsf--260409_14:23.json
파일명 형식: {session_id}_{started_at}.json
{
"session_id": "RM_9GkS7d8BWCsf",
"room": "english-practice",
"entries": [
{
"timestamp": "2026-04-09T14:23:05",
"role": "user",
"text": "Hello!",
"participant_identity": "user_1234",
"participant_name": "고준보"
},
{
"timestamp": "2026-04-09T14:23:07",
"role": "agent",
"text": "Hi! How are you today?"
}
]
}| 항목 | 위치 |
|---|---|
| Realtime AI 시스템 프롬프트 수정 | 기본값은 prompts/realtime/*.md, 런타임 수정은 /admin에서 Supabase realtime_prompt_versions로 저장 |
| 기본 AI 시스템 프롬프트 fallback | 그룹 대화는 agent/prompt_pipeline.py, 개별 대화는 prompts/realtime/*.md |
| 그룹 대화 모드 모델 변경 | agent/main.py (STT deepgram/nova-3, LLM openai/gpt-4.1-mini, TTS cartesia/sonic-3) |
| 개별 대화 모드 모델 변경 | agent/main.py (openai.realtime.RealtimeModel) |
| 실행할 worker 모드 | AGENT_WORKER_MODE=pipeline 또는 AGENT_WORKER_MODE=realtime + AGENT_ROLE=dominant/collaborative |
| 수업 운영 모드 변경 | /admin 또는 Supabase app_settings.agent_mode |
| Realtime 상호작용 role 변경 | /admin 또는 Supabase app_settings.agent_role |
| 토큰 서버 포트 변경 | server/main.py + client/static/app.js 상단 SERVER 변수 |
| legacy static Room 이름 변경 | .env → ROOM_NAME |
| 에이전트 이름 변경 | agent/main.py, client/lib/agent-role.ts (pipeline-agent, realtime-agent) |
| 로그 저장 위치 변경 | agent/logger.py → LOGS_DIR |
Realtime 기본 프롬프트를 수정할 때는 prompts/realtime/ 아래의 문서형 프롬프트를 수정합니다. agent와 admin 기본값은 이 md 파일들을 직접 읽습니다.
pnpm prompts:check원본 파일은 base.md, roles/dominant.md, roles/collaborative.md, condition-combinations/*.md, task-cards/*.md입니다. Realtime runtime stack은 base prompt, role prompt, role/feedback condition-combination prompt, task card 순서로 조합됩니다. task-cards/manifest.json에 등록된 주제별 task card 중 /admin에서 선택한 항목이 개별 Realtime 세션에 적용됩니다. pnpm prompts:check는 파일 누락이나 빈 파일 같은 기본 오류를 검사합니다.
/admin에서 프롬프트를 저장하면 Supabase realtime_prompt_versions에 새 version row가 생성되고 해당 row만 active가 됩니다. active custom version은 prompts/realtime/*.md 기본값보다 우선하며, base/role prompt와 함께 conditionCombinationPrompts, feedbackConditionId, taskCardId, taskCardPrompt snapshot을 보존합니다. feedbackPrompt는 legacy metadata로 남아 있지만 runtime stack에는 삽입되지 않습니다. md 기본값을 다시 쓰려면 /admin에서 "기본값으로 복원"을 실행해 active custom version을 비활성화합니다.
단일 문서에서 복사한 프롬프트를 한 파일로 검사기에 넘길 수도 있습니다. 이 경우 검사기는 # BASE PROMPT:, # INTERLOCUTOR ROLE PROMPT: Dominant, # INTERLOCUTOR ROLE PROMPT: Collaborative, # TASK CARD: 헤딩을 기준으로 네 구간을 나눕니다.
pnpm dev, pnpm build, pnpm start, pnpm dev:agent:*, 프로덕션 배포 스크립트는 실행 전에 prompts:check와 같은 검사를 수행합니다.
현재 production 기준값과 배포 절차는 docs/operations/를 따른다.
- Production Environment: 현재 EC2, 서비스명, 운영 모드, 외부 서비스 기준값
- Deployment Runbook: GitHub Actions 배포, 재시작, health check 절차
- Capacity Plan: 평상시/실험일 인스턴스와 Realtime 동시 세션 기준
- Incident Checklist: 장애 확인 순서와 주요 로그
요약:
| 항목 | 현재 기준 |
|---|---|
| Production domain | https://tblt-agent.net/ |
| App path | /opt/cscl-tblt |
| Client | PM2 cscl-client, port 3000 |
| Agent | systemd cscl-agent.service |
| Operation mode source of truth | Supabase app_settings |
| Current production mode | realtime |
| Deploy script | scripts/deploy-production.sh |
| Health check | https://tblt-agent.net/api/health |
GitHub Actions는 세 종류로 구성한다.
| Workflow | Trigger | 역할 |
|---|---|---|
pre-commit |
PR/push → main, develop, 수동 실행 |
공통 파일 형식 및 repo guardrail 검사 |
CI |
PR → main, develop, 수동 실행 |
Client lint/format/build, Agent tests |
Deploy Production |
main push 또는 수동 실행 |
검증 후 EC2 production 배포 |
Production 배포는 GitHub Actions가 SSH로 EC2에 접속해 scripts/deploy-production.sh를 실행한다.
필요한 GitHub Environment/Secrets:
| 이름 | 종류 | 설명 |
|---|---|---|
PROD_SSH_HOST |
Secret | EC2 host 또는 IP |
PROD_SSH_USER |
Secret | 배포 SSH 사용자, 예: ubuntu |
PROD_SSH_KEY |
Secret | 배포용 private key |
PROD_SSH_PORT |
Secret | SSH port, 기본값 22 |
PROD_APP_DIR |
Environment variable | 서버 repo 경로, 기본값 /opt/cscl-tblt |
서버 조건:
/opt/cscl-tblt가 Git checkout이어야 한다.- 배포 사용자가
systemctl restart/is-active cscl-agent를 password 없이 실행할 수 있어야 한다. pnpm,uv,pm2,curl이 설치되어 있어야 한다.- 서버의
.env,client/.env.local,config.json,prompt_config.json은 배포 중 백업 후 복원된다. config.json은 git 추적 대상이 아니며, Supabase가 없는 로컬 fallback/import 용도로만 사용한다.- Production에서는
client/.env.local에NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,SUPABASE_SECRET_KEY가 있어야 한다. Realtime custom prompt를 쓰는 agent host의 root.env에도 Supabase URL과 server-only secret이 있어야 한다. - Production 운영 설정은 Supabase
app_settings(id = 'default')row에 저장된다. - 최초 admin 계정은 Supabase Auth 사용자 생성 후
profiles.role = 'admin'으로 bootstrap해야 한다.
배포 후 health check:
cscl-agentactive 확인cscl-clientPM2 process 확인 및 재시작http://localhost:3000/api/health응답 확인
main에 merge되면 배포가 바로 실행되므로, GitHub branch protection에서 pre-commit, CI / Client, CI / Agent를 required check로 설정한다.
# Agent 상태
sudo systemctl status cscl-agent
sudo journalctl -u cscl-agent --no-pager | tail -30
# Client 상태
pm2 list
pm2 logs cscl-client --lines 30
# nginx 상태
sudo systemctl status nginx
sudo nginx -t| 항목 | 내용 |
|---|---|
| 운영 모드 저장 | Supabase app_settings.agent_mode에 pipeline 또는 realtime 저장 |
| Realtime 상호작용 role 저장 | Supabase app_settings.agent_role에 dominant 또는 collaborative 저장 |
| Agent entrypoint | agent/main.py에 pipeline-agent, realtime-agent 등록 |
| 프롬프트 분리 | 그룹 대화는 agent/prompt_pipeline.py, 개별 대화 기본값은 prompts/realtime/*.md |
| Realtime 의존성 | livekit-agents[openai,silero,turn-detector]~=1.5 |
| Egress 업로드 | S3_ENDPOINT 값이 http로 시작하는 경우만 endpoint로 사용 |
세션 시작 시 자동으로 LiveKit Egress API가 호출되어 모든 참가자(학생 + AI) 음성이 혼합된 MP3 파일이 S3에 저장됩니다.
- 저장 경로:
s3://tblt-agent-recordings/recordings/{룸명}-{LiveKit room SID}-{타임스탬프}.mp3 - 트리거:
session.start()직후 자동 시작 - 종료: 룸
disconnected이벤트 발생 시 자동 종료 - 관련 코드:
agent/egress_recorder.py
- 로컬 다운로드:
scripts/download_s3_recordings.sh [output_dir] - 날짜 필터:
SINCE_YYYYMMDD=20260701 scripts/download_s3_recordings.sh [output_dir] - 삭제 dry run:
BEFORE_YYYYMMDD=20260601 scripts/delete_s3_recordings.sh - 실제 삭제:
BEFORE_YYYYMMDD=20260601 APPLY=true scripts/delete_s3_recordings.sh - LastModified 기준 삭제:
BEFORE_LAST_MODIFIED=2026-06-01T00:00:00Z scripts/delete_s3_recordings.sh
Egress는 LiveKit Cloud 인프라에서 실행되므로 서버 부하 없음. 단, S3 버킷 및 IAM 권한(
s3:PutObject) 설정이 되어 있어야 함.
S3에 저장된 녹음 파일을 로컬로 받거나 정리할 때는 아래 스크립트를 사용한다.
사전 조건:
- 로컬에 AWS CLI(
aws)가 설치되어 있어야 한다. .env또는 지정한ENV_FILE에 최소한 다음 값이 있어야 한다.S3_BUCKETS3_REGIONAWS_ACCESS_KEY또는AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY
- AWS IAM 권한:
- 다운로드:
s3:ListBucket,s3:GetObject - 삭제:
s3:ListBucket,s3:DeleteObject
- 다운로드:
스크립트는 기본적으로 프로젝트 루트의 .env를 읽는다. 운영 서버의 값을 쓰고 싶으면 ENV_FILE=/opt/cscl-tblt/.env처럼 지정하면 된다.
전체 녹음 파일을 로컬 디렉터리로 다운로드한다.
scripts/download_s3_recordings.sh기본 다운로드 경로는 ./downloads/recordings이다. 다른 경로로 받고 싶으면 첫 번째 인자로 넘긴다.
scripts/download_s3_recordings.sh ./tmp/recordings파일명에 포함된 날짜 기준으로 특정 시점 이후만 받고 싶으면 SINCE_YYYYMMDD를 사용한다. 형식은 반드시 YYYYMMDD다.
SINCE_YYYYMMDD=20260701 scripts/download_s3_recordings.sh
SINCE_YYYYMMDD=20260601 scripts/download_s3_recordings.sh ./tmp/recordings이 필터는 S3 LastModified가 아니라 파일명 규칙 recordings/{룸명}-{LiveKit room SID}-YYYYMMDD_HHMMSS.mp3의 날짜 부분을 기준으로 동작한다.
사용 가능한 환경변수:
ENV_FILE: 읽어올 env 파일 경로S3_PREFIX: 기본값recordingsSINCE_YYYYMMDD: 해당 날짜 이상인 파일만 다운로드S3_ENDPOINT: Cloudflare R2, MinIO 같은 S3 호환 스토리지용 endpoint
S3 객체 삭제는 실수 비용이 크므로 기본값이 dry run이다. APPLY=true를 주지 않으면 실제 삭제하지 않고 삭제 대상만 출력한다.
파일명 날짜 기준으로 특정 날짜 이전 파일을 확인:
BEFORE_YYYYMMDD=20260601 scripts/delete_s3_recordings.sh파일명 날짜 기준으로 실제 삭제:
BEFORE_YYYYMMDD=20260601 APPLY=true scripts/delete_s3_recordings.shS3 객체의 LastModified 기준으로 특정 시각 이전 파일을 확인:
BEFORE_LAST_MODIFIED=2026-06-01T00:00:00Z scripts/delete_s3_recordings.shLastModified 기준 실제 삭제:
BEFORE_LAST_MODIFIED=2026-06-01T00:00:00Z APPLY=true scripts/delete_s3_recordings.sh주의사항:
BEFORE_YYYYMMDD와BEFORE_LAST_MODIFIED는 동시에 설정할 수 없다.BEFORE_YYYYMMDD형식은YYYYMMDD다.BEFORE_LAST_MODIFIED형식은 UTC ISO-8601이어야 한다. 예:2026-06-01T00:00:00Z- 실제 삭제 전에 dry run 출력이 의도한 대상과 맞는지 먼저 확인하는 것이 좋다.
운영 서버 env를 직접 써서 삭제 대상을 확인하려면:
ENV_FILE=/opt/cscl-tblt/.env BEFORE_YYYYMMDD=20260601 scripts/delete_s3_recordings.sh- 그룹 대화 모드 기준: 동시 30명 = 약 15개 Agent 세션 (2인 1그룹)
- 그룹 대화 모드 병목: Silero VAD (10ms 단위 로컬 신경망 추론, 세션당
812% CPU) - 그룹 대화 모드 STT/LLM/TTS: LiveKit Cloud inference에서 처리
- 개별 대화 모드: 학생 1명당 realtime Agent 세션 1개
- 개별 대화 모드에는 OpenAI Realtime 사용량과 동시 세션 한도 확인 필요
- 현재 production 기준은 Capacity Plan을 따른다.
- t3 계열은 지속 고부하 수업 운영 시 CPU 크레딧 소진으로 스로틀링될 수 있다.