diff --git a/.gitignore b/.gitignore index 0ec2db9..8a2775e 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,4 @@ jupyter/*.pptx .gradio/* nohup.out images/* +test_results.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0d1a3df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# 使用 Python 3.10 slim 作为基础镜像 +FROM python:3.10-slim + +# 设置工作目录 +WORKDIR /app + +# 复制并安装项目依赖 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 复制项目文件到容器 +COPY . . + +# 赋予验证脚本执行权限 +RUN chmod +x validate_tests.sh + +# 设置环境变量,以便在运行时可以传入实际的 API Key +ENV LANGCHAIN_API_KEY=${LANGCHAIN_API_KEY} +ENV OPENAI_API_KEY=${OPENAI_API_KEY} + +# 在构建过程中运行单元测试 +RUN ./validate_tests.sh + +# 设置容器的入口点,默认运行 ChatPPT Gradio Server +CMD ["python", "src/gradio_server.py"] \ No newline at end of file diff --git a/README.md b/README.md index d83c111..d8472d9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,34 @@ # ChatPPT +## 目录 + +- [主要功能](#主要功能) +- [产品演示](#产品演示) +- [快速开始](#快速开始) + - [1. 安装依赖](#1-安装依赖) + - [2. 配置应用](#2-配置应用) + - [3. 如何运行](#3-如何运行) + - [A. 作为 Gradio 服务运行](#a-作为-gradio-服务运行) + - [B. 命令行方式运行](#b-命令行方式运行) +- [使用 Docker 部署服务](#使用-docker-部署服务) + - [1. 运行 Docker 容器](#1-运行-docker-容器) + - [2. 配置环境变量](#2-配置环境变量) +- [PowerPoint 母版布局命名规范](#powerpoint-母版布局命名规范) +- [单元测试](#单元测试) + - [单元测试和验证脚本 `validate_tests.sh`](#单元测试和验证脚本-validate_testssh) + - [用途](#用途) + - [功能](#功能) +- [使用 Docker 构建与验证](#使用-docker-构建与验证) + - [1. `Dockerfile`](#1-dockerfile) + - [用途](#用途) + - [关键步骤](#关键步骤) + - [2. `build_image.sh`](#2-build_imagesh) + - [用途](#用途) + - [功能](#功能) +- [贡献](#贡献) +- [许可证](#许可证) +- [联系](#联系) + ChatPPT 是一个基于多模态 AI 技术的智能助手,旨在提升企业办公自动化流程的效率。它能够处理语音、图像和文本等多种输入形式,通过精确的提示工程和强大的自然语言处理能力,为用户生成高质量的 PowerPoint 演示文稿。ChatPPT 不仅简化了信息收集和内容创作过程,还通过自动化的报告生成和分析功能,帮助企业快速、准确地完成各类汇报和展示任务,从而显著提升工作效率和业务价值。 ### 主要功能 @@ -58,7 +87,7 @@ pip install -r requirements.txt python src/gradio_server.py ``` -#### 命令行方式运行 +#### B. 命令行方式运行 您可以通过命令行模式运行 ChatPPT: @@ -68,6 +97,34 @@ python src/main.py test_input.md 通过此模式,您可以手动提供 PowerPoint 文件内容(格式请参考:[ChatPPT 输入文本格式说明](docs/ppt_input_format.md)),并按照配置的 [PowerPoint 模板](templates/MasterTemplate.pptx),生成演示文稿。 +## 使用 Docker 部署服务 + +ChatPPT 提供了 Docker 支持,以便在隔离环境中运行。以下是使用 Docker 运行的步骤。 + +### 1. 运行 Docker 容器 + +使用以下命令运行 ChatPPT 指定版本(如:v0.7)Docker 容器服务。关于如何 [使用 Docker 构建与验证](#使用-docker-构建与验证)。 + +```sh +docker run -it -p 7860:7860 -e LANGCHAIN_API_KEY=$LANGCHAIN_API_KEY -e OPENAI_API_KEY=$OPENAI_API_KEY -v $(pwd)/outputs:/app/outputs chatppt:v0.7 + +``` + +### 2. 参数说明 + +在运行容器时,可以通过环境变量传入`LANGCHAIN_API_KEY` 和 `OPENAI_API_KEY`,例如: + +```sh +-e LANGCHAIN_API_KEY=$LANGCHAIN_API_KEY -e OPENAI_API_KEY=$OPENAI_API_KEY +``` + +将本地的 `outputs` 文件夹挂载到容器内的 `/app/outputs`,便于访问生成的文件。 + +```sh +-v $(pwd)/outputs:/app/outputs` +``` + + ## PowerPoint 母版布局命名规范 为确保 ChatPPT 能正确匹配布局,PowerPoint 母版文件 ([PowerPoint 模板](templates/MasterTemplate.pptx)) 中的布局名称应遵循以下命名规范: @@ -83,16 +140,66 @@ python src/main.py test_input.md 该规范确保布局匹配的灵活性,同时支持多种不同内容的组合和扩展。 -### 4. 贡献 +## 单元测试 + +为了确保 ChatPPT 项目的代码质量和可靠性,我们使用了 `unittest` 模块进行单元测试。关于 `unittest` 及其相关工具(如 `@patch` 和 `MagicMock`)的详细说明,请参考 [单元测试详细说明](docs/unit_test.md)。 + +### 单元测试和验证脚本 `validate_tests.sh` + +#### 用途 +`validate_tests.sh` 是一个用于运行单元测试并验证结果的 Shell 脚本。它会在 Docker 镜像构建过程中执行,以确保代码的正确性和稳定性。 + +#### 功能 +- 脚本运行所有单元测试,并将结果输出到 `test_results.txt` 文件中。 +- 如果测试失败,脚本会输出测试结果,并导致 Docker 构建失败,确保未通过测试的代码不会进入生产环境。 +- 如果所有测试通过,脚本会继续进行 Docker 镜像的构建。 + +## 使用 Docker 构建与验证 + +为了便于在各种环境中构建和部署 ChatPPT 项目,我们提供了 Docker 支持。该支持包括以下文件和功能: + +### 1. `Dockerfile` + +#### 用途 +`Dockerfile` 是用于定义 ChatPPT 项目 Docker 镜像构建过程的配置文件。它描述了构建步骤,包括安装依赖、复制项目文件、运行单元测试等。 + +#### 关键步骤 +- 使用 `python:3.10-slim` 作为基础镜像,并设置工作目录为 `/app`。 +- 复制项目的 `requirements.txt` 文件,并安装所有 Python 依赖。 +- 复制项目的所有文件到容器中,并赋予 `validate_tests.sh` 脚本执行权限。 +- 在构建过程中执行 `validate_tests.sh` 脚本,以确保所有单元测试通过。如果测试失败,构建过程将中止。 +- 构建成功后,将默认运行 `src/main.py` 作为容器的入口 + +点,以启动 ChatPPT 服务。 + +### 2. `build_image.sh` + +#### 用途 +`build_image.sh` 是一个自动构建 Docker 镜像的 Shell 脚本。它从当前的 Git 分支中获取分支名称,并将其用作 Docker 镜像的标签,便于在不同开发分支上生成不同的 Docker 镜像。 + +#### 功能 +- 获取当前 Git 分支名称,并将其用作 Docker 镜像的标签,以便追踪不同开发分支的版本。 +- 使用 `docker build` 命令构建 Docker 镜像,并使用当前 Git 分支名称作为标签。 + +#### 使用示例 +```bash +./build_image.sh +``` + +![build_docker_image](images/build_docker_image.png) + +通过这些脚本和配置文件,ChatPPT 项目可以在不同的开发分支中确保构建的 Docker 镜像基于通过单元测试的代码,从而提高了代码质量和部署的可靠性。 + +### 贡献 我们欢迎所有的贡献!如果你有任何建议或功能请求,请先开启一个议题讨论。你的帮助将使 ChatPPT 变得更加完善。 -### 5. 许可证 +### 许可证 该项目根据 **Apache 2.0** 许可证进行许可。详情请参见 [LICENSE](LICENSE) 文件。 -### 6. 联系 +### 联系 项目作者: Django Peng -项目链接: https://github.com/DjangoPeng/ChatPPT +项目链接: https://github.com/DjangoPeng/ChatPPT \ No newline at end of file diff --git a/build_image.sh b/build_image.sh new file mode 100755 index 0000000..2bc288f --- /dev/null +++ b/build_image.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# 获取当前的 Git 分支名称 +BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + +# 如果需要,可以处理分支名称,例如替换无效字符 +BRANCH_NAME=${BRANCH_NAME//\//-} + +# 使用 Git 分支名称作为 Docker 镜像的标签 +IMAGE_TAG="chatppt:${BRANCH_NAME}" + +# 构建 Docker 镜像 +docker build -t $IMAGE_TAG . + +# 输出构建结果 +echo "Docker 镜像已构建并打上标签: $IMAGE_TAG" \ No newline at end of file diff --git a/src/docx_parser.py b/src/docx_parser.py index 0adab6a..2d1fec6 100644 --- a/src/docx_parser.py +++ b/src/docx_parser.py @@ -103,8 +103,8 @@ def generate_markdown_from_docx(docx_filename): elif text: markdown_content += f'{text}\n\n' # 普通段落直接添加文本 - # 记录调试信息 - LOG.debug(f"从 docx 文件解析的 markdown 内容:\n{markdown_content}") + # 记录调试信息 + LOG.debug(f"从 docx 文件解析的 markdown 内容:\n{markdown_content}") return markdown_content diff --git a/src/gradio_server.py b/src/gradio_server.py index 61cb244..d25bfb0 100644 --- a/src/gradio_server.py +++ b/src/gradio_server.py @@ -14,7 +14,7 @@ from layout_manager import LayoutManager from logger import LOG from openai_whisper import asr, transcribe -from minicpm_v_model import chat_with_image +# from minicpm_v_model import chat_with_image from docx_parser import generate_markdown_from_docx @@ -56,12 +56,12 @@ def generate_contents(message, history): 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 + # 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 内容 diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py new file mode 100644 index 0000000..3da8920 --- /dev/null +++ b/tests/test_data_structures.py @@ -0,0 +1,41 @@ +import unittest +import os +import sys + +# 添加 src 目录到模块搜索路径,以便可以导入 src 目录中的模块 +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) + +from data_structures import PowerPoint, Slide, SlideContent + +class TestDataStructures(unittest.TestCase): + """ + 测试 PowerPoint、Slide、SlideContent 数据类,验证数据结构的正确性。 + """ + + def test_slide_content(self): + slide_content = SlideContent(title="Test Slide", bullet_points=[{'text': "Bullet 1", 'level': 0}], image_path="images/test.png") + self.assertEqual(slide_content.title, "Test Slide") + self.assertEqual(slide_content.bullet_points, [{'text': "Bullet 1", 'level': 0}]) + self.assertEqual(slide_content.image_path, "images/test.png") + + def test_slide(self): + slide_content = SlideContent(title="Slide with Layout") + slide = Slide(layout_id=2, layout_name="Title, Content 0", content=slide_content) + self.assertEqual(slide.layout_id, 2) + self.assertEqual(slide.layout_name, "Title, Content 0") + self.assertEqual(slide.content.title, "Slide with Layout") + + def test_powerpoint(self): + slide_content1 = SlideContent(title="Slide 1") + slide_content2 = SlideContent(title="Slide 2") + slide1 = Slide(layout_id=1, layout_name="Title 1", content=slide_content1) + slide2 = Slide(layout_id=2, layout_name="Title, Content 0", content=slide_content2) + ppt = PowerPoint(title="Test Presentation", slides=[slide1, slide2]) + + self.assertEqual(ppt.title, "Test Presentation") + self.assertEqual(len(ppt.slides), 2) + self.assertEqual(ppt.slides[0].content.title, "Slide 1") + self.assertEqual(ppt.slides[1].content.title, "Slide 2") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_doc_parser.py b/tests/test_doc_parser.py new file mode 100644 index 0000000..f6db4fb --- /dev/null +++ b/tests/test_doc_parser.py @@ -0,0 +1,88 @@ +import unittest +import os +import sys + +# 添加 src 目录到模块搜索路径,以便可以导入 src 目录中的模块 +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) + +from docx_parser import generate_markdown_from_docx + +class TestGenerateMarkdownFromDocx(unittest.TestCase): + """ + 测试从 docx 文件生成 Markdown 格式内容的功能。 + """ + + def setUp(self): + """ + 在每个测试方法执行前运行。用于准备测试所需的文件和目录。 + """ + # 定义测试 docx 文件的路径 + self.test_docx_filename = 'inputs/docx/multimodal_llm_overview.docx' + + # 生成 Markdown 内容 + self.generated_markdown = generate_markdown_from_docx(self.test_docx_filename) + + def test_generated_markdown_content(self): + """ + 测试生成的 Markdown 内容是否符合预期。 + """ + # 期望的 Markdown 输出内容 + expected_markdown = """ +# 多模态大模型概述 + +多模态大模型是指能够处理多种数据模态(如文本、图像、音频等)的人工智能模型。它们在自然语言处理、计算机视觉等领域有广泛的应用。 + +## 1. 多模态大模型的特点 + +- 支持多种数据类型: +- 跨模态学习能力: +- 广泛的应用场景: +### 1.1 支持多种数据类型 + +多模态大模型能够同时处理文本、图像、音频等多种类型的数据,实现数据的融合。 + +## 2. 多模态模型架构 + +以下是多模态模型的典型架构示意图: + +![图片1](images/multimodal_llm_overview/1.png) + +TransFormer 架构图: + +![图片2](images/multimodal_llm_overview/2.png) + +### 2.1 模态融合技术 + +通过模态融合,可以提升模型对复杂数据的理解能力。 + +关键技术:注意力机制、Transformer架构等。 + +- 应用领域: + - 自然语言处理: + - 机器翻译、文本生成等。 + - 计算机视觉: + - 图像识别、目标检测等。 +## 3. 未来展望 + +多模态大模型将在人工智能领域持续发挥重要作用,推动技术创新。 +""" + + # 比较生成的 Markdown 内容与预期内容 + self.assertEqual(self.generated_markdown.strip(), expected_markdown.strip(), "生成的 Markdown 内容与预期不匹配") + + def tearDown(self): + """ + 在每个测试方法执行后运行。用于清理测试产生的文件和目录。 + """ + # 获取图像目录路径 + images_dir = 'images/multimodal_llm_overview' + # 删除生成的图像文件和目录 + if os.path.exists(images_dir): + for filename in os.listdir(images_dir): + file_path = os.path.join(images_dir, filename) + if os.path.isfile(file_path): + os.unlink(file_path) # 删除文件 + os.rmdir(images_dir) # 删除目录 + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_input_parser.py b/tests/test_input_parser.py new file mode 100644 index 0000000..8e7adf2 --- /dev/null +++ b/tests/test_input_parser.py @@ -0,0 +1,110 @@ +import unittest +import os +import sys + +# 添加 src 目录到模块搜索路径,以便可以导入 src 目录中的模块 +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) + +from layout_manager import LayoutManager +from data_structures import PowerPoint +from input_parser import parse_input_text + +class TestInputParser(unittest.TestCase): + """ + 测试 input_parser 模块,检查解析输入文本生成 PowerPoint 数据结构的功能。 + """ + + def setUp(self): + """ + 初始化测试设置,读取输入文件并创建 LayoutManager 实例。 + """ + # 模拟布局映射字典 + self.layout_mapping = { + "Title 1": 1, + "Title, Content 0": 2, + "Title, Content, Picture 2": 8, + } + self.layout_manager = LayoutManager(self.layout_mapping) + + # 读取测试输入文件 + input_file_path = 'inputs/markdown/test_input.md' + with open(input_file_path, 'r', encoding='utf-8') as f: + self.input_text = f.read() + + def test_parse_input_text(self): + """ + 测试 parse_input_text 函数生成的 PowerPoint 数据结构是否符合预期。 + """ + # 解析输入文本 + presentation, presentation_title = parse_input_text(self.input_text, self.layout_manager) + + # 期望的 PowerPoint 数据结构 + expected_presentation_title = "ChatPPT Demo" + expected_slides = [ + { + "title": "ChatPPT Demo", + "layout_id": 1, + "layout_name": "Title 1", + "bullet_points": [], + "image_path": None, + }, + { + "title": "2024 业绩概述", + "layout_id": 2, + "layout_name": "Title, Content 0", + "bullet_points": [ + {"text": "总收入增长15%", "level": 0}, + {"text": "市场份额扩大至30%", "level": 0}, + ], + "image_path": None, + }, + { + "title": "业绩图表", + "layout_id": 8, + "layout_name": "Title, Content, Picture 2", + "bullet_points": [ + {"text": "OpenAI 利润不断增加", "level": 0}, + ], + "image_path": "images/performance_chart.png", + }, + { + "title": "新产品发布", + "layout_id": 8, + "layout_name": "Title, Content, Picture 2", + "bullet_points": [ + {"text": "产品A: **特色功能介绍**", "level": 0}, + {"text": "增长潜力巨大", "level": 1}, + {"text": "新兴市场", "level": 1}, + {"text": "**非洲**市场", "level": 2}, + {"text": "**东南亚**市场", "level": 2}, + {"text": "产品B: 市场定位", "level": 0}, + ], + "image_path": "images/forecast.png", + }, + ] + + # 检查演示文稿标题是否匹配 + self.assertEqual(presentation_title, expected_presentation_title) + + # 检查幻灯片数量是否匹配 + self.assertEqual(len(presentation.slides), len(expected_slides)) + + # 检查每张幻灯片的内容是否符合预期 + for slide, expected in zip(presentation.slides, expected_slides): + self.assertEqual(slide.content.title, expected["title"]) + self.assertEqual(slide.layout_id, expected["layout_id"]) + self.assertEqual(slide.layout_name, expected["layout_name"]) + + # 检查每个要点是否符合预期 + bullet_points = slide.content.bullet_points + expected_bullet_points = expected["bullet_points"] + self.assertEqual(len(bullet_points), len(expected_bullet_points)) + for bullet, expected_bullet in zip(bullet_points, expected_bullet_points): + self.assertEqual(bullet["text"], expected_bullet["text"]) + self.assertEqual(bullet["level"], expected_bullet["level"]) + + # 检查图片路径是否符合预期 + self.assertEqual(slide.content.image_path, expected["image_path"]) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_layout_manager.py b/tests/test_layout_manager.py new file mode 100644 index 0000000..3f99c39 --- /dev/null +++ b/tests/test_layout_manager.py @@ -0,0 +1,44 @@ +import unittest +import os +import sys + +# 添加 src 目录到模块搜索路径,以便可以导入 src 目录中的模块 +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) + +from layout_manager import LayoutManager +from data_structures import SlideContent + +class TestLayoutManager(unittest.TestCase): + """ + 测试 LayoutManager 类,验证布局分配逻辑是否正确。 + """ + + def setUp(self): + # 模拟布局映射字典 + layout_mapping = { + "Title 1": 1, + "Title, Content 0": 2, + "Title, Content, Picture 2": 8 + } + self.layout_manager = LayoutManager(layout_mapping) + + def test_assign_layout_title_only(self): + content = SlideContent(title="Only Title") + layout_id, layout_name = self.layout_manager.assign_layout(content) + self.assertEqual(layout_id, 1) + self.assertEqual(layout_name, "Title 1") + + def test_assign_layout_title_and_content(self): + content = SlideContent(title="Title with Content", bullet_points=[{'text': "Content Bullet", 'level': 0}]) + layout_id, layout_name = self.layout_manager.assign_layout(content) + self.assertEqual(layout_id, 2) + self.assertEqual(layout_name, "Title, Content 0") + + def test_assign_layout_title_content_and_image(self): + content = SlideContent(title="Full Slide", bullet_points=[{'text': "Full Content", 'level': 0}], image_path="images/test.png") + layout_id, layout_name = self.layout_manager.assign_layout(content) + self.assertEqual(layout_id, 8) + self.assertEqual(layout_name, "Title, Content, Picture 2") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ppt_generator.py b/tests/test_ppt_generator.py new file mode 100644 index 0000000..9343bdc --- /dev/null +++ b/tests/test_ppt_generator.py @@ -0,0 +1,117 @@ +import unittest +import os +import sys +from pptx import Presentation + +# 添加 src 目录到模块搜索路径,以便可以导入 src 目录中的模块 +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) + +from data_structures import PowerPoint, Slide, SlideContent +from ppt_generator import generate_presentation + +class TestPPTGenerator(unittest.TestCase): + """ + 测试 ppt_generator 模块的 generate_presentation 函数,验证生成的 PowerPoint 文件内容是否符合预期。 + """ + + def setUp(self): + """ + 设置测试数据和输出路径。 + """ + # 定义输入 PowerPoint 数据结构 + self.powerpoint_data = PowerPoint( + title="ChatPPT Demo", + slides=[ + Slide( + layout_id=1, + layout_name="Title 1", + content=SlideContent(title="ChatPPT Demo") + ), + Slide( + layout_id=2, + layout_name="Title, Content 0", + content=SlideContent( + title="2024 业绩概述", + bullet_points=[ + {"text": "总收入增长15%", "level": 0}, + {"text": "市场份额扩大至30%", "level": 0} + ] + ) + ), + Slide( + layout_id=8, + layout_name="Title, Content, Picture 2", + content=SlideContent( + title="业绩图表", + bullet_points=[{"text": "OpenAI 利润不断增加", "level": 0}], + image_path="images/performance_chart.png" + ) + ), + Slide( + layout_id=8, + layout_name="Title, Content, Picture 2", + content=SlideContent( + title="新产品发布", + bullet_points=[ + {"text": "产品A: **特色功能介绍**", "level": 0}, + {"text": "增长潜力巨大", "level": 1}, + {"text": "新兴市场", "level": 1}, + {"text": "**非洲**市场", "level": 2}, + {"text": "**东南亚**市场", "level": 2}, + {"text": "产品B: 市场定位", "level": 0} + ], + image_path="images/forecast.png" + ) + ) + ] + ) + + self.template_path = "templates/SimpleTemplate.pptx" # 假设存在模板文件 + self.output_path = "outputs/test_presentation.pptx" # 定义输出文件路径 + + def test_generate_presentation(self): + """ + 测试 generate_presentation 函数生成的 PowerPoint 文件是否符合预期。 + """ + # 调用函数生成 PowerPoint 演示文稿 + generate_presentation(self.powerpoint_data, self.template_path, self.output_path) + + # 检查输出文件是否存在 + self.assertTrue(os.path.exists(self.output_path), "输出 PowerPoint 文件未找到。") + + # 打开生成的 PowerPoint 文件并验证内容 + prs = Presentation(self.output_path) + + # 检查演示文稿标题 + self.assertEqual(prs.core_properties.title, self.powerpoint_data.title) + + # 检查幻灯片数量 + self.assertEqual(len(prs.slides), len(self.powerpoint_data.slides)) + + # 验证每张幻灯片的内容 + for idx, slide_data in enumerate(self.powerpoint_data.slides): + slide = prs.slides[idx] + + # 验证幻灯片标题 + self.assertEqual(slide.shapes.title.text, slide_data.content.title) + + # 验证项目符号列表内容 + bullet_points = [shape.text_frame.text for shape in slide.shapes if shape.has_text_frame and shape != slide.shapes.title] + expected_bullets = [point["text"].replace("**", "") for point in slide_data.content.bullet_points] + for bullet, expected in zip(bullet_points, expected_bullets): + self.assertIn(expected, bullet) + + # 验证图片路径(如果存在) + if slide_data.content.image_path: + images = [shape for shape in slide.shapes if shape.shape_type == 13] # 13 为图片形状类型 + self.assertGreater(len(images), 0, f"幻灯片 {idx + 1} 应该包含图片,但未找到。") + + def tearDown(self): + """ + 清理生成的文件。 + """ + if os.path.exists(self.output_path): + os.remove(self.output_path) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_slide_builder.py b/tests/test_slide_builder.py new file mode 100644 index 0000000..d10b967 --- /dev/null +++ b/tests/test_slide_builder.py @@ -0,0 +1,51 @@ +import unittest +import os +import sys + +# 添加 src 目录到模块搜索路径,以便可以导入 src 目录中的模块 +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) + +from layout_manager import LayoutManager +from slide_builder import SlideBuilder +from data_structures import SlideContent + +class TestSlideBuilder(unittest.TestCase): + """ + 测试 SlideBuilder 类,验证幻灯片生成过程是否符合预期。 + """ + + @classmethod + def setUpClass(cls): + # 模拟布局映射字典,只初始化一次 + layout_mapping = {"Title 1": 1, "Title, Content 0": 2, "Title, Content, Picture 2": 8} + cls.layout_manager = LayoutManager(layout_mapping) + + def setUp(self): + # 使用已创建的 layout_manager 实例 + self.builder = SlideBuilder(self.layout_manager) + + def test_set_title(self): + self.builder.set_title("Test Title") + self.assertEqual(self.builder.title, "Test Title") + + def test_add_bullet_point(self): + self.builder.add_bullet_point("Test Bullet 1", level=0) + self.builder.add_bullet_point("Test Bullet 2", level=1) + self.assertEqual(self.builder.bullet_points, [{'text': "Test Bullet 1", 'level': 0}, {'text': "Test Bullet 2", 'level': 1}]) + + def test_set_image(self): + self.builder.set_image("images/test.png") + self.assertEqual(self.builder.image_path, "images/test.png") + + def test_finalize(self): + self.builder.set_title("Final Slide") + self.builder.add_bullet_point("Bullet 1", level=0) + self.builder.set_image("images/final.png") + slide = self.builder.finalize() + + self.assertEqual(slide.content.title, "Final Slide") + self.assertEqual(slide.content.bullet_points, [{'text': "Bullet 1", 'level': 0}]) + self.assertEqual(slide.content.image_path, "images/final.png") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/validate_tests.sh b/validate_tests.sh new file mode 100755 index 0000000..94fdf95 --- /dev/null +++ b/validate_tests.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# 运行单元测试并将结果输出到 test_results.txt +python -m unittest discover -s tests -p "test_*.py" > test_results.txt + +# 检查测试结果,如果有失败,输出失败信息并让脚本退出状态为 1 +if grep -q "FAILED" test_results.txt; then + cat test_results.txt + exit 1 +else + echo "All tests passed!" + exit 0 +fi \ No newline at end of file