import os import google.generativeai as genai import anthropic import os import openai from openai import OpenAI import pandas as pd import numpy as numpy import json import re import time import gradio as gr import io import google.generativeai as genai from typing import List, Dict import unicodedata, importlib from typing import Tuple import tempfile from huggingface_hub import hf_hub_download from huggingface_hub import list_repo_files, HfApi import os from pykakasi import kakasi # =================================== # 공용: API Key 선택 로직 # =================================== def _resolve_api_key(model_choice: str, user_key: str | None) -> str | None: """ UI에서 받은 user_key만 사용. 비어있으면 None 반환. """ if user_key and str(user_key).strip(): return user_key.strip() # ❌ 환경변수 대체 로직 완전 제거 return None # =================================== # 모델 통합 호출 함수 # =================================== def generate_with_model(model_choice: str, api_key: str | None, prompt: str, temperature: float = 0.3) -> str: name = (model_choice or "").lower() key = _resolve_api_key(model_choice, api_key) # --- OpenAI (gpt-*) --- if name.startswith("gpt"): if not key: raise RuntimeError("🔑 OPENAI API KEY를 입력해주세요.") # gpt-5-mini는 temperature=1 고정 if "gpt-5-mini" in model_choice: temperature = 1 # 허용되지 않는 값으로 들어올 경우 자동 보정 client = OpenAI(api_key=key) resp = client.chat.completions.create( model=model_choice, messages=[{"role": "user", "content": prompt}], temperature=temperature, ) return resp.choices[0].message.content #-gemini # --- Gemini (gemini*) --- if name.startswith("gemini"): if not key: raise RuntimeError("🔑 GEMINI API KEY를 입력해주세요.") import google.generativeai as genai # Gradio UI에서 입력받은 키를 SDK에 설정 genai.configure(api_key=key) # 기본 모델 자동 라우팅 _model = "gemini-2.5-flash" if name == "gemini" else model_choice resp = genai.GenerativeModel(_model).generate_content( prompt, generation_config={"temperature": temperature} ) return getattr(resp, "text", "") or "" # --- Claude (claude*) --- if name.startswith("claude"): if not key: raise RuntimeError("🔑 CLAUDE API KEY를 입력해주세요.") client = anthropic.Anthropic(api_key=key) _model = "claude-4-sonnet-20250514" if name == "claude" else model_choice # SDK 호환 이슈 대비: 두 방식 시도 try: msg = client.messages.create( model=_model, max_tokens=800, temperature=temperature, messages=[{"role": "user", "content": prompt}], ) return "".join(getattr(b, "text", "") for b in getattr(msg, "content", [])) except Exception: msg = client.messages.create( model=_model, max_tokens=800, temperature=temperature, messages=[{"role": "user", "content": [{"type": "text", "text": prompt}]}], ) return "".join(getattr(b, "text", "") for b in getattr(msg, "content", [])) raise ValueError("지원하지 않는 모델 선택입니다. (gpt / gemini / claude)") kks = kakasi() def katakana_to_hiragana(text): """가타카나 → 히라가나""" if not text or not text.strip(): return "" try: result = kks.convert(text) hiragana = ''.join([item['hira'] for item in result]) return hiragana except: return "" def add_hiragana_synonyms(jp_main, jp_syn): """가타카나 동의어에 히라가나 버전 자동 추가""" synonyms = [] # 기존 가타카나 동의어 추가 if jp_syn: synonyms.extend([x.strip() for x in jp_syn.split("|") if x.strip()]) # 대표키워드의 히라가나 버전 추가 if jp_main: hiragana_main = katakana_to_hiragana(jp_main) if hiragana_main and hiragana_main != jp_main: synonyms.append(hiragana_main) # 기존 동의어들의 히라가나 버전 추가 if jp_syn: for kata in jp_syn.split("|"): kata = kata.strip() if kata: hira = katakana_to_hiragana(kata) if hira and hira != kata and hira not in synonyms: synonyms.append(hira) # 중복 제거 synonyms = list(dict.fromkeys(synonyms)) return "|".join(synonyms) # ------------------------------- # JSONL 데이터 로드 (경로는 필요에 맞게 수정) # ------------------------------- HF_TOKEN = os.getenv("token") # 정확히 secret 이름과 일치해야 함 jp_jsonl_path = hf_hub_download( repo_id="DataOperation/MUSINSA_Gen_Synonym_Dictionary_Dataset", filename="japan_musinsa_data.jsonl", repo_type="space", # ✅ 여기 token=HF_TOKEN ) kor_jsonl_path = hf_hub_download( repo_id="DataOperation/MUSINSA_Gen_Synonym_Dictionary_Dataset", filename="korean_musinsa_data.jsonl", repo_type="space", # ✅ 여기 token=HF_TOKEN ) def load_jsonl(path): data = [] if os.path.exists(path): with open(path, "r", encoding="utf-8") as f: for line in f: if line.strip(): try: data.append(json.loads(line)) except: continue return data jp_data = load_jsonl(jp_jsonl_path) kor_data = load_jsonl(kor_jsonl_path) # ------------------------------- # 유틸 함수: 문자열 정규화 # ------------------------------- def normalize_word(x): if pd.isna(x) or x is None: return "" return str(x).strip().replace("\u3000", " ").replace("\n", " ").lower() # ------------------------------- # JP 데이터에서 키워드 검색 (KOR 기반 기존 방식) - 개선 버전 # ------------------------------- def get_jp_synonyms_from_json(category, word): cat_norm = normalize_word(category) word_norm = normalize_word(word) for item in jp_data: key_norm = normalize_word(item.get("국문 대표검색어")) cat_item = normalize_word(item.get("분류")) # ✅ 분류 + 국문 대표검색어 매칭 if word_norm == key_norm and cat_norm == cat_item: jp_keywords, eng_keywords = [], [] # ✅ 일문 대표검색어 (normalize 하지 않고 원본 사용) jp_col = item.get("일문 대표検索어", "") if jp_col and str(jp_col).strip(): jp_keywords.append(str(jp_col).strip()) # ✅ Jap synonym (normalize 하지 않고 원본 사용) jp_syn = item.get("Jap synonym", "") if jp_syn and str(jp_syn).strip(): jp_keywords.extend([k.strip() for k in str(jp_syn).split("|") if k.strip()]) # ✅ Eng synonym (normalize 하지 않고 원본 사용) eng_syn = item.get("Eng synonym", "") if eng_syn and str(eng_syn).strip(): eng_keywords.extend([k.strip() for k in str(eng_syn).split("|") if k.strip()]) # 중복 제거 jp_keywords = list(dict.fromkeys(jp_keywords)) eng_keywords = list(dict.fromkeys(eng_keywords)) # 반환값 구성 jp_main = jp_keywords[0] if jp_keywords else "" jp_syn_str = "|".join(jp_keywords[1:]) if len(jp_keywords) > 1 else "" eng_str = "|".join(eng_keywords) if eng_keywords else "" return jp_main, jp_syn_str, eng_str # ❌ 못 찾으면 None 반환 return "", "", "" # ------------------------------- # KOR 데이터에서 검색 - 개선 버전 # ------------------------------- def get_kor_synonyms_from_json(category, word): cat_norm = normalize_word(category) word_norm = normalize_word(word) for item in kor_data: key_norm = normalize_word(item.get("국문 대표검색어")) cat_item = normalize_word(item.get("분류")) # ✅ 분류 + 국문 대표검색어 매칭 if word_norm == key_norm and cat_norm == cat_item: # ✅ 원본 데이터 사용 (normalize 제거) kor_syn = item.get("Kor synonym", "") eng_syn = item.get("Eng synonym", "") # 빈 문자열을 None으로 처리하지 않고 그대로 반환 kor_syn = kor_syn.strip() if kor_syn else "" eng_syn = eng_syn.strip() if eng_syn else "" return kor_syn, eng_syn # ❌ 못 찾으면 None 반환 return "", "" # ------------------------------- # GPT 기반 일본어 동의어 조회 (공식 영문명 지원) # ------------------------------- # 일본어 공통 규칙 jp_common_rules = """ <동의어 사전 작업 시 포함 권장 패턴 유형(모든 키워드에 적용되는 사례는 아님을 유의)> 1. 일본어의 모든 단어는 한국어와 달리 단어의 음절을 박자개념으로 이해함(장,단음이 존재하는 이유) 2. 실제 발음을 기반으로 동의어 작성 3. 대표키워드는 한자표기를 제외하고 대부분 가타가나로 표기하며, 대표키워드 및 동의어 가타가나 표기를 히라가나 표기로도 동일하게 넣는다. 4. L,R 은 동일하게 ラ행으로 발음 ラ、リ、ル、レ、ロ 5. V는 ヴ에 코가키모지를 넣어 ヴァ、ヴィ、ヴ、ヴェ、ヴォ로 표기하거나 バ행으로 표기 6. TH는 サ、ザ행으로 대체 7. 발음상 모음이 길게 발음되는 경우 장음 부호를 사용 예) ミッドナイトムーブ(미드나잇 무-브), シューマーカー(슈-마-커-) 🔴 장음(ー) 사용 규칙 (매우 중요!): 7-1. 영문 -y로 끝나는 단어는 마지막에 장음 예) curry → カリー, accessory → アクセサリー, energy → エネルギー 7-2. 모음이 겹치는 경우 (이중모음) 예) ice cream → アイスクリーム, coat → コート ⚠️ 예외: necktie → ネクタイ (장음 없음) 7-3. 모음(a,e,o) + r 조합 예) apartment → アパート, dessert → デザート, soccer → サッカー 7-4. 일본어 발음 규칙 - あ단 + あ → 장음 (カア → カー) - い단 + い → 장음 (キイ → キー) - う단 + う → 장음 (クウ → クー) - え단 + え/い → 장음 (ケイ → ケー, セエ → セー) - お단 + お/う → 장음 (コウ → コー, ソオ → ソー) - 요음 + う → 장음 (キョウ → キョー) 7-5. -ong 발음 → グ로 끝남 예) chungmong → チュンモング 7-6. di 발음 → ディ (또는 ヂ) 7-7. K, T 등 알파벳 브랜드 예) K → ケイ / ケー (둘 다 사용 가능) 8. 빈번하게 사이에 촉음이 오는 발음 패턴 ック、ッグ、ット、ッド 예) エックス(엑스-엨쿠스)、ブラック(블랙-브락쿠)、クレット(클렛-크렛토) 9. 알파벳 브랜드일경우 공식 알파벳 표기법을 따름 예)エスイーエスティー(에스이에스티), エルエムシー(엘엠씨) 10. 영문표기시 모음이 두번 오거나 장음패턴에 부합한 경우 장음ー표기 가능(あ행+あ、い행+い、う행+う、え행+え、え행+い、お행+お、お행+う、요음(ゃ、ゅ、ょ)う)-> 해당 표기를 억지로 만들어 내지 않게 주의 예)킵(keep->キープ) 메이드(メイド->メード), (EEASEE->イーズ) (MOONSUN->ムーンサン) 등 11. di,de 발음은 ディ、デ、ジ、ジェ로 치환할 수 있고, te,ti 발음은 テ、ティ、チェ、チ로 치환할 수 있는 경우가 많음(70%의 확률, 실제 발음에 따라 달라짐) 12. S,J,Z발음은 サ행, ザ행과 ジャ、ジ、ジュ、ジェ、ジョ를 함께 씀 13. 브랜드명에 자주 쓰이는 유사어 패턴 예) スタジオ(스튜디오)-> 유사어 패턴 ストゥディオ, スタディオ,ストゥジオ アーカイブ(아카이브)->アーカイヴ / ウーマン(우먼)->ウィメン /'-ay'로 끝나는 단어일 때, 한국어 발음 '-ㅔ이'일 때 ステー(stay)-> ステイ, デー(day)->デイ 14. 특수문자, 숫자,알파벳,한자는 발음표기도 병행한다 예) &->アンド, 77->セブンティーセブン、ナナナナ, 無印良品->ムジルシリヨウヒン, lllデザインスタジオ(엘엘엘 스튜디오) 15. 영문병기시 소문자로 작업 예)보다나랩->ボダナlab 16. 영문 유사어 기입시에도 소문자로 작업하며, 띄어쓰기 문자가 있을 경우 해당 문자의 붙여쓰기도 반드시 포함 예) addidas sports, addidassports 17. 한글자, 숫자 브랜드일 경우 대표키워드,대표키워드의 히라가나 변환만 입력하고 D열에 한글자, 숫자브랜드 기입 18. 우먼, 맨, 레이디스, 옴므, 키즈, 스포츠, 퍼퓸, 아이웨어, 언더웨어 와 같이 하위 브랜드 라인은 D열에 카테고리 기입하고, 해당 카테고리명을 떼지 않고 한번에 작업 예) 크리스씨 옴므: 크리스시 옴므O, 크리스씨X 19.대표 키워드 내에 촉음, 장음 포함될 경우 제외된 유사어 생성 가능 예) ニッティド(니티드)-> ニティド, ニジュール(니주르)->ニジュル 20.ing로 끝나는 키워드 일경우 イ행+ング로 표기 예) オープニング(오프닝), エージング(에이징) 21. 대표키워드가 영문+일문일 경우 대표키워드는 가타카나(일문) 표기, 동의어에서 영문+일문 살리기 예) dcコミックス(디씨 코믹스) > 대표키워드: ディーシーコミックス, 동의어:dcコミックス 22. 작은 촉음이 연속으로 있는 단어의 경우 두개 중 하나를 소거해서 동의어를 만들 수 있다. 예) トリップショップ (트립샵)-> トリップショプ/ ロックフィッシュウェザーウェアー(락피쉬언더웨어) -> ロックフィシュウェザーウェアー <동의어 사전 작업 시 제외해야 할 패턴 유형 정리> 1. 대표키워드를 포함한 모든 유사어는 중복이 없어야 한다. (기존 적재된 모든 키워드를 다 포함하여 중복이 없어야 함) 2. 단어의 어미에는 촉음ッ을 넣지 않는다(한국어에는 받침이라는 개념이 있지만 일본은 없음) 예) エオブッ,ファンショッ,ハイネッ 등 3. 단어 내에 이유없이 반복되는 글자를 넣지 않는다 예) エルムードド(엘무드드),オディエエール(오디에에르),シンジモモル(신지모모루) 4.브랜드명 실제 발음과 너무 동떨어진 발음의 키워드는 넣지 않는다. 예) 아크네스튜디오: 아크네스투디오O, 아즈네스튜디오X,아크네스튜디오스X 5. 절대로 히라가나와 가타가나는 한 키워드에 병기하지 않는다. 예) バイしクルトろフィー 검정:가타가나, 빨강:히라가나 6. 일본어는 띄어쓰기 없으므로 키워드 내에 공백을 넣지 않는다 7. 영문 직역 키워드는 되도록 넣지 않는다. 일본 내에서 실제로 사용중인 표기면 괜찮음. 대표키워드가 한글 음차일 경우에는 괜찮음. 예)パピーエンジェル(퍼피엔젤): 小型犬天使(소형견천사)X サムスン(삼성): サムスン電子(삼성전자)O, 三星電子O 東国製薬(동국제약):ヒガシコクセイヤク(히가시코쿠세이야쿠-한자의 일본발음)トンクック製薬(통쿡쿠제약) """ # 상단 import 섹션에 추가 from korean_romanizer.romanizer import Romanizer # get_jp_synonyms_formatted 함수를 아래 코드로 교체 def get_jp_synonyms_formatted(category, word, official_eng=None, use_gpt=True, model="gpt-4o-mini", api_key=None, temperature=0.3, sleep_sec=1): jp_main = jp_syn = eng_syn = comment = "" if use_gpt: # ✅ 일문 표기 포함 여부 감지 has_jp_notation = "(일문 표기:" in word or "(일문:" in word # ✅ 입력 언어 판단 (일본어 문자 포함 여부) import re is_japanese_input = bool(re.search(r'[\u3040-\u309F\u30A0-\u30FF]', word)) and not has_jp_notation prompts = { "브랜드": f"""너는 한국어/일본어/영어 패션 브랜드 동의어 전문가야. '{word}'라는 브랜드 이름의 동의어를 최대한 정확하게 찾아서 정리해줘. {"⚠️ 중요: 입력에 '일문 표기'가 포함되어 있습니다. 이 일문 표기를 참고하여 동의어를 생성해주세요." if has_jp_notation else ""} {"⚠️ 중요: 입력이 일본어입니다. 대표키워드(일문)는 '{word}'를 그대로 사용하고, 동의어만 생성해주세요." if is_japanese_input else ""} 조건: - 대표키워드(일문): { f"국문 '{word.split('(')[0].strip()}'의 영문 표기를 기준으로 발음을 옮긴 일본어 가타카나 표기 1개 (의미 번역 금지)" if has_jp_notation else f"'{word}' (입력값 그대로)" if is_japanese_input else f"국문 '{word}'의 영문 표기를 기준으로 발음을 옮긴 일본어 가타카나 표기 1개 (의미 번역 금지)" } - 동의어(일문): 브랜드 관련 일본어 동의어, 없으면 비워두기, 중복 제거, ex.마르디 메크르디 - 마르디 매크르디,마르디 (Typing 변경 관점) {f" * 참고: 제공된 일문 표기를 동의어 생성 시 참고하되, 동의어에 포함시키지 마세요" if has_jp_notation else ""} - 동의어(영문): '{word.split('(')[0].strip() if has_jp_notation else word}'의 영어 공식명 기반 최대 3개 {jp_common_rules} ⚠️ 중요 규칙: - 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} - 동의어에는 '{word}' 또는 대표키워드와 동일한 단어를 절대 포함하지 마세요 - 동의어 목록 내에서도 중복된 단어는 제거하세요 - 대소문자만 다른 경우도 중복으로 간주합니다 - 대표키워드(일문)와 동의어(일문)는 모두 가타가나만 사용하세요. 히라가나는 절대 사용하지 마세요. - 「モール」、「ストア」、「ショップ」、「オンライン」、「売り場」、「アウトドア」 같은 관련 없는 명사는 절대 사용 금지. 출력 형식: 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} 대표키워드(일문): {word if is_japanese_input else "単語"} 동의어(일문): 単語1|単語2 동의어(영문): word1|word2|word3 생성 이유: (대표키워드와 동의어를 이렇게 정한 간단한 이유 1문장)""", "카테고리": f"""너는 한국어/일본어/영어 패션 카테고리 동의어 전문가야. '{word}' 카테고리 관련 동의어를 최대한 정확하게 찾아서 정리해줘. {"⚠️ 중요: 입력에 '일문 표기'가 포함되어 있습니다. 이 일문 표기를 참고하여 동의어를 생성해주세요." if has_jp_notation else ""} {"⚠️ 중요: 입력이 일본어입니다. 대표키워드(일문)는 '{word}'를 그대로 사용하고, 동의어만 생성해주세요." if is_japanese_input else ""} 조건: - 대표키워드(일문): { f"국문 '{word.split('(')[0].strip()}'를 일본어로 번역한 카테고리 단어 1개" if has_jp_notation else f"'{word}' (입력값 그대로)" if is_japanese_input else f"국문 '{word}'를 일본어로 번역한 카테고리 단어 1개" } - 동의어(일문): 카테고리 관련 일본어 동의어, 없으면 비워두기, 중복 제거, 의미가 똑같은 용어들로 (ex.맨투맨 - 맨투맨 티셔츠,스웨트셔츠 (용어적 관점))5개 {f" * 참고: 제공된 일문 표기를 동의어 생성 시 참고하되, 동의어에 포함시키지 마세요" if has_jp_notation else ""} - 동의어(영문): '{word.split('(')[0].strip() if has_jp_notation else word}' 카테고리 영어 동의어, 최대 3개 {jp_common_rules} ⚠️ 중요 규칙: - 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} - 동의어에는 '{word}' 또는 대표키워드와 동일한 단어를 절대 포함하지 마세요 - 동의어 목록 내에서도 중복된 단어는 제거하세요 - 대소문자만 다른 경우도 중복으로 간주합니다 출력 형식: 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} 대표키워드(일문): {word if is_japanese_input else "単語"} 동의어(일문): 単語1|単語2 동의어(영문): word1|word2|word3 생성 이유: (대표키워드와 동의어를 이렇게 정한 간단한 이유 1문장)""", "색상": f"""너는 패션 색상 동의어 전문가야. '{word}' 색상 관련 동의어를 최대한 정확하게 찾아서 정리해줘. {"⚠️ 중요: 입력에 '일문 표기'가 포함되어 있습니다. 이 일문 표기를 참고하여 동의어를 생성해주세요." if has_jp_notation else ""} {"⚠️ 중요: 입력이 일본어입니다. 대표키워드(일문)는 '{word}'를 그대로 사용하고, 동의어만 생성해주세요." if is_japanese_input else ""} 조건: - 대표키워드(일문): { f"국문 '{word.split('(')[0].strip()}'를 일본어 색상 단어 1개로 번역" if has_jp_notation else f"'{word}' (입력값 그대로)" if is_japanese_input else f"국문 '{word}'를 일본어 색상 단어 1개로 번역" } - 동의어(일문): 색상 관련 일본어 동의어, 없으면 비워두기, 중복 제거, 의미가 똑같은 용어들로 (ex. 옐로우 - 노랑,노란 (용어적 관점))5개 {f" * 참고: 제공된 일문 표기를 동의어 생성 시 참고하되, 동의어에 포함시키지 마세요" if has_jp_notation else ""} - 동의어(영문): '{word.split('(')[0].strip() if has_jp_notation else word}' 색상 영어 동의어, 최대 3개 {jp_common_rules} ⚠️ 중요 규칙: - 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} - 동의어에는 '{word}' 또는 대표키워드와 동일한 단어를 절대 포함하지 마세요 - 동의어 목록 내에서도 중복된 단어는 제거하세요 - 대소문자만 다른 경우도 중복으로 간주합니다 출력 형식: 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} 대표키워드(일문): {word if is_japanese_input else "単語1"} 동의어(일문): 単語2|単語3 동의어(영문): word1|word2|word3 생성 이유: (대표키워드와 동의어를 이렇게 정한 간단한 이유 1문장)""", "속성": f"""너는 패션 속성/스타일 동의어 전문가야. '{word}' 속성 관련 동의어를 최대한 정확하게 찾아서 정리해줘. {"⚠️ 중요: 입력에 '일문 표기'가 포함되어 있습니다. 이 일문 표기를 참고하여 동의어를 생성해주세요." if has_jp_notation else ""} {"⚠️ 중요: 입력이 일본어입니다. 대표키워드(일문)는 '{word}'를 그대로 사용하고, 동의어만 생성해주세요." if is_japanese_input else ""} 조건: - 대표키워드(일문): { f"국문 '{word.split('(')[0].strip()}'를 일본어 속성 단어 1개로 번역" if has_jp_notation else f"'{word}' (입력값 그대로)" if is_japanese_input else f"국문 '{word}'를 일본어 속성 단어 1개로 번역" } - 동의어(일문): 속성 관련 일본어 동의어, 없으면 비워두기, 중복 제거, 의미가 똑같은 용어들로 (ex. 반소매- 반팔,숏슬리브(용어적 관점))5개 {f" * 참고: 제공된 일문 표기를 동의어 생성 시 참고하되, 동의어에 포함시키지 마세요" if has_jp_notation else ""} - 동의어(영문): '{word.split('(')[0].strip() if has_jp_notation else word}' 속성 영어 동의어, 최대 3개 {jp_common_rules} ⚠️ 중요 규칙: - 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} - 동의어에는 '{word}' 또는 대표키워드와 동일한 단어를 절대 포함하지 마세요 - 동의어 목록 내에서도 중복된 단어는 제거하세요 - 대소문자만 다른 경우도 중복으로 간주합니다 출력 형식: 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} 대표키워드(일문): {word if is_japanese_input else "単語"} 동의어(일문): 単語1|単語2 동의어(영문): word1|word2|word3 생성 이유: (대표키워드와 동의어를 이렇게 정한 간단한 이유 1문장)""", "일반": f"""너는 일반 단어 패션 동의어 전문가야. '{word}' 일반 단어 관련 동의어를 최대한 정확하게 찾아서 정리해줘. {"⚠️ 중요: 입력에 '일문 표기'가 포함되어 있습니다. 이 일문 표기를 참고하여 동의어를 생성해주세요." if has_jp_notation else ""} {"⚠️ 중요: 입력이 일본어입니다. 대표키워드(일문)는 '{word}'를 그대로 사용하고, 동의어만 생성해주세요." if is_japanese_input else ""} 조건: - 대표 키워드 (일문): { f"국문 '{word.split('(')[0].strip()}'를 일본어로 번역한 단어 1개" if has_jp_notation else f"'{word}' (입력값 그대로)" if is_japanese_input else f"국문 '{word}'를 일본어로 번역한 단어 1개" } - 동의어 (일문): 일반 단어 관련 일본어 동의어, 없으면 비워두기, 중복 제거 ex.재팬-제팬,자팬(Typing 변경 관점)5개 {f" * 참고: 제공된 일문 표기를 동의어 생성 시 참고하되, 동의어에 포함시키지 마세요" if has_jp_notation else ""} - 동의어(영문): '{word.split('(')[0].strip() if has_jp_notation else word}' 일반 단어 영어 동의어, 최대 3개 {jp_common_rules} ⚠️ 중요 규칙: - 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} - 동의어에는 '{word}' 또는 대표키워드와 동일한 단어를 절대 포함하지 마세요 - 동의어 목록 내에서도 중복된 단어는 제거하세요 - 대소문자만 다른 경우도 중복으로 간주합니다 출력 형식: 대표키워드: {word.split('(')[0].strip() if has_jp_notation else word} 대표키워드(일문): {word if is_japanese_input else "単語"} 동의어(일문): 単語1|単語2 동의어(영문): word1|word2|word3 생성 이유: (대표키워드와 동의어를 이렇게 정한 간단한 이유 1문장)""" } prompt = prompts.get(category, f"'{word}' 단어의 동의어를 찾아 대표키워드, 일본어 동의어, 영어 동의어를 알려줘.") # 공식 영문명이 있으면 프롬프트 수정 if official_eng: prompt += f" 영어 동의어는 반드시 '{official_eng}' 공식 영문명을 기준으로 정확히 3개까지만 제시해줘." try: completion = generate_with_model( model_choice=model, api_key=api_key, prompt=prompt, temperature=temperature ) time.sleep(sleep_sec) for line in completion.splitlines(): if line.startswith("대표키워드(일문):"): raw_main = line.split(":", 1)[1].strip() jp_main = raw_main.split("|")[0].strip() elif line.startswith("동의어(일문):"): jp_syn = line.split(":", 1)[1].strip() elif line.startswith("동의어(영문):"): raw_eng = line.split(":", 1)[1].strip() eng_list = [x.strip().lower().replace(" ", "") for x in raw_eng.split("|") if x.strip()] eng_syn = "|".join(eng_list) elif line.lower().startswith("생성 이유") or line.lower().startswith("comment"): comment = line.split(":", 1)[1].strip() # ✅ 중복 제거 로직 if jp_main and jp_syn: jp_main_norm = normalize_word(jp_main) jp_syn_list = [x.strip() for x in jp_syn.split("|") if x.strip()] jp_syn_list = [x for x in jp_syn_list if normalize_word(x) != jp_main_norm] jp_syn_list = list(dict.fromkeys(jp_syn_list)) jp_syn = "|".join(jp_syn_list) jp_syn_with_hiragana = add_hiragana_synonyms(jp_main, jp_syn) if eng_syn: eng_syn_list = [x.strip().lower().replace(" ", "") for x in eng_syn.split("|") if x.strip()] eng_syn_list = list(dict.fromkeys(eng_syn_list)) eng_syn = "|".join(eng_syn_list) except Exception as e: comment = f"{str(e)}" BAD_TOKENS = {"なし", "ナシ", "n/a", "none", "-", "없음", ""} def clean_synonyms(text): if not text: return "" items = [] for t in text.split("|"): t = t.strip() if t and t not in BAD_TOKENS: items.append(t) # 중복 제거 items = list(dict.fromkeys(items)) return "|".join(items) # ✅ なし, N/A 등을 빈 문자열로 변환 def clean_none_value(text): if not text: return "" text = text.strip() if text.lower() in ["なし", "n/a", "none", "-", "없음"]: return "" return text return ( clean_none_value(jp_main), clean_synonyms(jp_syn_with_hiragana), clean_none_value(eng_syn), clean_none_value(comment) ) # ------------------------------- # GPT 기반 한국어 동의어 조회 (공식 영문명 지원) # ------------------------------- def get_kor_synonyms_formatted(category, word, official_eng=None, use_gpt=True, model="gpt-4o-mini", api_key=None, temperature=0.3, sleep_sec=1) -> Tuple[str, str,str]: kor_syn = eng_syn = comment = "" if use_gpt: # 카테고리별 프롬프트 기본 prompts = { "브랜드": f"""당신은 패션 브랜드 검색 동의어 전문가입니다. '{word}' 브랜드의 검색 동의어를 생성하세요. **한글 동의어 생성 규칙:** - 사용자가 타이핑 실수로 잘못 입력할 수 있는 표기 5개 - 예시: "마르디 메크르디" → "마르디매크르디", "마르디메크르디", "말디메크르디", "마르디멕크르디", "마르디메끄르디" - 공백 유무, 받침 오타, 비슷한 발음 등을 고려 - "몰", "스토어", "샵", "온라인", "매장", "이" 같은 관련 없는 명사는 절대 추가 금지 **영문 동의어 생성 규칙:** - '{word}' 브랜드의 공식 영문명과 같은 영문 표기 1~3개 - 모든 영문자는 소문자로만 작성 - 공백 없이 붙여쓰기 (예: "mardi mercredi" → "mardimercredi") - 예시: "마르디 메크르디" → "mardimercredi" **절대 규칙:** 1. '{word}'와 완전히 동일한 표기는 동의어에 포함 금지 2. 동의어 목록 내 중복 제거 (대소문자 구분 없이) 3. 모든 동의어는 공백 없이 붙여쓰기 **출력 형식:** 대표키워드: {word} 한글 동의어: 단어1|단어2|단어3|단어4|단어5 영문 동의어: word1|word2|word3 생성 이유: 해당 브랜드의 타이핑 실수 패턴과 영문 표기 특성을 반영하여 생성했습니다.""", "카테고리": f"""당신은 패션 카테고리 검색 동의어 전문가입니다. '{word}' 카테고리의 검색 동의어를 생성하세요. **한글 동의어 생성 규칙:** - '{word}'와 의미가 동일한 다른 한국어 표현 1~개 - 예시: "맨투맨" → "맨투맨티셔츠", "스웨트셔츠" - 업계/소비자들이 실제 검색하는 유사 용어 포함 - 모든 단어는 공백 없이 붙여쓰기 **영문 동의어 생성 규칙:** - '{word}'의 공식 영문명과 같은 영문 표기 1~3개 - 모든 영문자는 소문자로만 작성 - 공백 없이 붙여쓰기 - 예시: "맨투맨" → "sweatshirt", "crewneck", "sweater" **절대 규칙:** 1. '{word}'와 완전히 동일한 표기는 동의어에 포함 금지 2. 동의어 목록 내 중복 제거 (대소문자 구분 없이) 3. 의미가 다른 카테고리는 제외 **출력 형식:** 대표키워드: {word} 한글 동의어: 단어1|단어2|단어3|단어4|단어5 영문 동의어: word1|word2|word3 생성 이유: 해당 카테고리의 업계 표준 용어와 소비자 검색 패턴을 반영하여 생성했습니다.""", "색상": f"""당신은 색상 검색 동의어 전문가입니다. '{word}' 색상의 검색 동의어를 생성하세요. **한글 동의어 생성 규칙:** - '{word}'와 의미가 동일한 색상 표현 1~개 - 예시: "옐로우" → "노랑", "노란색", "노란", "옐로", "황색" - 한글 고유어, 외래어, 한자어 등 다양한 표현 포함 - 모든 단어는 공백 없이 붙여쓰기 **영문 동의어 생성 규칙:** - '{word}'의 공식 영문 색상명 1~3개 - 모든 영문자는 소문자로만 작성 - 공백 없이 붙여쓰기 - 예시: "옐로우" → "yellow", "lemon", "gold" **절대 규칙:** 1. '{word}'와 완전히 동일한 표기는 동의어에 포함 금지 2. 동의어 목록 내 중복 제거 (대소문자 구분 없이) 3. 완전히 다른 색상은 제외 **출력 형식:** 대표키워드: {word} 한글 동의어: 단어1|단어2|단어3|단어4|단어5 영문 동의어: word1|word2|word3 생성 이유: 해당 색상의 다양한 언어적 표현을 반영하여 생성했습니다.""", "속성": f"""당신은 패션 속성 검색 동의어 전문가입니다. '{word}' 속성의 검색 동의어를 생성하세요. **한글 동의어 생성 규칙:** - '{word}'와 의미가 동일한 속성 표현 1~5개 - 예시: "반소매" → "반팔", "숏슬리브", "하프슬리브", "5부소매", "짧은팔" - 한글 표현과 외래어 표현 모두 포함 - 모든 단어는 공백 없이 붙여쓰기 **영문 동의어 생성 규칙:** - '{word}'의 영문 표기 1~3개 - 모든 영문자는 소문자로만 작성 - 공백 없이 붙여쓰기 - 예시: "반소매" → "shortsleeve", "halfsleeve" **절대 규칙:** 1. '{word}'와 완전히 동일한 표기는 동의어에 포함 금지 2. 동의어 목록 내 중복 제거 (대소문자 구분 없이) 3. 의미가 다른 속성은 제외 **출력 형식:** 대표키워드: {word} 한글 동의어: 단어1|단어2|단어3|단어4|단어5 영문 동의어: word1|word2|word3 생성 이유: 해당 속성의 다양한 표현 방식을 반영하여 생성했습니다.""", "일반": f"""당신은 패션 검색어 동의어 전문가입니다. '{word}'의 검색 동의어를 생성하세요. **한글 동의어 생성 규칙:** - 사용자가 타이핑 실수로 잘못 입력할 수 있는 표기 5개 - 예시: "재팬" → "제팬", "자팬", "쟈팬", "재펜", "자펜" - 자음/모음 혼동, 받침 오타, 발음 유사 표기 고려 - 모든 단어는 공백 없이 붙여쓰기 **영문 동의어 생성 규칙:** - '{word}'의 영문 번역 1~3개 - 모든 영문자는 소문자로만 작성 - 공백 없이 붙여쓰기 - 예시: "재팬" → "japan" **절대 규칙:** 1. '{word}'와 완전히 동일한 표기는 동의어에 포함 금지 2. 동의어 목록 내 중복 제거 (대소문자 구분 없이) 3. 의미가 완전히 다른 단어는 제외 **출력 형식:** 대표키워드: {word} 한글 동의어: 단어1|단어2|단어3|단어4|단어5 영문 동의어: word1|word2|word3 생성 이유: 타이핑 오류 패턴과 다양한 표기법을 반영하여 생성했습니다.""" } prompt = prompts.get(category, f"'{word}' 단어의 동의어를 찾아 한글 동의어, 영어 동의어를 알려줘.") # 공식 영문명이 있으면 프롬프트 수정 if official_eng: prompt += f" 영어 동의어는 반드시 '{official_eng}' 공식 영문명을 기준으로 정확히 3개까지만 제시해줘." try: completion = generate_with_model( model_choice=model, api_key=api_key, prompt=prompt, temperature=temperature ) time.sleep(sleep_sec) for line in completion.splitlines(): if line.startswith("한글 동의어:"): kor_syn = line.split(":", 1)[1].strip() elif line.startswith("영문 동의어:"): raw_eng = line.split(":", 1)[1].strip() # ✅ 영문 동의어 후처리 eng_list = [x.strip().lower().replace(" ", "") for x in raw_eng.split("|") if x.strip()] eng_syn = "|".join(eng_list) elif line.lower().startswith("생성 이유") or line.lower().startswith("comment"): comment = line.split(":", 1)[1].strip() # ✅ 중복 제거 로직 추가 word_norm = normalize_word(word) if kor_syn: kor_syn_list = [x.strip() for x in kor_syn.split("|") if x.strip()] # 대표키워드와 중복 제거 + 동의어 내부 중복 제거 kor_syn_list = [x for x in kor_syn_list if normalize_word(x) != word_norm] kor_syn_list = list(dict.fromkeys(kor_syn_list)) kor_syn = "|".join(kor_syn_list) if eng_syn: eng_syn_list = [x.strip() for x in eng_syn.split("|") if x.strip()] eng_syn_list = list(dict.fromkeys(eng_syn_list)) eng_syn = "|".join(eng_syn_list) except Exception as e: comment = f"{str(e)}" # ✅ 항상 3개 반환 (형태 맞춤) return kor_syn or "", eng_syn or "", comment or "" # ------------------------------- # 다중 단어 처리 # ------------------------------- OUTPUT_COLUMNS = ["순번","국문 대표검색어","일문 대표검색어","Kor synonym","Jap synonym","Eng synonym","Comment"] def process_multiple_jp(input_pairs, model, api_key, temperature): results = [] idx = 1 for cat, kor_w, jp_w in input_pairs: try: cat = normalize_word(cat) kor_w = kor_w.strip() if kor_w else "" jp_w = jp_w.strip() if jp_w else "" if cat == "" or (kor_w == "" and jp_w == ""): continue comment = "" jp_main = jp_syn = eng = "" # 국문 + 일문 동시 입력 if kor_w and jp_w: # ✅ JSONL 우선 조회 try: jp_main, jp_syn, eng = get_jp_synonyms_from_json(cat, kor_w) if jp_main or jp_syn or eng: comment = "🔍 동의어 사전 기반 조회" else: raise ValueError("동의어 사전에 데이터 없음") except: # JSONL 없으면 GPT 사용 combined_prompt = f"{kor_w} (일문 표기: {jp_w})" _, jp_syn, eng, cmt = get_jp_synonyms_formatted( cat, combined_prompt, official_eng=None, use_gpt=True, model=model, api_key=api_key, temperature=temperature ) comment = cmt or "🤖 AI Fallback" # 일문 대표검색어는 입력값 그대로 row = [idx, kor_w, jp_w, "", jp_syn or "", eng or "", comment or ""] # ✅ 국문만 입력 (JSONL 우선 조회 추가!) elif kor_w: try: jp_main, jp_syn, eng = get_jp_synonyms_from_json(cat, kor_w) if jp_main or jp_syn or eng: comment = "🔍 동의어 사전 기반 조회" else: raise ValueError("동의어 사전에 데이터 없음") except: # JSONL 없으면 GPT 사용 jp_main, jp_syn, eng, comment = get_jp_synonyms_formatted( cat, kor_w, official_eng=None, use_gpt=True, model=model, api_key=api_key, temperature=temperature ) row = [idx, kor_w, jp_main or "", "", jp_syn or "", eng or "", comment or ""] # 일문만 입력 else: try: jp_main, jp_syn, eng = get_jp_synonyms_from_jp_input(cat, jp_w) if jp_syn or eng: # ✅ 조건 개선 comment = "🔍 동의어 사전 기반 조회" else: raise ValueError("JSONL에 데이터 없음") except: # JSONL 없으면 GPT 사용 jp_main, jp_syn, eng, cmt = get_jp_synonyms_formatted( cat, jp_w, official_eng=None, use_gpt=True, model=model, api_key=api_key, temperature=temperature ) comment = cmt or "🤖 AI Fallback" row = [idx, "", jp_main or jp_w, "", jp_syn or "", eng or "", comment or ""] results.append(row) idx += 1 except Exception as e: results.append([idx, kor_w, jp_w, "", "", "", f"⚠️ {e}"]) idx += 1 if not results: return pd.DataFrame(columns=OUTPUT_COLUMNS) return pd.DataFrame(results, columns=OUTPUT_COLUMNS) def process_multiple_kor(input_pairs, model="gpt-5-mini", api_key=None, temperature=0.3): results = [] idx = 1 for cat, kor_w, jp_w in input_pairs: try: cat = normalize_word(cat) kor_w = kor_w.strip() if kor_w else "" jp_w = jp_w.strip() if jp_w else "" if cat == "" or (kor_w == "" and jp_w == ""): continue comment = "" kor_syn = eng_syn = "" if kor_w: # ✅ JSONL 우선 조회 try: kor_syn, eng_syn = get_kor_synonyms_from_json(cat, kor_w) if kor_syn or eng_syn: comment = "🐥 동의어 사전 기반 조회" else: raise ValueError("JSONL에 데이터 없음") except: # JSONL 없으면 GPT 사용 try: kor_syn, eng_syn, comment = get_kor_synonyms_formatted( cat, kor_w, use_gpt=True, model=model, api_key=api_key, temperature=temperature ) except Exception as e: comment = f"⚠️{str(e)}" row = [idx, kor_w, "", kor_syn or "", "", eng_syn or "", comment or ""] else: row = [idx, "", jp_w, "", "", "", "입력 없음"] results.append(row) idx += 1 except Exception as e: results.append([idx, kor_w, jp_w, "", "", "", f"⚠️ {e}"]) idx += 1 if not results: return pd.DataFrame(columns=OUTPUT_COLUMNS) return pd.DataFrame(results, columns=OUTPUT_COLUMNS) # ------------------------------- # CSV 파일 처리 함수 (UTF-8 / CP949 대응 + 처리 완료 메시지) # ------------------------------- def excel_csv_process(file, lang, category, model, api_key, temperature): import pandas as pd # category 필수 체크 if not category or str(category).strip() == "": return "⚠️분류 선택은 필수입니다.", pd.DataFrame(columns=OUTPUT_COLUMNS), None if file is None: return "⚠️파일이 없습니다.", pd.DataFrame(columns=OUTPUT_COLUMNS), None # 확장자 확인 ext = file.name.split(".")[-1].lower() if ext != "csv": return "⚠️ CSV 파일만 지원합니다.", pd.DataFrame(columns=OUTPUT_COLUMNS), None try: df = pd.read_csv(file.name, encoding="utf-8") except UnicodeDecodeError: try: df = pd.read_csv(file.name, encoding="cp949") except Exception as e: return f"⚠️ CSV 읽기 실패: 인코딩 문제 ({e})", pd.DataFrame(columns=OUTPUT_COLUMNS), None except pd.errors.ParserError: return "⚠️ CSV 읽기 실패: 파일 구조가 잘못되었거나 CSV 형식이 아닙니다.", pd.DataFrame(columns=OUTPUT_COLUMNS), None except Exception as e: return f"⚠️ CSV 읽기 실패: 알 수 없는 오류 ({e})", pd.DataFrame(columns=OUTPUT_COLUMNS), None # 필수 컬럼 검사 required_cols = ["국문 대표검색어", "일문 대표검색어"] if not any(c in df.columns for c in required_cols): return "⚠️국문 대표검색어 또는 일문 대표검색어 필수 열이 없습니다.", pd.DataFrame(columns=OUTPUT_COLUMNS), None # DropDown에서 받은 category 적용 categories = [category] * len(df) input_pairs = [] for i, row in df.iterrows(): kor_w = "" if pd.isna(row.get("국문 대표검색어")) else str(row["국문 대표검색어"]).strip() jp_w = "" if pd.isna(row.get("일문 대표검색어")) else str(row["일문 대표검색어"]).strip() if kor_w == "" and jp_w == "": continue input_pairs.append((category, kor_w, jp_w)) if lang == "jp": out_df = process_multiple_jp(input_pairs, model=model, api_key=api_key, temperature=temperature) #csv_file = "output_jp_synonyms.csv" else: out_df = process_multiple_kor(input_pairs, model=model, api_key=api_key, temperature=temperature) #csv_file = "output_kor_synonyms.csv" csv_file = f"output_{lang}_synonyms.csv" out_df.to_csv(csv_file, index=False, encoding="utf-8-sig") # ✅ 처리 완료 메시지 추가 return "✅ 파일이 정상적으로 처리되었습니다.", out_df, csv_file # ------------------------------- # 파일 업로드 핸들러 정의 (일본어 / 한국어) # ------------------------------- def handle_jp_file(file, category, model, api_key, temperature): return excel_csv_process(file, "jp", category, model, api_key, temperature) def handle_kor_file(file, category, model, api_key, temperature): return excel_csv_process(file, "kor", category, model, api_key, temperature) # ------------------------------- # 여러 단어 처리 # ------------------------------- def split_multiple_words(input_str): """ 입력 문자열을 줄바꿈(\n) 또는 쉼표(,) 기준으로 리스트로 분리 """ if not input_str or str(input_str).strip() == "": return [] # \n 또는 , 기준으로 분리 words = re.split(r'[\n,]+', input_str) words = [w.strip() for w in words if w.strip()] return words def validate_word_count(words_list, max_count=20): """ 단어 개수 검증 (최대 20개) """ if len(words_list) > max_count: raise ValueError(f"⚠️ 최대 {max_count}개까지만 입력 가능합니다. (현재: {len(words_list)}개)") return words_list def jp_search_multiple(cat_text, kor_texts, jp_texts, model, api_key, temperature): if not cat_text or str(cat_text).strip() == "": return pd.DataFrame(columns=OUTPUT_COLUMNS), None # 다중 단어 분리 kor_list = split_multiple_words(kor_texts) jp_list = split_multiple_words(jp_texts) # kor/jp 리스트 길이 맞추기 max_len = max(len(kor_list), len(jp_list)) kor_list += [""] * (max_len - len(kor_list)) jp_list += [""] * (max_len - len(jp_list)) # ✅ 20개 제한 검증 try: if kor_list: kor_list = validate_word_count(kor_list, max_count=20) if jp_list: jp_list = validate_word_count(jp_list, max_count=20) except ValueError as e: # 에러 발생 시 빈 결과 반환 error_df = pd.DataFrame([[str(e)]], columns=["⚠️ 오류"]) return error_df, None # 각 단어마다 입력 쌍 생성 input_pairs = [(cat_text, k, j) for k, j in zip(kor_list, jp_list)] # 기존 다중 처리 함수 호출 df = process_multiple_jp(input_pairs, model=model, api_key=api_key, temperature=temperature) if df.empty: return df, None csv_file = "jp_synonyms_multiple.csv" df.to_csv(csv_file, index=False, encoding="utf-8-sig") return df, csv_file def kor_search_multiple_wrapper(cat_text, kor_texts, model, api_key, temperature): if not cat_text or str(cat_text).strip() == "": return pd.DataFrame(columns=OUTPUT_COLUMNS), None kor_list = split_multiple_words(kor_texts) # ✅ 20개 제한 검증 try: if kor_list: kor_list = validate_word_count(kor_list, max_count=20) except ValueError as e: error_df = pd.DataFrame([[str(e)]], columns=["⚠️ 오류"]) return error_df, None # 한국어 탭은 국문만 사용 → 길이 맞추기, jp_list 모두 제거 if not kor_list: return pd.DataFrame(columns=OUTPUT_COLUMNS), None input_pairs = [(cat_text, k, "") for k in kor_list] df = process_multiple_kor(input_pairs, model=model, api_key=api_key, temperature=temperature) if df.empty: return df, None csv_file = "kor_synonyms_multiple.csv" df.to_csv(csv_file, index=False, encoding="utf-8-sig") return df, csv_file # ------------------------------- # Gradio UI # ------------------------------- with gr.Blocks() as demo: gr.Markdown("# 🧠 MUSINSA 동의어 생성기") # ------------------------------- # 일본어 탭 # ------------------------------- with gr.Tab("일본어"): gr.Markdown("## ✏️ 단어 입력") jp_category = gr.Dropdown( choices=["브랜드", "카테고리", "색상", "속성", "일반"], value=None, label="분류 선택 (필수)" ) jp_kor_words = gr.Textbox(label="국문 대표검색어",placeholder="단어 20개까지 입력 가능 (줄바꿈 또는 ,로 구분)",max_lines=20) jp_jp_words = gr.Textbox(label="일문 대표검색어",placeholder="단어 20개까지 입력 가능 (줄바꿈 또는 ,로 구분)",max_lines=20) official_eng_input_jp = gr.Textbox(label="공식 영문명 (단일 단어 검색 시)") with gr.Accordion("AI 옵션", open=False): api_key_input = gr.Textbox(type="password", label="API KEY", placeholder="본인의 모델 Key를 입력해주세요.") model_input = gr.Dropdown( choices=["gemini", "claude","gpt-5-mini", "gpt-4o-mini"], value="gemini", label="모델 선택" ) temperature_input = gr.Slider(0.0, 1.0, step=0.05, value=0.3, label="Temperature") jp_button1 = gr.Button("📘 동의어 찾기") jp_output = gr.Dataframe(headers=OUTPUT_COLUMNS, label="결과") jp_download_csv = gr.DownloadButton(label="📥 CSV 다운로드") jp_button1.click( fn=jp_search_multiple, inputs=[jp_category, jp_kor_words, jp_jp_words, model_input, api_key_input, temperature_input], outputs=[jp_output, jp_download_csv] ) gr.Markdown("## 📂 파일 업로드 처리") jp_file = gr.File(label="CSV 업로드 (.csv)") jp_file_output = gr.Dataframe(headers=OUTPUT_COLUMNS) jp_file_download = gr.DownloadButton(label="📥 CSV 다운로드") jp_status = gr.Textbox(label="Progress Status") jp_file.upload(handle_jp_file, inputs=[jp_file, jp_category, model_input, api_key_input, temperature_input], outputs=[jp_status, jp_file_output, jp_file_download]) # ------------------------------- # 한국어 탭 # ------------------------------- with gr.Tab("한국어"): gr.Markdown("## ✏️ 단어 입력") kor_category = gr.Dropdown( choices=["브랜드", "카테고리", "색상", "속성", "일반"], value=None, label="분류 선택 (필수)") kor_kor_words = gr.Textbox(label="국문 대표검색어",placeholder="단어 20개까지 입력 가능 (줄바꿈 또는 ,로 구분)",max_lines=20) official_eng_input = gr.Textbox(label="공식 영문명 (단일 단어 검색 시)") with gr.Accordion("AI 옵션", open=False): api_key_k = gr.Textbox(type="password", label="API KEY", placeholder="본인의 모델 Key를 입력해주세요.") model_k = gr.Dropdown( choices=[ "gemini", "claude","gpt-5-mini", "gpt-4o-mini"], value="gemini", label="Model" ) temperature_k = gr.Slider(0.0, 1.0, step=0.05, value=0.3, label="Temperature") kor_button1 = gr.Button("📘 동의어 찾기") kor_output = gr.Dataframe(headers=OUTPUT_COLUMNS, label="결과") kor_download_csv = gr.DownloadButton(label="📥 CSV 다운로드") kor_button1.click( kor_search_multiple_wrapper, inputs=[kor_category, kor_kor_words, model_k, api_key_k, temperature_k], outputs=[kor_output, kor_download_csv] ) gr.Markdown("## 📂 파일 업로드 처리") kor_file = gr.File(label="CSV 업로드 (.csv)") kor_file_output = gr.Dataframe(headers=OUTPUT_COLUMNS) kor_file_download = gr.DownloadButton(label="📥 CSV 다운로드") kor_status = gr.Textbox(label="Progress Status") kor_file.upload(handle_kor_file, inputs=[kor_file, kor_category, model_k, api_key_k, temperature_k], outputs=[kor_status, kor_file_output, kor_file_download]) if __name__ == "__main__": demo.launch()