CoolFace
Apppublic

jeongsoo/RAG6_DockerLLM_API_Service

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
vito_stt.py246 linesDownload Raw Back to utils
1# -*- coding: utf-8 -*-2"""3VITO API를 사용한 음성 인식(STT) 모듈4"""5 6import os7import logging8import requests9import json10import time11from dotenv import load_dotenv12 13# 환경 변수 로드14load_dotenv()15 16# 로거 설정17logger = logging.getLogger("VitoSTT")18# 기본 로깅 레벨 설정 (핸들러가 없으면 출력이 안될 수 있으므로 기본 핸들러 추가 고려)19if not logger.hasHandlers():20    handler = logging.StreamHandler()21    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')22    handler.setFormatter(formatter)23    logger.addHandler(handler)24    logger.setLevel(logging.INFO) # 기본 레벨 INFO로 설정25 26class VitoSTT:27    """VITO STT API 래퍼 클래스"""28 29    def __init__(self):30        """VITO STT 클래스 초기화"""31        self.client_id = os.getenv("VITO_CLIENT_ID")32        self.client_secret = os.getenv("VITO_CLIENT_SECRET")33 34        if not self.client_id or not self.client_secret:35            logger.warning("VITO API 인증 정보가 .env 파일에 설정되지 않았습니다.")36            logger.warning("VITO_CLIENT_ID와 VITO_CLIENT_SECRET를 확인하세요.")37            # 에러를 발생시키거나, 기능 사용 시점에 체크하도록 둘 수 있습니다.38            # 여기서는 경고만 하고 넘어갑니다.39        else:40            logger.info("VITO STT API 클라이언트 ID/Secret 로드 완료.")41 42        # API 엔드포인트43        self.token_url = "https://openapi.vito.ai/v1/authenticate"44        self.stt_url = "https://openapi.vito.ai/v1/transcribe"45 46        # 액세스 토큰47        self.access_token = None48        self._token_expires_at = 0 # 토큰 만료 시간 추적 (선택적 개선)49 50    def get_access_token(self):51        """VITO API 액세스 토큰 획득"""52        # 현재 시간을 가져와 토큰 만료 여부 확인 (선택적 개선)53        now = time.time()54        if self.access_token and now < self._token_expires_at:55            logger.debug("기존 VITO API 토큰 사용")56            return self.access_token57 58        if not self.client_id or not self.client_secret:59            logger.error("API 키가 설정되지 않아 토큰을 획득할 수 없습니다.")60            raise ValueError("VITO API 인증 정보가 설정되지 않았습니다.")61 62        logger.info("VITO API 액세스 토큰 요청 중...")63        try:64            response = requests.post(65                self.token_url,66                data={"client_id": self.client_id, "client_secret": self.client_secret},67                timeout=10 # 타임아웃 설정68            )69            response.raise_for_status() # HTTP 오류 발생 시 예외 발생70 71            result = response.json()72            self.access_token = result.get("access_token")73            expires_in = result.get("expires_in", 3600) # 만료 시간 (초), 기본값 1시간74            self._token_expires_at = time.time() + expires_in - 60 # 60초 여유75 76            if not self.access_token:77                logger.error("VITO API 응답에서 토큰을 찾을 수 없습니다.")78                raise ValueError("VITO API 토큰을 받아오지 못했습니다.")79 80            logger.info("VITO API 액세스 토큰 획득 성공")81            return self.access_token82        except requests.exceptions.Timeout:83            logger.error(f"VITO API 토큰 획득 시간 초과: {self.token_url}")84            raise TimeoutError("VITO API 토큰 획득 시간 초과")85        except requests.exceptions.RequestException as e:86            logger.error(f"VITO API 토큰 획득 실패: {e}")87            if hasattr(e, 'response') and e.response is not None:88                 logger.error(f"응답 코드: {e.response.status_code}, 내용: {e.response.text}")89            raise ConnectionError(f"VITO API 토큰 획득 실패: {e}")90 91 92    def transcribe_audio(self, audio_bytes, language="ko"):93        """94        오디오 바이트 데이터를 텍스트로 변환95 96        Args:97            audio_bytes: 오디오 파일 바이트 데이터98            language: 언어 코드 (기본값: 'ko')99 100        Returns:101            인식된 텍스트 또는 오류 메시지를 포함한 딕셔너리102            {'success': True, 'text': '인식된 텍스트'}103            {'success': False, 'error': '오류 메시지', 'details': '상세 내용'}104        """105        if not self.client_id or not self.client_secret:106            logger.error("API 키가 설정되지 않았습니다.")107            return {"success": False, "error": "API 키가 설정되지 않았습니다."}108 109        try:110            # 토큰 획득 또는 갱신111            # (선택적 개선: 만료 시간 체크 로직 추가 시 self._token_expires_at 사용)112            if not self.access_token or time.time() >= self._token_expires_at:113                logger.info("VITO API 토큰 획득/갱신 시도...")114                self.get_access_token()115 116            headers = {117                "Authorization": f"Bearer {self.access_token}"118            }119 120            files = {121                "file": ("audio_file", audio_bytes) # 파일명 튜플로 전달122            }123 124            # API 설정값 (필요에 따라 수정)125            config = {126                "use_multi_channel": False,127                "use_itn": True, # Inverse Text Normalization (숫자, 날짜 등 변환)128                "use_disfluency_filter": True, # 필러 (음, 아...) 제거129                "use_profanity_filter": False, # 비속어 필터링130                "language": language,131                # "type": "audio" # type 파라미터는 VITO 문서상 필수 아님 (자동 감지)132            }133            data = {"config": json.dumps(config)}134 135            logger.info(f"VITO STT API ({self.stt_url}) 요청 전송 중...")136            response = requests.post(137                self.stt_url,138                headers=headers,139                files=files,140                data=data,141                timeout=20 # 업로드 타임아웃142            )143            response.raise_for_status()144 145            result = response.json()146            job_id = result.get("id")147 148            if not job_id:149                logger.error("VITO API 작업 ID를 받아오지 못했습니다.")150                return {"success": False, "error": "VITO API 작업 ID를 받아오지 못했습니다."}151 152            logger.info(f"VITO STT 작업 ID: {job_id}, 결과 확인 시작...")153 154            # 결과 확인 URL155            transcript_url = f"{self.stt_url}/{job_id}"156            max_tries = 15 # 최대 시도 횟수 증가157            wait_time = 2 # 대기 시간 증가 (초)158 159            for try_count in range(max_tries):160                time.sleep(wait_time) # API 부하 감소 위해 대기161                logger.debug(f"결과 확인 시도 ({try_count + 1}/{max_tries}) - URL: {transcript_url}")162                get_response = requests.get(163                    transcript_url,164                    headers=headers,165                    timeout=10 # 결과 확인 타임아웃166                )167                get_response.raise_for_status()168 169                result = get_response.json()170                status = result.get("status")171                logger.debug(f"현재 상태: {status}")172 173                if status == "completed":174                    # 결과 추출 (utterances 구조 확인 필요)175                    utterances = result.get("results", {}).get("utterances", [])176                    if utterances:177                         # 전체 텍스트를 하나로 합침178                        transcript = " ".join([seg.get("msg", "") for seg in utterances if seg.get("msg")]).strip()179                        logger.info(f"VITO STT 인식 성공 (일부): {transcript[:50]}...")180                        return {181                            "success": True,182                            "text": transcript183                            # "raw_result": result # 필요시 전체 결과 반환184                        }185                    else:186                         logger.warning("VITO STT 완료되었으나 결과 utterances가 비어있습니다.")187                         return {"success": True, "text": ""} # 성공이지만 텍스트 없음188 189                elif status == "failed":190                    error_msg = f"VITO API 변환 실패: {result.get('message', '알 수 없는 오류')}"191                    logger.error(error_msg)192                    return {"success": False, "error": error_msg, "details": result}193 194                elif status == "transcribing":195                    logger.info(f"VITO API 처리 중... ({try_count + 1}/{max_tries})")196                else: # registered, waiting 등 다른 상태197                    logger.info(f"VITO API 상태 '{status}', 대기 중... ({try_count + 1}/{max_tries})")198 199 200            logger.error(f"VITO API 응답 타임아웃 ({max_tries * wait_time}초 초과)")201            return {"success": False, "error": "VITO API 응답 타임아웃"}202 203        except requests.exceptions.HTTPError as e:204            # 토큰 만료 오류 처리 (401 Unauthorized)205            if e.response.status_code == 401:206                logger.warning("VITO API 토큰이 만료되었거나 유효하지 않습니다. 토큰 재발급 시도...")207                self.access_token = None # 기존 토큰 무효화208                try:209                    # 재귀 호출 대신, 토큰 재발급 후 다시 시도하는 로직 구성210                    self.get_access_token()211                    logger.info("새 토큰으로 재시도합니다.")212                    # 재시도는 이 함수를 다시 호출하는 대신, 호출하는 쪽에서 처리하는 것이 더 안전할 수 있음213                    return self.transcribe_audio(audio_bytes, language) # 재귀 호출 방식214 215                except Exception as token_e:216                    logger.error(f"토큰 재획득 실패: {token_e}")217                    return {"success": False, "error": f"토큰 재획득 실패: {str(token_e)}"}218 219            else:220                # 401 외 다른 HTTP 오류221                error_body = ""222                try:223                    error_body = e.response.text224                except Exception:225                    pass226                logger.error(f"VITO API HTTP 오류: {e.response.status_code}, 응답: {error_body}")227                return {228                    "success": False,229                    "error": f"API HTTP 오류: {e.response.status_code}",230                    "details": error_body231                }232 233        except requests.exceptions.Timeout:234            logger.error("VITO API 요청 시간 초과")235            return {"success": False, "error": "API 요청 시간 초과"}236        except requests.exceptions.RequestException as e:237            logger.error(f"VITO API 요청 중 네트워크 오류 발생: {str(e)}")238            return {"success": False, "error": "API 요청 네트워크 오류", "details": str(e)}239        except Exception as e:240            logger.error(f"음성인식 처리 중 예상치 못한 오류 발생: {str(e)}", exc_info=True)241            return {242                "success": False,243                "error": "음성인식 내부 처리 실패",244                "details": str(e)245            }246