Conversation
|
Caution Review failedThe pull request is closed. WalkthroughFastAPI 앱에 S3 업로드/목록, Wav2Vec2 기반 한국어 음성 감정 분석, Google Speech-to-Text STT, 환경변수 자동 로드 및 실행/환경 가이드, 요구사항 목록이 추가되었고 관련 엔드포인트들이 통합되었습니다. Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related issues
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
Comment |
There was a problem hiding this comment.
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 제거 필요
io와numpy가 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: 로깅 개선 및 주석 명확화 필요두 가지 개선이 필요합니다:
print()대신 적절한 로깅 사용- 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
📒 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 고려
| 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) |
There was a problem hiding this comment.
🧩 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.
app/emotion_service.py
Outdated
| finally: | ||
| # 임시 파일 정리 | ||
| try: | ||
| import os | ||
| os.unlink(tmp_file_path) | ||
| except: | ||
| pass |
There was a problem hiding this comment.
임시 파일 정리 로직 개선 필요
여러 문제가 있습니다:
os모듈이 finally 블록 내부에서 import됩니다- bare except가 모든 예외를 무시합니다
- 정리 실패 시 로깅이 없습니다
다음 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.
| 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 | ||
| } |
There was a problem hiding this comment.
S3 업로드 후 파일 포인터 미리셋 및 키/입력값 검증 누락
upload_fileobj가file.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.
| 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.
| finally: | ||
| # 임시 파일 정리 | ||
| try: | ||
| os.unlink(tmp_file_path) | ||
| except: | ||
| pass |
There was a problem hiding this comment.
임시 파일 정리 로직 개선 필요
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.
| 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.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
app/emotion_service.py (3)
22-30: 모델 로딩 오류 처리 개선 권장현재
다음과 같이 개선하세요:
파일 상단에 로깅 추가:
+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: 임시 파일 정리 실패 로깅 개선 권장현재
다음 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
📒 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 인스턴스를 유지하는 패턴이 적절하게 구현되어 있습니다. 모델을 매번 로드하지 않아 효율적입니다.
| def upload_fileobj(bucket: str, key: str, fileobj) -> str: | ||
| s3 = get_s3_client() | ||
| s3.upload_fileobj(fileobj, bucket, key) | ||
| return key |
There was a problem hiding this comment.
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.
| 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.
🔎 Description
🔗 Related Issue
🏷️ What type of PR is this?
📋 Changes Made
🧪 Testing
📸 Screenshots (if applicable)
📝 Additional Notes
Summary by CodeRabbit
릴리스 노트
New Features
Documentation
Chores