|
""" |
|
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 |
|
|
|
|
|
|
|
|
|
if not os.getenv("OPENAI_API_KEY"): |
|
raise EnvironmentError("OPENAI_API_KEY ํ๊ฒฝ ๋ณ์๋ฅผ ์ค์ ํ์ธ์.") |
|
|
|
client = OpenAI() |
|
|
|
|
|
|
|
|
|
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] |
|
|
|
except Exception as e: |
|
last_error = e |
|
continue |
|
|
|
raise RuntimeError(f"๋ชจ๋ ๋ชจ๋ธ์์ ์คํจ: {last_error}") |
|
|
|
|
|
|
|
|
|
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> |
|
""" |
|
|
|
|
|
|
|
|
|
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: |
|
|
|
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" |
|
] |
|
|
|
|
|
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']}] |
|
``` |
|
|
|
--- |
|
""") |
|
|
|
|
|
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)}", [], "" |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
) |