4242
4343# 오디오 저장 폴더
4444OUTPUT_DIR = "generated_audio"
45- SHARED_DIR = "shared/tts" # 다른 서비스와 공유되는 폴더
45+ SHARED_DIR = "shared" # 세션 기반 공유 폴더 (상위)
46+ SHARED_TTS_DIR = "shared/tts" # 일반 TTS 공유 폴더
4647os .makedirs (OUTPUT_DIR , exist_ok = True )
4748os .makedirs (SHARED_DIR , exist_ok = True )
49+ os .makedirs (SHARED_TTS_DIR , exist_ok = True )
50+
51+
52+ def get_session_dir (session_id : str ) -> str :
53+ """세션 디렉토리 경로 반환 (없으면 생성)"""
54+ session_dir = os .path .join (SHARED_DIR , session_id )
55+ os .makedirs (session_dir , exist_ok = True )
56+ return session_dir
4857
4958# 파일 자동 삭제 설정 (시간 단위)
5059FILE_MAX_AGE_HOURS = 3
@@ -72,7 +81,7 @@ async def periodic_cleanup():
7281 while True :
7382 await asyncio .sleep (1800 ) # 30분
7483 cleanup_old_files (OUTPUT_DIR )
75- cleanup_old_files (SHARED_DIR )
84+ cleanup_old_files (SHARED_TTS_DIR )
7685
7786
7887# Static 파일 서빙
@@ -85,12 +94,13 @@ async def startup_event():
8594 """서버 시작 시 초기화"""
8695 # 시작 시 오래된 파일 정리
8796 cleanup_old_files (OUTPUT_DIR )
88- cleanup_old_files (SHARED_DIR )
97+ cleanup_old_files (SHARED_TTS_DIR )
8998
9099 # 백그라운드 정리 태스크 시작
91100 asyncio .create_task (periodic_cleanup ())
92101 print (f"[Cleanup] 자동 파일 정리 활성화 ({ FILE_MAX_AGE_HOURS } 시간 이상 파일 삭제)" )
93- print (f"[Shared] TTS 파일이 { SHARED_DIR } 에도 저장됩니다" )
102+ print (f"[Shared] 일반 TTS: { SHARED_TTS_DIR } " )
103+ print (f"[Shared] 세션 기반 TTS: { SHARED_DIR } /{{session_id}}/" )
94104
95105
96106class TTSRequest (BaseModel ):
@@ -110,10 +120,43 @@ class TTSResponse(BaseModel):
110120 shared_path : str # 다른 서비스에서 접근 가능한 경로
111121
112122
123+ class SessionTTSRequest (BaseModel ):
124+ """세션 기반 TTS 요청"""
125+ session_id : str # 세션 ID (필수)
126+ text : str # 생성할 텍스트 (필수)
127+ output_filename : Optional [str ] = "tts_audio.mp3" # 저장할 파일명
128+ voice_id : Optional [str ] = None
129+ model_id : Optional [str ] = None
130+ stability : Optional [float ] = 0.8
131+ similarity_boost : Optional [float ] = 0.8
132+ style : Optional [float ] = 0.4
133+ use_speaker_boost : Optional [bool ] = True
134+
135+
136+ class SessionTTSResponse (BaseModel ):
137+ """세션 기반 TTS 응답"""
138+ success : bool
139+ session_id : str
140+ filename : str
141+ session_path : str # 세션 폴더 내 경로
142+ message : str
143+
144+
113145@app .get ("/" )
114146async def root ():
115- """메인 페이지 - static/index.html 반환"""
116- return FileResponse ("static/index.html" )
147+ """API 루트"""
148+ return {
149+ "message" : "ElevenLabs TTS API" ,
150+ "version" : "1.0.0" ,
151+ "endpoints" : {
152+ "POST /generate" : "TTS 생성 (일반)" ,
153+ "POST /session/generate" : "TTS 생성 (세션 기반)" ,
154+ "GET /audio/{filename}" : "오디오 파일 다운로드" ,
155+ "GET /session/{session_id}/files" : "세션 내 파일 목록" ,
156+ "GET /session/{session_id}/audio/{filename}" : "세션 내 오디오 다운로드" ,
157+ "GET /health" : "헬스 체크"
158+ }
159+ }
117160
118161
119162@app .get ("/health" )
@@ -157,7 +200,7 @@ async def generate_tts(request: TTSRequest):
157200 timestamp = datetime .now ().strftime ("%Y%m%d_%H%M%S" )
158201 filename = f"tts_{ timestamp } .mp3"
159202 filepath = os .path .join (OUTPUT_DIR , filename )
160- shared_filepath = os .path .join (SHARED_DIR , filename )
203+ shared_filepath = os .path .join (SHARED_TTS_DIR , filename )
161204
162205 # 오디오 데이터를 메모리에 먼저 저장
163206 audio_data = b""
@@ -233,6 +276,107 @@ async def serve_audio(filename: str):
233276 raise HTTPException (status_code = 404 , detail = "File not found" )
234277
235278
279+ # ============================================
280+ # 세션 기반 엔드포인트
281+ # ============================================
282+ @app .post ("/session/generate" , response_model = SessionTTSResponse , tags = ["Session" ])
283+ async def session_generate_tts (request : SessionTTSRequest ):
284+ """
285+ 세션 기반 TTS 생성
286+
287+ 결과 오디오가 세션 폴더에 저장됩니다.
288+ 다른 서비스(z_image, i2v 등)에서 동일한 session_id로 접근 가능합니다.
289+ """
290+ try :
291+ if not request .text :
292+ raise HTTPException (status_code = 400 , detail = "텍스트가 비어있습니다" )
293+
294+ session_dir = get_session_dir (request .session_id )
295+
296+ voice_id = request .voice_id or VOICE_ID
297+ model_id = request .model_id or MODEL_ID
298+
299+ # ElevenLabs 클라이언트
300+ client = ElevenLabs (api_key = API_KEY )
301+
302+ # Voice Settings
303+ voice_settings = VoiceSettings (
304+ stability = request .stability ,
305+ similarity_boost = request .similarity_boost ,
306+ style = request .style ,
307+ use_speaker_boost = request .use_speaker_boost
308+ )
309+
310+ # TTS 생성
311+ audio_stream = client .text_to_speech .convert (
312+ text = request .text ,
313+ voice_id = voice_id ,
314+ model_id = model_id ,
315+ voice_settings = voice_settings ,
316+ )
317+
318+ # 오디오 데이터를 메모리에 먼저 저장
319+ audio_data = b""
320+ for chunk in audio_stream :
321+ audio_data += chunk
322+
323+ # 파일명 설정
324+ output_filename = request .output_filename or "tts_audio.mp3"
325+ if not output_filename .endswith (".mp3" ):
326+ output_filename += ".mp3"
327+
328+ # 세션 폴더에 저장
329+ session_filepath = os .path .join (session_dir , output_filename )
330+ with open (session_filepath , "wb" ) as f :
331+ f .write (audio_data )
332+
333+ return SessionTTSResponse (
334+ success = True ,
335+ session_id = request .session_id ,
336+ filename = output_filename ,
337+ session_path = f"/app/shared/{ request .session_id } /{ output_filename } " ,
338+ message = f"세션 '{ request .session_id } '에 TTS 저장 완료"
339+ )
340+
341+ except HTTPException :
342+ raise
343+ except Exception as e :
344+ raise HTTPException (status_code = 500 , detail = str (e ))
345+
346+
347+ @app .get ("/session/{session_id}/files" , tags = ["Session" ])
348+ async def list_session_files (session_id : str ):
349+ """세션 폴더 내 파일 목록 조회"""
350+ session_dir = os .path .join (SHARED_DIR , session_id )
351+
352+ if not os .path .exists (session_dir ):
353+ return {"session_id" : session_id , "files" : [], "count" : 0 , "exists" : False }
354+
355+ files = []
356+ for f in Path (session_dir ).glob ("*" ):
357+ if f .is_file ():
358+ files .append ({
359+ "filename" : f .name ,
360+ "size_mb" : round (f .stat ().st_size / (1024 * 1024 ), 2 ),
361+ "created" : datetime .fromtimestamp (f .stat ().st_ctime ).isoformat ()
362+ })
363+ files .sort (key = lambda x : x ["created" ], reverse = True )
364+
365+ return {"session_id" : session_id , "files" : files , "count" : len (files ), "exists" : True }
366+
367+
368+ @app .get ("/session/{session_id}/audio/{filename}" , tags = ["Session" ])
369+ async def get_session_audio (session_id : str , filename : str ):
370+ """세션 폴더 내 오디오 파일 다운로드"""
371+ session_dir = os .path .join (SHARED_DIR , session_id )
372+ filepath = os .path .join (session_dir , filename )
373+
374+ if not os .path .exists (filepath ):
375+ raise HTTPException (status_code = 404 , detail = "파일을 찾을 수 없습니다" )
376+
377+ return FileResponse (filepath , media_type = "audio/mpeg" , filename = filename )
378+
379+
236380if __name__ == "__main__" :
237381 import uvicorn
238382 print ("=" * 60 )
0 commit comments