Spaces:
Running
on
Zero
Running
on
Zero
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()
|