From 31bc285949f61d32ca77ab096b51c250cc398fbf Mon Sep 17 00:00:00 2001 From: dlwlsrud815 Date: Thu, 8 May 2025 18:47:47 +0900 Subject: [PATCH 1/8] feat: record --- .gitignore | 3 +- app/convertFileExtension.py | 35 +++++++ app/record.py | 89 ++++++++++++++++++ .../af07bfb5-7b12-4209-8681-2d3f5dedfd11.mp3 | Bin 18513 -> 0 bytes .../b8016cd9-c376-47c2-8e87-cceb4abff687.mp3 | Bin 8900 -> 0 bytes requirements.txt | 15 +++ 6 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 app/record.py delete mode 100644 audio/af07bfb5-7b12-4209-8681-2d3f5dedfd11.mp3 delete mode 100644 audio/b8016cd9-c376-47c2-8e87-cceb4abff687.mp3 diff --git a/.gitignore b/.gitignore index 0497577..b24e2d7 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ venv.bak/ *.h5 __pycache__/ -*.pyc \ No newline at end of file +*.pyc +app/audio/ \ No newline at end of file diff --git a/app/convertFileExtension.py b/app/convertFileExtension.py index c7dee54..2bbe19e 100644 --- a/app/convertFileExtension.py +++ b/app/convertFileExtension.py @@ -2,6 +2,40 @@ from pydub import AudioSegment +from datetime import datetime + +def merge_all_wavs_to_mp3(audio_dir="audio", silence_duration_ms=500): + wav_files = sorted([ + os.path.join(audio_dir, f) for f in os.listdir(audio_dir) + if f.endswith(".wav") + ]) + + if not wav_files: + print("병합할 .wav 파일이 없습니다.") + return None + + print(f"{len(wav_files)}개의 wav 파일을 병합 중...") + + combined = AudioSegment.empty() + silence = AudioSegment.silent(duration=silence_duration_ms) + + for i, wav in enumerate(wav_files): + audio = AudioSegment.from_wav(wav) + combined += audio + if i != len(wav_files) - 1: + combined += silence # 마지막 파일 뒤에는 무음 안 넣음 + + today_str = datetime.now().strftime("%Y%m%d") + mp3_path = os.path.join(audio_dir, f"{today_str}_final.mp3") + + combined.export(mp3_path, format="mp3") + + for wav in wav_files: + os.remove(wav) + + print(f"최종 mp3 저장 완료: {mp3_path}") + return mp3_path + def convert_to_mp3(file_path): audio = AudioSegment.from_file(file_path) @@ -9,3 +43,4 @@ def convert_to_mp3(file_path): os.remove(file_path) audio.export(output_path, format="mp3") return output_path +0 \ No newline at end of file diff --git a/app/record.py b/app/record.py new file mode 100644 index 0000000..3f3af4b --- /dev/null +++ b/app/record.py @@ -0,0 +1,89 @@ +import pyaudio +import wave + +from app import convertFileExtension +import sounddevice as sd +import numpy as np +import torch +import time +from scipy.io.wavfile import write +import os +from datetime import datetime +import torchaudio + +# 사일로 VAD 모델 불러오기 +model, utils = torch.hub.load(repo_or_dir='snakers4/silero-vad', model='silero_vad', force_reload=False) +(get_speech_timestamps, save_audio, read_audio, VADIterator, collect_chunks) = utils + +SAMPLE_RATE = 16000 +FRAME_SIZE = 512 +SILENCE_LIMIT = 2.0 # 2초 이상 침묵하면 종료 +FILENAME = "output.wav" # 녹음된 오디오 파일 이름 + +audio_queue = [] +recorded_audio = [] + + +def callback(indata, frames, time_info, status): + # 받은 오디오 데이터를 audio_queue에 추가 + audio_queue.append(indata[:, 0].copy()) + + +print("Start talking... (녹음 중, 침묵 시 자동 종료)") + +with sd.InputStream(callback=callback, channels=1, samplerate=SAMPLE_RATE, blocksize=FRAME_SIZE): + silence_counter = 0 + while True: + if len(audio_queue) == 0: + time.sleep(0.01) + continue + + chunk = audio_queue.pop(0) + if len(chunk) < 512: + continue + + audio_tensor = torch.from_numpy(chunk[:512]).float() + audio_tensor = audio_tensor / (torch.max(torch.abs(audio_tensor)) + 1e-9) + + speech_prob = model(audio_tensor, SAMPLE_RATE).item() + print(f"Speech prob: {speech_prob:.3f}") + + # 음성이 인식되었을 때만 녹음 + if speech_prob > 0.5: + recorded_audio.append(chunk) + silence_counter = 0 # 음성이 인식되면 침묵 카운터 리셋 + else: + silence_counter += FRAME_SIZE / SAMPLE_RATE + print(f"Silence counter: {silence_counter:.2f}") + + # 침묵이 2초 이상 지속되면 녹음 종료 + if silence_counter >= SILENCE_LIMIT: + print("Silence detected for 2 seconds! Stopping.") + break + +# 녹음된 오디오가 있을 경우에만 파일로 저장 + +# 저장할 디렉토리 설정 +print(os.getcwd()) +save_dir = os.path.join(os.getcwd(), "audio") +os.makedirs(save_dir, exist_ok=True) # 디렉토리가 없으면 생성 + + +# 오늘 날짜 문자열 +today_str = datetime.now().strftime("%Y%m%d") +# 파일 이름 설정 +FILENAME = "output.wav" +file_path = os.path.join(save_dir, FILENAME) +if recorded_audio: + recorded_audio = np.concatenate(recorded_audio) + + # 오디오 데이터를 .wav 파일로 저장 + write(file_path, SAMPLE_RATE, recorded_audio.astype(np.float32)) # 저장 형식: .wav + print(f"녹음된 파일을 {FILENAME}로 저장했습니다.") +else: + print("녹음된 음성이 없습니다.") + + + + + diff --git a/audio/af07bfb5-7b12-4209-8681-2d3f5dedfd11.mp3 b/audio/af07bfb5-7b12-4209-8681-2d3f5dedfd11.mp3 deleted file mode 100644 index d6a96b6d14c6d9078c695fe08708bd23f4e38844..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18513 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

VCPnJqw>Y@jPeOL8on3`tm6(N0WI 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 QIxmA3Lx_YME$vwa04<1jq5uE@ diff --git a/audio/b8016cd9-c376-47c2-8e87-cceb4abff687.mp3 b/audio/b8016cd9-c376-47c2-8e87-cceb4abff687.mp3 deleted file mode 100644 index 641325c39636c127cff05c44719b468757b72622..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8900 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 Date: Thu, 8 May 2025 19:28:50 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20user=5FId=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81(api=EC=95=84=EA=B9=8C=EC=9B=8C=EC=84=9C=20?= =?UTF-8?q?=ED=95=98=EB=93=9C=EB=A1=9C=20=EB=B0=95=EC=95=84=EB=86=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/RecordController.py | 47 +++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/app/controller/RecordController.py b/app/controller/RecordController.py index 4e022f9..b296a34 100644 --- a/app/controller/RecordController.py +++ b/app/controller/RecordController.py @@ -36,6 +36,18 @@ ) +async def save_local_file(file: UploadFile) -> str: + """하나의 파일을 로컬에 저장하고 경로를 반환합니다.""" + audio_dir = "./audio" + if not os.path.exists(audio_dir): + os.makedirs(audio_dir) + + local_file_path = os.path.join(audio_dir, file.filename) + with open(local_file_path, "wb") as f: + f.write(await file.read()) + return local_file_path + + async def save_local_files(files: List[UploadFile]) -> list: """업로드된 파일을 로컬에 저장하고 파일 경로를 반환합니다.""" audio_dir = "./audio" @@ -52,16 +64,16 @@ async def save_local_files(files: List[UploadFile]) -> list: # 첫 로그인 시 1분 목소리 녹음 api @router.post("/voices") -async def getVoice(request: Request, files: List[UploadFile] = File(...)): - # token = request.headers.get("Authorization").split(" ")[1] - local_file_path_list = await save_local_files(files) - name = 'yjg' - voice_id = add_voice(name=name, local_file_paths=local_file_path_list) - print(voice_id) +async def getVoice(request: Request, user_id: int = Form(...), file: UploadFile = File(...)): + token = request.headers.get("Authorization").split(" ")[1] + local_file_path = await save_local_file(file) + name = str(user_id) + # voice_id = add_voice(name=name, local_file_paths=[local_file_path]) + print(name) # voice_url = s3Service.upload_to_s3(local_file_path) - # os.remove(local_file_path) + os.remove(local_file_path) - # send_user_voice_file_to_spring(token=token, voice_url=voice_url) + send_user_voice_id_to_spring(token=token, voice_id=yjg_voice_id) @router.post("/save/basic-tts") @@ -71,8 +83,10 @@ async def save_S3_basic_tts(request: Request, firstTTSRequestDtoList: FirstTTSRe # TTS 처리 (MP3 파일 생성 후 s3 저장) response = { - firstTTSRequestDtoList.basic_schedule_id[i]: text_to_speech_file_save_AWS(firstTTSRequestDtoList.basic_schedule_text[i], - yjg_voice_id) + firstTTSRequestDtoList.basic_schedule_id[i]: text_to_speech_file_save_AWS( + firstTTSRequestDtoList.basic_schedule_text[i], + yjg_voice_id + ) for i in range(len(firstTTSRequestDtoList.basic_schedule_id)) } @@ -100,7 +114,7 @@ async def speak_schedule_tts(request: Request, extraTTSRequestDto: ExtraTTSReque # token = request.headers.get("Authorization").split(" ")[1] schedule_text = extraTTSRequestDto.schedule_text - #진짜 실제로 쓸 코드 + # 진짜 실제로 쓸 코드 local_file_path = text_to_speech_file(schedule_text, yjg_voice_id) # 테스트하면서 AWS에 올려놓으려고 남긴 코드 @@ -130,6 +144,17 @@ def send_user_voice_file_to_spring(token: str, voice_url: str): # requests.post("https://peachmentor.com/api/spring/records/voices", headers=headers, json=data) +def send_user_voice_id_to_spring(token: str, voice_id: str): + headers = { + "Authorization": f"Bearer {token}" + } + data = { + "voiceId": voice_id + } + requests.post("http://localhost:8080/api/spring/records/voices", headers=headers, json=data) + # requests.post("https://peachmentor.com/api/spring/records/voices", headers=headers, json=data) + + def send_user_speech_file_to_spring(token: str, before_audio_link: str, answerId: int): headers = { "Authorization": f"Bearer {token}" From b7898efa86461f853316f49c0030fa55e6492753 Mon Sep 17 00:00:00 2001 From: dlwlsrud815 Date: Thu, 8 May 2025 20:25:53 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20gpt=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/RecordController.py | 37 +++++++++------ app/dto/BasicTTSRequestDto.py | 10 ---- app/dto/FirstTTSRequestDto.py | 8 ---- app/dto/ScheduleSpeakRequestDto.py | 10 ++++ app/dto/ScheduleTTSRequestDto.py | 9 ++++ app/gpt.py | 74 +++++++----------------------- app/utils/parsing_json.py | 15 ++++++ 7 files changed, 73 insertions(+), 90 deletions(-) delete mode 100644 app/dto/BasicTTSRequestDto.py delete mode 100644 app/dto/FirstTTSRequestDto.py create mode 100644 app/dto/ScheduleSpeakRequestDto.py create mode 100644 app/dto/ScheduleTTSRequestDto.py create mode 100644 app/utils/parsing_json.py diff --git a/app/controller/RecordController.py b/app/controller/RecordController.py index b296a34..854fbfc 100644 --- a/app/controller/RecordController.py +++ b/app/controller/RecordController.py @@ -11,8 +11,9 @@ from pydub.playback import play from app import s3Service -from app.dto.BasicTTSRequestDto import BasicTTSRequestDto -from app.dto.FirstTTSRequestDto import FirstTTSRequestDto +from app.dto.ScheduleSpeakRequestDto import BasicTTSRequestDto +from app.dto.ScheduleTTSRequestDto import ScheduleTTSRequestDto +from app.gpt import ChatgptAPI from app.dto.ExtraTTSRequestDto import ExtraTTSRequestDto from app.elevenLabs import add_voice, text_to_speech_file_save_AWS, get_voice, delete_all_voice, text_to_speech_file @@ -62,7 +63,7 @@ async def save_local_files(files: List[UploadFile]) -> list: return local_file_path_list -# 첫 로그인 시 1분 목소리 녹음 api +# 첫 로그인 시 목소리 녹음 api @router.post("/voices") async def getVoice(request: Request, user_id: int = Form(...), file: UploadFile = File(...)): token = request.headers.get("Authorization").split(" ")[1] @@ -76,25 +77,33 @@ async def getVoice(request: Request, user_id: int = Form(...), file: UploadFile send_user_voice_id_to_spring(token=token, voice_id=yjg_voice_id) -@router.post("/save/basic-tts") -async def save_S3_basic_tts(request: Request, firstTTSRequestDtoList: FirstTTSRequestDto): +@router.post("/schedules") +async def schedule_tts(request: Request, schedules: ScheduleTTSRequestDto): # token = request.headers.get("Authorization").split(" ")[1] - # text가 어떤형식으로 올지 몰라서 일단 그대로 내보낸다고 가정 (변환시 지피티 사용) + voice_id = schedules.voice_id + + prompt = ChatgptAPI(schedules.schedule_text) + + # schedule_dict: {"저녁": "엄마~ 저녁 잘 챙겨 먹었어?", "운동": "오늘 운동했어? 건강 챙겨~!"} + schedule_dict = prompt.get_schedule_json() # TTS 처리 (MP3 파일 생성 후 s3 저장) response = { - firstTTSRequestDtoList.basic_schedule_id[i]: text_to_speech_file_save_AWS( - firstTTSRequestDtoList.basic_schedule_text[i], - yjg_voice_id - ) - for i in range(len(firstTTSRequestDtoList.basic_schedule_id)) + schedules.schedule_id[i]: { + "keyword": schedules.schedule_text[i], # 키워드 직접 저장 + "text": schedule_dict.get(schedules.schedule_text[i], ""), # 문장은 GPT 결과에서 매핑 + "url": text_to_speech_file_save_AWS( + schedule_dict.get(schedules.schedule_text[i], ""), + yjg_voice_id + ) + } + for i in range(len(schedules.schedule_id)) } - return response -@router.post("/basic-tts") -async def speak_schedule_tts(request: Request, basicTTSRequestDto: BasicTTSRequestDto): +@router.post("/schedules-speak") +async def speak_schedule(request: Request, basicTTSRequestDto: BasicTTSRequestDto): # token = request.headers.get("Authorization").split(" ")[1] local_file_path = download_from_s3(basicTTSRequestDto.schedule_voice_Url) print(f"Downloaded file path: {local_file_path}") diff --git a/app/dto/BasicTTSRequestDto.py b/app/dto/BasicTTSRequestDto.py deleted file mode 100644 index 1d56edf..0000000 --- a/app/dto/BasicTTSRequestDto.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import List - -from pydantic import BaseModel - - -class BasicTTSRequestDto(BaseModel): - basic_schedule_id: int - schedule_voice_Url: str # AWS 저장은 basic schedule 저장시 같이 콜 날라간다고 가정 - target_time: str # "10:00:00" 형식 - diff --git a/app/dto/FirstTTSRequestDto.py b/app/dto/FirstTTSRequestDto.py deleted file mode 100644 index 021cfb9..0000000 --- a/app/dto/FirstTTSRequestDto.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import List - -from pydantic import BaseModel - - -class FirstTTSRequestDto(BaseModel): - basic_schedule_id: List[int] - basic_schedule_text: List[str] diff --git a/app/dto/ScheduleSpeakRequestDto.py b/app/dto/ScheduleSpeakRequestDto.py new file mode 100644 index 0000000..5e88d78 --- /dev/null +++ b/app/dto/ScheduleSpeakRequestDto.py @@ -0,0 +1,10 @@ +from typing import List + +from pydantic import BaseModel + + +class ScheduleSpeakRequestDto(BaseModel): + schedule_id: int + schedule_voice_Url: str + target_time: str # "10:00:00" 형식 + diff --git a/app/dto/ScheduleTTSRequestDto.py b/app/dto/ScheduleTTSRequestDto.py new file mode 100644 index 0000000..92d850d --- /dev/null +++ b/app/dto/ScheduleTTSRequestDto.py @@ -0,0 +1,9 @@ +from typing import List + +from pydantic import BaseModel + + +class ScheduleTTSRequestDto(BaseModel): + voice_id: int + schedule_id: List[int] + schedule_text: List[str] diff --git a/app/gpt.py b/app/gpt.py index a3bf90b..88506fb 100644 --- a/app/gpt.py +++ b/app/gpt.py @@ -1,61 +1,29 @@ import os from dotenv import load_dotenv from openai import OpenAI +from app.utils import parsing_json load_dotenv() client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) class ChatgptAPI: - def __init__(self, transcript, statistics_filler_json=None, statistics_silence_json=None): - if statistics_silence_json is None: - statistics_silence_json = [] - if statistics_filler_json is None: - statistics_filler_json = [] - self.transcript = transcript - self.statistics_filler_json = statistics_filler_json - self.statistics_silence_json = statistics_silence_json + def __init__(self, schedules): + self.schedules = schedules - if not self.transcript: - raise ValueError("Transcript is missing or empty.") - - def create_insight_prompt(self): + def create_schedule_prompt(self): system_message = f""" - 너는 지금부터 이 서비스의 speech mentor야, 너는 질문에 대해 사용자에게 새로운 인사이트를 제공하면 돼. - 질문은 다음과 같아: {str(self.transcript)} + 너는 지금부터 혼자 사시는 부모님을 걱정하는 보호자야. + 네 역할은 키워드를 보고, 키워드와 관련한 문제에 대해서 부모님을 걱정하고, 생활은 챙겨주는거야. + 키워드는 다음과 같아: {str(self.schedules)} 너의 목표는 두 가지야: - 1. 질문에 대한 답변을 만들어. 그런데 대답의 길이가 1분을 넘어선 안 돼. - 2. 3가지 종류의 답변을 만들어. - - 결과는 답변끼리 구분할 수 있게 개행으로 구분해서 만들어줘. - """ - - messages = [ - {"role": "system", "content": system_message} - ] - return messages + 1. 키워드에 대한 질문 혹은 문장을 한 줄의 텍스트로 만들어. + ex) 키워드가 '저녁' 이라면, "엄마~ 하루 잘 보냈어? 저녁도 맛있는거 챙겨먹어! 사랑해~" + 2. 만든 텍스트는 ?, !, ~ 등이 다양하고 많이 들어갈 수 있어. 감정이 강하게 느껴지게 작성해줘. - def create_feedback_prompt(self): - system_message = f""" - 너는 지금부터 speech mentor로서, 내가 제공한 서비스 사용자의 음성 기록 텍스트를 평가하고 개선하는 역할을 맡고 있어. - 사용자의 목표는 본인의 생각을 조리있게 표현하는 것이야. - 텍스트는 다음과 같아: {str(self.transcript)} + 결과는 {"키워드": "문장"} 형태의 JSON 문자열로 반환해줘. 꼭 큰따옴표(")만 사용해. - 너의 목표는 3가지야: - 1. 내가 제공하는 텍스트에서 침묵 시간으로 기록되어 있는 부분은 제거해줘. - 2. 내가 제공하는 텍스트에서 추임새라고 기록되어 있는 부분은 제거해줘. - 3. 추임새라고 기록되어 있진 않지만, 문맥상 추임새로 판단되는 부분은 제거해줘. - - 이 때 너가 주의 해야 할 점은 3가지야: - 1. 3가지의 목표를 달성하면서, 텍스트의 다른 부분은 아예 건드려선 안 돼. - 2. 그렇지만 3가지의 목표를 달성한 이후, 너가 생각했을 때 문맥 상 부자연스러운 부분은 자연스럽게 바꿔도 되는데 원래 문장을 최대한 유지해줘. - 3. 단어나 문장이 정확하게 기록되지 않았을 수도 있으니, 그에 맞는 단어나 형태로 고쳐줘. - - 기존 텍스트에 기록되어 있던 추임새의 종류는 다음과 같고 : {self.statistics_filler_json} - 기록된 침묵 시간은 다음과 같아 : {self.statistics_silence_json} - - 너가 목표를 달성하고 만들어낸 텍스트와 기존 텍스트에 대한 피드백을 개행으로 구분해서 보여줘. 각각 한 문장으로 보여주면 되고 피드백은 추임새의 개수와 침묵 시간을 기반으로 이야기 해줘. """ messages = [ @@ -63,8 +31,8 @@ def create_feedback_prompt(self): ] return messages - def get_feedback(self): - prompt = self.create_feedback_prompt() + def get_schedule_json(self): + prompt = self.create_schedule_prompt() response = client.chat.completions.create( model="gpt-4-turbo", messages=prompt, @@ -72,17 +40,7 @@ def get_feedback(self): max_tokens=2048 ) - feedback = response.choices[0].message.content.split("\n") - return [f for f in feedback if f] - - def get_insight(self): - prompt = self.create_insight_prompt() - response = client.chat.completions.create( - model="gpt-4-turbo", - messages=prompt, - temperature=0.5, - max_tokens=2048 - ) + content = response.choices[0].message.content + schedule_dict = parsing_json.extract_json_from_content(content) - insights = response.choices[0].message.content.split("\n") - return [r for r in insights if r] # 빈 문자열 제거 + return schedule_dict diff --git a/app/utils/parsing_json.py b/app/utils/parsing_json.py new file mode 100644 index 0000000..a3a13af --- /dev/null +++ b/app/utils/parsing_json.py @@ -0,0 +1,15 @@ +import re +import json + + +def extract_json_from_content(content): + match = re.search(r"\{[\s\S]*\}", content) + if match: + try: + return json.loads(match.group()) + except json.JSONDecodeError as e: + print("JSON 파싱 실패:", e) + return {} + else: + print("JSON 형태가 아님") + return {} From 0742c16983fa10c703b602cdc73df29db04c36c2 Mon Sep 17 00:00:00 2001 From: dlwlsrud815 Date: Thu, 8 May 2025 20:34:36 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20target=20time=20=EB=A7=9E=EC=B6=B0?= =?UTF-8?q?=EC=84=9C=20=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/RecordController.py | 41 ++++++++++-------------------- app/utils/play_file.py | 16 ++++++++++++ 2 files changed, 29 insertions(+), 28 deletions(-) create mode 100644 app/utils/play_file.py diff --git a/app/controller/RecordController.py b/app/controller/RecordController.py index 854fbfc..ded5a10 100644 --- a/app/controller/RecordController.py +++ b/app/controller/RecordController.py @@ -11,10 +11,11 @@ from pydub.playback import play from app import s3Service -from app.dto.ScheduleSpeakRequestDto import BasicTTSRequestDto +from app.dto.ScheduleSpeakRequestDto import ScheduleSpeakRequestDto from app.dto.ScheduleTTSRequestDto import ScheduleTTSRequestDto from app.gpt import ChatgptAPI from app.dto.ExtraTTSRequestDto import ExtraTTSRequestDto +from app.utils import play_file from app.elevenLabs import add_voice, text_to_speech_file_save_AWS, get_voice, delete_all_voice, text_to_speech_file from app.s3Service import download_from_s3_links, download_from_s3 @@ -103,43 +104,27 @@ async def schedule_tts(request: Request, schedules: ScheduleTTSRequestDto): @router.post("/schedules-speak") -async def speak_schedule(request: Request, basicTTSRequestDto: BasicTTSRequestDto): +async def speak_schedule(request: Request, scheduleSpeakRequestDto: ScheduleTTSRequestDto): # token = request.headers.get("Authorization").split(" ")[1] - local_file_path = download_from_s3(basicTTSRequestDto.schedule_voice_Url) + local_file_path = download_from_s3(scheduleSpeakRequestDto.schedule_voice_Url) print(f"Downloaded file path: {local_file_path}") # 블루투스 헤드셋 또는 기본 스피커로 출력 - os.system("pactl list sinks | grep 'bluez_sink'") # 블루투스 출력 장치 확인 - os.system("pactl set-default-sink `pactl list sinks short | grep bluez_sink | awk '{print $2}'`") # 기본 출력 변경 + # os.system("pactl list sinks | grep 'bluez_sink'") # 블루투스 출력 장치 확인 + # os.system("pactl set-default-sink `pactl list sinks short | grep bluez_sink | awk '{print $2}'`") # 기본 출력 변경 + + # 스피커를 기본 출력 장치로 설정 + os.system("pactl list sinks | grep 'analog-output'") # 스피커 장치 확인 + os.system("pactl set-default-sink `pactl list sinks short | grep analog-output | awk '{print $2}'`") # 기본 출력 변경 # 로컬 파일을 직접 재생 subprocess.run(["mpg321", local_file_path]) - return {"message": "TTS completed and played on Bluetooth headset or speaker"} - - -@router.post("/extra-tts") -async def speak_schedule_tts(request: Request, extraTTSRequestDto: ExtraTTSRequestDto): - # token = request.headers.get("Authorization").split(" ")[1] - schedule_text = extraTTSRequestDto.schedule_text - - # 진짜 실제로 쓸 코드 - local_file_path = text_to_speech_file(schedule_text, yjg_voice_id) - - # 테스트하면서 AWS에 올려놓으려고 남긴 코드 - url = text_to_speech_file_save_AWS(schedule_text, yjg_voice_id) - local_file_path = download_from_s3(url) + # target_time에 맞춰서 TTS 파일 재생 + play_file.play_at_target_time(scheduleSpeakRequestDto.target_time, local_file_path) - # local_file_path = os.getcwd()+"/test_audio/test8.mp3" # test - # 블루투스 헤드셋 또는 기본 스피커로 출력 - os.system("pactl list sinks | grep 'bluez_sink'") # 블루투스 출력 장치 확인 - os.system("pactl set-default-sink `pactl list sinks short | grep bluez_sink | awk '{print $2}'`") # 기본 출력 변경 + return {"message": "TTS completed and played on speaker"} - # 로컬 파일을 직접 재생 - subprocess.run(["/usr/bin/mpg321", local_file_path]) - # subprocess.run(["ffplay", "-nodisp", "-autoexit", local_file_path], - # stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # 윈도우용 - return {"message": "TTS completed and played on Bluetooth headset or speaker"} def send_user_voice_file_to_spring(token: str, voice_url: str): diff --git a/app/utils/play_file.py b/app/utils/play_file.py new file mode 100644 index 0000000..7b4ae4e --- /dev/null +++ b/app/utils/play_file.py @@ -0,0 +1,16 @@ +from datetime import datetime +import subprocess +import time + + +def play_at_target_time(target_time: str, local_file_path: str): + # 현재 시간과 target_time 비교 + current_time = datetime.now().strftime("%H:%M:%S") + + # target_time이 현재 시간보다 크면 대기 (target_time까지 대기) + while current_time != target_time: + time.sleep(1) # 1초마다 시간 확인 + current_time = datetime.now().strftime("%H:%M:%S") + + # target_time에 맞춰서 TTS 파일 재생 + subprocess.run(["mpg321", local_file_path]) \ No newline at end of file From d597c38725eb0c98f0b606220c545b23cca9932b Mon Sep 17 00:00:00 2001 From: dlwlsrud815 Date: Thu, 8 May 2025 20:46:36 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20=EB=B3=B4=ED=98=B8=EC=9E=90=20?= =?UTF-8?q?=EC=A7=80=EC=B9=AD=20alias=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/RecordController.py | 2 +- app/dto/ScheduleTTSRequestDto.py | 1 + app/gpt.py | 8 +++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/controller/RecordController.py b/app/controller/RecordController.py index ded5a10..0b088a1 100644 --- a/app/controller/RecordController.py +++ b/app/controller/RecordController.py @@ -83,7 +83,7 @@ async def schedule_tts(request: Request, schedules: ScheduleTTSRequestDto): # token = request.headers.get("Authorization").split(" ")[1] voice_id = schedules.voice_id - prompt = ChatgptAPI(schedules.schedule_text) + prompt = ChatgptAPI(schedules.schedule_text, schedules.alias) # schedule_dict: {"저녁": "엄마~ 저녁 잘 챙겨 먹었어?", "운동": "오늘 운동했어? 건강 챙겨~!"} schedule_dict = prompt.get_schedule_json() diff --git a/app/dto/ScheduleTTSRequestDto.py b/app/dto/ScheduleTTSRequestDto.py index 92d850d..13744ba 100644 --- a/app/dto/ScheduleTTSRequestDto.py +++ b/app/dto/ScheduleTTSRequestDto.py @@ -5,5 +5,6 @@ class ScheduleTTSRequestDto(BaseModel): voice_id: int + alias : str schedule_id: List[int] schedule_text: List[str] diff --git a/app/gpt.py b/app/gpt.py index 88506fb..ce33cc9 100644 --- a/app/gpt.py +++ b/app/gpt.py @@ -8,8 +8,9 @@ class ChatgptAPI: - def __init__(self, schedules): + def __init__(self, schedules, alias): self.schedules = schedules + self.alias = alias def create_schedule_prompt(self): system_message = f""" @@ -19,8 +20,9 @@ def create_schedule_prompt(self): 너의 목표는 두 가지야: 1. 키워드에 대한 질문 혹은 문장을 한 줄의 텍스트로 만들어. - ex) 키워드가 '저녁' 이라면, "엄마~ 하루 잘 보냈어? 저녁도 맛있는거 챙겨먹어! 사랑해~" - 2. 만든 텍스트는 ?, !, ~ 등이 다양하고 많이 들어갈 수 있어. 감정이 강하게 느껴지게 작성해줘. + ex) 키워드가 '저녁' 이라면, "{self.alias}~ 하루 잘 보냈어? 저녁도 맛있는거 챙겨먹어! 사랑해~" + 2. 만든 텍스트는 ?, !, ~ 등이 다양하고 많이 들어갈 수 있어. 감정이 강하게 느껴지게 작성해줘. + 3. 부모님을 지칭하는 별명은 {self.alias} 로 해줘. 결과는 {"키워드": "문장"} 형태의 JSON 문자열로 반환해줘. 꼭 큰따옴표(")만 사용해. From 69e5e3e345c6fd10b07a5e2e433d9422dc3b1772 Mon Sep 17 00:00:00 2001 From: dlwlsrud815 Date: Thu, 8 May 2025 21:45:03 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=ED=95=98=EC=9D=B4=ED=8D=BC=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=ED=8A=9C=EB=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- app/controller/RecordController.py | 13 +------------ app/elevenLabs.py | 14 +++++++------- app/gpt.py | 8 +++++--- app/utils/play_file.py | 20 +++++++++++++++++--- 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index b24e2d7..dfc76c3 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ venv.bak/ *.h5 __pycache__/ *.pyc -app/audio/ \ No newline at end of file +audio/ \ No newline at end of file diff --git a/app/controller/RecordController.py b/app/controller/RecordController.py index 0b088a1..faea1fd 100644 --- a/app/controller/RecordController.py +++ b/app/controller/RecordController.py @@ -104,22 +104,11 @@ async def schedule_tts(request: Request, schedules: ScheduleTTSRequestDto): @router.post("/schedules-speak") -async def speak_schedule(request: Request, scheduleSpeakRequestDto: ScheduleTTSRequestDto): +async def speak_schedule(request: Request, scheduleSpeakRequestDto: ScheduleSpeakRequestDto): # token = request.headers.get("Authorization").split(" ")[1] local_file_path = download_from_s3(scheduleSpeakRequestDto.schedule_voice_Url) print(f"Downloaded file path: {local_file_path}") - # 블루투스 헤드셋 또는 기본 스피커로 출력 - # os.system("pactl list sinks | grep 'bluez_sink'") # 블루투스 출력 장치 확인 - # os.system("pactl set-default-sink `pactl list sinks short | grep bluez_sink | awk '{print $2}'`") # 기본 출력 변경 - - # 스피커를 기본 출력 장치로 설정 - os.system("pactl list sinks | grep 'analog-output'") # 스피커 장치 확인 - os.system("pactl set-default-sink `pactl list sinks short | grep analog-output | awk '{print $2}'`") # 기본 출력 변경 - - # 로컬 파일을 직접 재생 - subprocess.run(["mpg321", local_file_path]) - # target_time에 맞춰서 TTS 파일 재생 play_file.play_at_target_time(scheduleSpeakRequestDto.target_time, local_file_path) diff --git a/app/elevenLabs.py b/app/elevenLabs.py index 82548f4..8ac529c 100644 --- a/app/elevenLabs.py +++ b/app/elevenLabs.py @@ -46,13 +46,13 @@ def text_to_speech_file_save_AWS(text: str, voice_id: str) -> str: voice_id=voice_id, output_format="mp3_22050_32", text=text, - model_id="eleven_turbo_v2_5", - voice_settings=VoiceSettings( - stability=0.3, - similarity_boost=1.0, - style=0.0, - use_speaker_boost=True, - ), + model_id="eleven_multilingual_v2", + # voice_settings=VoiceSettings( + # stability=0.3, + # similarity_boost=1.0, + # style=0.0, + # use_speaker_boost=True, + # ), ) save_file_path = f"{uuid.uuid4()}.mp3" diff --git a/app/gpt.py b/app/gpt.py index ce33cc9..6647621 100644 --- a/app/gpt.py +++ b/app/gpt.py @@ -20,11 +20,13 @@ def create_schedule_prompt(self): 너의 목표는 두 가지야: 1. 키워드에 대한 질문 혹은 문장을 한 줄의 텍스트로 만들어. - ex) 키워드가 '저녁' 이라면, "{self.alias}~ 하루 잘 보냈어? 저녁도 맛있는거 챙겨먹어! 사랑해~" - 2. 만든 텍스트는 ?, !, ~ 등이 다양하고 많이 들어갈 수 있어. 감정이 강하게 느껴지게 작성해줘. + ex) 키워드가 '저녁' 이라면, "{self.alias}~~ 하루 잘 보냈어?? 저녁도 맛있는거 챙겨먹어!! 사랑해~~ " + 2. 만든 텍스트는 ?? !! ~~ ,, .. 등의 다양한 특수문자가 많이 들어갈 수 있어. 감정이 강하게 느껴지게 작성해줘. + 2-a. 특수문자를 붙일 때는 꼭 2개씩 붙여줘 3. 부모님을 지칭하는 별명은 {self.alias} 로 해줘. + 4. 문장과 문장 사이의 띄어쓰기를 2개씩 넣어줘 - 결과는 {"키워드": "문장"} 형태의 JSON 문자열로 반환해줘. 꼭 큰따옴표(")만 사용해. + 결과는 {{"키워드": "문장"}} 형태의 JSON 문자열로 반환해줘. 꼭 큰따옴표(")만 사용해. """ diff --git a/app/utils/play_file.py b/app/utils/play_file.py index 7b4ae4e..40e3d58 100644 --- a/app/utils/play_file.py +++ b/app/utils/play_file.py @@ -8,9 +8,23 @@ def play_at_target_time(target_time: str, local_file_path: str): current_time = datetime.now().strftime("%H:%M:%S") # target_time이 현재 시간보다 크면 대기 (target_time까지 대기) - while current_time != target_time: + while current_time < target_time: time.sleep(1) # 1초마다 시간 확인 current_time = datetime.now().strftime("%H:%M:%S") - # target_time에 맞춰서 TTS 파일 재생 - subprocess.run(["mpg321", local_file_path]) \ No newline at end of file + # 블루투스 헤드셋 또는 기본 스피커로 출력 + # os.system("pactl list sinks | grep 'bluez_sink'") # 블루투스 출력 장치 확인 + # os.system("pactl set-default-sink `pactl list sinks short | grep bluez_sink | awk '{print $2}'`") # 기본 출력 변경 + + # # 스피커를 기본 출력 장치로 설정 + # os.system("pactl list sinks | grep 'analog-output'") # 스피커 장치 확인 + # os.system("pactl set-default-sink `pactl list sinks short | grep analog-output | awk '{print $2}'`") # 기본 출력 변경 + + # 로컬 파일을 직접 재생 + # subprocess.run(["mpg321", local_file_path]) + + # window 테스트 용 + from playsound import playsound + from pathlib import Path + safe_path = Path(local_file_path).resolve().as_posix() + playsound(safe_path) From 7f44629e7764b8a526aad2581a36b1a24f31f912 Mon Sep 17 00:00:00 2001 From: dlwlsrud815 Date: Thu, 8 May 2025 21:54:30 +0900 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20folder=20+=20import=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/RecordController.py | 17 ++++------------- app/dto/ExtraTTSRequestDto.py | 3 --- app/dto/ScheduleSpeakRequestDto.py | 3 --- app/dto/ScheduleTTSRequestDto.py | 2 +- app/{ => service}/elevenLabs.py | 5 +++-- app/{ => service}/gpt.py | 2 ++ app/{ => service}/main.py | 7 +++---- app/{ => service}/record.py | 18 ++++-------------- app/{ => service}/s3Service.py | 3 +-- app/{ => utils}/convertFileExtension.py | 6 ++++-- app/utils/parsing_json.py | 2 +- app/utils/play_file.py | 3 +-- 12 files changed, 24 insertions(+), 47 deletions(-) rename app/{ => service}/elevenLabs.py (98%) rename app/{ => service}/gpt.py (99%) rename app/{ => service}/main.py (97%) rename app/{ => service}/record.py (96%) rename app/{ => service}/s3Service.py (98%) rename app/{ => utils}/convertFileExtension.py (99%) diff --git a/app/controller/RecordController.py b/app/controller/RecordController.py index faea1fd..1de9ea7 100644 --- a/app/controller/RecordController.py +++ b/app/controller/RecordController.py @@ -1,25 +1,17 @@ import os -import subprocess -import time from typing import List import requests - -from fastapi import APIRouter, Request, UploadFile, File, Form from boto3 import client -from pydub import AudioSegment -from pydub.playback import play +from fastapi import APIRouter, Request, UploadFile, File, Form -from app import s3Service from app.dto.ScheduleSpeakRequestDto import ScheduleSpeakRequestDto from app.dto.ScheduleTTSRequestDto import ScheduleTTSRequestDto -from app.gpt import ChatgptAPI -from app.dto.ExtraTTSRequestDto import ExtraTTSRequestDto +from app.service.elevenLabs import text_to_speech_file_save_AWS +from app.service.gpt import ChatgptAPI +from app.service.s3Service import download_from_s3 from app.utils import play_file -from app.elevenLabs import add_voice, text_to_speech_file_save_AWS, get_voice, delete_all_voice, text_to_speech_file -from app.s3Service import download_from_s3_links, download_from_s3 - router = APIRouter( prefix="/api/fastapi", ) @@ -115,7 +107,6 @@ async def speak_schedule(request: Request, scheduleSpeakRequestDto: ScheduleSpea return {"message": "TTS completed and played on speaker"} - def send_user_voice_file_to_spring(token: str, voice_url: str): headers = { "Authorization": f"Bearer {token}" diff --git a/app/dto/ExtraTTSRequestDto.py b/app/dto/ExtraTTSRequestDto.py index 3aa3945..0a1b600 100644 --- a/app/dto/ExtraTTSRequestDto.py +++ b/app/dto/ExtraTTSRequestDto.py @@ -1,5 +1,3 @@ -from typing import List - from pydantic import BaseModel @@ -8,4 +6,3 @@ class ExtraTTSRequestDto(BaseModel): is_basic_schedule: bool schedule_text: str target_time: str # "10:00:00" 형식 - diff --git a/app/dto/ScheduleSpeakRequestDto.py b/app/dto/ScheduleSpeakRequestDto.py index 5e88d78..cef86db 100644 --- a/app/dto/ScheduleSpeakRequestDto.py +++ b/app/dto/ScheduleSpeakRequestDto.py @@ -1,5 +1,3 @@ -from typing import List - from pydantic import BaseModel @@ -7,4 +5,3 @@ class ScheduleSpeakRequestDto(BaseModel): schedule_id: int schedule_voice_Url: str target_time: str # "10:00:00" 형식 - diff --git a/app/dto/ScheduleTTSRequestDto.py b/app/dto/ScheduleTTSRequestDto.py index 13744ba..44f7b68 100644 --- a/app/dto/ScheduleTTSRequestDto.py +++ b/app/dto/ScheduleTTSRequestDto.py @@ -5,6 +5,6 @@ class ScheduleTTSRequestDto(BaseModel): voice_id: int - alias : str + alias: str schedule_id: List[int] schedule_text: List[str] diff --git a/app/elevenLabs.py b/app/service/elevenLabs.py similarity index 98% rename from app/elevenLabs.py rename to app/service/elevenLabs.py index 8ac529c..8eeddb0 100644 --- a/app/elevenLabs.py +++ b/app/service/elevenLabs.py @@ -1,9 +1,10 @@ import os import uuid -from elevenlabs import ElevenLabs, VoiceSettings + from dotenv import load_dotenv +from elevenlabs import ElevenLabs, VoiceSettings -from app.s3Service import upload_to_s3 +from app.service.s3Service import upload_to_s3 load_dotenv() client = ElevenLabs( diff --git a/app/gpt.py b/app/service/gpt.py similarity index 99% rename from app/gpt.py rename to app/service/gpt.py index 6647621..33118ea 100644 --- a/app/gpt.py +++ b/app/service/gpt.py @@ -1,6 +1,8 @@ import os + from dotenv import load_dotenv from openai import OpenAI + from app.utils import parsing_json load_dotenv() diff --git a/app/main.py b/app/service/main.py similarity index 97% rename from app/main.py rename to app/service/main.py index 8175679..cab8d43 100644 --- a/app/main.py +++ b/app/service/main.py @@ -1,11 +1,10 @@ +from fastapi import FastAPI, Depends, HTTPException from fastapi.middleware.cors import CORSMiddleware +from fastapi.openapi.utils import get_openapi +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from app.controller.RecordController import router -from fastapi import FastAPI, Depends, HTTPException, Request -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from fastapi.openapi.utils import get_openapi - app = FastAPI() auth_scheme = HTTPBearer() diff --git a/app/record.py b/app/service/record.py similarity index 96% rename from app/record.py rename to app/service/record.py index 3f3af4b..75729b3 100644 --- a/app/record.py +++ b/app/service/record.py @@ -1,15 +1,11 @@ -import pyaudio -import wave +import os +import time +from datetime import datetime -from app import convertFileExtension -import sounddevice as sd import numpy as np +import sounddevice as sd import torch -import time from scipy.io.wavfile import write -import os -from datetime import datetime -import torchaudio # 사일로 VAD 모델 불러오기 model, utils = torch.hub.load(repo_or_dir='snakers4/silero-vad', model='silero_vad', force_reload=False) @@ -68,7 +64,6 @@ def callback(indata, frames, time_info, status): save_dir = os.path.join(os.getcwd(), "audio") os.makedirs(save_dir, exist_ok=True) # 디렉토리가 없으면 생성 - # 오늘 날짜 문자열 today_str = datetime.now().strftime("%Y%m%d") # 파일 이름 설정 @@ -82,8 +77,3 @@ def callback(indata, frames, time_info, status): print(f"녹음된 파일을 {FILENAME}로 저장했습니다.") else: print("녹음된 음성이 없습니다.") - - - - - diff --git a/app/s3Service.py b/app/service/s3Service.py similarity index 98% rename from app/s3Service.py rename to app/service/s3Service.py index 888440d..b992d24 100644 --- a/app/s3Service.py +++ b/app/service/s3Service.py @@ -9,7 +9,7 @@ from dotenv import load_dotenv from fastapi import UploadFile -from app.convertFileExtension import convert_to_mp3 +from app.utils.convertFileExtension import convert_to_mp3 load_dotenv() @@ -116,4 +116,3 @@ def download_from_s3_model(file_s3_url: str) -> str: print(f"HTTP error occurred: {e}") except Exception as e: print(f"An error occurred: {e}") - diff --git a/app/convertFileExtension.py b/app/utils/convertFileExtension.py similarity index 99% rename from app/convertFileExtension.py rename to app/utils/convertFileExtension.py index 2bbe19e..e27dd38 100644 --- a/app/convertFileExtension.py +++ b/app/utils/convertFileExtension.py @@ -1,8 +1,8 @@ import os +from datetime import datetime from pydub import AudioSegment -from datetime import datetime def merge_all_wavs_to_mp3(audio_dir="audio", silence_duration_ms=500): wav_files = sorted([ @@ -43,4 +43,6 @@ def convert_to_mp3(file_path): os.remove(file_path) audio.export(output_path, format="mp3") return output_path -0 \ No newline at end of file + + +0 diff --git a/app/utils/parsing_json.py b/app/utils/parsing_json.py index a3a13af..e0eb1be 100644 --- a/app/utils/parsing_json.py +++ b/app/utils/parsing_json.py @@ -1,5 +1,5 @@ -import re import json +import re def extract_json_from_content(content): diff --git a/app/utils/play_file.py b/app/utils/play_file.py index 40e3d58..3575fc5 100644 --- a/app/utils/play_file.py +++ b/app/utils/play_file.py @@ -1,6 +1,5 @@ -from datetime import datetime -import subprocess import time +from datetime import datetime def play_at_target_time(target_time: str, local_file_path: str): From 53b965a5d6a93f90dc15921e558c749738d7dbf4 Mon Sep 17 00:00:00 2001 From: dlwlsrud815 Date: Thu, 8 May 2025 22:08:33 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20Pyaudio=20docker=20build=20conflict?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/record.py | 2 +- app/utils/play_file.py | 18 ++++++++++-------- requirements.txt | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/service/record.py b/app/service/record.py index 75729b3..b968f48 100644 --- a/app/service/record.py +++ b/app/service/record.py @@ -61,7 +61,7 @@ def callback(indata, frames, time_info, status): # 저장할 디렉토리 설정 print(os.getcwd()) -save_dir = os.path.join(os.getcwd(), "audio") +save_dir = os.path.join(os.getcwd(), "first_audio") os.makedirs(save_dir, exist_ok=True) # 디렉토리가 없으면 생성 # 오늘 날짜 문자열 diff --git a/app/utils/play_file.py b/app/utils/play_file.py index 3575fc5..2a549e3 100644 --- a/app/utils/play_file.py +++ b/app/utils/play_file.py @@ -1,5 +1,7 @@ import time from datetime import datetime +import os +import subprocess def play_at_target_time(target_time: str, local_file_path: str): @@ -11,16 +13,16 @@ def play_at_target_time(target_time: str, local_file_path: str): time.sleep(1) # 1초마다 시간 확인 current_time = datetime.now().strftime("%H:%M:%S") - # 블루투스 헤드셋 또는 기본 스피커로 출력 - # os.system("pactl list sinks | grep 'bluez_sink'") # 블루투스 출력 장치 확인 - # os.system("pactl set-default-sink `pactl list sinks short | grep bluez_sink | awk '{print $2}'`") # 기본 출력 변경 + #블루투스 헤드셋 또는 기본 스피커로 출력 + os.system("pactl list sinks | grep 'bluez_sink'") # 블루투스 출력 장치 확인 + os.system("pactl set-default-sink `pactl list sinks short | grep bluez_sink | awk '{print $2}'`") # 기본 출력 변경 - # # 스피커를 기본 출력 장치로 설정 - # os.system("pactl list sinks | grep 'analog-output'") # 스피커 장치 확인 - # os.system("pactl set-default-sink `pactl list sinks short | grep analog-output | awk '{print $2}'`") # 기본 출력 변경 + # 스피커를 기본 출력 장치로 설정 + os.system("pactl list sinks | grep 'analog-output'") # 스피커 장치 확인 + os.system("pactl set-default-sink `pactl list sinks short | grep analog-output | awk '{print $2}'`") # 기본 출력 변경 - # 로컬 파일을 직접 재생 - # subprocess.run(["mpg321", local_file_path]) + #로컬 파일을 직접 재생 + subprocess.run(["mpg321", local_file_path]) # window 테스트 용 from playsound import playsound diff --git a/requirements.txt b/requirements.txt index d9c54bb..52a2fd7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ mpmath==1.3.0 networkx==3.2.1 numpy==2.0.2 openai==1.68.2 -PyAudio==0.2.14 +playsound==1.3.0 pycparser==2.22 pydantic==2.10.6 pydantic_core==2.27.2