Skip to content

Latest commit

 

History

History
715 lines (530 loc) · 32.4 KB

File metadata and controls

715 lines (530 loc) · 32.4 KB

English Speaking Practice — CSCL TBLT

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에도 반영되도록 한다.

아키텍처

현재 기본 구조: Next.js 클라이언트

브라우저 클라이언트 (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-agent worker를 실행한다. Realtime role은 dominant 또는 collaborative로 선택한다.
  • legacy 토큰 서버 (server/): client/static HTML 클라이언트용이다. 현재 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

사전 준비

1. uv 설치 (없는 경우)

curl -LsSf https://astral.sh/uv/install.sh | sh

2. .env 작성

cp .env.example .env
cp config.example.json config.json

config.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가 아니라 Supabase app_settings.agent_mode로 결정되며, /admin에서 변경한다. Supabase가 없는 로컬 개발 환경에서는 config.json fallback을 사용할 수 있다. Realtime 상호작용 role은 관리자 전용 agentRole 설정으로만 노출된다.

3. 에이전트 모델 파일 다운로드 (최초 1회)

cd agent
uv sync
uv run python main.py download-files

실행 방법

현재 Next.js 앱 실행

최초 1회 의존성을 설치한다.

pnpm setup

루트에서 다음 명령 하나로 agent worker와 Next.js 클라이언트를 함께 실행한다. pnpm dev의 auto 모드는 로컬 프로세스 선택을 위해 config.jsonagentMode/agentRole을 읽는다. 앱 route의 런타임 운영 설정은 Supabase app_settings를 사용하며, Supabase가 없는 로컬 fallback 환경에서는 config.json 값을 사용한다.

pnpm dev

브라우저에서:

Next.js 클라이언트는 자체 /api/token 라우트를 가지므로 server/ FastAPI 토큰 서버가 필요 없다.

개별 대화 모드 worker로 실행하려면 다음 명령을 사용한다.

pnpm dev:realtime

collaborative 조건으로 실행하려면 다음 명령을 사용한다.

pnpm dev:realtime:collaborative

pipeline, realtime dominant, realtime collaborative worker를 모두 함께 띄우려면 다음 명령을 사용한다.

pnpm dev:all

/admin에서 개별 대화 모드와 상호작용 방식을 선택하면 학생은 조건명을 보지 않고 개별 room으로 입장한다.

기존처럼 터미널을 나눠 실행해야 할 때는 다음 개별 명령을 사용할 수 있다.

pnpm dev:agent:pipeline
pnpm dev:client

legacy static 클라이언트 실행 (선택)

client/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 접속 → 이름 입력 → 입장 → 🤖 에이전트 생성 버튼 클릭.


CLI 단독 실험 (브라우저/프론트엔드 없이)

에이전트를 터미널에서 직접 실행해 백엔드만 빠르게 실험할 수 있습니다. 마이크와 스피커만 있으면 되고, 토큰 서버와 클라이언트는 불필요합니다.

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 모델 다운로드

Next.js 클라이언트

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_PASSWORD Basic Auth는 더 이상 사용하지 않는다.

Supabase runtime storage

Supabase 도입은 GitHub issue #4의 단계별 작업으로 진행한다. 현재 admin auth와 수업 운영 설정은 Supabase를 사용한다.

  • client/lib/supabase/client.ts: browser/client component용 Supabase client
  • client/lib/supabase/server.ts: server component, route handler, server action용 cookie-aware client
  • client/lib/supabase/admin.ts: 서버 전용 secret-key client
  • client/lib/supabase/proxy.ts: Auth session refresh와 admin route guard용 proxy/middleware helper
  • client/lib/settings-store.ts: app_settings 기반 운영 설정 store와 local fallback adapter
  • supabase/config.toml: 로컬 Supabase CLI 실행 포트와 migration 설정
  • supabase/migrations/20260611000000_foundation.sql: 초기 schema와 RLS policy
  • supabase/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.roleadmin이어야 /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_URLSUPABASE_SECRET_KEY 또는 SUPABASE_SERVICE_ROLE_KEY가 필요하다. Supabase가 미설정이거나 DB 장애가 있으면 운영 설정 route는 setup/runtime error를 반환한다. 로컬 개발에서는 Supabase가 없거나 일시적으로 실패할 때 config.json fallback을 사용한다.

기존 prompt_config.json override는 supabase/README.mdrealtime_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 status

client/.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 동작 테스트

마이크·브라우저 없이 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 이름 변경 .envROOM_NAME
에이전트 이름 변경 agent/main.py, client/lib/agent-role.ts (pipeline-agent, realtime-agent)
로그 저장 위치 변경 agent/logger.pyLOGS_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와 같은 검사를 수행합니다.


프로덕션 배포 (AWS EC2)

현재 production 기준값과 배포 절차는 docs/operations/를 따른다.

요약:

항목 현재 기준
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

CI/CD 파이프라인

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.localNEXT_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-agent active 확인
  • cscl-client PM2 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_modepipeline 또는 realtime 저장
Realtime 상호작용 role 저장 Supabase app_settings.agent_roledominant 또는 collaborative 저장
Agent entrypoint agent/main.pypipeline-agent, realtime-agent 등록
프롬프트 분리 그룹 대화는 agent/prompt_pipeline.py, 개별 대화 기본값은 prompts/realtime/*.md
Realtime 의존성 livekit-agents[openai,silero,turn-detector]~=1.5
Egress 업로드 S3_ENDPOINT 값이 http로 시작하는 경우만 endpoint로 사용

음성 녹음 (Egress)

세션 시작 시 자동으로 LiveKit Egress API가 호출되어 모든 참가자(학생 + AI) 음성이 혼합된 MP3 파일이 S3에 저장됩니다.

  • 저장 경로: s3://tblt-agent-recordings/recordings/{룸명}-{LiveKit room SID}-{타임스탬프}.mp3
  • 트리거: session.start() 직후 자동 시작
  • 종료: 룸 disconnected 이벤트 발생 시 자동 종료
  • 관련 코드: agent/egress_recorder.py

음성 녹음 (Egress) 다운로드 방법

  • 로컬 다운로드: 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_BUCKET
    • S3_REGION
    • AWS_ACCESS_KEY 또는 AWS_ACCESS_KEY_ID
    • AWS_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

전체 녹음 파일을 로컬 디렉터리로 다운로드한다.

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: 기본값 recordings
  • SINCE_YYYYMMDD: 해당 날짜 이상인 파일만 다운로드
  • S3_ENDPOINT: Cloudflare R2, MinIO 같은 S3 호환 스토리지용 endpoint

scripts/delete_s3_recordings.sh

S3 객체 삭제는 실수 비용이 크므로 기본값이 dry run이다. APPLY=true를 주지 않으면 실제 삭제하지 않고 삭제 대상만 출력한다.

파일명 날짜 기준으로 특정 날짜 이전 파일을 확인:

BEFORE_YYYYMMDD=20260601 scripts/delete_s3_recordings.sh

파일명 날짜 기준으로 실제 삭제:

BEFORE_YYYYMMDD=20260601 APPLY=true scripts/delete_s3_recordings.sh

S3 객체의 LastModified 기준으로 특정 시각 이전 파일을 확인:

BEFORE_LAST_MODIFIED=2026-06-01T00:00:00Z scripts/delete_s3_recordings.sh

LastModified 기준 실제 삭제:

BEFORE_LAST_MODIFIED=2026-06-01T00:00:00Z APPLY=true scripts/delete_s3_recordings.sh

주의사항:

  • BEFORE_YYYYMMDDBEFORE_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 크레딧 소진으로 스로틀링될 수 있다.