From 8618867f92a5f980b588f8fc2a9c45a3174feeb8 Mon Sep 17 00:00:00 2001 From: xiaji Date: Fri, 29 May 2026 14:14:53 +0800 Subject: [PATCH] =?UTF-8?q?V2.0=20=E4=BA=94=E5=A4=A7=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA:=20=E9=94=9A=E7=82=B9=E5=AE=9A=E4=BD=8D/?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=E5=9B=BE=E8=A1=A8/=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E6=9E=B6=E6=9E=84/WebSocket/LLM=E6=99=BA=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ppt_manager/README.md | 115 ++++++ ppt_manager/app.py | 70 ++++ ppt_manager/config/project_config.yaml | 42 ++ ppt_manager/create_sample_template.py | 56 +++ ppt_manager/main.py | 38 ++ ppt_manager/requirements.txt | 11 + ppt_manager/scripts/employment_table.py | 62 +++ ppt_manager/scripts/gdp_chart.py | 32 ++ ppt_manager/scripts/inflation_chart.py | 34 ++ ppt_manager/scripts/market_analysis.py | 66 +++ ppt_manager/scripts/trade_chart.py | 34 ++ ppt_manager/src/__init__.py | 0 ppt_manager/src/config_loader.py | 39 ++ ppt_manager/src/dynamic_generator.py | 93 +++++ ppt_manager/src/ppt_core.py | 71 ++++ ppt_manager/src/ppt_generator.py | 119 ++++++ ppt_manager/templates/index.html | 179 ++++++++ ppt_manager_v2/README.md | 383 ++++++++++++++++++ .../__pycache__/orchestrator.cpython-311.pyc | Bin 0 -> 16535 bytes .../__pycache__/llm_analyst.cpython-311.pyc | Bin 0 -> 8509 bytes ppt_manager_v2/ai/llm_analyst.py | 124 ++++++ ppt_manager_v2/config/project_config_v2.yaml | 86 ++++ ppt_manager_v2/connectors/sql_connector.py | 142 +++++++ ppt_manager_v2/core/__init__.py | 0 .../core/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 147 bytes .../__pycache__/anchor_engine.cpython-311.pyc | Bin 0 -> 5804 bytes .../conditional_renderer.cpython-311.pyc | Bin 0 -> 9520 bytes .../__pycache__/native_chart.cpython-311.pyc | Bin 0 -> 9532 bytes ppt_manager_v2/core/anchor_engine.py | 80 ++++ ppt_manager_v2/core/conditional_renderer.py | 137 +++++++ ppt_manager_v2/core/native_chart.py | 148 +++++++ ppt_manager_v2/logs/run_20260529_105045.log | 62 +++ ppt_manager_v2/logs/run_20260529_105104.log | 58 +++ ppt_manager_v2/logs/run_20260529_105156.log | 4 + ppt_manager_v2/logs/run_20260529_105202.log | 28 ++ ppt_manager_v2/logs/run_20260529_105938.log | 38 ++ ppt_manager_v2/logs/run_20260529_110321.log | 34 ++ ppt_manager_v2/logs/run_20260529_110441.log | 4 + ppt_manager_v2/logs/run_20260529_111029.log | 4 + ppt_manager_v2/orchestrator.py | 251 ++++++++++++ .../base_generator.cpython-311.pyc | Bin 0 -> 7109 bytes ppt_manager_v2/plugins/base_generator.py | 109 +++++ ppt_manager_v2/plugins/generators/__init__.py | 0 .../__pycache__/cpi_generator.cpython-311.pyc | Bin 0 -> 3634 bytes .../__pycache__/gdp_generator.cpython-311.pyc | Bin 0 -> 3500 bytes .../plugins/generators/cpi_generator.py | 50 +++ .../plugins/generators/gdp_generator.py | 53 +++ ppt_manager_v2/requirements_v2.txt | 14 + ppt_manager_v2/templates/index_v2.html | 201 +++++++++ ppt_manager_v2/web/templates/index_v2.html | 201 +++++++++ ppt_manager_v2/web_socket_app.py | 96 +++++ 51 files changed, 3368 insertions(+) create mode 100644 ppt_manager/README.md create mode 100644 ppt_manager/app.py create mode 100644 ppt_manager/config/project_config.yaml create mode 100644 ppt_manager/create_sample_template.py create mode 100644 ppt_manager/main.py create mode 100644 ppt_manager/requirements.txt create mode 100644 ppt_manager/scripts/employment_table.py create mode 100644 ppt_manager/scripts/gdp_chart.py create mode 100644 ppt_manager/scripts/inflation_chart.py create mode 100644 ppt_manager/scripts/market_analysis.py create mode 100644 ppt_manager/scripts/trade_chart.py create mode 100644 ppt_manager/src/__init__.py create mode 100644 ppt_manager/src/config_loader.py create mode 100644 ppt_manager/src/dynamic_generator.py create mode 100644 ppt_manager/src/ppt_core.py create mode 100644 ppt_manager/src/ppt_generator.py create mode 100644 ppt_manager/templates/index.html create mode 100644 ppt_manager_v2/README.md create mode 100644 ppt_manager_v2/__pycache__/orchestrator.cpython-311.pyc create mode 100644 ppt_manager_v2/ai/__pycache__/llm_analyst.cpython-311.pyc create mode 100644 ppt_manager_v2/ai/llm_analyst.py create mode 100644 ppt_manager_v2/config/project_config_v2.yaml create mode 100644 ppt_manager_v2/connectors/sql_connector.py create mode 100644 ppt_manager_v2/core/__init__.py create mode 100644 ppt_manager_v2/core/__pycache__/__init__.cpython-311.pyc create mode 100644 ppt_manager_v2/core/__pycache__/anchor_engine.cpython-311.pyc create mode 100644 ppt_manager_v2/core/__pycache__/conditional_renderer.cpython-311.pyc create mode 100644 ppt_manager_v2/core/__pycache__/native_chart.cpython-311.pyc create mode 100644 ppt_manager_v2/core/anchor_engine.py create mode 100644 ppt_manager_v2/core/conditional_renderer.py create mode 100644 ppt_manager_v2/core/native_chart.py create mode 100644 ppt_manager_v2/logs/run_20260529_105045.log create mode 100644 ppt_manager_v2/logs/run_20260529_105104.log create mode 100644 ppt_manager_v2/logs/run_20260529_105156.log create mode 100644 ppt_manager_v2/logs/run_20260529_105202.log create mode 100644 ppt_manager_v2/logs/run_20260529_105938.log create mode 100644 ppt_manager_v2/logs/run_20260529_110321.log create mode 100644 ppt_manager_v2/logs/run_20260529_110441.log create mode 100644 ppt_manager_v2/logs/run_20260529_111029.log create mode 100644 ppt_manager_v2/orchestrator.py create mode 100644 ppt_manager_v2/plugins/__pycache__/base_generator.cpython-311.pyc create mode 100644 ppt_manager_v2/plugins/base_generator.py create mode 100644 ppt_manager_v2/plugins/generators/__init__.py create mode 100644 ppt_manager_v2/plugins/generators/__pycache__/cpi_generator.cpython-311.pyc create mode 100644 ppt_manager_v2/plugins/generators/__pycache__/gdp_generator.cpython-311.pyc create mode 100644 ppt_manager_v2/plugins/generators/cpi_generator.py create mode 100644 ppt_manager_v2/plugins/generators/gdp_generator.py create mode 100644 ppt_manager_v2/requirements_v2.txt create mode 100644 ppt_manager_v2/templates/index_v2.html create mode 100644 ppt_manager_v2/web/templates/index_v2.html create mode 100644 ppt_manager_v2/web_socket_app.py diff --git a/ppt_manager/README.md b/ppt_manager/README.md new file mode 100644 index 0000000..fe6bea1 --- /dev/null +++ b/ppt_manager/README.md @@ -0,0 +1,115 @@ +# 📊 PPT智能管理系统 + +PPT管理系统,实现**静态模板内容**与**动态数据内容**的智能合并。 + +## ✨ 核心特性 + +- 📁 **静态PPT管理**: 固定不变的原理内容放在本地PPT模板中 +- 🔄 **动态内容生成**: Python脚本负责数据采集、图表生成 +- 🎯 **页面映射配置**: YAML配置文件定义每页是静态还是动态 +- 🖱️ **一键生成**: Web界面点击按钮自动合成最终PPT +- 📝 **日志记录**: 使用loguru完整记录生成过程 + +## 📁 项目结构 + +``` +ppt_manager/ +├── static_ppt/ # 静态PPT模板目录 +│ └── macro_analysis_template.pptx # 23页模板示例 +├── config/ +│ └── project_config.yaml # 项目配置:页面映射关系 +├── scripts/ # 动态内容生成脚本 +│ ├── gdp_chart.py # GDP图表生成 +│ ├── inflation_chart.py # CPI/PPI图表 +│ ├── employment_table.py # 就业数据表 +│ ├── trade_chart.py # 进出口图表 +│ └── market_analysis.py # 市场分析图表 +├── dynamic_content/ # 动态生成的图片存放 +├── output/ # 最终生成的PPT输出目录 +├── logs/ # 日志目录 +├── src/ +│ ├── config_loader.py # 配置加载器 +│ ├── ppt_core.py # PPT核心操作 +│ ├── dynamic_generator.py # 动态内容生成器 +│ └── ppt_generator.py # 主合成引擎 +├── templates/ +│ └── index.html # Web界面 +├── app.py # Flask Web应用 +├── main.py # 命令行入口 +├── create_sample_template.py # 创建示例模板脚本 +└── requirements.txt # Python依赖 +``` + +## 🚀 快速开始 + +### 1. 安装依赖 + +```bash +cd ppt_manager +pip install -r requirements.txt +``` + +### 2. 初始化示例静态PPT模板 + +```bash +python create_sample_template.py +``` + +### 3. 运行系统 + +**方式一:Web界面(推荐)** +```bash +python app.py +``` +然后在浏览器打开: http://localhost:5000 + +**方式二:命令行** +```bash +python main.py macro_analysis +``` + +## ⚙️ 配置说明 + +在 [config/project_config.yaml](file:///f:/ppt/ppt_manager/config/project_config.yaml) 中配置: + +```yaml +projects: + macro_analysis: + name: "宏观数据分析报告" + static_ppt: "static_ppt/macro_analysis_template.pptx" + total_slides: 23 + slide_mapping: + 1: static # 静态页,来自模板 + 2: static + 3: static + 4: dynamic_chart_gdp # 动态页,执行对应脚本 + 5: dynamic_chart_inflation + ... + dynamic_generators: + dynamic_chart_gdp: "scripts/gdp_chart.py" # 脚本对应关系 + ... +``` + +## 📝 动态脚本编写规范 + +每个动态脚本必须包含一个 `generate(output_dir)` 函数,返回生成的图片路径: + +```python +def generate(output_dir): + # 1. 采集数据(API/爬虫/数据库等) + # 2. 生成图表(matplotlib/plotly等) + # 3. 返回图片完整路径 + return str(image_path) +``` + +## 🎯 使用场景示例 + +| 页面 | 类型 | 内容 | 更新频率 | +|------|------|------|----------| +| 1-3页 | static | 封面、目录、分析框架 | 固定不变 | +| 4页 | dynamic | GDP趋势图 | 每月更新 | +| 5页 | dynamic | CPI/PPI通胀图 | 每月更新 | +| 7页 | dynamic | 就业数据表 | 每月更新 | +| 10页 | dynamic | 进出口贸易 | 每月更新 | +| 13页 | dynamic | A股市场分析 | 每周更新 | +| 其他页 | static | 原理、方法论 | 固定不变 | diff --git a/ppt_manager/app.py b/ppt_manager/app.py new file mode 100644 index 0000000..c945666 --- /dev/null +++ b/ppt_manager/app.py @@ -0,0 +1,70 @@ +from flask import Flask, render_template, jsonify, send_file, request +from pathlib import Path +import sys +import os + +sys.path.insert(0, str(Path(__file__).parent / "src")) + +from src.ppt_generator import PPTGenerator +from loguru import logger + +app = Flask(__name__) +app.config['JSON_AS_ASCII'] = False + +generator = PPTGenerator() + +@app.route('/') +def index(): + projects = generator.list_projects() + return render_template('index.html', projects=projects) + +@app.route('/api/projects') +def list_projects(): + projects = generator.list_projects() + return jsonify({'success': True, 'projects': projects}) + +@app.route('/api/generate/', methods=['POST']) +def generate_ppt(project_name): + try: + output_path = generator.generate_project(project_name) + if output_path: + filename = os.path.basename(output_path) + return jsonify({ + 'success': True, + 'message': 'PPT生成成功', + 'filename': filename, + 'download_url': f'/download/{filename}' + }) + else: + return jsonify({'success': False, 'message': 'PPT生成失败,请查看日志'}) + except Exception as e: + logger.exception(f"生成PPT时发生错误: {e}") + return jsonify({'success': False, 'message': str(e)}) + +@app.route('/download/') +def download_file(filename): + output_dir = Path(__file__).parent / "output" + file_path = output_dir / filename + if file_path.exists(): + return send_file(str(file_path), as_attachment=True) + return jsonify({'success': False, 'message': '文件不存在'}) + +@app.route('/api/files') +def list_files(): + output_dir = Path(__file__).parent / "output" + files = [] + if output_dir.exists(): + for f in sorted(output_dir.glob("*.pptx"), reverse=True): + files.append({ + 'name': f.name, + 'size': round(f.stat().st_size / 1024 / 1024, 2), + 'modified': f.stat().st_mtime + }) + return jsonify({'success': True, 'files': files}) + +if __name__ == '__main__': + print("\n" + "="*60) + print("PPT管理系统 Web 界面启动中...") + print("请在浏览器中打开: http://localhost:5000") + print("="*60 + "\n") + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/ppt_manager/config/project_config.yaml b/ppt_manager/config/project_config.yaml new file mode 100644 index 0000000..8cff72a --- /dev/null +++ b/ppt_manager/config/project_config.yaml @@ -0,0 +1,42 @@ +projects: + macro_analysis: + name: "宏观数据分析报告" + static_ppt: "static_ppt/macro_analysis_template.pptx" + total_slides: 23 + slide_mapping: + 1: static + 2: static + 3: static + 4: dynamic_chart_gdp + 5: dynamic_chart_inflation + 6: static + 7: dynamic_table_employment + 8: static + 9: static + 10: dynamic_chart_trade + 11: static + 12: static + 13: dynamic_content_market + 14: static + 15: static + 16: static + 17: static + 18: static + 19: static + 20: static + 21: static + 22: static + 23: static + dynamic_generators: + dynamic_chart_gdp: "scripts/gdp_chart.py" + dynamic_chart_inflation: "scripts/inflation_chart.py" + dynamic_table_employment: "scripts/employment_table.py" + dynamic_chart_trade: "scripts/trade_chart.py" + dynamic_content_market: "scripts/market_analysis.py" + +settings: + output_dir: "output" + dynamic_content_dir: "dynamic_content" + log_level: "INFO" + image_width: 900 + image_height: 500 diff --git a/ppt_manager/create_sample_template.py b/ppt_manager/create_sample_template.py new file mode 100644 index 0000000..d30992e --- /dev/null +++ b/ppt_manager/create_sample_template.py @@ -0,0 +1,56 @@ +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.enum.text import PP_ALIGN +from pathlib import Path +from loguru import logger + +def create_sample_template(): + prs = Presentation() + + prs.slide_width = Inches(10) + prs.slide_height = Inches(7.5) + + logger.info("正在创建23页示例静态PPT模板...") + + for page_num in range(1, 24): + slide_layout = prs.slide_layouts[5] if page_num == 1 else prs.slide_layouts[6] + slide = prs.slides.add_slide(slide_layout) + + left = Inches(1) + top = Inches(0.5) + width = Inches(8) + height = Inches(1) + + title_box = slide.shapes.add_textbox(left, top, width, height) + tf = title_box.text_frame + p = tf.paragraphs[0] + + if page_num == 1: + p.text = "宏观数据分析报告" + p.font.size = Pt(36) + else: + p.text = f"第 {page_num} 页 - 示例内容" + p.font.size = Pt(24) + + p.font.bold = True + p.alignment = PP_ALIGN.CENTER + + if page_num not in [4, 5, 7, 10, 13]: + content_left = Inches(1) + content_top = Inches(2) + content_width = Inches(8) + content_height = Inches(4) + + content_box = slide.shapes.add_textbox(content_left, content_top, content_width, content_height) + content_tf = content_box.text_frame + content_tf.text = "这里是静态内容区域,原理固定不变的部分。\n\n定期更新的数据内容将由Python脚本自动生成并替换对应页面。" + content_tf.paragraphs[0].font.size = Pt(14) + + output_path = Path(__file__).parent / "static_ppt" / "macro_analysis_template.pptx" + output_path.parent.mkdir(parents=True, exist_ok=True) + prs.save(str(output_path)) + logger.success(f"示例模板已创建: {output_path}") + return str(output_path) + +if __name__ == "__main__": + create_sample_template() diff --git a/ppt_manager/main.py b/ppt_manager/main.py new file mode 100644 index 0000000..7045af5 --- /dev/null +++ b/ppt_manager/main.py @@ -0,0 +1,38 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent / "src")) + +print("=" * 60) +print("📊 PPT智能管理系统 - 命令行工具") +print("=" * 60) + +from src.ppt_generator import PPTGenerator + +def main(): + generator = PPTGenerator() + + print("\n可用项目:") + projects = generator.list_projects() + for proj in projects: + print(f" [{proj['id']}]: {proj['name']} ({proj['total_slides']}页)") + + if len(sys.argv) > 1: + project_name = sys.argv[1] + print(f"\n正在生成项目: {project_name}...") + output_path = generator.generate_project(project_name) + if output_path: + print(f"\n✅ 生成成功! 文件位置: {output_path}") + else: + print("\n❌ 生成失败,请查看日志") + else: + print("\n使用方法:") + print(" 命令行方式: python main.py macro_analysis") + print(" Web界面方式: python app.py (然后打开 http://localhost:5000)") + + choice = input("\n是否现在生成示例项目 [macro_analysis]? (y/n): ").strip().lower() + if choice == 'y': + generator.generate_project('macro_analysis') + +if __name__ == "__main__": + main() diff --git a/ppt_manager/requirements.txt b/ppt_manager/requirements.txt new file mode 100644 index 0000000..b32f329 --- /dev/null +++ b/ppt_manager/requirements.txt @@ -0,0 +1,11 @@ +python-pptx>=0.6.21 +Flask>=2.3.0 +pandas>=2.0.0 +matplotlib>=3.7.0 +plotly>=5.15.0 +requests>=2.31.0 +PyYAML>=6.0 +loguru>=0.7.0 +kaleido>=0.2.1 +lxml>=4.9.0 +Pillow>=10.0.0 diff --git a/ppt_manager/scripts/employment_table.py b/ppt_manager/scripts/employment_table.py new file mode 100644 index 0000000..0cc6afa --- /dev/null +++ b/ppt_manager/scripts/employment_table.py @@ -0,0 +1,62 @@ +import pandas as pd +import numpy as np +from pathlib import Path +from loguru import logger +import matplotlib.pyplot as plt + +def generate(output_dir): + logger.info("开始生成就业数据表") + + categories = ['城镇调查失业率', '青年失业率', '制造业PMI', '服务业PMI'] + current = [5.1, 15.3, 49.8, 53.2] + previous = [5.2, 15.6, 49.5, 53.0] + change = [c - p for c, p in zip(current, previous)] + + data = pd.DataFrame({ + '指标': categories, + '当前值': current, + '上期值': previous, + '变动': change + }) + + fig, ax = plt.subplots(figsize=(10, 6)) + ax.axis('tight') + ax.axis('off') + + table_data = [] + for _, row in data.iterrows(): + change_color = '#4CAF50' if row['变动'] >= 0 else '#F44336' + table_data.append([ + row['指标'], + f"{row['当前值']:.1f}", + f"{row['上期值']:.1f}", + f"{row['变动']:+.1f}" + ]) + + table = ax.table( + cellText=table_data, + colLabels=['指标', '当前值', '上期值', '变动'], + cellLoc='center', + loc='center', + colColours=['#4A90E2'] * 4 + ) + + table.auto_set_font_size(False) + table.set_fontsize(12) + table.scale(1, 2) + + for key, cell in table.get_celld().items(): + if key[0] > 0 and key[1] == 3: + cell.set_text_props(color='green' if '+' in cell.get_text().get_text() else 'red') + + ax.set_title('就业与PMI数据摘要', fontsize=16, fontweight='bold', pad=20) + + output_path = Path(output_dir) / 'employment_table.png' + plt.savefig(str(output_path), dpi=150, bbox_inches='tight') + plt.close() + + logger.info(f"就业数据表已生成: {output_path}") + return str(output_path) + +if __name__ == "__main__": + generate(Path.cwd()) diff --git a/ppt_manager/scripts/gdp_chart.py b/ppt_manager/scripts/gdp_chart.py new file mode 100644 index 0000000..4c43d15 --- /dev/null +++ b/ppt_manager/scripts/gdp_chart.py @@ -0,0 +1,32 @@ +import pandas as pd +import numpy as np +from pathlib import Path +from loguru import logger + +def generate(output_dir): + logger.info("开始生成GDP趋势图表") + + quarters = ['2024Q1', '2024Q2', '2024Q3', '2024Q4', '2025Q1', '2025Q2'] + gdp_growth = [5.2, 4.8, 5.0, 5.3, np.random.uniform(4.5, 5.5), np.random.uniform(4.5, 5.5)] + + data = pd.DataFrame({ + '季度': quarters, + 'GDP增长率(%)': [round(x, 2) for x in gdp_growth] + }) + data = data.set_index('季度') + + from dynamic_generator import DynamicContentGenerator + generator = DynamicContentGenerator() + output_path = generator.generate_chart_matplotlib( + data, + title='中国GDP增长趋势', + x_label='季度', + y_label='增长率 (%)', + filename='gdp_chart.png', + kind='line' + ) + + return output_path + +if __name__ == "__main__": + generate(Path.cwd()) diff --git a/ppt_manager/scripts/inflation_chart.py b/ppt_manager/scripts/inflation_chart.py new file mode 100644 index 0000000..f8f0fc5 --- /dev/null +++ b/ppt_manager/scripts/inflation_chart.py @@ -0,0 +1,34 @@ +import pandas as pd +import numpy as np +from pathlib import Path +from loguru import logger + +def generate(output_dir): + logger.info("开始生成CPI/PPI通胀图表") + + months = ['1月', '2月', '3月', '4月', '5月', '6月'] + cpi = [0.7, 0.8, 0.9, 1.0, np.random.uniform(0.8, 1.2), np.random.uniform(0.8, 1.2)] + ppi = [-2.5, -2.3, -2.0, -1.8, np.random.uniform(-2.5, -1.5), np.random.uniform(-2.5, -1.5)] + + data = pd.DataFrame({ + '月份': months, + 'CPI(%)': [round(x, 2) for x in cpi], + 'PPI(%)': [round(x, 2) for x in ppi] + }) + data = data.set_index('月份') + + from dynamic_generator import DynamicContentGenerator + generator = DynamicContentGenerator() + output_path = generator.generate_chart_plotly( + data, + title='CPI与PPI走势', + x_label='月份', + y_label='同比 (%)', + filename='inflation_chart.png', + kind='line' + ) + + return output_path + +if __name__ == "__main__": + generate(Path.cwd()) diff --git a/ppt_manager/scripts/market_analysis.py b/ppt_manager/scripts/market_analysis.py new file mode 100644 index 0000000..51ef3d9 --- /dev/null +++ b/ppt_manager/scripts/market_analysis.py @@ -0,0 +1,66 @@ +import pandas as pd +import numpy as np +from loguru import logger + +def generate(output_dir): + logger.info("开始生成市场分析图") + + indices = ['上证指数', '深证成指', '创业板指', '沪深300'] + current = [3150, 10200, 2050, 3800] + change_pct = [ + np.random.uniform(-2, 2), + np.random.uniform(-2, 2), + np.random.uniform(-2, 2), + np.random.uniform(-2, 2) + ] + + data = pd.DataFrame({ + '指数': indices, + '点位': current, + '涨跌幅(%)': [round(x, 2) for x in change_pct] + }) + + import matplotlib.pyplot as plt + plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] + plt.rcParams['axes.unicode_minus'] = False + + colors = ['green' if x < 0 else 'red' for x in change_pct] + + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 7)) + + bars = ax1.bar(data['指数'], data['点位'], color=colors, alpha=0.7) + ax1.set_title('主要指数点位', fontsize=14, fontweight='bold') + ax1.set_ylabel('点位') + ax1.grid(axis='y', linestyle='--', alpha=0.3) + + for bar in bars: + height = bar.get_height() + ax1.text(bar.get_x() + bar.get_width()/2., height, + f'{int(height)}', ha='center', va='bottom') + + bars2 = ax2.bar(data['指数'], data['涨跌幅(%)'], color=colors, alpha=0.7) + ax2.set_title('涨跌幅', fontsize=14, fontweight='bold') + ax2.set_ylabel('涨跌幅 (%)') + ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5) + ax2.grid(axis='y', linestyle='--', alpha=0.3) + + for bar in bars2: + height = bar.get_height() + va = 'bottom' if height > 0 else 'top' + ax2.text(bar.get_x() + bar.get_width()/2., height, + f'{height:+.2f}%', ha='center', va=va, fontweight='bold') + + plt.suptitle('A股市场概况', fontsize=18, fontweight='bold') + plt.tight_layout() + + from pathlib import Path + output_path = Path(output_dir) / 'market_analysis.png' + plt.savefig(str(output_path), dpi=150, bbox_inches='tight') + plt.close() + + logger.info(f"市场分析图已生成: {output_path}") + return str(output_path) + +if __name__ == "__main__": + from pathlib import Path + generate(Path.cwd()) diff --git a/ppt_manager/scripts/trade_chart.py b/ppt_manager/scripts/trade_chart.py new file mode 100644 index 0000000..ccfdfb6 --- /dev/null +++ b/ppt_manager/scripts/trade_chart.py @@ -0,0 +1,34 @@ +import pandas as pd +import numpy as np +from loguru import logger + +def generate(output_dir): + logger.info("开始生成进出口贸易图表") + + months = ['1月', '2月', '3月', '4月', '5月', '6月'] + exports = np.random.randint(2800, 3200, 6) + imports = np.random.randint(2000, 2400, 6) + + data = pd.DataFrame({ + '月份': months, + '出口(亿美元)': exports, + '进口(亿美元)': imports + }) + data = data.set_index('月份') + + from dynamic_generator import DynamicContentGenerator + generator = DynamicContentGenerator() + output_path = generator.generate_chart_matplotlib( + data, + title='进出口贸易情况', + x_label='月份', + y_label='金额 (亿美元)', + filename='trade_chart.png', + kind='bar' + ) + + return output_path + +if __name__ == "__main__": + from pathlib import Path + generate(Path.cwd()) diff --git a/ppt_manager/src/__init__.py b/ppt_manager/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ppt_manager/src/config_loader.py b/ppt_manager/src/config_loader.py new file mode 100644 index 0000000..9d1f775 --- /dev/null +++ b/ppt_manager/src/config_loader.py @@ -0,0 +1,39 @@ +import yaml +import os +from pathlib import Path +from loguru import logger + +class ConfigLoader: + def __init__(self, config_path=None): + if config_path is None: + base_dir = Path(__file__).parent.parent + config_path = base_dir / "config" / "project_config.yaml" + + self.config_path = Path(config_path) + self.config = self._load_config() + logger.info(f"配置文件已加载: {self.config_path}") + + def _load_config(self): + with open(self.config_path, 'r', encoding='utf-8') as f: + return yaml.safe_load(f) + + def get_project(self, project_name): + return self.config.get('projects', {}).get(project_name) + + def get_all_projects(self): + return self.config.get('projects', {}) + + def get_settings(self): + return self.config.get('settings', {}) + + def get_dynamic_script(self, project_name, dynamic_key): + project = self.get_project(project_name) + if project: + return project.get('dynamic_generators', {}).get(dynamic_key) + return None + + def get_slide_mapping(self, project_name): + project = self.get_project(project_name) + if project: + return project.get('slide_mapping', {}) + return {} diff --git a/ppt_manager/src/dynamic_generator.py b/ppt_manager/src/dynamic_generator.py new file mode 100644 index 0000000..2fa363c --- /dev/null +++ b/ppt_manager/src/dynamic_generator.py @@ -0,0 +1,93 @@ +import subprocess +import sys +from pathlib import Path +from loguru import logger +import importlib.util +import pandas as pd +import matplotlib.pyplot as plt +import plotly.express as px +import plotly.io as pio + +class DynamicContentGenerator: + def __init__(self, base_dir=None): + if base_dir is None: + self.base_dir = Path(__file__).parent.parent + else: + self.base_dir = Path(base_dir) + self.dynamic_content_dir = self.base_dir / "dynamic_content" + self.dynamic_content_dir.mkdir(parents=True, exist_ok=True) + logger.info(f"动态内容生成器初始化, 输出目录: {self.dynamic_content_dir}") + + def run_script(self, script_path): + script_file = self.base_dir / script_path + if not script_file.exists(): + logger.error(f"脚本文件不存在: {script_file}") + return None + + logger.info(f"执行动态脚本: {script_file}") + try: + spec = importlib.util.spec_from_file_location("dynamic_module", str(script_file)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if hasattr(module, 'generate'): + result = module.generate(self.dynamic_content_dir) + logger.success(f"脚本执行完成: {result}") + return result + else: + logger.error(f"脚本中没有找到 generate() 函数") + return None + except Exception as e: + logger.exception(f"执行脚本时出错: {e}") + return None + + def generate_chart_matplotlib(self, data, title, x_label, y_label, filename, kind='line'): + plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] + plt.rcParams['axes.unicode_minus'] = False + + fig, ax = plt.subplots(figsize=(12, 7)) + + if kind == 'line': + if isinstance(data, pd.DataFrame): + data.plot(ax=ax, kind='line', linewidth=2, marker='o') + else: + ax.plot(data) + elif kind == 'bar': + if isinstance(data, pd.DataFrame): + data.plot(ax=ax, kind='bar') + else: + ax.bar(range(len(data)), data) + + ax.set_title(title, fontsize=16, fontweight='bold') + ax.set_xlabel(x_label, fontsize=12) + ax.set_ylabel(y_label, fontsize=12) + ax.grid(True, linestyle='--', alpha=0.7) + plt.legend() + plt.tight_layout() + + output_path = self.dynamic_content_dir / filename + plt.savefig(str(output_path), dpi=150, bbox_inches='tight') + plt.close() + logger.info(f"图表已生成: {output_path}") + return str(output_path) + + def generate_chart_plotly(self, data, title, x_label, y_label, filename, kind='line'): + if isinstance(data, pd.DataFrame): + if kind == 'line': + fig = px.line(data, title=title) + elif kind == 'bar': + fig = px.bar(data, title=title) + else: + fig = px.line(y=data, title=title) + + fig.update_layout( + xaxis_title=x_label, + yaxis_title=y_label, + font=dict(family="Microsoft YaHei, SimHei"), + title_x=0.5 + ) + + output_path = self.dynamic_content_dir / filename + pio.write_image(fig, str(output_path), width=1200, height=700, scale=2) + logger.info(f"Plotly图表已生成: {output_path}") + return str(output_path) diff --git a/ppt_manager/src/ppt_core.py b/ppt_manager/src/ppt_core.py new file mode 100644 index 0000000..b02c75d --- /dev/null +++ b/ppt_manager/src/ppt_core.py @@ -0,0 +1,71 @@ +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.enum.text import PP_ALIGN +from pathlib import Path +from loguru import logger +import os + +class PPTCore: + def __init__(self, base_dir=None): + if base_dir is None: + self.base_dir = Path(__file__).parent.parent + else: + self.base_dir = Path(base_dir) + logger.info(f"PPT核心模块初始化, 基础目录: {self.base_dir}") + + def load_static_ppt(self, static_ppt_path): + ppt_path = self.base_dir / static_ppt_path + logger.info(f"加载静态PPT: {ppt_path}") + if not ppt_path.exists(): + logger.error(f"静态PPT文件不存在: {ppt_path}") + raise FileNotFoundError(f"静态PPT文件不存在: {ppt_path}") + return Presentation(str(ppt_path)) + + def create_new_ppt(self): + return Presentation() + + def copy_slide(self, source_prs, target_prs, slide_index): + source_slide = source_prs.slides[slide_index - 1] + slide_layout = target_prs.slide_layouts[6] if len(target_prs.slide_layouts) > 6 else target_prs.slide_layouts[0] + new_slide = target_prs.slides.add_slide(slide_layout) + + for shp in new_slide.shapes: + sp = shp.element + sp.getparent().remove(sp) + + for shape in source_slide.shapes: + el = shape.element + new_slide.shapes._spTree.insert_element_before(el, 'p:extLst') + + logger.debug(f"已复制第 {slide_index} 页") + return new_slide + + def insert_image_to_slide(self, slide, image_path, left=1, top=1.5, width=8, height=5): + left_inch = Inches(left) + top_inch = Inches(top) + width_inch = Inches(width) + height_inch = Inches(height) + + slide.shapes.add_picture(str(image_path), left_inch, top_inch, width_inch, height_inch) + logger.info(f"已插入图片: {image_path}") + + def add_title_to_slide(self, slide, title_text, font_size=24, is_bold=True): + left = Inches(0.5) + top = Inches(0.2) + width = Inches(9) + height = Inches(0.8) + + title_box = slide.shapes.add_textbox(left, top, width, height) + tf = title_box.text_frame + tf.text = title_text + p = tf.paragraphs[0] + p.font.size = Pt(font_size) + p.font.bold = is_bold + p.alignment = PP_ALIGN.CENTER + + def save_ppt(self, prs, output_path): + output_file = self.base_dir / output_path + output_file.parent.mkdir(parents=True, exist_ok=True) + prs.save(str(output_file)) + logger.success(f"PPT已保存: {output_file}") + return str(output_file) diff --git a/ppt_manager/src/ppt_generator.py b/ppt_manager/src/ppt_generator.py new file mode 100644 index 0000000..8ef0bd9 --- /dev/null +++ b/ppt_manager/src/ppt_generator.py @@ -0,0 +1,119 @@ +from pathlib import Path +from loguru import logger +from datetime import datetime +import sys + +sys.path.insert(0, str(Path(__file__).parent)) + +from config_loader import ConfigLoader +from ppt_core import PPTCore +from dynamic_generator import DynamicContentGenerator + +class PPTGenerator: + def __init__(self, base_dir=None): + if base_dir is None: + self.base_dir = Path(__file__).parent.parent + else: + self.base_dir = Path(base_dir) + + self.config_loader = ConfigLoader() + self.ppt_core = PPTCore(self.base_dir) + self.dynamic_generator = DynamicContentGenerator(self.base_dir) + + log_file = self.base_dir / "logs" / f"ppt_generator_{datetime.now().strftime('%Y%m%d')}.log" + log_file.parent.mkdir(parents=True, exist_ok=True) + logger.add(str(log_file), rotation="10 MB", level="INFO", encoding="utf-8") + + logger.info("PPT生成器初始化完成") + + def generate_project(self, project_name): + logger.info(f"开始生成项目: {project_name}") + + project_config = self.config_loader.get_project(project_name) + if not project_config: + logger.error(f"找不到项目配置: {project_name}") + return None + + slide_mapping = self.config_loader.get_slide_mapping(project_name) + total_slides = project_config.get('total_slides', 0) + static_ppt_path = project_config.get('static_ppt', '') + + logger.info(f"加载静态PPT: {static_ppt_path}") + logger.info(f"总页数: {total_slides}") + + dynamic_generated_files = {} + for slide_num, mapping in slide_mapping.items(): + if mapping != 'static': + script_path = self.config_loader.get_dynamic_script(project_name, mapping) + if script_path: + logger.info(f"执行第 {slide_num} 页动态脚本: {script_path}") + result = self.dynamic_generator.run_script(script_path) + if result: + dynamic_generated_files[slide_num] = result + logger.success(f"第 {slide_num} 页动态内容生成成功: {result}") + + try: + source_prs = self.ppt_core.load_static_ppt(static_ppt_path) + target_prs = self.ppt_core.create_new_ppt() + + for slide_num in range(1, total_slides + 1): + mapping = slide_mapping.get(slide_num, 'static') + + if mapping == 'static': + logger.info(f"处理第 {slide_num} 页: 静态内容") + if slide_num - 1 < len(source_prs.slides): + self.ppt_core.copy_slide(source_prs, target_prs, slide_num) + else: + logger.warning(f"静态PPT中缺少第 {slide_num} 页,创建空白页") + slide_layout = target_prs.slide_layouts[6] if len(target_prs.slide_layouts) > 6 else target_prs.slide_layouts[0] + target_prs.slides.add_slide(slide_layout) + else: + logger.info(f"处理第 {slide_num} 页: 动态内容 - {mapping}") + slide_layout = target_prs.slide_layouts[6] if len(target_prs.slide_layouts) > 6 else target_prs.slide_layouts[0] + new_slide = target_prs.slides.add_slide(slide_layout) + + self.ppt_core.add_title_to_slide(new_slide, f"第{slide_num}页 - 动态更新内容", font_size=20) + + if slide_num in dynamic_generated_files: + image_path = dynamic_generated_files[slide_num] + if Path(image_path).exists(): + self.ppt_core.insert_image_to_slide(new_slide, image_path, left=0.8, top=1.2, width=8.5, height=5) + + output_filename = f"{project_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pptx" + output_path = Path("output") / output_filename + final_path = self.ppt_core.save_ppt(target_prs, str(output_path)) + + logger.success(f"=" * 50) + logger.success(f"项目 {project_name} 生成完成!") + logger.success(f"输出文件: {final_path}") + logger.success(f"=" * 50) + + return final_path + + except Exception as e: + logger.exception(f"生成PPT时发生错误: {e}") + return None + + def list_projects(self): + projects = self.config_loader.get_all_projects() + result = [] + for key, info in projects.items(): + result.append({ + 'id': key, + 'name': info.get('name', key), + 'total_slides': info.get('total_slides', 0), + 'slide_mapping': info.get('slide_mapping', {}) + }) + return result + +if __name__ == "__main__": + generator = PPTGenerator() + + if len(sys.argv) > 1: + project_name = sys.argv[1] + generator.generate_project(project_name) + else: + print("可用项目:") + for proj in generator.list_projects(): + print(f" - {proj['id']}: {proj['name']} ({proj['total_slides']}页)") + print("\n使用方法: python ppt_generator.py <项目名称>") diff --git a/ppt_manager/templates/index.html b/ppt_manager/templates/index.html new file mode 100644 index 0000000..e8a6c73 --- /dev/null +++ b/ppt_manager/templates/index.html @@ -0,0 +1,179 @@ + + + + + + PPT智能管理系统 + + + + +
+
+

+ 📊 PPT智能管理系统 +

+

静态模板 + 动态数据 = 一键生成最新报告

+
+ +
+
+

+ 📁 可用项目 +

+ +
+ {% for project in projects %} +
+
+
+

{{ project.name }}

+

共 {{ project.total_slides }} 页

+
+ +
+
+ + 静态: {{ project.slide_mapping.values() | list | count('static') }} 页 + + + 动态: {{ project.total_slides - (project.slide_mapping.values() | list | count('static')) }} 页 + +
+
+ {% endfor %} +
+ + + + + + +
+ +
+

+ 📋 已生成文件 +

+
+

加载中...

+
+
+
+ +
+

+ 📖 使用说明 +

+
+
+

1️⃣ 准备静态模板

+

将固定不变的PPT模板放入 static_ppt 目录

+
+
+

2️⃣ 配置页面映射

+

在 config/project_config.yaml 中配置每页是静态或动态

+
+
+

3️⃣ 编写动态脚本

+

在 scripts 目录编写数据爬取和图表生成脚本

+
+
+
+
+ + + + diff --git a/ppt_manager_v2/README.md b/ppt_manager_v2/README.md new file mode 100644 index 0000000..ff486e5 --- /dev/null +++ b/ppt_manager_v2/README.md @@ -0,0 +1,383 @@ +# 📊 PPT智能管理系统 V2.0 说明书 + +基于 **锚点定位** + **原生图表更新** + **插件化架构** + **WebSocket实时日志** 的下一代PPT自动化平台。 + +--- + +## 🚀 快速开始 + +### 1. 环境准备 + +```bash +cd f:\ppt\ppt_manager_v2 +pip install -r requirements_v2.txt +``` + +### 2. 启动WebSocket服务 + +```bash +python web_socket_app.py +``` + +### 3. 浏览器访问 + +打开 **http://localhost:5001** + +--- + +## 📁 项目目录结构 + +``` +ppt_manager_v2/ +├── 📄 web_socket_app.py # WebSocket服务端 +├── 📄 orchestrator.py # 主编排引擎(Pipeline) +├── 📄 requirements_v2.txt # Python依赖包 +├── 📄 README.md # 本文件 +├── +├── 📂 core/ # 核心层 +│ ├── anchor_engine.py # ✅ 锚点定位引擎 +│ ├── native_chart.py # ✅ 原生图表数据源更新 +│ └── conditional_renderer.py # ✅ 条件渲染引擎(动态增删页) +├── +├── 📂 plugins/ # 插件化架构 +│ ├── base_generator.py # BaseGenerator 标准接口 +│ └── generators/ # 插件目录(自动扫描注册) +│ ├── gdp_generator.py # GDP趋势图生成插件 +│ └── cpi_generator.py # CPI/PPI图表生成插件 +├── +├── 📂 ai/ # AI智能化 +│ └── llm_analyst.py # LLM分析师 + Diff对比器 +├── +├── 📂 connectors/ # 多数据源适配 +│ └── sql_connector.py # MySQL/ClickHouse/REST API/CSV +├── +├── 📂 config/ +│ └── project_config_v2.yaml # V2版配置文件范式 +├── +├── 📂 templates/ # Flask模板目录 +│ └── index_v2.html # WebSocket前端页面 +├── +├── 📂 logs/ # 日志目录 +└── 📂 web/ # 备用(template_dir配置用) + └── templates/ + └── index_v2.html +``` + +> **输出目录**: `f:\ppt\output\` (所有生成的PPT文件在这里) + +--- + +## 🎯 五大核心增强能力 + +### 一、PPT核心操作层(从"拼图"到"原生替换") + +#### 1.1 锚点(Anchor)定位引擎 → 无惧页码变动 + +**痛点解决**: 旧版YAML写 `page: 5`,老板在第3页插一页,所有配置页码全乱。 + +**新版方案**: + +1. 打开你的PPT模板 → 选中一个 **Shape/图表/文本框** +2. 在PowerPoint的 **"选择窗格"** 里给Shape改名(例如:`chart_gdp_trend`) +3. 在YAML里直接配置 **锚点名**,不是页码: + +```yaml +anchors: + - name: chart_gdp_trend # Shape Name(选择窗格里的名称) + type: native_chart_update + plugin: gdp_chart +``` + +**代码参考**: [core/anchor_engine.py](file:///f:/ppt/ppt_manager_v2/core/anchor_engine.py#L21-L60) + +--- + +#### 1.2 原生图表数据源直接更新(不是插PNG!) + +**痛点解决**: 旧版先生成PNG再插入PPT → 图片无法编辑、丢失主题配色和动画效果、文件体积大。 + +**新版方案**: 用 `python-pptx` 直接替换PPT内嵌图表的Excel数据源: + +```python +# 在native_chart.py里的实现 +chart_data = CategoryChartData() +chart_data.categories = ['2026Q1', '2026Q2', '2026Q3'] +chart_data.add_series('GDP同比增长', [5.2, 5.0, 5.1]) +chart.replace_data(chart_data) # ✅ 原生数据源替换! +``` + +**这样做的好处**: +- ✅ 生成的PPT里**双击图表可以进入Excel编辑** +- ✅ 自动继承PPT母版的**主题配色** +- ✅ 保留图表原有的**进入/强调动画**效果 +- ✅ 文本清晰,不是模糊的点阵图 + +**代码参考**: [core/native_chart.py](file:///f:/ppt/ppt_manager_v2/core/native_chart.py#L12-L90) + +--- + +#### 1.3 条件渲染与动态增删页 + +**能力描述**: 支持规则判断动态插入/隐藏页面: + +```yaml +slide_conditions: + - condition: unemployment_rate > 5.1 + action: insert_slide + template: "risk_warning_template.pptx" + position: 5 +``` + +表达式求值:`unemployment_rate > 5.1 AND gdp_growth < 5.0` 等复合条件。 + +**代码参考**: [core/conditional_renderer.py](file:///f:/ppt/ppt_manager_v2/core/conditional_renderer.py#L1-L100) + +--- + +### 二、插件化数据流架构(真正的即插即用) + +#### 2.1 BaseGenerator 标准接口 + +所有插件继承 `BaseGenerator` 抽象基类,实现两个方法: + +```python +# plugins/base_generator.py 定义 +class BaseGenerator(ABC): + generator_id = "gdp_chart" + generator_name = "GDP趋势生成器" + + @abstractmethod + def fetch_data(self, params): # 步骤A:取数 + pass + + @abstractmethod + def render(self): # 步骤B:渲染返回原生图表数据 + pass +``` + +插件例子 → [plugins/generators/gdp_generator.py](file:///f:/ppt/ppt_manager_v2/plugins/generators/gdp_generator.py) + +--- + +#### 2.2 插件自动扫描注册 + +启动时系统自动扫描 `plugins/generators/` 目录下所有 `.py` 文件,发现继承 BaseGenerator 且有 `generator_id` 的类自动注册到插件管理器,**无需修改任何注册代码**,真正即插即用。 + +``` +# 启动日志里可以看到: +SUCCESS | 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +SUCCESS | 加载插件 [gdp_chart]: GDP趋势图表生成器 +INFO | 插件扫描完成,共加载 2 个生成器 +``` + +**代码参考**: [plugins/base_generator.py - GeneratorPluginManager.discover_plugins()](file:///f:/ppt/ppt_manager_v2/plugins/base_generator.py#L46-L89) + +--- + +#### 2.3 参数化生成 + +Web端不是单一"开始生成"按钮,而是传参给后端插件: + +```javascript +// 前端传params给WebSocket +socket.emit('start_generation', { + params: { + year: 2026, + quarter: "Q2", + theme: "保守型" + } +}); +``` + +插件的 `fetch_data(params)` 拿到这些参数去数据库/API拉对应区间的数据。 + +--- + +### 三、WebSocket Web交互体验(实时日志推流) + +#### 3.1 启动服务 + +```bash +python web_socket_app.py +``` + +控制台输出: +``` +================================================================= + 🚀 PPT智能管理系统 V2.0 - WebSocket实时日志版 + 请在浏览器打开: http://localhost:5001 +================================================================= +``` + +#### 3.2 前端页面三大区域 + +| 区域 | 功能 | +|------|------| +| **左上角参数表单** | Year/Quarter 参数化输入 | +| **左下角实时面板** | 动态进度条 0-100% + Loguru日志逐行推送 | +| **右侧边栏** | 已加载插件清单 + 历史生成文件下载列表 | + +**WebSocket后端代码**: [web_socket_app.py](file:///f:/ppt/ppt_manager_v2/web_socket_app.py#L1-L95) + +--- + +### 四、主编排引擎 Pipeline + +Orchestrator 是串联所有模块的大脑: + +```python +# orchestrator.py - run_full_pipeline() +def run_full_pipeline(self): + self.load_template() # 1. 打开PPT,扫描所有锚点 + self.run_plugins(params) # 2. 并行执行所有插件 fetch_data + render + self.update_native_charts() # 3. 原生图表数据源逐个更新 + self.ai_generate_summary() # 4. LLM生成200字洞察文本 + self.process_conditions() # 5. 条件表达式求值动态增删页 + return self.save() # 6. 保存最终PPT +``` + +**Pipeline代码位置**: [orchestrator.py - 第244-251行](file:///f:/ppt/ppt_manager_v2/orchestrator.py#L244-L251) + +--- + +### 五、AI 智能化(LLM + Diff对比) + +#### 5.1 LLM 自动生成"分析结论" + +图表只能展示"是什么",LLM帮你解释"为什么": + +```python +# ai/llm_analyst.py +prompt = "基于以下数据:{gdp}、{cpi},200字专业分析师口吻总结本月市场" +llm.generate_analysis({ + 'gdp_growth': 5.1, + 'cpi': 0.9, + 'unemployment': 5.2 +}) + +# 返回示例: +""" +本月宏观经济洞察:GDP增长5.1%,动能平稳。CPI同比0.9%, +通胀水平温和为货币政策留出空间。就业失业率5.2%, +青年失业率需重点关注... +""" +``` + +支持 **Mock模式** / **OpenAI** / **通义千问** 三种provider切换。 + +**代码参考**: [ai/llm_analyst.py](file:///f:/ppt/ppt_manager_v2/ai/llm_analyst.py#L1-L90) + +--- + +## 📄 YAML V2版 配置范式参考 + +```yaml +project_name: "宏观经济月度分析报告" +version: "2.0" +template: "macro_analysis_template.pptx" + +params: # 参数化生成默认值 + default_year: 2026 + default_quarter: "Q2" + +anchors: # 锚点绑定关系 + - name: chart_gdp + type: native_chart_update + plugin: gdp_chart + chart_type: line + fallback: gdp_fallback.png + + - name: chart_cpi + type: native_chart_update + plugin: cpi_chart + +slide_conditions: # 条件渲染规则 + - condition: unemployment_rate > 5.1 + action: insert_slide + position: 5 + +connectors: # 多数据源配置 + mysql_stats: {type: mysql, database: macro_stats} + +ai: # LLM配置 + provider: mock # mock/openai/tongyi + model: qwen-turbo + +scheduler: # 定时调度 + cron: "0 8 1 * *" # 每月1号早上8点 +``` + +配置文件位置: [config/project_config_v2.yaml](file:///f:/ppt/ppt_manager_v2/config/project_config_v2.yaml) + +--- + +## 🔧 命令行非交互模式测试 + +直接跑整个Pipeline验证所有环节: + +```bash +cd f:\ppt\ppt_manager_v2 +python -c " +import sys +sys.path.insert(0, '.') +from orchestrator import Orchestrator +orch = Orchestrator() +orch.load_template() # 检查: 锚点扫描 +result = orch.run_plugins() # 检查: 插件取数+渲染 +print('插件结果:', list(result.keys())) +orch.update_native_charts() # 检查: 原生图表更新 +orch.ai_generate_summary() # 检查: LLM生成 +orch.save() # 检查: 输出文件 +print('✅ 全流程通过!') +" +``` + +--- + +## ❓ 常见问题排查 + +### Q1: TemplateNotFound jinja2 错误 + +**现象**: `jinja2.exceptions.TemplateNotFound: index_v2.html` + +**已修复**: web_socket_app.py第16行明确了 template_folder 绝对路径,且文件在两处备份: +- `f:\ppt\ppt_manager_v2\templates\index_v2.html` (Flask标准位置) +- `f:\ppt\ppt_manager_v2\web\templates\index_v2.html` (配置位置) + +--- + +### Q2: 下载文件找不到 /api/files 返回空 + +**已修复**: `/api/files` 和 `/download/` 两处的 `base_dir / "output"` 统一改为 `base_dir.parent / "output"` 指向 `f:\ppt\output\`。 + +--- + +### Q3: 'NoneType' object has no attribute 'text' + +**现象**: 加载模板时报错 + +**已修复**: [orchestrator.py](file:///f:/ppt/ppt_manager_v2/orchestrator.py#L75-L90) 先判断 `if slide.shapes.title:` 再安全访问 `.text`。 + +--- + +### Q4: 插件不显示/不执行 + +检查两点: +1. 插件类继承 `BaseGenerator(ABC)` +2. 类属性 `generator_id` 不能是 `None` +3. 目录在 `plugins/generators/*.py` 且文件名不以 `_` 开头 + +--- + +## 📌 维护信息 + +| 项目 | 详情 | +|------|------| +| **版本** | V2.0 2026-05-29 | +| **入口** | `python web_socket_app.py` | +| **前端地址** | http://localhost:5001 | +| **输出目录** | `f:\ppt\output\*.pptx` | +| **日志目录** | `f:\ppt\ppt_manager_v2\logs\` | +| **核心模块** | orchestrator.py / anchor_engine.py / native_chart.py | +| **插件目录** | plugins/generators/ | + diff --git a/ppt_manager_v2/__pycache__/orchestrator.cpython-311.pyc b/ppt_manager_v2/__pycache__/orchestrator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e3bd1545dee9f3d1d0a64f213fac5e55ce05f43 GIT binary patch literal 16535 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%0A$HA`SU;Xk2 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 str: + default_prompt = """ +基于以下宏观经济数据,用专业分析师的口吻撰写市场洞察: +{data_context} + +要求: +1. 不超过 {max_words} 汉字 +2. 专业、客观、具有洞察力 +3. 突出核心指标的边际变化 +4. 适合放在PPT首页作为摘要 +""" + + prompt = prompt_template or default_prompt + data_str = json.dumps(data_context, ensure_ascii=False, indent=2) + final_prompt = prompt.format(data_context=data_str, max_words=max_words) + + if self.provider == 'mock': + return self._mock_analysis(data_context) + elif self.provider == 'openai': + return self._call_openai(final_prompt) + elif self.provider == 'tongyi': + return self._call_tongyi(final_prompt) + else: + return self._mock_analysis(data_context) + + def _mock_analysis(self, context: Dict) -> str: + gdp = context.get('gdp_growth', 'N/A') + cpi = context.get('cpi', 'N/A') + unemployment = context.get('unemployment', 'N/A') + + return f"""本月宏观经济洞察:GDP增长{gdp}%,经济扩张动能平稳。CPI同比{cpi}%,通胀水平温和,为货币政策留出空间。就业市场{self._format_unemployment(unemployment)},青年失业率需重点关注。整体来看,经济处于弱复苏通道,建议关注基建投资与消费修复的进度。""" + + def _format_unemployment(self, val): + try: + v = float(val) + if v > 5.5: + return f"压力较大({val}%)" + elif v > 5: + return f"基本稳定({val}%)" + else: + return f"表现良好({val}%)" + except: + return "数据待更新" + + def _call_openai(self, prompt: str) -> str: + try: + from openai import OpenAI + client = OpenAI(api_key=self.config.get('api_key')) + response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": prompt}] + ) + return response.choices[0].message.content + except Exception as e: + logger.warning(f"OpenAI调用失败: {e}") + return self._mock_analysis({}) + + def _call_tongyi(self, prompt: str) -> str: + try: + import dashscope + dashscope.api_key = self.config.get('api_key') + response = dashscope.Generation.call( + model='qwen-turbo', + prompt=prompt + ) + return response.output.text + except Exception as e: + logger.warning(f"通义千问调用失败: {e}") + return self._mock_analysis({}) + +class DiffAnalyzer: + def __init__(self): + self.changes = [] + + def compare_dataframes(self, df_old, df_new, key_cols: List = None): + key_cols = key_cols or df_old.columns[:1].tolist() + + for col in df_old.columns: + if col not in key_cols and col in df_new.columns: + try: + old_val = df_old[col].iloc[-1] if len(df_old) > 0 else 0 + new_val = df_new[col].iloc[-1] if len(df_new) > 0 else 0 + + if isinstance(old_val, (int, float)) and isinstance(new_val, (int, float)): + diff = float(new_val) - float(old_val) + if abs(diff) > 0.01: + self.changes.append({ + 'indicator': col, + 'old': round(float(old_val), 2), + 'new': round(float(new_val), 2), + 'delta': round(diff, 2), + 'direction': 'up' if diff > 0 else 'down' + }) + except: + pass + + logger.info(f"Diff分析发现 {len(self.changes)} 项显著变动") + return self.changes + + def generate_diff_report(self) -> str: + if not self.changes: + return "本月与上月数据相比无显著变动。" + + report_parts = ["📊 数据异动 Diff 简报:\n"] + for chg in self.changes[:5]: + arrow = "↑" if chg['direction'] == 'up' else "↓" + report_parts.append( + f"• {chg['indicator']}: {chg['old']} → {chg['new']} " + f"({arrow}{abs(chg['delta'])})" + ) + + return "\n".join(report_parts) diff --git a/ppt_manager_v2/config/project_config_v2.yaml b/ppt_manager_v2/config/project_config_v2.yaml new file mode 100644 index 0000000..bdf3da4 --- /dev/null +++ b/ppt_manager_v2/config/project_config_v2.yaml @@ -0,0 +1,86 @@ +project_name: "宏观经济月度分析报告" +version: "2.0" +template: "macro_analysis_template.pptx" +output_dir: "../output" + +params: + default_year: 2026 + default_quarter: "Q2" + theme: "professional" + +global_context: + report_month: "2026-05" + author: "宏观研究组" + +anchors: + - name: "text_report_period" + type: "text_replace" + template: "宏观经济月度分析 - {report_month}" + + - name: "text_llm_summary" + type: "ai_text_generate" + prompt_template: | + 基于以下数据:{all_data} + 请用专业分析师的口吻总结本月市场趋势,限200字以内。 + + - name: "chart_gdp" + type: "native_chart_update" + plugin: "gdp_chart" + chart_type: "line" + fallback: "gdp_fallback.png" + + - name: "chart_cpi" + type: "native_chart_update" + plugin: "cpi_chart" + chart_type: "line_markers" + + - name: "chart_employment" + type: "native_chart_update_or_image" + script: "scripts/employment_table.py" + chart_type: "column" + + - name: "table_trade" + type: "native_table_update" + connector: "sql" + query: "SELECT month, exports, imports FROM trade_data WHERE year = {params.year}" + +slide_conditions: + - slide_anchor: "slide_risk_warning" + condition: "unemployment_rate > 5.1" + action: "show" + alternate: "hide" + + - condition: "{gdp_growth} < 5.0" + action: "insert_slide" + template: "templates/slowdown_warning.pptx" + position: 5 + +connectors: + mysql_stats: + type: "mysql" + host: "localhost" + port: 3306 + database: "macro_stats" + username: "readonly" + + api_nbs: + type: "rest_api" + base_url: "https://api.stats.gov.cn/v1/" + timeout: 30 + +ai: + provider: "mock" + api_key: "" + model: "qwen-turbo" + cache_ttl: 3600 + +scheduler: + cron: "0 8 1 * *" + notify_channels: + - type: "wechat_work" + webhook: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" + +logging: + level: "INFO" + file: "../logs/ppt_generator_v2.log" + rotation: "10 MB" diff --git a/ppt_manager_v2/connectors/sql_connector.py b/ppt_manager_v2/connectors/sql_connector.py new file mode 100644 index 0000000..03ace05 --- /dev/null +++ b/ppt_manager_v2/connectors/sql_connector.py @@ -0,0 +1,142 @@ +from typing import Dict, Any, List, Optional +import pandas as pd +from pathlib import Path +from loguru import logger + +class SQLConnector: + def __init__(self, config: Dict[str, Any] = None): + self.config = config or {} + self._connection = None + + def connect(self, conn_type: str = None, **kwargs) -> bool: + conn_type = conn_type or self.config.get('type', 'sqlite') + try: + if conn_type == 'sqlite': + import sqlite3 + db_path = kwargs.get('db_path', self.config.get('db_path', ':memory:')) + self._connection = sqlite3.connect(db_path) + logger.info(f"已连接到 SQLite: {db_path}") + + elif conn_type in ['mysql', 'mariadb']: + try: + import pymysql + self._connection = pymysql.connect( + host=kwargs.get('host', self.config.get('host', 'localhost')), + port=kwargs.get('port', self.config.get('port', 3306)), + user=kwargs.get('user', self.config.get('user', 'root')), + password=kwargs.get('password', self.config.get('password', '')), + database=kwargs.get('database', self.config.get('database')), + charset='utf8mb4' + ) + logger.info("已连接到 MySQL") + except ImportError: + logger.warning("pymysql 未安装,尝试使用 sqlite") + import sqlite3 + self._connection = sqlite3.connect(':memory:') + + elif conn_type == 'clickhouse': + self._connection = {'type': 'clickhouse', 'config': {**self.config, **kwargs}} + logger.info("ClickHouse 连接配置已保存") + + return True + + except Exception as e: + logger.exception(f"SQL连接失败: {e}") + return False + + def query(self, sql: str, params: tuple = None) -> Optional[pd.DataFrame]: + if not self._connection: + logger.error("未建立数据库连接") + return None + + try: + if isinstance(self._connection, dict) and self._connection.get('type') == 'clickhouse': + logger.warning("ClickHouse 需要 clickhouse_driver 库,这里使用模拟") + return pd.DataFrame({'column1': [1, 2, 3], 'column2': ['a', 'b', 'c']}) + + df = pd.read_sql_query(sql, self._connection, params=params or ()) + logger.success(f"SQL查询成功,返回 {len(df)} 行") + return df + + except Exception as e: + logger.exception(f"SQL查询失败: {e}") + return None + + def execute(self, sql: str, params: tuple = None) -> int: + if not self._connection: + return -1 + + try: + cursor = self._connection.cursor() + cursor.execute(sql, params or ()) + self._connection.commit() + return cursor.rowcount + except Exception as e: + logger.exception(f"SQL执行失败: {e}") + return -1 + + def close(self): + if self._connection and hasattr(self._connection, 'close'): + self._connection.close() + logger.info("数据库连接已关闭") + +class APIConnector: + def __init__(self, base_url: str = None, headers: Dict[str, str] = None): + self.base_url = base_url + self.headers = headers or {} + + def get(self, endpoint: str, params: Dict[str, Any] = None, **kwargs) -> Optional[Dict]: + import requests + try: + url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}" if self.base_url else endpoint + response = requests.get(url, params=params, headers={**self.headers, **kwargs.pop('headers', {})}, timeout=30) + response.raise_for_status() + logger.info(f"API GET成功: {endpoint}") + return response.json() + except Exception as e: + logger.exception(f"API GET失败: {e}") + return None + + def post(self, endpoint: str, json: Dict = None, **kwargs) -> Optional[Dict]: + import requests + try: + url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}" if self.base_url else endpoint + response = requests.post(url, json=json, headers=self.headers, timeout=30) + response.raise_for_status() + return response.json() + except Exception as e: + logger.exception(f"API POST失败: {e}") + return None + +class FileConnector: + @staticmethod + def read_csv(file_path: str, **kwargs) -> Optional[pd.DataFrame]: + try: + df = pd.read_csv(file_path, **kwargs) + logger.info(f"读取CSV: {file_path}, {len(df)}行") + return df + except Exception as e: + logger.exception(f"读取CSV失败: {e}") + return None + + @staticmethod + def read_excel(file_path: str, sheet_name: str = None, **kwargs) -> Optional[pd.DataFrame]: + try: + df = pd.read_excel(file_path, sheet_name=sheet_name or 0, **kwargs) + logger.info(f"读取Excel: {file_path}, sheet={sheet_name}") + return df + except Exception as e: + logger.exception(f"读取Excel失败: {e}") + return None + + @staticmethod + def read_json(file_path: str, **kwargs) -> Optional[Dict]: + import json + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + logger.info(f"读取JSON: {file_path}") + return data + except Exception as e: + logger.exception(f"读取JSON失败: {e}") + return None diff --git a/ppt_manager_v2/core/__init__.py b/ppt_manager_v2/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ppt_manager_v2/core/__pycache__/__init__.cpython-311.pyc b/ppt_manager_v2/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc6abc4d914ded9f4da4d7100f58f40cc948c33f GIT binary patch literal 147 zcmZ3^%ge<81cDzWvOx4>5CH>>P{wCAAY(d13PUi1CZpd9p<|U@57R8qt#U$q!rN+d^XXa&=#K-FuRQ}?y$<0qG%}KQ@Vg+gg8C=W{Bt9@R OGBSQ(fDuK^KrsM`w;(wH literal 0 HcmV?d00001 diff --git a/ppt_manager_v2/core/__pycache__/anchor_engine.cpython-311.pyc b/ppt_manager_v2/core/__pycache__/anchor_engine.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3d7410042ddf8daf04ea2f6b8ad26f11de20cfb GIT binary patch literal 5804 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 literal 0 HcmV?d00001 diff --git a/ppt_manager_v2/core/anchor_engine.py b/ppt_manager_v2/core/anchor_engine.py new file mode 100644 index 0000000..77ba9af --- /dev/null +++ b/ppt_manager_v2/core/anchor_engine.py @@ -0,0 +1,80 @@ +from pptx import Presentation +from pptx.shapes.base import BaseShape +from pathlib import Path +from loguru import logger +from typing import Dict, List, Optional, Tuple, Any + +class AnchorEngine: + def __init__(self, presentation: Presentation = None): + self.prs = presentation + self.anchor_cache = {} + + def load_presentation(self, ppt_path: Path) -> Presentation: + logger.info(f"加载PPT模板用于锚点扫描: {ppt_path}") + self.prs = Presentation(str(ppt_path)) + self.scan_anchors() + return self.prs + + def scan_anchors(self) -> Dict[str, Tuple[int, BaseShape]]: + self.anchor_cache = {} + if not self.prs: + return self.anchor_cache + + for slide_idx, slide in enumerate(self.prs.slides): + for shape in slide.shapes: + shape_name = shape.name.strip() if shape.name else "" + + if shape_name and not shape_name.startswith("Picture") and not shape_name.startswith("Rectangle"): + self.anchor_cache[shape_name] = (slide_idx, shape) + logger.debug(f"发现锚点 [{shape_name}] 在第 {slide_idx+1} 页") + + if shape.has_text_frame: + for para in shape.text_frame.paragraphs: + text = para.text.strip() + if text.startswith("{{") and text.endswith("}}"): + anchor_name = text[2:-2].strip() + self.anchor_cache[anchor_name] = (slide_idx, shape) + logger.debug(f"发现文本模板锚点 [{anchor_name}] 在第 {slide_idx+1} 页") + + if shape.name and shape.name.lower().startswith(('chart_', 'table_', 'text_', 'img_')): + self.anchor_cache[shape.name] = (slide_idx, shape) + logger.debug(f"发现标准命名锚点 [{shape.name}] 在第 {slide_idx+1} 页") + + logger.info(f"锚点扫描完成,共发现 {len(self.anchor_cache)} 个可绑定锚点") + return self.anchor_cache + + def find_anchor(self, anchor_name: str) -> Optional[Tuple[int, BaseShape]]: + if anchor_name in self.anchor_cache: + return self.anchor_cache[anchor_name] + + for slide_idx, slide in enumerate(self.prs.slides): + for shape in slide.shapes: + if shape.name == anchor_name: + self.anchor_cache[anchor_name] = (slide_idx, shape) + return (slide_idx, shape) + + logger.warning(f"未找到锚点: {anchor_name}") + return None + + def get_all_anchor_names(self) -> List[str]: + return list(self.anchor_cache.keys()) + + def replace_text_anchor(self, anchor_name: str, new_text: str) -> bool: + result = self.find_anchor(anchor_name) + if not result: + return False + + slide_idx, shape = result + if shape.has_text_frame: + shape.text_frame.text = new_text + logger.success(f"文本锚点 [{anchor_name}] 已替换为: {new_text[:30]}...") + return True + return False + + def get_shape_alternative_text(self, shape: BaseShape) -> str: + try: + if hasattr(shape, 'alternative_text'): + return shape.alternative_text or "" + except: + pass + return "" diff --git a/ppt_manager_v2/core/conditional_renderer.py b/ppt_manager_v2/core/conditional_renderer.py new file mode 100644 index 0000000..6d4b558 --- /dev/null +++ b/ppt_manager_v2/core/conditional_renderer.py @@ -0,0 +1,137 @@ +from typing import Dict, Any, List, Optional, Callable +from pptx import Presentation +from loguru import logger +import ast +import operator + +class ConditionalRenderer: + def __init__(self, presentation: Presentation = None): + self.prs = presentation + self.context = {} + self._slides_to_remove = [] + + self.operators = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, + ast.Gt: operator.gt, + ast.GtE: operator.ge, + ast.Lt: operator.lt, + ast.LtE: operator.le, + ast.Eq: operator.eq, + ast.NotEq: operator.ne, + ast.And: lambda a, b: a and b, + ast.Or: lambda a, b: a or b, + } + + def set_context(self, context: Dict[str, Any]): + self.context = context + logger.info(f"条件渲染上下文已设置: {list(context.keys())}") + + def update_context(self, key: str, value: Any): + self.context[key] = value + + def evaluate_condition(self, condition_expr: str, context: Dict[str, Any] = None) -> bool: + eval_context = {**self.context, **(context or {})} + + try: + if "{" in condition_expr: + for key, value in eval_context.items(): + condition_expr = condition_expr.replace(f"{{{key}}}", str(value)) + + result = self._safe_eval(condition_expr, eval_context) + logger.info(f"条件 [{condition_expr}] 评估结果: {result}") + return bool(result) + + except Exception as e: + logger.warning(f"条件表达式评估失败 [{condition_expr}]: {e}") + return False + + def _safe_eval(self, expr: str, context: Dict) -> Any: + try: + node = ast.parse(expr, mode='eval') + return self._eval_ast(node.body, context) + except: + safe_dict = {k: v for k, v in context.items() if isinstance(k, str)} + safe_dict['__builtins__'] = {} + return eval(expr, safe_dict) + + def _eval_ast(self, node, context): + if isinstance(node, ast.Constant): + return node.value + elif isinstance(node, ast.Num): + return node.n + elif isinstance(node, ast.Str): + return node.s + elif isinstance(node, ast.Name): + return context.get(node.id, node.id) + elif isinstance(node, ast.BinOp): + op_type = type(node.op) + if op_type in self.operators: + return self.operators[op_type]( + self._eval_ast(node.left, context), + self._eval_ast(node.right, context) + ) + elif isinstance(node, ast.Compare): + left_val = self._eval_ast(node.left, context) + for op, comparator in zip(node.ops, node.comparators): + op_type = type(op) + if op_type in self.operators: + if not self.operators[op_type](left_val, self._eval_ast(comparator, context)): + return False + return True + elif isinstance(node, ast.BoolOp): + op_type = type(node.op) + if op_type == ast.And: + for value in node.values: + if not self._eval_ast(value, context): + return False + return True + elif op_type == ast.Or: + for value in node.values: + if self._eval_ast(value, context): + return True + return False + return None + + def process_slide_conditions(self, slide_configs: List[Dict], presentation: Presentation = None) -> Presentation: + prs = presentation or self.prs + if not prs: + logger.error("没有提供Presentation对象") + return prs + + logger.info(f"开始处理条件渲染,当前页数: {len(prs.slides)}") + + slides_to_keep = [] + for idx, slide_config in enumerate(slide_configs): + if 'condition' in slide_config: + condition = slide_config['condition'] + if not self.evaluate_condition(condition): + logger.info(f"跳过第 {idx+1} 页,条件不满足: {condition}") + continue + + if 'action' in slide_config and slide_config['action'] == 'insert_slide': + logger.info(f"执行插入幻灯片操作: {slide_config.get('template_source')}") + + if idx < len(prs.slides): + slides_to_keep.append(idx) + + logger.info(f"条件渲染完成,保留 {len(slides_to_keep)} / {len(slide_configs)} 页配置") + return prs + + def insert_new_slide(self, presentation: Presentation, + slide_layout_idx: int = 6, + position: int = None) -> int: + if position is None: + position = len(presentation.slides) + + slide_layout = presentation.slide_layouts[slide_layout_idx] if slide_layout_idx < len(presentation.slide_layouts) else presentation.slide_layouts[0] + + xml_slides = presentation.slides._sldIdLst + slides = list(xml_slides) + xml_slides.insert(position, slides[0] if slides else None) + + new_slide = presentation.slides.add_slide(slide_layout) + logger.info(f"已在位置 {position} 插入新页面") + return position diff --git a/ppt_manager_v2/core/native_chart.py b/ppt_manager_v2/core/native_chart.py new file mode 100644 index 0000000..eb69808 --- /dev/null +++ b/ppt_manager_v2/core/native_chart.py @@ -0,0 +1,148 @@ +from pptx import Presentation +from pptx.chart.data import CategoryChartData, XyChartData, BubbleChartData +from pptx.enum.chart import XL_CHART_TYPE +from pptx.util import Inches, Pt +from pathlib import Path +from loguru import logger +import pandas as pd +from typing import List, Dict, Optional, Union, Any + +class NativeChartManager: + def __init__(self, presentation: Presentation = None): + self.prs = presentation + + def set_presentation(self, presentation: Presentation): + self.prs = presentation + + def update_chart_by_anchor(self, anchor_name: str, + categories: List[str], + series_data: Dict[str, List[float]], + chart_type: str = None) -> bool: + if not self.prs: + logger.error("Presentation未设置") + return False + + for slide_idx, slide in enumerate(self.prs.slides): + for shape in slide.shapes: + if shape.name == anchor_name or (shape.has_chart and shape.name == anchor_name): + if shape.has_chart: + return self._update_existing_chart(shape.chart, categories, series_data) + else: + logger.info(f"锚点 {anchor_name} 不是图表,在原位创建新图表") + return self._create_chart_in_shape_position(slide, shape, categories, series_data, chart_type) + + logger.warning(f"未找到图表锚点: {anchor_name}") + return False + + def _update_existing_chart(self, chart, categories: List[str], series_data: Dict[str, List[float]]) -> bool: + try: + chart_data = CategoryChartData() + chart_data.categories = categories + + for series_name, values in series_data.items(): + chart_data.add_series(series_name, values) + + chart.replace_data(chart_data) + logger.success(f"原生图表数据源已更新,系列数: {len(series_data)}") + return True + except Exception as e: + logger.exception(f"更新图表数据源失败: {e}") + return False + + def _create_chart_in_shape_position(self, slide, placeholder_shape, + categories: List[str], + series_data: Dict[str, List[float]], + chart_type_str: str = None) -> bool: + try: + left = placeholder_shape.left + top = placeholder_shape.top + width = placeholder_shape.width + height = placeholder_shape.height + + chart_type_map = { + 'line': XL_CHART_TYPE.LINE, + 'line_markers': XL_CHART_TYPE.LINE_MARKERS, + 'bar': XL_CHART_TYPE.BAR_CLUSTERED, + 'bar_stacked': XL_CHART_TYPE.BAR_STACKED, + 'column': XL_CHART_TYPE.COLUMN_CLUSTERED, + 'column_stacked': XL_CHART_TYPE.COLUMN_STACKED, + 'pie': XL_CHART_TYPE.PIE, + 'doughnut': XL_CHART_TYPE.DOUGHNUT, + 'area': XL_CHART_TYPE.AREA, + } + + xl_chart_type = chart_type_map.get(chart_type_str or 'line', XL_CHART_TYPE.LINE) + + chart_data = CategoryChartData() + chart_data.categories = categories + for series_name, values in series_data.items(): + chart_data.add_series(series_name, values) + + slide.shapes.add_chart(xl_chart_type, left, top, width, height, chart_data) + logger.success(f"已在位置创建新原生图表: {chart_type_str}") + return True + + except Exception as e: + logger.exception(f"创建原生图表失败: {e}") + return False + + def update_table_by_anchor(self, anchor_name: str, data_frame: pd.DataFrame) -> bool: + if not self.prs: + logger.error("Presentation未设置") + return False + + for slide_idx, slide in enumerate(self.prs.slides): + for shape in slide.shapes: + if shape.has_table and (shape.name == anchor_name or f"table_{anchor_name}" == shape.name): + return self._update_table_content(shape.table, data_frame) + + logger.warning(f"未找到表格锚点: {anchor_name},尝试查找任意表格") + for slide_idx, slide in enumerate(self.prs.slides): + for shape in slide.shapes: + if shape.has_table: + return self._update_table_content(shape.table, data_frame) + return False + + def _update_table_content(self, table, data_frame: pd.DataFrame) -> bool: + try: + rows, cols = data_frame.shape + headers = list(data_frame.columns) + + for col_idx, header in enumerate(headers): + if col_idx < len(table.columns) and 0 < len(table.rows): + cell = table.cell(0, col_idx) + cell.text = str(header) + for para in cell.text_frame.paragraphs: + para.font.bold = True + + for row_idx in range(min(rows, len(table.rows) - 1)): + for col_idx in range(min(cols, len(table.columns))): + cell = table.cell(row_idx + 1, col_idx) + value = data_frame.iloc[row_idx, col_idx] + if isinstance(value, (int, float)): + cell.text = f"{value:.2f}" + else: + cell.text = str(value) + + logger.success(f"原生表格已更新,数据维度: {rows}x{cols}") + return True + except Exception as e: + logger.exception(f"更新表格内容失败: {e}") + return False + + def dataframe_to_series(self, df: pd.DataFrame, + category_col: str = None) -> tuple: + if category_col and category_col in df.columns: + categories = df[category_col].tolist() + else: + categories = df.index.tolist() + + series_data = {} + for col in df.columns: + if col != category_col: + try: + series_data[col] = df[col].tolist() + except: + pass + + return categories, series_data diff --git a/ppt_manager_v2/logs/run_20260529_105045.log b/ppt_manager_v2/logs/run_20260529_105045.log new file mode 100644 index 0000000..5208b8e --- /dev/null +++ b/ppt_manager_v2/logs/run_20260529_105045.log @@ -0,0 +1,62 @@ +2026-05-29 10:50:45.872 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 10:50:45.878 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 10:50:45.879 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 10:50:45.879 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock +2026-05-29 10:51:04.624 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 10:51:04.626 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 10:51:04.626 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 10:51:04.628 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock +2026-05-29 10:51:04.629 | INFO | orchestrator:_report_progress:54 - [LOAD] 创建新演示文稿(模板不存在: macro_analysis_template.pptx) +2026-05-29 10:51:04.757 | ERROR | __main__:handle_start_generation:82 - 生成过程出错 +Traceback (most recent call last): + + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\eventlet\greenthread.py", line 272, in main + result = function(*args, **kwargs) + │ │ └ {} + │ └ (, '04KQWf-21Cy15gXGAAAB', 'CF55wc3dPDdRhyZXAAAA', ['start_generation', ... + └ > + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\socketio\server.py", line 597, in _handle_event_internal + r = server._trigger_event(data[0], namespace, sid, *data[1:]) + │ │ │ │ │ └ ['start_generation', {'params': {'year': 2026, 'quarter': 'Q1'}}] + │ │ │ │ └ '04KQWf-21Cy15gXGAAAB' + │ │ │ └ '/' + │ │ └ ['start_generation', {'params': {'year': 2026, 'quarter': 'Q1'}}] + │ └ + └ + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\socketio\server.py", line 623, in _trigger_event + return handler(*args) + │ └ ('04KQWf-21Cy15gXGAAAB', {'params': {'year': 2026, 'quarter': 'Q1'}}) + └ + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask_socketio\__init__.py", line 306, in _handler + return self._handle_event(handler, message, real_ns, sid, + │ │ │ │ │ └ '04KQWf-21Cy15gXGAAAB' + │ │ │ │ └ '/' + │ │ │ └ 'start_generation' + │ │ └ + │ └ + └ + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask_socketio\__init__.py", line 858, in _handle_event + ret = handler(*args) + │ └ ({'params': {'year': 2026, 'quarter': 'Q1'}},) + └ + +> File "F:\ppt\ppt_manager_v2\web_socket_app.py", line 72, in handle_start_generation + output_path = orch.run_full_pipeline(params=params) + │ │ └ {'year': 2026, 'quarter': 'Q1'} + │ └ + └ + + File "F:\ppt\ppt_manager_v2\orchestrator.py", line 238, in run_full_pipeline + self.load_template(template_path) + │ │ └ None + │ └ + └ + + File "F:\ppt\ppt_manager_v2\orchestrator.py", line 76, in load_template + slide.shapes.title.text = "GDP趋势图" if slide.shapes.title else "" + │ │ │ └ + │ │ └ + │ └ + └ + +AttributeError: 'NoneType' object has no attribute 'text' diff --git a/ppt_manager_v2/logs/run_20260529_105104.log b/ppt_manager_v2/logs/run_20260529_105104.log new file mode 100644 index 0000000..ea680e7 --- /dev/null +++ b/ppt_manager_v2/logs/run_20260529_105104.log @@ -0,0 +1,58 @@ +2026-05-29 10:51:04.624 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 10:51:04.626 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 10:51:04.626 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 10:51:04.628 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock +2026-05-29 10:51:04.629 | INFO | orchestrator:_report_progress:54 - [LOAD] 创建新演示文稿(模板不存在: macro_analysis_template.pptx) +2026-05-29 10:51:04.757 | ERROR | __main__:handle_start_generation:82 - 生成过程出错 +Traceback (most recent call last): + + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\eventlet\greenthread.py", line 272, in main + result = function(*args, **kwargs) + │ │ └ {} + │ └ (, '04KQWf-21Cy15gXGAAAB', 'CF55wc3dPDdRhyZXAAAA', ['start_generation', ... + └ > + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\socketio\server.py", line 597, in _handle_event_internal + r = server._trigger_event(data[0], namespace, sid, *data[1:]) + │ │ │ │ │ └ ['start_generation', {'params': {'year': 2026, 'quarter': 'Q1'}}] + │ │ │ │ └ '04KQWf-21Cy15gXGAAAB' + │ │ │ └ '/' + │ │ └ ['start_generation', {'params': {'year': 2026, 'quarter': 'Q1'}}] + │ └ + └ + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\socketio\server.py", line 623, in _trigger_event + return handler(*args) + │ └ ('04KQWf-21Cy15gXGAAAB', {'params': {'year': 2026, 'quarter': 'Q1'}}) + └ + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask_socketio\__init__.py", line 306, in _handler + return self._handle_event(handler, message, real_ns, sid, + │ │ │ │ │ └ '04KQWf-21Cy15gXGAAAB' + │ │ │ │ └ '/' + │ │ │ └ 'start_generation' + │ │ └ + │ └ + └ + File "C:\Users\dxzq\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask_socketio\__init__.py", line 858, in _handle_event + ret = handler(*args) + │ └ ({'params': {'year': 2026, 'quarter': 'Q1'}},) + └ + +> File "F:\ppt\ppt_manager_v2\web_socket_app.py", line 72, in handle_start_generation + output_path = orch.run_full_pipeline(params=params) + │ │ └ {'year': 2026, 'quarter': 'Q1'} + │ └ + └ + + File "F:\ppt\ppt_manager_v2\orchestrator.py", line 238, in run_full_pipeline + self.load_template(template_path) + │ │ └ None + │ └ + └ + + File "F:\ppt\ppt_manager_v2\orchestrator.py", line 76, in load_template + slide.shapes.title.text = "GDP趋势图" if slide.shapes.title else "" + │ │ │ └ + │ │ └ + │ └ + └ + +AttributeError: 'NoneType' object has no attribute 'text' diff --git a/ppt_manager_v2/logs/run_20260529_105156.log b/ppt_manager_v2/logs/run_20260529_105156.log new file mode 100644 index 0000000..a81cab4 --- /dev/null +++ b/ppt_manager_v2/logs/run_20260529_105156.log @@ -0,0 +1,4 @@ +2026-05-29 10:51:56.363 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 10:51:56.365 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 10:51:56.366 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 10:51:56.366 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock diff --git a/ppt_manager_v2/logs/run_20260529_105202.log b/ppt_manager_v2/logs/run_20260529_105202.log new file mode 100644 index 0000000..289e841 --- /dev/null +++ b/ppt_manager_v2/logs/run_20260529_105202.log @@ -0,0 +1,28 @@ +2026-05-29 10:52:02.884 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 10:52:02.886 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 10:52:02.886 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 10:52:02.887 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock +2026-05-29 10:52:02.887 | INFO | orchestrator:_report_progress:54 - [LOAD] 创建新演示文稿(模板不存在: macro_analysis_template.pptx) +2026-05-29 10:52:02.944 | INFO | core.anchor_engine:scan_anchors:43 - 锚点扫描完成,共发现 4 个可绑定锚点 +2026-05-29 10:52:02.945 | INFO | orchestrator:_report_progress:54 - [LOAD] 发现 4 个可绑定锚点 +2026-05-29 10:52:02.945 | INFO | orchestrator:_report_progress:54 - [PLUGINS] 开始执行数据插件... +2026-05-29 10:52:02.946 | INFO | orchestrator:_report_progress:54 - [PLUGINS] 执行插件: CPI/PPI通胀图表生成器 +2026-05-29 10:52:02.964 | INFO | plugins.generators.cpi_generator:fetch_data:30 - CPI/PPI数据获取成功 +2026-05-29 10:52:02.967 | SUCCESS | orchestrator:run_plugins:123 - 插件 [cpi_chart] 执行成功 +2026-05-29 10:52:02.969 | INFO | orchestrator:_report_progress:54 - [PLUGINS] 执行插件: GDP趋势图表生成器 +2026-05-29 10:52:02.970 | INFO | plugins.generators.gdp_generator:fetch_data:33 - GDP数据获取成功,共 4 条记录 +2026-05-29 10:52:02.970 | SUCCESS | orchestrator:run_plugins:123 - 插件 [gdp_chart] 执行成功 +2026-05-29 10:52:02.971 | INFO | orchestrator:_report_progress:54 - [CHARTS] 开始更新原生图表... +2026-05-29 10:52:02.973 | INFO | core.native_chart:update_chart_by_anchor:31 - 锚点 chart_gdp 不是图表,在原位创建新图表 +2026-05-29 10:52:02.981 | SUCCESS | core.native_chart:_create_chart_in_shape_position:82 - 已在位置创建新原生图表: line +2026-05-29 10:52:02.983 | INFO | core.native_chart:update_chart_by_anchor:31 - 锚点 chart_cpi 不是图表,在原位创建新图表 +2026-05-29 10:52:02.991 | SUCCESS | core.native_chart:_create_chart_in_shape_position:82 - 已在位置创建新原生图表: line_markers +2026-05-29 10:52:02.992 | INFO | orchestrator:_report_progress:54 - [CHARTS] 完成 2 个原生图表更新 +2026-05-29 10:52:02.993 | INFO | orchestrator:_report_progress:54 - [AI] LLM正在生成分析摘要... +2026-05-29 10:52:02.997 | WARNING | core.anchor_engine:find_anchor:56 - 未找到锚点: text_llm_summary +2026-05-29 10:52:02.997 | SUCCESS | orchestrator:ai_generate_summary:189 - LLM分析完成: 本月宏观经济洞察:GDP增长N/A%,经济扩张动能平稳。CPI同比N/A%,通胀水平温和,为货币政策... +2026-05-29 10:52:02.998 | INFO | orchestrator:_report_progress:54 - [SAVE] 正在保存最终PPT... +2026-05-29 10:52:03.021 | INFO | orchestrator:_report_progress:54 - [DONE] PPT已保存: report_20260529_105202.pptx +2026-05-29 10:52:03.021 | SUCCESS | orchestrator:save:235 - ================================================== +2026-05-29 10:52:03.022 | SUCCESS | orchestrator:save:236 - 生成完成: F:\ppt\ppt_manager_v2\..\output\report_20260529_105202.pptx +2026-05-29 10:52:03.022 | SUCCESS | orchestrator:save:237 - ================================================== diff --git a/ppt_manager_v2/logs/run_20260529_105938.log b/ppt_manager_v2/logs/run_20260529_105938.log new file mode 100644 index 0000000..7fb609e --- /dev/null +++ b/ppt_manager_v2/logs/run_20260529_105938.log @@ -0,0 +1,38 @@ +2026-05-29 10:59:38.903 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 10:59:38.904 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 10:59:38.905 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 10:59:38.906 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock +2026-05-29 11:03:21.696 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 11:03:21.698 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 11:03:21.699 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 11:03:21.700 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock +2026-05-29 11:03:21.701 | INFO | orchestrator:_report_progress:54 - [LOAD] 创建新演示文稿(模板不存在: macro_analysis_template.pptx) +2026-05-29 11:03:21.772 | INFO | core.anchor_engine:scan_anchors:43 - 锚点扫描完成,共发现 4 个可绑定锚点 +2026-05-29 11:03:21.774 | INFO | orchestrator:_report_progress:54 - [LOAD] 发现 4 个可绑定锚点 +2026-05-29 11:03:21.777 | INFO | orchestrator:_report_progress:54 - [PLUGINS] 开始执行数据插件... +2026-05-29 11:03:21.779 | INFO | orchestrator:_report_progress:54 - [PLUGINS] 执行插件: CPI/PPI通胀图表生成器 +2026-05-29 11:03:21.781 | INFO | plugins.generators.cpi_generator:fetch_data:30 - CPI/PPI数据获取成功 +2026-05-29 11:03:21.783 | SUCCESS | orchestrator:run_plugins:123 - 插件 [cpi_chart] 执行成功 +2026-05-29 11:03:21.786 | INFO | orchestrator:_report_progress:54 - [PLUGINS] 执行插件: GDP趋势图表生成器 +2026-05-29 11:03:21.788 | INFO | plugins.generators.gdp_generator:fetch_data:33 - GDP数据获取成功,共 4 条记录 +2026-05-29 11:03:21.790 | SUCCESS | orchestrator:run_plugins:123 - 插件 [gdp_chart] 执行成功 +2026-05-29 11:03:21.790 | INFO | orchestrator:_report_progress:54 - [CHARTS] 开始更新原生图表... +2026-05-29 11:03:21.797 | INFO | core.native_chart:update_chart_by_anchor:31 - 锚点 chart_gdp 不是图表,在原位创建新图表 +2026-05-29 11:03:21.805 | SUCCESS | core.native_chart:_create_chart_in_shape_position:82 - 已在位置创建新原生图表: line +2026-05-29 11:03:21.810 | INFO | core.native_chart:update_chart_by_anchor:31 - 锚点 chart_cpi 不是图表,在原位创建新图表 +2026-05-29 11:03:21.818 | SUCCESS | core.native_chart:_create_chart_in_shape_position:82 - 已在位置创建新原生图表: line_markers +2026-05-29 11:03:21.819 | INFO | orchestrator:_report_progress:54 - [CHARTS] 完成 2 个原生图表更新 +2026-05-29 11:03:21.820 | INFO | orchestrator:_report_progress:54 - [AI] LLM正在生成分析摘要... +2026-05-29 11:03:21.827 | WARNING | core.anchor_engine:find_anchor:56 - 未找到锚点: text_llm_summary +2026-05-29 11:03:21.827 | SUCCESS | orchestrator:ai_generate_summary:189 - LLM分析完成: 本月宏观经济洞察:GDP增长N/A%,经济扩张动能平稳。CPI同比N/A%,通胀水平温和,为货币政策... +2026-05-29 11:03:21.829 | INFO | orchestrator:_report_progress:54 - [CONDITIONS] 处理条件渲染规则... +2026-05-29 11:03:21.831 | INFO | core.conditional_renderer:set_context:30 - 条件渲染上下文已设置: ['CPI同比', 'PPI同比', 'GDP同比', 'gdp_growth', 'GDP环比'] +2026-05-29 11:03:21.832 | WARNING | core.conditional_renderer:evaluate_condition:48 - 条件表达式评估失败 [unemployment_rate > 5.1]: name 'unemployment_rate' is not defined +2026-05-29 11:03:21.833 | INFO | orchestrator:process_conditions:218 - 条件规则: unemployment_rate > 5.1 -> 不满足 +2026-05-29 11:03:21.833 | INFO | core.conditional_renderer:evaluate_condition:44 - 条件 [1.10093365492678 < 5.0] 评估结果: True +2026-05-29 11:03:21.834 | INFO | orchestrator:process_conditions:218 - 条件规则: {gdp_growth} < 5.0 -> 满足 +2026-05-29 11:03:21.835 | INFO | orchestrator:_report_progress:54 - [SAVE] 正在保存最终PPT... +2026-05-29 11:03:21.872 | INFO | orchestrator:_report_progress:54 - [DONE] PPT已保存: report_20260529_110321.pptx +2026-05-29 11:03:21.873 | SUCCESS | orchestrator:save:235 - ================================================== +2026-05-29 11:03:21.873 | SUCCESS | orchestrator:save:236 - 生成完成: F:\ppt\ppt_manager_v2\..\output\report_20260529_110321.pptx +2026-05-29 11:03:21.874 | SUCCESS | orchestrator:save:237 - ================================================== diff --git a/ppt_manager_v2/logs/run_20260529_110321.log b/ppt_manager_v2/logs/run_20260529_110321.log new file mode 100644 index 0000000..db097b8 --- /dev/null +++ b/ppt_manager_v2/logs/run_20260529_110321.log @@ -0,0 +1,34 @@ +2026-05-29 11:03:21.696 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 11:03:21.698 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 11:03:21.699 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 11:03:21.700 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock +2026-05-29 11:03:21.701 | INFO | orchestrator:_report_progress:54 - [LOAD] 创建新演示文稿(模板不存在: macro_analysis_template.pptx) +2026-05-29 11:03:21.772 | INFO | core.anchor_engine:scan_anchors:43 - 锚点扫描完成,共发现 4 个可绑定锚点 +2026-05-29 11:03:21.774 | INFO | orchestrator:_report_progress:54 - [LOAD] 发现 4 个可绑定锚点 +2026-05-29 11:03:21.777 | INFO | orchestrator:_report_progress:54 - [PLUGINS] 开始执行数据插件... +2026-05-29 11:03:21.779 | INFO | orchestrator:_report_progress:54 - [PLUGINS] 执行插件: CPI/PPI通胀图表生成器 +2026-05-29 11:03:21.781 | INFO | plugins.generators.cpi_generator:fetch_data:30 - CPI/PPI数据获取成功 +2026-05-29 11:03:21.783 | SUCCESS | orchestrator:run_plugins:123 - 插件 [cpi_chart] 执行成功 +2026-05-29 11:03:21.786 | INFO | orchestrator:_report_progress:54 - [PLUGINS] 执行插件: GDP趋势图表生成器 +2026-05-29 11:03:21.788 | INFO | plugins.generators.gdp_generator:fetch_data:33 - GDP数据获取成功,共 4 条记录 +2026-05-29 11:03:21.790 | SUCCESS | orchestrator:run_plugins:123 - 插件 [gdp_chart] 执行成功 +2026-05-29 11:03:21.790 | INFO | orchestrator:_report_progress:54 - [CHARTS] 开始更新原生图表... +2026-05-29 11:03:21.797 | INFO | core.native_chart:update_chart_by_anchor:31 - 锚点 chart_gdp 不是图表,在原位创建新图表 +2026-05-29 11:03:21.805 | SUCCESS | core.native_chart:_create_chart_in_shape_position:82 - 已在位置创建新原生图表: line +2026-05-29 11:03:21.810 | INFO | core.native_chart:update_chart_by_anchor:31 - 锚点 chart_cpi 不是图表,在原位创建新图表 +2026-05-29 11:03:21.818 | SUCCESS | core.native_chart:_create_chart_in_shape_position:82 - 已在位置创建新原生图表: line_markers +2026-05-29 11:03:21.819 | INFO | orchestrator:_report_progress:54 - [CHARTS] 完成 2 个原生图表更新 +2026-05-29 11:03:21.820 | INFO | orchestrator:_report_progress:54 - [AI] LLM正在生成分析摘要... +2026-05-29 11:03:21.827 | WARNING | core.anchor_engine:find_anchor:56 - 未找到锚点: text_llm_summary +2026-05-29 11:03:21.827 | SUCCESS | orchestrator:ai_generate_summary:189 - LLM分析完成: 本月宏观经济洞察:GDP增长N/A%,经济扩张动能平稳。CPI同比N/A%,通胀水平温和,为货币政策... +2026-05-29 11:03:21.829 | INFO | orchestrator:_report_progress:54 - [CONDITIONS] 处理条件渲染规则... +2026-05-29 11:03:21.831 | INFO | core.conditional_renderer:set_context:30 - 条件渲染上下文已设置: ['CPI同比', 'PPI同比', 'GDP同比', 'gdp_growth', 'GDP环比'] +2026-05-29 11:03:21.832 | WARNING | core.conditional_renderer:evaluate_condition:48 - 条件表达式评估失败 [unemployment_rate > 5.1]: name 'unemployment_rate' is not defined +2026-05-29 11:03:21.833 | INFO | orchestrator:process_conditions:218 - 条件规则: unemployment_rate > 5.1 -> 不满足 +2026-05-29 11:03:21.833 | INFO | core.conditional_renderer:evaluate_condition:44 - 条件 [1.10093365492678 < 5.0] 评估结果: True +2026-05-29 11:03:21.834 | INFO | orchestrator:process_conditions:218 - 条件规则: {gdp_growth} < 5.0 -> 满足 +2026-05-29 11:03:21.835 | INFO | orchestrator:_report_progress:54 - [SAVE] 正在保存最终PPT... +2026-05-29 11:03:21.872 | INFO | orchestrator:_report_progress:54 - [DONE] PPT已保存: report_20260529_110321.pptx +2026-05-29 11:03:21.873 | SUCCESS | orchestrator:save:235 - ================================================== +2026-05-29 11:03:21.873 | SUCCESS | orchestrator:save:236 - 生成完成: F:\ppt\ppt_manager_v2\..\output\report_20260529_110321.pptx +2026-05-29 11:03:21.874 | SUCCESS | orchestrator:save:237 - ================================================== diff --git a/ppt_manager_v2/logs/run_20260529_110441.log b/ppt_manager_v2/logs/run_20260529_110441.log new file mode 100644 index 0000000..012e1bb --- /dev/null +++ b/ppt_manager_v2/logs/run_20260529_110441.log @@ -0,0 +1,4 @@ +2026-05-29 11:04:41.025 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 11:04:41.027 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 11:04:41.028 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 11:04:41.029 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock diff --git a/ppt_manager_v2/logs/run_20260529_111029.log b/ppt_manager_v2/logs/run_20260529_111029.log new file mode 100644 index 0000000..fbca313 --- /dev/null +++ b/ppt_manager_v2/logs/run_20260529_111029.log @@ -0,0 +1,4 @@ +2026-05-29 11:10:29.616 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [cpi_chart]: CPI/PPI通胀图表生成器 +2026-05-29 11:10:29.618 | SUCCESS | plugins.base_generator:discover_plugins:84 - 加载插件 [gdp_chart]: GDP趋势图表生成器 +2026-05-29 11:10:29.618 | INFO | plugins.base_generator:discover_plugins:90 - 插件扫描完成,共加载 2 个生成器 +2026-05-29 11:10:29.619 | INFO | ai.llm_analyst:__init__:9 - LLM分析师初始化,提供者: mock diff --git a/ppt_manager_v2/orchestrator.py b/ppt_manager_v2/orchestrator.py new file mode 100644 index 0000000..35cdcb5 --- /dev/null +++ b/ppt_manager_v2/orchestrator.py @@ -0,0 +1,251 @@ +import sys +from pathlib import Path +from typing import Dict, Any, List, Optional +from loguru import logger +from datetime import datetime +import yaml + +sys.path.insert(0, str(Path(__file__).parent)) + +from core.anchor_engine import AnchorEngine +from core.native_chart import NativeChartManager +from core.conditional_renderer import ConditionalRenderer +from plugins.base_generator import GeneratorPluginManager +from ai.llm_analyst import LLMAnalyst + +class Orchestrator: + def __init__(self, config_path: str = None): + self.base_dir = Path(__file__).parent + + if config_path: + self.config_path = Path(config_path) + else: + self.config_path = self.base_dir / "config" / "project_config_v2.yaml" + + self.config = self._load_config() + self.prs = None + self.results: Dict[str, Any] = {} + self.progress_callback = None + + log_file = self.base_dir / "logs" / f"run_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" + log_file.parent.mkdir(parents=True, exist_ok=True) + logger.add(str(log_file), rotation="10 MB", level="INFO", encoding="utf-8") + + self.plugin_manager = GeneratorPluginManager(self.base_dir / "plugins" / "generators") + self.plugin_manager.discover_plugins() + + self.anchor_engine = AnchorEngine() + self.chart_manager = NativeChartManager() + self.conditional_renderer = ConditionalRenderer() + + ai_config = self.config.get('ai', {}) + self.llm = LLMAnalyst(ai_config) + + def _load_config(self) -> Dict: + if self.config_path.exists(): + with open(self.config_path, 'r', encoding='utf-8') as f: + return yaml.safe_load(f) or {} + return {} + + def set_progress_callback(self, callback): + self.progress_callback = callback + + def _report_progress(self, step: str, message: str, percent: int = 0): + logger.info(f"[{step}] {message}") + if self.progress_callback: + self.progress_callback({ + 'step': step, + 'message': message, + 'percent': percent, + 'timestamp': datetime.now().isoformat() + }) + + def load_template(self, template_path: str = None) -> bool: + template_file = self.base_dir / "template_ppt" / (template_path or self.config.get('template', '')) + + if not template_file.exists(): + self._report_progress("LOAD", f"创建新演示文稿(模板不存在: {template_file.name})", 5) + from pptx import Presentation + self.prs = Presentation() + + for i in range(8): + layout_idx = 5 if i == 0 else 1 + if layout_idx < len(self.prs.slide_layouts): + slide = self.prs.slides.add_slide(self.prs.slide_layouts[layout_idx]) + if slide.shapes.title: + slide.shapes.title.text = f"示例页 {i+1}" + if i == 3: + if slide.shapes.title: + slide.shapes.title.text = "GDP趋势图" + for shp in slide.shapes: + if shp != slide.shapes.title and hasattr(shp, 'name'): + shp.name = "chart_gdp" + break + if i == 4: + if slide.shapes.title: + slide.shapes.title.text = "CPI/PPI走势图" + for shp in slide.shapes: + if shp != slide.shapes.title and hasattr(shp, 'name'): + shp.name = "chart_cpi" + break + logger.debug(f"创建示例页 {i+1}") + else: + self._report_progress("LOAD", f"加载模板: {template_file.name}", 5) + self.prs = self.anchor_engine.load_presentation(template_file) + + self.anchor_engine.prs = self.prs + self.chart_manager.set_presentation(self.prs) + self.conditional_renderer.prs = self.prs + + anchors_found = self.anchor_engine.scan_anchors() + self._report_progress("LOAD", f"发现 {len(anchors_found)} 个可绑定锚点", 10) + return True + + def run_plugins(self, params: Dict[str, Any] = None) -> Dict: + self._report_progress("PLUGINS", "开始执行数据插件...", 15) + + run_params = {**self.config.get('params', {}), **(params or {})} + plugin_results = {} + + total = len(self.plugin_manager.plugins) + for idx, (pid, plugin_class) in enumerate(self.plugin_manager.plugins.items()): + self._report_progress("PLUGINS", f"执行插件: {plugin_class.generator_name}", 20 + int(idx/total*30)) + + try: + plugin = plugin_class(run_params) + if plugin.fetch_data(run_params): + render_result = plugin.render() + plugin_results[pid] = { + 'success': True, + 'data': plugin.get_data(), + 'render': render_result + } + logger.success(f"插件 [{pid}] 执行成功") + else: + plugin_results[pid] = {'success': False} + except Exception as e: + logger.exception(f"插件 [{pid}] 执行失败: {e}") + plugin_results[pid] = {'success': False, 'error': str(e)} + + self.results['plugins'] = plugin_results + return plugin_results + + def update_native_charts(self) -> int: + self._report_progress("CHARTS", "开始更新原生图表...", 55) + + updates_count = 0 + anchors_config = self.config.get('anchors', []) + + for anchor_cfg in anchors_config: + anchor_type = anchor_cfg.get('type', '') + anchor_name = anchor_cfg.get('name', '') + + if 'native_chart' in anchor_type: + plugin_id = anchor_cfg.get('plugin') + chart_type = anchor_cfg.get('chart_type', 'line') + + if plugin_id and plugin_id in self.results.get('plugins', {}): + plugin_result = self.results['plugins'][plugin_id] + if plugin_result.get('success'): + render = plugin_result['render'] + + success = self.chart_manager.update_chart_by_anchor( + anchor_name=render.get('anchor', anchor_name), + categories=render.get('categories', []), + series_data=render.get('series', {}), + chart_type=chart_type + ) + if success: + updates_count += 1 + + self._report_progress("CHARTS", f"完成 {updates_count} 个原生图表更新", 70) + return updates_count + + def ai_generate_summary(self) -> str: + self._report_progress("AI", "LLM正在生成分析摘要...", 75) + + context = {} + for pid, presult in self.results.get('plugins', {}).items(): + if presult.get('success') and 'data' in presult: + df = presult['data'] + if hasattr(df, 'iloc'): + for col in df.columns: + try: + context[f"{pid}_{col}"] = round(float(df[col].iloc[-1]), 2) + except: + pass + + try: + summary = self.llm.generate_analysis(context, max_words=200) + self.results['llm_summary'] = summary + + for anchor_cfg in self.config.get('anchors', []): + if anchor_cfg.get('type') == 'text_replace' and 'summary' in anchor_cfg.get('name', ''): + self.anchor_engine.replace_text_anchor(anchor_cfg['name'], summary) + if anchor_cfg.get('type') == 'ai_text_generate': + anchor_name = anchor_cfg.get('name', 'text_llm_summary') + self.anchor_engine.replace_text_anchor(anchor_name, summary) + + logger.success(f"LLM分析完成: {summary[:50]}...") + return summary + except Exception as e: + logger.warning(f"LLM分析失败: {e}") + return "" + + def process_conditions(self): + self._report_progress("CONDITIONS", "处理条件渲染规则...", 80) + + context = {} + for pid, presult in self.results.get('plugins', {}).items(): + if presult.get('success') and 'data' in presult: + df = presult['data'] + if hasattr(df, 'iloc'): + for col in df.columns: + try: + val = float(df[col].iloc[-1]) + context[col] = val + if 'GDP' in col: context['gdp_growth'] = val + if 'unemploy' in col.lower(): context['unemployment_rate'] = val + except: + pass + + self.conditional_renderer.set_context(context) + + slide_conditions = self.config.get('slide_conditions', []) + for cond_cfg in slide_conditions: + if 'condition' in cond_cfg: + cond_result = self.conditional_renderer.evaluate_condition(cond_cfg['condition']) + logger.info(f"条件规则: {cond_cfg['condition']} -> {'满足' if cond_result else '不满足'}") + + return True + + def save(self, output_name: str = None) -> str: + self._report_progress("SAVE", "正在保存最终PPT...", 95) + + output_dir = self.base_dir / self.config.get('output_dir', 'output') + output_dir.mkdir(parents=True, exist_ok=True) + + if not output_name: + output_name = f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pptx" + + output_path = output_dir / output_name + self.prs.save(str(output_path)) + + self._report_progress("DONE", f"PPT已保存: {output_name}", 100) + logger.success(f"=" * 50) + logger.success(f"生成完成: {output_path}") + logger.success(f"=" * 50) + + return str(output_path) + + def run_full_pipeline(self, template_path: str = None, params: Dict = None) -> str: + self.load_template(template_path) + self.run_plugins(params) + self.update_native_charts() + self.ai_generate_summary() + self.process_conditions() + return self.save() + +if __name__ == "__main__": + orch = Orchestrator() + orch.run_full_pipeline() diff --git a/ppt_manager_v2/plugins/__pycache__/base_generator.cpython-311.pyc b/ppt_manager_v2/plugins/__pycache__/base_generator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bba32ca56f17623525131895db53cd03353dd2a GIT binary patch literal 7109 zcma)BYit`=cD}&JRzqA z`C68ba5{G}nUJ9|b4%x5PD-+F|JJ;m%x2=L$q1u!uf*kRklIq&*;zqMl;@_2tz3B0 z&EQ@I5@mQ2wfq_EIm!y0VC5|%&;?31T4KwkCR*ZX$r`oOk_}5q5_JepD7$7XZRN=c zfq9x}UmtJZ+d5<69k;Dfx8ULTZRedr^9;wkZd>ocj2~8Kje22(bKm*gx4DMCzi(#G zZ7zC%Z@x{U2cI&Z7kc=42JYg3?zs?`gb5)dh;cb9PU`O2No==({(|V;C^(^d|ZsDrI?hsCZyxK%_t_S_YYe|F2Uau5y-nF&&)GP zk|!}J89}b{5g~Kck$Ey#t^YZ54{rBE_!$94SMxm1#Z_8pslxxj>9Ai5vXG;?sfbNH z2)#voJ~}s@%pz(K$&-&Cmf7t}Gn>-Kp{6tMl_R{$lBP zCJqc4yME^Cd@47a%t%+K;b>#kkM+;r((SQWGLw{Jv4s}Xx%XG<;rSBj1t3dAYiTc} zik4+=#r2@2{qCFV7uCM=O3P@eWmIh$#cJ)DHb|UW=*Tjs+Jg#>Phc?7ZK5FO#Ej|i z@tEl0<2yl7Fh%Y&llO@jgmQUH;vpREoDt;2HM;MGpn0LSmS>>nlO`ZbLEj)ZiV{jr|>D zfUC$Hq(&&#ucuN;%&+Mpd1K@%%(rM+Wb3GF_X;9MmTz;ltpzXeSR_eY~4mak7=m%=-s|yH64+gc8Si!Bu=(^Gnh(QZDGLM`^hBR}VtD(+i(4mNES2Dk)tGO6icmg_Fk%a|iQTy7IzF?*$r;Su z@CqyST{r-R0&LRjUs+J>ZF|3uuZVKpFl5Z%2ac%n9ptMVRnIBNQEOJXeRdLW7UhoE z3`FOs>~YpakKQEZ5(z<);C`#!jGeXOsK)I%aT}r_#eo)z)0y?AndWLaas!m?|wr&)S({g)mlTlE}PG}OAO+0(lanlHh`iGc`Fvvov~Ot%jZ&*xSviiHT#Ci$Rz;L{B3>V5Y!TnFh{r01kZ?diDrd7Pu@lS3 zkeozv3dva{VI(LJC^L(0%ppO!Bwd9|SgQNeg6&H$7rGR#9b$=VhkY(h7RD5=17eBm zfE_P=v*1^_5X2G}()@>(CYGlZ?hwQhcj#ev&(f8`*;RIRYSn&sRN;Ccmbji>jb%V+^oW zhOTm}Q^l^;*VdYAJ!#I|owEjt{QAHKyMFG246Y-v8K6lcaWmFKIKE75O;+dX#iGBM zC8?@MjCV-N5 zp-FJmLoMj#6kIjvHDlxL2(F;sv$uC$KLlZ1w3&h&m)xtSz=X9%u@j4YnVY8NbDN@M|t34Fw2=m2b}*Xi_0EgaeZAgb`=IUIEc z2nfE@0{|}ETBt_}O@7q<(arn)Kkrw2|M;`MQtu?i8(Jt_hdT^rK=ok40LTpVAg{t9 z0q6iko+673O_@bFf|_&`12R6_a)6fVv6>Hc3N?K7;Hb_DBlx!);#Va zDKKwbS>*EE94ZGIigo*_)B(oe%p*l+rNcpZD~v=5O9Sq6(VDl+A&#J-i1>F;$XiXj zYM9x)mqnXtKL4J{pdxQGuWhj@Z<|A$LqidfZhaHEo`}PF#~kV>8vbe^@2KbN z`nD11Vd7AHN;L4c`hM?$%?;&_N-NxH!~FoR%oUiG!(Bukt@I+K8x-iG)AWUKrIokm zIhfaxckVaa>tHG@I^S}_oD)Xw26KblWM3yY7~Tm&tcq_;v!l+Yfp`63?;2L%e!qeL z;nx2^nGo?zinx#*a>AoKKlqqmbVz z$zZxlHG>8>%FgO;Ajx`^p%OHen}pvBbd~(_O+%7-BT* z-1WF9i6`)L&ZGoOkmH$zpmXxAc>!yr+;k!pmm~v<;l{-QBox{@F5OHB)Onyggld*c zW@fSxm!Y@Dab`#LSn50w5-Mp>h_~p%bys=4bhn(bV53UBf^)Fh={cPdB+$Cl+Chs4 zf?@RdDhe}J=7WXfPtlBx9Z?!cuK}ZhhO{4CcH*CAKde;ZLzh=|g#b`Fjy>?V6y&@4 zlD}K^cPsAhhi!+|w#ag`<~_Xa?cVZsm%Kfyw+GFoXCAl@u1qSSOB>$-0dMlmO_JSXs~1-Qtd;EeduB9vF+C0t=8W4p;GG^ zwH0ivfMbGrOzHB3=51NYZ+m;Tygfy!KRxz-etq*E+V_&TX}w+YFT2M%A{_ zf~{=B9pCaDU$>Qfr&Ql5#dm6_^H_1_uiHOp-|igP>KrI_4yv7l04EP5z(_>;!GD{P@@ZdyoVU z<3UWoa%%K}uSNBRmCj2WH-JF=tkXzKzH!wzu9zcKehp!l#R)KDBkcw*dSGu}aVdd6 z|5akM`;)%X(1bcP0YxL0?60c!R~7rK54;BpXYY=bykXTFR_tL3QS86GFg7y&yyah? zcTV(Jf6W}4=(PU2(+YXR07Rvop!x+e7!WB@lqEt2{c50Pu%3qNL~OB`ydrs~29@&- zUWqAE4{dAcjA!!9zBtM7tL4yf^yuw>LTS6aQ5AHCWE>7hSVN*Ah@wIaDsT<8{Ybd} z>I3zlrCOEE3*Aq7ou*!Izy#7oAn-I@t*d9(Mikd^h?@T-o^a8v`cJOUl>CFLe^7A` z;`KButrs?41_H5UA5-mPihb-U{su&X^o$W#zDs5S#nzv4v1;){b2XSTD0rIARy|G3 z{+=fADzP=Q0rGgaG(f(JdLeJC)bq@mwGk_=4Cc|l3;s^4Zb|Y~BsQ4aktX9xs4Ta| z=jXxswx3yE_9~%E5nT%4cLK*Sn@CBGj-(W*soI@M3q4P{w1$2O*oTw`0@rrHzib05 zea*cc?A;3XmV$k1un$Cq!%NBAmzT#1e$9RG&XsqrXf1724)khAj;{Ts7V230BdxPr z?K}fsix3#cKpqpD$NLo$C{{=_@wF^_>UHWrUXs%7!` z4F?694~B}H!5m@~5%(ny>=(A2d{AZaA8 zFksPBQ+^#fKz>8s9Q(|_dHG}Nc?c-qeqD*qs^7lWXzpX*UANQP0^WzJe}GD5sM{q^ zA;xLpJ|aGyjZY^;d8=D5rCFyIH-OA2F;vG%K4(<{s<0|&X6JjS}O_GzH|{XglVYJC6z literal 0 HcmV?d00001 diff --git a/ppt_manager_v2/plugins/base_generator.py b/ppt_manager_v2/plugins/base_generator.py new file mode 100644 index 0000000..f496d7a --- /dev/null +++ b/ppt_manager_v2/plugins/base_generator.py @@ -0,0 +1,109 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any, List, Optional +import pandas as pd +from pathlib import Path +from loguru import logger + +class BaseGenerator(ABC): + generator_id: str = None + generator_name: str = None + description: str = None + version: str = "1.0.0" + + params_schema: Dict[str, Any] = {} + + def __init__(self, params: Dict[str, Any] = None): + self.params = params or {} + self.logger = logger.bind(generator=self.generator_id) + self._data = None + self._chart_data = None + + @abstractmethod + def fetch_data(self, params: Dict[str, Any] = None) -> bool: + pass + + @abstractmethod + def render(self) -> Dict[str, Any]: + pass + + def validate_params(self, params: Dict[str, Any]) -> bool: + for param_name, param_config in self.params_schema.items(): + if param_config.get('required', False) and param_name not in params: + self.logger.warning(f"缺少必需参数: {param_name}") + return True + + def get_data(self): + return self._data + + def get_result(self) -> Dict[str, Any]: + return { + 'generator_id': self.generator_id, + 'generator_name': self.generator_name, + 'data': self._data, + 'chart_data': self._chart_data, + 'success': True + } + +class GeneratorPluginManager: + def __init__(self, plugins_dir: Path = None): + self.plugins: Dict[str, BaseGenerator] = {} + self.plugins_dir = plugins_dir or Path(__file__).parent / 'generators' + self._discovered = False + + def discover_plugins(self) -> int: + import sys + import importlib.util + + if self._discovered: + return len(self.plugins) + + generators_dir = self.plugins_dir + if not generators_dir.exists(): + logger.warning(f"插件目录不存在: {generators_dir}") + return 0 + + sys.path.insert(0, str(generators_dir.parent)) + + for py_file in generators_dir.glob("*.py"): + if py_file.name.startswith("_"): + continue + + try: + module_name = f"plugins.generators.{py_file.stem}" + spec = importlib.util.spec_from_file_location(module_name, str(py_file)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + for name, obj in vars(module).items(): + if (isinstance(obj, type) and + issubclass(obj, BaseGenerator) and + obj != BaseGenerator and + obj.generator_id is not None): + + self.plugins[obj.generator_id] = obj + logger.success(f"加载插件 [{obj.generator_id}]: {obj.generator_name}") + + except Exception as e: + logger.exception(f"加载插件失败 {py_file}: {e}") + + self._discovered = True + logger.info(f"插件扫描完成,共加载 {len(self.plugins)} 个生成器") + return len(self.plugins) + + def get_generator(self, generator_id: str, params: Dict[str, Any] = None) -> Optional[BaseGenerator]: + if generator_id in self.plugins: + return self.plugins[generator_id](params) + logger.warning(f"找不到生成器插件: {generator_id}") + return None + + def list_generators(self) -> List[Dict[str, Any]]: + result = [] + for gid, cls in self.plugins.items(): + result.append({ + 'id': gid, + 'name': cls.generator_name, + 'description': cls.description, + 'version': cls.version, + 'params_schema': cls.params_schema + }) + return result diff --git a/ppt_manager_v2/plugins/generators/__init__.py b/ppt_manager_v2/plugins/generators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ppt_manager_v2/plugins/generators/__pycache__/cpi_generator.cpython-311.pyc b/ppt_manager_v2/plugins/generators/__pycache__/cpi_generator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d8ec466e66d36519b7829ff0b07533c4b225f01 GIT binary patch literal 3634 zcmb_eeQevt6+coG^=Zprl4B>%oPI4tU4O|=)6|)g*+?3Aa|2fx2F-(@=qOnpMX4lJ zz=qQbU4|KfmwJG=xSfHhMVETZwgyFmWdFq1^R&iFC`vx#q5k{C2P=wU9 zK&9yjO(7ml*(~z{V{vI)%^;_{wDp=6f_(YkQrBp750mMSQRc&a^Ay zGI>gHM?9u~-bfwPj8LEUMSSUoNCO4)pGDYy31OCZT?cP(R6UL~V(JydId;gfV;p5L z=VQtw)OFvC$^2PfmC;}svw%3cxfPXKcjq7k7miqM#f*f1So zuq|T8OoYX5nA^cSc~^pNtM?#ka6uWipQY`wjbI|2q}3C#X>=L#5Z zUAYD=Wy9^C-Q51k?`G`%-M!ttV+NazNwKtSuoAE2Br#r%I*j6hk3{dj6X;cxgdOo7 zb&*!8Yl_fRm7k?Cjcu60c4g1b__FK9s&_iUsnjXXB z-S{@|$0%RoaZA}__^T=+6)fzte$K41zjlQHdjgP~tz#UUBU`+0dwI@e`Yk4CF#{Ge zXfeltNhZv)9ZJ6QHYq!y7tFgL-?%l3W;GY5uMBo9PeV`1KoV&nd0v)LKyOIiqrC^tO&)((k)I@V)0#JNvZG zzMI2p=ksdY^LJWGHmW_e*?zdd6!6N(t^SW+xE=Z^qy|T{;K-Vy21nJd(FePasGY~v zw&Qo6BHd3>B^UdoyM%z;Lu|nF86k7byt~wZp6k8!#K+w2o{xIe{$Z_ucrC8>pHsWe zeKPRrcvu_%-o|)reLSX)$F=czF_BWoQ|f4{`0NzvK3~7^>Qdu^yfj*D{krNrs(1B1 z+`&)6pN7Aqg}=WMj<1K~YM9r;d~s4x!-D#4q4;e2VS{Ztz4M+8=Ye(SfnwW$>KxRZ zgGJ|{?rWY??nAIqyn0@MK=RK#UnZdSDuRq8CT7XE%%Z|xTT6jzPYkN27p8X4+rr+H z>=C^$QhVpbdrs>D@#}$tzku0aWsaH|m_7dO+YH#gOkboiJ4?+Htgd>E9pvm=W&yL@ zV^STon_0d?0%ImNN6)a38AqSyW>~-ks12`>5_w29=?A)Zz@+!v~?y`Y0o#la#DSR>`rFgha$h=v*NY*DzP+((Xd@>^$b|s|M)HZUc|gK(_E9&<;Ek#np`I!pFO_$6Gcyl z?rnJM<;yRt-VV*%QS^3f`r8&G1&8K8y5a9x_xGs&KF!}Z=iUsoFHRK>YJulA0>Sk_ z@aAbXa7+sv+X$Ro51dp3AuSNnzk*H4Zfk<91v1B$yyzgOw?Cx^4nejrx9fo;y8oF{ zoudIP%EaZCz6Sns)EHN1IHOS!@G}}U+|g({gL4Amz0v5axtLJVc&i{6O<|*cS0Z}D zgLyeFrLsyYBN~p^U=4wh2*4XQS&<}idK=7zOhzyxFLiDyNTS^E>p`{RC2^J>bKWvJ32F&V<t_j?qmJLT1F6=_;f<$u1V2EU}a1ij4A`Orxh+raRMvdHS zc~LO}m9wJz0^IR;PY8)2=3pe^m@G9B!7k>~*=ec8RKSi-iAghZX3)xkIR?7jV14c! zDlgJEq|=}vXeIv%2*5W*>ByZo9~~Vi*1kHjU2^`|_oi=wS_s0UEGSDa{q*(4*R|Fz ztu?5%o=}@#)S6#Z(U69QifCvP^%iSi9W@uNS4T}n>($Yr;=|q&bCiNWKS&l%-XpD_ KJw#>x=>GwJ6ka+2 literal 0 HcmV?d00001 diff --git a/ppt_manager_v2/plugins/generators/__pycache__/gdp_generator.cpython-311.pyc b/ppt_manager_v2/plugins/generators/__pycache__/gdp_generator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75b3bbe0ff52178ce3c1eec70407b4736a2feb5f GIT binary patch literal 3500 zcmb^zZERE5^}Y{3f5%B2=LWoQl~5Q3%_=+aPBmO|4`MAP(^$r#^f$H;#6-g}Oe z;8fAX)QDw}N@2Kb7^WXtHrB$VX`>yi`8RE9HJe%D>Hb369LWC6C~VUDYv;aaJ26nH z+PEL*-gC~q=bm@&J?FfiyWI{1<@AI0Qzxwm{f#2kVyjHn2uv;_j4%^NA|2T{lVC-b zK@vK}RhQ@Z>TKhP5sM@XbXF1;8HBDvH_ItF?~7;kYZqDA3EeCV)q#uK5_ZvU%$c}D zbQ=1(L^q6lydmKcJ&8uKk%9A%AZ$5ejjvd%(%v=ksXvNLAL9$79$q`{YYLc->az=REc*Y{SVE!0C!s4bgJbbAe zwD%I%k;AXmlqL2kP6fkbAw^5KgGAun|Lu=TXFn;u{fGNkmhR18UYVIIy|t9y1xyoz zC8ZDEEq%OLdgo`W7e2Xn?v0hR*?S+;9i?-Z>6#1ISAP8Aq-AgKp58r#i^0`yKgv#zEXN`YhMTIMHE9BbP7r3xbCV$XZFn)4*bSJ4Vn7utqxBLS<)?V9dfy;#{oQAHY@_$8 zRsDr>Yb$fqNRO38pj@p{v2zmEPXdlzPeV$S$8Ow!J^zQS#+pV=PeTf5)V#pe`*a2c zYY_l$vu3@Vy=IsV6YSM|>nN6ugwEU+^`S(C&*V;gZ^{0V~z=s0? zPH6@?|DxYU{cUb`<=T7|yLavyVCl^!&-AwY=YCf@{}bw~XD|Qr=4|PQzuuarPOx?5 z(v|xkUN7CcFcNSoUZ4;zDrtmGVPZcV(n2qR4@rcTQ{YD-87C9OLPwccf*9CDh$o{_ zNg;epjwA^em*jwj@TwG#n9`V(VI^X#_@%1Sx4e~>&!2l~|7a?u(H=~Mi^QNO$d;(PmHQUAwa5 z+00TiOlBC}(|A68Ha++4>`1}mFM9kl{N1)q*}*r*=U$;ThnM=UL~_s5dVS>jPFk0@ z-#ATc-g|V$R`RvYkLqpRdS_Qw&As@6|5t%)yMMX6&~u>JbKpj}&@))rKDgZfS@WkI zf9d{ncVS2@4vG2iohS^QD7<(g-x2zsH@Bs0x&W9At@UxDx3)$VX&O5gD z+qU+6$MIX-jl(xz{>XFF1BlrQwvnQ3BySthJ>D5@4QzzM8$%p*1S2$#0`+2DV4QBr zpeKjJ`T)e#?ZV7@D#~pP!t`jP)bE~RHU?w7A(7>3skXY4j`$Jx{4!_MoxpF<@2Q=D zEeq@vi-io6Ve3wV#EtC>0t67pu;Ii?Cw#;zHH@+eK!8nC*RFZBnMJ?)l|Yqjx_6CC z_;^f~h~3E1L2#6`r?TBhzh<@$2*hD7Fw&|c9MYs{Qi(~B9#zR0?bMwkhPMkLIXsqB zh((KOaVfwneyUs#03v9~cudtG2qe=ohA^~)vid043qYmz1J_mpFw)E`V@a9x);Xw2 z+)h{1oCVJ5^vv$hJG*pOo(oDqwKae!U_}E9m-D7H^2-B8i*|zlp!z%gTxUGCXzTEr*l^@ z2w^T>;karMD;UG1VI8A bool: + months = ['1月', '2月', '3月', '4月', '5月', '6月'] + + self._data = pd.DataFrame({ + 'month': months, + 'CPI同比': [0.7, 0.8, 0.9, 1.0 + np.random.randn() * 0.1, + 0.95 + np.random.randn() * 0.1, None], + 'PPI同比': [-2.5, -2.3, -2.1, -1.9 + np.random.randn() * 0.15, + -1.8 + np.random.randn() * 0.15, None] + }) + + self._data.loc[4:5, 'CPI同比'] = [0.9 + np.random.randn() * 0.1, 0.95 + np.random.randn() * 0.1] + self._data.loc[4:5, 'PPI同比'] = [-1.7 + np.random.randn() * 0.15, -1.5 + np.random.randn() * 0.15] + + self.logger.info("CPI/PPI数据获取成功") + return True + + def render(self) -> Dict[str, Any]: + if self._data is None: + self.fetch_data() + + categories = self._data['month'].tolist() + series = { + 'CPI(%)': self._data['CPI同比'].round(2).tolist(), + 'PPI(%)': self._data['PPI同比'].round(2).tolist() + } + + return { + 'chart_type': 'line_markers', + 'categories': categories, + 'series': series, + 'dataframe': self._data, + 'anchor': 'chart_cpi', + 'title': 'CPI与PPI走势' + } diff --git a/ppt_manager_v2/plugins/generators/gdp_generator.py b/ppt_manager_v2/plugins/generators/gdp_generator.py new file mode 100644 index 0000000..081ab19 --- /dev/null +++ b/ppt_manager_v2/plugins/generators/gdp_generator.py @@ -0,0 +1,53 @@ +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from plugins.base_generator import BaseGenerator +import pandas as pd +import numpy as np +from typing import Dict, Any + +class GDPGenerator(BaseGenerator): + generator_id = "gdp_chart" + generator_name = "GDP趋势图表生成器" + description = "生成GDP季度增长率折线图原生数据" + version = "2.0.0" + + params_schema = { + 'year': {'type': 'int', 'default': 2026, 'description': '年份'}, + 'quarter': {'type': 'str', 'default': 'Q2', 'description': '季度'} + } + + def fetch_data(self, params: Dict[str, Any] = None) -> bool: + p = {**self.params, **(params or {})} + year = p.get('year', 2026) + + quarters = [f"{year-1}Q3", f"{year-1}Q4", f"{year}Q1", f"{year}Q2"] + + self._data = pd.DataFrame({ + 'quarter': quarters, + 'GDP同比': [5.2, 4.8, 5.0 + np.random.randn() * 0.3, 5.1 + np.random.randn() * 0.3], + 'GDP环比': [1.6, 1.4, 1.2 + np.random.randn() * 0.2, 1.3 + np.random.randn() * 0.2] + }) + + self.logger.info(f"GDP数据获取成功,共 {len(self._data)} 条记录") + return True + + def render(self) -> Dict[str, Any]: + if self._data is None: + self.fetch_data() + + categories = self._data['quarter'].tolist() + series = { + 'GDP同比增长(%)': self._data['GDP同比'].round(2).tolist(), + 'GDP环比增长(%)': self._data['GDP环比'].round(2).tolist() + } + + return { + 'chart_type': 'line', + 'categories': categories, + 'series': series, + 'dataframe': self._data, + 'anchor': 'chart_gdp', + 'title': 'GDP增长趋势' + } diff --git a/ppt_manager_v2/requirements_v2.txt b/ppt_manager_v2/requirements_v2.txt new file mode 100644 index 0000000..ae88488 --- /dev/null +++ b/ppt_manager_v2/requirements_v2.txt @@ -0,0 +1,14 @@ +python-pptx>=0.6.21 +Flask>=2.3.0 +Flask-SocketIO>=5.3.0 +python-socketio>=5.8.0 +eventlet>=0.33.0 +pandas>=2.0.0 +matplotlib>=3.7.0 +plotly>=5.15.0 +requests>=2.31.0 +PyYAML>=6.0 +loguru>=0.7.0 +numpy>=1.24.0 +openai>=1.0.0 +dashscope>=1.14.0 diff --git a/ppt_manager_v2/templates/index_v2.html b/ppt_manager_v2/templates/index_v2.html new file mode 100644 index 0000000..0e0d3ef --- /dev/null +++ b/ppt_manager_v2/templates/index_v2.html @@ -0,0 +1,201 @@ + + + + + + PPT智能管理系统 V2.0 + + + + + +
+
+

+ PPT智能管理系统 v2.0 +

+

基于锚点定位 | 原生图表更新 | 插件化架构 | WebSocket实时推送

+
+ +
+
+
+

+ + 参数配置 +

+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+

+ + 实时进度 +

+ +
+
+
+ +
等待开始...
+ +
+
+ LOG OUTPUT (实时推送) +
+
+
// 系统已就绪,等待开始
+
+
+
+
+ +
+
+

+ 📦 + 已加载插件 +

+
+
加载中...
+
+
+ +
+

+ 📋 + 已生成文件 +

+
+
暂无
+
+
+ +
+

✨ 核心增强

+
    +
  • • 锚点(Anchor)定位 - 无惧页码变动
  • +
  • • 原生图表更新 - 可编辑/保持主题动画
  • +
  • • 插件化架构 - 自动扫描注册即插即用
  • +
  • • 条件渲染 - 规则判断动态增删页
  • +
  • • LLM洞察 - 数据→分析结论自动生成
  • +
  • • WebSocket - 进度+日志实时推送
  • +
+
+
+
+
+ + + + diff --git a/ppt_manager_v2/web/templates/index_v2.html b/ppt_manager_v2/web/templates/index_v2.html new file mode 100644 index 0000000..0e0d3ef --- /dev/null +++ b/ppt_manager_v2/web/templates/index_v2.html @@ -0,0 +1,201 @@ + + + + + + PPT智能管理系统 V2.0 + + + + + +
+
+

+ PPT智能管理系统 v2.0 +

+

基于锚点定位 | 原生图表更新 | 插件化架构 | WebSocket实时推送

+
+ +
+
+
+

+ + 参数配置 +

+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+

+ + 实时进度 +

+ +
+
+
+ +
等待开始...
+ +
+
+ LOG OUTPUT (实时推送) +
+
+
// 系统已就绪,等待开始
+
+
+
+
+ +
+
+

+ 📦 + 已加载插件 +

+
+
加载中...
+
+
+ +
+

+ 📋 + 已生成文件 +

+
+
暂无
+
+
+ +
+

✨ 核心增强

+
    +
  • • 锚点(Anchor)定位 - 无惧页码变动
  • +
  • • 原生图表更新 - 可编辑/保持主题动画
  • +
  • • 插件化架构 - 自动扫描注册即插即用
  • +
  • • 条件渲染 - 规则判断动态增删页
  • +
  • • LLM洞察 - 数据→分析结论自动生成
  • +
  • • WebSocket - 进度+日志实时推送
  • +
+
+
+
+
+ + + + diff --git a/ppt_manager_v2/web_socket_app.py b/ppt_manager_v2/web_socket_app.py new file mode 100644 index 0000000..0260de6 --- /dev/null +++ b/ppt_manager_v2/web_socket_app.py @@ -0,0 +1,96 @@ +import sys +from pathlib import Path +from flask import Flask, render_template, jsonify, send_file, request +from flask_socketio import SocketIO, emit +import threading +from datetime import datetime + +sys.path.insert(0, str(Path(__file__).parent)) + +from orchestrator import Orchestrator +from loguru import logger + +base_dir = Path(__file__).parent +template_dir = base_dir / "web" / "templates" + +app = Flask(__name__, template_folder=str(template_dir)) +app.config['SECRET_KEY'] = 'ppt_manager_v2_secret!' +socketio = SocketIO(app, cors_allowed_origins="*") + +@app.route('/') +def index(): + return render_template('index_v2.html') + +@app.route('/api/projects') +def list_projects(): + orch = Orchestrator() + plugins = orch.plugin_manager.list_generators() + return jsonify({ + 'success': True, + 'plugins': plugins, + 'config': orch.config + }) + +@app.route('/api/files') +def list_files(): + output_dir = base_dir.parent / "output" + files = [] + if output_dir.exists(): + for f in sorted(output_dir.glob("*.pptx"), reverse=True): + files.append({ + 'name': f.name, + 'size': round(f.stat().st_size / 1024 / 1024, 2), + 'modified': datetime.fromtimestamp(f.stat().st_mtime).isoformat() + }) + return jsonify({'success': True, 'files': files}) + +@app.route('/download/') +def download(filename): + f = base_dir.parent / "output" / filename + if f.exists(): + return send_file(str(f), as_attachment=True) + return jsonify(success=False, error="File not found"), 404 + +@socketio.on('start_generation') +def handle_start_generation(data): + params = data.get('params', {}) + + def progress_callback(message): + socketio.emit('progress', message, room=request.sid) + logger.debug(f"WebSocket进度: {message['percent']}% - {message['message']}") + + orch = Orchestrator() + orch.set_progress_callback(progress_callback) + + try: + socketio.emit('progress', { + 'step': 'START', + 'message': '开始执行PPT生成管线...', + 'percent': 0 + }, room=request.sid) + + output_path = orch.run_full_pipeline(params=params) + + socketio.emit('complete', { + 'success': True, + 'output_file': Path(output_path).name, + 'download_url': f"/download/{Path(output_path).name}", + 'results': {k: v.get('success', False) for k,v in orch.results.get('plugins', {}).items()} + }, room=request.sid) + + except Exception as e: + logger.exception("生成过程出错") + socketio.emit('complete', { + 'success': False, + 'error': str(e) + }, room=request.sid) + +def run_server(): + print("\n" + "=" * 65) + print(" 🚀 PPT智能管理系统 V2.0 - WebSocket实时日志版") + print(" 请在浏览器打开: http://localhost:5001") + print("=" * 65 + "\n") + socketio.run(app, host='0.0.0.0', port=5001, debug=True) + +if __name__ == '__main__': + run_server()