GINI-Deck / process_flow_generator.py
ginipick's picture
Update process_flow_generator.py
2755a30 verified
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