搜公版书或导入文件,让语音把书读给你听
30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/21 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add AIcivilization/dsh-audiobook该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · other
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 12 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件写入剪贴板(不计风险)读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 125 处,展示前 123 条)
- 读取本地文件plugin/index.js:45
try { return JSON.parse(await readFile(join(PKG_ROOT, 'package.json'), 'utf8')) } catch { return {} } - 发起网络请求plugin/client.js:46
const r = await fetch('/api-dsh-audiobook/' + path, { - 写入剪贴板plugin/client.js:58
try { await navigator.clipboard.writeText(command) } catch { - 发起网络请求plugin/client.js:160
fetch('/api-dsh-audiobook/status', { credentials: 'same-origin' }).then(r => r.json()).then(j => { if (alive) setSt(j) }).catch(() => {}) - 读取本地文件lib/asr.js:39
try { return (await stat(path)).size === asset.bytes } catch { return false } - 读取本地文件lib/asr.js:48
if (existsSync(p) && await sizeMatches(p, a)) { found[key] = p; break } - 读取本地文件lib/asr.js:57
for await (const c of createReadStream(path)) h.update(c) - 写入本地文件lib/asr.js:68
await mkdir(dir, { recursive: true }) - 发起网络请求lib/asr.js:74
const res = await fetch(origin + asset.path, { redirect: 'follow' }) - 写入本地文件lib/asr.js:77
await pipeline(Readable.fromWeb(res.body), meter, createWriteStream(part)) - 写入本地文件lib/asr.js:79
await rename(part, dest) - 删除本地文件lib/asr.js:84
await rm(part, { force: true }) - 执行外部命令来自配置/变量lib/asr.js:130
const c = spawn(process.execPath, [WORKER, JSON.stringify({ models, threads })], { - 读取本地文件lib/edge.js:50
for (const root of [edgeDir(), join(tingdongRoot(), 'edge-tts')]) if (existsSync(venvBin(root, 'edge-tts'))) return venvBin(root, 'edge-tts') - 读取本地文件lib/edge.js:51
for (const p of ['/opt/homebrew/bin/edge-tts', '/usr/local/bin/edge-tts', '/usr/bin/edge-tts']) if (existsSync(p)) return p - 执行外部命令来自配置/变量lib/edge.js:57
execFile(cmd, args, { timeout, maxBuffer: 8 << 20, env: env ? { ...process.env, ...env } : undefined }, (err, stdout, stderr) => { - 读取本地文件lib/edge.js:68
if (p.includes('/') && !existsSync(p)) continue - 发起网络请求lib/edge.js:79
const res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(180000) }) - 写入本地文件lib/edge.js:81
await mkdir(dirname(file), { recursive: true }) - 写入本地文件lib/edge.js:82
await pipeline(Readable.fromWeb(res.body), createWriteStream(file)) - 读取本地文件lib/edge.js:91
if (!existsSync(venvPy)) { - 删除本地文件递归lib/edge.js:92
await rm(edgeDir(), { recursive: true, force: true }) - 删除本地文件递归lib/edge.js:95
await rm(edgeDir(), { recursive: true, force: true }) - 删除本地文件lib/edge.js:103
await rm(getPip, { force: true }) - 读取本地文件lib/edge.js:124
let uv = [join(uvDir, exe), join(uvDir, asset.replace(/ (tar gz|zip)$/, ''), exe)].find(p => existsSync(p)) - 删除本地文件lib/edge.js:128
await rm(pkg, { force: true }) - 读取本地文件lib/edge.js:129
uv = [join(uvDir, exe), join(uvDir, asset.replace(/ (tar gz|zip)$/, ''), exe)].find(p => existsSync(p)) - 删除本地文件递归lib/edge.js:133
await rm(edgeDir(), { recursive: true, force: true }) - 发起网络请求lib/llm.js:50
const res = await fetch(base + '/chat/completions', { - 读取本地文件lib/llm.js:72
const cfg = JSON.parse(await readFile(join(root, 'config.json'), 'utf8')) - 读取本地文件lib/llm.js:73
const yaml = await readFile(join(root, 'engine.yaml'), 'utf8') - 读取密钥环境变量lib/llm.js:86
let apiKey = flags['api-key'] || process.env.AUDIOBOOK_API_KEY || '' - 发起网络请求lib/llm.js:95
const r = await fetch(baseURL.replace(/ +$/, '') + '/models', { headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}, signal: AbortSignal.tim - 读取本地文件lib/paths.js:45
return candidates.find(p => existsSync(p)) || null - 读取本地文件lib/targets.js:44
const j = JSON.parse(readFileSync(cfg, 'utf8')) - 读取本地文件lib/targets.js:45
return Object.values(j.vaults || {}).map(v => v.path).filter(p => p && existsSync(p)) - 读取本地文件lib/targets.js:53
return readdirSync(dir).map(f => /^logseq_local_(.+)\.transit$/.exec(f)).filter(Boolean) - 读取本地文件lib/targets.js:54
.map(m => m[1].replace(/ /g, '/')).filter(p => existsSync(p)) - 读取本地文件lib/targets.js:68
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) - 读取本地文件lib/targets.js:74
const patch = readFileSync(join(dir, 'cordis.patch.yml'), 'utf8') - 执行外部命令固定命令lib/targets.js:109
return new Promise((resolve, reject) => execFile('/usr/bin/osascript', ['-e', script, ...args], { timeout: 30000 }, - 写入本地文件lib/targets.js:131
await mkdir(dir, { recursive: true }) - 写入本地文件lib/targets.js:132
await writeFile(join(dir, file), markdown, 'utf8') - 写入本地文件lib/targets.js:137
await mkdir(dir, { recursive: true }) - 写入本地文件lib/targets.js:138
await writeFile(join(dir, file), markdown, 'utf8') - 读取本地文件lib/server.js:84
try { st = await stat(file) } catch { return send(res, 404, { error: '这段声音还没有合成' }) } - 读取本地文件lib/server.js:92
return createReadStream(file).pipe(res) - 读取本地文件lib/server.js:103
createReadStream(file, { start, end }).pipe(res) - 读取本地文件lib/server.js:133
return res.end(await readFile(join(o.pkgRoot, rel))) - 写入本地文件lib/server.js:210
await mkdir(uploadsDir(), { recursive: true }) - 写入本地文件lib/server.js:214
await pipeline(req, createWriteStream(tmp)) - 删除本地文件lib/server.js:217
await rm(tmp, { force: true }) - 写入本地文件lib/server.js:225
await mkdir(uploadsDir(), { recursive: true }) - 写入本地文件lib/server.js:228
await pipeline(req, createWriteStream(tmp)) - 删除本地文件lib/server.js:235
await rm(tmp, { force: true }) - 读取本地文件lib/speech.js:70
for (const ext of ['.mp3', '.wav']) if (existsSync(base + ext)) return base + ext - 写入本地文件lib/speech.js:82
await mkdir(join(base, '..'), { recursive: true }) - 写入本地文件lib/speech.js:85
await writeFile(tmp + '.wav', silenceWav()) - 写入本地文件lib/speech.js:86
await rename(tmp + '.wav', base + '.wav') - 写入本地文件lib/speech.js:92
await rename(tmp + '.mp3', base + '.mp3') - 执行外部命令来自配置/变量lib/speech.js:101
await new Promise((resolve, reject) => execFile(ff, ['-hide_banner', '-nostdin', '-y', '-i', tmp + '.wav', '-codec:a', 'libmp3lame', '-b:a', '64k', tm - 写入本地文件lib/speech.js:103
await rename(tmp + '.mp3', base + '.mp3') - 删除本地文件lib/speech.js:104
await rm(tmp + '.wav', { force: true }) - 写入本地文件lib/speech.js:107
await rename(tmp + '.wav', base + '.wav') - 删除本地文件lib/speech.js:110
await rm(tmp + '.mp3', { force: true }) - 删除本地文件lib/speech.js:111
await rm(tmp + '.wav', { force: true }) - 读取本地文件lib/speech.js:158
try { names = await readdir(dir, { withFileTypes: true }) } catch { return } - 读取本地文件lib/speech.js:162
else if (e.isFile()) { try { const s = await stat(p); files.push({ p, size: s.size, t: s.mtimeMs }) } catch { } } - 删除本地文件lib/speech.js:171
await rm(f.p, { force: true }) - 删除本地文件递归lib/speech.js:212
await rm(join(cacheDir(), id), { recursive: true, force: true }) - 读取本地文件plugin/manage.js:56
const readPkg = async () => { try { return JSON.parse(await readFile(join(o.pkgRoot, 'package.json'), 'utf8')) } catch { return {} } } - 发起网络请求plugin/manage.js:64
const res = await fetch(`${r}/${PACKAGE}/latest`, { signal: AbortSignal.timeout(6000) }) - 读取本地文件lib/importer.js:20
const st = await stat(file).catch(() => null) - 删除本地文件lib/importer.js:77
await rm(file, { force: true }) - 读取本地文件lib/store.js:15
try { return JSON.parse(await readFile(path, 'utf8')) } catch { return fallback } - 写入本地文件lib/store.js:21
await writeFile(tmp, pretty ? JSON.stringify(value, null, 1) : JSON.stringify(value)) - 写入本地文件lib/store.js:22
await rename(tmp, path) - 写入本地文件lib/store.js:57
await mkdir(bookDir(book.id), { recursive: true }) - 删除本地文件递归lib/store.js:88
await rm(bookDir(checkId(id)), { recursive: true, force: true }) - 读取本地文件lib/store.js:123
try { names = await readdir(booksDir()) } catch { return [] } - 写入本地文件lib/store.js:162
await mkdir(dataRoot(), { recursive: true }) - 执行外部命令固定命令lib/extract.js:282
execFile('/usr/bin/textutil', ['-convert', 'txt', '-stdout', '-encoding', 'UTF-8', file], { maxBuffer: 256 << 20, timeout: 120000 }, - 读取本地文件lib/extract.js:300
const buf = await readFile(file) - 读取本地文件lib/kokoro.js:31
const complete = d => existsSync(join(d, 'model.int8.onnx')) && existsSync(join(d, 'lexicon-zh.txt')) && existsSync(join(d, 'dict')) - 写入本地文件lib/kokoro.js:57
await mkdir(root, { recursive: true }) - 发起网络请求lib/kokoro.js:60
const res = await fetch(MODEL.url, { redirect: 'follow' }) - 写入本地文件lib/kokoro.js:63
await pipeline(Readable.fromWeb(res.body), meter, createWriteStream(part)) - 读取本地文件lib/kokoro.js:64
if ((await stat(part)).size !== MODEL.bytes) throw new Error('文件大小不对') - 删除本地文件递归lib/kokoro.js:66
await rm(tmpDir, { recursive: true, force: true }) - 写入本地文件lib/kokoro.js:67
await mkdir(tmpDir) - 执行外部命令固定命令lib/kokoro.js:69
const t = spawn('tar', ['-xjf', part, '-C', tmpDir], { stdio: 'ignore' }) - 删除本地文件递归lib/kokoro.js:73
await rm(ownDir(), { recursive: true, force: true }) - 写入本地文件lib/kokoro.js:74
await rename(join(tmpDir, MODEL.name), ownDir()) - 删除本地文件递归lib/kokoro.js:75
await rm(tmpDir, { recursive: true, force: true }) - 删除本地文件lib/kokoro.js:80
await rm(part, { force: true }) - 执行外部命令来自配置/变量lib/kokoro.js:105
const c = spawn(process.execPath, [WORKER, JSON.stringify({ dir, threads })], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, ELECTRON_RUN_A - 读取本地文件lib/notes.js:18
try { return JSON.parse(await readFile(path, 'utf8')) } catch { return fallback } - 写入本地文件lib/notes.js:21
await mkdir(join(path, '..'), { recursive: true }) - 写入本地文件lib/notes.js:23
await writeFile(tmp, JSON.stringify(value, null, 1)) - 写入本地文件lib/notes.js:24
await rename(tmp, path) - 发起网络请求lib/sources.js:36
const res = await fetch(url, { headers: { 'user-agent': UA }, redirect: 'follow', signal: AbortSignal.timeout(timeout) }) - 发起网络请求lib/sources.js:256
const res = await fetch(url, { headers: { 'user-agent': UA }, redirect: 'follow', signal: AbortSignal.timeout(300000) }) - 写入本地文件lib/sources.js:259
await pipeline(Readable.fromWeb(res.body), createWriteStream(file)) - 读取本地文件lib/sources.js:260
if ((await stat(file)).size < 200) throw new Error('下载到的文件是空的') - 写入本地文件lib/sources.js:269
await mkdir(uploadsDir(), { recursive: true }) - 删除本地文件lib/sources.js:279
} catch { await rm(tmp + ext, { force: true }) } - 删除本地文件lib/sources.js:290
await rm(tmp + '.epub', { force: true }) - 删除本地文件lib/sources.js:306
await rm(tmp + f.ext, { force: true }) - 删除本地文件lib/sources.js:317
await rm(tmp + '.epub', { force: true }) - 发起网络请求lib/custom.js:92
const res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(10000), headers: { accept: 'application/atom+xml, application/xml;q=0.9 - 发起网络请求lib/custom.js:115
const res = await fetch(u, { redirect: 'follow', signal: AbortSignal.timeout(300000) }) - 写入本地文件lib/custom.js:125
await mkdir(uploadsDir(), { recursive: true }) - 写入本地文件lib/custom.js:128
await pipeline(Readable.fromWeb(res.body), createWriteStream(file)) - 读取本地文件lib/custom.js:129
if ((await stat(file)).size < 100) throw new Error('下载到的文件是空的') - 删除本地文件lib/custom.js:131
await rm(file, { force: true }) - 读取本地文件lib/asr-worker.js:80
emit({ id: req.id, text: transcribe(readWav(readFileSync(req.file))) }) - 写入本地文件lib/kokoro-worker.js:49
writeFileSync(req.out, wav(a.samples, a.sampleRate)) - 发起网络请求web/panel.js:30
const r = await fetch(path, init) - 发起网络请求web/panel.js:174
const r = await fetch(`upload?name=${encodeURIComponent(file.name)}`, { - 发起网络请求web/player.js:80
.then(url => fetch(url, { credentials: 'same-origin' })) - 发起网络请求web/talk.js:119
const r = await fetch('asr', { method: 'POST', credentials: 'same-origin', headers: { 'x-audiobook': '1', 'content-type': 'audio/wav' }, body: blob }) - 发起网络请求web/talk.js:251
const r = await fetch('say', { - 写入剪贴板web/talk.js:556
try { await navigator.clipboard.writeText(r.text); b.textContent = '已复制' } catch { toast('复制不了:手动选中文字复制') }
扫描于 2026/11/1(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
把任意一本书变成有声书,就在 DeepSeek Harness(dsh)里 · 搜书听:导入一本书,在 DSH 里读给你听
README
由 DeepSeek 最新模型翻译生成搜书听 · dsh-audiobook
在 DeepSeek Harness(dsh)里把任何一本书变成有声书。
按书名、作者、角色名,或者只记得一半的情节,去公版书库(古腾堡计划、维基文库、Internet Archive、Standard Ebooks)里搜;也可以导入你自己的 EPUB、PDF、TXT、DOCX 或 Markdown 文件,然后在侧边栏里听——微软 Edge 神经网络语音、离线 Kokoro,或者设备自带的语音;语速可调;自动记住你听到哪。
搜一本书,或者导入一本书,读给你听。 书名、作者、角色名、大概的情节都能搜;声音、语速可选,自动记住听到哪。DSH 插件,也能脱离 DSH 单独用。
dsh plugin --profile desktop add dsh-audiobook
它做什么
1. 搜书:面板顶上的搜索框,输入书名、作者,或者只记得的角色名、大概情节(「主角叫贾宝玉的那本」)。在 DSH 里会先让 DSH 选的模型猜是哪几本书,再去书源里找;找到点「加入书架」,下载完直接出现在书单里。
展开完整文档(README 剩余 1 节)
搜书听 · dsh-audiobook
在 DeepSeek Harness(dsh)里把任何一本书变成有声书。
按书名、作者、角色名,或者只记得一半的情节,去公版书库(古腾堡计划、维基文库、Internet Archive、Standard Ebooks)里搜;也可以导入你自己的 EPUB、PDF、TXT、DOCX 或 Markdown 文件,然后在侧边栏里听——微软 Edge 神经网络语音、离线 Kokoro,或者设备自带的语音;语速可调;自动记住你听到哪。
搜一本书,或者导入一本书,读给你听。 书名、作者、角色名、大概的情节都能搜;声音、语速可选,自动记住听到哪。DSH 插件,也能脱离 DSH 单独用。
dsh plugin --profile desktop add dsh-audiobook
它做什么
1. 搜书:面板顶上的搜索框,输入书名、作者,或者只记得的角色名、大概情节(「主角叫贾宝玉的那本」)。在 DSH 里会先让 DSH 选的模型猜是哪几本书,再去书源里找;找到点「加入书架」,下载完直接出现在书单里。
| 书源 | 有什么 |
| 古腾堡计划 | 7 万多本公版书,英文为主,也有《紅樓夢》《西遊記》等中文古籍(多为繁体) |
| 中文维基文库 | 中文公版古籍、民国作品(鲁迅等),整部作品导出成 EPUB(大书要一两分钟) |
| 英文维基文库 | 英文公版作品 |
| Internet Archive | 只收标明公有领域或 CC 授权、而且有文字版的书;有不少中文旧书。扫描版的文字是识别出来的,可能有错字,结果里会标出来 |
| Standard Ebooks | 精校排版的英文公版书 |
只接这些公版和授权免费的书源。受版权保护的现代书(比如《围城》)搜不到,AI 猜到了也会照实说「书源里没有」:请买正版电子书后导入。
常规网页搜索:同时用 DSH 自己的网页搜索(就是「插件 → 网页搜索」里配的那个)搜一遍,结果是链接,点「打开」在浏览器里看;网页上下载到的书,用「导入」加进书架。
去别处搜 / 自定义搜索地址:搜索框下面一排链接(默认百度、必应、Google、豆瓣读书、微信读书),点一下在浏览器里打开那个网站的搜索结果。「管理搜索地址」里可以增删改,地址里用 {q} 代表要搜的词。地址类型选「OPDS 书库」时(Calibre 内容服务器、calibre-web、Kavita 等自建书库,比如 http://地址:8080/opds/search/{q}),结果直接合进上面的书源结果,可以一键加入书架;只会从这个书库自己的地址下载。
书是 DSH 所在的机器去下载的:DSH 装在海外 VPS 上时,在中国大陆用手机打开面板,维基文库也能用;DSH 装在大陆、又不能访问外网的电脑上,维基文库连不上,只剩古腾堡。
2. 导入:把文件拖进面板、选文件,或者填本机路径。支持的格式:
| 格式 | 怎么读 |
| TXT | 自动认编码:UTF-8、UTF-16、GBK / GB18030、Big5 |
| EPUB | 按书脊顺序,目录里的标题当章名(DRM 加密的读不了) |
| PDF | 取文字层,把版面折行接回段落(扫描版没有文字层,读不了) |
| DOCX | 标题样式当章节 |
| Markdown / HTML | # 标题、 当章节 |
| RTF / DOC | 用 macOS 自带的 textutil 转换(只在 Mac 上) |
纯文本按「第X章」「Chapter N」「序章」「尾声」这类标题分章;认不出标题的每约 8000 字分一个「第 N 部分」,方便跳转。
3. 朗读(右侧栏「搜书听」页签,或手机浏览器直接打开):正文按约 20–30 秒一段切开,读到哪段高亮到哪段,点任意一段从那里读。
| 朗读方式 | 说明 |
| 浏览器朗读 | 不用安装,用这台设备自带的语音,文字不出这台设备。Mac 上在系统设置里下载「高级 / 增强」中文语音后会排在最前。手机锁屏后可能会停 |
| 微软 Edge | 最自然。云希、晓晓、云健等 12 个中文声音(含台湾、粤语、方言),另有 6 个英文声音。调用开源工具 edge-tts,插件启动时自动装好,装在插件自己的目录里,不用管理员权限:有 Python 3 就用它(没有 venv / pip 也能装),连 Python 都没有就自动下载一份。要联网,文字会发到微软的服务器;这不是微软公开的正式接口,可能失效 |
| 本地离线 | Kokoro 中文模型,55 个女声、45 个男声,完全离线。第一次用时下载约 147 MB 模型。比 Edge 生硬,合成也慢:开播的第一段要等十几秒,之后一边播一边提前合成 |
已经装了「听懂」的话,它下好的 Kokoro 中文模型和装好的 Edge 语音组件直接借用,不用再下。
4. 语速:0.75× 到 2.5×。Edge 和本地语音由播放器加速,音调不变,换语速不用重新合成。
5. 进度:每读一段存一次,存在服务器上和本机。关掉面板、换设备打开,都从上次的地方接着读。
6. 书单:每本书右边的「⋯」可以改书名和作者、从头开始、删除。
7. 其他:目录跳章、全书进度条拖动、定时停止(15 分钟到 1.5 小时,或读完本章停),锁屏界面和耳机线控(播放 / 暂停 / 上一段 / 下一段)。
电脑快捷键:空格 播放 / 暂停 · ← / → 上一段 / 下一段
在 DSH 对话里
插件给 AI 注册了 6 个工具,可以直接说:
- 「帮我找那本主角叫贾宝玉的书来听」:AI 自己判断书名,用 audiobook_search 搜,audiobook_download 下进书架
- 「把 ~/Downloads/三体.epub 导入搜书听」:audiobook_import
- 「我在听哪些书?听到哪了?」:audiobook_books
- 「刚才那段讲了什么?」「阿青第一次出场在哪?」「把第三章讲给我听」:audiobook_text,可以取刚听过的内容、在全书里找词、按章取正文
- 「跳到第十章」「跳到阿青出场的地方」:audiobook_goto。面板没在播放时几秒内就会跟上新位置
更新 / 卸载:在 DSH 的「设置 → 搜书听」里。卸载只移除插件,书和收听进度留在 ~/.dsh/dsh-audiobook。
安装
装进 DSH(把 desktop 换成你的 profile 名;先完全退出 DSH):
dsh plugin --profile desktop add dsh-audiobook
重新打开 DSH,在对话页打开右侧栏,选「搜书听 · 读书给你听」。VPS 上的 DSH 一样装(以运行 DSH 的用户执行,再重启 DSH),手机浏览器打开 https://<你的域名>/api-dsh-audiobook/panel 就能听,点「选择文件」直接从手机里挑书(文件只传一次,服务器取出文字后就删掉原文件)。VPS 上建议用微软 Edge 或浏览器朗读:本地离线语音很吃 CPU,小 VPS 跟不上。前面有 nginx 的话,把 client_max_body_size 调大(默认只放行 1 MB)。
微软 Edge 语音组件会在插件启动后自动在后台装好(几秒到一分钟)。
ffmpeg 可选:有就把本地语音压成 MP3(隔着 VPS 用手机听更流畅),没有就直接给 WAV。
可选配置
在 profile 的 cordis.patch.yml 里:
- id: dsh-audiobook
config:
provider: dsh-model-workbuddy-ai # 搜书时猜书名用的模型;不填就用 DSH 的默认模型 model: workbuddy-ai/deepseek-v4.1-flash-sg edge: false # 不要自动安装 Edge 语音组件 ffmpeg: /opt/homebrew/bin/ffmpeg # 找不到 ffmpeg 时填 threads: 2 # 每个本地语音进程的线程数 workers: 2 # 同时开几个本地语音进程(每个占 300–400 MB 内存;VPS 内存小就设 1)
不开 DSH 也能用
npx dsh-audiobook import ~/Downloads/书.epub # 导入
npx dsh-audiobook serve # 打开 http://127.0.0.1:4811
npx dsh-audiobook list
数据
都在 ~/.dsh/dsh-audiobook/:
- books/<书>/book.json:书名、作者、目录
- books/<书>/text.json:切好的朗读段
- books/<书>/progress.json:听到第几段
- settings.json:朗读方式、声音、语速
- edge-tts/:Edge 语音组件(Python 虚拟环境);没有 Python 的机器上还有 uv/、python/
- cache/:合成好的声音,超过 500 MB 自动删最久没用的,可以随时整个删掉
同一本书换个位置再导入,还是同一本(编号按内容算),进度接得上。DSH 插件和命令行版共用这份数据。
开发
npm install
npm test
- lib/extract.js 各种格式取文字;lib/zip.js 读 EPUB / DOCX 用的最小 ZIP 解包
- lib/segment.js 分章、分句、切朗读段
- lib/speech.js 服务器端合成:缓存、排队、预取(正在听的那段插队)
- lib/sources.js 书源(古腾堡、维基文库)的搜索、下载和清理;lib/finder.js 搜书(让模型按角色名、情节猜书名);lib/llm.js 调 DSH 的模型
- lib/edge.js Edge 语音组件(edge-tts)的自动安装和调用;lib/kokoro.js + lib/kokoro-worker.js 本地中文语音的常驻子进程
- lib/store.js 书库、进度、设置(原子写入的 JSON);lib/server.js 面板接口
- plugin/ DSH 宿主端(路由、AI 工具、更新卸载)和界面入口(右侧栏、设置);路由先过 DSH 的 connection.requestRejection() 登录校验
- web/ 朗读面板(原生 JS 模块,无构建):panel.js 书库和朗读页,player.js 一段接一段地读(始终用同一个 ,下一段提前下载进内存:段间几乎不停顿,iPhone 锁屏后也能接着读)预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
AIcivilization/dsh-audiobook
搜公版书或导入文件,让语音把书读给你听
相关插件 · 其他
模型破甲清洗YuJunZhiXue/dsh-purge
⚔️ DeepSeek Harness 网络安全红队工具dsh-purge
★ 14,822已汉化DeepSeek 客户端
73
逆向分析桥接Fanqi89/dsh-rea
REA(Reverse Engineer Anything)MCP 桥接,用来把 REA 逆向工具接入 DSH 插件
★ 51已汉化DSH
72
红队越狱测试套件Minglink/dsh-infinite-gen-5
DeepSeek v4.1 flash 逆向工具无限五代 求 Star 收藏 ⭐欢迎大家提交项目的改进
★ 3,759已汉化DSH
71.4
语音女友beiyege-01/dsh-voice-ai-girlfriend
语音 AI 女友Voice AI girlfriend for DeepSeek Harness:Whisper…
★ 718已汉化DSH
71.2
协同办公空间dream-num/univer-workspace
一个让人类与 AI Agent 共同创作、协作和审阅的开源 Office 工作空间。
★ 2,261已汉化DSH
70.7
破甲评测WoYun520/dsh-purge
在本机分离宿主拦截与模型拒绝,做受控红队评测
★ 199已汉化DSH
69.7