import os import json import tempfile import gradio as gr import openai from transformers import pipeline # ========================= # πŸ”§ μ„€μ • # ========================= openai.api_key = os.environ.get("OPENAI_API_KEY") # Hugging Face λ²ˆμ—­ νŒŒμ΄ν”„λΌμΈ (μ–‘λ°©ν–₯ 포함) translator_ko_to_en = pipeline("translation", model="Helsinki-NLP/opus-mt-ko-en") translator_ko_to_de = pipeline("translation", model="Helsinki-NLP/opus-mt-ko-de") translator_en_to_ko = pipeline("translation", model="Helsinki-NLP/opus-mt-en-ko") translator_de_to_ko = pipeline("translation", model="Helsinki-NLP/opus-mt-de-ko") # ========================= # 🧠 μœ ν‹Έ: OpenAI 호좜 # ========================= def gpt(messages, temperature=0.7, model="gpt-4"): """단일 ChatCompletion 래퍼""" resp = openai.ChatCompletion.create( model=model, messages=messages, temperature=temperature ) return resp.choices[0].message["content"].strip() # ========================= # 🧩 핡심 둜직 # ========================= def make_variants(input_text, source_lang, target_lang, direct_translation): """ 직역을 κΈ°μ€€μœΌλ‘œ 원어민이 μžμ—°μŠ€λŸ½κ²Œ μ“°λŠ” λ³€ν˜• λ²ˆμ—­ 2개λ₯Ό μΆ”κ°€λ‘œ 생성 (총 3개) """ sys_msg = "You are a bilingual translator who produces concise, natural alternatives." user_msg = f""" [원문] ({source_lang}): {input_text} [직역] ({target_lang}): {direct_translation} μœ„ 직역을 κΈ°μ€€μœΌλ‘œ, {target_lang} 원어민이 μ‹€μ œλ‘œ 많이 μ“°λŠ” μžμ—°μŠ€λŸ¬μš΄ λ³€ν˜• 2κ°€μ§€λ₯Ό λ§Œλ“€μ–΄μ€˜. - λ§₯락: 일상 λŒ€ν™” κΈ°μ€€ - 각 λ³€ν˜•μ€ 1λ¬Έμž₯ - κ³Όμž₯/μŠ¬λž­μ€ κ³Όν•˜μ§€ μ•Šκ²Œ - 좜λ ₯ ν˜•μ‹: 1) λ³€ν˜•A: ... 2) λ³€ν˜•B: ... """ out = gpt([{"role":"system","content":sys_msg},{"role":"user","content":user_msg}], temperature=0.6) # 간단 νŒŒμ‹± variants = [direct_translation] for line in out.splitlines(): line = line.strip() if line.startswith("1)") or line.lower().startswith("λ³€ν˜•a"): variants.append(line.split(":",1)[1].strip() if ":" in line else line) elif line.startswith("2)") or line.lower().startswith("λ³€ν˜•b"): variants.append(line.split(":",1)[1].strip() if ":" in line else line) # fallback return variants[:3] if len(variants)>=3 else (variants + ["", ""])[:3] def back_translate_list(variants, source_lang, target_lang): """각 λ³€ν˜• λ²ˆμ—­μ„ λͺ¨κ΅­μ–΄λ‘œ μ—­λ²ˆμ—­ν•˜μ—¬ 비ꡐ ν…Œμ΄λΈ”μš© 데이터 생성""" back_list = [] for v in variants: if not v: back_list.append("") continue if source_lang == "ν•œκ΅­μ–΄" and target_lang == "μ˜μ–΄": back_ = translator_en_to_ko(v)[0]["translation_text"] elif source_lang == "ν•œκ΅­μ–΄" and target_lang == "독일어": back_ = translator_de_to_ko(v)[0]["translation_text"] else: back_ = "(μ—­λ²ˆμ—­ 미지원)" back_list.append(back_) return back_list def build_explanations(input_text, variants, source_lang, target_lang): """ν‘œν˜„/문법/단어/λ¬Έν™” μ„€λͺ…을 μ„Ήμ…˜λ³„ λ§ˆν¬λ‹€μš΄μœΌλ‘œ 생성""" best = variants[0] if variants else "" sys_msg = "You are a concise yet friendly language tutor who explains in Korean with clear headings and bullet points." user_msg = f""" λ‹€μŒ ν‘œν˜„μ— λŒ€ν•΄ ν•œκ΅­μ–΄λ‘œ μ„€λͺ…ν•΄μ€˜. κ°„κ²°ν•˜μ§€λ§Œ 핡심은 빠짐없이. [원문] ({source_lang}): {input_text} [λŒ€ν‘œ λ²ˆμ—­] ({target_lang}): {best} [λ‹€λ₯Έ λ³€ν˜• λ²ˆμ—­λ“€]: {variants[1:]} μ•„λž˜ μ„Ήμ…˜ 제λͺ©μ„ κ·ΈλŒ€λ‘œ μ‚¬μš©ν•΄: ## ν‘œν˜„ μ„€λͺ… - μ–΄λ–€ 상황/κ΄€κ³„μ—μ„œ μ“°λŠ”μ§€, λ‰˜μ•™μŠ€(격식/친근감) ## 문법 포인트 - 핡심 문법 μš”μ†Œ 2~3개 (쑰사/μ „μΉ˜μ‚¬, μ‹œμ œ, μ–΄μˆœ λ“±) - 간단 예문 각 1개 ## 단어/ν‘œν˜„ μ„€λͺ… - μ–΄λ €μšΈ 수 μžˆλŠ” 단어/ꡬ절 3개: 의미 + 짧은 예문 ## 문화적 차이 - ν•œκ΅­μ–΄μ™€ λŒ€μƒ μ–Έμ–΄ μ‚¬μ΄μ˜ κΈ°λŒ€/예의/κ΄€μŠ΅ 차이 2~3κ°€μ§€ """ return gpt([{"role":"system","content":sys_msg},{"role":"user","content":user_msg}], temperature=0.5) def build_pronunciation(input_text, variants, source_lang, target_lang): """발음 κ°€μ΄λ“œ(ν…μŠ€νŠΈ). IPA/κ°•μ„Έ/리듬 포인트""" best = variants[0] if variants else "" sys_msg = "You provide compact pronunciation guides (IPA-ish, stress, rhythm)." user_msg = f""" λ‹€μŒ 두 λ¬Έμž₯에 λŒ€ν•œ 발음 κ°€μ΄λ“œλ₯Ό ν•œκ΅­μ–΄λ‘œ κ°„λ‹¨νžˆ μ μ–΄μ€˜. [원문] ({source_lang}): {input_text} [λŒ€ν‘œ λ²ˆμ—­] ({target_lang}): {best} ν˜•μ‹: - 원문: (κ°€λŠ₯ν•˜λ©΄ 간단 IPA/ν•œκΈ€ν‘œκΈ°) + κ°•μ„Έ/리듬 포인트 - λ²ˆμ—­: (IPA/κ°•μ„Έ) + μžμ—°μŠ€λŸ¬μš΄ μ–΅μ–‘ 팁 """ return gpt([{"role":"system","content":sys_msg},{"role":"user","content":user_msg}], temperature=0.4) def build_roleplay(input_text, variants, target_lang): """격식/친근 2κ°€μ§€ ν†€μ˜ 짧은 Role Play""" best = variants[0] if variants else "" sys_msg = "You create short, practical role-play dialogues for language learners." user_msg = f""" λ‹€μŒ ν‘œν˜„μ„ ν™œμš©ν•œ 짧은 λŒ€ν™” 2κ°€μ§€λ₯Ό λ§Œλ“€μ–΄μ€˜. 각 λŒ€ν™”λŠ” 6~8 ν„΄. - 톀1: 격식(직μž₯/곡적인 상황) - 톀2: 친근(친ꡬ/κ°€λ²Όμš΄ 상황) - λŒ€μƒ μ–Έμ–΄: {target_lang} - λŒ€ν™” ν›„ ν•œκ΅­μ–΄ μš”μ•½ ν•œ 쀄 ν‘œν˜„: "{best}" """ return gpt([{"role":"system","content":sys_msg},{"role":"user","content":user_msg}], temperature=0.7) def suggest_resources(input_text, target_lang): """ν•™μŠ΅ 자료 μΆ”μ²œ: 유튜브/검색 ν‚€μ›Œλ“œ""" sys_msg = "You suggest search keywords for YouTube and web to find usage contexts." user_msg = f""" μ•„λž˜ ν‘œν˜„μ„ μ‹€μ œ λ§₯λ½μ—μ„œ λ³Ό 수 μžˆλŠ” 자료λ₯Ό μ°ΎκΈ° μœ„ν•œ 검색 ν‚€μ›Œλ“œλ₯Ό μ œμ•ˆν•΄μ€˜. - μ–Έμ–΄: {target_lang} - 5~7개 ν‚€μ›Œλ“œ, λ”°μ˜΄ν‘œ 없이, ν•œ 쀄에 ν•˜λ‚˜ ν‘œν˜„: {input_text} """ out = gpt([{"role":"system","content":sys_msg},{"role":"user","content":user_msg}], temperature=0.5) # 클릭 κ°€λŠ₯ν•œ 검색 URL λ¬Έμžμ—΄ 생성 items = [s.strip("-β€’ ").strip() for s in out.splitlines() if s.strip()] md_lines = [] base = "https://www.youtube.com/results?search_query=" for k in items: url = base + k.replace(" ", "+") md_lines.append(f"- [{k}]({url})") return "\n".join(md_lines) # ========================= # πŸš€ 메인 ν•¨μˆ˜ (Gradio에 μ—°κ²°) # ========================= def run_pipeline(input_text, source_lang, target_lang, favorites_state): if not input_text.strip(): return ( "", [], None, "", "", "", favorites_state, gr.update(visible=False), None ) # 1) κΈ°λ³Έ λ²ˆμ—­ if source_lang == "ν•œκ΅­μ–΄" and target_lang == "μ˜μ–΄": direct = translator_ko_to_en(input_text)[0]['translation_text'] elif source_lang == "ν•œκ΅­μ–΄" and target_lang == "독일어": direct = translator_ko_to_de(input_text)[0]['translation_text'] else: return ( input_text, ["(μ§€μ›λ˜μ§€ μ•ŠλŠ” μ–Έμ–΄μŒμž…λ‹ˆλ‹€.)"], None, "(μ§€μ›λ˜μ§€ μ•ŠλŠ” μ–Έμ–΄μŒ)", "", "", favorites_state, gr.update(visible=False), None ) # 2) λ³€ν˜• 3κ°€μ§€ variants = make_variants(input_text, source_lang, target_lang, direct) # 3) μ—­λ²ˆμ—­ ν…Œμ΄λΈ” 데이터 backs = back_translate_list(variants, source_lang, target_lang) back_table = { "λ²ˆμ—­(Variant)": variants, "μ—­λ²ˆμ—­(λͺ¨κ΅­μ–΄)": backs } # 4) μ„€λͺ… μ„Ήμ…˜ explanations_md = build_explanations(input_text, variants, source_lang, target_lang) # 5) 발음 κ°€μ΄λ“œ pron_md = build_pronunciation(input_text, variants, source_lang, target_lang) # 6) Role Play roleplay_md = build_roleplay(input_text, variants, target_lang) # 7) 자료 μΆ”μ²œ resources_md = suggest_resources(input_text, target_lang) # 8) 즐겨찾기 μΉ΄λ“œ(ν˜„μž¬ κ²°κ³Ό) current_card = { "원문": input_text, "λŒ€ν‘œ λ²ˆμ—­": variants[0], "λ‹€λ₯Έ λ³€ν˜•": variants[1:], "μ—­λ²ˆμ—­": backs, "μ„€λͺ…": explanations_md, "발음": pron_md, "role_play": roleplay_md } # λ‹€μš΄λ‘œλ“œ νŒŒμΌμ€ Save λ²„νŠΌ 클릭 μ‹œ μƒμ„±ν•˜λ„λ‘ ν•˜λ―€λ‘œ μ—¬κΈ°μ„œλŠ” None return ( input_text, variants, back_table, explanations_md, pron_md, roleplay_md, favorites_state, gr.update(visible=True), resources_md ) def save_to_favorites(input_text, variants, backs, explanations_md, pron_md, roleplay_md, favorites_state): if favorites_state is None: favorites_state = [] entry = { "원문": input_text, "λ³€ν˜•λ²ˆμ—­": variants, "μ—­λ²ˆμ—­": backs, "μ„€λͺ…": explanations_md, "발음": pron_md, "role_play": roleplay_md } favorites_state.append(entry) return favorites_state, f"μ €μž₯ μ™„λ£Œ! (총 {len(favorites_state)}건)" def export_favorites(favorites_state): if not favorites_state: return None fd, path = tempfile.mkstemp(suffix=".json") with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(favorites_state, f, ensure_ascii=False, indent=2) return path def load_sample(sample_text): return gr.update(value=sample_text) # ========================= # πŸŽ›οΈ Gradio UI # ========================= with gr.Blocks(title="🌐 λ¬Έν™” κ°„ ν‘œν˜„ 비ꡐ + 문법 & μ–΄νœ˜ λ„μš°λ―Έ (ν™•μž₯판)") as demo: gr.Markdown("## 🌐 λ¬Έν™” κ°„ ν‘œν˜„ 비ꡐ + 문법 & μ–΄νœ˜ λ„μš°λ―Έ\nμž…λ ₯ν•œ ν‘œν˜„μ„ 기반으둜 **μžμ—°μŠ€λŸ¬μš΄ λ²ˆμ—­ 3κ°€μ§€, μ—­λ²ˆμ—­ 비ꡐ, 문법/λ¬Έν™” μ„€λͺ…, Role Play, 발음 κ°€μ΄λ“œ**κΉŒμ§€ ν•œ λ²ˆμ—!") with gr.Row(): with gr.Column(scale=5): input_text = gr.Textbox(label="비ꡐ할 λ¬Έμž₯ μž…λ ₯", placeholder="예: κ³ μƒν–ˆμ–΄!", lines=2) with gr.Row(): src_dd = gr.Dropdown(["ν•œκ΅­μ–΄"], label="λͺ¨κ΅­μ–΄ 선택", value="ν•œκ΅­μ–΄") tgt_dd = gr.Dropdown(["μ˜μ–΄", "독일어"], label="비ꡐ μ–Έμ–΄ 선택", value="μ˜μ–΄") with gr.Accordion("μƒ˜ν”Œ λ¬Έμž₯ 뢈러였기", open=False): gr.Markdown("- μƒν™©λ³„λ‘œ λ°”λ‘œ ν…ŒμŠ€νŠΈν•΄λ³΄μ„Έμš”.") with gr.Row(): b1 = gr.Button("친ꡬ μœ„λ‘œ: κ³ μƒν–ˆμ–΄!") b2 = gr.Button("격렀: 수고 λ§Žμ•˜μ–΄, 정말 κ³ λ§ˆμ›Œ.") b3 = gr.Button("업무: 였늘 일정 확인 λΆ€νƒλ“œλ¦½λ‹ˆλ‹€.") submit = gr.Button("πŸš€ Submit", variant="primary") with gr.Column(scale=7): tabs = gr.Tabs() with tabs: with gr.Tab("κ²°κ³Ό μš”μ•½"): orig_out = gr.Textbox(label="원문", interactive=False) variants_out = gr.HighlightedText( label="λ²ˆμ—­ 3κ°€μ§€ (직역 + μžμ—°μŠ€λŸ¬μš΄ λ³€ν˜•)", combine_adjacent=True ) resources_md = gr.Markdown(visible=False) with gr.Tab("μ—­λ°©ν–₯ 비ꡐ"): back_table = gr.Dataframe(headers=["λ²ˆμ—­(Variant)", "μ—­λ²ˆμ—­(λͺ¨κ΅­μ–΄)"], interactive=False) with gr.Tab("μ„€λͺ…"): explain_out = gr.Markdown() with gr.Tab("발음 κ°€μ΄λ“œ"): pron_out = gr.Markdown() with gr.Tab("Role Play"): role_out = gr.Markdown() with gr.Tab("즐겨찾기"): fav_state = gr.State([]) save_btn = gr.Button("⭐ ν˜„μž¬ κ²°κ³Ό μ €μž₯") save_status = gr.Markdown("") export_btn = gr.Button("⬇️ 즐겨찾기 JSON 내보내기") export_file = gr.File(label="λ‹€μš΄λ‘œλ“œ 파일") # ---------- 이벀트 바인딩 ---------- submit.click( fn=run_pipeline, inputs=[input_text, src_dd, tgt_dd, fav_state], outputs=[orig_out, variants_out, back_table, explain_out, pron_out, role_out, fav_state, resources_md, resources_md], ) # μƒ˜ν”Œ λ²„νŠΌ b1.click(fn=load_sample, inputs=None, outputs=input_text, _js=None, kwargs={"sample_text":"κ³ μƒν–ˆμ–΄!"}) b2.click(fn=load_sample, inputs=None, outputs=input_text, kwargs={"sample_text":"수고 λ§Žμ•˜μ–΄, 정말 κ³ λ§ˆμ›Œ."}) b3.click(fn=load_sample, inputs=None, outputs=input_text, kwargs={"sample_text":"였늘 일정 확인 λΆ€νƒλ“œλ¦½λ‹ˆλ‹€."}) # 즐겨찾기 μ €μž₯ save_btn.click( fn=save_to_favorites, inputs=[orig_out, variants_out, back_table, explain_out, pron_out, role_out, fav_state], outputs=[fav_state, save_status] ) # 내보내기 export_btn.click(fn=export_favorites, inputs=[fav_state], outputs=[export_file]) demo.launch()