마지막 업데이트 2026-07-22
기획 한 줄: 수업이 완료되면 수업 중 쌓인 MP3들을 시각순으로 이어 하나의 16kHz mono FLAC으로 만들고, GCS 업로드 → Google STT V2 Chirp3 배치 전사(화자분리 2인) → 보고서 생성 → Notion 문서까지 자동으로 잇는다. 상태는 별도 서비스가 아니라 기존 Lesson 데이터에 reportTranscription·reportGeneration으로 얹어 저장하고, 운영자는 보고서탭(일별 진행 현황·회기 보고서 세팅)과 세션 로그의 전사 탭에서 조회한다.
리뷰 시 먼저 볼 지점: ① MP3 시각순 결합·FLAC 표준화(report_merge.go), ② 중복 처리 방지(보고서는 원자적 claim, 전사 큐잉은 read-then-write라 비원자적), ③ 프롬프트 우선순위(개별>공통)와 인젝션 방어, ④ 화자분리를 켰는데 segments가 비는 loadTranscript 분기.
수업 완료 후 녹음·전사·보고서 결과가 여러 시스템과 로그로 흩어져, 운영자가 수업별 처리 상황과 원인을 한 화면에서 볼 수 없었다. 한 수업에서 MP3가 여러 개 생길 수 있어 실제 수업 흐름 순서를 보장한 전사 입력이 필요했고, 녹음 길이와 실제 진행 시간의 차이를 빠르게 점검할 수단도 없었다.
리포트 생성 완료까지만 이번 범위. 메시지·알림 발송은 포함하지 않는다.apps/stt), socket recordingManager(합본 MP3), 세션 로그(role=system,type=audio), 학습플랜(curriculum), report-generation.ts.| 역할 | 권한 | 구현 가드 |
|---|---|---|
| 관리자 | 일별 수업 상태·녹음·전사·리포트·Notion 문서 조회 | withAuthAdminOrDeveloper (reports/daily·recordings) |
| 개발자 | 전체 조회·관리, 실패 원인 확인, 재처리·설정 변경 | 동일 + curriculum 설정 API |
| 진행자 | 보고서탭 접근 대상 아님 (로그탭의 전사 조회는 허용) | 보고서탭은 admin/dev 게이팅, 로그/전사 탭은 세션 로그 화면에 노출 |
녹음 저장 + 실제 길이 기록 Socket
recording/recordingManager.ts localRecordingStorage.ts
수업 종료 시 합본 MP3를 storeRecording()(local/s3)로 저장하고, 실제 길이(ms)를 probe해 세션 로그 recordingDurationMs·caption에 기록.
수업 완료 → 전사 큐잉 트리거 Web API
PUT /api/lessons/[userId]/[index]
completionStatus === COMPLETED면 queueReportTranscription() 호출. 실패해도 수업 완료 자체는 성공 처리(전사만 재시도).
녹음 key 수집 → 전사 요청 Web lib
lib/report-transcription.ts
세션 로그에서 녹음 key를 시각순 수집. 없으면 waiting_for_audio(확인 필요), 있으면 requesting 기록 후 STT /transcriptions에 POST → transcribing(operationName 저장).
MP3 시각순 결합 → FLAC 표준화 → GCS 업로드 STT Go
handler/report_transcription.go audio/report_merge.go
key 검증·중복 제거 → 녹음 다운로드 → ffmpeg concat + 16kHz mono FLAC을 파이프로 스트리밍하며 GCS 업로드 → StartReportBatch.
Chirp3 배치 전사 + 폴링 STT Go
stt/client.go
BatchRecognize(ko-KR, chirp_3, 자동 문장부호, 화자분리 2인, word offsets, GCS output). asia-northeast1 / Recognizer _. goroutine이 완료까지 폴링.
콜백 수신 → 전사문·세그먼트 저장 → 보고서 큐잉 Web API
POST /api/reports/transcriptions/callback
토큰·operationName 검증 → reportTranscription completed/failed 갱신 → 완료면 queueReportGeneration().
보고서 생성 → Notion 문서 Web lib
lib/report-generation.ts
프롬프트 해석(개별>공통) → OpenAI로 보고서 → Markdown→Notion 블록 변환(100블록 단위 분할) → 페이지 생성 → reportGeneration.completed(notionPageUrl). 알림 발송 없음.
백로그 체크리스트 항목이 실제로 어디에 구현됐는지 대응표. 리뷰 시 "기획 요구가 코드에 다 들어갔는가"를 이 표로 대조한다.
| 기획 세부작업 | 구현 위치 |
|---|---|
| 수업 완료 시 진행 상태 저장·멱등 생성 | report-transcription.ts, lessons/[userId]/[index]/route.ts, report.types.ts(상태머신) |
| MP3 목록 시각순 확인·결합 | report-transcription.ts(key 수집) + audio/report_merge.go(concat) |
| 16kHz·mono·FLAC 표준화 | report_merge.go -ac 1 -ar 16000 -c:a flac |
| 신규 MP3 실제 길이 로그 저장 / 기존은 캡션 조회 | recordingManager.ts(probe), session-logs/route.ts, recording-captions fallback |
Chirp3 배치(ko-KR, asia-northeast1, Recognizer _, 화자 2) | stt/client.go, config/config.go |
| 작업 식별자·결과 경로·상태 저장 / 전사문·세그먼트·시각 저장 | callback → reportTranscription(operationName·inputGcsUri·text·segments) |
| 일별 진행 현황(상태·아동·수업진행시간·MP3 합계·재생·Notion) | reports/daily/route.ts, report-daily-progress.tsx, reports/recordings |
| 학습플랜별 공통/개별 프롬프트 설정(값만 저장) | report-session-settings.tsx, curriculums/[id]/route.ts |
| 로그탭: 로그/전사 탭 | session-log.tsx, transcript-result-table.tsx, session-log-transcript-preview.tsx |
| 사람이 읽는 상태명 + 실패 단계 구분 저장 | report-progress.ts(phase 매핑), toGenerationErrorMessage |
| 재처리 시 기존 결과·이력 보존 | callback 스프레드 보존 + operation-mismatch 409 |
여러 조각을 concat으로 입력 순서대로 이어 붙이고 무손실 FLAC로 재인코딩. 파이프라인 오디오 정합의 핵심.
중복 Notion 페이지·OpenAI 호출을 막는 유일한 원자적 락. 단, 전사 큐잉(queueReportTranscription)은 read-then-write라 이 보호가 없다(관전 포인트 참조).
회차별 개별 프롬프트가 공통보다 우선. promptSource를 결과에 저장해 운영 화면에서 출처를 확인.
재처리·지연 콜백 안전장치. 다른 operation 콜백은 409로 거부하고, 완료 시 이전 errorMessage만 벗겨내되 recordingS3Keys 등은 유지.
| 레이어 | 파일 | 핵심 변경 |
|---|---|---|
| STT(Go) | handler/report_transcription.go 신규 | /transcriptions 핸들러: key 검증·GCS 준비·배치 시작·폴링·콜백 |
| STT(Go) | audio/report_merge.go 신규 | ffmpeg concat + 16kHz mono FLAC 스트리밍 병합 |
| STT(Go) | stt/client.go | StartReportBatch/CheckReportBatch, 모델 env화, chirp_3 조건부 denoiser |
| STT(Go) | storage/{storage,local}.go 신규, s3.go | RecordingStorage 인터페이스 + local 구현 + DownloadKey(.. 탈출 방어) |
| Socket | recording/recordingManager.ts, localRecordingStorage.ts 신규 | storeRecording 전환, recordingDurationMs probe·전파, local/s3 분기 |
| Web-API | reports/transcriptions/{route,callback} 신규 | 전사 큐잉/조회, 콜백 수신·보고서 큐잉 |
| Web-API | reports/daily/route.ts 신규 | 날짜별 수업×전사×보고서 진행 현황 집계(admin/dev) |
| Web-API | lessons/[userId]/[index], curriculums/* | 완료 시 전사 큐잉 트리거, 리포트 프롬프트 파싱·검증·저장 |
| Web-lib | report-transcription.ts, report-generation.ts, report-progress.ts 신규 | 큐잉·생성·상태매핑 로직 |
| Web-UI | report-page.tsx, report-daily-progress.tsx, report-session-settings.tsx 신규 | 보고서탭(일별 현황 + 회기 세팅) |
| Web-UI | session-log.tsx, transcript-result-table.tsx | 로그/전사 탭, 세그먼트(구간·화자·내용) 테이블 |
| Types | db/report.types.ts 신규 외 | 상태머신·프롬프트·durationMs·completedAt 타입 |
전사 큐잉 멱등성이 원자적이지 않다queueReportTranscription은 getLesson→상태확인→updateLesson의 read-then-write라 락이 아니다(startReportGeneration과 달리 조건부 UpdateExpression 미사용). 완료 PUT이 동시 2회 오거나 완료 후 수동 재시도가 겹치면 배치 전사가 이중 제출될 수 있다. 보고서만 원자적이고 전사는 아니라는 비대칭.
화자분리를 켰는데 segments가 빌 수 있다loadTranscript가 len(alternative.Words)==0일 때만 segment를 추가한다. 배치 config는 diarization(word offsets)을 켜므로 words가 채워지고 → segments가 비어 화자/구간이 사라진다. TranscriptResultTable은 segments 기반이라 정상 케이스에서 전사 탭이 빌 소지. 실제 응답으로 확인 필요.
폴링 goroutine에 전체 데드라인/재개 경로 없음pollAndCallback은 context.Background()로 무기한 루프. 서버 재시작 중 완료된 배치는 콜백 유실 → transcribing에 영구 정체 가능(operationName은 저장돼 있으나 resume 경로 부재). GCS 준비+병합+업로드+배치시작이 단일 5분 컨텍스트에 묶인 점도 대용량 녹음에서 확인.
Chirp2 fallback은 코드 분기가 아니라 env 전환TC16의 Chirp2(asia-southeast1, 화자분리 없음) 대체는 자동 폴백 로직이 아니라 GCP_MODEL/GCP_LOCATION env 재설정으로만 이뤄진다. Chirp3 접근 오류 시 자동 전환은 없고 확인 필요 상태 + 원문 오류만 남는다(TC14/15).
local 저장소는 단일 호스트 공유 디렉터리 전제 · 기본값 localRECORDING_STORAGE=local은 socket·web·stt가 같은 /tmp/ppi-recordings를 공유해야 동작한다. 멀티 인스턴스/컨테이너 분리 배포에선 깨지므로 배포는 s3 필수. 기본값이 local인 점이 운영 오설정 위험(.env.example에 명시).
실패 단계 구분·기존 결과 보존은 촘촘함녹음없음/변환실패/업로드실패/전사요청실패/작업실패/조회실패/보고서실패/Notion실패를 구분 저장하고 toGenerationErrorMessage로 한글 매핑. 재처리 시 기존 필드 스프레드 보존 + operation-mismatch 409. 완료 트리거는 non-blocking(전사 실패가 수업 완료 API를 깨뜨리지 않음). 알림·메시지 발송은 의도대로 없음.
기획 TC1~16은 대부분 파이프라인 분기와 상태 전이를 다룬다. 자동 테스트는 Go 3개 파일뿐이고 web 상태머신·콜백·FE는 미검증이 가장 큰 리스크.
| 테스트 파일 | 검증 내용 |
|---|---|
audio/report_merge_test.go | ffmpeg 인자 조립 — 단일-map 0:a:0, 다중concat 순서 보존, 공통 16k/mono/flac, 빈 입력 에러 (TC1·TC2) |
handler/report_transcription_test.go | normalizedRecordingKeys — 배열 순서 보존+트림, 레거시 단일 key, 중복 key 거부 |
storage/local_test.go | LocalStorage Upload→DownloadKey 왕복·크기 일치, .. 루트 탈출 거부 |
검증 공백: mapReportTranscription·콜백 operation-mismatch/보존, startReportGeneration 조건부 claim, Notion 블록 변환/분할, 폴링·타임아웃 경로, FE 컴포넌트 전반에 자동 테스트가 없다. 핵심 멱등성/상태 전이 로직이 미검증.
관련 문서:
apps/stt 서버 구조