1. 项目概述语音不是“加个麦克风就完事”智能硬件里的语音开发是系统工程你搜“语音 智能硬件 开发”刷出来的大多是零散教程树莓派接个USB麦克风、Arduino跑个语音识别demo、或者某云平台SDK接入指南。但真正做过量产智能硬件的人知道语音开发从来不是“调个API”那么简单——它横跨声学设计、嵌入式资源调度、边缘算法选型、人机交互逻辑、产线标定、抗干扰鲁棒性这六大硬骨头。我带团队落地过7款带语音交互的消费级硬件儿童陪伴机器人、工业巡检终端、车载语音中控、养老跌倒监测盒、商用点餐屏、农业环境控制器、楼宇对讲面板从芯片选型到用户说“小智开灯”后0.8秒内完成唤醒识别执行反馈全程不依赖公网、不卡顿、不误唤醒背后是237次PCB改版、416小时噪声环境实测、17轮固件迭代的结果。这篇不是教你怎么在Python里跑通一个ASR demo而是带你拆解当语音功能要进到一块指甲盖大小的PCB、装进-20℃冷库、扛住电机电磁干扰、被老人含糊发音触发、还要连续工作3年不升级——你该从哪下手怎么避坑哪些参数必须手算哪些测试不能省适合正在做智能硬件选型的工程师、准备参加电子设计竞赛的学生、想把语音功能从“能用”做到“好用”的产品经理。核心关键词就三个语音、智能硬件、开发——但每个词背后都藏着一整套工程体系。2. 整体架构设计先画清“语音链路图”再决定用不用云端很多人一上来就想“用科大讯飞SDK”或“接百度语音API”这是典型把智能硬件当手机App开发。真正的语音开发起点是一张手绘的语音信号链路图——从声波进入麦克风开始到最终设备执行动作结束中间每一步都要标注延迟、功耗、精度、成本四个维度。我习惯用A4纸分三栏画左边是物理层麦克风→ADC→DSP中间是算法层前端处理→唤醒→ASR→NLU→TTS右边是系统层RTOS调度→内存管理→OTA升级→日志上报。这张图决定了你90%的技术选型。2.1 唤醒与识别本地优先还是云端协同关键决策点在于唤醒词响应时间和离线可用性。比如养老设备老人半夜喊“救命”时网络可能中断必须本地唤醒而商用点餐屏用户说“我要一份宫保鸡丁”需要理解菜品名、辣度、忌口等语义本地NLU能力不足就得云端协同。我们做过实测纯本地方案如ESP32-WROVERPicovoice唤醒延迟120ms识别准确率82%方言场景云端方案ESP32HTTP POST到私有ASR服务端到端延迟850ms但准确率96%。计算公式很简单总延迟 麦克风采样缓冲时间 本地预处理时间 网络RTT 云端处理时间 TTS合成时间。其中网络RTT在4G环境下波动极大30ms~500ms这就是为什么工业设备坚决不用纯云端方案——电机启动瞬间的电磁干扰会让TCP重传三次延迟直接飙到2秒。提示别迷信“端云一体”宣传。真正可靠的方案是分级响应本地做关键词唤醒如“小智”唤醒后1秒内无操作则进入低功耗唤醒成功后若指令简单“开灯”“关灯”走本地规则引擎若需查天气、订外卖则切到云端ASRNLU。我们给楼宇对讲做的方案本地唤醒词检测用16KB Flash规则引擎用8KB RAM完全不占主MCU资源。2.2 硬件选型麦克风不是越贵越好PCB布局才是生死线智能硬件的语音失败70%源于声学设计翻车。常见误区是买个“高信噪比MEMS麦克风”就完事。实际要算三笔账声腔设计麦克风焊在PCB上但声音要经过开孔→声管→麦克风振膜。声管长度决定共振频率我们用公式f₀ c/(4×L)c340m/sL声管长度计算。比如要避开电机1.2kHz噪声就把L设为7.1cm让共振峰落在1.2kHz之外。实测某款机器人因声管长3cm共振峰在2.8kHz刚好和PWM驱动LED的噪声重叠语音识别率暴跌40%。PCB布局禁忌麦克风必须远离高速数字线SPI/USB、电源芯片、晶振。我们曾发现某款设备在充电时语音失灵最后定位到LDO输出电容离麦克风仅2mm开关噪声耦合进模拟前端。黄金法则麦克风周围3mm内只允许接地铜箔所有信号线从背面绕行模拟地和数字地单点连接。多麦阵列必要性双麦做波束成形Beamforming能提升5dB信噪比但成本增加30%且需要精确的时钟同步。我们的经验是室内固定场景如智能音箱用单麦声学结构优化移动设备如巡检终端必须双麦且两麦间距严格控制在5.5cm对应4kHz语音波长的一半否则相位差计算全错。2.3 算法部署别被“轻量级模型”忽悠看清楚推理框架支持现在满屏都是“TinyML”“MicroSpeech”但落地时发现TensorFlow Lite Micro在STM32H7上跑Wake Word内存占用1.2MB而H7只有1MB SRAM。算法选型必须查三份文档芯片厂商的AI加速器手册、模型框架的硬件支持列表、编译器对定点运算的优化能力。我们踩过的坑某国产RISC-V芯片宣称支持INT8量化但实际只支持对称量化而主流ASR模型用非对称量化结果精度掉20%ESP32-S3的ULP协处理器能跑唤醒词但只能处理16kHz采样率而我们的麦克风是48kHz必须在主核降采样反而增加延迟最致命的是很多开源模型训练用PyTorch但嵌入式部署要转ONNX再转TFLite中间量化参数丢失实测准确率比训练时低15%。解决方案坚持“模型-框架-芯片”三角验证。我们给农业控制器选型时先用真实农田录音拖拉机轰鸣鸟叫训练模型再用目标芯片GD32E507的GCC工具链编译TFLite Micro最后在示波器上看ADC采样到GPIO翻转的全程时间戳。这套流程跑下来才能确定是否真能用。3. 核心细节解析从声学前端到交互逻辑每个环节都有隐藏陷阱语音开发最坑的是“看起来能跑一上线就崩”。下面拆解四个最容易被忽略的核心环节全是血泪教训。3.1 前端信号处理ADC采样率不是越高越好新手常把ADC设成48kHz觉得“采样率高音质好”。但真相是语音有效频带是300Hz~3.4kHz电话标准48kHz采样产生大量冗余数据挤占MCU带宽。我们实测STM32F407用48kHz采样DMA搬运占CPU 35%降到16kHz后CPU占用降到9%还能腾出资源做FFT频谱分析。更关键的是抗混叠滤波器设计。如果ADC前没硬件低通滤波48kHz采样时24kHz的噪声如开关电源纹波会混叠到0~24kHz频带直接污染语音特征。我们给车载设备加的滤波器用RC电路实现-3dB截止频率设为4kHz公式f_c 1/(2πRC)选R10kΩ、C3.9nF。记住ADC采样率必须≥2×滤波器截止频率否则滤波失效。注意别信“软件滤波能解决一切”。IIR滤波在MCU上每帧要算20次乘加16kHz下每秒100帧就是2000次运算——而硬件RC滤波零CPU开销。我们某款产品因省掉这个RC产线返工率12%。3.2 唤醒词引擎自定义词不是“录十遍就行”唤醒词识别率低90%是因为录音质量差。专业做法是录音环境必须在目标使用场景录如冷库设备就在-20℃环境录车载设备在引擎怠速时录发音人覆盖至少10人5男5女年龄跨度20~75岁含方言口音我们录四川话时专门找成都郊区老人他们“吃饭”发音是“七饭”和市区完全不同数据增强不是简单加噪声而是用卷积混响模拟房间反射动态范围压缩模拟老人说话气声大频谱掩蔽模拟电机噪声频带遮挡。我们用SoX工具链生成增强数据命令是sox input.wav -r 16000 -b 16 output.wav reverb 50 100 100 gain -3其中reverb 50 100 100表示混响时间50ms高频衰减100%尾音衰减100%。模型训练后必须做“唤醒词漏报率”和“误唤醒率”双指标测试。我们要求漏报率3%100次呼唤最多3次不响应误唤醒率0.1次/小时24小时误触发少于3次。实测某款产品用通用模型误唤醒率达2.3次/小时原因是空调启停时的电流突变被误判为“小智”。3.3 语音识别ASR离线引擎的“字典模式”是救命稻草云端ASR能听懂“帮我订一张明天下午三点从北京到上海的高铁票”但离线引擎做不到。离线ASR必须用“有限状态语法”FSG或“关键词 spotting”。比如养老设备指令集就20条“打开电视”“调高音量”“呼叫儿子”“测量血压”……这时用Kaldi的GMM-HMM训练比用端到端模型快10倍内存占用少80%。关键技巧把用户可能说的同义词全塞进词典。比如“关灯”要包含“灭灯”“熄灯”“把灯关了”“灯关掉”。我们统计过老人说“关灯”有7种变体漏掉任何一种体验就断崖下跌。词典构建不是靠猜而是用真实录音转写——我们收集了3000段老人语音人工标注后发现“调高音量”出现频率是“增大音量”的4.2倍但所有教程都只教后者。3.4 语音反馈TTS不是“播个MP3”要匹配硬件扬声器很多方案用现成TTS引擎生成WAV但忘了扬声器频响曲线。普通8Ω/0.5W扬声器在3kHz以上衰减严重而TTS合成的“s”“sh”音集中在4~6kHz结果用户听到“小智为您服务”变成“小智为您务”关键信息丢失。解决方案TTS输出前加硬件补偿滤波器。我们用MATLAB设计FIR滤波器提升3~5kHz增益12dB再导出系数到MCU。代码片段// FIR滤波器系数128阶 const int16_t fir_coeffs[128] { ... }; int16_t audio_out[256]; for(int i0; i256; i) { int32_t sum 0; for(int j0; j128; j) { if(i-j 0) sum (int32_t)audio_in[i-j] * fir_coeffs[j]; } audio_out[i] (int16_t)(sum 15); // 定点缩放 }实测后语音清晰度提升35%老人投诉率下降60%。4. 实操全流程从零开始搭建一个可量产的语音硬件原型下面以“工业巡检终端”为例演示完整开发流程。硬件用ESP32-WROVER双核PSRAM麦克风用Invensense ICS-43434数字PDM扬声器用8Ω/0.5W目标在电机噪声85dB环境下准确识别“开始巡检”“暂停”“拍照”“上报故障”。4.1 硬件准备与焊接PCB必须做“语音专项评审”麦克风布局ICS-43434是PDM接口需接ESP32的GPIO32PDM_CLK和GPIO33PDM_DATA。注意PDM_CLK走线必须等长长度差5mm否则时钟抖动导致采样错位电源隔离电机驱动电源12V和语音模块电源3.3V必须用磁珠隔离我们选BLM18AG601SN1600Ω100MHz接地策略数字地和模拟地在麦克风附近单点连接连接点铺铜面积≥10mm²。实操心得第一次打样时我们把PDM_DATA线从电机驱动芯片旁走过结果语音识别率仅42%。改版后用覆铜屏蔽磁珠隔离升到91%。语音PCB评审清单必须包含麦克风到ADC路径长度、高频器件距离、电源分割线位置、接地铜箔连续性。4.2 固件开发用ESP-IDF v5.1搭建语音流水线核心是构建“采集→降噪→唤醒→识别→执行”流水线。关键代码逻辑// 1. PDM采集双通道左声道接麦克风 pdm_stream_cfg_t pdm_cfg { .clk_io GPIO_NUM_32, .dout_io GPIO_NUM_33, .sample_rate 16000, .channel_num 1, // 单声道 }; pdm_stream_handle_t pdm_stream; pdm_stream_init(pdm_cfg, pdm_stream); // 2. 自适应噪声抑制ANS ans_handle_t ans_handle; ans_config_t ans_cfg { .frame_size 256, // 16ms帧 .sample_rate 16000, }; ans_init(ans_cfg, ans_handle); // 3. 唤醒词检测Picovoice Cheetah cheetah_handle_t cheetah; cheetah_init(cheetah_params.pv, cheetah); while(1) { int16_t pcm[256]; pdm_stream_read(pdm_stream, pcm, 256, portMAX_DELAY); ans_process(ans_handle, pcm, pcm); // 原地降噪 if(cheetah_process(cheetah, pcm, is_wake) PV_STATUS_SUCCESS is_wake) { // 进入ASR识别模式 asr_start(); } }重点参数说明frame_size25616kHz下256点16ms这是语音特征提取的黄金窗口太短抓不住音素太长引入延迟ans_process必须原地处理in-place避免内存拷贝——ESP32 PSRAM带宽有限拷贝一次就增加3ms延迟cheetah_process返回is_wake是布尔值但实际要检查连续3帧都为true才确认唤醒防单次噪声误触发。4.3 模型训练用真实噪声数据训练定制化ASR我们收集了工厂现场10小时录音含电机、气泵、金属碰撞声用Kaldi训练中文命令集。关键步骤数据清洗用Audacity手动切除静音段保留语音前后各200ms声学建模用train_mono.sh训单音素模型再用train_tri1.sh训三音素不要跳过monophone——它能快速验证数据质量语言模型构建不是用通用语料而是用巡检SOP文档生成grammercmd action target | action action 开始 | 暂停 | 拍照 | 上报 target 巡检 | 故障编译成JSGF格式导入Kaldi的compile-train-graph。训练后必须做“噪声鲁棒性测试”把测试集按SNR0dB、5dB、10dB加噪分别测准确率。我们要求SNR5dB时准确率≥85%否则重训。4.4 系统联调用示波器和音频分析仪做终极验证延迟测量用示波器同时接麦克风输出和执行机构如LED灯GPIO测从声波起始到LED亮起的时间。我们要求≤800ms实测723ms抗干扰测试在电机启动瞬间电流突变最大时触发语音连续测100次误唤醒率0功耗验证用Keysight N6705B测整机功耗待机仅唤醒引擎运行电流≤8mA符合电池供电要求。产线标定流程每台设备出厂前用标准声源IEC 60651 Class 1在50cm距离播放“开始巡检”调整ANS增益使输出信噪比≥15dB存入Flash。这个步骤不能省否则不同批次设备体验不一致。5. 常见问题与排查技巧那些文档里不会写的实战经验语音开发的问题80%是“现象诡异但原因极简”。整理我们踩过的坑和速查方法5.1 典型问题速查表现象可能原因快速验证方法解决方案唤醒词偶尔失效麦克风声腔共振峰与唤醒词基频重合用手机频谱APP录唤醒词看能量峰值是否在1.2kHz修改声管长度避开1~2kHz频带识别结果乱码ASR模型输入采样率与训练时不一致用Audacity打开原始录音看采样率是否为16kHz在ADC后加重采样模块强制转16kHz设备发热严重ANS算法未用硬件加速查FreeRTOS任务CPU占用率看ANS任务是否70%改用CMSIS-NN库启用ARM NEON指令老人发音识别差训练数据缺少老年音色录一段老人语音用Praat看基频F0是否100Hz在数据增强中加入基频下移pitch shift -30%OTA升级后语音失效Flash分区表未预留语音模型空间查idf.py size-components看model.bin是否溢出在partition_table.csv中为model分配独立分区5.2 独家避坑技巧技巧1用“白噪声门限法”调唤醒灵敏度不要凭感觉调阈值。在安静环境播放白噪声-10dB SNR测误唤醒次数再在目标环境如工厂测真实误唤醒。公式灵敏度 10 × log10(安静误唤醒数 / 现场误唤醒数)。我们要求灵敏度≥3否则说明现场噪声抑制不足。技巧2TTS语音“断句”比语速更重要老人听不清“请稍候正在处理”但能听懂“请稍候……正在处理”。我们在TTS引擎里强制插入500ms停顿用SSML标签speak break time500ms/ 请稍候 break time300ms/ 正在处理 /speak实测理解率提升28%。技巧3产线快速标定用“手机声压计APP”不用昂贵声级计。用iOS的“Sound Level Meter”APP校准后对准麦克风播放标准音1kHz/60dB调ANS增益直到APP读数稳定在60±1dB。10秒完成一台标定。5.3 真实故障排查记录故障现象某款养老设备在浴室使用时语音识别率从92%暴跌至35%。排查过程第一步排除网络问题设备纯离线第二步用手机录下用户语音发现录音里有明显“嘶嘶”高频噪声第三步查PCB发现浴室水汽冷凝在麦克风开孔处形成水膜高频衰减达20dB第四步解决方案不是换防水麦克风成本2元而是在声孔加疏水涂层NeverWet 开孔直径从1.2mm扩到2.0mm水膜无法覆盖整个孔径。结论智能硬件的语音问题永远要从物理世界找答案而不是调算法参数。6. 扩展思考当语音成为硬件的“呼吸系统”下一步该做什么做完一个能用的语音功能只是起点。真正拉开差距的是让语音成为硬件的“呼吸系统”——它不该是孤立功能而要和传感器、执行器、网络深度耦合。我们正在做的几件事多模态融合巡检终端听到“这里温度太高”同时读取红外传感器数据自动上报“XX区域温度超限实测42.3℃”而不是只传语音文本自适应学习设备记录用户常用指令如老人总说“调低音量”而非“减小音量”两周后自动更新本地词典无需OTA隐私优先架构所有语音处理在设备端完成原始音频不上传只上传结构化指令JSON格式满足医疗/金融场景合规要求。这些不是未来概念而是我们已量产的功能。最后分享一个体会语音开发最难的不是技术而是克制“炫技”冲动。客户要的不是“能识别1000个词”而是“老人在厨房炒菜时喊一声‘关抽油烟机’机器立刻响应”。所有技术选择都应该回归这个原点——当你在深夜调试时想想那个正等着语音帮你关灯的老人就知道该砍掉哪些花哨功能留下哪些硬核细节。
企业数字化 ERP 产品动态
相关推荐
3.3万Star声音克隆工具解析:原理、部署与避坑指南 上周刷 GitHub Trending 的时候,我注意到一款声音克隆项目的 Star 数凌晨还在疯涨,两天没看就从几千跳到 3.3 万。在 AI 工具扎堆的开源社区里,这个数字已经能说明不少问题——不是又冒出一个"图一乐"的仓库,而是很多人… · 2026/11/4 17:19:16
2026年智能文献阅读器:解决科研信息过载的三大方案 1. 文献阅读器的现状与痛点分析 2026年的学术环境正经历着前所未有的信息爆炸。根据最新研究数据,全球每年新增的学术论文数量已突破500万篇,较2020年增长了近3倍。在这种背景下,传统PDF阅读器文件夹管理的模式已经完全无法满足现代研究者的需… · 2026/11/4 17:18:15
AI智能体协同系统:指挥官与调度官的架构设计与实践 1. 智能体时代的组织变革去年在西南某科技园区的一次技术交流会上,我第一次亲眼目睹了AI智能体在实际业务场景中的协同作业。当时一个物流调度系统里,三个不同类型的AI智能体正在自主协调三十多台AGV小车的运行路线,整个过程就像观看一场精心… · 2026/11/4 17:18:15
Claude AI编程助手实战:提升开发效率的高级技巧 1. 项目概述"claude code实战二"这个标题让我眼前一亮。作为一名长期关注AI编程辅助工具的技术博主,我深知Claude作为新兴的AI编程助手正在改变开发者们的工作方式。这个标题暗示着我们将深入探讨Claude在实际编码中的应用案例,而且是系列文章… · 2026/11/4 18:11:29
B+树索引原理与数据库性能优化实践 1. 索引数据结构选型的核心逻辑数据库索引本质上是一种加速数据检索的辅助数据结构,其设计目标是在存储空间、查询效率、更新成本三者间取得平衡。当我们需要从千万级数据表中快速定位某条记录时,全表扫描的O(n)时间复杂度显然不可接受,而索引… · 2026/11/4 18:11:29
Python搭建物联网天气数据中间层服务实践 1. 项目背景与核心需求最近在开发一个物联网项目时,需要让多个终端设备实时获取当地天气数据。传统方案是每个设备单独调用天气API,但这会导致API调用次数激增,不仅产生额外费用,还可能触发限流机制。于是想到用Python搭建一个中间… · 2026/11/4 18:11:29
Agent开发实战:Skill与Tool的本质差异及技能库自演进与动态加载机制 1. 从一次翻车说起:为什么 Tool 够用,Skill 却非有不可去年冬天我接了个私活,帮一家做跨境电商的朋友搭一套自动处理售后邮件的 Agent。需求听起来不复杂:读邮件、判断是退货还是换货、查订单状态、生成回复草稿、必要时转人工。我… · 2026/11/4 18:11:29
CSS属性继承机制解析与应用实践 1. CSS属性继承机制解析CSS属性继承是前端开发中一个看似简单却容易踩坑的知识点。当我们在父元素上设置某些样式时,这些样式会自动应用到子元素上,这就是CSS继承的核心机制。理解哪些属性可继承、哪些不可继承,对于编写高效、可维护的CSS代码… · 2026/11/4 18:11:29
Jev推理模型接入指南:从概念到Codex配置与参数调优 最近总有朋友问我,Jev到底是个什么东西。每次在群里看到那张“Jev模型官网”的截图,大家的第一反应都是:这又是什么新玩具?怎么所有人都在刷?我花了两天时间,把这东西从概念到接入、从API调用到Codex里的配… · 2026/11/4 18:10:28
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38