|
|
#!/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()
|