Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -6,11 +6,32 @@ import json
|
|
6 |
import requests
|
7 |
from collections.abc import Iterator
|
8 |
from threading import Thread
|
|
|
|
|
|
|
|
|
9 |
|
10 |
import gradio as gr
|
11 |
from loguru import logger
|
12 |
import pandas as pd
|
13 |
import PyPDF2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
##############################################################################
|
16 |
# API Configuration
|
@@ -27,6 +48,282 @@ SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
|
|
27 |
if not SERPHOUSE_API_KEY:
|
28 |
logger.warning("SERPHOUSE_API_KEY not set. Web search functionality will be limited.")
|
29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
##############################################################################
|
31 |
# File Processing Constants
|
32 |
##############################################################################
|
@@ -182,11 +479,17 @@ def analyze_csv_file(path: str) -> str:
|
|
182 |
summary += f"**Showing**: Top {min(50, total_rows)} rows\n"
|
183 |
summary += f"**Columns**: {', '.join(df.columns)}\n\n"
|
184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
df_str = df.to_string()
|
186 |
if len(df_str) > MAX_CONTENT_CHARS:
|
187 |
df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
|
188 |
|
189 |
-
return f"**[CSV File: {os.path.basename(path)}]**\n\n{summary}{df_str}"
|
190 |
except Exception as e:
|
191 |
logger.error(f"CSV read error: {e}")
|
192 |
return f"Failed to read CSV file ({os.path.basename(path)}): {str(e)}"
|
@@ -256,127 +559,972 @@ def pdf_to_markdown(pdf_path: str) -> str:
|
|
256 |
return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
|
257 |
|
258 |
##############################################################################
|
259 |
-
#
|
260 |
##############################################################################
|
261 |
-
def
|
262 |
-
"""
|
263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
264 |
|
265 |
-
def is_video_file(file_path: str) -> bool:
|
266 |
-
"""Check if file is a video"""
|
267 |
-
return bool(re.search(r"\.(mp4|avi|mov|mkv)$", file_path, re.IGNORECASE))
|
268 |
|
269 |
-
|
270 |
-
|
271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
272 |
|
273 |
##############################################################################
|
274 |
-
#
|
275 |
##############################################################################
|
276 |
-
def
|
277 |
-
"""
|
278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
279 |
|
280 |
-
|
281 |
-
|
282 |
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
image_files = []
|
288 |
-
video_files = []
|
289 |
-
unknown_files = []
|
290 |
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
325 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
326 |
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
333 |
)
|
|
|
|
|
|
|
|
|
334 |
|
335 |
-
|
336 |
-
|
337 |
-
content_parts.append(
|
338 |
-
f"\nโ ๏ธ **Unsupported file format**: {', '.join(unknown_names)}\n"
|
339 |
-
"Supported formats: PDF, CSV, TXT"
|
340 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
341 |
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
380 |
headers = {
|
381 |
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
|
382 |
"Content-Type": "application/json"
|
@@ -385,9 +1533,9 @@ def stream_friendli_response(messages: list[dict], max_tokens: int = 1000) -> It
|
|
385 |
payload = {
|
386 |
"model": FRIENDLI_MODEL_ID,
|
387 |
"messages": messages,
|
388 |
-
"max_tokens":
|
389 |
-
"top_p": 0.
|
390 |
-
"temperature": 0.
|
391 |
"stream": True,
|
392 |
"stream_options": {
|
393 |
"include_usage": True
|
@@ -425,341 +1573,517 @@ def stream_friendli_response(messages: list[dict], max_tokens: int = 1000) -> It
|
|
425 |
logger.warning(f"JSON parsing failed: {data_str}")
|
426 |
continue
|
427 |
|
428 |
-
except requests.exceptions.Timeout:
|
429 |
-
yield "โ ๏ธ Response timeout. Please try again."
|
430 |
-
except requests.exceptions.RequestException as e:
|
431 |
-
logger.error(f"Friendli API network error: {e}")
|
432 |
-
yield f"โ ๏ธ Network error occurred: {str(e)}"
|
433 |
except Exception as e:
|
434 |
-
logger.error(f"
|
435 |
-
yield f"โ ๏ธ
|
436 |
|
437 |
##############################################################################
|
438 |
-
# Main
|
439 |
##############################################################################
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
history: list[dict],
|
444 |
-
max_new_tokens: int = 512,
|
445 |
use_web_search: bool = False,
|
446 |
-
use_korean: bool =
|
447 |
-
|
448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
449 |
|
450 |
try:
|
451 |
-
#
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
|
|
458 |
|
459 |
-
if
|
460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
461 |
|
462 |
-
# Web search
|
463 |
if use_web_search:
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
logger.info(f"[Auto web search keywords] {ws_query!r}")
|
469 |
-
ws_result = do_web_search(ws_query, use_korean=use_korean)
|
470 |
-
if not ws_result.startswith("Web search"):
|
471 |
-
combined_system_msg += f"\n\n[Search Results]\n{ws_result}"
|
472 |
-
if use_korean:
|
473 |
-
combined_system_msg += "\n\n[์ค์: ๋ต๋ณ์ ๊ฒ์ ๊ฒฐ๊ณผ์ ์ถ์ฒ๋ฅผ ๋ฐ๋์ ์ธ์ฉํ์ธ์]"
|
474 |
-
else:
|
475 |
-
combined_system_msg += "\n\n[Important: Always cite sources from search results in your answer]"
|
476 |
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
|
|
481 |
|
482 |
-
#
|
483 |
-
|
484 |
|
485 |
-
#
|
486 |
-
|
487 |
-
|
488 |
-
"
|
489 |
-
"
|
490 |
-
|
|
|
|
|
491 |
|
492 |
-
#
|
493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
494 |
|
495 |
-
# Call Friendli API and stream
|
496 |
-
for response_text in stream_friendli_response(messages, max_new_tokens):
|
497 |
-
yield response_text
|
498 |
-
|
499 |
except Exception as e:
|
500 |
-
logger.error(f"
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
##############################################################################
|
506 |
-
examples = [
|
507 |
-
# PDF comparison example
|
508 |
-
[
|
509 |
-
{
|
510 |
-
"text": "Compare the contents of the two PDF files.",
|
511 |
-
"files": [
|
512 |
-
"assets/additional-examples/before.pdf",
|
513 |
-
"assets/additional-examples/after.pdf",
|
514 |
-
],
|
515 |
-
}
|
516 |
-
],
|
517 |
-
# CSV analysis example
|
518 |
-
[
|
519 |
-
{
|
520 |
-
"text": "Summarize and analyze the contents of the CSV file.",
|
521 |
-
"files": ["assets/additional-examples/sample-csv.csv"],
|
522 |
-
}
|
523 |
-
],
|
524 |
-
# Web search example
|
525 |
-
[
|
526 |
-
{
|
527 |
-
"text": "Explain discord.gg/openfreeai",
|
528 |
-
"files": [],
|
529 |
-
}
|
530 |
-
],
|
531 |
-
# Code generation example
|
532 |
-
[
|
533 |
-
{
|
534 |
-
"text": "Write Python code to generate Fibonacci sequence.",
|
535 |
-
"files": [],
|
536 |
-
}
|
537 |
-
],
|
538 |
-
]
|
539 |
|
540 |
##############################################################################
|
541 |
-
# Gradio UI
|
542 |
##############################################################################
|
543 |
css = """
|
544 |
/* Full width UI */
|
545 |
.gradio-container {
|
546 |
-
background: rgba(255, 255, 255, 0.
|
547 |
-
padding:
|
548 |
-
margin:
|
549 |
width: 100% !important;
|
550 |
-
max-width:
|
551 |
-
border-radius:
|
552 |
-
box-shadow: 0
|
553 |
-
}
|
554 |
-
|
555 |
-
.fillable {
|
556 |
-
width: 100% !important;
|
557 |
-
max-width: 100% !important;
|
558 |
}
|
559 |
|
560 |
/* Background */
|
561 |
body {
|
562 |
-
background: linear-gradient(135deg, #
|
563 |
margin: 0;
|
564 |
padding: 0;
|
565 |
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
566 |
-
color: #333;
|
567 |
}
|
568 |
|
569 |
-
/*
|
570 |
-
|
571 |
-
background: #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
572 |
border: none;
|
573 |
color: white !important;
|
574 |
-
padding: 10px 20px;
|
575 |
-
text-transform: uppercase;
|
576 |
font-weight: 600;
|
577 |
-
|
578 |
-
|
579 |
-
border-radius: 6px;
|
580 |
transition: all 0.3s ease;
|
|
|
|
|
581 |
}
|
582 |
|
583 |
-
button
|
584 |
-
|
585 |
-
|
586 |
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
587 |
-
}
|
588 |
-
|
589 |
-
/* Examples section */
|
590 |
-
#examples_container, .examples-container {
|
591 |
-
margin: 20px auto;
|
592 |
-
width: 90%;
|
593 |
-
background: rgba(255, 255, 255, 0.8);
|
594 |
-
padding: 20px;
|
595 |
-
border-radius: 8px;
|
596 |
-
}
|
597 |
-
|
598 |
-
#examples_row, .examples-row {
|
599 |
-
justify-content: center;
|
600 |
}
|
601 |
|
602 |
-
/*
|
603 |
-
.
|
604 |
-
|
605 |
-
|
606 |
-
|
607 |
-
|
608 |
-
|
609 |
-
|
610 |
-
font-size: 14px;
|
611 |
}
|
612 |
|
613 |
-
.
|
614 |
-
|
615 |
-
|
616 |
-
|
617 |
-
border-color: #9ca3af;
|
618 |
}
|
619 |
|
620 |
-
/*
|
621 |
-
.
|
622 |
-
background: white
|
623 |
-
border-radius:
|
624 |
-
|
|
|
|
|
|
|
625 |
}
|
626 |
|
627 |
-
|
628 |
-
|
629 |
-
|
630 |
-
border-radius:
|
|
|
|
|
|
|
631 |
}
|
632 |
|
633 |
-
|
634 |
-
|
635 |
-
background: white !important;
|
636 |
-
border: 1px solid #d1d5db;
|
637 |
-
border-radius: 6px;
|
638 |
-
padding: 10px;
|
639 |
-
font-size: 16px;
|
640 |
}
|
641 |
|
642 |
-
|
643 |
-
|
644 |
-
|
645 |
-
|
|
|
646 |
}
|
647 |
|
648 |
-
/*
|
649 |
-
.
|
650 |
-
|
651 |
-
border:
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
|
656 |
}
|
657 |
|
658 |
-
|
659 |
-
|
660 |
-
|
|
|
661 |
}
|
662 |
|
663 |
-
/*
|
664 |
-
|
665 |
-
|
666 |
-
|
|
|
|
|
667 |
}
|
668 |
|
669 |
-
|
670 |
-
|
671 |
-
|
|
|
|
|
672 |
}
|
673 |
|
674 |
-
|
675 |
-
|
676 |
-
|
677 |
}
|
678 |
|
679 |
-
|
680 |
-
|
681 |
-
|
682 |
-
|
683 |
-
|
|
|
|
|
|
|
684 |
}
|
685 |
|
686 |
-
|
687 |
-
|
688 |
-
|
689 |
-
height: 8px;
|
690 |
}
|
691 |
|
692 |
-
|
693 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
694 |
}
|
695 |
|
696 |
-
|
697 |
-
|
698 |
-
|
|
|
|
|
699 |
}
|
700 |
|
701 |
-
|
702 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
703 |
}
|
704 |
"""
|
705 |
|
706 |
-
|
707 |
-
|
708 |
-
|
709 |
-
|
710 |
-
|
711 |
-
|
712 |
-
|
|
|
|
|
713 |
|
714 |
-
# UI Components
|
715 |
with gr.Row():
|
716 |
with gr.Column(scale=2):
|
717 |
-
|
718 |
-
label="
|
719 |
-
|
720 |
-
|
|
|
721 |
)
|
722 |
-
|
723 |
-
|
724 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
725 |
value=False,
|
726 |
-
info="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
727 |
)
|
|
|
728 |
with gr.Column(scale=1):
|
729 |
-
|
730 |
-
label="
|
731 |
-
|
732 |
-
|
733 |
-
step=50,
|
734 |
-
value=1000,
|
735 |
-
info="Adjust response length"
|
736 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
737 |
|
738 |
-
#
|
739 |
-
|
740 |
-
fn=
|
741 |
-
|
742 |
-
|
743 |
-
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
|
749 |
-
|
750 |
-
|
751 |
-
|
752 |
-
multimodal=True,
|
753 |
-
additional_inputs=[
|
754 |
-
max_tokens_slider,
|
755 |
-
web_search_checkbox,
|
756 |
-
korean_checkbox,
|
757 |
],
|
758 |
-
|
759 |
-
examples=examples,
|
760 |
-
run_examples_on_click=False,
|
761 |
-
cache_examples=False,
|
762 |
-
delete_cache=(1800, 1800),
|
763 |
)
|
764 |
|
765 |
if __name__ == "__main__":
|
|
|
6 |
import requests
|
7 |
from collections.abc import Iterator
|
8 |
from threading import Thread
|
9 |
+
import tempfile
|
10 |
+
import random
|
11 |
+
from typing import Dict, List, Tuple, Optional
|
12 |
+
import shutil
|
13 |
|
14 |
import gradio as gr
|
15 |
from loguru import logger
|
16 |
import pandas as pd
|
17 |
import PyPDF2
|
18 |
+
from PIL import Image
|
19 |
+
from gradio_client import Client
|
20 |
+
import time
|
21 |
+
|
22 |
+
# python-pptx ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ธ
|
23 |
+
try:
|
24 |
+
from pptx import Presentation
|
25 |
+
from pptx.util import Inches, Pt
|
26 |
+
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
27 |
+
from pptx.dml.color import RGBColor
|
28 |
+
from pptx.enum.shapes import MSO_SHAPE
|
29 |
+
from pptx.chart.data import CategoryChartData
|
30 |
+
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
|
31 |
+
PPTX_AVAILABLE = True
|
32 |
+
except ImportError:
|
33 |
+
PPTX_AVAILABLE = False
|
34 |
+
logger.warning("python-pptx ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ค์น๋์ง ์์์ต๋๋ค. pip install python-pptx")
|
35 |
|
36 |
##############################################################################
|
37 |
# API Configuration
|
|
|
48 |
if not SERPHOUSE_API_KEY:
|
49 |
logger.warning("SERPHOUSE_API_KEY not set. Web search functionality will be limited.")
|
50 |
|
51 |
+
##############################################################################
|
52 |
+
# AI Image Generation API Configuration
|
53 |
+
##############################################################################
|
54 |
+
AI_IMAGE_API_URL = "http://211.233.58.201:7971/"
|
55 |
+
AI_IMAGE_ENABLED = False
|
56 |
+
ai_image_client = None
|
57 |
+
|
58 |
+
def initialize_ai_image_api():
|
59 |
+
"""AI ์ด๋ฏธ์ง ์์ฑ API ์ด๊ธฐํ"""
|
60 |
+
global AI_IMAGE_ENABLED, ai_image_client
|
61 |
+
|
62 |
+
try:
|
63 |
+
logger.info("Connecting to AI image generation API...")
|
64 |
+
ai_image_client = Client(AI_IMAGE_API_URL)
|
65 |
+
AI_IMAGE_ENABLED = True
|
66 |
+
logger.info("AI image generation API connected successfully")
|
67 |
+
return True
|
68 |
+
except Exception as e:
|
69 |
+
logger.error(f"Failed to connect to AI image API: {e}")
|
70 |
+
AI_IMAGE_ENABLED = False
|
71 |
+
return False
|
72 |
+
|
73 |
+
##############################################################################
|
74 |
+
# Design Themes and Color Schemes
|
75 |
+
##############################################################################
|
76 |
+
DESIGN_THEMES = {
|
77 |
+
"professional": {
|
78 |
+
"name": "ํ๋กํ์
๋",
|
79 |
+
"colors": {
|
80 |
+
"primary": RGBColor(46, 134, 171), # #2E86AB
|
81 |
+
"secondary": RGBColor(162, 59, 114), # #A23B72
|
82 |
+
"accent": RGBColor(241, 143, 1), # #F18F01
|
83 |
+
"background": RGBColor(250, 250, 250), # #FAFAFA - Lighter background
|
84 |
+
"text": RGBColor(44, 44, 44), # #2C2C2C - Darker text for better contrast
|
85 |
+
},
|
86 |
+
"fonts": {
|
87 |
+
"title": "Arial",
|
88 |
+
"subtitle": "Arial",
|
89 |
+
"body": "Calibri"
|
90 |
+
}
|
91 |
+
},
|
92 |
+
"modern": {
|
93 |
+
"name": "๋ชจ๋",
|
94 |
+
"colors": {
|
95 |
+
"primary": RGBColor(114, 9, 183), # #7209B7
|
96 |
+
"secondary": RGBColor(247, 37, 133), # #F72585
|
97 |
+
"accent": RGBColor(76, 201, 240), # #4CC9F0
|
98 |
+
"background": RGBColor(252, 252, 252), # #FCFCFC - Very light background
|
99 |
+
"text": RGBColor(40, 40, 40), # #282828 - Dark text
|
100 |
+
},
|
101 |
+
"fonts": {
|
102 |
+
"title": "Arial",
|
103 |
+
"subtitle": "Arial",
|
104 |
+
"body": "Helvetica"
|
105 |
+
}
|
106 |
+
},
|
107 |
+
"nature": {
|
108 |
+
"name": "์์ฐ",
|
109 |
+
"colors": {
|
110 |
+
"primary": RGBColor(45, 106, 79), # #2D6A4F
|
111 |
+
"secondary": RGBColor(82, 183, 136), # #52B788
|
112 |
+
"accent": RGBColor(181, 233, 185), # #B5E9B9 - Softer accent
|
113 |
+
"background": RGBColor(248, 252, 248), # #F8FCF8 - Light green tint
|
114 |
+
"text": RGBColor(27, 38, 44), # #1B262C
|
115 |
+
},
|
116 |
+
"fonts": {
|
117 |
+
"title": "Georgia",
|
118 |
+
"subtitle": "Verdana",
|
119 |
+
"body": "Calibri"
|
120 |
+
}
|
121 |
+
},
|
122 |
+
"creative": {
|
123 |
+
"name": "ํฌ๋ฆฌ์์ดํฐ๋ธ",
|
124 |
+
"colors": {
|
125 |
+
"primary": RGBColor(255, 0, 110), # #FF006E
|
126 |
+
"secondary": RGBColor(251, 86, 7), # #FB5607
|
127 |
+
"accent": RGBColor(255, 190, 11), # #FFBE0B
|
128 |
+
"background": RGBColor(255, 248, 240), # #FFF8F0 - Light warm background
|
129 |
+
"text": RGBColor(33, 33, 33), # #212121 - Dark text on light bg
|
130 |
+
},
|
131 |
+
"fonts": {
|
132 |
+
"title": "Impact",
|
133 |
+
"subtitle": "Arial",
|
134 |
+
"body": "Segoe UI"
|
135 |
+
}
|
136 |
+
},
|
137 |
+
"minimal": {
|
138 |
+
"name": "๋ฏธ๋๋ฉ",
|
139 |
+
"colors": {
|
140 |
+
"primary": RGBColor(55, 55, 55), # #373737 - Softer than pure black
|
141 |
+
"secondary": RGBColor(120, 120, 120), # #787878
|
142 |
+
"accent": RGBColor(0, 122, 255), # #007AFF - Blue accent
|
143 |
+
"background": RGBColor(252, 252, 252), # #FCFCFC
|
144 |
+
"text": RGBColor(33, 33, 33), # #212121
|
145 |
+
},
|
146 |
+
"fonts": {
|
147 |
+
"title": "Helvetica",
|
148 |
+
"subtitle": "Helvetica",
|
149 |
+
"body": "Arial"
|
150 |
+
}
|
151 |
+
}
|
152 |
+
}
|
153 |
+
|
154 |
+
##############################################################################
|
155 |
+
# Slide Layout Types
|
156 |
+
##############################################################################
|
157 |
+
SLIDE_LAYOUTS = {
|
158 |
+
"title": 0, # ์ ๋ชฉ ์ฌ๋ผ์ด๋
|
159 |
+
"title_content": 1, # ์ ๋ชฉ๊ณผ ๋ด์ฉ
|
160 |
+
"section_header": 2, # ์น์
ํค๋
|
161 |
+
"two_content": 3, # 2๋จ ๋ ์ด์์
|
162 |
+
"comparison": 4, # ๋น๊ต ๋ ์ด์์
|
163 |
+
"title_only": 5, # ์ ๋ชฉ๋ง
|
164 |
+
"blank": 6 # ๋น ์ฌ๋ผ์ด๋
|
165 |
+
}
|
166 |
+
|
167 |
+
##############################################################################
|
168 |
+
# Emoji Bullet Points Mapping
|
169 |
+
##############################################################################
|
170 |
+
def has_emoji(text: str) -> bool:
|
171 |
+
"""Check if text already contains emoji"""
|
172 |
+
# Check for common emoji unicode ranges
|
173 |
+
for char in text[:3]: # Check first 3 characters
|
174 |
+
code = ord(char)
|
175 |
+
# Common emoji ranges
|
176 |
+
if (0x1F300 <= code <= 0x1F9FF) or \
|
177 |
+
(0x2600 <= code <= 0x26FF) or \
|
178 |
+
(0x2700 <= code <= 0x27BF) or \
|
179 |
+
(0x1F000 <= code <= 0x1F02F) or \
|
180 |
+
(0x1F0A0 <= code <= 0x1F0FF) or \
|
181 |
+
(0x1F100 <= code <= 0x1F1FF):
|
182 |
+
return True
|
183 |
+
return False
|
184 |
+
|
185 |
+
def get_emoji_for_content(text: str) -> str:
|
186 |
+
"""Get relevant emoji based on content"""
|
187 |
+
text_lower = text.lower()
|
188 |
+
|
189 |
+
# Technology
|
190 |
+
if any(word in text_lower for word in ['ai', '์ธ๊ณต์ง๋ฅ', 'ml', '๋จธ์ ๋ฌ๋', '๋ฅ๋ฌ๋', 'deep learning']):
|
191 |
+
return '๐ค'
|
192 |
+
elif any(word in text_lower for word in ['๋ฐ์ดํฐ', 'data', '๋ถ์', 'analysis', 'ํต๊ณ']):
|
193 |
+
return '๐'
|
194 |
+
elif any(word in text_lower for word in ['์ฝ๋', 'code', 'ํ๋ก๊ทธ๋๋ฐ', 'programming', '๊ฐ๋ฐ']):
|
195 |
+
return '๐ป'
|
196 |
+
elif any(word in text_lower for word in ['ํด๋ผ์ฐ๋', 'cloud', '์๋ฒ', 'server']):
|
197 |
+
return 'โ๏ธ'
|
198 |
+
elif any(word in text_lower for word in ['๋ณด์', 'security', '์์ ', 'safety']):
|
199 |
+
return '๐'
|
200 |
+
elif any(word in text_lower for word in ['๋คํธ์ํฌ', 'network', '์ฐ๊ฒฐ', 'connection', '์ธํฐ๋ท']):
|
201 |
+
return '๐'
|
202 |
+
elif any(word in text_lower for word in ['๋ชจ๋ฐ์ผ', 'mobile', '์ค๋งํธํฐ', 'smartphone', '์ฑ']):
|
203 |
+
return '๐ฑ'
|
204 |
+
|
205 |
+
# Business
|
206 |
+
elif any(word in text_lower for word in ['์ฑ์ฅ', 'growth', '์ฆ๊ฐ', 'increase', '์์น']):
|
207 |
+
return '๐'
|
208 |
+
elif any(word in text_lower for word in ['๋ชฉํ', 'goal', 'target', 'ํ๊ฒ', '๋ชฉ์ ']):
|
209 |
+
return '๐ฏ'
|
210 |
+
elif any(word in text_lower for word in ['๋', 'money', '๋น์ฉ', 'cost', '์์ฐ', 'budget', '์์ต']):
|
211 |
+
return '๐ฐ'
|
212 |
+
elif any(word in text_lower for word in ['ํ', 'team', 'ํ์
', 'collaboration', 'ํ๋ ฅ']):
|
213 |
+
return '๐ฅ'
|
214 |
+
elif any(word in text_lower for word in ['์๊ฐ', 'time', '์ผ์ ', 'schedule', '๊ธฐํ']):
|
215 |
+
return 'โฐ'
|
216 |
+
elif any(word in text_lower for word in ['์์ด๋์ด', 'idea', '์ฐฝ์', 'creative', 'ํ์ ']):
|
217 |
+
return '๐ก'
|
218 |
+
elif any(word in text_lower for word in ['์ ๋ต', 'strategy', '๊ณํ', 'plan']):
|
219 |
+
return '๐'
|
220 |
+
elif any(word in text_lower for word in ['์ฑ๊ณต', 'success', '๋ฌ์ฑ', 'achieve']):
|
221 |
+
return '๐'
|
222 |
+
|
223 |
+
# Education
|
224 |
+
elif any(word in text_lower for word in ['ํ์ต', 'learning', '๊ต์ก', 'education', '๊ณต๋ถ']):
|
225 |
+
return '๐'
|
226 |
+
elif any(word in text_lower for word in ['์ฐ๊ตฌ', 'research', '์กฐ์ฌ', 'study', '์คํ']):
|
227 |
+
return '๐ฌ'
|
228 |
+
elif any(word in text_lower for word in ['๋ฌธ์', 'document', '๋ณด๊ณ ์', 'report']):
|
229 |
+
return '๐'
|
230 |
+
elif any(word in text_lower for word in ['์ ๋ณด', 'information', '์ง์', 'knowledge']):
|
231 |
+
return '๐'
|
232 |
+
|
233 |
+
# Communication
|
234 |
+
elif any(word in text_lower for word in ['์ํต', 'communication', '๋ํ', 'conversation']):
|
235 |
+
return '๐ฌ'
|
236 |
+
elif any(word in text_lower for word in ['์ด๋ฉ์ผ', 'email', '๋ฉ์ผ', 'mail']):
|
237 |
+
return '๐ง'
|
238 |
+
elif any(word in text_lower for word in ['์ ํ', 'phone', 'call', 'ํตํ']):
|
239 |
+
return '๐'
|
240 |
+
elif any(word in text_lower for word in ['ํ์', 'meeting', '๋ฏธํ
', '์ปจํผ๋ฐ์ค']):
|
241 |
+
return '๐'
|
242 |
+
|
243 |
+
# Nature/Environment
|
244 |
+
elif any(word in text_lower for word in ['ํ๊ฒฝ', 'environment', '์์ฐ', 'nature']):
|
245 |
+
return '๐ฑ'
|
246 |
+
elif any(word in text_lower for word in ['์ง์๊ฐ๋ฅ', 'sustainable', '์นํ๊ฒฝ', 'eco']):
|
247 |
+
return 'โป๏ธ'
|
248 |
+
elif any(word in text_lower for word in ['์๋์ง', 'energy', '์ ๋ ฅ', 'power']):
|
249 |
+
return 'โก'
|
250 |
+
elif any(word in text_lower for word in ['์ง๊ตฌ', 'earth', '์ธ๊ณ', 'world']):
|
251 |
+
return '๐'
|
252 |
+
|
253 |
+
# Process/Steps
|
254 |
+
elif any(word in text_lower for word in ['ํ๋ก์ธ์ค', 'process', '์ ์ฐจ', 'procedure', '๋จ๊ณ']):
|
255 |
+
return '๐'
|
256 |
+
elif any(word in text_lower for word in ['์ฒดํฌ', 'check', 'ํ์ธ', 'verify', '๊ฒ์ฆ']):
|
257 |
+
return 'โ
'
|
258 |
+
elif any(word in text_lower for word in ['์ฃผ์', 'warning', '๊ฒฝ๊ณ ', 'caution']):
|
259 |
+
return 'โ ๏ธ'
|
260 |
+
elif any(word in text_lower for word in ['์ค์', 'important', 'ํต์ฌ', 'key', 'ํ์']):
|
261 |
+
return 'โญ'
|
262 |
+
elif any(word in text_lower for word in ['์ง๋ฌธ', 'question', '๋ฌธ์', 'ask']):
|
263 |
+
return 'โ'
|
264 |
+
elif any(word in text_lower for word in ['ํด๊ฒฐ', 'solution', '๋ต', 'answer']):
|
265 |
+
return '๐ฏ'
|
266 |
+
|
267 |
+
# Actions
|
268 |
+
elif any(word in text_lower for word in ['์์', 'start', '์ถ๋ฐ', 'begin']):
|
269 |
+
return '๐'
|
270 |
+
elif any(word in text_lower for word in ['์๋ฃ', 'complete', '์ข
๋ฃ', 'finish']):
|
271 |
+
return '๐'
|
272 |
+
elif any(word in text_lower for word in ['๊ฐ์ ', 'improve', 'ํฅ์', 'enhance']):
|
273 |
+
return '๐ง'
|
274 |
+
elif any(word in text_lower for word in ['๋ณํ', 'change', '๋ณ๊ฒฝ', 'modify']):
|
275 |
+
return '๐'
|
276 |
+
|
277 |
+
# Industries
|
278 |
+
elif any(word in text_lower for word in ['์๋ฃ', 'medical', '๋ณ์', 'hospital', '๊ฑด๊ฐ']):
|
279 |
+
return '๐ฅ'
|
280 |
+
elif any(word in text_lower for word in ['๊ธ์ต', 'finance', '์ํ', 'bank']):
|
281 |
+
return '๐ฆ'
|
282 |
+
elif any(word in text_lower for word in ['์ ์กฐ', 'manufacturing', '๊ณต์ฅ', 'factory']):
|
283 |
+
return '๐ญ'
|
284 |
+
elif any(word in text_lower for word in ['๋์
', 'agriculture', '๋์ฅ', 'farm']):
|
285 |
+
return '๐พ'
|
286 |
+
|
287 |
+
# Emotion/Status
|
288 |
+
elif any(word in text_lower for word in ['ํ๋ณต', 'happy', '๊ธฐ์จ', 'joy']):
|
289 |
+
return '๐'
|
290 |
+
elif any(word in text_lower for word in ['์ํ', 'danger', 'risk', '๋ฆฌ์คํฌ']):
|
291 |
+
return 'โก'
|
292 |
+
elif any(word in text_lower for word in ['์๋ก์ด', 'new', '์ ๊ท', 'novel']):
|
293 |
+
return 'โจ'
|
294 |
+
|
295 |
+
# Numbers
|
296 |
+
elif text_lower.startswith(('์ฒซ์งธ', 'first', '1.', '์ฒซ๋ฒ์งธ', '์ฒซ ๋ฒ์งธ')):
|
297 |
+
return '1๏ธโฃ'
|
298 |
+
elif text_lower.startswith(('๋์งธ', 'second', '2.', '๋๋ฒ์งธ', '๋ ๋ฒ์งธ')):
|
299 |
+
return '2๏ธโฃ'
|
300 |
+
elif text_lower.startswith(('์
์งธ', 'third', '3.', '์ธ๋ฒ์งธ', '์ธ ๋ฒ์งธ')):
|
301 |
+
return '3๏ธโฃ'
|
302 |
+
elif text_lower.startswith(('๋ท์งธ', 'fourth', '4.', '๋ค๋ฒ์งธ', '๋ค ๋ฒ์งธ')):
|
303 |
+
return '4๏ธโฃ'
|
304 |
+
elif text_lower.startswith(('๋ค์ฏ์งธ', 'fifth', '5.', '๋ค์ฏ๋ฒ์งธ', '๋ค์ฏ ๋ฒ์งธ')):
|
305 |
+
return '5๏ธโฃ'
|
306 |
+
|
307 |
+
# Default
|
308 |
+
else:
|
309 |
+
return 'โถ๏ธ'
|
310 |
+
|
311 |
+
##############################################################################
|
312 |
+
# Icon and Shape Mappings
|
313 |
+
##############################################################################
|
314 |
+
SHAPE_ICONS = {
|
315 |
+
"๋ชฉํ": MSO_SHAPE.STAR_5_POINT,
|
316 |
+
"ํ๋ก์ธ์ค": MSO_SHAPE.BLOCK_ARC,
|
317 |
+
"์ฑ์ฅ": MSO_SHAPE.UP_ARROW,
|
318 |
+
"์์ด๋์ด": MSO_SHAPE.LIGHTNING_BOLT,
|
319 |
+
"์ฒดํฌ": MSO_SHAPE.RECTANGLE,
|
320 |
+
"์ฃผ์": MSO_SHAPE.DIAMOND,
|
321 |
+
"์ง๋ฌธ": MSO_SHAPE.OVAL,
|
322 |
+
"๋ถ์": MSO_SHAPE.PENTAGON,
|
323 |
+
"์๊ฐ": MSO_SHAPE.DONUT,
|
324 |
+
"ํ": MSO_SHAPE.HEXAGON,
|
325 |
+
}
|
326 |
+
|
327 |
##############################################################################
|
328 |
# File Processing Constants
|
329 |
##############################################################################
|
|
|
479 |
summary += f"**Showing**: Top {min(50, total_rows)} rows\n"
|
480 |
summary += f"**Columns**: {', '.join(df.columns)}\n\n"
|
481 |
|
482 |
+
# Extract data for charts
|
483 |
+
chart_data = {
|
484 |
+
"columns": list(df.columns),
|
485 |
+
"sample_data": df.head(10).to_dict('records')
|
486 |
+
}
|
487 |
+
|
488 |
df_str = df.to_string()
|
489 |
if len(df_str) > MAX_CONTENT_CHARS:
|
490 |
df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
|
491 |
|
492 |
+
return f"**[CSV File: {os.path.basename(path)}]**\n\n{summary}{df_str}\n\nCHART_DATA:{json.dumps(chart_data)}"
|
493 |
except Exception as e:
|
494 |
logger.error(f"CSV read error: {e}")
|
495 |
return f"Failed to read CSV file ({os.path.basename(path)}): {str(e)}"
|
|
|
559 |
return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
|
560 |
|
561 |
##############################################################################
|
562 |
+
# AI Image Generation Functions using API
|
563 |
##############################################################################
|
564 |
+
def generate_cover_image_prompt(topic: str, slides_data: list) -> str:
|
565 |
+
"""PPT ์ฃผ์ ์ ๋ด์ฉ์ ๊ธฐ๋ฐ์ผ๋ก ํ์ง ์ด๋ฏธ์ง ํ๋กฌํํธ ์์ฑ - ํ๊ธ ์ง์"""
|
566 |
+
|
567 |
+
# ์ฃผ์ ํค์๋ ์ถ์ถ
|
568 |
+
keywords = []
|
569 |
+
topic_keywords = extract_keywords(topic, top_k=3)
|
570 |
+
keywords.extend(topic_keywords.split())
|
571 |
+
|
572 |
+
# ๊ฐ ์ฌ๋ผ์ด๋ ์ ๋ชฉ์์ ํค์๋ ์ถ์ถ
|
573 |
+
for slide in slides_data[:5]:
|
574 |
+
title = slide.get('title', '')
|
575 |
+
if title:
|
576 |
+
slide_keywords = extract_keywords(title, top_k=2)
|
577 |
+
keywords.extend(slide_keywords.split())
|
578 |
+
|
579 |
+
unique_keywords = list(dict.fromkeys(keywords))[:5]
|
580 |
+
|
581 |
+
# ์ฃผ์ ๋ถ์์ ํตํ ์คํ์ผ ๊ฒฐ์
|
582 |
+
style = "ํ๋์ ์ด๊ณ ์ ๋ฌธ์ ์ธ"
|
583 |
+
topic_lower = topic.lower()
|
584 |
+
|
585 |
+
if any(word in topic_lower for word in ['๊ธฐ์ ', 'tech', 'ai', '์ธ๊ณต์ง๋ฅ', 'digital', '๋์งํธ']):
|
586 |
+
style = "๋ฏธ๋์งํฅ์ ์ด๊ณ ํ์ดํ
ํฌํ ๋์งํธ ์ํธ"
|
587 |
+
elif any(word in topic_lower for word in ['๋น์ฆ๋์ค', 'business', '๊ฒฝ์', 'management']):
|
588 |
+
style = "์ ๋ฌธ์ ์ด๊ณ ๊ธฐ์
์ ์ธ ํ๋์ "
|
589 |
+
elif any(word in topic_lower for word in ['๊ต์ก', 'education', 'ํ์ต', 'learning']):
|
590 |
+
style = "๊ต์ก์ ์ด๊ณ ์๊ฐ์ ์ฃผ๋ ๋ฐ์"
|
591 |
+
elif any(word in topic_lower for word in ['ํ๊ฒฝ', 'environment', '์์ฐ', 'nature']):
|
592 |
+
style = "์์ฐ์ ์ด๊ณ ์นํ๊ฒฝ์ ์ธ ๋
น์"
|
593 |
+
elif any(word in topic_lower for word in ['์๋ฃ', 'medical', '๊ฑด๊ฐ', 'health']):
|
594 |
+
style = "์๋ฃ์ ์ด๊ณ ๊ฑด๊ฐํ ๊นจ๋ํ"
|
595 |
+
elif any(word in topic_lower for word in ['๊ธ์ต', 'finance', 'ํฌ์', 'investment']):
|
596 |
+
style = "๊ธ์ต์ ์ด๊ณ ์ ๋ฌธ์ ์ธ ์ ๋ขฐ๊ฐ ์๋"
|
597 |
+
|
598 |
+
# wbgmsst ์คํ์ผ์ ์ํ ํ๊ธ ํ๋กฌํํธ ๊ตฌ์ฑ
|
599 |
+
prompt = f"wbgmsst, 3D, {' '.join(unique_keywords)}๋ฅผ ์์งํ๋ ์์ด์๋ฉํธ๋ฆญ ์คํ์ผ์ {style} ์ผ๋ฌ์คํธ๋ ์ด์
, ํฐ์ ๋ฐฐ๊ฒฝ, ๊น๋ํ ๋ฏธ๋๋ฉ๋ฆฌ์คํฑ ๊ตฌ์ฑ, ์ ๋ฌธ์ ์ธ ํ๋ ์ ํ
์ด์
ํ์ง, ๊ณ ํ์ง, ์คํ๋์ค ์กฐ๋ช
"
|
600 |
+
|
601 |
+
return prompt
|
602 |
|
|
|
|
|
|
|
603 |
|
604 |
+
|
605 |
+
def generate_ai_cover_image_via_api(topic: str, slides_data: list) -> Optional[str]:
|
606 |
+
"""API๋ฅผ ํตํด AI ํ์ง ์ด๋ฏธ์ง ์์ฑ(PNG ๋ณํ ํฌํจ)"""
|
607 |
+
if not AI_IMAGE_ENABLED or not ai_image_client:
|
608 |
+
logger.warning("AI image generation API is not available")
|
609 |
+
return None
|
610 |
+
|
611 |
+
try:
|
612 |
+
# โโ 1. ํ๋กฌํํธ ๋ฐ ํ๋ผ๋ฏธํฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
613 |
+
prompt = generate_cover_image_prompt(topic, slides_data)
|
614 |
+
logger.info(f"Generated image prompt: {prompt}")
|
615 |
+
|
616 |
+
height, width = 1024.0, 1024.0
|
617 |
+
steps, scales = 8.0, 3.5
|
618 |
+
seed = float(random.randint(0, 1_000_000))
|
619 |
+
logger.info(f"Calling AI image APIโฆ (h={height}, w={width}, steps={steps}, scales={scales}, seed={seed})")
|
620 |
+
|
621 |
+
result = ai_image_client.predict(
|
622 |
+
height=height,
|
623 |
+
width=width,
|
624 |
+
steps=steps,
|
625 |
+
scales=scales,
|
626 |
+
prompt=prompt,
|
627 |
+
seed=seed,
|
628 |
+
api_name="/process_and_save_image"
|
629 |
+
)
|
630 |
+
logger.info(f"API call successful. Result type: {type(result)}")
|
631 |
+
|
632 |
+
# โโ 2. ๋ด๋ถ ํฌํผ: WEBP โ PNG ๋ณํ โโโโโโโโโโโโโโโโโโโโโโโโ
|
633 |
+
def _to_png(src_path: str) -> Optional[str]:
|
634 |
+
try:
|
635 |
+
with Image.open(src_path) as im:
|
636 |
+
png_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
|
637 |
+
im.save(png_tmp.name, format="PNG")
|
638 |
+
logger.info(f"Converted {src_path} โ {png_tmp.name}")
|
639 |
+
return png_tmp.name
|
640 |
+
except Exception as e:
|
641 |
+
logger.error(f"PNG ๋ณํ ์คํจ: {e}")
|
642 |
+
return None
|
643 |
+
|
644 |
+
# โโ 3. ๊ฒฐ๊ณผ ์ฒ๋ฆฌ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
645 |
+
image_path = None
|
646 |
+
if isinstance(result, dict):
|
647 |
+
image_path = result.get("path")
|
648 |
+
elif isinstance(result, str):
|
649 |
+
image_path = result
|
650 |
+
|
651 |
+
if image_path and os.path.exists(image_path):
|
652 |
+
ext = os.path.splitext(image_path)[1].lower()
|
653 |
+
if ext == ".png":
|
654 |
+
# ์ด๋ฏธ PNG๋ผ๋ฉด ๊ทธ๋๋ก ๋ณต์ฌ
|
655 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
|
656 |
+
shutil.copy2(image_path, tmp.name)
|
657 |
+
logger.info(f"PNG copied from {image_path} โ {tmp.name}")
|
658 |
+
return tmp.name
|
659 |
+
else:
|
660 |
+
# WEBPยทJPEG ๋ฑ์ PNG๋ก ๋ณํ
|
661 |
+
return _to_png(image_path)
|
662 |
+
|
663 |
+
logger.error(f"Image path not found or invalid: {result}")
|
664 |
+
return None
|
665 |
+
|
666 |
+
except Exception as e:
|
667 |
+
logger.error(f"Failed to generate AI image via API: {e}", exc_info=True)
|
668 |
+
return None
|
669 |
+
|
670 |
|
671 |
##############################################################################
|
672 |
+
# PPT Generation Functions - FIXED VERSION
|
673 |
##############################################################################
|
674 |
+
def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> list:
|
675 |
+
"""Parse LLM response to extract slide content - COMPLETELY FIXED VERSION"""
|
676 |
+
slides = []
|
677 |
+
|
678 |
+
# Debug: ์ ์ฒด ์๋ต ํ์ธ
|
679 |
+
logger.info(f"Parsing LLM response, total length: {len(response)}")
|
680 |
+
logger.debug(f"First 500 chars: {response[:500]}")
|
681 |
+
|
682 |
+
# Try JSON parsing first
|
683 |
+
try:
|
684 |
+
json_match = re.search(r'\[[\s\S]*\]', response)
|
685 |
+
if json_match:
|
686 |
+
slides_data = json.loads(json_match.group())
|
687 |
+
return slides_data
|
688 |
+
except:
|
689 |
+
pass
|
690 |
+
|
691 |
+
# Split by slide markers and process each section
|
692 |
+
# ์ฌ๋ผ์ด๋๋ฅผ ๊ตฌ๋ถํ๋ ๋ ๊ฐ๋ ฅํ ์ ๊ท์
|
693 |
+
slide_pattern = r'(?:^|\n)(?:์ฌ๋ผ์ด๋|Slide)\s*\d+|(?:^|\n)\d+[\.)](?:\s|$)'
|
694 |
+
|
695 |
+
# ์ฌ๋ผ์ด๋ ์น์
์ผ๋ก ๋ถํ
|
696 |
+
sections = re.split(slide_pattern, response, flags=re.MULTILINE | re.IGNORECASE)
|
697 |
+
|
698 |
+
# ์ฒซ ๋ฒ์งธ ๋น ์น์
์ ๊ฑฐ
|
699 |
+
if sections and not sections[0].strip():
|
700 |
+
sections = sections[1:]
|
701 |
+
|
702 |
+
logger.info(f"Found {len(sections)} potential slide sections")
|
703 |
+
|
704 |
+
for idx, section in enumerate(sections):
|
705 |
+
if not section.strip():
|
706 |
+
continue
|
707 |
+
|
708 |
+
logger.debug(f"Processing section {idx}: {section[:100]}...")
|
709 |
+
|
710 |
+
slide = {
|
711 |
+
'title': '',
|
712 |
+
'content': '',
|
713 |
+
'notes': '',
|
714 |
+
'layout': 'title_content',
|
715 |
+
'chart_data': None
|
716 |
+
}
|
717 |
+
|
718 |
+
# ์น์
๋ด์์ ์ ๋ชฉ, ๋ด์ฉ, ๋
ธํธ ์ถ์ถ
|
719 |
+
lines = section.strip().split('\n')
|
720 |
+
current_part = None
|
721 |
+
title_lines = []
|
722 |
+
content_lines = []
|
723 |
+
notes_lines = []
|
724 |
+
|
725 |
+
for line in lines:
|
726 |
+
line = line.strip()
|
727 |
+
if not line:
|
728 |
+
continue
|
729 |
+
|
730 |
+
# ์ ๋ชฉ ์น์
๊ฐ์ง
|
731 |
+
if line.startswith('์ ๋ชฉ:') or line.startswith('Title:'):
|
732 |
+
current_part = 'title'
|
733 |
+
title_text = line.split(':', 1)[1].strip() if ':' in line else ''
|
734 |
+
if title_text:
|
735 |
+
title_lines.append(title_text)
|
736 |
+
# ๋ด์ฉ ์น์
๊ฐ์ง
|
737 |
+
elif line.startswith('๋ด์ฉ:') or line.startswith('Content:'):
|
738 |
+
current_part = 'content'
|
739 |
+
content_text = line.split(':', 1)[1].strip() if ':' in line else ''
|
740 |
+
if content_text:
|
741 |
+
content_lines.append(content_text)
|
742 |
+
# ๋
ธํธ ์น์
๊ฐ์ง
|
743 |
+
elif line.startswith('๋
ธํธ:') or line.startswith('Notes:') or line.startswith('๋ฐํ์ ๋
ธํธ:'):
|
744 |
+
current_part = 'notes'
|
745 |
+
notes_text = line.split(':', 1)[1].strip() if ':' in line else ''
|
746 |
+
if notes_text:
|
747 |
+
notes_lines.append(notes_text)
|
748 |
+
# ํ์ฌ ์น์
์ ๋ฐ๋ผ ๋ด์ฉ ์ถ๊ฐ
|
749 |
+
else:
|
750 |
+
if current_part == 'title' and not title_lines:
|
751 |
+
title_lines.append(line)
|
752 |
+
elif current_part == 'content':
|
753 |
+
content_lines.append(line)
|
754 |
+
elif current_part == 'notes':
|
755 |
+
notes_lines.append(line)
|
756 |
+
elif not current_part and not title_lines:
|
757 |
+
# ์ฒซ ๋ฒ์งธ ์ค์ ์ ๋ชฉ์ผ๋ก
|
758 |
+
title_lines.append(line)
|
759 |
+
current_part = 'content' # ์ดํ ์ค๋ค์ content๋ก
|
760 |
+
elif not current_part:
|
761 |
+
content_lines.append(line)
|
762 |
+
|
763 |
+
# ์ฌ๋ผ์ด๋ ๋ฐ์ดํฐ ์ค์
|
764 |
+
slide['title'] = ' '.join(title_lines).strip()
|
765 |
+
slide['content'] = '\n'.join(content_lines).strip()
|
766 |
+
slide['notes'] = ' '.join(notes_lines).strip()
|
767 |
+
|
768 |
+
# ์ ๋ชฉ ์ ๋ฆฌ
|
769 |
+
slide['title'] = re.sub(r'^(์ฌ๋ผ์ด๋|Slide)\s*\d+\s*[:๏ผ\-]?\s*', '', slide['title'], flags=re.IGNORECASE)
|
770 |
+
slide['title'] = re.sub(r'^(์ ๋ชฉ|Title)\s*[:๏ผ]\s*', '', slide['title'], flags=re.IGNORECASE)
|
771 |
+
|
772 |
+
# ๋ด์ฉ์ด ์๋ ๊ฒฝ์ฐ์๋ง ์ถ๊ฐ
|
773 |
+
if slide['title'] or slide['content']:
|
774 |
+
logger.info(f"Slide {len(slides)+1}: Title='{slide['title'][:30]}...', Content length={len(slide['content'])}")
|
775 |
+
slides.append(slide)
|
776 |
+
|
777 |
+
# ๋ง์ฝ ์ ๋ฐฉ๋ฒ์ผ๋ก ํ์ฑ์ด ์ ๋์๋ค๋ฉด, ๋ ๊ฐ๋จํ ๋ฐฉ๋ฒ ์๋
|
778 |
+
if not slides:
|
779 |
+
logger.warning("Primary parsing failed, trying fallback method...")
|
780 |
+
|
781 |
+
# ๋๋ธ ๋ด๋ผ์ธ์ผ๋ก ๊ตฌ๋ถ
|
782 |
+
sections = response.split('\n\n')
|
783 |
+
for section in sections:
|
784 |
+
lines = section.strip().split('\n')
|
785 |
+
if len(lines) >= 2: # ์ต์ ์ ๋ชฉ๊ณผ ๋ด์ฉ์ด ์์ด์ผ ํจ
|
786 |
+
slide = {
|
787 |
+
'title': lines[0].strip(),
|
788 |
+
'content': '\n'.join(lines[1:]).strip(),
|
789 |
+
'notes': '',
|
790 |
+
'layout': 'title_content',
|
791 |
+
'chart_data': None
|
792 |
+
}
|
793 |
+
|
794 |
+
# ์ ๋ชฉ ์ ๋ฆฌ
|
795 |
+
slide['title'] = re.sub(r'^(์ฌ๋ผ์ด๋|Slide)\s*\d+\s*[:๏ผ\-]?\s*', '', slide['title'], flags=re.IGNORECASE)
|
796 |
+
|
797 |
+
if slide['title'] and slide['content']:
|
798 |
+
slides.append(slide)
|
799 |
|
800 |
+
logger.info(f"Total slides parsed: {len(slides)}")
|
801 |
+
return slides
|
802 |
|
803 |
+
def force_font_size(text_frame, font_size_pt: int, theme: Dict):
|
804 |
+
"""Force font size for all paragraphs and runs in a text frame"""
|
805 |
+
if not text_frame:
|
806 |
+
return
|
|
|
|
|
|
|
807 |
|
808 |
+
try:
|
809 |
+
# Ensure paragraphs exist
|
810 |
+
if not hasattr(text_frame, 'paragraphs'):
|
811 |
+
return
|
812 |
+
|
813 |
+
for paragraph in text_frame.paragraphs:
|
814 |
+
try:
|
815 |
+
# Set paragraph level font
|
816 |
+
if hasattr(paragraph, 'font'):
|
817 |
+
paragraph.font.size = Pt(font_size_pt)
|
818 |
+
paragraph.font.name = theme['fonts']['body']
|
819 |
+
paragraph.font.color.rgb = theme['colors']['text']
|
820 |
+
|
821 |
+
# Set run level font (most important for actual rendering)
|
822 |
+
if hasattr(paragraph, 'runs'):
|
823 |
+
for run in paragraph.runs:
|
824 |
+
run.font.size = Pt(font_size_pt)
|
825 |
+
run.font.name = theme['fonts']['body']
|
826 |
+
run.font.color.rgb = theme['colors']['text']
|
827 |
+
|
828 |
+
# If paragraph has no runs but has text, create a run
|
829 |
+
if paragraph.text and (not hasattr(paragraph, 'runs') or len(paragraph.runs) == 0):
|
830 |
+
# Force creation of runs by modifying text
|
831 |
+
temp_text = paragraph.text
|
832 |
+
paragraph.text = temp_text # This creates runs
|
833 |
+
if hasattr(paragraph, 'runs'):
|
834 |
+
for run in paragraph.runs:
|
835 |
+
run.font.size = Pt(font_size_pt)
|
836 |
+
run.font.name = theme['fonts']['body']
|
837 |
+
run.font.color.rgb = theme['colors']['text']
|
838 |
+
except Exception as e:
|
839 |
+
logger.warning(f"Error setting font for paragraph: {e}")
|
840 |
+
continue
|
841 |
+
except Exception as e:
|
842 |
+
logger.warning(f"Error in force_font_size: {e}")
|
843 |
+
|
844 |
+
def apply_theme_to_slide(slide, theme: Dict, layout_type: str = 'title_content'):
|
845 |
+
"""Apply design theme to a slide with consistent styling"""
|
846 |
+
# Add colored background shape for all slides
|
847 |
+
bg_shape = slide.shapes.add_shape(
|
848 |
+
MSO_SHAPE.RECTANGLE, 0, 0, Inches(10), Inches(5.625)
|
849 |
+
)
|
850 |
+
bg_shape.fill.solid()
|
851 |
+
|
852 |
+
# Use lighter background for content slides
|
853 |
+
if layout_type in ['title_content', 'two_content', 'comparison']:
|
854 |
+
# Light background with subtle gradient effect
|
855 |
+
bg_shape.fill.fore_color.rgb = theme['colors']['background']
|
856 |
+
|
857 |
+
# Add accent strip at top
|
858 |
+
accent_strip = slide.shapes.add_shape(
|
859 |
+
MSO_SHAPE.RECTANGLE, 0, 0, Inches(10), Inches(0.5)
|
860 |
)
|
861 |
+
accent_strip.fill.solid()
|
862 |
+
accent_strip.fill.fore_color.rgb = theme['colors']['primary']
|
863 |
+
accent_strip.line.fill.background()
|
864 |
+
|
865 |
+
# Add bottom accent
|
866 |
+
bottom_strip = slide.shapes.add_shape(
|
867 |
+
MSO_SHAPE.RECTANGLE, 0, Inches(5.125), Inches(10), Inches(0.5)
|
868 |
+
)
|
869 |
+
bottom_strip.fill.solid()
|
870 |
+
bottom_strip.fill.fore_color.rgb = theme['colors']['secondary']
|
871 |
+
bottom_strip.fill.transparency = 0.7
|
872 |
+
bottom_strip.line.fill.background()
|
873 |
+
|
874 |
+
else:
|
875 |
+
# Section headers get primary color background
|
876 |
+
bg_shape.fill.fore_color.rgb = theme['colors']['primary']
|
877 |
|
878 |
+
bg_shape.line.fill.background()
|
879 |
+
|
880 |
+
# Move background shapes to back
|
881 |
+
slide.shapes._spTree.remove(bg_shape._element)
|
882 |
+
slide.shapes._spTree.insert(2, bg_shape._element)
|
883 |
+
|
884 |
+
# Apply title formatting if exists
|
885 |
+
if slide.shapes.title:
|
886 |
+
try:
|
887 |
+
title = slide.shapes.title
|
888 |
+
if title.text_frame and title.text_frame.paragraphs:
|
889 |
+
for paragraph in title.text_frame.paragraphs:
|
890 |
+
paragraph.font.name = theme['fonts']['title']
|
891 |
+
paragraph.font.bold = True
|
892 |
+
|
893 |
+
# UPDATED: Increased font sizes for better readability
|
894 |
+
if layout_type == 'section_header':
|
895 |
+
paragraph.font.size = Pt(28) # Increased from 20
|
896 |
+
paragraph.font.color.rgb = RGBColor(255, 255, 255)
|
897 |
+
paragraph.alignment = PP_ALIGN.CENTER
|
898 |
+
else:
|
899 |
+
paragraph.font.size = Pt(24) # Increased from 18
|
900 |
+
paragraph.font.color.rgb = theme['colors']['primary']
|
901 |
+
paragraph.alignment = PP_ALIGN.LEFT
|
902 |
+
except Exception as e:
|
903 |
+
logger.warning(f"Title formatting failed: {e}")
|
904 |
+
|
905 |
+
# Apply content formatting with improved readability
|
906 |
+
# NOTE: Do NOT add emojis here - they will be added in create_advanced_ppt_from_content
|
907 |
+
for shape in slide.shapes:
|
908 |
+
if shape.has_text_frame and shape != slide.shapes.title:
|
909 |
+
try:
|
910 |
+
text_frame = shape.text_frame
|
911 |
+
|
912 |
+
# Set text frame margins for better spacing
|
913 |
+
text_frame.margin_left = Inches(0.25)
|
914 |
+
text_frame.margin_right = Inches(0.25)
|
915 |
+
text_frame.margin_top = Inches(0.1)
|
916 |
+
text_frame.margin_bottom = Inches(0.1)
|
917 |
+
|
918 |
+
# Only apply font formatting, no content modification
|
919 |
+
if text_frame.text.strip():
|
920 |
+
# Use force_font_size helper to ensure font is applied
|
921 |
+
force_font_size(text_frame, 16, theme) # Increased from 12
|
922 |
+
|
923 |
+
for paragraph in text_frame.paragraphs:
|
924 |
+
# Add line spacing for better readability
|
925 |
+
paragraph.space_after = Pt(4) # Increased from 3
|
926 |
+
paragraph.line_spacing = 1.2 # Increased from 1.1
|
927 |
+
|
928 |
+
except Exception as e:
|
929 |
+
logger.warning(f"Content formatting failed: {e}")
|
930 |
+
|
931 |
+
def add_gradient_background(slide, color1: RGBColor, color2: RGBColor):
|
932 |
+
"""Add gradient-like background to slide using shapes"""
|
933 |
+
# Note: python-pptx doesn't directly support gradients in backgrounds,
|
934 |
+
# so we'll create a gradient effect using overlapping shapes
|
935 |
+
left = top = 0
|
936 |
+
width = Inches(10)
|
937 |
+
height = Inches(5.625)
|
938 |
+
|
939 |
+
# Add base color rectangle
|
940 |
+
shape1 = slide.shapes.add_shape(
|
941 |
+
MSO_SHAPE.RECTANGLE, left, top, width, height
|
942 |
+
)
|
943 |
+
shape1.fill.solid()
|
944 |
+
shape1.fill.fore_color.rgb = color1
|
945 |
+
shape1.line.fill.background()
|
946 |
+
|
947 |
+
# Add semi-transparent overlay for gradient effect
|
948 |
+
shape2 = slide.shapes.add_shape(
|
949 |
+
MSO_SHAPE.RECTANGLE, left, top, width, Inches(2.8)
|
950 |
+
)
|
951 |
+
shape2.fill.solid()
|
952 |
+
shape2.fill.fore_color.rgb = color2
|
953 |
+
shape2.fill.transparency = 0.5
|
954 |
+
shape2.line.fill.background()
|
955 |
+
|
956 |
+
# Move shapes to back
|
957 |
+
slide.shapes._spTree.remove(shape1._element)
|
958 |
+
slide.shapes._spTree.remove(shape2._element)
|
959 |
+
slide.shapes._spTree.insert(2, shape1._element)
|
960 |
+
slide.shapes._spTree.insert(3, shape2._element)
|
961 |
+
|
962 |
+
def add_decorative_shapes(slide, theme: Dict):
|
963 |
+
"""Add decorative shapes to enhance visual appeal"""
|
964 |
+
try:
|
965 |
+
# Add corner accent circle
|
966 |
+
shape1 = slide.shapes.add_shape(
|
967 |
+
MSO_SHAPE.OVAL,
|
968 |
+
Inches(9.3), Inches(4.8),
|
969 |
+
Inches(0.7), Inches(0.7)
|
970 |
+
)
|
971 |
+
shape1.fill.solid()
|
972 |
+
shape1.fill.fore_color.rgb = theme['colors']['accent']
|
973 |
+
shape1.fill.transparency = 0.3
|
974 |
+
shape1.line.fill.background()
|
975 |
+
|
976 |
+
# Add smaller accent
|
977 |
+
shape2 = slide.shapes.add_shape(
|
978 |
+
MSO_SHAPE.OVAL,
|
979 |
+
Inches(0.1), Inches(0.1),
|
980 |
+
Inches(0.4), Inches(0.4)
|
981 |
+
)
|
982 |
+
shape2.fill.solid()
|
983 |
+
shape2.fill.fore_color.rgb = theme['colors']['secondary']
|
984 |
+
shape2.fill.transparency = 0.5
|
985 |
+
shape2.line.fill.background()
|
986 |
+
|
987 |
+
except Exception as e:
|
988 |
+
logger.warning(f"Failed to add decorative shapes: {e}")
|
989 |
+
|
990 |
+
def create_chart_slide(slide, chart_data: Dict, theme: Dict):
|
991 |
+
"""Create a chart on the slide based on data"""
|
992 |
+
try:
|
993 |
+
# Add chart
|
994 |
+
x, y, cx, cy = Inches(1), Inches(2), Inches(8), Inches(4.5)
|
995 |
+
|
996 |
+
# Prepare chart data
|
997 |
+
chart_data_obj = CategoryChartData()
|
998 |
+
|
999 |
+
# Simple bar chart example
|
1000 |
+
if 'columns' in chart_data and 'sample_data' in chart_data:
|
1001 |
+
# Use first numeric column for chart
|
1002 |
+
numeric_cols = []
|
1003 |
+
for col in chart_data['columns']:
|
1004 |
+
try:
|
1005 |
+
# Check if column has numeric data
|
1006 |
+
float(chart_data['sample_data'][0].get(col, 0))
|
1007 |
+
numeric_cols.append(col)
|
1008 |
+
except:
|
1009 |
+
pass
|
1010 |
+
|
1011 |
+
if numeric_cols:
|
1012 |
+
categories = [str(row.get(chart_data['columns'][0], ''))
|
1013 |
+
for row in chart_data['sample_data'][:5]]
|
1014 |
+
chart_data_obj.categories = categories
|
1015 |
+
|
1016 |
+
for col in numeric_cols[:3]: # Max 3 series
|
1017 |
+
values = [float(row.get(col, 0))
|
1018 |
+
for row in chart_data['sample_data'][:5]]
|
1019 |
+
chart_data_obj.add_series(col, values)
|
1020 |
+
|
1021 |
+
chart = slide.shapes.add_chart(
|
1022 |
+
XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data_obj
|
1023 |
+
).chart
|
1024 |
+
|
1025 |
+
# Style the chart
|
1026 |
+
chart.has_legend = True
|
1027 |
+
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
|
1028 |
+
except Exception as e:
|
1029 |
+
logger.warning(f"Chart creation failed: {e}")
|
1030 |
+
# If chart fails, add a text placeholder instead
|
1031 |
+
textbox = slide.shapes.add_textbox(x, y, cx, cy)
|
1032 |
+
text_frame = textbox.text_frame
|
1033 |
+
text_frame.text = "๋ฐ์ดํฐ ์ฐจํธ (์ฐจํธ ์์ฑ ์คํจ)"
|
1034 |
+
text_frame.paragraphs[0].font.size = Pt(16) # Increased font size
|
1035 |
+
text_frame.paragraphs[0].font.color.rgb = theme['colors']['secondary']
|
1036 |
+
|
1037 |
+
def create_advanced_ppt_from_content(
|
1038 |
+
slides_data: list,
|
1039 |
+
topic: str,
|
1040 |
+
theme_name: str,
|
1041 |
+
include_charts: bool = False,
|
1042 |
+
include_ai_image: bool = False
|
1043 |
+
) -> str:
|
1044 |
+
"""Create advanced PPT file with consistent visual design and AI image option"""
|
1045 |
+
if not PPTX_AVAILABLE:
|
1046 |
+
raise ImportError("python-pptx library is required")
|
1047 |
+
|
1048 |
+
prs = Presentation()
|
1049 |
+
theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES['professional'])
|
1050 |
+
|
1051 |
+
# Set slide size (16:9)
|
1052 |
+
prs.slide_width = Inches(10)
|
1053 |
+
prs.slide_height = Inches(5.625)
|
1054 |
+
|
1055 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
1056 |
+
# 1) ์ ๋ชฉ ์ฌ๋ผ์ด๋(ํ์ง) ์์ฑ
|
1057 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
1058 |
+
title_slide_layout = prs.slide_layouts[0]
|
1059 |
+
slide = prs.slides.add_slide(title_slide_layout)
|
1060 |
+
|
1061 |
+
# ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋์ธํธ
|
1062 |
+
add_gradient_background(slide, theme['colors']['primary'], theme['colors']['secondary'])
|
1063 |
+
|
1064 |
+
# AI ์ด๋ฏธ์ง(ํ์ง ๊ทธ๋ฆผ)
|
1065 |
+
title_top_position = Inches(2.0)
|
1066 |
+
if include_ai_image and AI_IMAGE_ENABLED:
|
1067 |
+
logger.info("Generating AI cover image via API...")
|
1068 |
+
ai_image_path = generate_ai_cover_image_via_api(topic, slides_data)
|
1069 |
+
if ai_image_path and os.path.exists(ai_image_path):
|
1070 |
+
img = Image.open(ai_image_path)
|
1071 |
+
img_width, img_height = img.size
|
1072 |
+
|
1073 |
+
# ์ด๋ฏธ์ง ๋ ํฌ๊ฒ (60% ํญ, ์ต๋ ๋์ด 4")
|
1074 |
+
max_width = Inches(6)
|
1075 |
+
ratio = img_height / img_width
|
1076 |
+
img_w = max_width
|
1077 |
+
img_h = max_width * ratio
|
1078 |
+
max_height = Inches(4)
|
1079 |
+
if img_h > max_height:
|
1080 |
+
img_h = max_height
|
1081 |
+
img_w = max_height / ratio
|
1082 |
+
|
1083 |
+
left = (prs.slide_width - img_w) / 2
|
1084 |
+
top = Inches(0.6)
|
1085 |
+
|
1086 |
+
pic = slide.shapes.add_picture(ai_image_path, left, top, width=img_w, height=img_h)
|
1087 |
+
pic.shadow.inherit = False
|
1088 |
+
pic.shadow.visible = True
|
1089 |
+
pic.shadow.blur_radius = Pt(15)
|
1090 |
+
pic.shadow.distance = Pt(8)
|
1091 |
+
pic.shadow.angle = 45
|
1092 |
+
|
1093 |
+
title_top_position = top + img_h + Inches(0.35)
|
1094 |
+
|
1095 |
+
try:
|
1096 |
+
os.unlink(ai_image_path)
|
1097 |
+
except Exception as e:
|
1098 |
+
logger.warning(f"Temp image delete failed: {e}")
|
1099 |
+
|
1100 |
+
# ์ ๋ชฉ / ๋ถ์ ๋ชฉ ํ
์คํธ
|
1101 |
+
title_shape = slide.shapes.title
|
1102 |
+
subtitle_shape = slide.placeholders[1] if len(slide.placeholders) > 1 else None
|
1103 |
+
|
1104 |
+
if title_shape:
|
1105 |
+
title_shape.left = Inches(0.5)
|
1106 |
+
title_shape.width = prs.slide_width - Inches(1)
|
1107 |
+
title_shape.top = int(title_top_position)
|
1108 |
+
title_shape.height = Inches(1.2)
|
1109 |
+
|
1110 |
+
tf = title_shape.text_frame
|
1111 |
+
tf.clear()
|
1112 |
+
tf.text = topic
|
1113 |
+
p = tf.paragraphs[0]
|
1114 |
+
p.font.name = theme['fonts']['title']
|
1115 |
+
p.font.size = Pt(32)
|
1116 |
+
p.font.bold = True
|
1117 |
+
p.font.color.rgb = RGBColor(255, 255, 255)
|
1118 |
+
p.alignment = PP_ALIGN.CENTER
|
1119 |
+
|
1120 |
+
# ๊ฐ๋ก ๋ฐฉํฅ ๊ณ ์
|
1121 |
+
bodyPr = tf._txBody.bodyPr
|
1122 |
+
bodyPr.set('vert', 'horz')
|
1123 |
+
|
1124 |
+
# ์ต์์ ๋ ์ด์ด๋ก ์ด๋
|
1125 |
+
slide.shapes._spTree.remove(title_shape._element)
|
1126 |
+
slide.shapes._spTree.append(title_shape._element)
|
1127 |
+
|
1128 |
+
if subtitle_shape:
|
1129 |
+
subtitle_shape.left = Inches(0.5)
|
1130 |
+
subtitle_shape.width = prs.slide_width - Inches(1)
|
1131 |
+
subtitle_shape.top = int(title_top_position + Inches(1.0))
|
1132 |
+
subtitle_shape.height = Inches(0.9)
|
1133 |
+
|
1134 |
+
tf2 = subtitle_shape.text_frame
|
1135 |
+
tf2.clear()
|
1136 |
+
tf2.text = f"์๋ ์์ฑ๋ ํ๋ ์ ํ
์ด์
โข ์ด {len(slides_data)}์ฅ"
|
1137 |
+
p2 = tf2.paragraphs[0]
|
1138 |
+
p2.font.name = theme['fonts']['subtitle']
|
1139 |
+
p2.font.size = Pt(18)
|
1140 |
+
p2.font.color.rgb = RGBColor(255, 255, 255)
|
1141 |
+
p2.alignment = PP_ALIGN.CENTER
|
1142 |
+
|
1143 |
+
bodyPr2 = tf2._txBody.bodyPr
|
1144 |
+
bodyPr2.set('vert', 'horz')
|
1145 |
+
|
1146 |
+
slide.shapes._spTree.remove(subtitle_shape._element)
|
1147 |
+
slide.shapes._spTree.append(subtitle_shape._element)
|
1148 |
+
|
1149 |
+
# ์ฅ์ ์์
|
1150 |
+
add_decorative_shapes(slide, theme)
|
1151 |
+
|
1152 |
+
# Add content slides with consistent design
|
1153 |
+
for i, slide_data in enumerate(slides_data):
|
1154 |
+
layout_type = slide_data.get('layout', 'title_content')
|
1155 |
+
|
1156 |
+
# Log slide creation
|
1157 |
+
logger.info(f"Creating slide {i+1}: {slide_data.get('title', 'No title')}")
|
1158 |
+
logger.debug(f"Content length: {len(slide_data.get('content', ''))}")
|
1159 |
+
|
1160 |
+
# Choose appropriate layout
|
1161 |
+
if layout_type == 'section_header' and len(prs.slide_layouts) > 2:
|
1162 |
+
slide_layout = prs.slide_layouts[2]
|
1163 |
+
elif layout_type == 'two_content' and len(prs.slide_layouts) > 3:
|
1164 |
+
slide_layout = prs.slide_layouts[3]
|
1165 |
+
elif layout_type == 'comparison' and len(prs.slide_layouts) > 4:
|
1166 |
+
slide_layout = prs.slide_layouts[4]
|
1167 |
+
else:
|
1168 |
+
slide_layout = prs.slide_layouts[1] if len(prs.slide_layouts) > 1 else prs.slide_layouts[0]
|
1169 |
+
|
1170 |
+
slide = prs.slides.add_slide(slide_layout)
|
1171 |
+
|
1172 |
+
# Apply theme to EVERY slide for consistency
|
1173 |
+
apply_theme_to_slide(slide, theme, layout_type)
|
1174 |
+
|
1175 |
+
# Set title
|
1176 |
+
if slide.shapes.title:
|
1177 |
+
slide.shapes.title.text = slide_data.get('title', '์ ๋ชฉ ์์')
|
1178 |
+
# IMMEDIATELY set title font size after setting text
|
1179 |
+
try:
|
1180 |
+
title_text_frame = slide.shapes.title.text_frame
|
1181 |
+
if title_text_frame and title_text_frame.paragraphs:
|
1182 |
+
for paragraph in title_text_frame.paragraphs:
|
1183 |
+
if layout_type == 'section_header':
|
1184 |
+
paragraph.font.size = Pt(28) # Increased from 20
|
1185 |
+
paragraph.font.color.rgb = RGBColor(255, 255, 255)
|
1186 |
+
paragraph.alignment = PP_ALIGN.CENTER
|
1187 |
+
else:
|
1188 |
+
paragraph.font.size = Pt(24) # Increased from 18
|
1189 |
+
paragraph.font.color.rgb = theme['colors']['primary']
|
1190 |
+
paragraph.font.bold = True
|
1191 |
+
paragraph.font.name = theme['fonts']['title']
|
1192 |
+
except Exception as e:
|
1193 |
+
logger.warning(f"Title font sizing failed: {e}")
|
1194 |
+
|
1195 |
+
# Add content based on layout - COMPLETELY FIXED VERSION
|
1196 |
+
if layout_type == 'section_header':
|
1197 |
+
# Section header content handling
|
1198 |
+
content = slide_data.get('content', '')
|
1199 |
+
if content:
|
1200 |
+
logger.info(f"Adding content to section header slide {i+1}: {content[:50]}...")
|
1201 |
+
textbox = slide.shapes.add_textbox(
|
1202 |
+
Inches(1), Inches(3.5), Inches(8), Inches(1.5)
|
1203 |
+
)
|
1204 |
+
tf = textbox.text_frame
|
1205 |
+
tf.clear()
|
1206 |
+
tf.text = content
|
1207 |
+
tf.word_wrap = True
|
1208 |
+
|
1209 |
+
for paragraph in tf.paragraphs:
|
1210 |
+
paragraph.font.name = theme['fonts']['body']
|
1211 |
+
paragraph.font.size = Pt(16)
|
1212 |
+
paragraph.font.color.rgb = RGBColor(255, 255, 255)
|
1213 |
+
paragraph.alignment = PP_ALIGN.CENTER
|
1214 |
+
|
1215 |
+
# Add decorative line
|
1216 |
+
line = slide.shapes.add_shape(
|
1217 |
+
MSO_SHAPE.RECTANGLE, Inches(3), Inches(3.2), Inches(4), Pt(4)
|
1218 |
+
)
|
1219 |
+
line.fill.solid()
|
1220 |
+
line.fill.fore_color.rgb = RGBColor(255, 255, 255)
|
1221 |
+
line.line.fill.background()
|
1222 |
+
|
1223 |
+
elif layout_type == 'two_content':
|
1224 |
+
content = slide_data.get('content', '')
|
1225 |
+
if content:
|
1226 |
+
logger.info(f"Creating two-column layout for slide {i+1}")
|
1227 |
+
content_lines = content.split('\n')
|
1228 |
+
mid_point = len(content_lines) // 2
|
1229 |
+
|
1230 |
+
# Left column
|
1231 |
+
left_box = slide.shapes.add_textbox(
|
1232 |
+
Inches(0.5), Inches(1.5), Inches(4.5), Inches(3.5)
|
1233 |
+
)
|
1234 |
+
left_tf = left_box.text_frame
|
1235 |
+
left_tf.clear()
|
1236 |
+
left_content = '\n'.join(content_lines[:mid_point])
|
1237 |
+
if left_content:
|
1238 |
+
left_tf.text = left_content
|
1239 |
+
left_tf.word_wrap = True
|
1240 |
+
force_font_size(left_tf, 14, theme)
|
1241 |
+
|
1242 |
+
# Apply emoji bullets
|
1243 |
+
for paragraph in left_tf.paragraphs:
|
1244 |
+
text = paragraph.text.strip()
|
1245 |
+
if text and text.startswith(('-', 'โข', 'โ')) and not has_emoji(text):
|
1246 |
+
clean_text = text.lstrip('-โขโ ')
|
1247 |
+
emoji = get_emoji_for_content(clean_text)
|
1248 |
+
paragraph.text = f"{emoji} {clean_text}"
|
1249 |
+
force_font_size(left_tf, 14, theme)
|
1250 |
+
|
1251 |
+
# Right column
|
1252 |
+
right_box = slide.shapes.add_textbox(
|
1253 |
+
Inches(5), Inches(1.5), Inches(4.5), Inches(3.5)
|
1254 |
+
)
|
1255 |
+
right_tf = right_box.text_frame
|
1256 |
+
right_tf.clear()
|
1257 |
+
right_content = '\n'.join(content_lines[mid_point:])
|
1258 |
+
if right_content:
|
1259 |
+
right_tf.text = right_content
|
1260 |
+
right_tf.word_wrap = True
|
1261 |
+
force_font_size(right_tf, 14, theme)
|
1262 |
+
|
1263 |
+
# Apply emoji bullets
|
1264 |
+
for paragraph in right_tf.paragraphs:
|
1265 |
+
text = paragraph.text.strip()
|
1266 |
+
if text and text.startswith(('-', 'โข', 'โ')) and not has_emoji(text):
|
1267 |
+
clean_text = text.lstrip('-โขโ ')
|
1268 |
+
emoji = get_emoji_for_content(clean_text)
|
1269 |
+
paragraph.text = f"{emoji} {clean_text}"
|
1270 |
+
force_font_size(right_tf, 14, theme)
|
1271 |
+
|
1272 |
+
else:
|
1273 |
+
# Regular content - MOST IMPORTANT PART - COMPLETELY FIXED
|
1274 |
+
content = slide_data.get('content', '')
|
1275 |
+
|
1276 |
+
# ๋๋ฒ๊น
๋ก๊ทธ
|
1277 |
+
logger.info(f"Slide {i+1} - Content to add: '{content[:100]}...' (length: {len(content)})")
|
1278 |
+
|
1279 |
+
if include_charts and slide_data.get('chart_data'):
|
1280 |
+
create_chart_slide(slide, slide_data['chart_data'], theme)
|
1281 |
+
|
1282 |
+
# content๊ฐ ์์ผ๋ฉด ๋ฌด์กฐ๊ฑด ํ
์คํธ๋ฐ์ค ์ถ๊ฐ
|
1283 |
+
if content and content.strip():
|
1284 |
+
# ๋ช
ํํ ์์น์ ํฌ๊ธฐ๋ก ํ
์คํธ๋ฐ์ค ์์ฑ
|
1285 |
+
textbox = slide.shapes.add_textbox(
|
1286 |
+
Inches(0.5), # left
|
1287 |
+
Inches(1.5), # top
|
1288 |
+
Inches(9), # width
|
1289 |
+
Inches(3.5) # height
|
1290 |
+
)
|
1291 |
+
|
1292 |
+
tf = textbox.text_frame
|
1293 |
+
tf.clear()
|
1294 |
+
|
1295 |
+
# ํ
์คํธ ์ค์
|
1296 |
+
tf.text = content.strip()
|
1297 |
+
tf.word_wrap = True
|
1298 |
+
|
1299 |
+
# ํ
์คํธ ํ๋ ์ ์ฌ๋ฐฑ ์ค์
|
1300 |
+
tf.margin_left = Inches(0.1)
|
1301 |
+
tf.margin_right = Inches(0.1)
|
1302 |
+
tf.margin_top = Inches(0.05)
|
1303 |
+
tf.margin_bottom = Inches(0.05)
|
1304 |
+
|
1305 |
+
# ํฐํธ ๊ฐ์ ์ ์ฉ
|
1306 |
+
force_font_size(tf, 16, theme)
|
1307 |
+
|
1308 |
+
# ๊ฐ ๋จ๋ฝ์ ๋ํด ์ฒ๋ฆฌ
|
1309 |
+
for p_idx, paragraph in enumerate(tf.paragraphs):
|
1310 |
+
if paragraph.text.strip():
|
1311 |
+
# ์ด๋ชจ์ง ์ถ๊ฐ
|
1312 |
+
text = paragraph.text.strip()
|
1313 |
+
if text.startswith(('-', 'โข', 'โ')) and not has_emoji(text):
|
1314 |
+
clean_text = text.lstrip('-โขโ ')
|
1315 |
+
emoji = get_emoji_for_content(clean_text)
|
1316 |
+
paragraph.text = f"{emoji} {clean_text}"
|
1317 |
+
|
1318 |
+
# ํฐํธ ์ฌ์ ์ฉ - ๊ฐ run์ ๋ํด ๋ช
์์ ์ผ๋ก ์ค์
|
1319 |
+
if paragraph.runs:
|
1320 |
+
for run in paragraph.runs:
|
1321 |
+
run.font.size = Pt(16)
|
1322 |
+
run.font.name = theme['fonts']['body']
|
1323 |
+
run.font.color.rgb = theme['colors']['text']
|
1324 |
+
else:
|
1325 |
+
# runs๊ฐ ์์ผ๋ฉด ์์ฑ
|
1326 |
+
paragraph.font.size = Pt(16)
|
1327 |
+
paragraph.font.name = theme['fonts']['body']
|
1328 |
+
paragraph.font.color.rgb = theme['colors']['text']
|
1329 |
+
|
1330 |
+
# ๋จ๋ฝ ๊ฐ๊ฒฉ
|
1331 |
+
paragraph.space_before = Pt(6)
|
1332 |
+
paragraph.space_after = Pt(6)
|
1333 |
+
paragraph.line_spacing = 1.3
|
1334 |
+
|
1335 |
+
logger.info(f"Successfully added content to slide {i+1}")
|
1336 |
+
else:
|
1337 |
+
logger.warning(f"Slide {i+1} has no content or empty content")
|
1338 |
+
|
1339 |
+
# Add slide notes if available
|
1340 |
+
if slide_data.get('notes'):
|
1341 |
+
try:
|
1342 |
+
notes_slide = slide.notes_slide
|
1343 |
+
notes_text_frame = notes_slide.notes_text_frame
|
1344 |
+
notes_text_frame.text = slide_data.get('notes', '')
|
1345 |
+
except Exception as e:
|
1346 |
+
logger.warning(f"Failed to add slide notes: {e}")
|
1347 |
+
|
1348 |
+
# Add slide number with better design
|
1349 |
+
slide_number_bg = slide.shapes.add_shape(
|
1350 |
+
MSO_SHAPE.ROUNDED_RECTANGLE,
|
1351 |
+
Inches(8.3), Inches(5.0), Inches(1.5), Inches(0.5)
|
1352 |
)
|
1353 |
+
slide_number_bg.fill.solid()
|
1354 |
+
slide_number_bg.fill.fore_color.rgb = theme['colors']['primary']
|
1355 |
+
slide_number_bg.fill.transparency = 0.8
|
1356 |
+
slide_number_bg.line.fill.background()
|
1357 |
|
1358 |
+
slide_number_box = slide.shapes.add_textbox(
|
1359 |
+
Inches(8.3), Inches(5.05), Inches(1.5), Inches(0.4)
|
|
|
|
|
|
|
1360 |
)
|
1361 |
+
slide_number_frame = slide_number_box.text_frame
|
1362 |
+
slide_number_frame.text = f"{i + 1} / {len(slides_data)}"
|
1363 |
+
slide_number_frame.paragraphs[0].font.size = Pt(10) # Increased from 8
|
1364 |
+
slide_number_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
|
1365 |
+
slide_number_frame.paragraphs[0].font.bold = False
|
1366 |
+
slide_number_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
|
1367 |
+
|
1368 |
+
# Add subtle design element on alternating slides
|
1369 |
+
if i % 2 == 0:
|
1370 |
+
accent_shape = slide.shapes.add_shape(
|
1371 |
+
MSO_SHAPE.OVAL,
|
1372 |
+
Inches(9.6), Inches(0.1),
|
1373 |
+
Inches(0.2), Inches(0.2)
|
1374 |
+
)
|
1375 |
+
accent_shape.fill.solid()
|
1376 |
+
accent_shape.fill.fore_color.rgb = theme['colors']['accent']
|
1377 |
+
accent_shape.line.fill.background()
|
1378 |
+
|
1379 |
+
# Add thank you slide with consistent design
|
1380 |
+
thank_you_layout = prs.slide_layouts[5] if len(prs.slide_layouts) > 5 else prs.slide_layouts[0]
|
1381 |
+
thank_you_slide = prs.slides.add_slide(thank_you_layout)
|
1382 |
+
|
1383 |
+
# Apply gradient background
|
1384 |
+
add_gradient_background(thank_you_slide, theme['colors']['secondary'], theme['colors']['primary'])
|
1385 |
|
1386 |
+
if thank_you_slide.shapes.title:
|
1387 |
+
thank_you_slide.shapes.title.text = "๊ฐ์ฌํฉ๋๋ค"
|
1388 |
+
try:
|
1389 |
+
if thank_you_slide.shapes.title.text_frame and thank_you_slide.shapes.title.text_frame.paragraphs:
|
1390 |
+
thank_you_slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(36) # Increased from 28
|
1391 |
+
thank_you_slide.shapes.title.text_frame.paragraphs[0].font.bold = True
|
1392 |
+
thank_you_slide.shapes.title.text_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
|
1393 |
+
thank_you_slide.shapes.title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
|
1394 |
+
except Exception as e:
|
1395 |
+
logger.warning(f"Thank you slide styling failed: {e}")
|
1396 |
+
|
1397 |
+
# Add contact or additional info placeholder
|
1398 |
+
info_box = thank_you_slide.shapes.add_textbox(
|
1399 |
+
Inches(2), Inches(3.5), Inches(6), Inches(1)
|
1400 |
+
)
|
1401 |
+
info_tf = info_box.text_frame
|
1402 |
+
info_tf.text = "AI๋ก ์์ฑ๋ ํ๋ ์ ํ
์ด์
"
|
1403 |
+
info_tf.paragraphs[0].font.size = Pt(18) # Increased from 14
|
1404 |
+
info_tf.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
|
1405 |
+
info_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
|
1406 |
+
|
1407 |
+
# Save to temporary file
|
1408 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pptx") as tmp_file:
|
1409 |
+
prs.save(tmp_file.name)
|
1410 |
+
return tmp_file.name
|
1411 |
+
|
1412 |
+
##############################################################################
|
1413 |
+
# Streaming Response Handler for PPT Generation - IMPROVED VERSION
|
1414 |
+
##############################################################################
|
1415 |
+
def generate_ppt_content(topic: str, num_slides: int, additional_context: str, use_korean: bool = False, layout_style: str = "consistent") -> Iterator[str]:
|
1416 |
+
"""Generate PPT content using LLM with clearer format"""
|
1417 |
+
|
1418 |
+
# Layout instructions based on style
|
1419 |
+
layout_instructions = ""
|
1420 |
+
if layout_style == "varied":
|
1421 |
+
layout_instructions = """
|
1422 |
+
์ฌ๋ผ์ด๋ ๋ ์ด์์์ ๋ค์ํ๊ฒ ๊ตฌ์ฑํด์ฃผ์ธ์:
|
1423 |
+
- ๋งค 5๋ฒ์งธ ์ฌ๋ผ์ด๋๋ '์น์
๊ตฌ๋ถ' ์ฌ๋ผ์ด๋๋ก ๋ง๋ค์ด์ฃผ์ธ์
|
1424 |
+
- ๋น๊ต๋ ๋์กฐ ๋ด์ฉ์ด ์์ผ๋ฉด '๋น๊ต' ๋ ์ด์์์ ์ฌ์ฉํ์ธ์
|
1425 |
+
- ๋ด์ฉ์ด ๋ง์ผ๋ฉด 2๋จ ๊ตฌ์ฑ์ ๊ณ ๋ คํ์ธ์
|
1426 |
+
"""
|
1427 |
+
elif layout_style == "consistent":
|
1428 |
+
layout_instructions = """
|
1429 |
+
์ผ๊ด๋ ๋ ์ด์์์ ์ ์งํด์ฃผ์ธ์:
|
1430 |
+
- ๋ชจ๋ ์ฌ๋ผ์ด๋๋ ๋์ผํ ๊ตฌ์กฐ๋ก ์์ฑ
|
1431 |
+
- ์ ๋ชฉ๊ณผ ๊ธ๋จธ๋ฆฌ ๊ธฐํธ ํ์ ํต์ผ
|
1432 |
+
- ๊ฐ๊ฒฐํ๊ณ ๋ช
ํํ ๊ตฌ์ฑ
|
1433 |
+
"""
|
1434 |
+
|
1435 |
+
# ๋ ๋ช
ํํ ์์คํ
ํ๋กฌํํธ
|
1436 |
+
if use_korean:
|
1437 |
+
system_prompt = f"""๋น์ ์ ์ ๋ฌธ์ ์ธ PPT ํ๋ ์ ํ
์ด์
์์ฑ ์ ๋ฌธ๊ฐ์
๋๋ค.
|
1438 |
+
์ฃผ์ด์ง ์ฃผ์ ์ ๋ํด ์ ํํ {num_slides}์ฅ์ ์ฌ๋ผ์ด๋ ๋ด์ฉ์ ์์ฑํด์ฃผ์ธ์.
|
1439 |
+
|
1440 |
+
**๋ฐ๋์ ์๋ ํ์์ ์ ํํ ๋ฐ๋ผ์ฃผ์ธ์:**
|
1441 |
+
|
1442 |
+
์ฌ๋ผ์ด๋ 1
|
1443 |
+
์ ๋ชฉ: [์ฌ๋ผ์ด๋ ์ ๋ชฉ - "์ฌ๋ผ์ด๋ 1" ๊ฐ์ ๋ฒํธ๋ ํฌํจํ์ง ๋ง์ธ์]
|
1444 |
+
๋ด์ฉ:
|
1445 |
+
- ์ฒซ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1446 |
+
- ๋ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1447 |
+
- ์ธ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1448 |
+
- ๋ค ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1449 |
+
- ๋ค์ฏ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1450 |
+
๋
ธํธ: [๋ฐํ์๊ฐ ์ด ์ฌ๋ผ์ด๋๋ฅผ ์ค๋ช
ํ ๋ ์ฌ์ฉํ ๊ตฌ์ด์ฒด ์คํฌ๋ฆฝํธ]
|
1451 |
+
|
1452 |
+
์ฌ๋ผ์ด๋ 2
|
1453 |
+
์ ๋ชฉ: [์ฌ๋ผ์ด๋ ์ ๋ชฉ]
|
1454 |
+
๋ด์ฉ:
|
1455 |
+
- ์ฒซ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1456 |
+
- ๋ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1457 |
+
- ์ธ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1458 |
+
- ๋ค ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1459 |
+
- ๋ค์ฏ ๋ฒ์งธ ํต์ฌ ํฌ์ธํธ
|
1460 |
+
๋
ธํธ: [๋ฐํ ์คํฌ๋ฆฝํธ]
|
1461 |
+
|
1462 |
+
(์ด๋ฐ ์์ผ๋ก ์ฌ๋ผ์ด๋ {num_slides}๊น์ง ๊ณ์)
|
1463 |
+
|
1464 |
+
{layout_instructions}
|
1465 |
+
|
1466 |
+
**์ค์ ์ง์นจ:**
|
1467 |
+
1. ๊ฐ ์ฌ๋ผ์ด๋๋ "์ฌ๋ผ์ด๋ ์ซ์"๋ก ์์
|
1468 |
+
2. ์ ๋ชฉ: ๋ค์ ์ค์ ์ ๋ชฉ ์์ฑ (๋ฒํธ ์ ์ธ)
|
1469 |
+
3. ๋ด์ฉ: ๋ค์ ์ ํํ 5๊ฐ์ ๊ธ๋จธ๋ฆฌ ๊ธฐํธ ํฌ์ธํธ
|
1470 |
+
4. ๋
ธํธ: ๋ค์ ๋ฐํ ์คํฌ๋ฆฝํธ
|
1471 |
+
5. ๊ฐ ์น์
์ฌ์ด์ ๋น ์ค ์์
|
1472 |
+
6. ์ด {num_slides}์ฅ ์์ฑ
|
1473 |
+
7. ๊ฐ ํฌ์ธํธ๋ '-' ๊ธฐํธ๋ก ์์ํ์ธ์ (์ด๋ชจ์ง๋ ์๋์ผ๋ก ์ถ๊ฐ๋ฉ๋๋ค)
|
1474 |
+
8. ๋
ธํธ๋ ํด๋น ์ฌ๋ผ์ด๋์ ๋ด์ฉ์ ๋ฐํ์๊ฐ ์ฒญ์ค์๊ฒ ์ค๋ช
ํ๋ ๊ตฌ์ด์ฒด ๋๋ณธ์ผ๋ก ์์ฑํ์ธ์"""
|
1475 |
+
else:
|
1476 |
+
system_prompt = f"""You are a professional PPT presentation expert.
|
1477 |
+
Create content for exactly {num_slides} slides on the given topic.
|
1478 |
+
|
1479 |
+
**You MUST follow this exact format:**
|
1480 |
+
|
1481 |
+
Slide 1
|
1482 |
+
Title: [Slide title - do NOT include "Slide 1" in the title]
|
1483 |
+
Content:
|
1484 |
+
- First key point
|
1485 |
+
- Second key point
|
1486 |
+
- Third key point
|
1487 |
+
- Fourth key point
|
1488 |
+
- Fifth key point
|
1489 |
+
Notes: [Speaker script in conversational style for explaining this slide]
|
1490 |
+
|
1491 |
+
Slide 2
|
1492 |
+
Title: [Slide title]
|
1493 |
+
Content:
|
1494 |
+
- First key point
|
1495 |
+
- Second key point
|
1496 |
+
- Third key point
|
1497 |
+
- Fourth key point
|
1498 |
+
- Fifth key point
|
1499 |
+
Notes: [Speaker script]
|
1500 |
+
|
1501 |
+
(Continue this way until Slide {num_slides})
|
1502 |
+
|
1503 |
+
**Important instructions:**
|
1504 |
+
1. Each slide starts with "Slide number"
|
1505 |
+
2. Title: followed by the actual title (no numbers)
|
1506 |
+
3. Content: followed by exactly 5 bullet points
|
1507 |
+
4. Notes: followed by speaker script
|
1508 |
+
5. No empty lines between sections
|
1509 |
+
6. Create exactly {num_slides} slides
|
1510 |
+
7. Start each point with '-' (emojis will be added automatically)
|
1511 |
+
8. Notes should be a speaker script explaining the slide content in conversational style"""
|
1512 |
+
|
1513 |
+
# Add search results if web search is performed
|
1514 |
+
if additional_context:
|
1515 |
+
system_prompt += f"\n\n์ฐธ๊ณ ์ ๋ณด:\n{additional_context}"
|
1516 |
+
|
1517 |
+
# Prepare messages
|
1518 |
+
user_prompt = f"์ฃผ์ : {topic}\n\n์์์ ์ค๋ช
ํ ํ์์ ๋ง์ถฐ ์ ํํ {num_slides}์ฅ์ PPT ์ฌ๋ผ์ด๋ ๋ด์ฉ์ ์์ฑํด์ฃผ์ธ์. ๊ฐ ์ฌ๋ผ์ด๋๋ง๋ค 5๊ฐ์ ํต์ฌ ํฌ์ธํธ์ ํจ๊ป, ๋ฐํ์๊ฐ ์ฒญ์ค์๊ฒ ํด๋น ๋ด์ฉ์ ์ค๋ช
ํ๋ ๊ตฌ์ด์ฒด ๋ฐํ ๋๋ณธ์ ๋
ธํธ๋ก ์์ฑํด์ฃผ์ธ์."
|
1519 |
+
if not use_korean:
|
1520 |
+
user_prompt = f"Topic: {topic}\n\nPlease create exactly {num_slides} PPT slides following the format described above. Include exactly 5 key points per slide, and write speaker notes as a conversational script explaining the content to the audience."
|
1521 |
+
|
1522 |
+
messages = [
|
1523 |
+
{"role": "system", "content": system_prompt},
|
1524 |
+
{"role": "user", "content": user_prompt}
|
1525 |
+
]
|
1526 |
+
|
1527 |
+
# Call LLM API
|
1528 |
headers = {
|
1529 |
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
|
1530 |
"Content-Type": "application/json"
|
|
|
1533 |
payload = {
|
1534 |
"model": FRIENDLI_MODEL_ID,
|
1535 |
"messages": messages,
|
1536 |
+
"max_tokens": min(4000, num_slides * 300), # More tokens for 5 points + notes
|
1537 |
+
"top_p": 0.9,
|
1538 |
+
"temperature": 0.8,
|
1539 |
"stream": True,
|
1540 |
"stream_options": {
|
1541 |
"include_usage": True
|
|
|
1573 |
logger.warning(f"JSON parsing failed: {data_str}")
|
1574 |
continue
|
1575 |
|
|
|
|
|
|
|
|
|
|
|
1576 |
except Exception as e:
|
1577 |
+
logger.error(f"LLM API error: {str(e)}")
|
1578 |
+
yield f"โ ๏ธ Error generating content: {str(e)}"
|
1579 |
|
1580 |
##############################################################################
|
1581 |
+
# Main PPT Generation Function - IMPROVED VERSION with API Image
|
1582 |
##############################################################################
|
1583 |
+
def generate_ppt(
|
1584 |
+
topic: str,
|
1585 |
+
num_slides: int = 10,
|
|
|
|
|
1586 |
use_web_search: bool = False,
|
1587 |
+
use_korean: bool = True,
|
1588 |
+
reference_files: list = None,
|
1589 |
+
design_theme: str = "professional",
|
1590 |
+
font_style: str = "modern",
|
1591 |
+
layout_style: str = "consistent",
|
1592 |
+
include_charts: bool = False,
|
1593 |
+
include_ai_image: bool = False
|
1594 |
+
) -> tuple:
|
1595 |
+
"""Main function to generate PPT with advanced design and API-based AI image"""
|
1596 |
+
|
1597 |
+
if not PPTX_AVAILABLE:
|
1598 |
+
return None, "โ python-pptx ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ค์น๋์ง ์์์ต๋๋ค.\n\n์ค์น ๋ช
๋ น: pip install python-pptx", ""
|
1599 |
+
|
1600 |
+
if not topic.strip():
|
1601 |
+
return None, "โ PPT ์ฃผ์ ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.", ""
|
1602 |
+
|
1603 |
+
if num_slides < 3 or num_slides > 20:
|
1604 |
+
return None, "โ ์ฌ๋ผ์ด๋ ์๋ 3์ฅ ์ด์ 20์ฅ ์ดํ๋ก ์ค์ ํด์ฃผ์ธ์.", ""
|
1605 |
|
1606 |
try:
|
1607 |
+
# AI ์ด๋ฏธ์ง ์์ฑ์ด ์์ฒญ๋์์ง๋ง ์ด๊ธฐํ๋์ง ์์ ๊ฒฝ์ฐ
|
1608 |
+
if include_ai_image and not AI_IMAGE_ENABLED:
|
1609 |
+
yield None, "๐ AI ์ด๋ฏธ์ง ์์ฑ API์ ์ฐ๊ฒฐํ๋ ์ค...", ""
|
1610 |
+
if initialize_ai_image_api():
|
1611 |
+
yield None, "โ
AI ์ด๋ฏธ์ง API ์ฐ๊ฒฐ ์ฑ๊ณต!", ""
|
1612 |
+
else:
|
1613 |
+
include_ai_image = False
|
1614 |
+
yield None, "โ ๏ธ AI ์ด๋ฏธ์ง API ์ฐ๊ฒฐ ์คํจ. AI ์ด๋ฏธ์ง ์์ด ์งํํฉ๋๋ค.", ""
|
1615 |
|
1616 |
+
# Process reference files if provided
|
1617 |
+
additional_context = ""
|
1618 |
+
chart_data = None
|
1619 |
+
if reference_files:
|
1620 |
+
file_contents = []
|
1621 |
+
for file_path in reference_files:
|
1622 |
+
if file_path.lower().endswith(".csv"):
|
1623 |
+
csv_content = analyze_csv_file(file_path)
|
1624 |
+
file_contents.append(csv_content)
|
1625 |
+
# Extract chart data if available
|
1626 |
+
if "CHART_DATA:" in csv_content:
|
1627 |
+
chart_json = csv_content.split("CHART_DATA:")[1]
|
1628 |
+
try:
|
1629 |
+
chart_data = json.loads(chart_json)
|
1630 |
+
except:
|
1631 |
+
pass
|
1632 |
+
elif file_path.lower().endswith(".txt"):
|
1633 |
+
file_contents.append(analyze_txt_file(file_path))
|
1634 |
+
elif file_path.lower().endswith(".pdf"):
|
1635 |
+
file_contents.append(pdf_to_markdown(file_path))
|
1636 |
+
|
1637 |
+
if file_contents:
|
1638 |
+
additional_context = "\n\n".join(file_contents)
|
1639 |
|
1640 |
+
# Web search if enabled
|
1641 |
if use_web_search:
|
1642 |
+
search_query = extract_keywords(topic, top_k=5)
|
1643 |
+
search_results = do_web_search(search_query, use_korean=use_korean)
|
1644 |
+
if not search_results.startswith("Web search"):
|
1645 |
+
additional_context += f"\n\n{search_results}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1646 |
|
1647 |
+
# Generate PPT content
|
1648 |
+
llm_response = ""
|
1649 |
+
for response in generate_ppt_content(topic, num_slides, additional_context, use_korean, layout_style):
|
1650 |
+
llm_response = response
|
1651 |
+
yield None, f"๐ ์์ฑ ์ค...\n\n{response}", response
|
1652 |
|
1653 |
+
# Parse LLM response
|
1654 |
+
slides_data = parse_llm_ppt_response(llm_response, layout_style)
|
1655 |
|
1656 |
+
# Debug: ํ์ฑ๋ ๊ฐ ์ฌ๋ผ์ด๋ ๋ด์ฉ ์ถ๋ ฅ
|
1657 |
+
logger.info(f"=== Parsed Slides Debug Info ===")
|
1658 |
+
for i, slide in enumerate(slides_data):
|
1659 |
+
logger.info(f"Slide {i+1}:")
|
1660 |
+
logger.info(f" Title: {slide.get('title', 'NO TITLE')}")
|
1661 |
+
logger.info(f" Content: {slide.get('content', 'NO CONTENT')[:100]}...")
|
1662 |
+
logger.info(f" Content Length: {len(slide.get('content', ''))}")
|
1663 |
+
logger.info("---")
|
1664 |
|
1665 |
+
# Add chart data to relevant slides if available
|
1666 |
+
if chart_data and include_charts:
|
1667 |
+
for slide in slides_data:
|
1668 |
+
if '๋ฐ์ดํฐ' in slide.get('title', '') or 'data' in slide.get('title', '').lower():
|
1669 |
+
slide['chart_data'] = chart_data
|
1670 |
+
break
|
1671 |
+
|
1672 |
+
# Debug logging
|
1673 |
+
logger.info(f"Parsed {len(slides_data)} slides from LLM response")
|
1674 |
+
logger.info(f"Design theme: {design_theme}, Layout style: {layout_style}")
|
1675 |
+
|
1676 |
+
if not slides_data:
|
1677 |
+
# Show the raw response for debugging
|
1678 |
+
error_msg = "โ PPT ๋ด์ฉ ํ์ฑ์ ์คํจํ์ต๋๋ค.\n\n"
|
1679 |
+
error_msg += "LLM ์๋ต์ ํ์ธํด์ฃผ์ธ์:\n"
|
1680 |
+
error_msg += "=" * 50 + "\n"
|
1681 |
+
error_msg += llm_response[:500] + "..." if len(llm_response) > 500 else llm_response
|
1682 |
+
yield None, error_msg, llm_response
|
1683 |
+
return
|
1684 |
+
|
1685 |
+
# AI ์ด๋ฏธ์ง ์์ฑ ์๋ฆผ
|
1686 |
+
if include_ai_image and AI_IMAGE_ENABLED:
|
1687 |
+
yield None, f"๐ ์ฌ๋ผ์ด๋ ์์ฑ ์๋ฃ!\n\n๐จ AI 3D ํ์ง ์ด๋ฏธ์ง ์์ฑ ์ค... (30์ด ์ ๋ ์์)", llm_response
|
1688 |
+
|
1689 |
+
# Create PPT file with advanced design
|
1690 |
+
ppt_path = create_advanced_ppt_from_content(slides_data, topic, design_theme, include_charts, include_ai_image)
|
1691 |
+
|
1692 |
+
success_msg = f"โ
PPT ํ์ผ์ด ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋์์ต๋๋ค!\n\n"
|
1693 |
+
success_msg += f"๐ ์ฃผ์ : {topic}\n"
|
1694 |
+
success_msg += f"๐ ์ฌ๋ผ์ด๋ ์: {len(slides_data)}์ฅ\n"
|
1695 |
+
success_msg += f"๐จ ๋์์ธ ํ
๋ง: {DESIGN_THEMES[design_theme]['name']}\n"
|
1696 |
+
success_msg += f"๐ ๋ ์ด์์ ์คํ์ผ: {layout_style}\n"
|
1697 |
+
if include_ai_image and AI_IMAGE_ENABLED:
|
1698 |
+
success_msg += f"๐ผ๏ธ AI ์์ฑ 3D ์คํ์ผ ํ์ง ์ด๋ฏธ์ง ํฌํจ\n"
|
1699 |
+
success_msg += f"๐ ์์ฑ๋ ์ฌ๋ผ์ด๋:\n"
|
1700 |
+
|
1701 |
+
for i, slide in enumerate(slides_data[:5]): # Show first 5 slides
|
1702 |
+
success_msg += f" {i+1}. {slide.get('title', '์ ๋ชฉ ์์')} [{slide.get('layout', 'standard')}]\n"
|
1703 |
+
if slide.get('notes'):
|
1704 |
+
success_msg += f" ๐ก ๋
ธํธ: {slide.get('notes', '')[:50]}...\n"
|
1705 |
+
|
1706 |
+
if len(slides_data) > 5:
|
1707 |
+
success_msg += f" ... ์ธ {len(slides_data) - 5}์ฅ"
|
1708 |
+
|
1709 |
+
yield ppt_path, success_msg, llm_response
|
1710 |
|
|
|
|
|
|
|
|
|
1711 |
except Exception as e:
|
1712 |
+
logger.error(f"PPT generation error: {str(e)}")
|
1713 |
+
import traceback
|
1714 |
+
error_details = traceback.format_exc()
|
1715 |
+
logger.error(f"Error details: {error_details}")
|
1716 |
+
yield None, f"โ PPT ์์ฑ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}\n\n์์ธ ์ค๋ฅ:\n{error_details}", ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1717 |
|
1718 |
##############################################################################
|
1719 |
+
# Gradio UI
|
1720 |
##############################################################################
|
1721 |
css = """
|
1722 |
/* Full width UI */
|
1723 |
.gradio-container {
|
1724 |
+
background: rgba(255, 255, 255, 0.98);
|
1725 |
+
padding: 40px 50px;
|
1726 |
+
margin: 30px auto;
|
1727 |
width: 100% !important;
|
1728 |
+
max-width: 1400px !important;
|
1729 |
+
border-radius: 20px;
|
1730 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
|
|
|
|
|
1731 |
}
|
1732 |
|
1733 |
/* Background */
|
1734 |
body {
|
1735 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
1736 |
margin: 0;
|
1737 |
padding: 0;
|
1738 |
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
|
|
1739 |
}
|
1740 |
|
1741 |
+
/* Title styling */
|
1742 |
+
h1 {
|
1743 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
1744 |
+
background-clip: text;
|
1745 |
+
-webkit-background-clip: text;
|
1746 |
+
-webkit-text-fill-color: transparent;
|
1747 |
+
font-weight: 700;
|
1748 |
+
margin-bottom: 10px;
|
1749 |
+
}
|
1750 |
+
|
1751 |
+
/* Button styles */
|
1752 |
+
button.primary {
|
1753 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
1754 |
border: none;
|
1755 |
color: white !important;
|
|
|
|
|
1756 |
font-weight: 600;
|
1757 |
+
padding: 15px 30px !important;
|
1758 |
+
font-size: 18px !important;
|
|
|
1759 |
transition: all 0.3s ease;
|
1760 |
+
text-transform: uppercase;
|
1761 |
+
letter-spacing: 1px;
|
1762 |
}
|
1763 |
|
1764 |
+
button.primary:hover {
|
1765 |
+
transform: translateY(-3px);
|
1766 |
+
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1767 |
}
|
1768 |
|
1769 |
+
/* Input styles */
|
1770 |
+
.textbox, textarea, input[type="text"], input[type="number"] {
|
1771 |
+
border: 2px solid #e5e7eb;
|
1772 |
+
border-radius: 12px;
|
1773 |
+
padding: 15px;
|
1774 |
+
font-size: 16px;
|
1775 |
+
transition: all 0.3s ease;
|
1776 |
+
background: white;
|
|
|
1777 |
}
|
1778 |
|
1779 |
+
.textbox:focus, textarea:focus, input[type="text"]:focus {
|
1780 |
+
border-color: #667eea;
|
1781 |
+
outline: none;
|
1782 |
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
|
1783 |
}
|
1784 |
|
1785 |
+
/* Card style */
|
1786 |
+
.card {
|
1787 |
+
background: white;
|
1788 |
+
border-radius: 16px;
|
1789 |
+
padding: 25px;
|
1790 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
1791 |
+
margin-bottom: 25px;
|
1792 |
+
border: 1px solid rgba(102, 126, 234, 0.1);
|
1793 |
}
|
1794 |
|
1795 |
+
/* Dropdown styles */
|
1796 |
+
.dropdown {
|
1797 |
+
border: 2px solid #e5e7eb;
|
1798 |
+
border-radius: 12px;
|
1799 |
+
padding: 12px;
|
1800 |
+
background: white;
|
1801 |
+
transition: all 0.3s ease;
|
1802 |
}
|
1803 |
|
1804 |
+
.dropdown:hover {
|
1805 |
+
border-color: #667eea;
|
|
|
|
|
|
|
|
|
|
|
1806 |
}
|
1807 |
|
1808 |
+
/* Slider styles */
|
1809 |
+
.gr-slider input[type="range"] {
|
1810 |
+
background: linear-gradient(to right, #667eea 0%, #764ba2 100%);
|
1811 |
+
height: 8px;
|
1812 |
+
border-radius: 4px;
|
1813 |
}
|
1814 |
|
1815 |
+
/* File upload area */
|
1816 |
+
.file-upload {
|
1817 |
+
border: 3px dashed #667eea;
|
1818 |
+
border-radius: 16px;
|
1819 |
+
padding: 40px;
|
1820 |
+
text-align: center;
|
1821 |
+
transition: all 0.3s ease;
|
1822 |
+
background: rgba(102, 126, 234, 0.02);
|
1823 |
}
|
1824 |
|
1825 |
+
.file-upload:hover {
|
1826 |
+
border-color: #764ba2;
|
1827 |
+
background: rgba(102, 126, 234, 0.05);
|
1828 |
+
transform: scale(1.01);
|
1829 |
}
|
1830 |
|
1831 |
+
/* Checkbox styles */
|
1832 |
+
input[type="checkbox"] {
|
1833 |
+
width: 20px;
|
1834 |
+
height: 20px;
|
1835 |
+
margin-right: 10px;
|
1836 |
+
cursor: pointer;
|
1837 |
}
|
1838 |
|
1839 |
+
/* Tab styles */
|
1840 |
+
.tabs {
|
1841 |
+
border-radius: 12px;
|
1842 |
+
overflow: hidden;
|
1843 |
+
margin-bottom: 20px;
|
1844 |
}
|
1845 |
|
1846 |
+
.tab-nav {
|
1847 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
1848 |
+
padding: 5px;
|
1849 |
}
|
1850 |
|
1851 |
+
.tab-nav button {
|
1852 |
+
background: transparent;
|
1853 |
+
color: white;
|
1854 |
+
border: none;
|
1855 |
+
padding: 10px 20px;
|
1856 |
+
margin: 0 5px;
|
1857 |
+
border-radius: 8px;
|
1858 |
+
transition: all 0.3s ease;
|
1859 |
}
|
1860 |
|
1861 |
+
.tab-nav button.selected {
|
1862 |
+
background: white;
|
1863 |
+
color: #667eea;
|
|
|
1864 |
}
|
1865 |
|
1866 |
+
/* Section headers */
|
1867 |
+
.section-header {
|
1868 |
+
font-size: 20px;
|
1869 |
+
font-weight: 600;
|
1870 |
+
color: #667eea;
|
1871 |
+
margin: 20px 0 15px 0;
|
1872 |
+
padding-bottom: 10px;
|
1873 |
+
border-bottom: 2px solid rgba(102, 126, 234, 0.2);
|
1874 |
}
|
1875 |
|
1876 |
+
/* Status box styling */
|
1877 |
+
.status-box {
|
1878 |
+
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
1879 |
+
border-radius: 12px;
|
1880 |
+
padding: 20px;
|
1881 |
}
|
1882 |
|
1883 |
+
/* Preview box styling */
|
1884 |
+
.preview-box {
|
1885 |
+
background: #f8f9fa;
|
1886 |
+
border-radius: 12px;
|
1887 |
+
padding: 20px;
|
1888 |
+
font-family: 'Courier New', monospace;
|
1889 |
+
font-size: 13px;
|
1890 |
+
line-height: 1.5;
|
1891 |
+
max-height: 500px;
|
1892 |
+
overflow-y: auto;
|
1893 |
}
|
1894 |
"""
|
1895 |
|
1896 |
+
with gr.Blocks(css=css, title="AI PPT Generator Pro") as demo:
|
1897 |
+
gr.Markdown(
|
1898 |
+
"""
|
1899 |
+
# ๐ฏ AI ๊ธฐ๋ฐ PPT ์๋ ์์ฑ ์์คํ
Pro
|
1900 |
+
|
1901 |
+
๊ณ ๊ธ ๋์์ธ ํ
๋ง์ ๋ ์ด์์์ ํ์ฉํ ์ ๋ฌธ์ ์ธ ํ๋ ์ ํ
์ด์
์ ์๋์ผ๋ก ์์ฑํฉ๋๋ค.
|
1902 |
+
๊ฐ ์ฌ๋ผ์ด๋๋ง๋ค 5๊ฐ์ ํต์ฌ ํฌ์ธํธ์ ๋ฐํ์ ๋
ธํธ๋ฅผ ํฌํจํฉ๋๋ค.
|
1903 |
+
"""
|
1904 |
+
)
|
1905 |
|
|
|
1906 |
with gr.Row():
|
1907 |
with gr.Column(scale=2):
|
1908 |
+
topic_input = gr.Textbox(
|
1909 |
+
label="๐ PPT ์ฃผ์ ",
|
1910 |
+
placeholder="์: ์ธ๊ณต์ง๋ฅ์ ๋ฏธ๋์ ์ฐ์
์ ์ฉ ์ฌ๋ก",
|
1911 |
+
lines=2,
|
1912 |
+
elem_classes="card"
|
1913 |
)
|
1914 |
+
|
1915 |
+
with gr.Row():
|
1916 |
+
with gr.Column():
|
1917 |
+
num_slides = gr.Slider(
|
1918 |
+
label="๐ ์ฌ๋ผ์ด๋ ์",
|
1919 |
+
minimum=3,
|
1920 |
+
maximum=20,
|
1921 |
+
step=1,
|
1922 |
+
value=10,
|
1923 |
+
info="์์ฑํ ์ฌ๋ผ์ด๋ ๊ฐ์ (3-20์ฅ)"
|
1924 |
+
)
|
1925 |
+
|
1926 |
+
with gr.Column():
|
1927 |
+
use_korean = gr.Checkbox(
|
1928 |
+
label="๐ฐ๐ท ํ๊ตญ์ด",
|
1929 |
+
value=True,
|
1930 |
+
info="ํ๊ตญ์ด๋ก ์์ฑ"
|
1931 |
+
)
|
1932 |
+
|
1933 |
+
use_web_search = gr.Checkbox(
|
1934 |
+
label="๐ ์น ๊ฒ์",
|
1935 |
+
value=False,
|
1936 |
+
info="์ต์ ์ ๋ณด ๊ฒ์"
|
1937 |
+
)
|
1938 |
+
|
1939 |
+
# Design Options Section
|
1940 |
+
gr.Markdown("<div class='section-header'>๐จ ๋์์ธ ์ต์
</div>")
|
1941 |
+
|
1942 |
+
with gr.Row():
|
1943 |
+
design_theme = gr.Dropdown(
|
1944 |
+
label="๋์์ธ ํ
๋ง",
|
1945 |
+
choices=[
|
1946 |
+
("ํ๋กํ์
๋ (ํ๋/ํ์)", "professional"),
|
1947 |
+
("๋ชจ๋ (๋ณด๋ผ/ํํฌ)", "modern"),
|
1948 |
+
("์์ฐ (์ด๋ก/๊ฐ์)", "nature"),
|
1949 |
+
("ํฌ๋ฆฌ์์ดํฐ๋ธ (๋ค์ฑ๋ก์ด)", "creative"),
|
1950 |
+
("๋ฏธ๋๋ฉ (ํ๋ฐฑ)", "minimal")
|
1951 |
+
],
|
1952 |
+
value="professional",
|
1953 |
+
elem_classes="dropdown"
|
1954 |
+
)
|
1955 |
+
|
1956 |
+
layout_style = gr.Dropdown(
|
1957 |
+
label="๋ ์ด์์ ์คํ์ผ",
|
1958 |
+
choices=[
|
1959 |
+
("์ผ๊ด๋ ๋ ์ด์์", "consistent"),
|
1960 |
+
("๋ค์ํ ๋ ์ด์์", "varied"),
|
1961 |
+
("๋ฏธ๋๋ฉ ๋ ์ด์์", "minimal")
|
1962 |
+
],
|
1963 |
+
value="consistent",
|
1964 |
+
elem_classes="dropdown"
|
1965 |
+
)
|
1966 |
+
|
1967 |
+
with gr.Row():
|
1968 |
+
font_style = gr.Dropdown(
|
1969 |
+
label="ํฐํธ ์คํ์ผ",
|
1970 |
+
choices=[
|
1971 |
+
("๋ชจ๋", "modern"),
|
1972 |
+
("ํด๋์", "classic"),
|
1973 |
+
("์บ์ฃผ์ผ", "casual")
|
1974 |
+
],
|
1975 |
+
value="modern",
|
1976 |
+
elem_classes="dropdown"
|
1977 |
+
)
|
1978 |
+
|
1979 |
+
include_charts = gr.Checkbox(
|
1980 |
+
label="๐ ์ฐจํธ ํฌํจ",
|
1981 |
+
value=False,
|
1982 |
+
info="CSV ๋ฐ์ดํฐ๊ฐ ์์ ๊ฒฝ์ฐ ์ฐจํธ ์์ฑ"
|
1983 |
+
)
|
1984 |
+
|
1985 |
+
include_ai_image = gr.Checkbox(
|
1986 |
+
label="๐ผ๏ธ AI 3D ํ์ง ์ด๋ฏธ์ง",
|
1987 |
value=False,
|
1988 |
+
info="AI๋ก ์์ฑํ 3D ์คํ์ผ ์ด๋ฏธ์ง๋ฅผ ํ์ง์ ์ถ๊ฐ (์์ฑ ์๊ฐ 30์ด ์ถ๊ฐ)"
|
1989 |
+
)
|
1990 |
+
|
1991 |
+
reference_files = gr.File(
|
1992 |
+
label="๐ ์ฐธ๊ณ ์๋ฃ (์ ํ์ฌํญ)",
|
1993 |
+
file_types=[".pdf", ".csv", ".txt"],
|
1994 |
+
file_count="multiple",
|
1995 |
+
elem_classes="file-upload"
|
1996 |
+
)
|
1997 |
+
|
1998 |
+
generate_btn = gr.Button(
|
1999 |
+
"๐ PPT ์์ฑํ๊ธฐ",
|
2000 |
+
variant="primary",
|
2001 |
+
size="lg"
|
2002 |
)
|
2003 |
+
|
2004 |
with gr.Column(scale=1):
|
2005 |
+
download_file = gr.File(
|
2006 |
+
label="๐ฅ ์์ฑ๋ PPT ๋ค์ด๋ก๋",
|
2007 |
+
interactive=False,
|
2008 |
+
elem_classes="card"
|
|
|
|
|
|
|
2009 |
)
|
2010 |
+
|
2011 |
+
status_text = gr.Textbox(
|
2012 |
+
label="๐ ์์ฑ ์ํ",
|
2013 |
+
lines=10,
|
2014 |
+
interactive=False,
|
2015 |
+
elem_classes="status-box"
|
2016 |
+
)
|
2017 |
+
|
2018 |
+
with gr.Row():
|
2019 |
+
content_preview = gr.Textbox(
|
2020 |
+
label="๐ ์์ฑ๋ ๋ด์ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ",
|
2021 |
+
lines=20,
|
2022 |
+
interactive=False,
|
2023 |
+
visible=True,
|
2024 |
+
elem_classes="preview-box"
|
2025 |
+
)
|
2026 |
+
|
2027 |
+
gr.Markdown(
|
2028 |
+
"""
|
2029 |
+
### ๐ ์ฌ์ฉ ๋ฐฉ๋ฒ
|
2030 |
+
1. **PPT ์ฃผ์ ์
๋ ฅ**: ๊ตฌ์ฒด์ ์ธ ์ฃผ์ ์ผ์๋ก ๋ ์ข์ ๊ฒฐ๊ณผ
|
2031 |
+
2. **์ฌ๋ผ์ด๋ ์ ์ ํ**: 3-20์ฅ ๋ฒ์์์ ์ ํ
|
2032 |
+
3. **๋์์ธ ํ
๋ง ์ ํ**: 5๊ฐ์ง ์ ๋ฌธ์ ์ธ ํ
๋ง ์ค ์ ํ
|
2033 |
+
4. **์ถ๊ฐ ์ต์
์ค์ **: ์น ๊ฒ์, ์ฐจํธ ํฌํจ ๋ฑ
|
2034 |
+
5. **AI 3D ์ด๋ฏธ์ง**: ์ฃผ์ ๋ฅผ ์์งํ๋ 3D ์คํ์ผ ํ์ง ์ด๋ฏธ์ง ์์ฑ
|
2035 |
+
6. **์ฐธ๊ณ ์๋ฃ ์
๋ก๋**: PDF, CSV, TXT ํ์ผ ์ง์
|
2036 |
+
7. **์์ฑ ๋ฒํผ ํด๋ฆญ**: AI๊ฐ ์๋์ผ๋ก PPT ์์ฑ
|
2037 |
+
|
2038 |
+
### ๐จ ๋์์ธ ํ
๋ง ํน์ง
|
2039 |
+
- **ํ๋กํ์
๋**: ๋น์ฆ๋์ค ํ๋ ์ ํ
์ด์
์ ์ ํฉํ ๊น๋ํ ๋์์ธ
|
2040 |
+
- **๋ชจ๋**: ํธ๋ ๋ํ๊ณ ์ธ๋ จ๋ ์์ ์กฐํฉ
|
2041 |
+
- **์์ฐ**: ํธ์ํ๊ณ ์น๊ทผํ ๋๋์ ์์ฐ ๏ฟฝ๏ฟฝ๏ฟฝ์
|
2042 |
+
- **ํฌ๋ฆฌ์์ดํฐ๋ธ**: ๋๋ดํ๊ณ ํ๊ธฐ์ฐฌ ์์์ผ๋ก ์ฃผ๋ชฉ๋ ๋์
|
2043 |
+
- **๋ฏธ๋๋ฉ**: ๊น๋ํ๊ณ ๋จ์ํ ํ๋ฐฑ ๋์์ธ
|
2044 |
+
|
2045 |
+
### โจ ์๋ก์ด ๊ธฐ๋ฅ
|
2046 |
+
- **5๊ฐ ํต์ฌ ํฌ์ธํธ**: ๊ฐ ์ฌ๋ผ์ด๋๋ง๋ค 5๊ฐ์ ํต์ฌ ๋ด์ฉ
|
2047 |
+
- **๋ฐํ์ ๋
ธํธ**: ๊ฐ ์ฌ๋ผ์ด๋๋ง๋ค ๋ฐํ์๋ฅผ ์ํ ๋
ธํธ ์๋ ์์ฑ
|
2048 |
+
- **ํฅ์๋ ํฐํธ ํฌ๊ธฐ**: ๋ ํฐ ํฐํธ๋ก ๊ฐ๋
์ฑ ํฅ์
|
2049 |
+
- **AI 3D ์ด๋ฏธ์ง**: API๋ฅผ ํตํ ๊ณ ํ์ง 3D ์คํ์ผ ํ์ง ์ด๋ฏธ์ง (ํ๊ธ ํ๋กฌํํธ ์ง์)
|
2050 |
+
|
2051 |
+
### ๐ก ๊ณ ๊ธ ํ
|
2052 |
+
- CSV ํ์ผ ์
๋ก๋ ์ '์ฐจํธ ํฌํจ' ์ต์
์ผ๋ก ๋ฐ์ดํฐ ์๊ฐํ ๊ฐ๋ฅ
|
2053 |
+
- ์น ๊ฒ์ ํ์ฑํ๋ก ์ต์ ์ ๋ณด ๋ฐ์
|
2054 |
+
- AI ์ด๋ฏธ์ง๋ ์ฃผ์ ์ ํต์ฌ ํค์๋๋ฅผ ๋ถ์ํ์ฌ ์๋ ์์ฑ
|
2055 |
+
- ๋ค์ํ ๋ ์ด์์์ผ๋ก ์๊ฐ์ ํฅ๋ฏธ ์ ๋ฐ
|
2056 |
+
"""
|
2057 |
+
)
|
2058 |
+
|
2059 |
+
# Examples
|
2060 |
+
gr.Examples(
|
2061 |
+
examples=[
|
2062 |
+
["์ธ๊ณต์ง๋ฅ์ ๋ฏธ๋์ ์ฐ์
์ ์ฉ ์ฌ๋ก", 10, False, True, [], "professional", "modern", "consistent", False, False],
|
2063 |
+
["2024๋
๋์งํธ ๋ง์ผํ
ํธ๋ ๋", 12, True, True, [], "modern", "modern", "consistent", False, True],
|
2064 |
+
["๊ธฐํ๋ณํ์ ์ง์๊ฐ๋ฅํ ๋ฐ์ ", 15, True, True, [], "nature", "classic", "consistent", False, True],
|
2065 |
+
["์คํํธ์
์ฌ์
๊ณํ์", 8, False, True, [], "creative", "modern", "varied", False, True],
|
2066 |
+
],
|
2067 |
+
inputs=[topic_input, num_slides, use_web_search, use_korean, reference_files,
|
2068 |
+
design_theme, font_style, layout_style, include_charts, include_ai_image],
|
2069 |
+
)
|
2070 |
|
2071 |
+
# Event handler
|
2072 |
+
generate_btn.click(
|
2073 |
+
fn=generate_ppt,
|
2074 |
+
inputs=[
|
2075 |
+
topic_input,
|
2076 |
+
num_slides,
|
2077 |
+
use_web_search,
|
2078 |
+
use_korean,
|
2079 |
+
reference_files,
|
2080 |
+
design_theme,
|
2081 |
+
font_style,
|
2082 |
+
layout_style,
|
2083 |
+
include_charts,
|
2084 |
+
include_ai_image
|
|
|
|
|
|
|
|
|
|
|
2085 |
],
|
2086 |
+
outputs=[download_file, status_text, content_preview]
|
|
|
|
|
|
|
|
|
2087 |
)
|
2088 |
|
2089 |
if __name__ == "__main__":
|