Skip to content

[Feat] voice to text by stt#6

Merged
H4nnhoi merged 5 commits intomainfrom
5-feat-voice-to-text-by-stt
Oct 26, 2025
Merged

[Feat] voice to text by stt#6
H4nnhoi merged 5 commits intomainfrom
5-feat-voice-to-text-by-stt

Conversation

@H4nnhoi
Copy link
Contributor

@H4nnhoi H4nnhoi commented Oct 25, 2025

🔎 Description

convert voice file to text by Google STT API

🔗 Related Issue

🏷️ What type of PR is this?

  • ✨ Feature
  • ♻️ Code Refactor
  • 🐛 Bug Fix
  • 🚑 Hot Fix
  • 📝 Documentation Update
  • 🎨 Style
  • ⚡️ Performance Improvements
  • ✅ Test
  • 👷 CI
  • 💚 CI Fix

📋 Changes Made

  • create Translate API
  • create stt method

🧪 Testing

  • Unit tests pass
  • Integration tests pass
  • Manual testing completed

📸 Screenshots (if applicable)

스크린샷 2025-10-25 오후 7 16 39

📝 Additional Notes

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 음성 파일 업로드 및 S3 저장, 목록/상세 조회 기능 추가
    • 음성 감정 분석 기능 추가
    • 음성 전사(STT) 및 업로드+분석 통합 엔드포인트 추가
  • Documentation

    • Uvicorn 실행 명령 및 API 문서(URL) 안내 추가
    • .env 자동 로드 및 AWS S3 / Google STT 환경변수 설정 가이드 추가
  • Chores

    • 런타임 의존성 목록 업데이트

H4nnhoi and others added 3 commits October 20, 2025 20:48
* feat : create default APIs(upload, query list, and query specific) #1

* refactor : upload file at s3 #1
@H4nnhoi H4nnhoi linked an issue Oct 25, 2025 that may be closed by this pull request
6 tasks
@coderabbitai
Copy link

coderabbitai bot commented Oct 25, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

FastAPI 앱에 S3 업로드/목록, Wav2Vec2 기반 한국어 음성 감정 분석, Google Speech-to-Text STT, 환경변수 자동 로드 및 실행/환경 가이드, 요구사항 목록이 추가되었고 관련 엔드포인트들이 통합되었습니다.

Changes

Cohort / File(s) 변경 요약
문서 및 의존성
README.md, requirements.txt
Uvicorn 실행법 및 API docs URL 추가; .env 및 AWS/Google Cloud 환경 변수 설명 추가; 런타임 의존성(예: fastapi, uvicorn[standard], boto3, transformers, torch, librosa, google-cloud-speech 등) 명시
환경 초기화
app/__init__.py
애플리케이션 시작 시 .env를 자동으로 로드하도록 load_dotenv() 호출 추가
상수
app/constants.py
VOICE_BASE_PREFIX = os.getenv("S3_PREFIX", "voices") 추가; DEFAULT_UPLOAD_FOLDER 값을 "voiceFile"로 변경; ALLOWED_FOLDERS는 주석 처리되어 비활성화됨
감정 분석 서비스
app/emotion_service.py
EmotionAnalyzer 클래스 추가( Wav2Vec2 모델 로드, librosa 전처리, 추론, 감정·신뢰도·스코어 반환 ); 전역 인스턴스 emotion_analyzer 및 래퍼 analyze_voice_emotion 제공
S3 유틸리티
app/s3_service.py
get_s3_client(), upload_fileobj(), list_bucket_objects() 추가(페이징 처리 포함, 환경 기반 자격증명 주입)
STT 서비스
app/stt_service.py
GoogleSTTService 추가(자격증명 초기화, librosa 기반 전처리, transcribe_audio() ), 전역 인스턴스 stt_servicetranscribe_voice 래퍼 제공
API 엔드포인트
app/main.py
upload_voice, list_voices, get_voice, analyze_emotion, transcribe_speech, upload_voice_with_analysis 등 엔드포인트 추가 — S3 업로드·목록, 감정 분석, STT 호출을 조합한 흐름 구현

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant FastAPI as FastAPI Endpoint
    participant S3 as S3 Service
    participant Emotion as Emotion Analyzer
    participant STT as Google STT

    rect rgb(200,220,255)
    Note over Client,FastAPI: 클라이언트 업로드 및 분석 흐름
    Client->>FastAPI: POST /upload_voice_with_analysis (file, folder, language_code)
    FastAPI->>S3: upload_fileobj(bucket, key, file)
    S3-->>FastAPI: uploaded_key
    FastAPI->>Emotion: analyze_voice_emotion(file)
    Emotion-->>FastAPI: emotion_result
    FastAPI->>STT: transcribe_voice(file, language_code)
    STT-->>FastAPI: transcript_result
    FastAPI->>S3: list_bucket_objects(bucket, prefix)
    S3-->>FastAPI: object_list
    FastAPI-->>Client: {uploaded_key, object_list, emotion_result, transcript_result}
    end

    rect rgb(220,255,220)
    Note over Emotion: 감정 분석 내부 요약
    Emotion->>Emotion: 모델/feature-extractor 로드 (CUDA/CPU)
    Emotion->>Emotion: librosa로 로드(16kHz) → feature 변환 → 추론 → softmax → 라벨 매핑
    end

    rect rgb(255,220,220)
    Note over STT: STT 내부 요약
    STT->>STT: Google 클라이언트 초기화 (자격증명 처리)
    STT->>STT: librosa로 오디오 로드(16kHz), int16 변환
    STT->>STT: Speech-to-Text API 호출 → 결과 반환
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • 주의 필요 영역:
    • app/main.py: 엔드포인트 입력 검증, 파일 스트리밍/임시 파일 수명, 동기/비동기 경계
    • app/emotion_service.py: 모델 초기화 실패 처리, 메모리·장치 관리, 대용량 모델 로드 시 지연
    • app/stt_service.py: Google 자격증명 경로 처리, 음성 인코딩·샘플레이트 일관성, 예외 케이스
    • app/s3_service.py: 페이징 및 권한/예외 처리, 키/프리픽스 생성 일관성
    • requirements.txt: ML·오디오 라이브러리 버전 충돌 가능성

Possibly related issues

  • [Feat] voice to text by STT #5 — PR에 추가된 GoogleSTTService, transcribe_voice 래퍼 및 엔드포인트(transcribe_speech, upload_voice_with_analysis)가 "음성 → 텍스트(STT)" 기능 요구를 직접 구현하므로 관련 있음.

Poem

🐰 귀 쫑긋, 파일 하나 톡 넣으면,
바구니엔 S3가 반짝이고,
파형 속 숨결은 모델이 읽어,
구름에게 물어 문장으로 돌려주니,
토끼는 깡충 — 코드가 춤춘다 🥕🎵

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "[Feat] voice to text by stt"는 변경사항의 주요 목표와 직접적으로 관련이 있습니다. PR 목표에 명시된 대로 이 PR의 주요 목적은 "Google STT API를 사용하여 음성 파일을 텍스트로 변환"하는 것이며, 제목은 이 핵심 목표를 명확하게 요약하고 있습니다. 비록 구현에 감정 분석(emotion_service.py)과 S3 파일 업로드(s3_service.py) 같은 추가 기능이 포함되어 있지만, 이들은 STT 기능을 지원하는 보조적 기능으로 보이며, 제목은 개발자의 관점에서 본 주요 변경사항을 정확하게 나타냅니다.
Description Check ✅ Passed PR 설명은 저장소의 설명 템플릿 구조를 따르고 있으며, 모든 주요 섹션이 포함되어 있습니다. 설명, 관련 이슈, PR 타입(Feature), 변경사항, 테스트 상태, 스크린샷이 모두 제공되었으며, 템플릿의 필수 정보들이 적절하게 작성되어 있습니다. "변경사항" 섹션이 다소 간략하고 통합 테스트와 수동 테스트가 완료되지 않았으며 추가 노트 섹션이 비어 있지만, 이들은 모두 비필수적이거나 부분적으로 채워지는 것이 허용되는 항목들입니다.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04e7f0d and 990df49.

📒 Files selected for processing (2)
  • app/constants.py (1 hunks)
  • app/stt_service.py (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (13)
app/__init__.py (1)

1-2: 패키지 임포트 시 .env 자동 로드의 사이드이펙트

앱 실행 맥락이라면 OK입니다. 다만 라이브러리처럼 임포트될 가능성이 있다면 엔트리포인트(예: uvicorn 실행 시 최초 모듈)에서만 load_dotenv(override=False)를 호출하도록 옮기는 것을 권장합니다. 또한 .env.example 제공을 함께 고려해 주세요.

README.md (1)

34-47: 코드 블록 언어 지정 및 보안 주의 문구 추가

markdownlint(MD040) 경고 해소와 가독성을 위해 언어를 지정해 주세요. 또한 .env/서비스 키 커밋 방지 안내를 추가하는 것을 권장합니다.

-### AWS S3 설정
-```
+### AWS S3 설정
+```bash
 AWS_ACCESS_KEY_ID=...
 AWS_SECRET_ACCESS_KEY=...
 AWS_REGION=ap-northeast-2
 S3_BUCKET_NAME=your-bucket
 S3_PREFIX=voices

-### Google Cloud Speech-to-Text 설정
- +### Google Cloud Speech-to-Text 설정 +bash

서비스 계정 키 파일 경로 설정

GOOGLE_APPLICATION_CREDENTIALS=/path/to/your/service-account-key.json


-```
+```text
caring-voice/
├── app/
│   ├── __init__.py
│   └── main.py          # FastAPI 엔트리 포인트 및 엔드포인트
...

+> 주의: .env와 서비스 계정 키 파일은 절대 저장소에 커밋하지 마세요.



Also applies to: 53-62

</blockquote></details>
<details>
<summary>app/constants.py (1)</summary><blockquote>

`3-11`: **상수 타입 명시 및 실제 검증 연계**

`ALLOWED_FOLDERS`가 정의만 되고 사용되지 않습니다. 폴더 입력 검증에 활용하거나 제거해 일관성을 맞춰 주세요. 또한 타입 명시/불변 집합으로 안정성을 높일 수 있습니다.


```diff
-import os
+import os
+from typing import FrozenSet
 
 # 업로드 기본 베이스 프리픽스 (환경변수 S3_PREFIX로 오버라이드 가능)
-VOICE_BASE_PREFIX = os.getenv("S3_PREFIX", "voices")
+VOICE_BASE_PREFIX: str = os.getenv("S3_PREFIX", "voices")
 
 # 기본 폴더명 (요청에 folder 미지정 시 사용)
-DEFAULT_UPLOAD_FOLDER = "raw"
+DEFAULT_UPLOAD_FOLDER: str = "raw"
 
 # 필요 시 허용 폴더 집합 정의 (예: 검증용)
-ALLOWED_FOLDERS = {"raw", "processed", "public"}
+ALLOWED_FOLDERS: FrozenSet[str] = frozenset({"raw", "processed", "public"})
app/s3_service.py (2)

19-22: S3에 Content-Type 지정 누락

업로드 시 Content-Type을 저장하지 않아 다운로드/브라우저 처리 시 문제가 될 수 있습니다. 선택 매개변수로 전달해 저장하도록 바꿔 주세요.

-from typing import List
+from typing import List, Optional
@@
-def upload_fileobj(bucket: str, key: str, fileobj) -> str:
+def upload_fileobj(bucket: str, key: str, fileobj, content_type: Optional[str] = None) -> str:
     s3 = get_s3_client()
-    s3.upload_fileobj(fileobj, bucket, key)
+    if content_type:
+        s3.upload_fileobj(fileobj, bucket, key, ExtraArgs={"ContentType": content_type})
+    else:
+        s3.upload_fileobj(fileobj, bucket, key)
     return key

25-32: 대규모 버킷 처리 및 정렬 안정성

모든 키를 수집 후 슬라이싱하면 대규모 버킷에서 비효율적입니다. ContinuationToken을 노출하는 서버사이드 페이지네이션 또는 정렬 기준(예: 최신순)을 명시하는 것을 권장합니다.

필요 시 제가 list_objects_v2의 토큰 기반 페이지네이션을 반영한 헬퍼를 제안드릴 수 있습니다.

app/main.py (2)

57-61: 리스트 페이징은 서버사이드 토큰을 사용하는 방식 권장

현재는 전체 키를 가져온 뒤 슬라이싱합니다. S3의 ContinuationToken 기반 페이지네이션으로 바꾸면 대용량에서도 안정적이고 빠릅니다. 필요 시 토큰을 next로 반환하세요.


79-83: 블로킹 연산(off-CPU/네트워크) 비동기 엔드포인트 차단 가능성

boto3(블로킹 I/O)와 torch 추론(무거운 CPU/GPU)이 이벤트 루프를 점유할 수 있습니다. Thread/Process offloading을 고려하세요.

예:

from fastapi.concurrency import run_in_threadpool
emotion_result = await run_in_threadpool(analyze_voice_emotion, file)
stt_result = await run_in_threadpool(transcribe_voice, file, language_code)

또는 aioboto3 채택/작업 큐로 백그라운드 처리.

Also applies to: 114-119

app/emotion_service.py (3)

1-8: 사용되지 않는 import 제거 필요

ionumpy가 import되었지만 코드에서 사용되지 않습니다.

다음 diff를 적용하여 사용되지 않는 import를 제거하세요:

-import io
 import tempfile
 from typing import Dict, Any
 import librosa
 import torch
 from transformers import Wav2Vec2ForSequenceClassification, Wav2Vec2FeatureExtractor
-import numpy as np

17-29: 로깅 개선 필요

모델 로드 실패 시 print()를 사용하고 있으며, 광범위한 Exception을 캐치하고 있습니다. 프로덕션 환경에서는 적절한 로깅이 필요합니다.

다음 diff를 적용하여 로깅을 개선하세요:

+import logging
+
+logger = logging.getLogger(__name__)
+
 class EmotionAnalyzer:
     def __init__(self):
         # ... existing code ...
     
     def _load_model(self):
         """Hugging Face 모델 로드"""
         model_name = "jungjongho/wav2vec2-xlsr-korean-speech-emotion-recognition"
         
         try:
             self.model = Wav2Vec2ForSequenceClassification.from_pretrained(model_name)
             self.feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(model_name)
             self.model.to(self.device)
             self.model.eval()
-        except Exception as e:
-            print(f"모델 로드 실패: {e}")
+        except (OSError, RuntimeError) as e:
+            logger.error("모델 로드 실패: %s", e, exc_info=True)
             self.model = None
             self.feature_extractor = None

96-101: f-string 변환 플래그 사용 권장

에러 메시지에서 str(e) 대신 f-string 변환 플래그를 사용할 수 있습니다.

다음 diff를 적용하세요:

         except Exception as e:
             return {
-                "error": f"분석 중 오류 발생: {str(e)}",
+                "error": f"분석 중 오류 발생: {e!s}",
                 "emotion": "unknown",
                 "confidence": 0.0
             }
app/stt_service.py (3)

1-7: 사용되지 않는 import 제거 필요

io 모듈이 import되었지만 코드에서 사용되지 않습니다.

다음 diff를 적용하세요:

-import io
 import tempfile
 import os

15-34: 로깅 개선 및 주석 명확화 필요

두 가지 개선이 필요합니다:

  1. print() 대신 적절한 로깅 사용
  2. Line 29의 주석이 오해의 소지가 있습니다 - 이는 credentials_path가 없거나 파일이 존재하지 않을 때의 폴백입니다

다음 diff를 적용하세요:

+import logging
+
+logger = logging.getLogger(__name__)
+
 class GoogleSTTService:
     def __init__(self):
         # ... existing code ...
     
     def _initialize_client(self):
         """Google Cloud Speech-to-Text 클라이언트 초기화"""
         try:
             # 환경변수에서 서비스 계정 키 파일 경로 가져오기
             credentials_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
             
             if credentials_path and os.path.exists(credentials_path):
                 # 서비스 계정 키 파일로 인증
                 credentials = service_account.Credentials.from_service_account_file(
                     credentials_path,
                     scopes=["https://www.googleapis.com/auth/cloud-platform"]
                 )
                 self.client = speech.SpeechClient(credentials=credentials)
             else:
-                # 기본 인증 (환경변수 GOOGLE_APPLICATION_CREDENTIALS 설정됨)
+                # 기본 인증 (Application Default Credentials 사용)
                 self.client = speech.SpeechClient()
                 
-        except Exception as e:
-            print(f"Google STT 클라이언트 초기화 실패: {e}")
+        except (OSError, ValueError) as e:
+            logger.error("Google STT 클라이언트 초기화 실패: %s", e, exc_info=True)
             self.client = None

101-106: f-string 변환 플래그 사용 권장

에러 메시지에서 str(e) 대신 f-string 변환 플래그를 사용할 수 있습니다.

다음 diff를 적용하세요:

         except Exception as e:
             return {
-                "error": f"STT 처리 중 오류 발생: {str(e)}",
+                "error": f"STT 처리 중 오류 발생: {e!s}",
                 "transcript": "",
                 "confidence": 0.0
             }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6a9b5f and af2290c.

📒 Files selected for processing (8)
  • README.md (1 hunks)
  • app/__init__.py (1 hunks)
  • app/constants.py (1 hunks)
  • app/emotion_service.py (1 hunks)
  • app/main.py (1 hunks)
  • app/s3_service.py (1 hunks)
  • app/stt_service.py (1 hunks)
  • requirements.txt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/emotion_service.py (1)
app/main.py (1)
  • analyze_emotion (79-82)
app/main.py (3)
app/s3_service.py (2)
  • upload_fileobj (19-22)
  • list_bucket_objects (25-32)
app/emotion_service.py (2)
  • analyze_voice_emotion (115-117)
  • analyze_emotion (31-108)
app/stt_service.py (1)
  • transcribe_voice (119-121)
🪛 markdownlint-cli2 (0.18.1)
README.md

35-35: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


44-44: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.1)
app/emotion_service.py

26-26: Do not catch blind exception: Exception

(BLE001)


96-96: Do not catch blind exception: Exception

(BLE001)


98-98: Use explicit conversion flag

Replace with conversion flag

(RUF010)


107-107: Do not use bare except

(E722)


107-108: try-except-pass detected, consider logging the exception

(S110)

app/stt_service.py

32-32: Do not catch blind exception: Exception

(BLE001)


101-101: Do not catch blind exception: Exception

(BLE001)


103-103: Use explicit conversion flag

Replace with conversion flag

(RUF010)


111-111: Do not use bare except

(E722)


111-112: try-except-pass detected, consider logging the exception

(S110)

app/main.py

21-21: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


79-79: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


88-88: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


99-99: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🔇 Additional comments (8)
requirements.txt (1)

1-10: 의존성 재현성 및 호환성 확보

>= 최소 버전만 지정되어 있어 빌드 재현성이 약하고, torch/transformers/librosa 조합에서 런타임 충돌 가능성이 있습니다. 배포 환경별로 고정 버전(또는 constraints.txt) 사용을 권장합니다. 특히 CPU 전용/CUDA 별도 휠 고려가 필요합니다.

다음 스크립트로 충돌 여부를 빠르게 점검하세요(깨끗한 venv 가정).

app/emotion_service.py (3)

10-16: LGTM!

클래스 초기화 로직이 명확하고 적절합니다. CUDA 사용 가능 여부에 따른 디바이스 선택도 올바르게 구현되었습니다.


41-46: LGTM!

모델이 로드되지 않았을 때의 early return 로직이 적절합니다.


111-117: LGTM!

전역 인스턴스와 래퍼 함수가 적절하게 구현되어 깔끔한 API를 제공합니다.

app/stt_service.py (4)

36-52: LGTM!

메서드 시그니처와 클라이언트 초기화 확인 로직이 적절합니다.


82-93: LGTM!

STT 결과 처리 로직이 명확하고 적절합니다. 필요한 정보를 모두 반환하고 있습니다.


115-121: LGTM!

전역 인스턴스와 래퍼 함수가 적절하게 구현되어 일관된 API를 제공합니다.


69-76: 사용 사례에 맞는 STT 모델 선택 확인 필요

현재 구현에서 latest_long 모델을 사용하고 있습니다. Google Cloud 문서에 따르면 latest_long은 회의, 미디어, 장시간 오디오 같은 장형식 콘텐츠에 최적화되어 있으며, latest_short는 음성 명령이나 짧은 쿼리 같은 단시간 발화에 최적화되어 있습니다. 또한 장형식 오디오의 경우 Google에서는 LongRunningRecognize 또는 BatchRecognize를 권장합니다.

현재 코드는 동기식 client.recognize() 호출을 사용하고 있으며, 오디오 길이에 대한 명시적 제약이 없습니다. 실제 사용 사례를 확인하여 모델을 선택해주세요:

  • 주로 짧은 음성 명령/쿼리를 처리하는 경우: latest_short 추천
  • 회의, 강의, 장시간 녹음을 처리하는 경우: latest_long 사용, 비동기 API 고려

Comment on lines +48 to +56
try:
# 임시 파일로 저장
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
content = audio_file.file.read()
tmp_file.write(content)
tmp_file_path = tmp_file.name

# 오디오 로드 (16kHz로 리샘플링)
audio, sr = librosa.load(tmp_file_path, sr=16000)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

파일 포인터 리셋 필요 검증

audio_file.file.read()를 호출하면 파일 포인터가 소진됩니다. 동일한 UploadFile 객체에 대해 이 함수가 여러 번 호출될 경우 문제가 발생할 수 있습니다.

다음 스크립트를 실행하여 파일이 여러 번 처리되는지 확인하세요:

파일이 여러 번 처리되는 경우, 다음과 같이 파일 포인터를 리셋하는 것을 고려하세요:

         try:
             # 임시 파일로 저장
             with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
                 content = audio_file.file.read()
+                audio_file.file.seek(0)  # 파일 포인터 리셋
                 tmp_file.write(content)
                 tmp_file_path = tmp_file.name

🏁 Script executed:

#!/bin/bash
# Description: Check if the audio_file is used multiple times in the codebase

# Search for multiple calls to analyze_emotion or analyze_voice_emotion on the same file object
rg -n -C5 'analyze_voice_emotion|analyze_emotion' --type=py -g '!**/test/**'

Length of output: 3131


파일 포인터 소진 문제 확인 - 즉시 수정 필요

코드 검증 결과, 동일한 UploadFile 객체가 순차적으로 여러 함수에 전달되면서 파일 포인터가 소진되는 문제가 실제로 발생합니다:

  • /upload 엔드포인트 (app/main.py 32-37): upload_fileobj()에서 file.file을 읽은 후, analyze_voice_emotion(file)이 호출되는데 이미 파일 포인터가 EOF 상태입니다.

  • /voices/upload-and-analyze 엔드포인트 (app/main.py 110-118): upload_fileobj()analyze_voice_emotion(file)transcribe_voice(file, language_code) 순서로 동일한 파일 객체가 세 번 읽히려고 시도합니다.

필수 수정 사항 (app/emotion_service.py 50-51):

         try:
             # 임시 파일로 저장
             with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
                 content = audio_file.file.read()
+                audio_file.file.seek(0)  # 다른 함수에서도 읽을 수 있도록 포인터 리셋
                 tmp_file.write(content)
                 tmp_file_path = tmp_file.name
🤖 Prompt for AI Agents
In app/emotion_service.py around lines 48 to 56, the code reads directly from
the UploadFile.file which exhausts the file pointer and breaks downstream
readers; instead, read the entire uploaded file into memory once (bytes), then
create and use independent file-like objects (e.g., io.BytesIO or temporary
files) for each consumer or reset the pointer before each read. Update the
function to read bytes = await audio_file.read() (or file.file.read()), use that
bytes buffer to write the temp WAV or pass BytesIO copies into
librosa/transcription functions, ensure any temporary files are closed/removed,
and avoid reusing the original UploadFile.file without seeking.

Comment on lines 102 to 108
finally:
# 임시 파일 정리
try:
import os
os.unlink(tmp_file_path)
except:
pass
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

임시 파일 정리 로직 개선 필요

여러 문제가 있습니다:

  1. os 모듈이 finally 블록 내부에서 import됩니다
  2. bare except가 모든 예외를 무시합니다
  3. 정리 실패 시 로깅이 없습니다

다음 diff를 적용하여 개선하세요:

먼저 파일 상단에 os import 추가:

+import os
 import tempfile
 from typing import Dict, Any

그리고 finally 블록을 개선:

         finally:
             # 임시 파일 정리
             try:
-                import os
                 os.unlink(tmp_file_path)
-            except:
-                pass
+            except OSError as e:
+                logger.warning("임시 파일 삭제 실패: %s", tmp_file_path, exc_info=True)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.1)

107-107: Do not use bare except

(E722)


107-108: try-except-pass detected, consider logging the exception

(S110)

🤖 Prompt for AI Agents
In app/emotion_service.py around lines 102-108, the finally block currently
imports os inline, uses a bare except, and doesn't log failures; add import os
at the top of the file, and replace the finally block with a targeted cleanup
that calls os.unlink(tmp_file_path) inside a try, catch FileNotFoundError and
ignore it, catch OSError as e and log a warning (using the module logger or
logging.getLogger(__name__) if no logger variable exists) including the file
path and error details so cleanup failures are visible.

Comment on lines 24 to 45
bucket = os.getenv("S3_BUCKET_NAME")
if not bucket:
raise HTTPException(status_code=500, detail="S3_BUCKET_NAME not configured")

# 키: optional prefix/YYYYMMDD_originalname
base_prefix = VOICE_BASE_PREFIX.rstrip("/")
effective_prefix = f"{base_prefix}/{folder or DEFAULT_UPLOAD_FOLDER}".rstrip("/")
key = f"{effective_prefix}/{file.filename}"

# 파일을 S3에 업로드
upload_fileobj(bucket=bucket, key=key, fileobj=file.file)

# 감정 분석 수행
emotion_result = analyze_voice_emotion(file)

# DB가 없으므로, 버킷의 파일 목록을 반환
names = list_bucket_objects(bucket=bucket, prefix=effective_prefix)
return {
"uploaded": key,
"files": names,
"emotion_analysis": emotion_result
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

S3 업로드 후 파일 포인터 미리셋 및 키/입력값 검증 누락

  • upload_fileobjfile.file을 읽은 뒤 포인터가 EOF에 놓여 analyze_voice_emotion(file)에서 빈 입력을 읽게 됩니다. 업로드 직후 seek(0)이 필요합니다.
  • file.filename을 그대로 키에 사용하면 경로 구분자/이상 문자로 예기치 않은 키가 생성될 수 있습니다. os.path.basename 등으로 정규화하세요.
  • 컨텐츠 타입을 S3에 저장해 두는 것이 좋습니다.
  • folder는 허용 문자만 받도록 서버 단에서 검증을 권장합니다.
@@
-    key = f"{effective_prefix}/{file.filename}"
+    # 안전한 파일명 정규화
+    filename = os.path.basename(file.filename or "upload.wav")
+    key = f"{effective_prefix}/{filename}"
@@
-    upload_fileobj(bucket=bucket, key=key, fileobj=file.file)
+    # Content-Type 저장
+    upload_fileobj(bucket=bucket, key=key, fileobj=file.file, content_type=file.content_type)
+    # 이후 소비자를 위해 포인터 리셋
+    try:
+        file.file.seek(0)
+    except Exception:
+        pass
@@
-    emotion_result = analyze_voice_emotion(file)
+    emotion_result = analyze_voice_emotion(file)

입력 검증(선택):

@@
 async def upload_voice(
     file: UploadFile = File(...),
-    folder: Optional[str] = Form(default=None),  # 예: "raw" 또는 "user123/session1"
+    folder: Optional[str] = Form(default=None),  # 예: "raw" 또는 "user123/session1"
 ):
@@
-    base_prefix = VOICE_BASE_PREFIX.rstrip("/")
+    base_prefix = VOICE_BASE_PREFIX.rstrip("/")
+    # 허용 문자만 통과 (영숫자, /, _, -), 길이 제한
+    if folder is not None:
+        import re
+        if not re.fullmatch(r"[A-Za-z0-9/_-]{1,128}", folder):
+            raise HTTPException(status_code=400, detail="Invalid folder")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bucket = os.getenv("S3_BUCKET_NAME")
if not bucket:
raise HTTPException(status_code=500, detail="S3_BUCKET_NAME not configured")
# 키: optional prefix/YYYYMMDD_originalname
base_prefix = VOICE_BASE_PREFIX.rstrip("/")
effective_prefix = f"{base_prefix}/{folder or DEFAULT_UPLOAD_FOLDER}".rstrip("/")
key = f"{effective_prefix}/{file.filename}"
# 파일을 S3에 업로드
upload_fileobj(bucket=bucket, key=key, fileobj=file.file)
# 감정 분석 수행
emotion_result = analyze_voice_emotion(file)
# DB가 없으므로, 버킷의 파일 목록을 반환
names = list_bucket_objects(bucket=bucket, prefix=effective_prefix)
return {
"uploaded": key,
"files": names,
"emotion_analysis": emotion_result
}
bucket = os.getenv("S3_BUCKET_NAME")
if not bucket:
raise HTTPException(status_code=500, detail="S3_BUCKET_NAME not configured")
# 키: optional prefix/YYYYMMDD_originalname
base_prefix = VOICE_BASE_PREFIX.rstrip("/")
effective_prefix = f"{base_prefix}/{folder or DEFAULT_UPLOAD_FOLDER}".rstrip("/")
# 안전한 파일명 정규화
filename = os.path.basename(file.filename or "upload.wav")
key = f"{effective_prefix}/{filename}"
# 파일을 S3에 업로드
# Content-Type 저장
upload_fileobj(bucket=bucket, key=key, fileobj=file.file, content_type=file.content_type)
# 이후 소비자를 위해 포인터 리셋
try:
file.file.seek(0)
except Exception:
pass
# 감정 분석 수행
emotion_result = analyze_voice_emotion(file)
# DB가 없으므로, 버킷의 파일 목록을 반환
names = list_bucket_objects(bucket=bucket, prefix=effective_prefix)
return {
"uploaded": key,
"files": names,
"emotion_analysis": emotion_result
}
🤖 Prompt for AI Agents
In app/main.py around lines 24 to 45: after uploading the file pointer is left
at EOF and file.filename is used unsanitized for S3 key; fix by 1) sanitize the
filename (e.g., use os.path.basename and remove/normalize unsafe chars) and
validate/whitelist the folder value before use, 2) construct effective_prefix
and key using the sanitized folder and filename, 3) pass content_type when
calling upload_fileobj so S3 stores the MIME type, and 4) immediately call
file.file.seek(0) after upload_fileobj so analyze_voice_emotion(file) reads the
file from the start.

Comment on lines +107 to +112
finally:
# 임시 파일 정리
try:
os.unlink(tmp_file_path)
except:
pass
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

임시 파일 정리 로직 개선 필요

bare except가 모든 예외를 무시하고 있으며, 정리 실패 시 로깅이 없습니다.

다음 diff를 적용하세요:

         finally:
             # 임시 파일 정리
             try:
                 os.unlink(tmp_file_path)
-            except:
-                pass
+            except OSError as e:
+                logger.warning("임시 파일 삭제 실패: %s", tmp_file_path, exc_info=True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
finally:
# 임시 파일 정리
try:
os.unlink(tmp_file_path)
except:
pass
finally:
# 임시 파일 정리
try:
os.unlink(tmp_file_path)
except OSError as e:
logger.warning("임시 파일 삭제 실패: %s", tmp_file_path, exc_info=True)
🧰 Tools
🪛 Ruff (0.14.1)

111-111: Do not use bare except

(E722)


111-112: try-except-pass detected, consider logging the exception

(S110)

🤖 Prompt for AI Agents
In app/stt_service.py around lines 107 to 112, the temporary file cleanup uses a
bare except that suppresses all errors and emits no logs; change it to catch
specific exceptions (e.g., OSError) and log failures so cleanup errors are
visible. Replace the bare except with an except OSError as e (or Exception as e
if broader handling is required in your context) and call the module/logger
(e.g., logger.error or process_logger.error) with a clear message including
tmp_file_path and the exception details; keep attempting to remove the file but
do not silently swallow errors.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
app/emotion_service.py (3)

22-30: 모델 로딩 오류 처리 개선 권장

현재 print를 사용하여 오류를 출력하고 있습니다. 프로덕션 환경에서 로그 추적을 위해 표준 로깅을 사용하는 것을 권장합니다.

다음과 같이 개선하세요:

파일 상단에 로깅 추가:

+import logging
 import io
 import os
 import tempfile
+
+logger = logging.getLogger(__name__)

그리고 print를 logger로 변경:

         except Exception as e:
-            print(f"모델 로드 실패: {e}")
+            logger.error("모델 로드 실패: %s", e, exc_info=True)
             self.model = None
             self.feature_extractor = None

50-54: 파일 포인터 리셋 추가 권장

audio_file.file.read()로 파일을 읽은 후 포인터를 리셋하지 않아, 동일한 파일 객체를 재사용하려는 후속 코드에서 빈 내용을 읽게 될 수 있습니다. 현재는 app/main.py에서 호출 후 seek(0)을 수행하고 있어 완화되었지만, 방어적 프로그래밍 관점에서 이 함수 내에서도 리셋하는 것을 권장합니다.

다음 diff를 적용하세요:

             # 임시 파일로 저장
             with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
                 content = audio_file.file.read()
+                audio_file.file.seek(0)  # 다른 함수에서도 읽을 수 있도록 포인터 리셋
                 tmp_file.write(content)
                 tmp_file_path = tmp_file.name

105-108: 임시 파일 정리 실패 로깅 개선 권장

현재 print를 사용하고 있습니다. 프로덕션 환경에서의 디버깅을 위해 표준 로깅으로 변경하세요.

다음 diff를 적용하세요 (위의 로깅 import가 추가된 경우):

             try:
                 os.unlink(tmp_file_path)
             except OSError as e:
-                print(f"임시 파일 삭제 실패: {tmp_file_path}, 오류: {e}")
+                logger.warning("임시 파일 삭제 실패: %s, 오류: %s", tmp_file_path, e)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af2290c and 04e7f0d.

📒 Files selected for processing (3)
  • app/emotion_service.py (1 hunks)
  • app/main.py (1 hunks)
  • app/s3_service.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/emotion_service.py (1)
app/main.py (1)
  • analyze_emotion (86-89)
app/main.py (3)
app/s3_service.py (2)
  • upload_fileobj (25-28)
  • list_bucket_objects (31-38)
app/emotion_service.py (2)
  • analyze_voice_emotion (115-117)
  • analyze_emotion (32-108)
app/stt_service.py (1)
  • transcribe_voice (119-121)
🪛 Ruff (0.14.1)
app/emotion_service.py

27-27: Do not catch blind exception: Exception

(BLE001)


97-97: Do not catch blind exception: Exception

(BLE001)


99-99: Use explicit conversion flag

Replace with conversion flag

(RUF010)

app/main.py

21-21: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


40-41: try-except-pass detected, consider logging the exception

(S110)


40-40: Do not catch blind exception: Exception

(BLE001)


86-86: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


95-95: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


106-106: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


123-124: try-except-pass detected, consider logging the exception

(S110)


123-123: Do not catch blind exception: Exception

(BLE001)


130-131: try-except-pass detected, consider logging the exception

(S110)


130-130: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (9)
app/s3_service.py (2)

8-22: 자격증명 처리 개선 완료!

이전 리뷰에서 제기된 AWS_SESSION_TOKEN 누락 및 자격증명 주입 방식 문제가 모두 해결되었습니다. 조건부 자격증명 주입과 기본 크레덴셜 체인 폴백이 올바르게 구현되어 있습니다.


31-38: 페이지네이션 및 엣지 케이스 처리 우수!

S3 paginator를 사용하여 대용량 목록을 안전하게 처리하고, Contents가 없는 경우를 방어적으로 처리하는 로직이 잘 구현되어 있습니다.

app/main.py (6)

31-41: 파일명 정규화 및 파일 포인터 관리 개선 완료!

이전 리뷰에서 제기된 문제들이 해결되었습니다:

  • os.path.basename을 사용한 안전한 파일명 정규화
  • S3 업로드 후 seek(0) 호출로 후속 소비자를 위한 포인터 리셋

단, upload_fileobj 함수가 content_type 매개변수를 지원하도록 수정되어야 정상 작동합니다 (app/s3_service.py 리뷰 참조).


56-67: 목록 조회 로직 정상 작동!

페이지네이션을 위한 슬라이싱이 적절하게 구현되어 있습니다.


71-81: 더미 엔드포인트 확인

현재는 하드코딩된 더미 데이터를 반환하는 스텁 구현입니다. 향후 실제 로직 구현이 필요합니다.


85-89: 감정 분석 엔드포인트 정상!

단일 소비자 패턴으로 파일 포인터 문제가 없습니다.


93-100: STT 엔드포인트 정상!

음성을 텍스트로 변환하는 로직이 적절히 구현되어 있습니다.


115-134: 다중 소비자 간 파일 포인터 관리 개선 완료!

이전 리뷰에서 지적된 파일 포인터 리셋 누락 문제가 해결되었습니다:

  • 업로드 후 seek(0) (121-124번 라인)
  • 감정 분석 후 seek(0) (128-131번 라인)
  • STT 변환으로 순차 처리

각 단계 간 파일 포인터가 올바르게 리셋되어 모든 소비자가 전체 파일을 읽을 수 있습니다.

app/emotion_service.py (1)

111-117: 전역 인스턴스 패턴 적절!

모듈 수준에서 단일 analyzer 인스턴스를 유지하는 패턴이 적절하게 구현되어 있습니다. 모델을 매번 로드하지 않아 효율적입니다.

Comment on lines +25 to +28
def upload_fileobj(bucket: str, key: str, fileobj) -> str:
s3 = get_s3_client()
s3.upload_fileobj(fileobj, bucket, key)
return key
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Content-Type 매개변수 누락으로 런타임 오류 발생

app/main.py의 36번 및 120번 라인에서 이 함수를 content_type 매개변수와 함께 호출하고 있지만, 현재 함수 시그니처에는 해당 매개변수가 없어 TypeError가 발생합니다.

다음 diff를 적용하여 Content-Type을 S3 메타데이터로 저장하도록 수정하세요:

-def upload_fileobj(bucket: str, key: str, fileobj) -> str:
+def upload_fileobj(bucket: str, key: str, fileobj, content_type: str = None) -> str:
     s3 = get_s3_client()
-    s3.upload_fileobj(fileobj, bucket, key)
+    extra_args = {}
+    if content_type:
+        extra_args["ContentType"] = content_type
+    s3.upload_fileobj(fileobj, bucket, key, ExtraArgs=extra_args if extra_args else None)
     return key
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def upload_fileobj(bucket: str, key: str, fileobj) -> str:
s3 = get_s3_client()
s3.upload_fileobj(fileobj, bucket, key)
return key
def upload_fileobj(bucket: str, key: str, fileobj, content_type: str = None) -> str:
s3 = get_s3_client()
extra_args = {}
if content_type:
extra_args["ContentType"] = content_type
s3.upload_fileobj(fileobj, bucket, key, ExtraArgs=extra_args if extra_args else None)
return key
🤖 Prompt for AI Agents
In app/s3_service.py around lines 25 to 28, the upload_fileobj function is
missing the content_type parameter expected by callers, causing a TypeError and
also failing to set the S3 object's Content-Type; add a content_type:
Optional[str] = None parameter to the function signature and, when content_type
is provided, pass it to s3.upload_fileobj via the ExtraArgs argument
(ExtraArgs={'ContentType': content_type}) so the Content-Type is stored in S3
metadata; keep returning the key as before.

@H4nnhoi H4nnhoi merged commit e0f0bb6 into main Oct 26, 2025
1 check was pending
@H4nnhoi H4nnhoi deleted the 5-feat-voice-to-text-by-stt branch October 26, 2025 06:08
@H4nnhoi H4nnhoi restored the 5-feat-voice-to-text-by-stt branch October 26, 2025 06:09
H4nnhoi added a commit that referenced this pull request Oct 26, 2025
H4nnhoi added a commit that referenced this pull request Oct 26, 2025
@H4nnhoi H4nnhoi deleted the 5-feat-voice-to-text-by-stt branch October 26, 2025 06:16
This was referenced Oct 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] voice to text by STT

1 participant