import gradio as gr import os import sys import json import requests # 현재 디렉토리와 폰트 파일 경로 설정 CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) FONT_PATH = os.path.join(CURRENT_DIR, 'NanumGothic-Regular.ttf') FONTS_CONF_PATH = os.path.join(CURRENT_DIR, 'fonts.conf') # fonts.conf 파일 생성 함수 def create_fonts_conf(): """Graphviz가 로컬 폰트를 인식할 수 있도록 fonts.conf 파일 생성""" fonts_conf_content = f""" {CURRENT_DIR} NanumGothic NanumGothic-Regular NanumGothic-Regular NanumGothic-Regular NanumGothic NanumGothic-Regular """ with open(FONTS_CONF_PATH, 'w', encoding='utf-8') as f: f.write(fonts_conf_content) print(f"fonts.conf 파일 생성됨: {FONTS_CONF_PATH}") # 폰트 설정 if os.path.exists(FONT_PATH): print(f"한글 폰트 파일 발견: {FONT_PATH}") # fonts.conf 파일 생성 create_fonts_conf() # Graphviz가 폰트를 찾을 수 있도록 환경 변수 설정 os.environ['GDFONTPATH'] = CURRENT_DIR os.environ['FONTCONFIG_PATH'] = CURRENT_DIR os.environ['FONTCONFIG_FILE'] = FONTS_CONF_PATH print(f"GDFONTPATH 설정: {CURRENT_DIR}") print(f"FONTCONFIG_FILE 설정: {FONTS_CONF_PATH}") else: print(f"경고: 한글 폰트 파일을 찾을 수 없습니다: {FONT_PATH}") # 환경 변수로 폰트 경로 저장 (generator들이 사용할 수 있도록) os.environ['KOREAN_FONT_PATH'] = FONT_PATH from concept_map_generator import generate_concept_map from synoptic_chart_generator import generate_synoptic_chart from radial_diagram_generator import generate_radial_diagram from process_flow_generator import generate_process_flow_diagram from wbs_diagram_generator import generate_wbs_diagram from sample_data import CONCEPT_MAP_JSON, SYNOPTIC_CHART_JSON, RADIAL_DIAGRAM_JSON, PROCESS_FLOW_JSON, WBS_DIAGRAM_JSON # LLM API 함수 def call_llm_api(prompt, diagram_type): """Friendli AI API를 호출하여 JSON 생성""" token = os.environ.get("FRIENDLI_TOKEN") or "YOUR_FRIENDLI_TOKEN" url = "https://api.friendli.ai/dedicated/v1/chat/completions" headers = { "Authorization": "Bearer " + token, "Content-Type": "application/json" } # 다이어그램 타입별 JSON 구조 가이드 - 실제 생성기가 요구하는 정확한 형식 json_guides = { "Concept Map": """Generate a JSON for a concept map with the EXACT following structure: { "central_node": "Main Topic", "nodes": [ { "id": "node1", "label": "First Concept", "relationship": "is part of", "subnodes": [ { "id": "node1_1", "label": "Sub Concept 1", "relationship": "includes", "subnodes": [] } ] }, { "id": "node2", "label": "Second Concept", "relationship": "relates to", "subnodes": [] } ] }""", "Synoptic Chart": """Generate a JSON for a synoptic chart with the EXACT following structure: { "central_node": "Chart Title", "nodes": [ { "id": "phase1", "label": "Phase 1 Name", "relationship": "starts with", "subnodes": [ { "id": "sub1_1", "label": "Sub Item 1", "relationship": "includes", "subnodes": [] } ] } ] }""", "Radial Diagram": """Generate a JSON for a radial diagram with the EXACT following structure: { "central_node": "Central Concept", "nodes": [ { "id": "branch1", "label": "Branch 1", "relationship": "connected to", "subnodes": [ { "id": "item1", "label": "Item 1", "relationship": "example", "subnodes": [] } ] } ] }""", "Process Flow": """Generate a JSON for a process flow diagram with the EXACT following structure: { "start_node": "Start Process", "nodes": [ {"id": "step1", "label": "First Step", "type": "process"}, {"id": "step2", "label": "Decision Point", "type": "decision"}, {"id": "step3", "label": "Another Step", "type": "process"}, {"id": "end", "label": "End Process", "type": "end"} ], "connections": [ {"from": "start_node", "to": "step1", "label": "Begin"}, {"from": "step1", "to": "step2", "label": "Next"}, {"from": "step2", "to": "step3", "label": "Yes"}, {"from": "step3", "to": "end", "label": "Complete"} ] }""", "WBS Diagram": """Generate a JSON for a WBS diagram with the EXACT following structure: { "project_title": "Project Name", "phases": [ { "id": "phase1", "label": "Phase 1", "tasks": [ { "id": "task1_1", "label": "Task 1.1", "subtasks": [ { "id": "subtask1_1_1", "label": "Subtask 1.1.1", "sub_subtasks": [] } ] } ] } ] }""" } system_prompt = f"""You are a helpful assistant that generates JSON structures for diagrams. {json_guides.get(diagram_type, '')} Important rules: 1. Generate ONLY valid JSON without any explanation or markdown formatting 2. The JSON must follow the EXACT structure shown above - do not change field names 3. Make the content relevant to the user's prompt 4. Use the user's language (Korean or English) for the content values 5. For IDs, use simple alphanumeric strings without spaces (e.g., "node1", "task1_1") 6. Ensure all connections reference existing node IDs 7. For Process Flow: 'type' can be: "process", "decision", "start", "end", "io" 8. For nested structures (Concept Map, Synoptic Chart, Radial, WBS), include empty 'subnodes' or 'subtasks' arrays when there are no children 9. Do not add any additional fields not shown in the example structure""" payload = { "model": "dep89a2fld32mcm", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"Create a {diagram_type} JSON for: {prompt}"} ], "max_tokens": 16384, "top_p": 0.8, "stream": False # 간단하게 처리하기 위해 stream을 False로 설정 } try: response = requests.post(url, json=payload, headers=headers, timeout=30) # API 응답 상태 확인 if response.status_code != 200: return json.dumps({"error": f"API returned status code {response.status_code}: {response.text}"}) response_data = response.json() if 'choices' in response_data and len(response_data['choices']) > 0: content = response_data['choices'][0]['message']['content'] # JSON 부분만 추출 (마크다운 코드 블록 제거) content = content.strip() if content.startswith("```json"): content = content[7:] if content.startswith("```"): content = content[3:] if content.endswith("```"): content = content[:-3] # 추가적인 텍스트 제거 (JSON 외의 설명이 있을 경우) content = content.strip() # JSON 시작 위치 찾기 json_start = content.find('{') if json_start != -1: content = content[json_start:] # JSON 끝 위치 찾기 bracket_count = 0 json_end = -1 for i, char in enumerate(content): if char == '{': bracket_count += 1 elif char == '}': bracket_count -= 1 if bracket_count == 0: json_end = i break if json_end != -1: content = content[:json_end + 1] return content.strip() else: return json.dumps({"error": "No response from LLM"}) except requests.exceptions.Timeout: return json.dumps({"error": "Request timed out"}) except requests.exceptions.RequestException as e: print(f"LLM API Request Error: {str(e)}") return json.dumps({"error": f"Request failed: {str(e)}"}) except Exception as e: print(f"LLM API Error: {str(e)}") return json.dumps({"error": str(e)}) def generate_with_llm(prompt, diagram_type, output_format): """LLM으로 JSON을 생성하고 다이어그램을 생성""" if not prompt: return None, "Please enter a prompt" # LLM으로 JSON 생성 generated_json = call_llm_api(prompt, diagram_type) try: # JSON 유효성 검사 json_data = json.loads(generated_json) json_str = json.dumps(json_data, indent=2, ensure_ascii=False) # JSON 구조 검증 if diagram_type in ["Concept Map", "Synoptic Chart", "Radial Diagram"]: if "central_node" not in json_data or "nodes" not in json_data: return None, f"Invalid JSON structure for {diagram_type}. Missing 'central_node' or 'nodes'. Generated JSON:\n{json_str}" elif diagram_type == "Process Flow": if "start_node" not in json_data or "nodes" not in json_data or "connections" not in json_data: return None, f"Invalid JSON structure for Process Flow. Missing 'start_node', 'nodes', or 'connections'. Generated JSON:\n{json_str}" elif diagram_type == "WBS Diagram": if "project_title" not in json_data or "phases" not in json_data: return None, f"Invalid JSON structure for WBS. Missing 'project_title' or 'phases'. Generated JSON:\n{json_str}" # 다이어그램 생성 try: if diagram_type == "Concept Map": diagram = generate_concept_map(json_str, output_format) elif diagram_type == "Synoptic Chart": diagram = generate_synoptic_chart(json_str, output_format) elif diagram_type == "Radial Diagram": diagram = generate_radial_diagram(json_str, output_format) elif diagram_type == "Process Flow": diagram = generate_process_flow_diagram(json_str, output_format) elif diagram_type == "WBS Diagram": diagram = generate_wbs_diagram(json_str, output_format) else: return None, "Invalid diagram type" # 에러 메시지가 반환된 경우 if isinstance(diagram, str) and diagram.startswith("Error:"): return None, f"Diagram generation error: {diagram}\n\nGenerated JSON:\n{json_str}" return diagram, json_str except Exception as e: return None, f"Error generating diagram: {str(e)}\n\nGenerated JSON:\n{json_str}" except json.JSONDecodeError as e: return None, f"Invalid JSON generated: {str(e)}\n\nGenerated content:\n{generated_json}" except Exception as e: return None, f"Unexpected error: {str(e)}\n\nGenerated content:\n{generated_json}" if __name__ == "__main__": DEFAULT_BASE_COLOR = '#19191a' with gr.Blocks( title="Advanced Graph Generator", theme=gr.themes.Soft( primary_hue="violet", secondary_hue="purple", ), css=""" /* 그라디언트 배경 */ .gradio-container { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } /* 메인 컨테이너 스타일 */ .main-container { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 20px; padding: 30px; margin: 20px auto; max-width: 1400px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); } /* 헤더 스타일 */ .header-section { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; border-radius: 15px; margin-bottom: 30px; text-align: center; box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3); } .header-section h1 { font-size: 2.5em; font-weight: 700; margin-bottom: 10px; } .header-section p { font-size: 1.2em; opacity: 0.9; } /* 탭 스타일 */ .gr-tab-item { padding: 15px 30px; font-size: 1.1em; font-weight: 600; background: white; border-radius: 10px 10px 0 0; transition: all 0.3s ease; margin-right: 5px; } .gr-tab-item:hover { background: linear-gradient(135deg, #f5f7fa 0%, #e9ecef 100%); } .gr-tab-item.selected { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white !important; } /* 버튼 스타일 */ .gr-button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px 30px; font-size: 1.1em; font-weight: 600; border-radius: 10px; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); } .gr-button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); } .gr-button.primary { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); } /* 입력 필드 스타일 */ .gr-textbox, .gr-dropdown { border: 2px solid #e9ecef; border-radius: 10px; padding: 12px; font-size: 1em; transition: all 0.3s ease; background: white; } .gr-textbox:focus, .gr-dropdown:focus { border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } /* 패널 스타일 */ .panel-section { background: white; border-radius: 15px; padding: 25px; margin-bottom: 20px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); } /* 이미지 컨테이너 스타일 */ .gr-image { border-radius: 15px; overflow: hidden; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); } /* 예제 이미지 스타일 */ .example-images { gap: 20px; } .example-images .gr-image { transition: transform 0.3s ease; } .example-images .gr-image:hover { transform: scale(1.02); } /* 라디오 버튼 스타일 */ .gr-radio { background: white; padding: 15px; border-radius: 10px; border: 2px solid #e9ecef; } /* LLM 탭 특별 스타일 */ .llm-tab { background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%); padding: 30px; border-radius: 15px; } .llm-input-section { background: white; padding: 25px; border-radius: 15px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); margin-bottom: 20px; } /* 반응형 디자인 */ @media (max-width: 768px) { .main-container { padding: 15px; margin: 10px; } .header-section h1 { font-size: 2em; } .gr-tab-item { padding: 10px 15px; font-size: 1em; } } """ ) as demo: with gr.Column(elem_classes=["main-container"]): with gr.Column(elem_classes=["header-section"]): gr.Markdown( """ # 🎨 ChartGPT : AI-Powered Multi Diagram Generator ### based LLM 'Gemma-3-R1984-27B' Model. Powered by VIDraft⚡ """ ) with gr.Row(variant="panel", elem_classes=["panel-section"]): output_format_radio = gr.Radio( choices=["png", "svg"], value="png", label="📁 Output Format", interactive=True ) with gr.Tabs(): # AI 어시스턴트 탭 (첫 번째) with gr.TabItem("🤖 AI Assistant", elem_classes=["llm-tab"]): gr.Markdown( """ ### 💡 Describe your diagram in Korean or English, and AI will create it for you! """ ) with gr.Row(): with gr.Column(scale=1, elem_classes=["llm-input-section"]): prompt_input = gr.Textbox( placeholder="예: '머신러닝 프로세스를 보여주는 플로우차트를 만들어줘' or 'Create a concept map about climate change'", label="📝 Enter your prompt", lines=3 ) diagram_type_select = gr.Dropdown( choices=["Concept Map", "Synoptic Chart", "Radial Diagram", "Process Flow", "WBS Diagram"], value="Concept Map", label="📊 Select Diagram Type", interactive=True ) generate_btn = gr.Button("✨ Generate with AI", variant="primary", size="lg") generated_json_output = gr.Textbox( label="📄 Generated JSON", lines=15, interactive=True, visible=True ) with gr.Column(scale=2): ai_output_image = gr.Image( label="🎨 Generated Diagram", type="filepath", show_download_button=True, height=600 ) generate_btn.click( fn=generate_with_llm, inputs=[prompt_input, diagram_type_select, output_format_radio], outputs=[ai_output_image, generated_json_output] ) with gr.Row(elem_classes=["panel-section"]): gr.Examples( examples=[ ["소프트웨어 개발 생명주기를 보여주는 프로세스 플로우를 만들어줘", "Process Flow"], ["인공지능의 종류와 응용 분야에 대한 컨셉맵을 만들어줘", "Concept Map"], ["온라인 쇼핑몰 구축 프로젝트의 WBS를 만들어줘", "WBS Diagram"], ["재생 가능 에너지의 종류를 중심으로 방사형 다이어그램을 만들어줘", "Radial Diagram"], ["머신러닝 파이프라인의 단계별 구성을 시놉틱 차트로 보여줘", "Synoptic Chart"] ], inputs=[prompt_input, diagram_type_select], label="💭 Example Prompts" ) # 기존 탭들 with gr.TabItem("🗺️ Concept Map"): with gr.Row(): with gr.Column(scale=1, elem_classes=["panel-section"]): json_input_cm = gr.Textbox( value=CONCEPT_MAP_JSON, placeholder="Paste JSON following the documented format", label="JSON Input", lines=20 ) submit_btn_cm = gr.Button("Generate Concept Map", variant="primary") with gr.Column(scale=2): output_cm = gr.Image( label="Generated Diagram", type="filepath", show_download_button=True, height=500 ) submit_btn_cm.click( fn=generate_concept_map, inputs=[json_input_cm, output_format_radio], outputs=output_cm ) gr.Markdown("## 📸 Examples") with gr.Row(elem_classes=["example-images"]): gr.Image(value="./images/cm1.svg", label="Sample 1", show_label=True, interactive=False) gr.Image(value="./images/cm2.svg", label="Sample 2", show_label=True, interactive=False) with gr.TabItem("📊 Synoptic Chart"): with gr.Row(): with gr.Column(scale=1, elem_classes=["panel-section"]): json_input_sc = gr.Textbox( value=SYNOPTIC_CHART_JSON, placeholder="Paste JSON following the documented format", label="JSON Input", lines=20 ) submit_btn_sc = gr.Button("Generate Synoptic Chart", variant="primary") with gr.Column(scale=2): output_sc = gr.Image( label="Generated Diagram", type="filepath", show_download_button=True, height=500 ) submit_btn_sc.click( fn=generate_synoptic_chart, inputs=[json_input_sc, output_format_radio], outputs=output_sc ) gr.Markdown("## 📸 Examples") with gr.Row(elem_classes=["example-images"]): gr.Image(value="./images/sc1.svg", label="Sample 1", show_label=True, interactive=False) gr.Image(value="./images/sc2.svg", label="Sample 2", show_label=True, interactive=False) with gr.TabItem("☀️ Radial Diagram"): with gr.Row(): with gr.Column(scale=1, elem_classes=["panel-section"]): json_input_rd = gr.Textbox( value=RADIAL_DIAGRAM_JSON, placeholder="Paste JSON following the documented format", label="JSON Input", lines=20 ) submit_btn_rd = gr.Button("Generate Radial Diagram", variant="primary") with gr.Column(scale=2): output_rd = gr.Image( label="Generated Diagram", type="filepath", show_download_button=True, height=500 ) submit_btn_rd.click( fn=generate_radial_diagram, inputs=[json_input_rd, output_format_radio], outputs=output_rd ) gr.Markdown("## 📸 Examples") with gr.Row(elem_classes=["example-images"]): gr.Image(value="./images/rd1.svg", label="Sample 1", show_label=True, interactive=False) gr.Image(value="./images/rd2.svg", label="Sample 2", show_label=True, interactive=False) gr.Image(value="./images/rd3.svg", label="Sample 3", show_label=True, interactive=False) gr.Image(value="./images/rd4.svg", label="Sample 4", show_label=True, interactive=False) with gr.TabItem("🔄 Process Flow"): with gr.Row(): with gr.Column(scale=1, elem_classes=["panel-section"]): json_input_pf = gr.Textbox( value=PROCESS_FLOW_JSON, placeholder="Paste JSON following the documented format", label="JSON Input", lines=20 ) submit_btn_pf = gr.Button("Generate Process Flow", variant="primary") with gr.Column(scale=2): output_pf = gr.Image( label="Generated Diagram", type="filepath", show_download_button=True, height=500 ) submit_btn_pf.click( fn=generate_process_flow_diagram, inputs=[json_input_pf, output_format_radio], outputs=output_pf ) gr.Markdown("## 📸 Examples") with gr.Row(elem_classes=["example-images"]): gr.Image(value="./images/pf1.svg", label="Sample 1", show_label=True, interactive=False) gr.Image(value="./images/pf2.svg", label="Sample 2", show_label=True, interactive=False) with gr.TabItem("📋 WBS Diagram"): with gr.Row(): with gr.Column(scale=1, elem_classes=["panel-section"]): json_input_wbs = gr.Textbox( value=WBS_DIAGRAM_JSON, placeholder="Paste JSON following the documented format", label="JSON Input", lines=20 ) submit_btn_wbs = gr.Button("Generate WBS Diagram", variant="primary") with gr.Column(scale=2): output_wbs = gr.Image( label="Generated Diagram", type="filepath", show_download_button=True, height=500 ) submit_btn_wbs.click( fn=generate_wbs_diagram, inputs=[json_input_wbs, output_format_radio], outputs=output_wbs ) gr.Markdown("## 📸 Examples") with gr.Row(elem_classes=["example-images"]): gr.Image(value="./images/wd1.svg", label="Sample 1", show_label=True, interactive=False) gr.Image(value="./images/wd2.svg", label="Sample 2", show_label=True, interactive=False) demo.launch( mcp_server=True, share=False, server_port=7860, server_name="0.0.0.0" )