dsh-ears-prosody

voicetested: passnpmMIT

Local offline prosody analysis for DeepSeek Harness: reports how something was said — speed, pitch, pauses, energy — alongside the transcript.

下载量 / Downloads
2,452
依赖体积 / node_modules
32.0 MB
测试时间 / Tested
2026-11-06
DSH 版本 / Version
0.2.2-rc.1

Install

dsh plugin --profile web add dsh-ears-prosody

Sandbox test results

StageResult
1 install✓
2 list✓
3 mount✓
4 boot—

Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).

README

dsh-ears —— 给 DSH 加一双耳朵

把音频变成「怎么说的」,而不只是「说了什么」。

它能做什么

一段录音丢进来,回你一份结构化的「怎么说」:语速、停顿结构、音高起伏、句尾走向、力度变化、唤醒度——全部本地离线计算,不联网、不要 key。

别人做不到什么

插件市场里的语音插件,几乎都在做两件事:把话变成字(转写 / 听写)和把字变成话(朗读 / TTS)。

这一层做的是第三件事:这话是怎么说出来的。

转写天生会把语气丢掉——停顿、重音、犹豫、句尾是扬起还是沉下去,全在变成文字的那一刻蒸发了。

而这些恰恰是文字里没有的信息:同样一句「哦」,是敷衍还是松了口气,靠的就是它们。

一句话:别的插件处理内容,这个插件处理「内容之外」。

为什么需要它

DSH 的内容块只有 text / reasoning / image / file / tool-*,模型模态只有 text / image

(见 ContentBlockMap 与 ModelModalityMap)——音频字节进不了模型上下文。

而官方语音链路是 speechToText.transcribe() → Transcript,也就是转写:

它把语气、停顿、重音、犹豫全部丢掉了。

本插件走旁路,把音频变成一份结构化文本证据再交给主模型:

音频 ──┬─ ① 本地声学层(零依赖·离线·不转写)
       │     语速 / 停顿结构 / 音高起伏 / 句尾走向 / 力度变化 / 唤醒度
       │
       └─ ② 听音模型层(可选·真的听过)
             语义 + 情绪 + 重读 + 背景音 + 副语言线索

它不做什么(纪律)

  • 不做转写。声学层 words 恒为 null,并在 uncertainty 里明说这一点。
  • 不猜正负情绪。impression.valence 恒为 无法判断(不猜)——声学无法可靠给出 valence。
  • 不假装听见。检测不到人声就直说是静音/音乐/噪声;音频太短就直说特征不可靠。
  • 印象标签标注为启发式·低置信,不给它披上"模型判断"的外衣。

安装(官方桌面端)

桌面端读 desktop profile,用内置 plugin_manager:

install_bundle  target: link:<本目录绝对路径>

插件没有客户端半区(纯宿主),装完重启客户端即可;之后 listen_audio 会出现在工具表里。

配置

写在 profile 的插件配置里(include:ears):

mode: auto            # auto | local | model
maxBytes: 12000000
toolName: listen_audio
address: 老大          # 怎么称呼用插件的人;也可用环境变量 EARS_ADDRESS 指定

# 想让它「真的听」时再加这一段;不加则只有声学层
model:
  endpoint: https://your-endpoint/v1     # OpenAI 兼容
  model: <一个支持音频输入的模型 id>
  apiKeyEnv: EARS_API_KEY                # 或直接写 apiKey(不推荐入库)
  timeoutMs: 120000
  jsonMode: true

听音模型层用的是 OpenAI 兼容的 chat/completions + input_audio 内容块。

任何支持音频输入的服务商都能用,只要它接受这个形状;本插件不绑定厂商。

用法

主模型(或你自己)调用:

listen_audio(path: "C:\\path\\to\\voice.wav", focus: "他是不是在敷衍我")
  • path:本地绝对路径或 http(s) URL;容器按魔数判断,不信扩展名。
  • mode: local 时只跑声学层(离线,秒回)。
  • 同一份音频 + 同一 focus + 同一 mode 只计算一次(有缓存)。

输出长这样(一次真实运行,节选)

音频:wav · 8.64s · 276524B
说话方式:语速偏快、音高变化平、句尾平稳、句尾明显变弱、有 3 处停顿(含明显长停顿)
说话段 4 段 / 有效语音 6.48s(占比 0.75);停顿 3 处(最长 1100ms)
音高:均值 95.4Hz,起伏 σ=9.6Hz,首段走向 下沉,句尾平稳
力度:动态范围 13.4dB,句尾明显变弱
听感印象(启发式·低置信):唤醒度中等,候选 平稳陈述
不确定:声学层不做转写……;正面/负面情绪(valence)无法仅凭声学可靠判断,本工具不猜……

它不是一句「我觉得他有点不高兴」,而是让主模型能自己下结论的证据。

输出约定(说话的人怎么被回复)

听是手段,回话才是目的。插件把这条约定写进了工具描述,装了它的 Agent 默认照做:

1. 先说听感,再说正文。 先用一两句大白话讲这段语音听着怎么样,再正常回答内容;

2. 别念参数。 不报 Hz、σ、dB 这些数字,把它们翻成「语速快、结尾收得轻」这种人话;

3. 别贴标签。 不要写「语气:」「分析如下」这类小标题——那会显得像在交作业;

4. 可以活泼。 按自己的角色说话,该贫就贫,别端着;

5. 称呼可自定义。 默认叫「老大」,在插件配置里写 address: 老板(或设环境变量 EARS_ADDRESS)就能换成任何一种叫法——别把称呼写死;

6. 简短。 一句听感 + 一句正文,别铺陈。

一句话:先让人觉得「他真听出来了」,再办事。

支持格式

容器 · 本地声学层 · 说明

wav · ✅ 内置解码 · PCM 8/16/24/32 位、float32/64、任意声道下混、EXTENSIBLE 外壳

webm / ogg (opus) · ❌ · 需先转 wav,或配置听音模型层

mp3 / m4a · ❌ · 同上

浏览器 MediaRecorder 默认出 webm/opus;要喂给本地层,先转成 wav

(例如 ffmpeg -i in.webm -ar 16000 -ac 1 out.wav)。

按住说话(输入框里的录音键)

装上之后,输入框右侧(发送键前)会多一个圆形麦克风键:

1. 按住说话,松开结束(鼠标按住 / 触屏长按 / 聚焦后按住空格或回车都行);

2. 录完自动剪掉首尾静音,重采样成 16 kHz 单声道 PCM16 WAV;

3. POST 到宿主的 /dsh-ears/wav,落盘为 ~/.dsh/voice/ear-<时间戳>.wav;

4. 草稿里插入标记 [语音 <秒数>s <时间戳>]——按下回车发出去,对话方的 Agent

就能按这个标记找回音频、调用 listen_audio 或本地转写去听。

5. 自带内容识别(0.1.8 起,0.2.0 起不写进草稿):落盘后宿主会顺手转写一次,

转出的正文写在同名侧车文件 ~/.dsh/voice/ear-<时间戳>.txt 里,

~/.dsh/voice/ears-latest.json 也带最近一条的 text——输入框里只留标记,不显示正文。

转写优先走宿主已有的 speechToText 服务(官方语音输入插件的本地 SenseVoice),

没有就退到 sherpa-onnx-node + 本机 SenseVoice 模型;两样都没有就只留标记,不编文字。

所以完整约定是:草稿里只有 [语音 Ns 时间戳];内容在 .txt,语气用 listen_audio 听 .wav。

Agent 侧看到这个标记就该先读 .txt(插件工具描述里也写明了这条)。

回放:GET /dsh-ears/wav?stamp=<时间戳>(只允许本机回环地址,录音不外传)。

这个圆键需要宿主提供 webServer;没有它的环境(纯 CLI)只是少一个键,

listen_audio 工具照常可用。

微信式聊天皮肤 + 可回放语音条(0.3.0 起)

0.3.0 把原来独立的 dsh-wechat-bubbles 并了进来:会话区底色灰、用户右侧绿气泡、

助手左侧白气泡、左右小尾巴和头像,暗色主题一起适配。音符条和录音键从此在同一个

插件里,回放和录制走同一条 /dsh-ears/wav 路由,不再跨插件依赖。

语音条:草稿里发出去的 [语音 12s 20261018-101047] 会被就地渲染成微信那样的

喇叭图标 + 时长,点一下回放那条录音(GET /dsh-ears/wav?stamp=…,只允许本机回环)。

⚠️ 如果之前单独装过 dsh-wechat-bubbles,请在插件管理里停用或卸载它:
两个都开着会重复画语音条、重复注入同一套样式。

皮肤只用会话区的稳定钩子([data-slot="conversation.chat.flow"]、

[data-chat-flow-kind="user"|"assistant-step"|"steering"]、[class*="_bubble"]、

body[data-ds-dark-theme]),不碰带哈希的类名,所以宿主换主题/改版后一般还能活。

自检

node test/selfcheck.mjs

会合成一段参数已知的音频(3 段说话、2 处停顿、一段上扬、一段下沉、句尾变弱),

再让分析器把它读回来并逐项断言——不是看输出像不像,而是构造值能不能被还原。

目录

lib/index.js     插件入口:注册工具、缓存、编排两层
lib/recorder-host.js   录音落盘/回放路由(宿主半边,可选)
lib/recorder-client.js 网页端半边:输入框「按住说话」圆键 + 微信式皮肤/语音条
lib/wav.js       最小 WAV 解码器(无依赖)
lib/hearing.js   声学分析(VAD / F0 / 停顿 / 节奏 / 力度)
lib/model.js     听音模型层(OpenAI 兼容 input_audio)
test/selfcheck.mjs  已知结构音频的还原断言

已知边界

  • 声学层的"语速"是音节率代理(浊音段内的能量峰计数),不是真正的音节识别;
  • F0 用自相关,非浊音帧不估音高,voicedFrames 太少时相关指标会标为不可靠;
  • 听音模型层需要第三方端点 —— 音频会被发往那个端点,用之前先想清楚这一点;
  • 录音键自己只负责落盘;语气分析由对话方按标记调 listen_audio 去做。

内容转写插件会顺手做一次(0.1.8 起),但没有模型/引擎时不硬编,只留标记。

  • 转写不会外传:要么走宿主本地的 speechToText 服务,要么走本机 ONNX 模型,

这条链路不联网(模型需要事先躺在 ~/.dsh/speech-to-text/sensevoice/)。

更新记录

  • 0.3.0 — 按用户要求把 dsh-wechat-bubbles 并进本插件:客户端单一入口

lib/recorder-client.js 同时负责「按住说话」圆键和微信式聊天皮肤(会话底色、

绿/白气泡、头像、暗色适配),并把 [语音 Ns 时间戳] 渲染成可点击回放的语音条。

类名前缀从 dwb- 统一改成 dser-;语音条回放复用本插件的 /dsh-ears/wav 路由。

装 0.3.0 之后请停用/卸载 dsh-wechat-bubbles。

  • 0.2.0 — 按用户要求:草稿里不再显示转写正文,只留 [语音 Ns 时间戳] 标记。

正文改写到同名侧车文件 ~/.dsh/voice/ear-<时间戳>.txt(ears-latest.json 也带 text),

工具描述里写明「看到标记先读 .txt 拿内容,再用 listen_audio 听语气」。

  • 0.1.9 — 修本地转写引擎加载:sherpa-onnx-node 的原生二进制在兄弟包

sherpa-onnx-<平台>,pnpm 提升到 profile 根后,包内相对查找会失败;改为从 profile 根解析。

另外把 SenseVoice 的 <|zh|> 特殊 token 从正文/语种里剥掉。

  • 0.1.8 — 录音键自带内容识别:宿主落盘后先试 ctx.get('speechToText')(官方语音输入

插件的本地 SenseVoice),失败退到 sherpa-onnx-node + ~/.dsh/speech-to-text/sensevoice

模型;转出文字写在草稿标记前面。新增 optionalDependencies: sherpa-onnx-node(装不上不影响

其它功能)。顺带给录音路由加保护:没有 effect/webServer 的上下文(比如测试替身)静默跳过,

不再打 scope.effect is not a function。

-

…(truncated)

Rendered from the README published on npm. Formatted only, not edited.

Versions

Published on npm::14,最后更新 2026-11-04

Links

GitHub · Homepage · npm

Same category