#!/usr/bin/env python3 """ 更新声源分析模块_项目交接文档.docx 将本次(2026-05-06)完成的多通道离线演示等工作追加到文档中。 """ from docx import Document from docx.shared import Pt, RGBColor from docx.enum.text import WD_PARAGRAPH_ALIGNMENT import os def add_heading(doc, text, level=1): """添加标题段落""" p = doc.add_paragraph() run = p.add_run(text) run.bold = True if level == 1: run.font.size = Pt(16) elif level == 2: run.font.size = Pt(14) else: run.font.size = Pt(12) p.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT return p def add_bullet(doc, text, indent=0): """添加项目符号段落""" p = doc.add_paragraph(style='List Bullet') p.paragraph_format.left_indent = Pt(indent * 10) p.add_run(text) return p def add_normal(doc, text): p = doc.add_paragraph() p.add_run(text) return p def main(): doc_path = '声源分析模块_项目交接文档.docx' doc = Document(doc_path) # ======================================================================== # 1. 在 "2.5 代码 Bug 修复记录" 之后插入 2.6 小节 # ======================================================================== insert_idx = None for i, para in enumerate(doc.paragraphs): if para.text.strip().startswith('2.5'): # 找到 2.5 的最后一项(CMakeLists.txt 那行)之后 pass if para.text.strip().startswith('三、架构设计'): insert_idx = i break if insert_idx is None: print("ERROR: 未找到插入点(三、架构设计)") return # 由于 python-docx 不支持在特定索引插入段落到 body, # 我们采用另一种策略:将新内容作为独立段落追加到文档末尾, # 然后手动调整顺序。但更简单的方法是:直接在 2.5 和 三 之间添加。 # python-docx 的 add_paragraph 总是追加到末尾,所以我们需要操作 XML。 # 为了避免 XML 操作复杂性,我们直接将整个文档重新组织: # 读取所有段落文本 -> 在合适位置插入新文本 -> 重新写入。 # 实际上,对于 docx,最安全的方式是: # 保存一份副本,然后在新段落要插入的位置,使用 paragraphs 列表的 _element 进行 insert。 paragraphs = doc.paragraphs # 找到 2.5 最后一行(CMakeLists.txt)的索引 idx_after_25 = None for i, p in enumerate(paragraphs): if 'CMakeLists.txt' in p.text and 'MinGW' in p.text: idx_after_25 = i + 1 break if idx_after_25 is None: # 备用:找到 "三、架构设计" for i, p in enumerate(paragraphs): if p.text.strip().startswith('三、架构设计'): idx_after_25 = i break # 构建要插入的新段落列表 (text, style_name) new_sections = [] # ---- 2.6 离线演示与多通道验证 ---- new_sections.append(('2.6 离线演示与多通道验证', 'Heading 2')) new_sections.append(('已完成完整的离线演示程序(tests/demo_offline.cpp),支持单通道分类与多通道阵列(分类+方位角+距离)一体化测试:', None)) new_sections.append(('', 'List Bullet')) # 修正:我们无法在 add_paragraph 之后修改之前的内容,所以换个策略 # 直接在 XML 层级插入 # 为了简化,我们把所有段落文本收集起来,重建文档 all_texts = [] for p in doc.paragraphs: style = p.style.name if p.style else None all_texts.append((p.text, style)) # 找到插入位置 insert_pos = None for i, (text, style) in enumerate(all_texts): if text.strip().startswith('三、架构设计'): insert_pos = i break if insert_pos is None: print("ERROR: 未找到 '三、架构设计'") return # 准备新内容 new_content = [] new_content.append(('2.6 离线演示与多通道验证', 'Heading 2')) new_content.append(('已完成完整的离线演示程序(tests/demo_offline.cpp),支持单通道分类与多通道阵列(分类+方位角+距离)一体化测试:', None)) new_content.append(('• 单通道验证:dataset/val 共 40 个文件,分类准确率 100%(ambient/artillery/explosion/gunshot 各 10 个)。', 'List Bullet')) new_content.append(('• 多通道模拟测试:scripts/generate_multichannel_test.py 可按指定方位角(0°–360°)和距离(1–1000m)生成 4 通道十字阵 WAV。', 'List Bullet')) new_content.append(('• 多通道验证结果(5 组典型工况):', 'List Bullet')) new_content.append((' – 方位角:全部 0° 误差(GCC-PHAT 对模拟数据精度极高)。', 'List Bullet')) new_content.append((' – 距离:最大误差 18.2m@300m(约 6%),其余 < 2m。', 'List Bullet')) new_content.append((' – 分类:全部正确识别为 gunshot,置信度 0.77–0.98。', 'List Bullet')) new_content.append(('• 一键演示脚本:run_demo.bat 自动执行核心单元测试 → ONNX 快速推理 → 完整离线流水线。', 'List Bullet')) new_content.append(('2.7 C++/Python 特征提取严格对齐', 'Heading 2')) new_content.append(('为保证 C++ 推理结果与 Python 训练一致,完成了特征提取全流程对齐:', None)) new_content.append(('• 导出 librosa 0.10.x 的 Mel 滤波器组到二进制文件 models/mel_filter_bank.bin(64×1025),C++ 加载后不再自行构造。', 'List Bullet')) new_content.append(('• 统一参数:Hann 窗、preemphasis=0.97、n_fft=2048、hop=512、center=False、pad_to=63 frames(edge 填充)。', 'List Bullet')) new_content.append(('• 验证脚本:scripts/verify_feature_consistency.py 对比 C++ 与 Python 输出,最大差异 < 0.008。', 'List Bullet')) # 插入 all_texts = all_texts[:insert_pos] + new_content + all_texts[insert_pos:] # ======================================================================== # 2. 更新 "五、待办事项与下一步计划" # ======================================================================== # 找到第五部分的起止范围 section5_start = None section5_end = None for i, (text, style) in enumerate(all_texts): if text.strip().startswith('五、待办事项与下一步计划'): section5_start = i elif section5_start is not None and text.strip().startswith('六、'): section5_end = i break if section5_start is not None and section5_end is not None: # 替换第五部分的内容 new_section5 = [] new_section5.append(('五、待办事项与下一步计划', 'Heading 1')) new_section5.append(('【已完成】', 'Heading 2')) new_section5.append(('• 单通道分类准确率提升至 100%(修复 Mel 滤波器、padding、window、ONNX NCHW layout 等 4 处不一致)。', 'List Bullet')) new_section5.append(('• GCC-PHAT 方位估计与 SPL 距离估计集成到离线 demo,使用模拟 4ch WAV 验证通过。', 'List Bullet')) new_section5.append(('• 生成 scripts/generate_multichannel_test.py 及 run_demo.bat 一键演示。', 'List Bullet')) new_section5.append(('【待完成】', 'Heading 2')) new_section5.append(('• [P0] 实机部署:将编译通过的节点部署到 P600 机载电脑(Jetson / x86),验证 ROS 话题发布与订阅。', 'List Bullet')) new_section5.append(('• [P0] 真实麦克风阵列驱动:接入 4 通道 USB / I2S 麦克风阵列,录制真实环境枪声/炮声样本。', 'List Bullet')) new_section5.append(('• [P1] 数据集扩充:收集真实场景样本(含环境噪声、混响、多声源叠加),重新训练以降低合成数据过拟合。', 'List Bullet')) new_section5.append(('• [P1] yaml-cpp CMake 链接修复:当前 MinGW 动态链接出现 __imp__ 未解析符号,需查明是 ABI 不兼容还是导入库生成错误。', 'List Bullet')) new_section5.append(('• [P2] 距离估计 SPL 校准:当前合成数据使用固定 offset=60dB,真实场景需根据麦克风灵敏度数据 sheet 校准。', 'List Bullet')) new_section5.append(('• [P2] Pipeline 类集成到 demo:当前 demo_offline 绕过 Pipeline 直接实例化模块,后续应统一走 Pipeline 以验证 YAML 配置加载。', 'List Bullet')) new_section5.append(('• [P2] 俯仰角估计:当前 GCC-PHAT 仅输出水平面方位角,若阵列有高度差可解俯仰角。', 'List Bullet')) all_texts = all_texts[:section5_start] + new_section5 + all_texts[section5_end:] # ======================================================================== # 3. 更新 "七、文件路径索引" # ======================================================================== section7_start = None section7_end = None for i, (text, style) in enumerate(all_texts): if text.strip().startswith('七、文件路径索引'): section7_start = i elif section7_start is not None and text.strip().startswith('八、'): section7_end = i break if section7_start is not None and section7_end is not None: new_section7 = [] new_section7.append(('七、文件路径索引', 'Heading 1')) new_section7.append(('项目根目录:software/src/drone-software/src/acoustic/', None)) new_section7.append(('• 核心算法:include/acoustic_analyzer/core/ & src/core/', 'List Bullet')) new_section7.append(('• IO 抽象:include/acoustic_analyzer/io/ & src/io/', 'List Bullet')) new_section7.append(('• ROS 封装:include/acoustic_analyzer/ros/ & src/ros/', 'List Bullet')) new_section7.append(('• 训练脚本:scripts/train_classifier.py, export_onnx.py, verify_onnx.py', 'List Bullet')) new_section7.append(('• 手机桥接:scripts/mobile_audio_bridge.py, android_audio_sender.py', 'List Bullet')) new_section7.append(('• 多通道生成:scripts/generate_multichannel_test.py', 'List Bullet')) new_section7.append(('• 特征对齐验证:scripts/verify_feature_consistency.py, verify_val_accuracy.py', 'List Bullet')) new_section7.append(('• 测试程序:tests/test_core_lib.cpp, extract_mel_cpp.cpp, test_classifier_cpp.cpp, demo_offline.cpp', 'List Bullet')) new_section7.append(('• 配置文件:config/acoustic_params.yaml', 'List Bullet')) new_section7.append(('• 模型权重:models/gunshot_classifier.onnx, models/mel_filter_bank.bin, train_output/best_model.pth', 'List Bullet')) new_section7.append(('• 数据集:dataset/{train,val}/{ambient,gunshot,artillery,explosion}/', 'List Bullet')) all_texts = all_texts[:section7_start] + new_section7 + all_texts[section7_end:] # ======================================================================== # 4. 更新 "八、新增构建脚本说明" # ======================================================================== section8_start = None section8_end = None for i, (text, style) in enumerate(all_texts): if text.strip().startswith('八、新增构建脚本说明'): section8_start = i elif section8_start is not None and (text.strip().startswith('AI 助手') or text.strip().startswith('附录') or i == len(all_texts)-1): section8_end = i if text.strip() else i if i == len(all_texts)-1: section8_end = len(all_texts) break if section8_start is not None and section8_end is None: section8_end = len(all_texts) if section8_start is not None and section8_end is not None: new_section8 = [] new_section8.append(('八、新增构建脚本说明', 'Heading 1')) new_section8.append(('• build_core_test.bat:一键命令行编译,适用于快速验证核心算法和 ONNX 推理', 'List Bullet')) new_section8.append((' 编译目标:test_core_lib.exe / extract_mel_cpp.exe / test_classifier_cpp.exe', 'List Bullet')) new_section8.append(('• build_demo.bat:编译离线演示程序 demo_offline.exe', 'List Bullet')) new_section8.append((' 不依赖 yaml-cpp 和 ROS,直接链接 ONNX Runtime + Eigen,用于快速验证完整流水线。', 'List Bullet')) new_section8.append(('• build_cmake_mingw.bat:标准 CMake + MinGW Makefiles 构建流程', 'List Bullet')) new_section8.append((' 自动将源码复制到 C:/temp/acoustic_src(规避中文路径问题),在 C:/temp/acoustic_build 构建,', 'List Bullet')) new_section8.append((' 完成后将可执行文件复制回原目录。适用于需要标准 CMake 流程的场景。', 'List Bullet')) new_section8.append(('• run_demo.bat:一键运行完整演示', 'List Bullet')) new_section8.append((' Step 1: 核心单元测试(test_core_lib.exe)', 'List Bullet')) new_section8.append((' Step 2: ONNX 快速推理(test_classifier_cpp.exe)', 'List Bullet')) new_section8.append((' Step 3: 离线流水线验证(demo_offline.exe dataset/val)', 'List Bullet')) all_texts = all_texts[:section8_start] + new_section8 + all_texts[section8_end:] # ======================================================================== # 重建文档 # ======================================================================== new_doc = Document() for text, style_name in all_texts: if not text and style_name == 'List Bullet': # 空项目符号,跳过 continue if style_name: try: p = new_doc.add_paragraph(text, style=style_name) except: p = new_doc.add_paragraph(text) else: p = new_doc.add_paragraph(text) # 保留原始文档的页眉页脚等信息?简化处理:不保留 output_path = '声源分析模块_项目交接文档.docx' new_doc.save(output_path) print(f"[OK] 文档已更新: {output_path}") if __name__ == '__main__': main()