diff --git a/.gitignore b/.gitignore index 5441a5f..5cbf40c 100644 --- a/.gitignore +++ b/.gitignore @@ -168,4 +168,5 @@ tests/* .DS_STore jupyter/*.pptx .gradio/* -nohup.out \ No newline at end of file +nohup.out +images/* diff --git a/README.md b/README.md index a39ad62..4479d81 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,16 @@ pip install -r requirements.txt { "input_mode": "text", "chatbot_prompt": "prompts/chatbot.txt", - "ppt_template": "templates/MasterTemplate.pptx" + "content_formatter_prompt": "prompts/content_formatter.txt", + "content_assistant_prompt": "prompts/content_assistant.txt", + "ppt_template": "templates/SimpleTemplate.pptx" } ``` ### 3. 如何运行 +作为生产服务发布,ChatPPT 还需要配置域名,SSL 证书和反向代理,详见文档:**[域名和反向代理设置说明文档](docs/proxy.md)** + #### A. 作为 Gradio 服务运行 要使用 Gradio 界面运行应用,允许用户通过 Web 界面与该工具交互: diff --git a/config.json b/config.json index 7cfa408..b34c602 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,7 @@ { "input_mode": "text", "chatbot_prompt": "prompts/chatbot.txt", + "content_formatter_prompt": "prompts/content_formatter.txt", + "content_assistant_prompt": "prompts/content_assistant.txt", "ppt_template": "templates/SimpleTemplate.pptx" } \ No newline at end of file diff --git a/docs/proxy.md b/docs/proxy.md new file mode 100644 index 0000000..fdf8f05 --- /dev/null +++ b/docs/proxy.md @@ -0,0 +1,276 @@ +# 域名和反向代理设置说明文档 + +本文档为域名配置、SSL 证书生成、安装及 Nginx 反向代理设置提供详细指导,以便为网站启用 HTTPS 安全访问。 + +--- + +## 1. 域名 A 记录配置 + +### 1.1 登录域名提供商 + +1. 使用您的域名服务商(如 Hexonet、阿里云、腾讯云等)提供的管理界面。 +2. 找到域名管理区域,选择您要配置的域名。 + +### 1.2 添加 A 记录 + +1. 进入 DNS 管理页面。 +2. 创建一条新的 **A 记录**,详细配置如下: + - **主机记录**:`@`(表示主域名)或 `subdomain`(如果使用子域名)。 + - **记录类型**:A 记录 + - **记录值**:您的服务器公网 IP 地址(例如 `123.45.67.89`)。 + - **TTL**:选择默认值,通常为 `600` 秒。 + +3. **保存配置**。等待 DNS 记录的传播,通常需要几分钟,但可能最长达 24 小时。 + +--- + +## 2. 安装并配置 SSL 证书 + +### 2.1 安装 Certbot + +Certbot 是 Let’s Encrypt 提供的免费 SSL 证书工具。首先在您的服务器上安装 Certbot 和 Nginx 插件: + +```bash +sudo apt update +sudo apt install certbot python3-certbot-nginx -y +``` + +### 2.2 生成 SSL 证书 + +运行以下命令,为域名(或子域名)生成 SSL 证书。以 `example.com` 为例: + +```bash +sudo certbot --nginx -d example.com +``` + +### 2.3 配置自动重定向到 HTTPS + +Certbot 会询问您是否需要将 HTTP 重定向到 HTTPS。选择 `2` 进行自动重定向配置: + +```plaintext +1: No redirect +2: Redirect - Make all requests redirect to secure HTTPS access. +``` + +### 2.4 验证证书安装 + +在浏览器中访问 `https://example.com`,查看是否显示安全锁标志,表示证书安装成功。 + +--- + +## 3. Nginx 反向代理设置 + +以下是使用 Nginx 反向代理的详细配置过程,确保所有 HTTP 请求自动重定向到 HTTPS,并通过 SSL 加密的 Nginx 代理将请求转发到后端服务。 + +### 3.1 配置 Nginx + +1. 编辑或创建 Nginx 配置文件(例如 `/etc/nginx/sites-available/example.com`): + + ```bash + sudo nano /etc/nginx/sites-available/example.com + ``` + +2. 将以下内容添加到配置文件中: + + ```nginx + # HTTP 到 HTTPS 的重定向 + server { + listen 80; + server_name example.com; + + # 重定向所有 HTTP 请求到 HTTPS + return 301 https://$host$request_uri; + } + + # HTTPS 服务器配置 + server { + listen 443 ssl; + server_name example.com; + + # SSL 证书路径 + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # Certbot 自动生成 + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Certbot 自动生成 + + # 启用 TLS 协议 + ssl_protocols TLSv1.2 TLSv1.3; + + # 配置加密套件 + ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305"; + ssl_prefer_server_ciphers off; + + # 其他 SSL 配置 + ssl_ecdh_curve X25519:P-256:P-384:P-521; + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 10m; + + # 上传文件大小限制 + client_max_body_size 50M; + + location / { + proxy_pass http://127.0.0.1:7860; # 后端服务地址,示例中使用本地 7860 端口 + proxy_http_version 1.1; + + # WebSocket 支持 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 转发客户端请求头 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + ``` + +3. **启用站点并重启 Nginx** + + 创建一个符号链接启用站点配置,并重启 Nginx: + + ```bash + sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/ + sudo nginx -t # 测试配置是否正确 + sudo systemctl restart nginx # 重启 Nginx + ``` + +### 3.2 测试反向代理 + +1. 通过 `https://example.com` 访问网站。 +2. 验证 HTTP 请求自动重定向到 HTTPS,页面显示安全锁标志,且内容正常加载。 + +--- + +## 4. 定期更新和维护 + +Let’s Encrypt 证书有效期为 90 天,Certbot 会自动创建续订任务。您可以手动测试自动续订: + +```bash +sudo certbot renew --dry-run +``` + +--- + +## 示例测试和故障排除 + +- **SSL 测试**:访问 [SSL Labs](https://www.ssllabs.com/ssltest/),输入您的域名进行 SSL 配置检查。 +- **日志查看**:在 `/var/log/nginx/error.log` 中查找 Nginx 错误日志,帮助诊断 SSL、代理和连接问题。 + +--- + +### 注意事项 +1. 确保您的域名 DNS 设置已正确生效,访问域名时指向服务器 IP。 +2. 定期检查和维护 Nginx 与 Certbot 版本,以确保安全性和兼容性。 + +--- + +以上步骤完成后,您的域名将通过 Nginx 反向代理为后端服务提供 HTTPS 安全访问。 + + +## 补充:Nginx 配置详细说明 + +以下是 Nginx 配置模板,将特定项替换为通用变量,并包含详细注释说明: + +```nginx +# 配置 HTTP 到 HTTPS 重定向 +server { + listen 80; + server_name example.com; # 将此替换为您的域名,例如 example.com + + # 将所有 HTTP 请求重定向到 HTTPS + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name example.com; # 将此替换为您的域名 + + # 配置 SSL 证书路径(替换为您的证书路径) + ssl_certificate /path/to/ssl/fullchain.pem; # 例如 /etc/letsencrypt/live/example.com/fullchain.pem + ssl_certificate_key /path/to/ssl/privkey.pem; # 例如 /etc/letsencrypt/live/example.com/privkey.pem + + # 允许的 TLS 协议版本 + ssl_protocols TLSv1.2 TLSv1.3; + + # 兼容 TLS 1.3 的加密套件 + ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305"; + ssl_prefer_server_ciphers off; + + # 其他 SSL 配置 + ssl_ecdh_curve X25519:P-256:P-384:P-521; + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 10m; + + # 设置最大上传文件大小限制 + client_max_body_size 50M; + + # 代理到后端服务(例如 Gradio 的 HTTP 服务) + location / { + proxy_pass http://127.0.0.1:7860; # 将流量转发到本地运行在端口 7860 的 HTTP 服务 + proxy_http_version 1.1; + + # WebSocket 支持 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 转发客户端请求头 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### 配置项注释说明 + +- **server_name**:设置服务器名称,通常为域名。将 `example.com` 替换为您自己的域名。 +- **ssl_certificate** 和 **ssl_certificate_key**:SSL 证书文件路径,指向 HTTPS 所需的证书和私钥文件。替换为实际证书路径。 +- **ssl_protocols**:允许的 TLS 协议版本。这里设置为只允许较新的 TLS 1.2 和 TLS 1.3。 +- **ssl_ciphers**:定义了服务器支持的加密套件,确保符合 TLS 1.3 要求并与常用浏览器兼容。 +- **ssl_prefer_server_ciphers**:设置为 `off`,让客户端选择其首选的加密套件。 +- **ssl_ecdh_curve**:定义服务器支持的椭圆曲线,用于 ECDH 密钥交换。 +- **client_max_body_size**:设置上传文件的大小限制,确保上传较大文件时不被拒绝。 +- **proxy_pass**:指向后端服务的 URL,在本例中是本地运行在 `127.0.0.1:7860` 的服务(如 Gradio)。 +- **proxy_set_header**:设置必要的请求头以支持 WebSocket 和客户端 IP 转发。 + +### 示例 + +假设域名为 `myapp.example.com`,证书路径位于 `/etc/letsencrypt/live/myapp.example.com/`,则配置为: + +```nginx +server { + listen 80; + server_name myapp.example.com; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name myapp.example.com; + + ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305"; + ssl_prefer_server_ciphers off; + + ssl_ecdh_curve X25519:P-256:P-384:P-521; + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 10m; + + client_max_body_size 50M; + + location / { + proxy_pass http://127.0.0.1:7860; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` \ No newline at end of file diff --git a/images/chatppt_presentation_demo.png b/images/chatppt_presentation_demo.png index bea30c9..289acfc 100644 Binary files a/images/chatppt_presentation_demo.png and b/images/chatppt_presentation_demo.png differ diff --git a/images/placeholder.png b/images/placeholder.png deleted file mode 100644 index 2c10dcb..0000000 Binary files a/images/placeholder.png and /dev/null differ diff --git a/images/slides.png b/images/slides.png deleted file mode 100644 index 9c7f89c..0000000 Binary files a/images/slides.png and /dev/null differ diff --git a/inputs/docx/multimodal_llm_overview.docx b/inputs/docx/multimodal_llm_overview.docx new file mode 100644 index 0000000..698d3b6 Binary files /dev/null and b/inputs/docx/multimodal_llm_overview.docx differ diff --git a/inputs/GitHubSentinel_intro.md b/inputs/markdown/GitHubSentinel_intro.md similarity index 100% rename from inputs/GitHubSentinel_intro.md rename to inputs/markdown/GitHubSentinel_intro.md diff --git a/inputs/openai_canvas_intro.md b/inputs/markdown/openai_canvas_intro.md similarity index 100% rename from inputs/openai_canvas_intro.md rename to inputs/markdown/openai_canvas_intro.md diff --git a/inputs/test_input.md b/inputs/markdown/test_input.md similarity index 100% rename from inputs/test_input.md rename to inputs/markdown/test_input.md diff --git a/prompts/chatbot.txt b/prompts/chatbot.txt index ab46c5d..eae0704 100644 --- a/prompts/chatbot.txt +++ b/prompts/chatbot.txt @@ -1,6 +1,6 @@ **Role**: You are a knowledgeable Chatbot capable of answering a wide range of user questions. -**Task**: When responding to user inquiries, format your answers in a presentation-friendly style suited for PowerPoint slides. Organize the content into a structured, slide-by-slide layout. +**Task**: When responding to user inquiries, format your answers in a presentation-friendly style suited for PowerPoint slides. Organize the content into a structured, slide-by-slide layout with **at least 10 slides**. Ensure each slide is rich in detail and elaboration. **Format**: Structure your responses as follows: @@ -8,15 +8,25 @@ # [Presentation Theme] // Only once, for the first slide as the presentation's theme ## [Slide Title] -- [Key point 1] - - [Second Level] - - [Third Level] -- [Key point 2] - - [Second Level] +- [Key point 1]: [Introduction or summary of the point] + - [Detailed explanation covering multiple aspects or subpoints] + - [Specific examples, case studies, or further insights] + - [Additional detail or secondary aspect] + - [Supporting data, quotes, or statistics] +- [Key point 2]: [Brief introduction or summary] + - [Expanded description with step-by-step breakdown] + - [Practical application, scenarios, or research findings] ## [Slide Title] -- [Key point 1] -- [Key point 2] +- [Key point 1]: [Comprehensive explanation] + - [Second Level]: [Elaboration on critical points, providing context or rationale] + - [Second Level]: [Additional insight or perspective] +- [Key point 2]: [Clear overview with actionable insights] + - [Second Level]: [Supporting data, strategies, or methods] + - [Third Level]: [Examples or further clarification] ``` -Ensure that the **Presentation Theme** appears only on the first slide. Avoid generating images or image URLs. Use clear, concise bullet points for the remaining slides to cover the key aspects of the topic. +**Guidelines**: +- Each response should include **a minimum of 10 slides**. +- Ensure that each slide has **multiple detailed points**, with second-level explanations providing thorough descriptions and third-level points adding examples or additional insights. +- The **Presentation Theme** should appear only on the first slide, and no images or image URLs are needed. \ No newline at end of file diff --git a/prompts/content_assistant.txt b/prompts/content_assistant.txt new file mode 100644 index 0000000..22dc464 --- /dev/null +++ b/prompts/content_assistant.txt @@ -0,0 +1,69 @@ +### Prompt Design in Role-Task-Format + +**Role**: You are a content-enhancing PowerPoint assistant specialized in converting structured markdown to presentation slides. Your main objectives are to handle content breakdown, structure adjustments, and ensure smooth narrative flow. + +**Task**: +1. **Separate Images Across Slides**: For any slide containing multiple images, split these into separate slides, ensuring that each slide displays only one image. +2. **Add Supplementary Content**: Where splitting content leaves a slide too brief or lacking coherence, add relevant details to maintain a logical and informative progression. +3. **Structure for Cohesiveness**: Maintain a smooth narrative by filling gaps with logical transitions, brief summaries, or additional key points, helping each slide to contribute effectively to the overall presentation flow. + +**Format**: +When processing the markdown content, follow this format strictly: +- **# [Presentation Theme]**: Appears only on the first slide and is used as the presentation's overarching theme. +- **## [Slide Title]**: Marks each new slide with the title, followed by relevant points. +- **- [Key Points and Subpoints]**: Retain the bullet structure but ensure images are limited to one per slide. Add transitions or descriptions as needed. +- **![Image Name](Image Path)**: For slides with images, include one image per slide, modifying content as necessary to make each slide’s information stand independently and flow smoothly. + +### Example of Generated Content + +Given the markdown: + +```markdown +# 多模态大模型概述 + +## 多模态模型架构 +- 多模态模型的典型架构示意图 +![图片1](images/multimodal_llm_overview/1.png) +- TransFormer 架构图 +![图片2](images/multimodal_llm_overview/2.png) + +## 未来展望 +- 多模态大模型将在人工智能领域持续发挥重要作用,推动技术创新 +``` + +Convert to: + +```markdown +# 多模态大模型概述 + +## 多模态模型架构 +- 多模态大模型融合文本、图像、音频等多种模态数据 + - 支持复杂任务的高效处理和全面理解 + - 数据整合解决了单一模态带来的信息孤岛问题 +- 模型在自然语言生成、情感分析、内容推荐等场景中的应用广泛 + +## 典型架构示意图 +- 特征提取模块:处理和提取每个模态的数据特征 + - 模态融合模块:合并多模态数据,创建共享表示空间 + - 输出生成模块:利用整合的信息生成最终输出 +- 多模态架构提供的系统化分析能力可以在多领域应用 +![图片1](images/multimodal_llm_overview/1.png) + +## TransFormer架构 +- TransFormer利用自注意力机制促进多模态信息交流 + - 多头注意力机制:提升模型捕捉语义关联的能力 + - 能够在输入数据中找到远程关联 + - 提供多维度的特征关注 + - 参数共享机制:提高训练效率和模型泛化能力 +- TransFormer架构 在图像识别、语言生成等领域同样表现出色 +- TransFormer架构对加速多模态模型的发展至关重要 + +## TransFormer架构示意图 +![图片2](images/multimodal_llm_overview/2.png) + +## 未来展望 +- 自动驾驶:通过融合激光雷达、摄像头等多模态数据提升感知和决策能力 + - 医疗诊断:结合影像、基因信息和电子健康记录支持个性化诊疗 +- 虚拟助手:分析语音、文本和图像,实现自然流畅的交互体验 + - 多模态大模型的发展将为实际应用场景带来深远影响 +``` diff --git a/prompts/content_formatter.txt b/prompts/content_formatter.txt new file mode 100644 index 0000000..5b83ed9 --- /dev/null +++ b/prompts/content_formatter.txt @@ -0,0 +1,102 @@ +**Role**: You are an expert content formatter with proficiency in transforming raw markdown input into a polished, presentation-ready structure suitable for PowerPoint slides. + +**Task**: Convert the provided markdown content into a structured format for presentation use. Ensure clarity by following a slide-by-slide layout and applying multi-level bullet points only as needed. Place the presentation theme only on the first slide. + +**Format**: Structure the output as follows: + +``` +# [Presentation Theme] // Appears only on the first slide + +## [Slide Title] +- [Primary Point 1] + - [Secondary Level] + - [Tertiary Level] +- [Primary Point 2] + - [Secondary Level] +![image_name](image_filepath) // If the original content includes images, insert them here + +## [Slide Title] +- [Primary Point 1] +- [Primary Point 2] +![image_name](image_filepath) // Images are optional based on the original content +``` + +Guidelines: +- **First Slide**: Add the **Presentation Theme** title from the original input. +- **Subsequent Slides**: Title each slide and organize points in concise bullets. Only include image placeholders if images are present in the original input. +- **Multi-level Bullet Points**: Use secondary and tertiary levels only as needed to capture hierarchical information. + +### Example Input and Output + +**Input:** + +``` +# 多模态大模型概述 + +多模态大模型是指能够处理多种数据模态(如文本、图像、音频等)的人工智能模型。它们在自然语言处理、计算机视觉等领域有广泛的应用。 + +## 1. 多模态大模型的特点 + +- 支持多种数据类型: +- 跨模态学习能力: +- 广泛的应用场景: +### 1.1 支持多种数据类型 + +多模态大模型能够同时处理文本、图像、音频等多种类型的数据,实现数据的融合。 + +## 2. 多模态模型架构 + +以下是多模态模型的典型架构示意图: + +![图片1](images/multimodal_llm_overview/1.png) + +TransFormer 架构图: + +![图片2](images/multimodal_llm_overview/2.png) + +### 2.1 模态融合技术 + +通过模态融合,可以提升模型对复杂数据的理解能力。 + +关键技术:注意力机制、Transformer架构等。 + +- 应用领域: + - 自然语言处理: + - 机器翻译、文本生成等。 + - 计算机视觉: + - 图像识别、目标检测等。 +## 3. 未来展望 + +多模态大模型将在人工智能领域持续发挥重要作用,推动技术创新。 +``` + +**Output:** + +``` +# 多模态大模型概述 + +## 多模态大模型的特点 +- 支持多种数据类型 + - 能够处理文本、图像、音频等多种类型的数据,实现数据的融合 +- 跨模态学习能力 +- 广泛的应用场景 + + +## 多模态模型架构 +- 多模态模型的典型架构示意图 +![图片1](images/multimodal_llm_overview/1.png) +- TransFormer 架构图 +![图片2](images/multimodal_llm_overview/2.png) + +## 模态融合技术 +- 提升对复杂数据的理解能力 + - 关键技术:注意力机制、Transformer架构 +- 应用领域 + - 自然语言处理 + - 机器翻译、文本生成 + - 计算机视觉 + - 图像识别、目标检测 + +## 未来展望 +- 多模态大模型将在人工智能领域持续发挥重要作用,推动技术创新 +``` \ No newline at end of file diff --git a/prompts/formatter.txt b/prompts/formatter.txt deleted file mode 100644 index 312630f..0000000 --- a/prompts/formatter.txt +++ /dev/null @@ -1,22 +0,0 @@ -**Role**: -You are a skilled assistant, proficient in processing written content and organizing input from various sources into a clear, standardized format that can be easily used by downstream colleagues or systems. You excel at transforming complex information into well-structured slides. - -**Task**: -Your task is to understand the user’s input and break it down into multiple slides based on the theme and logical structure. You should organize the input into slide titles and bullet points, ensuring that the content on each slide is concise and to the point. - -In Chinese - -**Format**: -Please present the organized slide content in the following format: - -``` -# [Main Title] - -## [Slide Title] -- [Key point 1] -- [Key point 2] - -## [Slide Title] -- [Key point 1] -![Image Description](Image Path) -``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5a5aba4..e06f907 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,15 @@ langchain_core==0.2.41 langchain_community==0.2.17 langchain_ollama==0.1.3 langchain_openai==0.1.25 +python-docx==1.1.2 +Pillow==9.1.0 torch==2.5.0 transformers==4.46.0 datasets==3.0.2 accelerate==1.0.1 librosa==0.10.2.post1 soundfile==0.12.1 -ffmpeg==1.4 \ No newline at end of file +ffmpeg==1.4 +torchvision==0.20.0 +sentencepiece==0.1.99 +bitsandbytes==0.44.1 \ No newline at end of file diff --git a/src/chatbot.py b/src/chatbot.py index 6b9f122..1484fe4 100644 --- a/src/chatbot.py +++ b/src/chatbot.py @@ -1,6 +1,5 @@ # chatbot.py -import json from abc import ABC, abstractmethod from langchain_openai import ChatOpenAI @@ -16,11 +15,11 @@ class ChatBot(ABC): """ 聊天机器人基类,提供聊天功能。 """ - def __init__(self, prompt_file, session_id=None): + def __init__(self, prompt_file="./prompts/chatbot.txt", session_id=None): self.prompt_file = prompt_file self.session_id = session_id if session_id else "default_session_id" self.prompt = self.load_prompt() - LOG.debug(f"[chatbot prompt]{self.prompt}") + # LOG.debug(f"[ChatBot Prompt]{self.prompt}") self.create_chatbot() def load_prompt(self): @@ -45,7 +44,11 @@ def create_chatbot(self): ]) # 初始化 ChatOllama 模型,配置参数 - self.chatbot = system_prompt | ChatOpenAI(model="gpt-4o-mini") # 使用的模型名称) + self.chatbot = system_prompt | ChatOpenAI( + model="gpt-4o-mini", + temperature=0.5, + max_tokens=4096 + ) # 将聊天机器人与消息历史记录关联 self.chatbot_with_history = RunnableWithMessageHistory(self.chatbot, get_session_history) diff --git a/src/config.py b/src/config.py index e632ec4..cdc2a93 100644 --- a/src/config.py +++ b/src/config.py @@ -3,10 +3,16 @@ class Config: def __init__(self, config_file='config.json'): + """ + 初始化 Config 类,并从指定的 config 文件中加载配置。 + """ self.config_file = config_file - self.load_config() - + self.load_config() # 加载配置文件 + def load_config(self): + """ + 从配置文件加载配置项,并设置默认值以防缺少某些键。 + """ # 检查 config 文件是否存在 if not os.path.exists(self.config_file): raise FileNotFoundError(f"Config file '{self.config_file}' not found.") @@ -14,11 +20,15 @@ def load_config(self): with open(self.config_file, 'r') as f: config = json.load(f) - # 加载 ChatPPT 运行模式(默认文本模态) + # 加载 ChatPPT 运行模式,默认为 "text" 模式 self.input_mode = config.get('input_mode', "text") - # 加载 PPT 默认模板 + # 加载 PPT 默认模板路径,若未指定则使用默认模板 self.ppt_template = config.get('ppt_template', "templates/MasterTemplate.pptx") - # 加载 ChatBot Prompt - self.chatbot_prompt = config.get('chatbot_prompt', '') \ No newline at end of file + # 加载 ChatBot 提示信息 + self.chatbot_prompt = config.get('chatbot_prompt', '') + + # 加载内容格式化提示和助手提示 + self.content_formatter_prompt = config.get('content_formatter_prompt', '') + self.content_assistant_prompt = config.get('content_assistant_prompt', '') diff --git a/src/content_assistant.py b/src/content_assistant.py new file mode 100644 index 0000000..1b620e2 --- /dev/null +++ b/src/content_assistant.py @@ -0,0 +1,65 @@ +# content_assistant.py +from abc import ABC, abstractmethod + +from langchain_openai import ChatOpenAI +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder # 导入提示模板相关类 +from langchain_core.messages import HumanMessage # 导入消息类 +from langchain_core.runnables.history import RunnableWithMessageHistory # 导入带有消息历史的可运行类 + +from logger import LOG # 导入日志工具 + +class ContentAssistant(ABC): + """ + 聊天机器人基类,提供聊天功能。 + """ + def __init__(self, prompt_file="./prompts/content_assistant.txt"): + self.prompt_file = prompt_file + self.prompt = self.load_prompt() + # LOG.debug(f"[Formatter Prompt]{self.prompt}") + self.create_assistant() + + def load_prompt(self): + """ + 从文件加载系统提示语。 + """ + try: + with open(self.prompt_file, "r", encoding="utf-8") as file: + return file.read().strip() + except FileNotFoundError: + raise FileNotFoundError(f"找不到提示文件 {self.prompt_file}!") + + + def create_assistant(self): + """ + 初始化聊天机器人,包括系统提示和消息历史记录。 + """ + # 创建聊天提示模板,包括系统提示和消息占位符 + system_prompt = ChatPromptTemplate.from_messages([ + ("system", self.prompt), # 系统提示部分 + ("human", "{input}"), # 消息占位符 + ]) + + self.model = ChatOpenAI( + model="gpt-4o-mini", + temperature=0.5, + max_tokens=4096, + ) + + self.assistant = system_prompt | self.model # 使用的模型名称) + + def adjust_single_picture(self, markdown_content): + """ + + + 参数: + markdown_content (str): PowerPoint markdown 原始格式 + + 返回: + str: 格式化后的 markdown 内容 + """ + response = self.assistant.invoke({ + "input": markdown_content, + }) + + LOG.debug(f"[Assistant 内容重构后]\n{response.content}") # 记录调试日志 + return response.content # 返回生成的回复内容 \ No newline at end of file diff --git a/src/content_formatter.py b/src/content_formatter.py new file mode 100644 index 0000000..6722993 --- /dev/null +++ b/src/content_formatter.py @@ -0,0 +1,66 @@ +# content_formatter.py +from abc import ABC, abstractmethod + +from langchain_openai import ChatOpenAI +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder # 导入提示模板相关类 +from langchain_core.messages import HumanMessage # 导入消息类 +from langchain_core.runnables.history import RunnableWithMessageHistory # 导入带有消息历史的可运行类 + +from logger import LOG # 导入日志工具 + +class ContentFormatter(ABC): + """ + 聊天机器人基类,提供聊天功能。 + """ + def __init__(self, prompt_file="./prompts/content_formatter.txt"): + self.prompt_file = prompt_file + self.prompt = self.load_prompt() + # LOG.debug(f"[Formatter Prompt]{self.prompt}") + self.create_formatter() + + def load_prompt(self): + """ + 从文件加载系统提示语。 + """ + try: + with open(self.prompt_file, "r", encoding="utf-8") as file: + return file.read().strip() + except FileNotFoundError: + raise FileNotFoundError(f"找不到提示文件 {self.prompt_file}!") + + + def create_formatter(self): + """ + 初始化聊天机器人,包括系统提示和消息历史记录。 + """ + # 创建聊天提示模板,包括系统提示和消息占位符 + system_prompt = ChatPromptTemplate.from_messages([ + ("system", self.prompt), # 系统提示部分 + ("human", "{input}"), # 消息占位符 + ]) + + self.model = ChatOpenAI( + model="gpt-4o-mini", + temperature=0.5, + max_tokens=4096, + ) + + self.formatter = system_prompt | self.model # 使用的模型名称) + + + def format(self, raw_content): + """ + + + 参数: + raw_content (str): 解析后的 markdown 原始格式 + + 返回: + str: 格式化后的 markdown 内容 + """ + response = self.formatter.invoke({ + "input": raw_content, + }) + + LOG.debug(f"[Formmater 格式化后]\n{response.content}") # 记录调试日志 + return response.content # 返回生成的回复内容 \ No newline at end of file diff --git a/src/docx_parser.py b/src/docx_parser.py new file mode 100644 index 0000000..0adab6a --- /dev/null +++ b/src/docx_parser.py @@ -0,0 +1,121 @@ +import os +from docx import Document +from docx.oxml.ns import qn +from PIL import Image +from io import BytesIO + +from logger import LOG # 引入日志模块,用于记录调试信息 + +def is_paragraph_list_item(paragraph): + """ + 检查段落是否为列表项。 + 判断依据是段落的样式名称是否包含 'list bullet' 或 'list number', + 分别对应项目符号列表和编号列表。 + """ + style_name = paragraph.style.name.lower() + return 'list bullet' in style_name or 'list number' in style_name + +def get_paragraph_list_level(paragraph): + """ + 获取段落的列表级别(缩进层级)。 + 首先尝试通过 XML 结构判断,如果无法获取,则通过样式名称中的数字判断。 + """ + p = paragraph._p + numPr = p.find(qn('w:numPr')) + if numPr is not None: + ilvl = numPr.find(qn('w:ilvl')) + if ilvl is not None: + return int(ilvl.get(qn('w:val'))) + + style_name = paragraph.style.name.lower() + if 'list bullet' in style_name or 'list number' in style_name: + for word in style_name.split(): + if word.isdigit(): + return int(word) - 1 + return 0 + +def generate_markdown_from_docx(docx_filename): + """ + 从指定的 docx 文件生成 Markdown 格式的内容,并将所有图像另存为文件并插入 Markdown 内容中。 + 支持标题、列表项、图像和普通段落的转换。 + """ + # 获取 docx 文件的基本名称,用于创建图像文件夹 + docx_basename = os.path.splitext(os.path.basename(docx_filename))[0] + images_dir = f'images/{docx_basename}/' + if not os.path.exists(images_dir): + os.makedirs(images_dir) # 如果目录不存在,则创建 + + document = Document(docx_filename) # 打开 docx 文件 + markdown_content = '' + image_counter = 1 # 图像编号计数器 + + for para in document.paragraphs: + style = para.style.name # 获取段落样式名称 + text = para.text.strip() # 获取段落文本并去除首尾空格 + + # 如果段落为空且没有任何运行对象,则跳过 + if not text and not para.runs: + continue + + # 检查段落类型:标题、列表项、普通段落 + is_heading = 'Heading' in style + is_title = style == 'Title' + is_list = is_paragraph_list_item(para) + list_level = get_paragraph_list_level(para) if is_list else 0 + + # 确定标题级别 + if is_title: + heading_level = 1 + elif is_heading: + heading_level = int(style.replace('Heading ', '')) + 1 + else: + heading_level = None + + # 检查段落中的每个运行,寻找并保存图像 + for run in para.runs: + # 查找 w:drawing 标签中的图像 + drawings = run.element.findall('.//w:drawing', namespaces={'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}) + for drawing in drawings: + # 查找图像的关系 ID + blips = drawing.findall('.//a:blip', namespaces={'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}) + for blip in blips: + rId = blip.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed') + image_part = document.part.related_parts[rId] + image_bytes = image_part.blob # 获取图像数据 + image_filename = f'{image_counter}.png' + image_path = os.path.join(images_dir, image_filename) + + # 使用 PIL 保存图像为 PNG 格式 + image = Image.open(BytesIO(image_bytes)) + if image.mode in ('RGBA', 'P', 'LA'): + image = image.convert('RGB') # 将图像转换为 RGB 模式,以兼容 PNG 格式 + image.save(image_path, 'PNG') + + # 在 Markdown 中添加图像链接 + markdown_content += f'![图片{image_counter}]({image_path})\n\n' + image_counter += 1 + + # 根据段落类型格式化文本内容 + if heading_level: + markdown_content += f'{"#" * heading_level} {text}\n\n' # 使用 Markdown 语法表示标题 + elif is_list: + markdown_content += f'{" " * list_level}- {text}\n' # 使用缩进和 “-” 表示列表项 + elif text: + markdown_content += f'{text}\n\n' # 普通段落直接添加文本 + + # 记录调试信息 + LOG.debug(f"从 docx 文件解析的 markdown 内容:\n{markdown_content}") + + return markdown_content + +if __name__ == "__main__": + # 指定输入的 docx 文件路径 + docx_filename = 'inputs/docx/multimodal_llm_overview.docx' + docx_basename = os.path.splitext(os.path.basename(docx_filename))[0] + + # 生成 Markdown 内容 + markdown_content = generate_markdown_from_docx(docx_filename) + + # 保存 Markdown 内容到文件 + with open(f'{docx_basename}.md', 'w', encoding='utf-8') as f: + f.write(markdown_content) diff --git a/src/gradio_server.py b/src/gradio_server.py index 225d22a..dd62a37 100644 --- a/src/gradio_server.py +++ b/src/gradio_server.py @@ -3,16 +3,23 @@ from config import Config from chatbot import ChatBot +from content_formatter import ContentFormatter +from content_assistant import ContentAssistant from input_parser import parse_input_text from ppt_generator import generate_presentation from template_manager import load_template, get_layout_mapping from layout_manager import LayoutManager from logger import LOG from openai_whisper import asr, transcribe +from minicpm_v_model import chat_with_image +from docx_parser import generate_markdown_from_docx + # 实例化 Config,加载配置文件 config = Config() chatbot = ChatBot(config.chatbot_prompt) +content_formatter = ContentFormatter(config.content_formatter_prompt) +content_assistant = ContentAssistant(config.content_assistant_prompt) # 加载 PowerPoint 模板,并获取可用布局 ppt_template = load_template(config.ppt_template) @@ -34,13 +41,26 @@ def generate_contents(message, history): # 获取上传的文件列表,如果存在则处理每个文件 for uploaded_file in message.get("files", []): + LOG.debug(f"[上传文件]: {uploaded_file}") # 获取文件的扩展名,并转换为小写 file_ext = os.path.splitext(uploaded_file)[1].lower() if file_ext in ('.wav', '.flac', '.mp3'): - LOG.debug(f"[音频文件]: {uploaded_file}") # 使用 OpenAI Whisper 模型进行语音识别 audio_text = asr(uploaded_file) texts.append(audio_text) + # 解释说明图像文件 + elif file_ext in ('.jpg', '.png', '.jpeg'): + if text_input: + image_desc = chat_with_image(uploaded_file, text_input) + else: + image_desc = chat_with_image(uploaded_file) + return image_desc + # 使用 Docx 文件作为素材创建 PowerPoint + elif file_ext in ('.docx', '.doc'): + # 调用 generate_markdown_from_docx 函数,获取 markdown 内容 + raw_content = generate_markdown_from_docx(uploaded_file) + markdown_content = content_formatter.format(raw_content) + return content_assistant.adjust_single_picture(markdown_content) else: LOG.debug(f"[格式不支持]: {uploaded_file}") diff --git a/src/main.py b/src/main.py index 313b3ed..ba7baaf 100644 --- a/src/main.py +++ b/src/main.py @@ -6,19 +6,41 @@ from layout_manager import LayoutManager from config import Config from logger import LOG # 引入 LOG 模块 +from content_formatter import ContentFormatter +from content_assistant import ContentAssistant + +# 新增导入 docx_parser 模块中的函数 +from docx_parser import generate_markdown_from_docx # 定义主函数,处理输入并生成 PowerPoint 演示文稿 def main(input_file): config = Config() # 加载配置文件 + content_formatter = ContentFormatter() + content_assistant = ContentAssistant() - # 检查输入的 markdown 文件是否存在 + # 检查输入文件是否存在 if not os.path.exists(input_file): LOG.error(f"{input_file} 不存在。") # 如果文件不存在,记录错误日志 return - - # 读取 markdown 文件的内容 - with open(input_file, 'r', encoding='utf-8') as file: - input_text = file.read() + + # 根据输入文件的扩展名判断文件类型 + file_extension = os.path.splitext(input_file)[1].lower() + + if file_extension in ['.md', '.markdown']: + # 处理 markdown 文件 + with open(input_file, 'r', encoding='utf-8') as file: + input_text = file.read() + elif file_extension == '.docx': + # 处理 docx 文件 + LOG.info(f"正在解析 docx 文件: {input_file}") + # 调用 generate_markdown_from_docx 函数,获取 markdown 内容 + raw_content = generate_markdown_from_docx(input_file) + markdown_content = content_formatter.format(raw_content) + input_text = content_assistant.adjust_single_picture(markdown_content) + else: + # 不支持的文件类型 + LOG.error(f"暂不支持的文件格式: {file_extension}") + return # 加载 PowerPoint 模板,并打印模板中的可用布局 ppt_template = load_template(config.ppt_template) # 加载模板文件 @@ -31,27 +53,27 @@ def main(input_file): # 调用 parse_input_text 函数,解析输入文本,生成 PowerPoint 数据结构 powerpoint_data, presentation_title = parse_input_text(input_text, layout_manager) - LOG.info(f"解析转换后的 ChatPPT PowerPoint 数据结构:\n{powerpoint_data}") # 记录调试日志,打印解析后的 PowerPoint 数据 + LOG.info(f"解析转换后的 ChatPPT PowerPoint 数据结构:\n{powerpoint_data}") # 记录信息日志,打印解析后的 PowerPoint 数据 # 定义输出 PowerPoint 文件的路径 output_pptx = f"outputs/{presentation_title}.pptx" - + # 调用 generate_presentation 函数生成 PowerPoint 演示文稿 generate_presentation(powerpoint_data, config.ppt_template, output_pptx) # 程序入口 if __name__ == "__main__": # 设置命令行参数解析器 - parser = argparse.ArgumentParser(description='从 markdown 文件生成 PowerPoint 演示文稿。') + parser = argparse.ArgumentParser(description='从 markdown 或 docx 文件生成 PowerPoint 演示文稿。') parser.add_argument( 'input_file', # 输入文件参数 nargs='?', # 可选参数 - default='inputs/test_input.md', # 默认值为 'inputs/test_input.md' - help='输入 markdown 文件的路径(默认: inputs/test_input.md)' + default='inputs/markdown/test_input.md', # 默认值 + help='输入 markdown 或 docx 文件的路径(默认: inputs/markdown/test_input.md)' ) - + # 解析命令行参数 args = parser.parse_args() # 使用解析后的输入文件参数运行主函数 - main(args.input_file) \ No newline at end of file + main(args.input_file) diff --git a/src/minicpm_v_model.py b/src/minicpm_v_model.py new file mode 100644 index 0000000..5306433 --- /dev/null +++ b/src/minicpm_v_model.py @@ -0,0 +1,53 @@ +from PIL import Image +from transformers import AutoModel, AutoTokenizer +from logger import LOG # 引入日志模块,用于记录日志 + +# 加载模型和分词器 +# 这里我们使用 `AutoModel` 和 `AutoTokenizer` 加载模型 'openbmb/MiniCPM-V-2_6-int4' +# 参数 `trust_remote_code=True` 表示信任远程代码(根据模型文档设置) +model = AutoModel.from_pretrained('openbmb/MiniCPM-V-2_6-int4', trust_remote_code=True) +tokenizer = AutoTokenizer.from_pretrained('openbmb/MiniCPM-V-2_6-int4', trust_remote_code=True) +model.eval() # 设置模型为评估模式,以确保不进行训练中的随机性操作 + +def chat_with_image(image_file, question='描述下这幅图', sampling=False, temperature=0.7, stream=False): + """ + 使用模型的聊天功能生成对图像的回答。 + + 参数: + image_file: 图像文件,用于处理的图像。 + question: 提问的问题,默认为 '描述下这幅图'。 + sampling: 是否使用采样进行生成,默认为 False。 + temperature: 采样温度,用于控制生成文本的多样性,值越高生成越多样。 + stream: 是否流式返回响应,默认为 False。 + + 返回: + 生成的回答文本字符串。 + """ + # 打开并转换图像为 RGB 模式 + image = Image.open(image_file).convert('RGB') + + # 创建消息列表,模拟用户和 AI 的对话 + msgs = [{'role': 'user', 'content': [image, question]}] + + # 如果不启用流式输出,直接返回生成的完整响应 + if not stream: + return model.chat(image=None, msgs=msgs, tokenizer=tokenizer, temperature=temperature) + else: + # 启用流式输出,则逐字生成并打印响应 + generated_text = "" + for new_text in model.chat(image=None, msgs=msgs, tokenizer=tokenizer, sampling=sampling, temperature=temperature, stream=True): + generated_text += new_text + print(new_text, flush=True, end='') # 实时输出每部分生成的文本 + return generated_text # 返回完整的生成文本 + +# 主程序入口 +if __name__ == "__main__": + import sys # 引入 sys 模块以获取命令行参数 + if len(sys.argv) != 2: + print("Usage: python src/minicpm_v_model.py ") # 提示正确的用法 + sys.exit(1) # 退出并返回状态码 1,表示错误 + + image_file = sys.argv[1] # 获取命令行传入的图像文件路径 + question = 'What is in the image?' # 定义默认问题 + response = chat_with_image(image_file, question, sampling=True, temperature=0.7, stream=True) # 调用生成响应函数 + print("\nFinal Response:", response) # 输出最终响应 diff --git a/src/ppt_generator.py b/src/ppt_generator.py index bf975ee..046c44f 100644 --- a/src/ppt_generator.py +++ b/src/ppt_generator.py @@ -1,5 +1,7 @@ import os from pptx import Presentation +from pptx.util import Inches +from PIL import Image from utils import remove_all_slides from logger import LOG # 引入日志模块 @@ -32,6 +34,60 @@ def format_text(paragraph, text): run = paragraph.add_run() run.text = text +def insert_image_centered_in_placeholder(new_slide, image_path): + """ + 将图片插入到 Slide 中,使其中心与 placeholder 的中心对齐。 + 如果图片尺寸超过 placeholder,则进行缩小适配。 + 在插入成功后删除 placeholder。 + """ + # 构建图片的绝对路径 + image_full_path = os.path.join(os.getcwd(), image_path) + + # 检查图片是否存在 + if not os.path.exists(image_full_path): + LOG.warning(f"图片路径 '{image_full_path}' 不存在,跳过此图片。") + return + + # 打开图片并获取其大小(以像素为单位) + with Image.open(image_full_path) as img: + img_width_px, img_height_px = img.size + + # 遍历找到图片的 placeholder(type 18 表示图片 placeholder) + for shape in new_slide.placeholders: + if shape.placeholder_format.type == 18: + placeholder_width = shape.width + placeholder_height = shape.height + placeholder_left = shape.left + placeholder_top = shape.top + + # 计算 placeholder 的中心点 + placeholder_center_x = placeholder_left + placeholder_width / 2 + placeholder_center_y = placeholder_top + placeholder_height / 2 + + # 图片的宽度和高度转换为 PowerPoint 的单位 (Inches) + img_width = Inches(img_width_px / 96) # 假设图片 DPI 为 96 + img_height = Inches(img_height_px / 96) + + # 如果图片的宽度或高度超过 placeholder,按比例缩放图片 + if img_width > placeholder_width or img_height > placeholder_height: + scale = min(placeholder_width / img_width, placeholder_height / img_height) + img_width *= scale + img_height *= scale + + # 计算图片左上角位置,使其中心对准 placeholder 中心 + left = placeholder_center_x - img_width / 2 + top = placeholder_center_y - img_height / 2 + + # 插入图片到指定位置并设定缩放后的大小 + new_slide.shapes.add_picture(image_full_path, left, top, width=img_width, height=img_height) + LOG.debug(f"图片已插入,并以 placeholder 中心对齐,路径: {image_full_path}") + + # 移除占位符 + sp = shape._element # 获取占位符的 XML 元素 + sp.getparent().remove(sp) # 从父元素中删除 + LOG.debug("已删除图片的 placeholder") + break + # 生成 PowerPoint 演示文稿 def generate_presentation(powerpoint_data, template_path: str, output_path: str): # 检查模板文件是否存在 @@ -64,26 +120,23 @@ def generate_presentation(powerpoint_data, template_path: str, output_path: str) if shape.has_text_frame and not shape == new_slide.shapes.title: text_frame = shape.text_frame text_frame.clear() # 清除原有内容 + + # 直接使用第一个段落,不添加新的段落,避免额外空行 + first_paragraph = text_frame.paragraphs[0] + # 将要点内容作为项目符号列表添加到文本框中 for point in slide.content.bullet_points: - p = text_frame.add_paragraph() - p.level = point["level"] # 设置项目符号的级别 - format_text(p, point["text"]) # 调用 format_text 方法来处理加粗文本 - LOG.debug(f"添加列表项: {p.text},级别: {p.level}") + # 第一个要点覆盖初始段落,其他要点添加新段落 + paragraph = first_paragraph if point == slide.content.bullet_points[0] else text_frame.add_paragraph() + paragraph.level = point["level"] # 设置项目符号的级别 + format_text(paragraph, point["text"]) # 调用 format_text 方法来处理加粗文本 + LOG.debug(f"添加列表项: {paragraph.text},级别: {paragraph.level}") + break # 插入图片 if slide.content.image_path: - image_full_path = os.path.join(os.getcwd(), slide.content.image_path) # 构建图片的绝对路径 - if os.path.exists(image_full_path): - # 插入图片到占位符中 - for shape in new_slide.placeholders: - if shape.placeholder_format.type == 18: # 18 表示图片占位符 - shape.insert_picture(image_full_path) - LOG.debug(f"插入图片: {image_full_path}") - break - else: - LOG.warning(f"图片路径 '{image_full_path}' 不存在,跳过此图片。") + insert_image_centered_in_placeholder(new_slide, slide.content.image_path) # 保存生成的 PowerPoint 文件 prs.save(output_path) diff --git a/templates/SimpleTemplate.pptx b/templates/SimpleTemplate.pptx index 2b9cc45..e11ad83 100644 Binary files a/templates/SimpleTemplate.pptx and b/templates/SimpleTemplate.pptx differ