spar. wordmark

들어가며

  • 면접 준비, 특정 주제 토론, 가벼운 잡담까지. 각자 입장을 가진 AI 참여자들과 한 방에서 대화하고, 끝나면 내 말하기에 대한 피드백을 받는 앱을 만들기로 했다.
  • 구현에 들어가기 전에 요구사항, 구조, 모델 선택, 개발 방식, 그리고 쇼츠 영상 계획까지 설계를 먼저 정리한 포스팅
  • 모델 라인업과 가격은 현시점(2026-10) OpenAI 공식 문서 기준이다. 모델은 자주 바뀌므로 구현 직전에 한 번 더 확인한다.

결론부터: 만들 수 있나?

  • 만들 수 있다. 범위는 나 혼자 쓰는 안드로이드 PoC로 정했다. 스토어 출시는 하지 않는다.
    • OpenAI 키는 기기 안에 둔다. PoC라서 서버 없이 이전 stock-summary-app처럼 키를 암호화해 저장한다. (아래 “키 관리” 참고)
  • 안드로이드 쪽은 이전 앱에서 쓰던 환경(SDK, Galaxy_S23 에뮬레이터, Roborazzi, Maestro)을 그대로 쓸 수 있다.

앱 이름과 로고: spar.

spar. logo

  • spar는 복싱의 스파링에서 왔다. 실전 전에 상대와 가볍게 주고받으며 연습한다는 뜻이고, 영어로는 “말다툼하다, 논쟁하다”라는 의미도 있다. 면접 연습, 토론, 잡담을 한 단어로 묶을 수 있다.
  • 소문자 + 마침표(spar.) 표기로 짧고 단정하게 간다. 마침표는 “할 말은 했다”는 느낌이고, 앱 안에서는 라임색 포인트로 쓴다.
  • 로고는 두 말풍선이 겹친 모양이다.
    • 라임(#C6FF3D) = 나, 바이올렛(#7C5CFF) = AI 참여자
    • 겹친 흰 부분 = 서로 다른 입장이 부딪히는 지점. 이 앱에서 연습하려는 바로 그 구간이다.
    • 다크 배경 + 네온 라임 + 바이올렛은 요즘 AI 제품에서 흔히 보이는 조합이라 트렌디하면서도 아이콘 크기에서 잘 읽힌다.
  • 태그라인: “AI 스파링 파트너와 말로 붙는 연습장”

요구사항 정리

ID 기능 내용
FR-1 대화방 초안 생성 유저가 한 줄 프롬프트를 쓰면 주제, 참여자 수, 각 참여자의 입장·말투를 LLM이 초안으로 만들어 준다.
FR-2 대화방 설정 편집 초안을 그대로 쓰거나 주제, 모드, 언어, 참여자 추가·삭제, 입장, 내 역할을 직접 고친다.
FR-3 대화 유저는 텍스트 또는 음성으로 말한다. AI 참여자는 각자의 입장으로 대답한다. AI 참여자가 1명이면 실시간 음성 모드도 쓸 수 있다.
FR-4 음성 출력 (옵션) AI 참여자 발언을 참여자별 목소리로 읽어 준다.
FR-5 히스토리 대화방별로 대화 기록이 남는다. 진행 중(ACTIVE)인 방은 나중에 이어서 하고, 종료(ENDED)된 방은 읽기 전용으로 다시 본다. 같은 연습을 또 하려면 “다시 연습하기”로 다음 회차를 만든다.
FR-6 종료 피드백 “대화 종료”를 누르면 잘한 점, 못한 점, 고쳐야 할 점을 실제 발언을 근거로 정리해 준다.
FR-7 회차별 성장 그래프 같은 연습을 반복하면 피드백 점수를 회차별 그래프로 보여 준다.
FR-8 다국어 대화방마다 언어를 정한다. 영어 면접처럼 한국어가 아닌 대화도 연습할 수 있다.
FR-9 설정 API 키, 모델 프리셋(절약/기본/정밀), 음성 출력 ON/OFF, 음성 원본 보관 ON/OFF, 앱 기본 언어
  • 대화 모드는 세 가지로 시작한다. 모드에 따라 진행 방식과 피드백 기준이 달라진다.
모드 예시 진행 방식 피드백 기준
INTERVIEW “카카오 백엔드 2차 면접, 압박 면접관 1 + 실무 면접관 1” 면접관이 질문하고 유저가 답한다. 꼬리 질문이 나온다. 질문 의도 파악, 구조(STAR), 근거, 간결성
DEBATE “주 4일제 도입, 나는 찬성” 찬반 참여자가 반박한다. 사회자가 필요하면 초안이 AI 참여자 중 하나로 만든다(인원 제한에 포함). 논리, 반박 대응, 근거, 상대 논점 인정
CHAT “여행 좋아하는 친구 둘이랑 수다” 자유롭게 주고받는다. 대화 흐름, 질문하기, 리액션 (가볍게)

사용자 흐름

[홈: 대화방 목록]
   │  + 새 대화방
   ▼
[프롬프트 입력] "내일 토스 서버 면접. 깐깐한 리드 한 명, 컬처핏 보는 사람 한 명"
   │  초안 생성 (LLM, 구조화 출력). 너무 애매하면 질문을 하나 되묻고, 답을 받아 다시 생성
   ▼
[초안 확인/편집] 주제 · 모드 · 언어 · 내 역할 · 참여자 카드(이름/입장/말투/목소리)
   │  시작
   ▼
[대화방] 말풍선 타임라인 + 입력창(텍스트 / 길게 눌러 음성)
   │  대화 종료
   ▼
[피드백 리포트] 총평 · 잘한 점 · 못한 점 · 이렇게 바꿔 말하기 · 점수 · 다음 연습 제안
   │  각 항목을 누르면 근거가 된 내 발언으로 이동
   ▼
[홈] 대화방 카드에 "종료됨 · 피드백 있음" 표시

아키텍처

기술 스택: 안드로이드 네이티브 (Kotlin + Jetpack Compose)

  • 이전 stock-summary-app과 같은 스택이다. OpenAI 클라이언트, Keystore 키 저장, Room, DataStore, 테스트 구조(FakeBackend, Roborazzi, Maestro)를 그대로 가져온다.
  • 구현은 Claude Code가 전부 하므로 Claude Code가 빌드·테스트·실행까지 자동으로 돌릴 수 있는 구조가 중요하다. Gradle 하나로 빌드·단위 테스트를 돌리고, 에뮬레이터(Galaxy_S23)로 UI·E2E 테스트를 돌린다.

패키지 구조

spar/app/src/main/.../
├── data/db/             # Room: room, participant, message, room_memory, feedback, feedback_score, api_usage
├── data/prefs/          # DataStore 설정, Keystore 로 암호화한 API 키
├── data/net/            # OpenAI Responses / Audio API / Live API(WebSocket) 클라이언트
├── audio/               # 녹음(MediaRecorder), 재생(ExoPlayer), 실시간용 PCM 입출력(AudioRecord/AudioTrack)
├── domain/
│   ├── draft/           # 프롬프트 → 대화방 초안
│   ├── turn/            # 다음 발언자 결정 + 페르소나 발언 생성
│   ├── memory/          # 긴 대화 롤링 요약
│   ├── voice/           # STT / TTS 호출, 재생 큐
│   ├── live/            # 1:1 실시간 세션 (gpt-live-1)
│   ├── feedback/        # 종료 피드백 생성
│   └── progress/        # 회차별 점수 집계
└── ui/                  # Compose 화면: Home, Draft, Room, Feedback, Progress, Settings

데이터 모델 (로컬 DB)

  • 서버 없이 기기 안에 저장한다. 히스토리는 대화방 단위다.
테이블 주요 컬럼
room id, seriesId(1회차 대화방 id), round(회차), title, mode, topic, goal, userRole, language, scoringSet(채점 항목 묶음), rules(JSON), status(ACTIVE/ENDED), preset, createdAt, updatedAt
participant id, roomId, name, role, stance, speakingStyle, voice, isUser, sortOrder (openingSpeaker는 sortOrder 0번으로 저장)
message id, roomId, participantId, text, inputType(TEXT/VOICE/LIVE), audioPath?, createdAt
room_memory roomId, summary, coveredUntilMessageId (긴 대화 롤링 요약)
feedback id, roomId, model, schemaVersion, json(리포트 본문), createdAt
feedback_score feedbackId, criterion(채점 항목 ID), score (그래프용으로 점수만 따로 저장)
api_usage id, roomId, task(DRAFT/DIRECTOR/PERSONA/SUMMARY/FEEDBACK/STT/TTS/LIVE), model, inputTokens, cachedTokens, outputTokens, reasoningTokens, audioSeconds, createdAt (대화방별 비용 표시용)
  • 피드백 본문은 구조가 바뀔 수 있어서 JSON 한 칸에 넣는다. 화면은 schemaVersion을 보고 그린다.
  • 토큰·음성 사용량은 메시지가 아니라 API 호출 단위로 api_usage에 쌓는다. 진행자·요약·피드백처럼 메시지와 1:1로 묶이지 않는 호출이 많기 때문이다.
  • 음성 파일은 앱 전용 폴더에 두고, 설정에서 “음성 원본 보관”을 끄면 텍스트만 남긴다.

LLM 설계

1) 대화방 초안 생성 (FR-1)

  • 유저 프롬프트 한 줄을 받아 Structured Outputs(JSON Schema) 로 초안을 만든다. 파싱 실패가 없도록 스키마를 강제한다.
{
  "clarifyingQuestion": null,
  "title": "토스 서버 개발자 2차 면접",
  "mode": "INTERVIEW",
  "language": "ko",
  "topic": "서버 개발자 기술 + 컬처핏 면접",
  "goal": "대규모 트래픽 경험과 장애 대응을 설득력 있게 설명하기",
  "userRole": "지원자",
  "participants": [
    { "name": "민재", "role": "테크 리드", "stance": "답변의 근거와 수치를 끝까지 파고든다", "speakingStyle": "짧고 건조한 존댓말, 꼬리 질문 위주", "voice": "cedar" },
    { "name": "서연", "role": "피플 매니저", "stance": "협업 방식과 실패 경험에서 태도를 본다", "speakingStyle": "부드럽지만 구체적인 사례를 요구", "voice": "marin" }
  ],
  "openingSpeaker": "민재",
  "rules": ["한 번에 질문 하나", "답변이 모호하면 꼬리 질문"]
}
  • 프롬프트가 애매하면(예: “토론하고 싶어”) 모드·주제를 추측하지 말고 clarifyingQuestion에 되물을 질문을 넣는다. 이때 나머지 필드는 모두 null이고 앱은 무시한다. 유저가 답하면 원래 프롬프트 + 답으로 초안을 다시 만든다. 평소에는 clarifyingQuestion이 null이다.
  • 스키마 제약 (Structured Outputs는 모든 필드를 선언해야 하므로 선택 값은 null 허용으로 둔다)
    • mode: INTERVIEW / DEBATE / CHAT 중 하나
    • language: ko, en, ja 같은 언어 코드. 프롬프트에서 추론한다.
    • participants: AI 참여자만 1~4명. 유저를 더하면 2~5명이다. 6명 이상은 발언 순서가 꼬이고 비용이 빠르게 늘어난다.
    • voice: TTS가 지원하는 목소리 중 하나 (아래 “목소리” 참고)

2) 대화 진행 (FR-3): 진행자 + 페르소나 분리

  • 참여자가 여럿일 때 한 번의 호출로 모든 참여자 대사를 만들면 입장이 섞인다. (A가 B 말투를 따라 하거나, 모두가 비슷한 결론으로 수렴) 그래서 호출을 두 단계로 나눈다.
유저 발언 저장
   │
   ▼
[진행자(Director)]  가벼운 모델, 구조화 출력
   입력: 대화방 설정 + 최근 대화 + 롤링 요약
   출력: { "nextSpeakers": ["민재"], "intent": "수치 근거 꼬리 질문", "handBackToUser": true }
         메타 요청은 intent로 표현. 예) "다시 말해 줘" → intent: "직전 발언을 더 쉽게 다시 말하기"
   │
   ▼
[페르소나 발언]  참여자마다 별도 호출, 스트리밍
   프롬프트 순서: 공통 규칙 + 대화방 설정 (모든 호출에서 동일, 캐시됨)
                → 페르소나 블록 "너는 민재다. 입장: … 말투: …"
                → 롤링 요약 → 최근 N턴 → 진행자 의도
   │
   ▼
메시지 저장 → (음성 ON이면) TTS 큐에 넣기
  • 진행 규칙
    • AI끼리는 최대 2턴까지만 이어서 말하고 반드시 유저에게 넘긴다. 연습 앱이므로 유저 발언 비중이 높아야 한다.
    • 토론 모드에서는 진행자가 반대 입장 참여자를 우선 고른다.
    • 유저가 “잠깐”, “다시 말해 줘” 같은 메타 발언을 하면 진행자가 처리한다.
  • 컨텍스트 관리
    • 공통 규칙과 대화방 설정을 프롬프트 맨 앞에 글자 하나 다르지 않게 고정해 프롬프트 캐싱을 받는다. (캐시 입력은 일반 입력의 1/20 가격, 접두부가 1,024토큰 이상이어야 캐시된다)
    • 페르소나 블록은 그 뒤에 둔다. 앞에 두면 참여자마다 접두부가 달라져 캐시를 나눠 쓰지 못한다.
    • 최근 20턴은 원문, 그 이전은 room_memory 롤링 요약으로 대체한다. 요약은 20턴마다 가벼운 모델로 갱신한다.
  • OpenAI Responses API를 쓴다. 최신 모델의 도구 호출과 구조화 출력이 Responses API 기준이라서다.

3) 종료 피드백 (FR-6)

  • 대화 전체(메시지 ID 포함)와 모드별 채점 항목을 넣고, 구조화 출력으로 받는다. 예시는 뒤의 “회차별 성장 그래프” 예시 중 2회차 피드백이다. 2회차부터는 지난 회차의 nextPractice도 함께 넣어 이번에 고쳤는지 확인하게 한다.
{
  "summary": "기술 질문에는 강했지만 장애 회고에서 본인 역할이 흐려졌다.",
  "scores": [
    { "criterion": "structure", "score": 3 },
    { "criterion": "evidence", "score": 3 },
    { "criterion": "conciseness", "score": 3 },
    { "criterion": "intent", "score": 4 }
  ],
  "strengths":  [{ "point": "트래픽 수치를 구체적으로 제시", "messageIds": ["m12"] }],
  "weaknesses": [{ "point": "꼬리 질문에 같은 답을 반복", "messageIds": ["m18", "m20"] }],
  "rewrites": [{
    "messageId": "m18",
    "before": "그때는 팀 전체가 같이 대응해서…",
    "after": "제가 맡은 건 DB 커넥션 풀 고갈 원인 분석이었고, 30분 안에 …",
    "why": "'우리'가 아니라 '나'의 행동과 결과를 먼저 말해야 한다"
  }],
  "naturalExpressions": [],
  "previousPracticeCheck": [{
    "suggestion": "'우리' 대신 '나'로 말하기",
    "result": "'우리' 5번 → 1번으로 줄었다",
    "messageIds": ["m31"]
  }],
  "nextPractice": ["장애 회고를 STAR로 3분 안에 말하기"]
}
  • 요구사항의 세 가지는 이렇게 대응한다: 잘한 점 = strengths, 못한 점 = weaknesses, 고쳐야 할 점 = rewrites(이렇게 바꿔 말하기) + nextPractice(다음 연습 제안).
  • 근거 없는 칭찬/지적을 막기 위해 strengths, weaknesses, rewrites, naturalExpressions, previousPracticeCheck의 각 항목에 근거 발언 ID를 필수로 둔다. 화면에서 항목을 누르면 해당 발언으로 이동한다.
  • 스키마의 메시지 ID는 그 대화의 유저 발언 ID 목록(enum) 으로 제한하고, 파싱 후에도 유저 발언인지 한 번 더 확인해 아닌 항목은 버린다.
  • AI 참여자의 발언은 평가 대상이 아니다. 유저 발언만 평가하라고 명시한다.
  • 필드 설명
    • scores: 아래 “채점 항목” 표의 ID만 쓸 수 있다(enum). 모드·언어마다 스키마를 따로 만들어 항목 수를 고정한다. 잡담 모드는 빈 배열이다.
    • naturalExpressions: 외국어 연습일 때만 채운다. [{messageId, before, after}] 형태로 “더 자연스러운 표현”을 준다.
    • previousPracticeCheck: 2회차부터 채운다. 1회차는 빈 배열이다.
  • 음성으로 말한 발언은 STT 결과가 들어가므로 “음, 어” 같은 군말 빈도도 피드백에 넣을 수 있다. 다만 전사 모델이 군말을 지울 수 있어서, 전사 힌트에 군말이 들어간 예시를 넣어 남기도록 유도한다. (구현 때 실제로 남는지 확인)

4) 회차별 성장 그래프 (FR-7)

  • 무엇을 묶나: 같은 연습을 반복한 대화방들. 자동으로 “비슷한 주제”를 찾아 묶지는 않는다. 잘못 묶이면 그래프가 의미 없어지기 때문이다.
  • 어떻게 묶나: 피드백 화면의 “다시 연습하기” 버튼으로만 묶는다. 누르면 주제·참여자·모드·언어를 그대로 복사한 새 대화방이 생기고, 이전 대화방과 같은 묶음의 다음 회차가 된다. 새로 만든 대화방은 항상 새 묶음의 1회차다.
날짜 한 일 결과
10/12 “토스 서버 면접” 대화방을 만들어 연습 → 종료 1회차 피드백: 구조 2, 근거 3, 간결성 2, 의도 파악 3
10/14 피드백 화면에서 “다시 연습하기” → 같은 면접관들과 연습 → 종료 2회차 피드백: 구조 3, 근거 3, 간결성 3, 의도 파악 4
10/16 다시 “다시 연습하기” 3회차 피드백 + 1~3회차 그래프
  • 무엇을 그리나: 피드백을 만들 때 항목별로 1~5점을 매긴다. 회차끼리 비교할 수 있어야 하므로 채점 항목은 모드·언어마다 고정된 목록을 쓴다. 회차마다 항목이 바뀌면 점수를 비교할 수 없다.
  • 외국어 연습은 “대화방 언어 ≠ 앱 기본 언어”인 경우다. 이때는 언어 항목 2개를 더한다.
  • 어떤 채점 항목 묶음을 쓸지는 대화방을 만들 때 정해 room.scoringSet에 저장하고, “다시 연습하기” 때 그대로 복사한다. 나중에 앱 기본 언어를 바꿔도 같은 묶음 안에서는 항목이 바뀌지 않는다.
연습 종류 채점 항목 (ID)
면접 구조(structure), 근거(evidence), 간결성(conciseness), 질문 의도 파악(intent)
토론 논리(logic), 반박 대응(rebuttal), 근거(evidence), 상대 논점 인정(acknowledge)
외국어 연습 (면접·토론 공통 추가) 문법(grammar), 자연스러움(naturalness)
잡담 점수 없음, 그래프 없음 (가볍게 쓰는 모드)
  • 어디서 보나: 2회차부터 피드백 화면 아래에 그래프가 붙는다. 가로축은 회차, 세로축은 점수(1~5), 항목마다 선 하나다. 위 예시라면 이렇게 그려진다.
항목 1회차 2회차 변화
구조 2 3 ▲
근거 3 3 -
간결성 2 3 ▲
질문 의도 파악 3 4 ▲
  • 그래프 아래에는 지난 회차의 “다음 연습 제안”을 이번에 고쳤는지(previousPracticeCheck)를 함께 보여 준다. 예) “1회차 제안: ‘우리’ 대신 ‘나’로 말하기 → 2회차: ‘우리’ 5번 → 1번”
  • 차트는 선 그래프 하나라 Compose Canvas로 직접 그린다.

5) 다국어 (FR-8)

  • 대화방마다 language를 둔다. 초안 생성 때 프롬프트에서 추론한다. 예) “구글 영어 면접 연습” → en
  • 언어에 따라 바뀌는 곳
위치 동작
AI 참여자 대화방 언어로만 말한다. 유저가 한국어로 섞어 말해도 대화방 언어로 답한다.
STT gpt-transcribe에 대화방 언어를 언어 힌트로 준다.
TTS / 실시간 모드 대화방 언어로 말한다. 참여자별 목소리는 그대로 쓴다.
피드백 설명은 앱 기본 언어(한국어)로, rewrites.after와 naturalExpressions.after는 대화방 언어로 쓴다.
  • 외국어 연습(대화방 언어 ≠ 앱 기본 언어)이면 채점 항목에 문법·자연스러움을 더하고, 피드백에 naturalExpressions(더 자연스러운 표현)를 채운다.

모델 선택 (2026-10 기준)

  • 원칙: task 수준에 맞는 가장 싼 모델. 결과 품질이 사용자 경험을 좌우하는 곳(페르소나 대사, 피드백)에만 상위 모델을 쓴다.
Task 모델 설정 이유
대화방 초안 생성 gpt-6.1-sol reasoning medium, Structured Outputs 한 번만 호출하고, 참여자 입장 설계 품질이 대화 전체를 좌우한다
진행자(다음 발언자 결정) gpt-6-luna 구조화 출력 매 턴 호출되는 짧은 분류 작업
페르소나 발언 gpt-6.1-sol (기본) / gpt-6-luna (절약) reasoning low, 스트리밍 한국어 뉘앙스와 입장 일관성이 핵심. 절약 프리셋에서는 luna
롤링 요약 gpt-6-luna   대량·단순 작업
종료 피드백 gpt-6.1-sol (절약·기본) / gpt-6-astra (정밀) reasoning high 대화당 1회라 절약 프리셋에서도 sol을 쓴다. 정밀 프리셋은 플래그십
음성 입력(STT) gpt-transcribe 언어 힌트(대화방 언어), 키워드 힌트(주제·참여자 이름) 파일 전사 고정확도, $0.0045/분
음성 출력(TTS) gpt-4o-mini-tts 참여자별 voice + 말투 instructions 현재 문서상 최신 TTS. 말투 지시 가능
실시간 음성 모드 (2단계, 1:1만) gpt-live-1 Live API (WebSocket) 끼어들기 처리되는 양방향 음성. $0.05/분
실시간 모드 백엔드 gpt-6-luna (절약·기본) / gpt-6-sol (정밀) Responses 위임 깊은 추론을 위임받는 모델. 문서 권장 조합이고, gpt-6.1-sol 지원 여부는 구현 때 확인
  • 참고 가격 (100만 토큰당 입력/출력): gpt-6-astra $10/$50, gpt-6.1-sol $2/$10 (캐시 입력 $0.10), gpt-6-luna $0.10/$0.50
  • 이전 앱에서 쓰던 gpt-6-sol은 gpt-6.1-sol로 올린다.
  • 모델 ID는 코드에 박지 않고 설정의 프리셋 → 모델 매핑 테이블로 둔다. 모델이 바뀌면 매핑만 고친다.

음성은 왜 2단계로 나누나?

  • 1단계: 녹음 → STT → 텍스트 파이프라인 → TTS
    • 참여자가 여럿이어도 문제가 없다. 텍스트 대화와 같은 코드 경로를 타서 히스토리·피드백이 그대로 동작한다.
    • 음성 입력은 “길게 눌러 말하기”(push-to-talk). 놓으면 전사 결과를 입력창에 띄우고, 보내기 전에 고칠 수 있다.
    • TTS는 문장 단위로 잘라 순서대로 재생해서 첫 소리까지의 지연을 줄인다.
  • 2단계: gpt-live-1 실시간 모드
    • 진짜 면접처럼 말을 끊고 들어오는 경험은 Live API가 낫다. 한 세션이 하나의 목소리라서 1:1 대화(AI 참여자 1명)에서만 지원한다. 다자 실시간 대화는 하지 않는다.
    • 세션 중 전사 텍스트를 받아 message에 저장해 히스토리·피드백은 똑같이 쓴다.

음성 API는 어디에 쓰나?

위치 모델
내가 말한 걸 텍스트로 바꾸기 (길게 눌러 말하기) gpt-transcribe
AI 참여자 발언을 참여자별 목소리로 읽어 주기 (ON/OFF) gpt-4o-mini-tts
1:1 면접 실시간 음성 모드 (서로 끊고 들어오기 가능) gpt-live-1
  • 대화방 초안, 페르소나 대사, 종료 피드백은 텍스트 모델만 쓴다. 음성으로 한 발언도 텍스트로 바뀐 뒤 피드백에 들어간다.

음성 ↔ 텍스트 변환은 언제 일어나나?

  • 핵심은 응답 경로 중간에 텍스트가 끼어 있느냐다.
  • 기본 음성 모드: 변환이 직렬로 두 번 일어난다.

    내 음성 ──① 음성→텍스트──▶ 내 말(텍스트) ──② 답변 생성──▶ 답변(텍스트) ──③ 텍스트→음성──▶ 면접관 음성
                gpt-transcribe                    gpt-6.1-sol                     gpt-4o-mini-tts
    
    • 버튼을 뗀 뒤에야 ①이 시작되고, ① → ② → ③을 차례로 기다려야 해서 몇 초가 걸린다.
  • 실시간 모드: 응답 경로에 텍스트가 없다.

    내 음성 ──(계속 스트리밍)──▶ gpt-live-1 ──(계속 스트리밍)──▶ 면접관 음성
                                      │
                                      └─ 옆에서 텍스트 기록도 같이 생성 → 히스토리·피드백용
    
    • 모델 하나가 음성을 듣고 바로 음성으로 대답해서 빠르다. 말하는 동안에도 듣고 있어서 끼어들기가 된다.
    • 텍스트는 응답을 만드는 데 쓰이지 않고, 대화 저장과 종료 후 피드백을 위한 기록용으로만 따로 생긴다.
  기본 음성 모드 실시간 모드
말하는 방식 버튼 누르고 말하고, 떼면 전사 → 확인 후 전송 그냥 말함
응답 텍스트로 바뀐 뒤 답변을 읽어 줌 (몇 초) 바로 대답
끼어들기 안 됨 서로 끊고 들어갈 수 있음
면접관 수 여러 명 가능 1명 (한 세션에 목소리 하나)

실시간 모드 사용 예시

  • 내일 토스 서버 면접 연습, 면접관 “민재” 1명
    1. 실시간 모드를 켜면 버튼 없이 통화하듯 대화가 시작된다.
    2. 민재: “가장 큰 장애 경험 하나 말씀해 주세요.”
    3. 나: “작년에 결제 서버에서 DB 커넥션이 고갈돼서요, 그때 팀이 다 같이…”
    4. 민재가 말을 끊는다: “잠깐만요, ‘팀이’가 아니라 본인이 한 일만 말씀해 주세요.”
    5. 반대로 민재가 길게 말할 때 내가 “아, 질문을 다시 정리하면…” 하고 끼어들면 민재가 바로 멈추고 듣는다.
    6. 종료를 누르면 텍스트 기록으로 보통 대화방과 똑같이 피드백이 나온다. 예) “꼬리 질문에서 ‘우리’라는 표현이 3번 나왔다”
  • 실제 면접의 압박감과 순발력을 연습하는 용도다. 비용은 분당 $0.05에 백엔드 모델 사용료가 따로 붙는다.

실시간 모드 구조

  • 언제 쓸 수 있나: AI 참여자가 정확히 1명인 대화방(모드 무관)에서만 “실시간” 버튼이 보인다. 같은 대화방 안에서 텍스트·기본 음성과 실시간을 오갈 수 있고, 기록은 한 타임라인에 쌓인다.
  • 연결: 앱이 wss://api.openai.com/v1/live/sessions에 WebSocket으로 직접 붙는다. 문서는 키를 서버에 두라고 권장하지만, PoC라서 기기 키 + 프로젝트 사용 한도로 위험을 받아들인다.
  • 세션 시작(session.start)에 넣는 것
    • 페르소나 지시: 공통 규칙 + 대화방 설정 + “너는 민재다…” + 대화방 언어
    • 이전 맥락: 롤링 요약과 최근 대화를 지시문 텍스트로 붙인다. 세션 중에 바꿀 게 생기면 session.instructions.append를 쓴다.
    • 목소리: 참여자의 voice
    • 백엔드 위임: delegation.type = "responses", 모델은 위 표의 백엔드 모델
  • 기록: 내 말은 session.input_transcript.delta, 면접관 말은 session.output_transcript.delta를 모아 발화가 끝날 때마다 message(inputType = LIVE)로 저장한다. 그래서 종료 피드백은 다른 대화와 똑같이 동작한다.
  • 오디오: AudioRecord(VOICE_COMMUNICATION 소스 + AcousticEchoCanceler)로 PCM16 24kHz 모노를 보내고, 받은 음성은 AudioTrack으로 바로 재생한다. 에코 제거를 안 하면 면접관이 스피커로 나온 자기 목소리를 듣고 스스로 말을 끊는다. 이어폰 사용을 권장 문구로 띄운다. 기기에서 24kHz 에코 제거가 잘 안 되면 세션을 16kHz(audio/pcm 16000Hz)로 바꾼다.

목소리

  • TTS(gpt-4o-mini-tts)는 alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer, verse, marin, cedar를 지원하고, 문서는 marin, cedar를 최고 품질로 권장한다.
  • 초안의 기본 목소리는 marin, cedar 를 우선 쓴다. 실시간 모드 문서 예시에도 marin이 나와서 두 모드 모두에서 안전하다. 유저가 다른 TTS 목소리를 고르면, 실시간 모드에서 지원되지 않는 경우 비슷한 목소리로 바꿔 쓰는 매핑을 둔다. (실시간 모드 목소리 목록은 구현 때 확인)

대화 1회 비용 추정

  • 가정: 유저 15턴, AI 발언 25회, 호출당 입력 약 5K 토큰(그중 고정 접두부 3K는 캐시), 발언당 출력 150 + reasoning 300 토큰, 피드백은 입력 20K + 출력·reasoning 6K
항목 기본 프리셋 절약 프리셋
페르소나 발언 (25회) 약 $0.22 (캐싱 반영) 약 $0.02
진행자 (15회) 약 $0.01 약 $0.01
피드백 (1회, gpt-6.1-sol) 약 $0.10 약 $0.10
STT (음성 10분) $0.045 $0.045
합계 약 $0.38 (TTS 제외) 약 $0.18 (TTS 제외)
  • 초안 생성(대화방당 1회)과 롤링 요약은 합쳐도 1센트 안팎이라 표에서 뺐다.
  • 정밀 프리셋은 피드백이 gpt-6-astra라서 피드백만 약 $0.50이다. (20K × $10/M + 6K × $50/M)
  • 실시간 모드는 10분에 약 $0.50(분당 $0.05)이고, 백엔드 모델 사용료가 더해진다.
  • TTS는 읽어 주는 분량에 비례하므로 실제 사용량을 보고 다시 계산한다. 앱에 대화방별 비용 표시(api_usage 합계)를 넣어 추정치를 실측으로 바꾼다.

키 관리

  • PoC라서 서버를 두지 않는다. 이전 앱처럼 설정 화면에 키를 넣고, Android Keystore로 암호화해 저장한다.
  • 키가 새어도 피해가 작도록 OpenAI 대시보드에서 이 앱 전용 프로젝트와 월 사용 한도를 건다.
  • 나중에 다른 사람에게 배포하게 되면 그때 얇은 프록시(키는 서버에만, 앱에는 기기별 토큰)를 붙인다.
  • 대화 내용은 기기에만 저장한다. OpenAI로는 요청할 때만 보내고, Responses API 호출에는 store: false를 넣어 OpenAI 쪽에 응답이 저장되지 않게 한다.

개발 방식: 전부 Claude Code로

  • 코드 작성, 빌드, 테스트, 에뮬레이터 실행, 커밋까지 Claude Code가 한다. 나는 설계 리뷰와 실기기 확인만 한다.
  • 브라우저 작업은 Claude가 Claude in Chrome 스킬로 직접 Chrome을 조작한다.
    • OpenAI 대시보드에서 프로젝트·API 키 생성, 사용량 한도 설정
    • Kling, ElevenLabs 웹에서 쇼츠 소스 생성 (아래)
    • 블로그 배포 후 렌더링 확인
    • 결제·키 노출이 걸린 단계는 Claude가 멈추고 나에게 확인받는다.
  • 테스트 전략 (이전 앱 방식 그대로)
    • FakeBackend: 테스트 빌드에서는 OpenAI 호출을 가짜 응답으로 바꾼다. 초안 JSON, 페르소나 대사, 피드백 JSON, STT 결과를 고정값으로 돌려준다.
    • 시나리오 문서(docs/test-scenarios.md)에 ID를 붙이고 테스트 이름을 시나리오 ID로 시작한다.
    • 단위 테스트: 진행자 규칙, 롤링 요약 시점, 피드백 JSON 파싱, 회차 묶기
    • 스크린샷 테스트: Roborazzi
    • E2E: Maestro (에뮬레이터)
    • 실제 모델 품질 확인: LIVE_LLM=1일 때만 실제 호출하는 평가 테스트. 예) “토론 모드에서 반대 측 참여자가 3턴 연속 같은 입장을 유지하는가”를 LLM 채점으로 확인

유튜브 쇼츠 계획

  • 이전처럼 Kling으로 영상 클립, ElevenLabs로 한국어 내레이션을 만들고, 실제 앱 화면 녹화(에뮬레이터 screenrecord)를 섞는다. 길이는 50초 안쪽.
구간 화면 내레이션 (ElevenLabs) Kling 프롬프트 방향
0~5초 면접장 문 앞에서 긴장한 사람 “면접 전날, 연습할 상대가 없다면?” dark room, nervous young developer, neon lime rim light, cinematic
5~15초 프롬프트 한 줄 → 참여자 카드가 펼쳐지는 앱 화면 “한 줄만 쓰면, 입장이 다른 AI 면접관들이 생깁니다.” 앱 녹화
15~30초 음성으로 답하고 꼬리 질문을 받는 장면 “말로 답하면, 바로 파고듭니다.” 앱 녹화 + speech bubbles colliding, lime and violet
30~42초 피드백 리포트, before/after 문장 “끝나면, 뭘 잘했고 뭘 고칠지 내 말 그대로 짚어 줍니다.” 앱 녹화
42~50초 로고 + 태그라인 “AI 스파링 파트너, spar.” logo reveal, two speech bubbles overlap into white
  • 앱 개발 과정 자체(Claude Code가 코드를 짜고 Chrome을 조작하는 화면)를 타임랩스로 넣은 2편도 고려한다.

결정 사항

항목 결정
배포 범위 나 혼자 쓰는 안드로이드 PoC. 서버 없음, 키는 기기에 암호화 저장
회차별 그래프 넣는다. “다시 연습하기”로 이어 간 대화방끼리 묶어 항목별 점수 추이를 보여 준다.
실시간 모드 1:1만 지원한다.
다국어 넣는다. 대화방별 언어 설정, 영어 면접을 우선 확인한다.

참고

댓글남기기