Naming / app.py
openfree's picture
Update app.py
faa3549 verified
raw
history blame
14.8 kB
"""
Square Theory Generator v12 - Enhanced Edition
=============================================
2025-05-28 | ํ’ˆ์งˆ ํ–ฅ์ƒ ๋ฐ ๊ธฐ๋Šฅ ํ™•์žฅ ๋ฒ„์ „
-------------------------------------------
์ฃผ์š” ๊ฐœ์„ ์‚ฌํ•ญ:
- ๋” ๊ตฌ์ฒด์ ์ธ ํ”„๋กฌํ”„ํŠธ๋กœ ๊ณ ํ’ˆ์งˆ ๊ฒฐ๊ณผ ์ƒ์„ฑ
- ์‹œ๊ฐ์  ํ”„๋ฆฌ๋ทฐ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
- CSV/JSON ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ
- ์žฌ์ƒ์„ฑ ๋ฐ ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ธฐ๋Šฅ
- ํ–ฅ์ƒ๋œ UI/UX
"""
import os
import json
import csv
import io
import gradio as gr
import openai
from openai import OpenAI
from datetime import datetime
from typing import List, Dict, Optional, Tuple
# -------------------------------------------------
# 0. OpenAI ํด๋ผ์ด์–ธํŠธ ์„ค์ •
# -------------------------------------------------
if not os.getenv("OPENAI_API_KEY"):
raise EnvironmentError("OPENAI_API_KEY ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜์„ธ์š”.")
client = OpenAI()
# -------------------------------------------------
# 1. ํ–ฅ์ƒ๋œ ํ”„๋กฌํ”„ํŠธ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ
# -------------------------------------------------
ENHANCED_SYSTEM_PROMPT = """
๋‹น์‹ ์€ 20๋…„ ๊ฒฝ๋ ฅ์˜ ํ•œ๊ตญ ์ตœ๊ณ  ์นดํ”ผ๋ผ์ดํ„ฐ์ด์ž ๋ธŒ๋žœ๋“œ ๋„ค์ด๋ฐ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
Square Theory๋Š” 4๊ฐœ์˜ ํ•ต์‹ฌ ๋‹จ์–ด๋ฅผ ์‚ฌ๊ฐํ˜• ๋ชจ์„œ๋ฆฌ์— ๋ฐฐ์น˜ํ•˜์—ฌ ๋ธŒ๋žœ๋“œ ์ •์ฒด์„ฑ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•๋ก ์ž…๋‹ˆ๋‹ค.
์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์‹œ๋“œ ๋‹จ์–ด(tl)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ตœ๋Œ€ 20๊ฐœ์˜ ๊ณ ํ’ˆ์งˆ ์ œ์•ˆ์„ ์ƒ์„ฑํ•˜์„ธ์š”.
๊ฐ ์ œ์•ˆ์€ ๋‹ค์Œ ์›์น™์„ ๋”ฐ๋ผ์•ผ ํ•ฉ๋‹ˆ๋‹ค:
1. **์‚ฌ๊ฐํ˜• ๊ตฌ์„ฑ (tl, tr, br, bl)**
- ๊ฐ ๋‹จ์–ด๋Š” ๊ณต๋ฐฑ ์—†๋Š” ๋‹จ์ผ ๋‹จ์–ด (ํ•œ๊ธ€ 1-2์–ด์ ˆ ๋˜๋Š” ์˜์–ด 1๋‹จ์–ด)
- tl(์™ผ์ชฝ์ƒ๋‹จ): ์‹œ๋“œ ๋‹จ์–ด
- tr(์˜ค๋ฅธ์ชฝ์ƒ๋‹จ): tl๊ณผ ๋Œ€๋น„๋˜๊ฑฐ๋‚˜ ๋ณด์™„ํ•˜๋Š” ๊ฐœ๋…
- br(์˜ค๋ฅธ์ชฝํ•˜๋‹จ): ๊ธ์ •์  ๊ฒฐ๊ณผ๋‚˜ ํ˜œํƒ
- bl(์™ผ์ชฝํ•˜๋‹จ): ๊ฐ์„ฑ์  ๊ฐ€์น˜๋‚˜ ๊ฒฝํ—˜
2. **์นดํ”ผ ์ž‘์„ฑ ์›์น™**
- top_phrase: ๊ณ ๊ฐ์˜ ๋‹ˆ์ฆˆ๋‚˜ ๋ฌธ์ œ๋ฅผ ์งš๋Š” ๊ณต๊ฐํ˜• ๋ฌธ๊ตฌ
- bottom_phrase: ํ•ด๊ฒฐ์ฑ…์ด๋‚˜ ์•ฝ์†์„ ์ œ์‹œํ•˜๋Š” ํ–‰๋™ ์œ ๋„ํ˜• ๋ฌธ๊ตฌ
- slogan: ๋ธŒ๋žœ๋“œ ์ •์ฒด์„ฑ์„ ํ•œ ๋ฌธ์žฅ์œผ๋กœ ์••์ถ• (7-15์ž ๊ถŒ์žฅ)
3. **๋ธŒ๋žœ๋“œ ๋„ค์ด๋ฐ**
- ๊ธฐ์–ตํ•˜๊ธฐ ์‰ฝ๊ณ  ๋ฐœ์Œ์ด ์ž์—ฐ์Šค๋Ÿฌ์šด ์ด๋ฆ„
- ์‚ฌ๊ฐํ˜•์˜ ํ•ต์‹ฌ ๊ฐ€์น˜๋ฅผ ๋ฐ˜์˜
- 2-4์Œ์ ˆ ๊ถŒ์žฅ
4. **ํ’ˆ์งˆ ๊ธฐ์ค€**
- ๋…์ฐฝ์„ฑ: ์ง„๋ถ€ํ•˜์ง€ ์•Š์€ ์‹ ์„ ํ•œ ์กฐํ•ฉ
- ์ผ๊ด€์„ฑ: 4๊ฐœ ๋‹จ์–ด๊ฐ€ ์œ ๊ธฐ์ ์œผ๋กœ ์—ฐ๊ฒฐ
- ์‹ค์šฉ์„ฑ: ์‹ค์ œ ๋งˆ์ผ€ํŒ…์— ํ™œ์šฉ ๊ฐ€๋Šฅ
- ๊ฐ์„ฑ์  ํ˜ธ์†Œ๋ ฅ: ํƒ€๊ฒŸ ๊ณ ๊ฐ์˜ ๋งˆ์Œ์„ ์›€์ง์ž„
JSON ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, ํ’ˆ์งˆ ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜์„ธ์š”.
๊ฐ ๊ฐ์ฒด: {tl, tr, br, bl, top_phrase, bottom_phrase, slogan, brand, concept_score(1-10), target_audience}
"""
FALLBACK_MODELS = ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"]
# ์˜ˆ์‹œ ์‹œ๋“œ ๋‹จ์–ด๋“ค
EXAMPLE_SEEDS = ["๊ณจ๋“ ", "์Šค๋งˆํŠธ", "๊ทธ๋ฆฐ", "ํ”„๋ฆฌ๋ฏธ์—„", "ํž๋ง", "ํŒŒ์›Œ", "ํ“จ์–ด", "๋“œ๋ฆผ"]
def _clean_json_response(text: str) -> str:
"""JSON ์‘๋‹ต ์ •์ œ"""
text = text.strip()
if text.startswith("```"):
lines = text.split("\n")
text = "\n".join(lines[1:-1]) if lines[-1] == "```" else "\n".join(lines[1:])
return text.strip()
def _validate_square_item(item: Dict) -> bool:
"""Square Theory ํ•ญ๋ชฉ ๊ฒ€์ฆ"""
required_fields = ["tl", "tr", "br", "bl", "top_phrase", "bottom_phrase", "slogan", "brand"]
if not all(field in item for field in required_fields):
return False
# ๋‹จ์–ด ๊ฒ€์ฆ (๊ณต๋ฐฑ ์—†๋Š” ๋‹จ์ผ ๋‹จ์–ด)
for corner in ["tl", "tr", "br", "bl"]:
if " " in item[corner].strip():
return False
return True
def _call_llm_with_retry(seed: str, temperature: float = 0.8) -> List[Dict]:
"""LLM ํ˜ธ์ถœ (์žฌ์‹œ๋„ ๋กœ์ง ํฌํ•จ)"""
last_error = None
for model in FALLBACK_MODELS:
try:
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": ENHANCED_SYSTEM_PROMPT},
{"role": "user", "content": f"์‹œ๋“œ ๋‹จ์–ด: {seed}"}
],
temperature=temperature,
max_tokens=3000,
response_format={"type": "json_object"}
)
content = _clean_json_response(response.choices[0].message.content)
data = json.loads(content)
# ์‘๋‹ต ํ˜•์‹ ์ •๊ทœํ™”
if "suggestions" in data:
results = data["suggestions"]
elif "items" in data:
results = data["items"]
elif isinstance(data, list):
results = data
else:
results = [data]
# ๊ฒ€์ฆ ๋ฐ ํ•„ํ„ฐ๋ง
valid_results = [item for item in results if _validate_square_item(item)]
if not valid_results:
raise ValueError("์œ ํšจํ•œ Square Theory ์ œ์•ˆ์ด ์—†์Šต๋‹ˆ๋‹ค")
# ์ ์ˆ˜ ๊ธฐ์ค€ ์ •๋ ฌ (์—†์œผ๋ฉด ์ˆœ์„œ ์œ ์ง€)
valid_results.sort(key=lambda x: x.get("concept_score", 0), reverse=True)
return valid_results[:20] # ์ตœ๋Œ€ 20๊ฐœ
except Exception as e:
last_error = e
continue
raise RuntimeError(f"๋ชจ๋“  ๋ชจ๋ธ์—์„œ ์‹คํŒจ: {last_error}")
# -------------------------------------------------
# 2. ์‹œ๊ฐํ™” ํ•จ์ˆ˜
# -------------------------------------------------
def create_square_preview(item: Dict) -> str:
"""Square Theory ์‹œ๊ฐ์  ํ”„๋ฆฌ๋ทฐ ์ƒ์„ฑ"""
return f"""
<div style="border: 2px solid #333; border-radius: 8px; padding: 20px; margin: 10px 0; background: #f9f9f9;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
<div style="text-align: center; padding: 15px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<strong style="color: #e74c3c;">{item['tl']}</strong>
</div>
<div style="text-align: center; padding: 15px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<strong style="color: #3498db;">{item['tr']}</strong>
</div>
<div style="text-align: center; padding: 15px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<strong style="color: #f39c12;">{item['bl']}</strong>
</div>
<div style="text-align: center; padding: 15px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<strong style="color: #27ae60;">{item['br']}</strong>
</div>
</div>
<div style="text-align: center; background: #2c3e50; color: white; padding: 15px; border-radius: 4px;">
<h3 style="margin: 0 0 10px 0; font-size: 1.5em;">{item['brand']}</h3>
<p style="margin: 5px 0; font-style: italic;">"{item['slogan']}"</p>
</div>
</div>
"""
# -------------------------------------------------
# 3. ๋ฉ”์ธ ์ƒ์„ฑ ํ•จ์ˆ˜
# -------------------------------------------------
def generate_squares(seed_word: str, temperature: float = 0.8) -> Tuple[str, List[Dict], str]:
"""Square Theory ์ œ์•ˆ ์ƒ์„ฑ"""
seed_word = seed_word.strip()
if not seed_word:
return "โš ๏ธ **์‹œ๋“œ ๋‹จ์–ด๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.**", [], ""
try:
# LLM ํ˜ธ์ถœ
results = _call_llm_with_retry(seed_word, temperature)
# ๋งˆํฌ๋‹ค์šด ์ƒ์„ฑ
markdown_parts = [
f"# ๐ŸŽฏ Square Theory ์ œ์•ˆ: '{seed_word}'",
f"*์ƒ์„ฑ ์‹œ๊ฐ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*",
f"\n## ๐Ÿ“Š ์ด {len(results)}๊ฐœ ์ œ์•ˆ\n"
]
# HTML ํ”„๋ฆฌ๋ทฐ ์ƒ์„ฑ
preview_html = ""
for idx, item in enumerate(results, 1):
score = item.get('concept_score', 'N/A')
target = item.get('target_audience', '์ผ๋ฐ˜')
markdown_parts.append(f"""
### {idx}. {item['brand']} {'โญ' * min(int(score) if isinstance(score, (int, float)) else 5, 5)}
**๐ŸŽฏ ํƒ€๊ฒŸ**: {target} | **์ ์ˆ˜**: {score}/10
**๐Ÿ“ ๋ฉ”์ธ ์นดํ”ผ**
- ์ƒ๋‹จ: *"{item['top_phrase']}"*
- ํ•˜๋‹จ: *"{item['bottom_phrase']}"*
**๐Ÿ’ฌ ์Šฌ๋กœ๊ฑด**
> {item['slogan']}
**๐Ÿ”ฒ Square ๊ตฌ์„ฑ**
```
[{item['tl']}] โ† โ†’ [{item['tr']}]
โ†‘ โ†“
[{item['bl']}] โ† โ†’ [{item['br']}]
```
---
""")
# ์ƒ์œ„ 3๊ฐœ๋Š” ํ”„๋ฆฌ๋ทฐ ์ถ”๊ฐ€
if idx <= 3:
preview_html += create_square_preview(item)
return "\n".join(markdown_parts), results, preview_html
except Exception as e:
return f"โŒ **์˜ค๋ฅ˜ ๋ฐœ์ƒ**: {str(e)}", [], ""
# -------------------------------------------------
# 4. ๋‚ด๋ณด๋‚ด๊ธฐ ํ•จ์ˆ˜
# -------------------------------------------------
def export_to_csv(results: List[Dict]) -> str:
"""CSV ํŒŒ์ผ ์ƒ์„ฑ"""
if not results:
return None
output = io.StringIO()
fieldnames = ["์ˆœ์œ„", "๋ธŒ๋žœ๋“œ๋ช…", "TL", "TR", "BR", "BL",
"์ƒ๋‹จ๋ฌธ๊ตฌ", "ํ•˜๋‹จ๋ฌธ๊ตฌ", "์Šฌ๋กœ๊ฑด", "์ ์ˆ˜", "ํƒ€๊ฒŸ"]
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
for idx, item in enumerate(results, 1):
writer.writerow({
"์ˆœ์œ„": idx,
"๋ธŒ๋žœ๋“œ๋ช…": item['brand'],
"TL": item['tl'],
"TR": item['tr'],
"BR": item['br'],
"BL": item['bl'],
"์ƒ๋‹จ๋ฌธ๊ตฌ": item['top_phrase'],
"ํ•˜๋‹จ๋ฌธ๊ตฌ": item['bottom_phrase'],
"์Šฌ๋กœ๊ฑด": item['slogan'],
"์ ์ˆ˜": item.get('concept_score', 'N/A'),
"ํƒ€๊ฒŸ": item.get('target_audience', '์ผ๋ฐ˜')
})
return output.getvalue()
def export_to_json(results: List[Dict]) -> str:
"""JSON ํŒŒ์ผ ์ƒ์„ฑ"""
if not results:
return None
export_data = {
"generated_at": datetime.now().isoformat(),
"total_count": len(results),
"results": results
}
return json.dumps(export_data, ensure_ascii=False, indent=2)
# -------------------------------------------------
# 5. Gradio UI
# -------------------------------------------------
# ์ „์—ญ ๋ณ€์ˆ˜๋กœ ๊ฒฐ๊ณผ ์ €์žฅ
current_results = []
def generate_and_update(seed_word, temperature):
"""์ƒ์„ฑ ๋ฐ UI ์—…๋ฐ์ดํŠธ"""
global current_results
markdown, results, preview = generate_squares(seed_word, temperature)
current_results = results
# ๋‚ด๋ณด๋‚ด๊ธฐ ํŒŒ์ผ ์ƒ์„ฑ
csv_content = export_to_csv(results) if results else None
json_content = export_to_json(results) if results else None
return markdown, preview, csv_content, json_content
# UI ๊ตฌ์„ฑ
with gr.Blocks(title="Square Theory Generator v12", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# ๐ŸŸง Square Theory Generator v12
### ๋ธŒ๋žœ๋“œ ์•„์ด๋ดํ‹ฐํ‹ฐ๋ฅผ ์œ„ํ•œ 4-Corner ์ „๋žต ๋„๊ตฌ
Square Theory๋Š” 4๊ฐœ์˜ ํ•ต์‹ฌ ๋‹จ์–ด๋ฅผ ์‚ฌ๊ฐํ˜• ๋ชจ์„œ๋ฆฌ์— ๋ฐฐ์น˜ํ•˜์—ฌ ๋ธŒ๋žœ๋“œ์˜ ์ •์ฒด์„ฑ๊ณผ ๊ฐ€์น˜๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๋Š” ๋ฐฉ๋ฒ•๋ก ์ž…๋‹ˆ๋‹ค.
""")
with gr.Row():
with gr.Column(scale=2):
seed_input = gr.Textbox(
label="๐ŸŒฑ ์‹œ๋“œ ๋‹จ์–ด (TL - ์™ผ์ชฝ ์ƒ๋‹จ)",
placeholder="์˜ˆ: ๊ณจ๋“ , ์Šค๋งˆํŠธ, ํ”„๋ฆฌ๋ฏธ์—„...",
info="๋ธŒ๋žœ๋“œ์˜ ํ•ต์‹ฌ์ด ๋  ๋‹จ์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
)
with gr.Row():
temperature_slider = gr.Slider(
minimum=0.1,
maximum=1.0,
value=0.8,
step=0.1,
label="์ฐฝ์˜์„ฑ ๋ ˆ๋ฒจ",
info="๋†’์„์ˆ˜๋ก ๋” ์ฐฝ์˜์ ์ด๊ณ  ๋‹ค์–‘ํ•œ ๊ฒฐ๊ณผ"
)
generate_btn = gr.Button("๐Ÿš€ Square Theory ์ƒ์„ฑ", variant="primary", size="lg")
gr.Examples(
examples=[[seed] for seed in EXAMPLE_SEEDS],
inputs=seed_input,
label="์˜ˆ์‹œ ์‹œ๋“œ ๋‹จ์–ด"
)
with gr.Column(scale=1):
gr.Markdown("""
### ๐Ÿ’ก ์‚ฌ์šฉ ํŒ
**์ข‹์€ ์‹œ๋“œ ๋‹จ์–ด:**
- ๊ฐ์ •: ํ–‰๋ณต, ํ‰ํ™”, ์—ด์ •
- ๊ฐ€์น˜: ํ˜์‹ , ์‹ ๋ขฐ, ํ’ˆ์งˆ
- ํŠน์„ฑ: ์Šค๋งˆํŠธ, ํ”„๋ฆฌ๋ฏธ์—„, ์—์ฝ”
**Square ๊ตฌ์กฐ:**
```
[์‹œ๋“œ] โ† โ†’ [๋Œ€๋น„/๋ณด์™„]
โ†‘ โ†“
[๊ฒฝํ—˜] โ† โ†’ [๊ฒฐ๊ณผ/ํ˜œํƒ]
```
""")
with gr.Tabs():
with gr.Tab("๐Ÿ“„ ๊ฒฐ๊ณผ"):
output_markdown = gr.Markdown()
with gr.Tab("๐ŸŽจ ์‹œ๊ฐ์  ํ”„๋ฆฌ๋ทฐ"):
preview_html = gr.HTML()
with gr.Tab("๐Ÿ’พ ๋‚ด๋ณด๋‚ด๊ธฐ"):
with gr.Row():
csv_file = gr.File(label="CSV ๋‹ค์šด๋กœ๋“œ", visible=False)
json_file = gr.File(label="JSON ๋‹ค์šด๋กœ๋“œ", visible=False)
export_info = gr.Markdown("""
์ƒ์„ฑ๋œ ๊ฒฐ๊ณผ๋Š” ์ž๋™์œผ๋กœ CSV์™€ JSON ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค.
์ƒ์„ฑ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•œ ํ›„ ํŒŒ์ผ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.
""")
# ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
def process_and_save(seed_word, temperature):
markdown, preview, csv_content, json_content = generate_and_update(seed_word, temperature)
# ํŒŒ์ผ ์ €์žฅ
csv_filename = json_filename = None
if csv_content:
csv_filename = f"square_theory_{seed_word}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
with open(csv_filename, 'w', encoding='utf-8-sig') as f:
f.write(csv_content)
if json_content:
json_filename = f"square_theory_{seed_word}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(json_filename, 'w', encoding='utf-8') as f:
f.write(json_content)
return markdown, preview, csv_filename, json_filename
generate_btn.click(
fn=process_and_save,
inputs=[seed_input, temperature_slider],
outputs=[output_markdown, preview_html, csv_file, json_file]
)
gr.Markdown("""
---
### ๐Ÿ“Œ ์ถ”๊ฐ€ ๊ฐœ๋ฐœ ์•„์ด๋””์–ด
1. **A/B ํ…Œ์ŠคํŠธ ๋ชจ๋“œ**: ๋™์ผ ์‹œ๋“œ๋กœ ์—ฌ๋Ÿฌ ๋ฒ„์ „ ์ƒ์„ฑ ํ›„ ๋น„๊ต
2. **ํ˜‘์—… ๊ธฐ๋Šฅ**: ์ƒ์„ฑ๋œ ๊ฒฐ๊ณผ์— ํŒ€์›๋“ค์ด ํˆฌํ‘œ/์ฝ”๋ฉ˜ํŠธ
3. **ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ**: ์ด์ „ ์ƒ์„ฑ ๊ฒฐ๊ณผ ์ €์žฅ ๋ฐ ๊ฒ€์ƒ‰
4. **์—…์ข…๋ณ„ ํ…œํ”Œ๋ฆฟ**: F&B, IT, ํŒจ์…˜ ๋“ฑ ์—…์ข…๋ณ„ ์ตœ์ ํ™”
5. **๋‹ค๊ตญ์–ด ์ง€์›**: ์˜์–ด, ์ค‘๊ตญ์–ด, ์ผ๋ณธ์–ด Square Theory
""")
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False
)