|
""" |
|
Square Theory Generator (len-flexible) |
|
===================================== |
|
2025โ05โ28 v6 โ LLM ๊ฒฐ๊ณผ๊ฐ 10๊ฐ ๋ฏธ๋ง์ผ ๋ ์ ์ฐ ์ฒ๋ฆฌ |
|
--------------------------------------------------- |
|
๋ณ๊ฒฝ ์์ฝ |
|
--------- |
|
* **๋ฌธ์ **: LLM์ด 10๊ฐ ๋ฏธ๋ง์ ์ ์์ ๋ฐํํ๋ฉด ๊ธธ์ด ๊ฒ์ฆ ์์ธ ๋ฐ์. |
|
* **ํด๊ฒฐ**: |
|
1. `call_llm()`์์ JSON์ด dict ๋จ์ผ ๊ฐ์ฒด or 1~9๊ฐ ๋ฆฌ์คํธ์ฌ๋ ํ์ฉ. |
|
2. ๋ฆฌ์คํธ๊ฐ 1๊ฐ ๋ฏธ๋ง์ด๋ฉด ์ค๋ฅ, 2~9๊ฐ๋ ๊ฒฝ๊ณ ๋ง ํ๊ธฐ. |
|
3. Markdown์ ์ค์ ๊ฐ์๋ก ํค๋ ์ถ๋ ฅ. |
|
|
|
์คํ๋ฒ |
|
------ |
|
```bash |
|
pip install --upgrade gradio matplotlib openai requests |
|
export OPENAI_API_KEY="sk-..." |
|
python square_theory_gradio.py |
|
``` |
|
""" |
|
|
|
import os |
|
import json |
|
import tempfile |
|
import urllib.request |
|
import gradio as gr |
|
import matplotlib.pyplot as plt |
|
from matplotlib import patches, font_manager, rcParams |
|
from openai import OpenAI |
|
|
|
|
|
|
|
|
|
PREFERRED_FONTS = ["Malgun Gothic", "NanumGothic", "AppleGothic", "DejaVu Sans"] |
|
NANUM_URL = ( |
|
"https://github.com/google/fonts/raw/main/ofl/nanumgothic/" |
|
"NanumGothic-Regular.ttf" |
|
) |
|
|
|
def _set_korean_font(): |
|
available = {f.name for f in font_manager.fontManager.ttflist} |
|
for cand in PREFERRED_FONTS: |
|
if cand in available: |
|
rcParams["font.family"] = cand |
|
break |
|
else: |
|
try: |
|
tmp_dir = tempfile.gettempdir() |
|
font_path = os.path.join(tmp_dir, "NanumGothic-Regular.ttf") |
|
if not os.path.exists(font_path): |
|
urllib.request.urlretrieve(NANUM_URL, font_path) |
|
font_manager.fontManager.addfont(font_path) |
|
rcParams["font.family"] = font_manager.FontProperties(fname=font_path).get_name() |
|
except Exception as e: |
|
print("[WARN] Font download failed, Korean text may break:", e) |
|
rcParams["axes.unicode_minus"] = False |
|
|
|
_set_korean_font() |
|
|
|
|
|
|
|
|
|
if not os.getenv("OPENAI_API_KEY"): |
|
raise EnvironmentError("OPENAI_API_KEY ํ๊ฒฝ ๋ณ์๋ฅผ ์ค์ ํ์ธ์.") |
|
|
|
client = OpenAI() |
|
|
|
|
|
|
|
|
|
|
|
def draw_square(words): |
|
fig, ax = plt.subplots(figsize=(4, 4)) |
|
ax.add_patch(patches.Rectangle((0, 0), 1, 1, fill=False, linewidth=2)) |
|
ax.text(-0.05, 1.05, str(words.get("tl", "")), ha="right", va="bottom", fontsize=14, fontweight="bold") |
|
ax.text(1.05, 1.05, str(words.get("tr", "")), ha="left", va="bottom", fontsize=14, fontweight="bold") |
|
ax.text(1.05, -0.05, str(words.get("br", "")), ha="left", va="top", fontsize=14, fontweight="bold") |
|
ax.text(-0.05, -0.05, str(words.get("bl", "")), ha="right", va="top", fontsize=14, fontweight="bold") |
|
ax.set_xticks([]) |
|
ax.set_yticks([]) |
|
ax.set_xlim(-0.2, 1.2) |
|
ax.set_ylim(-0.2, 1.2) |
|
ax.set_aspect("equal") |
|
return fig |
|
|
|
|
|
|
|
|
|
SYSTEM_PROMPT = ( |
|
"๋๋ ํ๊ตญ์ด ์นดํผยท๋ธ๋๋ ๋ค์ด๋ฐ ์ ๋ฌธ๊ฐ์ด์ Square Theory ๋์ฐ๋ฏธ๋ค. " |
|
"์ฌ์ฉ์๊ฐ ์ค ํ๋์ ๋จ์ด(tl)๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ์ง์ด ๋ฐ์ด๋ ์์๋๋ก ์ต๋ 10๊ฐ์ ์ ์์ JSON ๋ฐฐ์ด๋ก ๋ฐํํด๋ผ. " |
|
"๊ฐ ๋ฐฐ์ด ์์๋ tl, tr, br, bl, top_phrase, bottom_phrase, slogan, brand ํ๋๋ฅผ ๊ฐ์ง๋ค. " |
|
"์ฌ๊ฐํ ๋ค ๊ผญ์ง์ (tlโtrโbrโbl)์ด ์์ฐ์ค๋ ์ฐ๊ฒฐ๋ผ์ผ ํ๊ณ , ๋ฐฐ์ด ์ฒซ ์์๊ฐ ์ต์ฐ์ ์ ์์ด๋ค. " |
|
"๊ฒฐ๊ณผ๋ JSON ์ธ ํ
์คํธ๋ฅผ ํฌํจํ์ง ์์์ผ ํ๋ค." |
|
) |
|
|
|
|
|
def clean_json_block(text: str) -> str: |
|
text = text.strip() |
|
if text.startswith("```"): |
|
text = text.split("\n", 1)[1] if "\n" in text else text[3:] |
|
if text.endswith("```"): |
|
text = text[:-3] |
|
return text.strip() |
|
|
|
|
|
def call_llm(seed: str): |
|
resp = client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=[ |
|
{"role": "system", "content": SYSTEM_PROMPT}, |
|
{"role": "user", "content": seed}, |
|
], |
|
temperature=0.9, |
|
max_tokens=1024, |
|
) |
|
raw = resp.choices[0].message.content |
|
cleaned = clean_json_block(raw) |
|
try: |
|
data = json.loads(cleaned) |
|
|
|
if isinstance(data, dict): |
|
data = [data] |
|
if not isinstance(data, list): |
|
raise TypeError("LLM ์๋ต์ด ๋ฆฌ์คํธ๊ฐ ์๋") |
|
if len(data) == 0: |
|
raise ValueError("LLM์ด ๋น ๋ฐฐ์ด์ ๋ฐํ") |
|
except Exception as exc: |
|
raise ValueError(f"LLM JSON ํ์ฑ ์คํจ: {exc}\n์๋ฌธ ์ผ๋ถ: {cleaned[:300]} โฆ") |
|
return data |
|
|
|
|
|
|
|
|
|
|
|
def generate(seed_word: str): |
|
results = call_llm(seed_word) |
|
fig = draw_square({k: results[0][k] for k in ("tl", "tr", "br", "bl")}) |
|
md_lines = [f"## ์ด {len(results)}๊ฐ ์ ์\n"] |
|
for idx, item in enumerate(results, 1): |
|
md_lines.append( |
|
f"### {idx}. {item['top_phrase']} / {item['bottom_phrase']}\n" |
|
f"- **์ฌ๋ก๊ฑด**: {item['slogan']}\n" |
|
f"- **๋ธ๋๋ ๋ค์**: {item['brand']}\n" |
|
f"- (tl={item['tl']}, tr={item['tr']}, br={item['br']}, bl={item['bl']})\n" |
|
) |
|
return fig, "\n".join(md_lines) |
|
|
|
|
|
|
|
|
|
with gr.Blocks(title="Square Theory โ ์ต๊ณ ์ ์ ๐ฐ๐ท") as demo: |
|
gr.Markdown("""# ๐ง Square Theory ์ ์ (์ต๋ 10๊ฐ)\n๋จ์ด 1๊ฐ ์
๋ ฅ โ LLM์ด ํ๊ฐยท์ ๋ ฌํ ์ฌ๊ฐํ/์นดํผ/๋ธ๋๋ ๋ค์""") |
|
seed = gr.Textbox(label="์๋ ๋จ์ด(TL)", placeholder="์: ๊ณจ๋ ") |
|
run = gr.Button("์์ฑ") |
|
fig_out = gr.Plot(label="1์ ์ฌ๊ฐํ") |
|
md_out = gr.Markdown(label="์ ์ ๋ชฉ๋ก") |
|
|
|
run.click(generate, inputs=seed, outputs=[fig_out, md_out]) |
|
|
|
if __name__ == "__main__": |
|
demo.launch() |
|
|