Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
@@ -4,14 +4,11 @@ import os
|
|
4 |
import tempfile
|
5 |
from dotenv import load_dotenv
|
6 |
|
7 |
-
#
|
8 |
load_dotenv()
|
9 |
-
|
10 |
-
# OpenAI 클라이언트 설정
|
11 |
api_key = os.getenv("OPENAI_API_KEY")
|
12 |
if not api_key:
|
13 |
print("⚠️ OPENAI_API_KEY를 .env 파일에 설정하세요!")
|
14 |
-
print("예: OPENAI_API_KEY=sk-...")
|
15 |
else:
|
16 |
print(f"✅ API Key 로드됨: {api_key[:10]}...")
|
17 |
|
@@ -21,189 +18,181 @@ except Exception as e:
|
|
21 |
print(f"❌ OpenAI 클라이언트 초기화 실패: {e}")
|
22 |
client = None
|
23 |
|
24 |
-
|
|
|
|
|
25 |
def translate_audio(audio_file, source_lang, target_lang):
|
26 |
-
"""음성 파일을 번역하는 함수"""
|
27 |
-
|
28 |
-
# 입력 검증
|
29 |
if not audio_file:
|
30 |
return "⚠️ 오디오 파일을 업로드하거나 녹음하세요.", "", None
|
31 |
-
|
32 |
-
|
33 |
-
return "❌ API 키가 설정되지 않았습니다. .env 파일을 확인하세요.", "", None
|
34 |
-
|
35 |
-
if not client:
|
36 |
-
return "❌ OpenAI 클라이언트가 초기화되지 않았습니다.", "", None
|
37 |
-
|
38 |
-
# 같은 언어로 번역하려는 경우
|
39 |
if source_lang == target_lang:
|
40 |
return "⚠️ 입력 언어와 출력 언어가 같습니다.", "", None
|
41 |
|
42 |
try:
|
43 |
-
print(f"🎤 오디오 파일 처리 중: {audio_file}")
|
44 |
-
print(f"📊 파일 크기: {os.path.getsize(audio_file) / 1024 / 1024:.2f} MB")
|
45 |
-
|
46 |
-
# 1. Whisper로 음성을 텍스트로 변환
|
47 |
-
print("1️⃣ 음성 인식 시작...")
|
48 |
with open(audio_file, "rb") as f:
|
49 |
transcript = client.audio.transcriptions.create(
|
50 |
model="whisper-1",
|
51 |
file=f,
|
52 |
language=source_lang[:2].lower() if source_lang != "Chinese" else "zh"
|
53 |
)
|
54 |
-
original_text = transcript.text
|
55 |
-
|
|
|
56 |
|
57 |
-
# 빈 텍스트 체크
|
58 |
-
if not original_text.strip():
|
59 |
-
return "⚠️ 음성이 인식되지 않았습니다. 다시 녹음해주세요.", "", None
|
60 |
-
|
61 |
-
# 2. GPT-4로 번역
|
62 |
-
print("2️⃣ 번역 시작...")
|
63 |
response = client.chat.completions.create(
|
64 |
-
model="gpt-3.5-turbo",
|
65 |
messages=[
|
66 |
-
{
|
67 |
-
|
68 |
-
|
69 |
-
}
|
70 |
-
{
|
71 |
-
"role": "user",
|
72 |
-
"content": original_text
|
73 |
-
}
|
74 |
],
|
75 |
temperature=0.3,
|
76 |
max_tokens=2000
|
77 |
)
|
78 |
translated_text = response.choices[0].message.content.strip()
|
79 |
-
print(f"✅ 번역 완료: {translated_text[:50]}...")
|
80 |
-
|
81 |
-
# 3. TTS로 번역된 텍스트를 음성으로 변환
|
82 |
-
print("3️⃣ 음성 합성 시작...")
|
83 |
-
|
84 |
-
# 언어별 음성 선택
|
85 |
-
voice_map = {
|
86 |
-
"Korean": "nova",
|
87 |
-
"English": "alloy",
|
88 |
-
"Japanese": "nova",
|
89 |
-
"Chinese": "nova",
|
90 |
-
"Spanish": "nova",
|
91 |
-
"French": "nova"
|
92 |
-
}
|
93 |
-
voice = voice_map.get(target_lang, "alloy")
|
94 |
|
|
|
|
|
95 |
tts_response = client.audio.speech.create(
|
96 |
model="tts-1",
|
97 |
-
voice=
|
98 |
-
input=translated_text[:4096]
|
99 |
)
|
|
|
|
|
|
|
|
|
|
|
100 |
|
101 |
-
# 임시 파일로 저장
|
102 |
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
103 |
-
tmp_file.write(tts_response.content)
|
104 |
-
output_file = tmp_file.name
|
105 |
-
|
106 |
-
print("✅ 모든 처리 완료!")
|
107 |
-
return original_text, translated_text, output_file
|
108 |
-
|
109 |
-
except openai.APIError as e:
|
110 |
-
error_msg = f"❌ OpenAI API 오류: {str(e)}"
|
111 |
-
print(error_msg)
|
112 |
-
return error_msg, "", None
|
113 |
-
except openai.AuthenticationError:
|
114 |
-
error_msg = "❌ API 키가 올바르지 않습니다. .env 파일을 확인하세요."
|
115 |
-
print(error_msg)
|
116 |
-
return error_msg, "", None
|
117 |
-
except openai.RateLimitError:
|
118 |
-
error_msg = "❌ API 사용 한도를 초과했습니다. 잠시 후 다시 시도하세요."
|
119 |
-
print(error_msg)
|
120 |
-
return error_msg, "", None
|
121 |
except Exception as e:
|
122 |
-
|
123 |
-
print(error_msg)
|
124 |
-
import traceback
|
125 |
-
traceback.print_exc()
|
126 |
-
return error_msg, "", None
|
127 |
-
|
128 |
-
|
129 |
-
# Gradio 인터페이스
|
130 |
-
with gr.Blocks(title="음성 번역기", theme=gr.themes.Soft()) as app:
|
131 |
-
gr.Markdown(
|
132 |
-
"""
|
133 |
-
# 🎙️ AI 음성 번역기
|
134 |
-
음성을 녹음하거나 업로드하면 자동으로 번역합니다.
|
135 |
-
|
136 |
-
**지원 형식**: MP3, WAV, M4A, WEBM (최대 25MB)
|
137 |
-
"""
|
138 |
-
)
|
139 |
-
|
140 |
-
# API 키 상태 표시
|
141 |
-
if api_key:
|
142 |
-
gr.Markdown(f"✅ API 연결 상태: 정상 (키: {api_key[:10]}...)")
|
143 |
-
else:
|
144 |
-
gr.Markdown("❌ API 연결 상태: API 키를 설정하세요")
|
145 |
-
|
146 |
-
with gr.Row():
|
147 |
-
source_lang = gr.Dropdown(
|
148 |
-
["Korean", "English", "Japanese", "Chinese", "Spanish", "French"],
|
149 |
-
value="Korean",
|
150 |
-
label="입력 언어"
|
151 |
-
)
|
152 |
-
target_lang = gr.Dropdown(
|
153 |
-
["Korean", "English", "Japanese", "Chinese", "Spanish", "French"],
|
154 |
-
value="English",
|
155 |
-
label="출력 언어"
|
156 |
-
)
|
157 |
|
158 |
-
audio_input = gr.Audio(
|
159 |
-
sources=["microphone", "upload"],
|
160 |
-
type="filepath",
|
161 |
-
label="음성 입력 (녹음 또는 파일 업로드)"
|
162 |
-
)
|
163 |
|
164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
)
|
|
|
|
|
177 |
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
|
|
|
202 |
if __name__ == "__main__":
|
203 |
print("🚀 서버 시작 중...")
|
204 |
-
app.launch(
|
205 |
-
server_name="0.0.0.0",
|
206 |
-
server_port=7860,
|
207 |
-
share=False, # 로컬에서만 실행
|
208 |
-
debug=True # 디버그 모드 활성화
|
209 |
-
)
|
|
|
4 |
import tempfile
|
5 |
from dotenv import load_dotenv
|
6 |
|
7 |
+
# ===== 공통 초기화 =====
|
8 |
load_dotenv()
|
|
|
|
|
9 |
api_key = os.getenv("OPENAI_API_KEY")
|
10 |
if not api_key:
|
11 |
print("⚠️ OPENAI_API_KEY를 .env 파일에 설정하세요!")
|
|
|
12 |
else:
|
13 |
print(f"✅ API Key 로드됨: {api_key[:10]}...")
|
14 |
|
|
|
18 |
print(f"❌ OpenAI 클라이언트 초기화 실패: {e}")
|
19 |
client = None
|
20 |
|
21 |
+
# ----------------------------------------------------------
|
22 |
+
# (1) 기존: 음성(STT) → 번역 → 음성(TTS)
|
23 |
+
# ----------------------------------------------------------
|
24 |
def translate_audio(audio_file, source_lang, target_lang):
|
|
|
|
|
|
|
25 |
if not audio_file:
|
26 |
return "⚠️ 오디오 파일을 업로드하거나 녹음하세요.", "", None
|
27 |
+
if not api_key or not client:
|
28 |
+
return "❌ API 초기화 오류", "", None
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
if source_lang == target_lang:
|
30 |
return "⚠️ 입력 언어와 출력 언어가 같습니다.", "", None
|
31 |
|
32 |
try:
|
|
|
|
|
|
|
|
|
|
|
33 |
with open(audio_file, "rb") as f:
|
34 |
transcript = client.audio.transcriptions.create(
|
35 |
model="whisper-1",
|
36 |
file=f,
|
37 |
language=source_lang[:2].lower() if source_lang != "Chinese" else "zh"
|
38 |
)
|
39 |
+
original_text = transcript.text.strip()
|
40 |
+
if not original_text:
|
41 |
+
return "⚠️ 음성이 인식되지 않았습니다.", "", None
|
42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
response = client.chat.completions.create(
|
44 |
+
model="gpt-3.5-turbo",
|
45 |
messages=[
|
46 |
+
{"role": "system",
|
47 |
+
"content": f"You are a professional translator. Translate the following {source_lang} text to {target_lang}. "
|
48 |
+
f"Only provide the translation without any explanation or additional text."},
|
49 |
+
{"role": "user", "content": original_text}
|
|
|
|
|
|
|
|
|
50 |
],
|
51 |
temperature=0.3,
|
52 |
max_tokens=2000
|
53 |
)
|
54 |
translated_text = response.choices[0].message.content.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
+
voice_map = {"Korean": "nova", "English": "alloy", "Japanese": "nova",
|
57 |
+
"Chinese": "nova", "Spanish": "nova", "French": "nova"}
|
58 |
tts_response = client.audio.speech.create(
|
59 |
model="tts-1",
|
60 |
+
voice=voice_map.get(target_lang, "alloy"),
|
61 |
+
input=translated_text[:4096]
|
62 |
)
|
63 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
|
64 |
+
tmp.write(tts_response.content)
|
65 |
+
output_audio = tmp.name
|
66 |
+
|
67 |
+
return original_text, translated_text, output_audio
|
68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
except Exception as e:
|
70 |
+
return f"❌ 오류: {type(e).__name__}: {str(e)}", "", None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
|
|
|
|
|
|
|
|
|
|
|
72 |
|
73 |
+
# ----------------------------------------------------------
|
74 |
+
# (2) 신규 탭: PDF / 이미지 → 번역 텍스트
|
75 |
+
# ----------------------------------------------------------
|
76 |
+
def translate_document(file_obj, source_lang, target_lang):
|
77 |
+
if not file_obj:
|
78 |
+
return "⚠️ 파일을 업로드하세요.", ""
|
79 |
+
if not api_key or not client:
|
80 |
+
return "❌ API 초기화 오류", ""
|
81 |
+
if source_lang == target_lang:
|
82 |
+
return "⚠️ 입력 언어와 출력 언어가 같습니다.", ""
|
83 |
|
84 |
+
ext = os.path.splitext(file_obj.name)[1].lower()
|
85 |
+
try:
|
86 |
+
# --- 원본 텍스트 추출 ---
|
87 |
+
if ext == ".pdf":
|
88 |
+
import pdfplumber
|
89 |
+
text_chunks = []
|
90 |
+
with pdfplumber.open(file_obj.name) as pdf:
|
91 |
+
for page in pdf.pages[:5]: # 데모: 앞 5쪽만
|
92 |
+
text_chunks.append(page.extract_text() or "")
|
93 |
+
original_text = "\n".join(text_chunks).strip()
|
94 |
+
|
95 |
+
elif ext in [".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff"]:
|
96 |
+
from PIL import Image
|
97 |
+
import pytesseract
|
98 |
+
original_text = pytesseract.image_to_string(Image.open(file_obj.name))
|
99 |
+
|
100 |
+
else:
|
101 |
+
return "⚠️ 지원하지 않는 형식입니다.", ""
|
102 |
+
|
103 |
+
if not original_text:
|
104 |
+
return "⚠️ 텍스트를 추출할 수 없습니다.", ""
|
105 |
+
|
106 |
+
# --- 번역 ---
|
107 |
+
response = client.chat.completions.create(
|
108 |
+
model="gpt-3.5-turbo",
|
109 |
+
messages=[
|
110 |
+
{"role": "system",
|
111 |
+
"content": f"You are a professional translator. Translate the following {source_lang} text to {target_lang}. "
|
112 |
+
f"Only provide the translation without any explanation or additional text."},
|
113 |
+
{"role": "user", "content": original_text}
|
114 |
+
],
|
115 |
+
temperature=0.3,
|
116 |
+
max_tokens=4096
|
117 |
)
|
118 |
+
translated_text = response.choices[0].message.content.strip()
|
119 |
+
return original_text, translated_text
|
120 |
|
121 |
+
except Exception as e:
|
122 |
+
return f"❌ 오류: {type(e).__name__}: {str(e)}", ""
|
123 |
+
|
124 |
+
|
125 |
+
# ==========================================================
|
126 |
+
# Gradio UI (Tabs 구조)
|
127 |
+
# ==========================================================
|
128 |
+
with gr.Blocks(title="SMARTok Demo", theme=gr.themes.Soft()) as app:
|
129 |
+
with gr.Tabs():
|
130 |
+
# ----- ① 기존 음성 번역 -----
|
131 |
+
with gr.TabItem("🎙️ 음성 번역"):
|
132 |
+
gr.Markdown("""
|
133 |
+
# 🎙️ AI 음성 번역기
|
134 |
+
마이크로 녹음하거나 오디오 파일을 업로드하면 **실시간 자막 + 번역 + 음성합성**까지 한 번에!
|
135 |
+
""")
|
136 |
+
|
137 |
+
with gr.Row():
|
138 |
+
src_lang_a = gr.Dropdown(
|
139 |
+
["Korean", "English", "Japanese", "Chinese", "Spanish", "French"],
|
140 |
+
value="Korean", label="입력 언어"
|
141 |
+
)
|
142 |
+
tgt_lang_a = gr.Dropdown(
|
143 |
+
["Korean", "English", "Japanese", "Chinese", "Spanish", "French"],
|
144 |
+
value="English", label="출력 언어"
|
145 |
+
)
|
146 |
+
|
147 |
+
audio_in = gr.Audio(
|
148 |
+
sources=["microphone", "upload"],
|
149 |
+
type="filepath",
|
150 |
+
label="음성 입력 (녹음 또는 파일 업로드)"
|
151 |
+
)
|
152 |
+
btn_audio = gr.Button("🔄 번역하기")
|
153 |
+
|
154 |
+
with gr.Row():
|
155 |
+
stt_text = gr.Textbox(label="📝 원본 텍스트", lines=5)
|
156 |
+
tlt_text = gr.Textbox(label="🌐 번역된 텍스트", lines=5)
|
157 |
+
|
158 |
+
audio_out = gr.Audio(label="🔊 번역된 음성", type="filepath", autoplay=True)
|
159 |
+
|
160 |
+
btn_audio.click(
|
161 |
+
translate_audio,
|
162 |
+
inputs=[audio_in, src_lang_a, tgt_lang_a],
|
163 |
+
outputs=[stt_text, tlt_text, audio_out]
|
164 |
+
)
|
165 |
+
|
166 |
+
# ----- ② 신규 자료 번역 -----
|
167 |
+
with gr.TabItem("📄 자료 번역"):
|
168 |
+
gr.Markdown("""
|
169 |
+
# 📄 PDF / 이미지 번역 데모
|
170 |
+
교육자료·발표자료 등 **PDF 최대 5쪽** 또는 이미지 1장을 업로드하면 텍스트 추출 후 번역해줍니다.
|
171 |
+
""")
|
172 |
+
|
173 |
+
with gr.Row():
|
174 |
+
src_lang_d = gr.Dropdown(
|
175 |
+
["Korean", "English", "Japanese", "Chinese", "Spanish", "French"],
|
176 |
+
value="Korean", label="입력 언어"
|
177 |
+
)
|
178 |
+
tgt_lang_d = gr.Dropdown(
|
179 |
+
["Korean", "English", "Japanese", "Chinese", "Spanish", "French"],
|
180 |
+
value="English", label="출력 언어"
|
181 |
+
)
|
182 |
+
|
183 |
+
file_in = gr.File(label="PDF / 이미지 업로드")
|
184 |
+
btn_doc = gr.Button("🔄 번역하기")
|
185 |
+
|
186 |
+
original_doc = gr.Textbox(label="📝 추출된 원문", lines=15)
|
187 |
+
translated_doc = gr.Textbox(label="🌐 번역 결과", lines=15)
|
188 |
+
|
189 |
+
btn_doc.click(
|
190 |
+
translate_document,
|
191 |
+
inputs=[file_in, src_lang_d, tgt_lang_d],
|
192 |
+
outputs=[original_doc, translated_doc]
|
193 |
+
)
|
194 |
|
195 |
+
# ==========================================================
|
196 |
if __name__ == "__main__":
|
197 |
print("🚀 서버 시작 중...")
|
198 |
+
app.launch(server_name="0.0.0.0", server_port=7860, share=False, debug=True)
|
|
|
|
|
|
|
|
|
|