ginipick commited on
Commit
2755a30
ยท
verified ยท
1 Parent(s): f4b3178

Update process_flow_generator.py

Browse files
Files changed (1) hide show
  1. process_flow_generator.py +207 -164
process_flow_generator.py CHANGED
@@ -3,84 +3,70 @@ import json
3
  from tempfile import NamedTemporaryFile
4
  import os
5
  from PIL import Image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  def generate_process_flow_diagram(json_input: str, output_format: str) -> str:
8
  """
9
  Generates a Process Flow Diagram (Flowchart) from JSON input.
10
- Args:
11
- json_input (str): A JSON string describing the process flow structure.
12
- It must follow the Expected JSON Format Example below.
13
- Expected JSON Format Example:
14
- {
15
- "start_node": "Start Inference Request",
16
- "nodes": [
17
- {
18
- "id": "user_input",
19
- "label": "Receive User Input (Data)",
20
- "type": "io"
21
- },
22
- {
23
- "id": "preprocess_data",
24
- "label": "Preprocess Data",
25
- "type": "process"
26
- },
27
- {
28
- "id": "validate_data",
29
- "label": "Validate Data Format/Type",
30
- "type": "decision"
31
- },
32
- {
33
- "id": "data_valid_yes",
34
- "label": "Data Valid?",
35
- "type": "decision"
36
- },
37
- {
38
- "id": "load_model",
39
- "label": "Load AI Model (if not cached)",
40
- "type": "process"
41
- },
42
- {
43
- "id": "run_inference",
44
- "label": "Run AI Model Inference",
45
- "type": "process"
46
- },
47
- {
48
- "id": "postprocess_output",
49
- "label": "Postprocess Model Output",
50
- "type": "process"
51
- },
52
- {
53
- "id": "send_response",
54
- "label": "Send Response to User",
55
- "type": "io"
56
- },
57
- {
58
- "id": "log_error",
59
- "label": "Log Error & Notify User",
60
- "type": "process"
61
- },
62
- {
63
- "id": "end_inference_process",
64
- "label": "End Inference Process",
65
- "type": "end"
66
- }
67
- ],
68
- "connections": [
69
- {"from": "start_node", "to": "user_input", "label": "Request"},
70
- {"from": "user_input", "to": "preprocess_data", "label": "Data Received"},
71
- {"from": "preprocess_data", "to": "validate_data", "label": "Cleaned"},
72
- {"from": "validate_data", "to": "data_valid_yes", "label": "Check"},
73
- {"from": "data_valid_yes", "to": "load_model", "label": "Yes"},
74
- {"from": "data_valid_yes", "to": "log_error", "label": "No"},
75
- {"from": "load_model", "to": "run_inference", "label": "Model Ready"},
76
- {"from": "run_inference", "to": "postprocess_output", "label": "Output Generated"},
77
- {"from": "postprocess_output", "to": "send_response", "label": "Ready"},
78
- {"from": "send_response", "to": "end_inference_process", "label": "Response Sent"},
79
- {"from": "log_error", "to": "end_inference_process", "label": "Error Handled"}
80
- ]
81
- }
82
- Returns:
83
- str: The filepath to the generated PNG image file.
84
  """
85
  try:
86
  if not json_input.strip():
@@ -103,33 +89,41 @@ def generate_process_flow_diagram(json_input: str, output_format: str) -> str:
103
  "default": "box" # Fallback
104
  }
105
 
106
- # ํ•œ๊ธ€ ํฐํŠธ ์„ค์ •
107
- # GDFONTPATH๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉด ํฐํŠธ ํŒŒ์ผ๋ช…(ํ™•์žฅ์ž ์ œ์™ธ) ์‚ฌ์šฉ
108
- korean_font = 'NanumGothic-Regular'
109
-
110
  dot = graphviz.Digraph(
111
  name='ProcessFlowDiagram',
112
  format='png',
 
113
  graph_attr={
114
- 'rankdir': 'TB', # Top-to-Bottom flow is common for flowcharts
115
  'splines': 'ortho', # Straight lines with 90-degree bends
116
- 'bgcolor': 'white', # White background
117
- 'pad': '0.5', # Padding around the graph
118
- 'nodesep': '0.6', # Spacing between nodes on same rank
119
- 'ranksep': '0.8', # Spacing between ranks
120
- 'fontname': korean_font, # ๊ทธ๋ž˜ํ”„ ์ „์ฒด ํ•œ๊ธ€ ํฐํŠธ
121
- 'charset': 'UTF-8' # UTF-8 ์ธ์ฝ”๋”ฉ
 
 
 
 
122
  },
123
  node_attr={
124
- 'fontname': korean_font # ๋ชจ๋“  ๋…ธ๋“œ์˜ ๊ธฐ๋ณธ ํฐํŠธ
 
 
 
 
 
125
  },
126
  edge_attr={
127
- 'fontname': korean_font # ๋ชจ๋“  ์—ฃ์ง€์˜ ๊ธฐ๋ณธ ํฐํŠธ
 
 
128
  }
129
  )
130
 
131
- base_color = '#19191a' # Hardcoded base color
132
-
133
  fill_color_for_nodes = base_color
134
  font_color_for_nodes = 'white' if base_color == '#19191a' or base_color.lower() in ['#000000', '#19191a'] else 'black'
135
 
@@ -140,38 +134,46 @@ def generate_process_flow_diagram(json_input: str, output_format: str) -> str:
140
  start_node_id = data['start_node']
141
  dot.node(
142
  start_node_id,
143
- start_node_id, # Label is typically the ID itself for start/end
144
  shape=node_shapes['start'],
145
  style='filled,rounded',
146
- fillcolor='#2196F3', # A distinct blue for Start
147
  fontcolor='white',
148
- fontsize='14',
149
- fontname=korean_font # ํ•œ๊ธ€ ํฐํŠธ ์ถ”๊ฐ€
 
150
  )
151
 
152
- # Add all other nodes (process, decision, etc.)
153
  for node_id, node_info in all_defined_nodes.items():
154
- if node_id == start_node_id: # Skip if it's the start node, already added
155
  continue
156
 
157
  node_type = node_info.get("type", "default")
158
  shape = node_shapes.get(node_type, "box")
159
-
160
  node_label = node_info['label']
161
 
162
- # Use distinct color for end node if it exists
 
 
 
 
 
 
 
163
  if node_type == 'end':
164
- dot.node(
165
  node_id,
166
  node_label,
167
  shape=shape,
168
  style='filled,rounded',
169
- fillcolor='#F44336', # A distinct red for End
170
  fontcolor='white',
171
- fontsize='14',
172
- fontname=korean_font # ํ•œ๊ธ€ ํฐํŠธ ์ถ”๊ฐ€
 
173
  )
174
- else: # Regular process, decision, etc. nodes use the selected base color
175
  dot.node(
176
  node_id,
177
  node_label,
@@ -179,8 +181,9 @@ def generate_process_flow_diagram(json_input: str, output_format: str) -> str:
179
  style='filled,rounded',
180
  fillcolor=fill_color_for_nodes,
181
  fontcolor=font_color_for_nodes,
182
- fontsize='14',
183
- fontname=korean_font # ํ•œ๊ธ€ ํฐํŠธ ๏ฟฝ๏ฟฝ๏ฟฝ๊ฐ€
 
184
  )
185
 
186
  # Add connections (edges)
@@ -189,117 +192,157 @@ def generate_process_flow_diagram(json_input: str, output_format: str) -> str:
189
  connection['from'],
190
  connection['to'],
191
  label=connection.get('label', ''),
192
- color='#4a4a4a', # Dark gray for lines
193
  fontcolor='#4a4a4a',
194
- fontsize='10',
195
- fontname=korean_font # ํ•œ๊ธ€ ํฐํŠธ ์ถ”๊ฐ€
196
  )
197
 
 
198
  with NamedTemporaryFile(delete=False, suffix=f'.{output_format}') as tmp:
199
  dot.render(tmp.name, format=output_format, cleanup=True)
200
- return f"{tmp.name}.{output_format}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
  except json.JSONDecodeError:
203
  return "Error: Invalid JSON format"
204
  except Exception as e:
205
  return f"Error: {str(e)}"
206
 
207
- # PPT ํ†ตํ•ฉ์„ ์œ„ํ•œ ์ถ”๊ฐ€ ํ•จ์ˆ˜
208
  def generate_process_flow_for_ppt(topic: str, context: str, style: str = "Business Workflow") -> Image.Image:
209
  """
210
  PPT ์ƒ์„ฑ๊ธฐ์—์„œ ์‚ฌ์šฉํ•  ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
 
 
211
 
212
- Args:
213
- topic: ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ์ฃผ์ œ
214
- context: ์Šฌ๋ผ์ด๋“œ ์ปจํ…์ŠคํŠธ
215
- style: ์Šคํƒ€์ผ ํƒ€์ž…
216
 
217
- Returns:
218
- PIL Image ๊ฐ์ฒด
219
- """
220
- # ์ปจํ…์ŠคํŠธ์— ๋”ฐ๋ฅธ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ JSON ์ƒ์„ฑ
221
- if "ํ”„๋กœ์„ธ์Šค" in context or "process" in context.lower():
222
- # ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ
223
  flow_json = {
224
  "start_node": "์‹œ์ž‘",
225
  "nodes": [
226
- {"id": "analyze", "label": "ํ˜„ํ™ฉ ๋ถ„์„", "type": "process"},
227
- {"id": "plan", "label": "๊ณ„ํš ์ˆ˜๋ฆฝ", "type": "process"},
228
- {"id": "review", "label": "๊ฒ€ํ†  ํ•„์š”?", "type": "decision"},
229
- {"id": "implement", "label": "์‹คํ–‰", "type": "process"},
230
- {"id": "monitor", "label": "๋ชจ๋‹ˆํ„ฐ๋ง", "type": "process"},
231
- {"id": "complete", "label": "์™„๋ฃŒ", "type": "end"}
232
  ],
233
  "connections": [
234
- {"from": "์‹œ์ž‘", "to": "analyze", "label": ""},
235
- {"from": "analyze", "to": "plan", "label": "๋ถ„์„ ์™„๋ฃŒ"},
236
- {"from": "plan", "to": "review", "label": ""},
237
- {"from": "review", "to": "implement", "label": "์Šน์ธ"},
238
- {"from": "review", "to": "plan", "label": "์žฌ๊ฒ€ํ† "},
239
- {"from": "implement", "to": "monitor", "label": ""},
240
- {"from": "monitor", "to": "complete", "label": "๋ชฉํ‘œ ๋‹ฌ์„ฑ"}
241
  ]
242
  }
243
- elif "์ผ์ •" in context or "timeline" in context.lower():
244
- # ํ”„๋กœ์ ํŠธ ์ผ์ • ํ”Œ๋กœ์šฐ
245
  flow_json = {
246
- "start_node": "ํ”„๋กœ์ ํŠธ ์‹œ์ž‘",
247
  "nodes": [
248
- {"id": "phase1", "label": "1๋‹จ๊ณ„: ๊ธฐํš", "type": "process"},
249
- {"id": "milestone1", "label": "๊ธฐํš ๊ฒ€ํ† ", "type": "decision"},
250
- {"id": "phase2", "label": "2๋‹จ๊ณ„: ๊ฐœ๋ฐœ", "type": "process"},
251
- {"id": "phase3", "label": "3๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ", "type": "process"},
252
- {"id": "launch", "label": "๋Ÿฐ์นญ", "type": "io"},
253
- {"id": "end", "label": "ํ”„๋กœ์ ํŠธ ์ข…๋ฃŒ", "type": "end"}
254
  ],
255
  "connections": [
256
- {"from": "ํ”„๋กœ์ ํŠธ ์‹œ์ž‘", "to": "phase1", "label": ""},
257
- {"from": "phase1", "to": "milestone1", "label": "4์ฃผ"},
258
- {"from": "milestone1", "to": "phase2", "label": "์Šน์ธ"},
259
- {"from": "milestone1", "to": "phase1", "label": "๋ณด์™„"},
260
- {"from": "phase2", "to": "phase3", "label": "8์ฃผ"},
261
- {"from": "phase3", "to": "launch", "label": "2์ฃผ"},
262
- {"from": "launch", "to": "end", "label": ""}
263
  ]
264
  }
265
  else:
266
- # ๊ธฐ๋ณธ ์›Œํฌํ”Œ๋กœ์šฐ
267
  flow_json = {
268
  "start_node": "์‹œ์ž‘",
269
  "nodes": [
270
- {"id": "input", "label": "์ž…๋ ฅ ๋‹จ๊ณ„", "type": "io"},
271
- {"id": "process", "label": "์ฒ˜๋ฆฌ ๋‹จ๊ณ„", "type": "process"},
272
- {"id": "check", "label": "๊ฒ€์ฆ", "type": "decision"},
273
- {"id": "output", "label": "์ถœ๋ ฅ ๋‹จ๊ณ„", "type": "io"},
274
- {"id": "end", "label": "์ข…๋ฃŒ", "type": "end"}
275
  ],
276
  "connections": [
277
- {"from": "์‹œ์ž‘", "to": "input", "label": ""},
278
- {"from": "input", "to": "process", "label": "๋ฐ์ดํ„ฐ"},
279
- {"from": "process", "to": "check", "label": ""},
280
- {"from": "check", "to": "output", "label": "ํ†ต๊ณผ"},
281
- {"from": "check", "to": "process", "label": "์žฌ์ฒ˜๋ฆฌ"},
282
- {"from": "output", "to": "end", "label": ""}
283
  ]
284
  }
285
 
286
  # JSON์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
287
  json_str = json.dumps(flow_json, ensure_ascii=False)
288
 
 
 
289
  # ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
290
  png_path = generate_process_flow_diagram(json_str, 'png')
291
 
292
  if png_path.startswith("Error:"):
 
293
  # ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ๋ฐ˜ํ™˜
294
- from PIL import Image, ImageDraw, ImageFont
295
- img = Image.new('RGB', (800, 600), 'white')
296
  draw = ImageDraw.Draw(img)
297
- draw.text((400, 300), png_path, fill='red', anchor='mm')
 
 
 
 
 
 
 
 
 
298
  return img
299
 
 
 
300
  # PNG ํŒŒ์ผ์„ PIL Image๋กœ ๋ณ€ํ™˜
301
  with Image.open(png_path) as img:
302
- # ์ด๋ฏธ์ง€ ๋ณต์‚ฌ๋ณธ ์ƒ์„ฑ (ํŒŒ์ผ ๋‹ซ๊ธฐ ์œ„ํ•ด)
303
  img_copy = img.copy()
304
 
305
  # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
 
3
  from tempfile import NamedTemporaryFile
4
  import os
5
  from PIL import Image
6
+ import platform
7
+ import subprocess
8
+
9
+ def setup_korean_font_env():
10
+ """ํ•œ๊ธ€ ํฐํŠธ ํ™˜๊ฒฝ ์„ค์ •"""
11
+ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
12
+ FONT_PATH = os.path.join(CURRENT_DIR, 'NanumGothic-Regular.ttf')
13
+
14
+ # ํฐํŠธ ํŒŒ์ผ ์กด์žฌ ํ™•์ธ
15
+ if not os.path.exists(FONT_PATH):
16
+ print(f"[๊ฒฝ๊ณ ] ํ•œ๊ธ€ ํฐํŠธ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {FONT_PATH}")
17
+ return None
18
+
19
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •
20
+ os.environ['GDFONTPATH'] = CURRENT_DIR
21
+
22
+ # fonts.conf ์ƒ์„ฑ
23
+ fonts_conf_path = os.path.join(CURRENT_DIR, 'fonts.conf')
24
+ fonts_conf_content = f"""<?xml version="1.0"?>
25
+ <!DOCTYPE fontconfig SYSTEM "fonts.dtd">
26
+ <fontconfig>
27
+ <dir>{CURRENT_DIR}</dir>
28
+ <cachedir>/tmp/fontconfig-cache</cachedir>
29
+
30
+ <match target="pattern">
31
+ <test name="family">
32
+ <string>NanumGothic</string>
33
+ </test>
34
+ <edit name="family" mode="assign" binding="same">
35
+ <string>NanumGothic-Regular</string>
36
+ </edit>
37
+ </match>
38
+
39
+ <match target="pattern">
40
+ <test name="family">
41
+ <string>NanumGothic-Regular</string>
42
+ </test>
43
+ <edit name="file" mode="assign" binding="same">
44
+ <string>{FONT_PATH}</string>
45
+ </edit>
46
+ </match>
47
+
48
+ <alias binding="same">
49
+ <family>NanumGothic</family>
50
+ <default>
51
+ <family>NanumGothic-Regular</family>
52
+ </default>
53
+ </alias>
54
+ </fontconfig>"""
55
+
56
+ with open(fonts_conf_path, 'w', encoding='utf-8') as f:
57
+ f.write(fonts_conf_content)
58
+
59
+ os.environ['FONTCONFIG_FILE'] = fonts_conf_path
60
+ os.environ['FONTCONFIG_PATH'] = CURRENT_DIR
61
+
62
+ return FONT_PATH
63
+
64
+ # ํฐํŠธ ์„ค์ • ์ดˆ๊ธฐํ™”
65
+ FONT_PATH = setup_korean_font_env()
66
 
67
  def generate_process_flow_diagram(json_input: str, output_format: str) -> str:
68
  """
69
  Generates a Process Flow Diagram (Flowchart) from JSON input.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  """
71
  try:
72
  if not json_input.strip():
 
89
  "default": "box" # Fallback
90
  }
91
 
92
+ # Graphviz ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ - ํฌ๊ธฐ์™€ ์—ฌ๋ฐฑ ์กฐ์ •
 
 
 
93
  dot = graphviz.Digraph(
94
  name='ProcessFlowDiagram',
95
  format='png',
96
+ encoding='utf-8',
97
  graph_attr={
98
+ 'rankdir': 'TB', # Top-to-Bottom flow
99
  'splines': 'ortho', # Straight lines with 90-degree bends
100
+ 'bgcolor': 'white',
101
+ 'pad': '0.2', # ์ „์ฒด ํŒจ๋”ฉ ์ค„์ž„ (0.5 -> 0.2)
102
+ 'margin': '0.2', # ๋งˆ์ง„ ์ถ”๊ฐ€
103
+ 'nodesep': '0.4', # ๋…ธ๋“œ ๊ฐ„ ๊ฐ„๊ฒฉ ์ค„์ž„ (0.6 -> 0.4)
104
+ 'ranksep': '0.6', # ๋žญํฌ ๊ฐ„ ๊ฐ„๊ฒฉ ์ค„์ž„ (0.8 -> 0.6)
105
+ 'fontname': 'NanumGothic-Regular',
106
+ 'charset': 'UTF-8',
107
+ 'dpi': '96', # DPI ์ค„์ž„ (150 -> 96)
108
+ 'size': '10,7.5', # ์ „์ฒด ๊ทธ๋ž˜ํ”„ ํฌ๊ธฐ ์ œํ•œ (์ธ์น˜ ๋‹จ์œ„)
109
+ 'ratio': 'compress' # ๋น„์œจ ์••์ถ•
110
  },
111
  node_attr={
112
+ 'fontname': 'NanumGothic-Regular',
113
+ 'fontsize': '12', # ํฐํŠธ ํฌ๊ธฐ ์ค„์ž„ (14 -> 12)
114
+ 'charset': 'UTF-8',
115
+ 'height': '0.6', # ๋…ธ๋“œ ๋†’์ด ์ง€์ •
116
+ 'width': '2.0', # ๋…ธ๋“œ ๋„ˆ๋น„ ์ง€์ •
117
+ 'margin': '0.2,0.1' # ๋…ธ๋“œ ๋‚ด๋ถ€ ๋งˆ์ง„
118
  },
119
  edge_attr={
120
+ 'fontname': 'NanumGothic-Regular',
121
+ 'fontsize': '9', # ์—ฃ์ง€ ํฐํŠธ ํฌ๊ธฐ ์ค„์ž„ (10 -> 9)
122
+ 'charset': 'UTF-8'
123
  }
124
  )
125
 
126
+ base_color = '#19191a'
 
127
  fill_color_for_nodes = base_color
128
  font_color_for_nodes = 'white' if base_color == '#19191a' or base_color.lower() in ['#000000', '#19191a'] else 'black'
129
 
 
134
  start_node_id = data['start_node']
135
  dot.node(
136
  start_node_id,
137
+ start_node_id,
138
  shape=node_shapes['start'],
139
  style='filled,rounded',
140
+ fillcolor='#2196F3',
141
  fontcolor='white',
142
+ fontsize='12',
143
+ height='0.5',
144
+ width='1.8'
145
  )
146
 
147
+ # Add all other nodes
148
  for node_id, node_info in all_defined_nodes.items():
149
+ if node_id == start_node_id:
150
  continue
151
 
152
  node_type = node_info.get("type", "default")
153
  shape = node_shapes.get(node_type, "box")
 
154
  node_label = node_info['label']
155
 
156
+ # ๋…ธ๋“œ ํƒ€์ž…์— ๋”ฐ๋ฅธ ํฌ๊ธฐ ์กฐ์ •
157
+ if node_type == 'decision':
158
+ height = '0.8'
159
+ width = '1.5'
160
+ else:
161
+ height = '0.6'
162
+ width = '2.0'
163
+
164
  if node_type == 'end':
165
+ dot.node(
166
  node_id,
167
  node_label,
168
  shape=shape,
169
  style='filled,rounded',
170
+ fillcolor='#F44336',
171
  fontcolor='white',
172
+ fontsize='12',
173
+ height='0.5',
174
+ width='1.8'
175
  )
176
+ else:
177
  dot.node(
178
  node_id,
179
  node_label,
 
181
  style='filled,rounded',
182
  fillcolor=fill_color_for_nodes,
183
  fontcolor=font_color_for_nodes,
184
+ fontsize='12',
185
+ height=height,
186
+ width=width
187
  )
188
 
189
  # Add connections (edges)
 
192
  connection['from'],
193
  connection['to'],
194
  label=connection.get('label', ''),
195
+ color='#4a4a4a',
196
  fontcolor='#4a4a4a',
197
+ fontsize='9'
 
198
  )
199
 
200
+ # PNG๋กœ ์ง์ ‘ ๋ Œ๋”๋ง (ํฌ๊ธฐ ์กฐ์ •๋จ)
201
  with NamedTemporaryFile(delete=False, suffix=f'.{output_format}') as tmp:
202
  dot.render(tmp.name, format=output_format, cleanup=True)
203
+ png_path = f"{tmp.name}.{output_format}"
204
+
205
+ # ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ PIL๋กœ ์—ด์–ด์„œ ํฌ๊ธฐ ํ™•์ธ ๋ฐ ์กฐ์ •
206
+ from PIL import Image
207
+ with Image.open(png_path) as img:
208
+ width, height = img.size
209
+ print(f"[ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ] ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ํฌ๊ธฐ: {width}x{height}")
210
+
211
+ # ์ด๋ฏธ์ง€๊ฐ€ ๋„ˆ๋ฌด ํฌ๋ฉด ๋ฆฌ์‚ฌ์ด์ฆˆ
212
+ max_width = 1200
213
+ max_height = 900
214
+
215
+ if width > max_width or height > max_height:
216
+ # ๋น„์œจ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฆฌ์‚ฌ์ด์ฆˆ
217
+ ratio = min(max_width/width, max_height/height)
218
+ new_width = int(width * ratio)
219
+ new_height = int(height * ratio)
220
+
221
+ img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
222
+
223
+ # ์—ฌ๋ฐฑ ์ถ”๊ฐ€ํ•˜์—ฌ ์ค‘์•™ ์ •๋ ฌ
224
+ final_img = Image.new('RGB', (max_width, max_height), 'white')
225
+ x = (max_width - new_width) // 2
226
+ y = (max_height - new_height) // 2
227
+ final_img.paste(img_resized, (x, y))
228
+
229
+ # ์ƒˆ๋กœ์šด ํŒŒ์ผ๋กœ ์ €์žฅ
230
+ new_path = png_path.replace('.png', '_resized.png')
231
+ final_img.save(new_path)
232
+ os.unlink(png_path) # ์›๋ณธ ์‚ญ์ œ
233
+ return new_path
234
+
235
+ return png_path
236
 
237
  except json.JSONDecodeError:
238
  return "Error: Invalid JSON format"
239
  except Exception as e:
240
  return f"Error: {str(e)}"
241
 
 
242
  def generate_process_flow_for_ppt(topic: str, context: str, style: str = "Business Workflow") -> Image.Image:
243
  """
244
  PPT ์ƒ์„ฑ๊ธฐ์—์„œ ์‚ฌ์šฉํ•  ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
245
+ """
246
+ print(f"[ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ] ์ƒ์„ฑ ์‹œ์ž‘ - ์ฃผ์ œ: {topic}, ์ปจํ…์ŠคํŠธ: {context}")
247
 
248
+ # ํ•œ๊ธ€ ํฐํŠธ ์žฌ์„ค์ • (๋งค๋ฒˆ ํ™•์ธ)
249
+ setup_korean_font_env()
 
 
250
 
251
+ # ์ปจํ…์ŠคํŠธ ๋ถ„์„ํ•˜์—ฌ ์ ์ ˆํ•œ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ JSON ์ƒ์„ฑ
252
+ context_lower = context.lower()
253
+
254
+ # ๋…ธ๋“œ ์ˆ˜๋ฅผ ์ค„์ด๊ณ  ๋ ˆ์ด๋ธ”์„ ์งง๊ฒŒ ๋งŒ๋“ค์–ด ๊ณต๊ฐ„ ์ ˆ์•ฝ
255
+ if "ํ”„๋กœ์ ํŠธ" in context or "project" in context_lower:
 
256
  flow_json = {
257
  "start_node": "์‹œ์ž‘",
258
  "nodes": [
259
+ {"id": "plan", "label": "๊ธฐํš", "type": "process"},
260
+ {"id": "design", "label": "์„ค๊ณ„", "type": "process"},
261
+ {"id": "develop", "label": "๊ฐœ๋ฐœ", "type": "process"},
262
+ {"id": "test", "label": "ํ…Œ์ŠคํŠธ", "type": "decision"},
263
+ {"id": "deploy", "label": "๋ฐฐํฌ", "type": "process"},
264
+ {"id": "end", "label": "์™„๋ฃŒ", "type": "end"}
265
  ],
266
  "connections": [
267
+ {"from": "์‹œ์ž‘", "to": "plan", "label": ""},
268
+ {"from": "plan", "to": "design", "label": ""},
269
+ {"from": "design", "to": "develop", "label": ""},
270
+ {"from": "develop", "to": "test", "label": ""},
271
+ {"from": "test", "to": "deploy", "label": "ํ†ต๊ณผ"},
272
+ {"from": "test", "to": "develop", "label": "์ˆ˜์ •"},
273
+ {"from": "deploy", "to": "end", "label": ""}
274
  ]
275
  }
276
+ elif "์ž‘๋™" in context or "๊ธฐ๋Šฅ" in context:
 
277
  flow_json = {
278
+ "start_node": "์‹œ์ž‘",
279
  "nodes": [
280
+ {"id": "input", "label": "์ž…๋ ฅ", "type": "io"},
281
+ {"id": "validate", "label": "๊ฒ€์ฆ", "type": "decision"},
282
+ {"id": "process", "label": "์ฒ˜๋ฆฌ", "type": "process"},
283
+ {"id": "output", "label": "์ถœ๋ ฅ", "type": "io"},
284
+ {"id": "end", "label": "์ข…๋ฃŒ", "type": "end"}
 
285
  ],
286
  "connections": [
287
+ {"from": "์‹œ์ž‘", "to": "input", "label": ""},
288
+ {"from": "input", "to": "validate", "label": ""},
289
+ {"from": "validate", "to": "process", "label": "์œ ํšจ"},
290
+ {"from": "validate", "to": "input", "label": "์žฌ์ž…๋ ฅ"},
291
+ {"from": "process", "to": "output", "label": ""},
292
+ {"from": "output", "to": "end", "label": ""}
 
293
  ]
294
  }
295
  else:
296
+ # ๊ธฐ๋ณธ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ (๋” ๊ฐ„๋‹จํ•˜๊ฒŒ)
297
  flow_json = {
298
  "start_node": "์‹œ์ž‘",
299
  "nodes": [
300
+ {"id": "analyze", "label": "๋ถ„์„", "type": "process"},
301
+ {"id": "plan", "label": "๊ณ„ํš", "type": "process"},
302
+ {"id": "execute", "label": "์‹คํ–‰", "type": "process"},
303
+ {"id": "check", "label": "๊ฒ€ํ† ", "type": "decision"},
304
+ {"id": "complete", "label": "์™„๋ฃŒ", "type": "end"}
305
  ],
306
  "connections": [
307
+ {"from": "์‹œ์ž‘", "to": "analyze", "label": ""},
308
+ {"from": "analyze", "to": "plan", "label": ""},
309
+ {"from": "plan", "to": "execute", "label": ""},
310
+ {"from": "execute", "to": "check", "label": ""},
311
+ {"from": "check", "to": "complete", "label": "์Šน์ธ"},
312
+ {"from": "check", "to": "plan", "label": "์ˆ˜์ •"}
313
  ]
314
  }
315
 
316
  # JSON์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
317
  json_str = json.dumps(flow_json, ensure_ascii=False)
318
 
319
+ print(f"[ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ] JSON ์ƒ์„ฑ ์™„๋ฃŒ")
320
+
321
  # ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
322
  png_path = generate_process_flow_diagram(json_str, 'png')
323
 
324
  if png_path.startswith("Error:"):
325
+ print(f"[ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ] ์ƒ์„ฑ ์‹คํŒจ: {png_path}")
326
  # ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ๋ฐ˜ํ™˜
327
+ from PIL import ImageDraw, ImageFont
328
+ img = Image.new('RGB', (1200, 900), 'white')
329
  draw = ImageDraw.Draw(img)
330
+
331
+ try:
332
+ if FONT_PATH and os.path.exists(FONT_PATH):
333
+ font = ImageFont.truetype(FONT_PATH, 20)
334
+ else:
335
+ font = ImageFont.load_default()
336
+ except:
337
+ font = ImageFont.load_default()
338
+
339
+ draw.text((600, 450), "ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ ์ƒ์„ฑ ์‹คํŒจ", fill='red', anchor='mm', font=font)
340
  return img
341
 
342
+ print(f"[ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ] PNG ์ƒ์„ฑ ์„ฑ๊ณต: {png_path}")
343
+
344
  # PNG ํŒŒ์ผ์„ PIL Image๋กœ ๋ณ€ํ™˜
345
  with Image.open(png_path) as img:
 
346
  img_copy = img.copy()
347
 
348
  # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ