Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,582 +1,35 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
#
|
| 6 |
-
# 선택 패키지 (비디오 처리):
|
| 7 |
-
# - ffmpeg 설치: sudo apt-get install ffmpeg (Linux) / brew install ffmpeg (Mac)
|
| 8 |
-
# - 또는 pip install moviepy
|
| 9 |
-
#
|
| 10 |
-
# 환경 변수:
|
| 11 |
-
# .env 파일에 OPENAI_API_KEY 설정 필요
|
| 12 |
|
| 13 |
-
|
| 14 |
-
import gradio as gr
|
| 15 |
-
import openai
|
| 16 |
-
from dotenv import load_dotenv
|
| 17 |
-
import numpy as np
|
| 18 |
-
import wave
|
| 19 |
-
import subprocess
|
| 20 |
-
import mimetypes
|
| 21 |
-
|
| 22 |
-
# ─── 0. 초기화 ───────────────────────────────────────────────
|
| 23 |
-
load_dotenv()
|
| 24 |
-
openai.api_key = os.getenv("OPENAI_API_KEY")
|
| 25 |
-
if not openai.api_key:
|
| 26 |
-
raise RuntimeError("OPENAI_API_KEY 가 .env 에 없습니다!")
|
| 27 |
-
|
| 28 |
-
# ffmpeg 설치 확인
|
| 29 |
-
def check_ffmpeg():
|
| 30 |
-
try:
|
| 31 |
-
subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True)
|
| 32 |
-
return True
|
| 33 |
-
except:
|
| 34 |
-
return False
|
| 35 |
-
|
| 36 |
-
HAS_FFMPEG = check_ffmpeg()
|
| 37 |
-
if not HAS_FFMPEG:
|
| 38 |
-
print("⚠️ ffmpeg가 설치되어 있지 않습니다. 비디오 처리가 제한될 수 있습니다.")
|
| 39 |
-
print("설치 방법: sudo apt-get install ffmpeg (Linux) / brew install ffmpeg (Mac)")
|
| 40 |
-
|
| 41 |
-
LANG = ["Korean","English","Japanese","Chinese",
|
| 42 |
-
"Thai","Russian","Vietnamese","Spanish","French"]
|
| 43 |
-
VOICE = {l: ("nova" if l in ["Korean","Japanese","Chinese"] else "alloy")
|
| 44 |
-
for l in LANG}
|
| 45 |
-
FOUR = ["English","Chinese","Thai","Russian"]
|
| 46 |
-
WS_URL = "wss://api.openai.com/v1/realtime" # 올바른 엔드포인트로 수정
|
| 47 |
-
|
| 48 |
-
# ─── 1. 공통 GPT 번역 / TTS ─────────────────────────────────
|
| 49 |
-
# 전역 클라이언트 관리
|
| 50 |
-
client = None
|
| 51 |
-
|
| 52 |
-
def get_client():
|
| 53 |
-
global client
|
| 54 |
-
if client is None:
|
| 55 |
-
client = openai.AsyncClient()
|
| 56 |
-
return client
|
| 57 |
-
|
| 58 |
-
async def gpt_translate(text, src, tgt):
|
| 59 |
-
try:
|
| 60 |
-
client = get_client()
|
| 61 |
-
rsp = await client.chat.completions.create(
|
| 62 |
-
model="gpt-3.5-turbo",
|
| 63 |
-
messages=[{"role":"system",
|
| 64 |
-
"content":f"Translate {src} → {tgt}. Return only the text."},
|
| 65 |
-
{"role":"user","content":text}],
|
| 66 |
-
temperature=0.3,max_tokens=2048)
|
| 67 |
-
return rsp.choices[0].message.content.strip()
|
| 68 |
-
except Exception as e:
|
| 69 |
-
print(f"번역 오류: {e}")
|
| 70 |
-
return ""
|
| 71 |
-
|
| 72 |
-
async def gpt_tts(text, lang):
|
| 73 |
-
try:
|
| 74 |
-
client = get_client()
|
| 75 |
-
rsp = await client.audio.speech.create(
|
| 76 |
-
model="tts-1", voice=VOICE[lang], input=text[:4096])
|
| 77 |
-
tmp = tempfile.NamedTemporaryFile(delete=False,suffix=".mp3")
|
| 78 |
-
tmp.write(rsp.content); tmp.close(); return tmp.name
|
| 79 |
-
except Exception as e:
|
| 80 |
-
print(f"TTS 오류: {e}")
|
| 81 |
-
return None
|
| 82 |
-
|
| 83 |
-
# ─── 2. PDF 번역 ────────────────────────────────────────────
|
| 84 |
-
def translate_pdf(file, src, tgt):
|
| 85 |
-
if not file: return "⚠️ PDF 업로드 필요", ""
|
| 86 |
-
with pdfplumber.open(file.name) as pdf:
|
| 87 |
-
text = "\n".join(p.extract_text() or "" for p in pdf.pages[:5]).strip()
|
| 88 |
-
if not text:
|
| 89 |
-
return "⚠️ 텍스트 추출 실패", ""
|
| 90 |
-
return text, asyncio.run(gpt_translate(text, src, tgt))
|
| 91 |
-
|
| 92 |
-
# ─── 2-1. 오디오 번역 (탭1용) ────────────────────────────────
|
| 93 |
-
def extract_audio_from_video(video_path):
|
| 94 |
-
"""MP4 등 비디오 파일에서 오디오 추출"""
|
| 95 |
-
audio_output = None
|
| 96 |
-
try:
|
| 97 |
-
# 임시 오디오 파일 생성
|
| 98 |
-
audio_output = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
|
| 99 |
-
audio_output.close()
|
| 100 |
-
|
| 101 |
-
# 방법 1: ffmpeg 사용 시도
|
| 102 |
-
if HAS_FFMPEG:
|
| 103 |
-
cmd = [
|
| 104 |
-
'ffmpeg',
|
| 105 |
-
'-i', video_path,
|
| 106 |
-
'-vn', # 비디오 스트림 제거
|
| 107 |
-
'-acodec', 'pcm_s16le', # WAV 포맷
|
| 108 |
-
'-ar', '16000', # 16kHz 샘플링
|
| 109 |
-
'-ac', '1', # 모노
|
| 110 |
-
'-y', # 덮어쓰기
|
| 111 |
-
audio_output.name
|
| 112 |
-
]
|
| 113 |
-
|
| 114 |
-
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 115 |
-
|
| 116 |
-
if result.returncode == 0:
|
| 117 |
-
return audio_output.name
|
| 118 |
-
else:
|
| 119 |
-
print(f"ffmpeg 오류: {result.stderr}")
|
| 120 |
-
|
| 121 |
-
# 방법 2: moviepy 사용 시도
|
| 122 |
-
try:
|
| 123 |
-
from moviepy.editor import VideoFileClip
|
| 124 |
-
print("moviepy를 사용하여 오디오 추출 중...")
|
| 125 |
-
video = VideoFileClip(video_path)
|
| 126 |
-
video.audio.write_audiofile(
|
| 127 |
-
audio_output.name,
|
| 128 |
-
fps=16000,
|
| 129 |
-
nbytes=2,
|
| 130 |
-
codec='pcm_s16le',
|
| 131 |
-
verbose=False,
|
| 132 |
-
logger=None
|
| 133 |
-
)
|
| 134 |
-
video.close()
|
| 135 |
-
return audio_output.name
|
| 136 |
-
except ImportError:
|
| 137 |
-
raise Exception(
|
| 138 |
-
"비디오 처리를 위해 ffmpeg 또는 moviepy가 필요합니다.\n"
|
| 139 |
-
"설치: pip install moviepy 또는 ffmpeg 설치"
|
| 140 |
-
)
|
| 141 |
-
except Exception as e:
|
| 142 |
-
raise Exception(f"moviepy 오류: {str(e)}")
|
| 143 |
-
|
| 144 |
-
except Exception as e:
|
| 145 |
-
# 오류 시 임시 파일 정리
|
| 146 |
-
if audio_output and os.path.exists(audio_output.name):
|
| 147 |
-
os.unlink(audio_output.name)
|
| 148 |
-
raise e
|
| 149 |
-
|
| 150 |
-
async def translate_audio_async(file, src, tgt):
|
| 151 |
-
if not file: return "⚠️ 오디오/비디오 업로드 필요", "", None
|
| 152 |
-
|
| 153 |
try:
|
| 154 |
-
#
|
| 155 |
-
|
| 156 |
-
audio_file_path = file
|
| 157 |
-
temp_audio_path = None
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
print(f"파일 크기: {os.path.getsize(file) / 1024 / 1024:.1f} MB")
|
| 163 |
-
print("비디오에서 오디오 추출 중... (시간이 걸릴 수 있습니다)")
|
| 164 |
-
temp_audio_path = extract_audio_from_video(file)
|
| 165 |
-
audio_file_path = temp_audio_path
|
| 166 |
-
print("오디오 추출 완료!")
|
| 167 |
|
| 168 |
-
#
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
transcript = await client.audio.transcriptions.create(
|
| 173 |
-
model="whisper-1",
|
| 174 |
-
file=audio_file,
|
| 175 |
-
language=src[:2].lower() # 언어 코드 간소화
|
| 176 |
-
)
|
| 177 |
|
| 178 |
-
#
|
| 179 |
-
|
| 180 |
-
os.unlink(temp_audio_path)
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
# 번역
|
| 189 |
-
print(f"{src} → {tgt} 번역 중...")
|
| 190 |
-
trans_text = await gpt_translate(orig_text, src, tgt)
|
| 191 |
-
|
| 192 |
-
# TTS
|
| 193 |
-
print("음성 합성 중...")
|
| 194 |
-
audio_path = await gpt_tts(trans_text, tgt)
|
| 195 |
-
|
| 196 |
-
return orig_text, trans_text, audio_path
|
| 197 |
-
except Exception as e:
|
| 198 |
-
print(f"오디오 번역 오류: {e}")
|
| 199 |
-
# 임시 파일 정리
|
| 200 |
-
if 'temp_audio_path' in locals() and temp_audio_path and os.path.exists(temp_audio_path):
|
| 201 |
-
os.unlink(temp_audio_path)
|
| 202 |
-
|
| 203 |
-
error_msg = str(e)
|
| 204 |
-
if "ffmpeg" in error_msg.lower():
|
| 205 |
-
error_msg += "\n\n💡 해결 방법:\n1. ffmpeg 설치: sudo apt-get install ffmpeg\n2. 또는 pip install moviepy"
|
| 206 |
-
|
| 207 |
-
return "⚠️ 번역 중 오류 발생", error_msg, None
|
| 208 |
-
|
| 209 |
-
def translate_audio(file, src, tgt):
|
| 210 |
-
return asyncio.run(translate_audio_async(file, src, tgt))
|
| 211 |
-
|
| 212 |
-
# ─── 3. 실시간 STT (Whisper API 사용) ──────────────────────────
|
| 213 |
-
async def process_audio_chunk(audio_data, src_lang):
|
| 214 |
-
"""오디오 청크를 처리하여 텍스트로 변환"""
|
| 215 |
-
if audio_data is None:
|
| 216 |
-
return ""
|
| 217 |
-
|
| 218 |
-
try:
|
| 219 |
-
# Gradio는 (sample_rate, audio_array) 튜플을 반환
|
| 220 |
-
if isinstance(audio_data, tuple):
|
| 221 |
-
sample_rate, audio_array = audio_data
|
| 222 |
-
|
| 223 |
-
# 오디오가 너무 짧으면 무시 (0.5초 미만)
|
| 224 |
-
if len(audio_array) < sample_rate * 0.5:
|
| 225 |
-
return ""
|
| 226 |
-
|
| 227 |
-
# 오디오 정규화 및 노이즈 필터링
|
| 228 |
-
audio_array = audio_array.astype(np.float32)
|
| 229 |
-
|
| 230 |
-
# 무음 감지 - RMS가 너무 낮으면 무시
|
| 231 |
-
rms = np.sqrt(np.mean(audio_array**2))
|
| 232 |
-
if rms < 0.01: # 무음 임계값
|
| 233 |
-
return ""
|
| 234 |
-
|
| 235 |
-
# 정규화
|
| 236 |
-
max_val = np.max(np.abs(audio_array))
|
| 237 |
-
if max_val > 0:
|
| 238 |
-
audio_array = audio_array / max_val * 0.95
|
| 239 |
-
|
| 240 |
-
# numpy array를 WAV 파일로 변환
|
| 241 |
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
| 242 |
-
with wave.open(tmp.name, 'wb') as wav_file:
|
| 243 |
-
wav_file.setnchannels(1) # mono
|
| 244 |
-
wav_file.setsampwidth(2) # 16-bit
|
| 245 |
-
wav_file.setframerate(sample_rate)
|
| 246 |
-
|
| 247 |
-
# float32를 16-bit PCM으로 변환
|
| 248 |
-
audio_int16 = (audio_array * 32767).astype(np.int16)
|
| 249 |
-
wav_file.writeframes(audio_int16.tobytes())
|
| 250 |
-
tmp_path = tmp.name
|
| 251 |
-
else:
|
| 252 |
-
# bytes 데이터인 경우
|
| 253 |
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
| 254 |
-
tmp.write(audio_data)
|
| 255 |
-
tmp_path = tmp.name
|
| 256 |
-
|
| 257 |
-
# Whisper API로 변환 - 언어 힌트와 프롬프트 추가
|
| 258 |
-
with open(tmp_path, 'rb') as audio_file:
|
| 259 |
-
# 언어별 프롬프트 설정으로 hallucination 방지
|
| 260 |
-
language_prompts = {
|
| 261 |
-
"Korean": "이것은 한국어 대화입니다.",
|
| 262 |
-
"English": "This is an English conversation.",
|
| 263 |
-
"Japanese": "これは日本語の会話です。",
|
| 264 |
-
"Chinese": "这是中文对话。",
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
prompt = language_prompts.get(src_lang, "")
|
| 268 |
|
| 269 |
-
client = get_client()
|
| 270 |
-
transcript = await client.audio.transcriptions.create(
|
| 271 |
-
model="whisper-1",
|
| 272 |
-
file=audio_file,
|
| 273 |
-
language=src_lang[:2].lower(),
|
| 274 |
-
prompt=prompt,
|
| 275 |
-
temperature=0.0 # 더 보수적인 추론
|
| 276 |
-
)
|
| 277 |
-
|
| 278 |
-
os.unlink(tmp_path) # 임시 파일 삭제
|
| 279 |
-
|
| 280 |
-
# 결과 후처리 - 반복되는 패턴 제거
|
| 281 |
-
text = transcript.text.strip()
|
| 282 |
-
|
| 283 |
-
# 같은 문장이 반복되는 경우 처리
|
| 284 |
-
sentences = text.split('.')
|
| 285 |
-
if len(sentences) > 1:
|
| 286 |
-
unique_sentences = []
|
| 287 |
-
for sent in sentences:
|
| 288 |
-
sent = sent.strip()
|
| 289 |
-
if sent and (not unique_sentences or sent != unique_sentences[-1]):
|
| 290 |
-
unique_sentences.append(sent)
|
| 291 |
-
text = '. '.join(unique_sentences)
|
| 292 |
-
if text and not text.endswith('.'):
|
| 293 |
-
text += '.'
|
| 294 |
-
|
| 295 |
-
# 뉴스 관련 hallucination 패턴 감지 및 제거
|
| 296 |
-
hallucination_patterns = [
|
| 297 |
-
"MBC 뉴스", "KBS 뉴스", "SBS 뉴스", "JTBC 뉴스",
|
| 298 |
-
"뉴스룸", "뉴스데스크", "앵커", "기자입니다"
|
| 299 |
-
]
|
| 300 |
-
|
| 301 |
-
# 짧은 텍스트에서 뉴스 패턴이 감지되면 무시
|
| 302 |
-
if len(text) < 50 and any(pattern in text for pattern in hallucination_patterns):
|
| 303 |
-
return ""
|
| 304 |
-
|
| 305 |
-
return text
|
| 306 |
-
|
| 307 |
except Exception as e:
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
# ─── 4. Gradio 스트림 핸들러 (동기 버전) ─────────────────────
|
| 312 |
-
def realtime_single_sync(audio, src, tgt, state):
|
| 313 |
-
"""동기 버전의 실시간 단일 언어 번역"""
|
| 314 |
-
if state is None:
|
| 315 |
-
state = {"orig": "", "trans": "", "audio_buffer": [], "sample_rate": None}
|
| 316 |
-
|
| 317 |
-
if audio is None:
|
| 318 |
-
# 스트림 종료 시 남은 버퍼 처리
|
| 319 |
-
if state["audio_buffer"] and state["sample_rate"]:
|
| 320 |
-
try:
|
| 321 |
-
# 버퍼의 오디오 합치기
|
| 322 |
-
combined_audio = np.concatenate(state["audio_buffer"])
|
| 323 |
-
audio_data = (state["sample_rate"], combined_audio)
|
| 324 |
-
|
| 325 |
-
# 비동기 작업 실행
|
| 326 |
-
text = asyncio.run(process_audio_chunk(audio_data, src))
|
| 327 |
-
if text:
|
| 328 |
-
state["orig"] = state["orig"] + " " + text if state["orig"] else text
|
| 329 |
-
trans = asyncio.run(gpt_translate(text, src, tgt))
|
| 330 |
-
state["trans"] = state["trans"] + " " + trans if state["trans"] else trans
|
| 331 |
-
except Exception as e:
|
| 332 |
-
print(f"처리 오류: {e}")
|
| 333 |
-
state["audio_buffer"] = []
|
| 334 |
-
|
| 335 |
-
return state["orig"], state["trans"], state
|
| 336 |
-
|
| 337 |
-
# 오디오 데이터 버퍼링
|
| 338 |
-
if isinstance(audio, tuple):
|
| 339 |
-
sample_rate, audio_array = audio
|
| 340 |
-
state["sample_rate"] = sample_rate
|
| 341 |
-
state["audio_buffer"].append(audio_array)
|
| 342 |
-
|
| 343 |
-
# 버퍼가 충분히 쌓였을 때만 처리 (약 2-3초 분량)
|
| 344 |
-
if state["audio_buffer"]: # 버퍼가 비어있지 않은지 확인
|
| 345 |
-
buffer_duration = len(np.concatenate(state["audio_buffer"])) / sample_rate
|
| 346 |
-
if buffer_duration >= 2.0: # 2초마다 처리
|
| 347 |
-
try:
|
| 348 |
-
# 버퍼의 오디오 합치기
|
| 349 |
-
combined_audio = np.concatenate(state["audio_buffer"])
|
| 350 |
-
audio_data = (sample_rate, combined_audio)
|
| 351 |
-
|
| 352 |
-
# STT
|
| 353 |
-
text = asyncio.run(process_audio_chunk(audio_data, src))
|
| 354 |
-
if text:
|
| 355 |
-
state["orig"] = state["orig"] + " " + text if state["orig"] else text
|
| 356 |
-
|
| 357 |
-
# 번역
|
| 358 |
-
trans = asyncio.run(gpt_translate(text, src, tgt))
|
| 359 |
-
state["trans"] = state["trans"] + " " + trans if state["trans"] else trans
|
| 360 |
-
|
| 361 |
-
# 버퍼 초기화
|
| 362 |
-
state["audio_buffer"] = []
|
| 363 |
-
except Exception as e:
|
| 364 |
-
print(f"처리 오류: {e}")
|
| 365 |
-
state["audio_buffer"] = [] # 오류 시에도 버퍼 초기화
|
| 366 |
-
|
| 367 |
-
return state["orig"], state["trans"], state
|
| 368 |
-
|
| 369 |
-
def realtime_four_sync(audio, src, state):
|
| 370 |
-
"""동기 버전의 실시간 4언어 번역"""
|
| 371 |
-
if state is None:
|
| 372 |
-
state = {"orig": "", "English": "", "Chinese": "", "Thai": "", "Russian": "",
|
| 373 |
-
"audio_buffer": [], "sample_rate": None}
|
| 374 |
-
|
| 375 |
-
if audio is None:
|
| 376 |
-
# 스트림 종료 시 남은 버퍼 처리
|
| 377 |
-
if state["audio_buffer"] and state["sample_rate"]:
|
| 378 |
-
try:
|
| 379 |
-
combined_audio = np.concatenate(state["audio_buffer"])
|
| 380 |
-
audio_data = (state["sample_rate"], combined_audio)
|
| 381 |
-
|
| 382 |
-
text = asyncio.run(process_audio_chunk(audio_data, src))
|
| 383 |
-
if text:
|
| 384 |
-
state["orig"] = state["orig"] + " " + text if state["orig"] else text
|
| 385 |
-
|
| 386 |
-
# 순차적으로 번역 (병렬 처리 시 문제 발생 가능)
|
| 387 |
-
for lang in FOUR:
|
| 388 |
-
trans = asyncio.run(gpt_translate(text, src, lang))
|
| 389 |
-
state[lang] = state[lang] + " " + trans if state[lang] else trans
|
| 390 |
-
except Exception as e:
|
| 391 |
-
print(f"처리 오류: {e}")
|
| 392 |
-
state["audio_buffer"] = []
|
| 393 |
-
|
| 394 |
-
return (state["orig"], state["English"], state["Chinese"],
|
| 395 |
-
state["Thai"], state["Russian"], state)
|
| 396 |
-
|
| 397 |
-
# 오디오 데이터 버퍼링
|
| 398 |
-
if isinstance(audio, tuple):
|
| 399 |
-
sample_rate, audio_array = audio
|
| 400 |
-
state["sample_rate"] = sample_rate
|
| 401 |
-
state["audio_buffer"].append(audio_array)
|
| 402 |
-
|
| 403 |
-
# 버퍼가 충분히 쌓였을 때만 처리
|
| 404 |
-
if state["audio_buffer"]: # 버퍼가 비어있지 않은지 확인
|
| 405 |
-
buffer_duration = len(np.concatenate(state["audio_buffer"])) / sample_rate
|
| 406 |
-
if buffer_duration >= 2.0: # 2초마다 처리
|
| 407 |
-
try:
|
| 408 |
-
combined_audio = np.concatenate(state["audio_buffer"])
|
| 409 |
-
audio_data = (sample_rate, combined_audio)
|
| 410 |
-
|
| 411 |
-
# STT
|
| 412 |
-
text = asyncio.run(process_audio_chunk(audio_data, src))
|
| 413 |
-
if text:
|
| 414 |
-
state["orig"] = state["orig"] + " " + text if state["orig"] else text
|
| 415 |
-
|
| 416 |
-
# 4개 언어로 순차 번역
|
| 417 |
-
for lang in FOUR:
|
| 418 |
-
trans = asyncio.run(gpt_translate(text, src, lang))
|
| 419 |
-
state[lang] = state[lang] + " " + trans if state[lang] else trans
|
| 420 |
-
|
| 421 |
-
state["audio_buffer"] = []
|
| 422 |
-
except Exception as e:
|
| 423 |
-
print(f"처리 오류: {e}")
|
| 424 |
-
state["audio_buffer"] = []
|
| 425 |
-
|
| 426 |
-
return (state["orig"], state["English"], state["Chinese"],
|
| 427 |
-
state["Thai"], state["Russian"], state)
|
| 428 |
-
|
| 429 |
-
# ─── 5. UI ──────────────────────────────────────────────────
|
| 430 |
-
with gr.Blocks(title="SMARTok Demo", theme=gr.themes.Soft()) as demo:
|
| 431 |
-
gr.Markdown(
|
| 432 |
-
"""
|
| 433 |
-
# 🌍 SMARTok 실시간 번역 시스템
|
| 434 |
-
|
| 435 |
-
다국어 실시간 번역을 지원하는 통합 번역 플랫폼
|
| 436 |
-
"""
|
| 437 |
-
)
|
| 438 |
-
|
| 439 |
-
with gr.Tabs():
|
| 440 |
-
# 탭 1 – 오디오 번역
|
| 441 |
-
with gr.TabItem("🎙️ 오디오/비디오"):
|
| 442 |
-
gr.Markdown("### 🌐 오디오/비디오 파일 번역")
|
| 443 |
-
|
| 444 |
-
with gr.Row():
|
| 445 |
-
src1 = gr.Dropdown(LANG, value="Korean", label="입력 언어")
|
| 446 |
-
tgt1 = gr.Dropdown(LANG, value="English", label="출력 언어")
|
| 447 |
-
|
| 448 |
-
with gr.Tabs():
|
| 449 |
-
with gr.TabItem("📁 파일 업로드"):
|
| 450 |
-
# 파일 업로드 - 오디오와 비디오 모두 지원
|
| 451 |
-
aud1_file = gr.File(
|
| 452 |
-
label="오디오/비디오 파일 업로드",
|
| 453 |
-
file_types=[".mp3", ".wav", ".m4a", ".flac", ".ogg", ".opus",
|
| 454 |
-
".mp4", ".avi", ".mov", ".mkv", ".webm", ".flv"],
|
| 455 |
-
type="filepath"
|
| 456 |
-
)
|
| 457 |
-
gr.Markdown(
|
| 458 |
-
"📌 **지원 형식**\n"
|
| 459 |
-
"- 오디오: MP3, WAV, M4A, FLAC, OGG, OPUS\n"
|
| 460 |
-
"- 비디오: MP4, AVI, MOV, MKV, WebM, FLV\n\n"
|
| 461 |
-
"⚠️ **주의사항**\n"
|
| 462 |
-
"- 비디오 파일은 오디오 추출 시간이 필요합니다\n"
|
| 463 |
-
"- 대용량 파일은 처리 시간이 오래 걸릴 수 있습니다"
|
| 464 |
-
)
|
| 465 |
-
|
| 466 |
-
with gr.TabItem("🎤 마이크 녹음"):
|
| 467 |
-
aud1_mic = gr.Audio(
|
| 468 |
-
sources=["microphone"],
|
| 469 |
-
type="filepath",
|
| 470 |
-
label="마이크 녹음"
|
| 471 |
-
)
|
| 472 |
-
gr.Markdown("💡 **팁**: 녹음 후 '정지' 버튼을 눌러주세요")
|
| 473 |
-
|
| 474 |
-
btn1 = gr.Button("🔄 번역 시작", variant="primary", size="lg")
|
| 475 |
-
|
| 476 |
-
# 진행 상태 표시
|
| 477 |
-
status1 = gr.Textbox(label="진행 상태", value="대기 중...", interactive=False)
|
| 478 |
-
|
| 479 |
-
with gr.Row():
|
| 480 |
-
with gr.Column():
|
| 481 |
-
o1 = gr.Textbox(label="📝 원문", lines=6)
|
| 482 |
-
with gr.Column():
|
| 483 |
-
t1 = gr.Textbox(label="📝 번역", lines=6)
|
| 484 |
-
|
| 485 |
-
a1 = gr.Audio(label="🔊 번역된 음성 (TTS)", type="filepath", autoplay=True)
|
| 486 |
-
|
| 487 |
-
# 파일이나 마이크 중 활성화된 입력 사용
|
| 488 |
-
def translate_with_status(file_input, mic_input, src, tgt):
|
| 489 |
-
active_input = file_input if file_input else mic_input
|
| 490 |
-
if not active_input:
|
| 491 |
-
return "⚠️ 파일을 업로드하거나 녹음을 해주세요", "", None
|
| 492 |
-
|
| 493 |
-
# 상태 업데이트는 동기 함수에서 처리
|
| 494 |
-
return translate_audio(active_input, src, tgt)
|
| 495 |
-
|
| 496 |
-
btn1.click(
|
| 497 |
-
lambda: "처리 중... 잠시만 기다려주세요 ⏳",
|
| 498 |
-
outputs=status1
|
| 499 |
-
).then(
|
| 500 |
-
translate_with_status,
|
| 501 |
-
[aud1_file, aud1_mic, src1, tgt1],
|
| 502 |
-
[o1, t1, a1]
|
| 503 |
-
).then(
|
| 504 |
-
lambda: "✅ 완료!",
|
| 505 |
-
outputs=status1
|
| 506 |
-
)
|
| 507 |
-
|
| 508 |
-
# 탭 2 – PDF 번역
|
| 509 |
-
with gr.TabItem("📄 PDF"):
|
| 510 |
-
src2 = gr.Dropdown(LANG, value="Korean", label="입력 언어")
|
| 511 |
-
tgt2 = gr.Dropdown(LANG, value="English", label="출력 언어")
|
| 512 |
-
pdf = gr.File(file_types=[".pdf"])
|
| 513 |
-
btn2 = gr.Button("번역")
|
| 514 |
-
o2 = gr.Textbox(label="추출 원문", lines=15)
|
| 515 |
-
t2 = gr.Textbox(label="번역 결과", lines=15)
|
| 516 |
-
|
| 517 |
-
btn2.click(translate_pdf, [pdf, src2, tgt2], [o2, t2])
|
| 518 |
-
|
| 519 |
-
# 탭 3 – 실시간 1언어
|
| 520 |
-
with gr.TabItem("⏱️ 실시간 1"):
|
| 521 |
-
src3 = gr.Dropdown(LANG, value="Korean", label="입력 언어")
|
| 522 |
-
tgt3 = gr.Dropdown(LANG, value="English", label="출력 언어")
|
| 523 |
-
|
| 524 |
-
with gr.Row():
|
| 525 |
-
with gr.Column():
|
| 526 |
-
gr.Markdown("🎤 **마이크 입력**")
|
| 527 |
-
mic3 = gr.Audio(
|
| 528 |
-
sources=["microphone"],
|
| 529 |
-
streaming=True,
|
| 530 |
-
type="numpy", # numpy 형식 명시
|
| 531 |
-
label="마이크"
|
| 532 |
-
)
|
| 533 |
-
gr.Markdown("💡 **사용 방법**\n- 2-3초 정도 문장을 말씀해주세요\n- 너무 짧거나 긴 문장은 인식이 어려울 수 있습니다")
|
| 534 |
-
|
| 535 |
-
with gr.Column():
|
| 536 |
-
o3 = gr.Textbox(label="원문(실시간)", lines=8, interactive=False)
|
| 537 |
-
t3 = gr.Textbox(label="번역(실시간)", lines=8, interactive=False)
|
| 538 |
-
|
| 539 |
-
st3 = gr.State()
|
| 540 |
-
|
| 541 |
-
# stream 메서드 수정
|
| 542 |
-
mic3.stream(
|
| 543 |
-
realtime_single_sync,
|
| 544 |
-
inputs=[mic3, src3, tgt3, st3],
|
| 545 |
-
outputs=[o3, t3, st3],
|
| 546 |
-
stream_every=0.5 # 0.5초마다 스트림 (time_limit 제거)
|
| 547 |
-
)
|
| 548 |
-
|
| 549 |
-
# 탭 4 – 실시간 4언어
|
| 550 |
-
with gr.TabItem("🌏 실시간 4"):
|
| 551 |
-
src4 = gr.Dropdown(LANG, value="Korean", label="입력 언어")
|
| 552 |
-
|
| 553 |
-
with gr.Row():
|
| 554 |
-
with gr.Column(scale=1):
|
| 555 |
-
gr.Markdown("🎤 **마이크 입력**")
|
| 556 |
-
mic4 = gr.Audio(
|
| 557 |
-
sources=["microphone"],
|
| 558 |
-
streaming=True,
|
| 559 |
-
type="numpy",
|
| 560 |
-
label="마이크"
|
| 561 |
-
)
|
| 562 |
-
o4 = gr.Textbox(label="원문", lines=8, interactive=False)
|
| 563 |
-
|
| 564 |
-
with gr.Column(scale=2):
|
| 565 |
-
with gr.Row():
|
| 566 |
-
e4 = gr.Textbox(label="English", lines=8, interactive=False)
|
| 567 |
-
c4 = gr.Textbox(label="Chinese(简体)", lines=8, interactive=False)
|
| 568 |
-
with gr.Row():
|
| 569 |
-
th4 = gr.Textbox(label="Thai", lines=8, interactive=False)
|
| 570 |
-
r4 = gr.Textbox(label="Russian", lines=8, interactive=False)
|
| 571 |
-
|
| 572 |
-
st4 = gr.State()
|
| 573 |
-
|
| 574 |
-
# stream 메서드 수정
|
| 575 |
-
mic4.stream(
|
| 576 |
-
realtime_four_sync,
|
| 577 |
-
inputs=[mic4, src4, st4],
|
| 578 |
-
outputs=[o4, e4, c4, th4, r4, st4],
|
| 579 |
-
stream_every=0.5
|
| 580 |
-
)
|
| 581 |
|
| 582 |
-
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import streamlit as st
|
| 4 |
+
from tempfile import NamedTemporaryFile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
+
def main():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
try:
|
| 8 |
+
# Get the code from secrets
|
| 9 |
+
code = os.environ.get("MAIN_CODE")
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
if not code:
|
| 12 |
+
st.error("⚠️ The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
|
| 13 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
# Create a temporary Python file
|
| 16 |
+
with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
|
| 17 |
+
tmp.write(code)
|
| 18 |
+
tmp_path = tmp.name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
# Execute the code
|
| 21 |
+
exec(compile(code, tmp_path, 'exec'), globals())
|
|
|
|
| 22 |
|
| 23 |
+
# Clean up the temporary file
|
| 24 |
+
try:
|
| 25 |
+
os.unlink(tmp_path)
|
| 26 |
+
except:
|
| 27 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
except Exception as e:
|
| 30 |
+
st.error(f"⚠️ Error loading or executing the application: {str(e)}")
|
| 31 |
+
import traceback
|
| 32 |
+
st.code(traceback.format_exc())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
if __name__ == "__main__":
|
| 35 |
+
main()
|