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 |