openfree commited on
Commit
b7a60fa
ยท
verified ยท
1 Parent(s): a9296fd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1686 -362
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
- # File Type Check Functions
260
  ##############################################################################
261
- def is_image_file(file_path: str) -> bool:
262
- """Check if file is an image"""
263
- return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- def is_document_file(file_path: str) -> bool:
270
- """Check if file is a document"""
271
- return bool(re.search(r"\.(pdf|csv|txt)$", file_path, re.IGNORECASE))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
  ##############################################################################
274
- # Message Processing Functions
275
  ##############################################################################
276
- def process_new_user_message(message: dict) -> str:
277
- """Process user message and convert to text"""
278
- content_parts = [message["text"]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- if not message.get("files"):
281
- return message["text"]
282
 
283
- # Classify files
284
- csv_files = []
285
- txt_files = []
286
- pdf_files = []
287
- image_files = []
288
- video_files = []
289
- unknown_files = []
290
 
291
- for file_path in message["files"]:
292
- if file_path.lower().endswith(".csv"):
293
- csv_files.append(file_path)
294
- elif file_path.lower().endswith(".txt"):
295
- txt_files.append(file_path)
296
- elif file_path.lower().endswith(".pdf"):
297
- pdf_files.append(file_path)
298
- elif is_image_file(file_path):
299
- image_files.append(file_path)
300
- elif is_video_file(file_path):
301
- video_files.append(file_path)
302
- else:
303
- unknown_files.append(file_path)
304
-
305
- # Process document files
306
- for csv_path in csv_files:
307
- csv_analysis = analyze_csv_file(csv_path)
308
- content_parts.append(csv_analysis)
309
-
310
- for txt_path in txt_files:
311
- txt_analysis = analyze_txt_file(txt_path)
312
- content_parts.append(txt_analysis)
313
-
314
- for pdf_path in pdf_files:
315
- pdf_markdown = pdf_to_markdown(pdf_path)
316
- content_parts.append(pdf_markdown)
317
-
318
- # Warning messages for unsupported files
319
- if image_files:
320
- image_names = [os.path.basename(f) for f in image_files]
321
- content_parts.append(
322
- f"\nโš ๏ธ **Image files detected**: {', '.join(image_names)}\n"
323
- "This demo currently does not support image analysis. "
324
- "Please describe the image content in text if you need help with it."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
- if video_files:
328
- video_names = [os.path.basename(f) for f in video_files]
329
- content_parts.append(
330
- f"\nโš ๏ธ **Video files detected**: {', '.join(video_names)}\n"
331
- "This demo currently does not support video analysis. "
332
- "Please describe the video content in text if you need help with it."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  )
 
 
 
 
334
 
335
- if unknown_files:
336
- unknown_names = [os.path.basename(f) for f in unknown_files]
337
- content_parts.append(
338
- f"\nโš ๏ธ **Unsupported file format**: {', '.join(unknown_names)}\n"
339
- "Supported formats: PDF, CSV, TXT"
340
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
- return "\n\n".join(content_parts)
343
-
344
- def process_history(history: list[dict]) -> list[dict]:
345
- """Convert conversation history to Friendli API format"""
346
- messages = []
347
-
348
- for item in history:
349
- if item["role"] == "assistant":
350
- messages.append({
351
- "role": "assistant",
352
- "content": item["content"]
353
- })
354
- else: # user
355
- content = item["content"]
356
- if isinstance(content, str):
357
- messages.append({
358
- "role": "user",
359
- "content": content
360
- })
361
- elif isinstance(content, list) and len(content) > 0:
362
- # File processing
363
- file_info = []
364
- for file_path in content:
365
- if isinstance(file_path, str):
366
- file_info.append(f"[File: {os.path.basename(file_path)}]")
367
- if file_info:
368
- messages.append({
369
- "role": "user",
370
- "content": " ".join(file_info)
371
- })
372
-
373
- return messages
374
-
375
- ##############################################################################
376
- # Streaming Response Handler
377
- ##############################################################################
378
- def stream_friendli_response(messages: list[dict], max_tokens: int = 1000) -> Iterator[str]:
379
- """Get streaming response from Friendli AI API"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": max_tokens,
389
- "top_p": 0.8,
390
- "temperature": 0.7,
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"Friendli API error: {str(e)}")
435
- yield f"โš ๏ธ API call error: {str(e)}"
436
 
437
  ##############################################################################
438
- # Main Inference Function
439
  ##############################################################################
440
-
441
- def run(
442
- message: dict,
443
- history: list[dict],
444
- max_new_tokens: int = 512,
445
  use_web_search: bool = False,
446
- use_korean: bool = False,
447
- system_prompt: str = "",
448
- ) -> Iterator[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
 
450
  try:
451
- # Prepare system message
452
- messages = []
453
-
454
- if use_korean:
455
- combined_system_msg = "๋„ˆ๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ ์—ญํ• ์ด๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์นœ์ ˆํ•˜๊ณ  ์ •ํ™•ํ•˜๊ฒŒ ๋‹ต๋ณ€ํ•ด๋ผ."
456
- else:
457
- combined_system_msg = "You are an AI assistant. Please respond helpfully and accurately in English."
 
458
 
459
- if system_prompt.strip():
460
- combined_system_msg += f"\n\n{system_prompt.strip()}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
 
462
- # Web search processing
463
  if use_web_search:
464
- user_text = message.get("text", "")
465
- if user_text:
466
- ws_query = extract_keywords(user_text, top_k=5)
467
- if ws_query.strip():
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
- messages.append({
478
- "role": "system",
479
- "content": combined_system_msg
480
- })
 
481
 
482
- # Add conversation history
483
- messages.extend(process_history(history))
484
 
485
- # Process current message
486
- user_content = process_new_user_message(message)
487
- messages.append({
488
- "role": "user",
489
- "content": user_content
490
- })
 
 
491
 
492
- # Debug log
493
- logger.debug(f"Total messages: {len(messages)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"run function error: {str(e)}")
501
- yield f"โš ๏ธ Sorry, an error occurred: {str(e)}"
502
-
503
- ##############################################################################
504
- # Examples
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 - CSS Styles (Removed blue colors)
542
  ##############################################################################
543
  css = """
544
  /* Full width UI */
545
  .gradio-container {
546
- background: rgba(255, 255, 255, 0.95);
547
- padding: 30px 40px;
548
- margin: 20px auto;
549
  width: 100% !important;
550
- max-width: none !important;
551
- border-radius: 12px;
552
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
553
- }
554
-
555
- .fillable {
556
- width: 100% !important;
557
- max-width: 100% !important;
558
  }
559
 
560
  /* Background */
561
  body {
562
- background: linear-gradient(135deg, #f5f7fa 0%, #e0e0e0 100%);
563
  margin: 0;
564
  padding: 0;
565
  font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
566
- color: #333;
567
  }
568
 
569
- /* Button styles - neutral gray */
570
- button, .btn {
571
- background: #6b7280 !important;
 
 
 
 
 
 
 
 
 
 
572
  border: none;
573
  color: white !important;
574
- padding: 10px 20px;
575
- text-transform: uppercase;
576
  font-weight: 600;
577
- letter-spacing: 0.5px;
578
- cursor: pointer;
579
- border-radius: 6px;
580
  transition: all 0.3s ease;
 
 
581
  }
582
 
583
- button:hover, .btn:hover {
584
- background: #4b5563 !important;
585
- transform: translateY(-1px);
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
- /* Example buttons */
603
- .gr-samples-table button,
604
- .gr-examples button,
605
- .examples button {
606
- background: #f0f2f5 !important;
607
- border: 1px solid #d1d5db;
608
- color: #374151 !important;
609
- margin: 5px;
610
- font-size: 14px;
611
  }
612
 
613
- .gr-samples-table button:hover,
614
- .gr-examples button:hover,
615
- .examples button:hover {
616
- background: #e5e7eb !important;
617
- border-color: #9ca3af;
618
  }
619
 
620
- /* Chat interface */
621
- .chatbox, .chatbot {
622
- background: white !important;
623
- border-radius: 8px;
624
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 
 
 
625
  }
626
 
627
- .message {
628
- padding: 15px;
629
- margin: 10px 0;
630
- border-radius: 8px;
 
 
 
631
  }
632
 
633
- /* Input styles */
634
- .multimodal-textbox, textarea, input[type="text"] {
635
- background: white !important;
636
- border: 1px solid #d1d5db;
637
- border-radius: 6px;
638
- padding: 10px;
639
- font-size: 16px;
640
  }
641
 
642
- .multimodal-textbox:focus, textarea:focus, input[type="text"]:focus {
643
- border-color: #6b7280;
644
- outline: none;
645
- box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
 
646
  }
647
 
648
- /* Warning messages */
649
- .warning-box {
650
- background: #fef3c7 !important;
651
- border: 1px solid #f59e0b;
652
- border-radius: 8px;
653
- padding: 15px;
654
- margin: 10px 0;
655
- color: #92400e;
656
  }
657
 
658
- /* Headings */
659
- h1, h2, h3 {
660
- color: #1f2937;
 
661
  }
662
 
663
- /* Links - neutral gray */
664
- a {
665
- color: #6b7280;
666
- text-decoration: none;
 
 
667
  }
668
 
669
- a:hover {
670
- text-decoration: underline;
671
- color: #4b5563;
 
 
672
  }
673
 
674
- /* Slider */
675
- .gr-slider {
676
- margin: 15px 0;
677
  }
678
 
679
- /* Checkbox */
680
- input[type="checkbox"] {
681
- width: 18px;
682
- height: 18px;
683
- margin-right: 8px;
 
 
 
684
  }
685
 
686
- /* Scrollbar */
687
- ::-webkit-scrollbar {
688
- width: 8px;
689
- height: 8px;
690
  }
691
 
692
- ::-webkit-scrollbar-track {
693
- background: #f1f1f1;
 
 
 
 
 
 
694
  }
695
 
696
- ::-webkit-scrollbar-thumb {
697
- background: #888;
698
- border-radius: 4px;
 
 
699
  }
700
 
701
- ::-webkit-scrollbar-thumb:hover {
702
- background: #555;
 
 
 
 
 
 
 
 
703
  }
704
  """
705
 
706
- ##############################################################################
707
- # Gradio UI Main
708
- ##############################################################################
709
- with gr.Blocks(css=css, title="Gemma-3-R1984-27B Chatbot") as demo:
710
- # Title
711
- gr.Markdown("# ๐Ÿค— Gemma-3-R1984-27B Chatbot")
712
- gr.Markdown("Community: [https://discord.gg/openfreeai](https://discord.gg/openfreeai)")
 
 
713
 
714
- # UI Components
715
  with gr.Row():
716
  with gr.Column(scale=2):
717
- web_search_checkbox = gr.Checkbox(
718
- label="๐Ÿ” Enable Deep Research (Web Search)",
719
- value=False,
720
- info="Check for questions requiring latest information"
 
721
  )
722
- with gr.Column(scale=1):
723
- korean_checkbox = gr.Checkbox(
724
- label="๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ธ€ (Korean)",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  value=False,
726
- info="Check for Korean responses"
 
 
 
 
 
 
 
 
 
 
 
 
 
727
  )
 
728
  with gr.Column(scale=1):
729
- max_tokens_slider = gr.Slider(
730
- label="Max Tokens",
731
- minimum=100,
732
- maximum=8000,
733
- step=50,
734
- value=1000,
735
- info="Adjust response length"
736
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
 
738
- # Main chat interface
739
- chat = gr.ChatInterface(
740
- fn=run,
741
- type="messages",
742
- chatbot=gr.Chatbot(type="messages", scale=1),
743
- textbox=gr.MultimodalTextbox(
744
- file_types=[
745
- ".webp", ".png", ".jpg", ".jpeg", ".gif",
746
- ".mp4", ".csv", ".txt", ".pdf"
747
- ],
748
- file_count="multiple",
749
- autofocus=True,
750
- placeholder="Enter text or upload PDF, CSV, TXT files. (Images/videos not supported in this demo)"
751
- ),
752
- multimodal=True,
753
- additional_inputs=[
754
- max_tokens_slider,
755
- web_search_checkbox,
756
- korean_checkbox,
757
  ],
758
- stop_btn=False,
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__":