Spaces:
Running
Running
import graphviz | |
import json | |
from tempfile import NamedTemporaryFile | |
import os | |
from PIL import Image | |
import platform | |
import subprocess | |
def setup_korean_font_env(): | |
"""ํ๊ธ ํฐํธ ํ๊ฒฝ ์ค์ """ | |
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
FONT_PATH = os.path.join(CURRENT_DIR, 'NanumGothic-Regular.ttf') | |
# ํฐํธ ํ์ผ ์กด์ฌ ํ์ธ | |
if not os.path.exists(FONT_PATH): | |
print(f"[๊ฒฝ๊ณ ] ํ๊ธ ํฐํธ ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: {FONT_PATH}") | |
return None | |
# ํ๊ฒฝ ๋ณ์ ์ค์ | |
os.environ['GDFONTPATH'] = CURRENT_DIR | |
# fonts.conf ์์ฑ | |
fonts_conf_path = os.path.join(CURRENT_DIR, 'fonts.conf') | |
fonts_conf_content = f"""<?xml version="1.0"?> | |
<!DOCTYPE fontconfig SYSTEM "fonts.dtd"> | |
<fontconfig> | |
<dir>{CURRENT_DIR}</dir> | |
<cachedir>/tmp/fontconfig-cache</cachedir> | |
<match target="pattern"> | |
<test name="family"> | |
<string>NanumGothic</string> | |
</test> | |
<edit name="family" mode="assign" binding="same"> | |
<string>NanumGothic-Regular</string> | |
</edit> | |
</match> | |
<match target="pattern"> | |
<test name="family"> | |
<string>NanumGothic-Regular</string> | |
</test> | |
<edit name="file" mode="assign" binding="same"> | |
<string>{FONT_PATH}</string> | |
</edit> | |
</match> | |
<alias binding="same"> | |
<family>NanumGothic</family> | |
<default> | |
<family>NanumGothic-Regular</family> | |
</default> | |
</alias> | |
</fontconfig>""" | |
with open(fonts_conf_path, 'w', encoding='utf-8') as f: | |
f.write(fonts_conf_content) | |
os.environ['FONTCONFIG_FILE'] = fonts_conf_path | |
os.environ['FONTCONFIG_PATH'] = CURRENT_DIR | |
return FONT_PATH | |
# ํฐํธ ์ค์ ์ด๊ธฐํ | |
FONT_PATH = setup_korean_font_env() | |
def generate_process_flow_diagram(json_input: str, output_format: str) -> str: | |
""" | |
Generates a Process Flow Diagram (Flowchart) from JSON input. | |
""" | |
try: | |
if not json_input.strip(): | |
return "Error: Empty input" | |
data = json.loads(json_input) | |
# Validate required top-level keys for a flowchart | |
if 'start_node' not in data or 'nodes' not in data or 'connections' not in data: | |
raise ValueError("Missing required fields: 'start_node', 'nodes', or 'connections'") | |
# Define specific node shapes for flowchart types | |
node_shapes = { | |
"process": "box", # Rectangle for processes | |
"decision": "diamond", # Diamond for decisions | |
"start": "oval", # Oval for start | |
"end": "oval", # Oval for end | |
"io": "parallelogram", # Input/Output | |
"document": "note", # Document symbol | |
"default": "box" # Fallback | |
} | |
# Graphviz ๋ค์ด์ด๊ทธ๋จ ์์ฑ - ํฌ๊ธฐ์ ์ฌ๋ฐฑ ์กฐ์ | |
dot = graphviz.Digraph( | |
name='ProcessFlowDiagram', | |
format='png', | |
encoding='utf-8', | |
graph_attr={ | |
'rankdir': 'TB', # Top-to-Bottom flow | |
'splines': 'ortho', # Straight lines with 90-degree bends | |
'bgcolor': 'white', | |
'pad': '0.2', # ์ ์ฒด ํจ๋ฉ ์ค์ (0.5 -> 0.2) | |
'margin': '0.2', # ๋ง์ง ์ถ๊ฐ | |
'nodesep': '0.4', # ๋ ธ๋ ๊ฐ ๊ฐ๊ฒฉ ์ค์ (0.6 -> 0.4) | |
'ranksep': '0.6', # ๋ญํฌ ๊ฐ ๊ฐ๊ฒฉ ์ค์ (0.8 -> 0.6) | |
'fontname': 'NanumGothic-Regular', | |
'charset': 'UTF-8', | |
'dpi': '96', # DPI ์ค์ (150 -> 96) | |
'size': '10,7.5', # ์ ์ฒด ๊ทธ๋ํ ํฌ๊ธฐ ์ ํ (์ธ์น ๋จ์) | |
'ratio': 'compress' # ๋น์จ ์์ถ | |
}, | |
node_attr={ | |
'fontname': 'NanumGothic-Regular', | |
'fontsize': '12', # ํฐํธ ํฌ๊ธฐ ์ค์ (14 -> 12) | |
'charset': 'UTF-8', | |
'height': '0.6', # ๋ ธ๋ ๋์ด ์ง์ | |
'width': '2.0', # ๋ ธ๋ ๋๋น ์ง์ | |
'margin': '0.2,0.1' # ๋ ธ๋ ๋ด๋ถ ๋ง์ง | |
}, | |
edge_attr={ | |
'fontname': 'NanumGothic-Regular', | |
'fontsize': '9', # ์ฃ์ง ํฐํธ ํฌ๊ธฐ ์ค์ (10 -> 9) | |
'charset': 'UTF-8' | |
} | |
) | |
base_color = '#19191a' | |
fill_color_for_nodes = base_color | |
font_color_for_nodes = 'white' if base_color == '#19191a' or base_color.lower() in ['#000000', '#19191a'] else 'black' | |
# Store all nodes by ID for easy lookup | |
all_defined_nodes = {node['id']: node for node in data['nodes']} | |
# Add start node explicitly | |
start_node_id = data['start_node'] | |
dot.node( | |
start_node_id, | |
start_node_id, | |
shape=node_shapes['start'], | |
style='filled,rounded', | |
fillcolor='#2196F3', | |
fontcolor='white', | |
fontsize='12', | |
height='0.5', | |
width='1.8' | |
) | |
# Add all other nodes | |
for node_id, node_info in all_defined_nodes.items(): | |
if node_id == start_node_id: | |
continue | |
node_type = node_info.get("type", "default") | |
shape = node_shapes.get(node_type, "box") | |
node_label = node_info['label'] | |
# ๋ ธ๋ ํ์ ์ ๋ฐ๋ฅธ ํฌ๊ธฐ ์กฐ์ | |
if node_type == 'decision': | |
height = '0.8' | |
width = '1.5' | |
else: | |
height = '0.6' | |
width = '2.0' | |
if node_type == 'end': | |
dot.node( | |
node_id, | |
node_label, | |
shape=shape, | |
style='filled,rounded', | |
fillcolor='#F44336', | |
fontcolor='white', | |
fontsize='12', | |
height='0.5', | |
width='1.8' | |
) | |
else: | |
dot.node( | |
node_id, | |
node_label, | |
shape=shape, | |
style='filled,rounded', | |
fillcolor=fill_color_for_nodes, | |
fontcolor=font_color_for_nodes, | |
fontsize='12', | |
height=height, | |
width=width | |
) | |
# Add connections (edges) | |
for connection in data['connections']: | |
dot.edge( | |
connection['from'], | |
connection['to'], | |
label=connection.get('label', ''), | |
color='#4a4a4a', | |
fontcolor='#4a4a4a', | |
fontsize='9' | |
) | |
# PNG๋ก ์ง์ ๋ ๋๋ง (ํฌ๊ธฐ ์กฐ์ ๋จ) | |
with NamedTemporaryFile(delete=False, suffix=f'.{output_format}') as tmp: | |
dot.render(tmp.name, format=output_format, cleanup=True) | |
png_path = f"{tmp.name}.{output_format}" | |
# ์์ฑ๋ ์ด๋ฏธ์ง๋ฅผ PIL๋ก ์ด์ด์ ํฌ๊ธฐ ํ์ธ ๋ฐ ์กฐ์ | |
from PIL import Image | |
with Image.open(png_path) as img: | |
width, height = img.size | |
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] ์์ฑ๋ ์ด๋ฏธ์ง ํฌ๊ธฐ: {width}x{height}") | |
# ์ด๋ฏธ์ง๊ฐ ๋๋ฌด ํฌ๋ฉด ๋ฆฌ์ฌ์ด์ฆ | |
max_width = 1200 | |
max_height = 900 | |
if width > max_width or height > max_height: | |
# ๋น์จ ์ ์งํ๋ฉด์ ๋ฆฌ์ฌ์ด์ฆ | |
ratio = min(max_width/width, max_height/height) | |
new_width = int(width * ratio) | |
new_height = int(height * ratio) | |
img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS) | |
# ์ฌ๋ฐฑ ์ถ๊ฐํ์ฌ ์ค์ ์ ๋ ฌ | |
final_img = Image.new('RGB', (max_width, max_height), 'white') | |
x = (max_width - new_width) // 2 | |
y = (max_height - new_height) // 2 | |
final_img.paste(img_resized, (x, y)) | |
# ์๋ก์ด ํ์ผ๋ก ์ ์ฅ | |
new_path = png_path.replace('.png', '_resized.png') | |
final_img.save(new_path) | |
os.unlink(png_path) # ์๋ณธ ์ญ์ | |
return new_path | |
return png_path | |
except json.JSONDecodeError: | |
return "Error: Invalid JSON format" | |
except Exception as e: | |
return f"Error: {str(e)}" | |
def generate_process_flow_for_ppt(topic: str, context: str, style: str = "Business Workflow") -> Image.Image: | |
""" | |
PPT ์์ฑ๊ธฐ์์ ์ฌ์ฉํ ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ์์ฑ | |
""" | |
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] ์์ฑ ์์ - ์ฃผ์ : {topic}, ์ปจํ ์คํธ: {context}") | |
# ํ๊ธ ํฐํธ ์ฌ์ค์ (๋งค๋ฒ ํ์ธ) | |
setup_korean_font_env() | |
# ์ปจํ ์คํธ ๋ถ์ํ์ฌ ์ ์ ํ ํ๋ก์ธ์ค ํ๋ก์ฐ JSON ์์ฑ | |
context_lower = context.lower() | |
# ๋ ธ๋ ์๋ฅผ ์ค์ด๊ณ ๋ ์ด๋ธ์ ์งง๊ฒ ๋ง๋ค์ด ๊ณต๊ฐ ์ ์ฝ | |
if "ํ๋ก์ ํธ" in context or "project" in context_lower: | |
flow_json = { | |
"start_node": "์์", | |
"nodes": [ | |
{"id": "plan", "label": "๊ธฐํ", "type": "process"}, | |
{"id": "design", "label": "์ค๊ณ", "type": "process"}, | |
{"id": "develop", "label": "๊ฐ๋ฐ", "type": "process"}, | |
{"id": "test", "label": "ํ ์คํธ", "type": "decision"}, | |
{"id": "deploy", "label": "๋ฐฐํฌ", "type": "process"}, | |
{"id": "end", "label": "์๋ฃ", "type": "end"} | |
], | |
"connections": [ | |
{"from": "์์", "to": "plan", "label": ""}, | |
{"from": "plan", "to": "design", "label": ""}, | |
{"from": "design", "to": "develop", "label": ""}, | |
{"from": "develop", "to": "test", "label": ""}, | |
{"from": "test", "to": "deploy", "label": "ํต๊ณผ"}, | |
{"from": "test", "to": "develop", "label": "์์ "}, | |
{"from": "deploy", "to": "end", "label": ""} | |
] | |
} | |
elif "์๋" in context or "๊ธฐ๋ฅ" in context: | |
flow_json = { | |
"start_node": "์์", | |
"nodes": [ | |
{"id": "input", "label": "์ ๋ ฅ", "type": "io"}, | |
{"id": "validate", "label": "๊ฒ์ฆ", "type": "decision"}, | |
{"id": "process", "label": "์ฒ๋ฆฌ", "type": "process"}, | |
{"id": "output", "label": "์ถ๋ ฅ", "type": "io"}, | |
{"id": "end", "label": "์ข ๋ฃ", "type": "end"} | |
], | |
"connections": [ | |
{"from": "์์", "to": "input", "label": ""}, | |
{"from": "input", "to": "validate", "label": ""}, | |
{"from": "validate", "to": "process", "label": "์ ํจ"}, | |
{"from": "validate", "to": "input", "label": "์ฌ์ ๋ ฅ"}, | |
{"from": "process", "to": "output", "label": ""}, | |
{"from": "output", "to": "end", "label": ""} | |
] | |
} | |
else: | |
# ๊ธฐ๋ณธ ํ๋ก์ธ์ค ํ๋ก์ฐ (๋ ๊ฐ๋จํ๊ฒ) | |
flow_json = { | |
"start_node": "์์", | |
"nodes": [ | |
{"id": "analyze", "label": "๋ถ์", "type": "process"}, | |
{"id": "plan", "label": "๊ณํ", "type": "process"}, | |
{"id": "execute", "label": "์คํ", "type": "process"}, | |
{"id": "check", "label": "๊ฒํ ", "type": "decision"}, | |
{"id": "complete", "label": "์๋ฃ", "type": "end"} | |
], | |
"connections": [ | |
{"from": "์์", "to": "analyze", "label": ""}, | |
{"from": "analyze", "to": "plan", "label": ""}, | |
{"from": "plan", "to": "execute", "label": ""}, | |
{"from": "execute", "to": "check", "label": ""}, | |
{"from": "check", "to": "complete", "label": "์น์ธ"}, | |
{"from": "check", "to": "plan", "label": "์์ "} | |
] | |
} | |
# JSON์ ๋ฌธ์์ด๋ก ๋ณํ | |
json_str = json.dumps(flow_json, ensure_ascii=False) | |
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] JSON ์์ฑ ์๋ฃ") | |
# ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ์์ฑ | |
png_path = generate_process_flow_diagram(json_str, 'png') | |
if png_path.startswith("Error:"): | |
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] ์์ฑ ์คํจ: {png_path}") | |
# ์๋ฌ ๋ฐ์ ์ ๊ธฐ๋ณธ ์ด๋ฏธ์ง ๋ฐํ | |
from PIL import ImageDraw, ImageFont | |
img = Image.new('RGB', (1200, 900), 'white') | |
draw = ImageDraw.Draw(img) | |
try: | |
if FONT_PATH and os.path.exists(FONT_PATH): | |
font = ImageFont.truetype(FONT_PATH, 20) | |
else: | |
font = ImageFont.load_default() | |
except: | |
font = ImageFont.load_default() | |
draw.text((600, 450), "ํ๋ก์ธ์ค ํ๋ก์ฐ ์์ฑ ์คํจ", fill='red', anchor='mm', font=font) | |
return img | |
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] PNG ์์ฑ ์ฑ๊ณต: {png_path}") | |
# PNG ํ์ผ์ PIL Image๋ก ๋ณํ | |
with Image.open(png_path) as img: | |
img_copy = img.copy() | |
# ์์ ํ์ผ ์ญ์ | |
try: | |
os.unlink(png_path) | |
except: | |
pass | |
return img_copy |