diff --git a/.gitignore b/.gitignore index 0497577..dfc76c3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ venv.bak/ *.h5 __pycache__/ -*.pyc \ No newline at end of file +*.pyc +audio/ \ No newline at end of file diff --git a/app/controller/RecordController.py b/app/controller/RecordController.py index 4e022f9..1de9ea7 100644 --- a/app/controller/RecordController.py +++ b/app/controller/RecordController.py @@ -1,22 +1,16 @@ 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 app import s3Service -from app.dto.BasicTTSRequestDto import BasicTTSRequestDto -from app.dto.FirstTTSRequestDto import FirstTTSRequestDto -from app.dto.ExtraTTSRequestDto import ExtraTTSRequestDto +from fastapi import APIRouter, Request, UploadFile, File, Form -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 +from app.dto.ScheduleSpeakRequestDto import ScheduleSpeakRequestDto +from app.dto.ScheduleTTSRequestDto import ScheduleTTSRequestDto +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 router = APIRouter( prefix="/api/fastapi", @@ -36,6 +30,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" @@ -50,73 +56,55 @@ 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, 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") -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, schedules.alias) + + # 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, scheduleSpeakRequestDto: ScheduleSpeakRequestDto): # 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}'`") # 기본 출력 변경 + # target_time에 맞춰서 TTS 파일 재생 + play_file.play_at_target_time(scheduleSpeakRequestDto.target_time, local_file_path) - # 로컬 파일을 직접 재생 - 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) - - # 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}'`") # 기본 출력 변경 - - # 로컬 파일을 직접 재생 - 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"} + return {"message": "TTS completed and played on speaker"} def send_user_voice_file_to_spring(token: str, voice_url: str): @@ -130,6 +118,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}" diff --git a/app/convertFileExtension.py b/app/convertFileExtension.py deleted file mode 100644 index c7dee54..0000000 --- a/app/convertFileExtension.py +++ /dev/null @@ -1,11 +0,0 @@ -import os - -from pydub import AudioSegment - - -def convert_to_mp3(file_path): - audio = AudioSegment.from_file(file_path) - output_path = file_path.replace(".wav", ".mp3") - os.remove(file_path) - audio.export(output_path, format="mp3") - return output_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/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/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..cef86db --- /dev/null +++ b/app/dto/ScheduleSpeakRequestDto.py @@ -0,0 +1,7 @@ +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..44f7b68 --- /dev/null +++ b/app/dto/ScheduleTTSRequestDto.py @@ -0,0 +1,10 @@ +from typing import List + +from pydantic import BaseModel + + +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 deleted file mode 100644 index a3bf90b..0000000 --- a/app/gpt.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -from dotenv import load_dotenv -from openai import OpenAI - -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 - - if not self.transcript: - raise ValueError("Transcript is missing or empty.") - - def create_insight_prompt(self): - system_message = f""" - 너는 지금부터 이 서비스의 speech mentor야, 너는 질문에 대해 사용자에게 새로운 인사이트를 제공하면 돼. - 질문은 다음과 같아: {str(self.transcript)} - - 너의 목표는 두 가지야: - 1. 질문에 대한 답변을 만들어. 그런데 대답의 길이가 1분을 넘어선 안 돼. - 2. 3가지 종류의 답변을 만들어. - - 결과는 답변끼리 구분할 수 있게 개행으로 구분해서 만들어줘. - """ - - messages = [ - {"role": "system", "content": system_message} - ] - return messages - - def create_feedback_prompt(self): - system_message = f""" - 너는 지금부터 speech mentor로서, 내가 제공한 서비스 사용자의 음성 기록 텍스트를 평가하고 개선하는 역할을 맡고 있어. - 사용자의 목표는 본인의 생각을 조리있게 표현하는 것이야. - 텍스트는 다음과 같아: {str(self.transcript)} - - 너의 목표는 3가지야: - 1. 내가 제공하는 텍스트에서 침묵 시간으로 기록되어 있는 부분은 제거해줘. - 2. 내가 제공하는 텍스트에서 추임새라고 기록되어 있는 부분은 제거해줘. - 3. 추임새라고 기록되어 있진 않지만, 문맥상 추임새로 판단되는 부분은 제거해줘. - - 이 때 너가 주의 해야 할 점은 3가지야: - 1. 3가지의 목표를 달성하면서, 텍스트의 다른 부분은 아예 건드려선 안 돼. - 2. 그렇지만 3가지의 목표를 달성한 이후, 너가 생각했을 때 문맥 상 부자연스러운 부분은 자연스럽게 바꿔도 되는데 원래 문장을 최대한 유지해줘. - 3. 단어나 문장이 정확하게 기록되지 않았을 수도 있으니, 그에 맞는 단어나 형태로 고쳐줘. - - 기존 텍스트에 기록되어 있던 추임새의 종류는 다음과 같고 : {self.statistics_filler_json} - 기록된 침묵 시간은 다음과 같아 : {self.statistics_silence_json} - - 너가 목표를 달성하고 만들어낸 텍스트와 기존 텍스트에 대한 피드백을 개행으로 구분해서 보여줘. 각각 한 문장으로 보여주면 되고 피드백은 추임새의 개수와 침묵 시간을 기반으로 이야기 해줘. - """ - - messages = [ - {"role": "system", "content": system_message} - ] - return messages - - def get_feedback(self): - prompt = self.create_feedback_prompt() - response = client.chat.completions.create( - model="gpt-4-turbo", - messages=prompt, - temperature=0.5, - 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 - ) - - insights = response.choices[0].message.content.split("\n") - return [r for r in insights if r] # 빈 문자열 제거 diff --git a/app/elevenLabs.py b/app/service/elevenLabs.py similarity index 89% rename from app/elevenLabs.py rename to app/service/elevenLabs.py index 82548f4..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( @@ -46,13 +47,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/service/gpt.py b/app/service/gpt.py new file mode 100644 index 0000000..33118ea --- /dev/null +++ b/app/service/gpt.py @@ -0,0 +1,52 @@ +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, schedules, alias): + self.schedules = schedules + self.alias = alias + + def create_schedule_prompt(self): + system_message = f""" + 너는 지금부터 혼자 사시는 부모님을 걱정하는 보호자야. + 네 역할은 키워드를 보고, 키워드와 관련한 문제에 대해서 부모님을 걱정하고, 생활은 챙겨주는거야. + 키워드는 다음과 같아: {str(self.schedules)} + + 너의 목표는 두 가지야: + 1. 키워드에 대한 질문 혹은 문장을 한 줄의 텍스트로 만들어. + ex) 키워드가 '저녁' 이라면, "{self.alias}~~ 하루 잘 보냈어?? 저녁도 맛있는거 챙겨먹어!! 사랑해~~ " + 2. 만든 텍스트는 ?? !! ~~ ,, .. 등의 다양한 특수문자가 많이 들어갈 수 있어. 감정이 강하게 느껴지게 작성해줘. + 2-a. 특수문자를 붙일 때는 꼭 2개씩 붙여줘 + 3. 부모님을 지칭하는 별명은 {self.alias} 로 해줘. + 4. 문장과 문장 사이의 띄어쓰기를 2개씩 넣어줘 + + 결과는 {{"키워드": "문장"}} 형태의 JSON 문자열로 반환해줘. 꼭 큰따옴표(")만 사용해. + + """ + + messages = [ + {"role": "system", "content": system_message} + ] + return messages + + def get_schedule_json(self): + prompt = self.create_schedule_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) + + return schedule_dict 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/service/record.py b/app/service/record.py new file mode 100644 index 0000000..b968f48 --- /dev/null +++ b/app/service/record.py @@ -0,0 +1,79 @@ +import os +import time +from datetime import datetime + +import numpy as np +import sounddevice as sd +import torch +from scipy.io.wavfile import write + +# 사일로 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(), "first_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/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/utils/convertFileExtension.py b/app/utils/convertFileExtension.py new file mode 100644 index 0000000..e27dd38 --- /dev/null +++ b/app/utils/convertFileExtension.py @@ -0,0 +1,48 @@ +import os +from datetime import datetime + +from pydub import AudioSegment + + +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) + output_path = file_path.replace(".wav", ".mp3") + os.remove(file_path) + audio.export(output_path, format="mp3") + return output_path + + +0 diff --git a/app/utils/parsing_json.py b/app/utils/parsing_json.py new file mode 100644 index 0000000..e0eb1be --- /dev/null +++ b/app/utils/parsing_json.py @@ -0,0 +1,15 @@ +import json +import re + + +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 {} diff --git a/app/utils/play_file.py b/app/utils/play_file.py new file mode 100644 index 0000000..2a549e3 --- /dev/null +++ b/app/utils/play_file.py @@ -0,0 +1,31 @@ +import time +from datetime import datetime +import os +import subprocess + + +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") + + #블루투스 헤드셋 또는 기본 스피커로 출력 + 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) diff --git a/audio/af07bfb5-7b12-4209-8681-2d3f5dedfd11.mp3 b/audio/af07bfb5-7b12-4209-8681-2d3f5dedfd11.mp3 deleted file mode 100644 index d6a96b6..0000000 Binary files a/audio/af07bfb5-7b12-4209-8681-2d3f5dedfd11.mp3 and /dev/null differ diff --git a/audio/b8016cd9-c376-47c2-8e87-cceb4abff687.mp3 b/audio/b8016cd9-c376-47c2-8e87-cceb4abff687.mp3 deleted file mode 100644 index 641325c..0000000 Binary files a/audio/b8016cd9-c376-47c2-8e87-cceb4abff687.mp3 and /dev/null differ diff --git a/requirements.txt b/requirements.txt index 6a33f62..52a2fd7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ anyio==4.9.0 boto3==1.37.16 botocore==1.37.16 certifi==2025.1.31 +cffi==1.17.1 charset-normalizer==3.4.1 click==8.1.8 colorama==0.4.6 @@ -11,13 +12,22 @@ dotenv==0.9.9 elevenlabs==1.54.0 exceptiongroup==1.2.2 fastapi==0.115.11 +filelock==3.18.0 +fsspec==2025.3.2 h11==0.14.0 httpcore==1.0.7 httpx==0.28.1 idna==3.10 +Jinja2==3.1.6 jiter==0.9.0 jmespath==1.0.1 +MarkupSafe==3.0.2 +mpmath==1.3.0 +networkx==3.2.1 +numpy==2.0.2 openai==1.68.2 +playsound==1.3.0 +pycparser==2.22 pydantic==2.10.6 pydantic_core==2.27.2 pydub==0.25.1 @@ -26,9 +36,14 @@ python-dotenv==1.0.1 python-multipart==0.0.20 requests==2.32.3 s3transfer==0.11.4 +scipy==1.13.1 six==1.17.0 sniffio==1.3.1 +sounddevice==0.5.1 starlette==0.46.1 +sympy==1.14.0 +torch==2.7.0 +torchaudio==2.7.0 tqdm==4.67.1 typing_extensions==4.12.2 urllib3==1.26.20