Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 1.3.0

- Added Deepgram Nova-3 as an optional recognition provider for US English dictation. Users supply their own API key; interim and final transcripts appear in the existing voice-input flow. The key is stored separately with owner-only permissions.
- Kept Doubao web-account recognition and the official Volcengine Seed ASR 2.0 API available through the same recognition-service settings.
- Allowed another key to finish an Fn-triggered dictation session.
- Fixed the Debian offline runtime dependency installation path and refreshed the English and Chinese README screenshots and setup guidance.

## 1.2.0

- Added optional Vibekey receiver support with authenticated heartbeat and hot-plug
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ For marketplace preparation, use `make marketplace-check` with Gitleaks on PATH
and follow [the publishing checklist](packaging/MARKETPLACE.md). Reports and local
review drafts belong in ignored `artifacts/` and `docs/`, not in release payloads.

The current public version is `1.2.0`. Keep `pyproject.toml`, `manifest.json` and
The current public version is `1.3.0`. Keep `pyproject.toml`, `manifest.json` and
`src/doubao_input/product.py` aligned; `python packaging/version_check.py` verifies
them. A pushed tag such as `v1.2.0` runs all checks and secret scanning, builds app
them. A pushed tag such as `v1.3.0` runs all checks and secret scanning, builds app
and plugin archives for every supported Python version, creates `SHA256SUMS`, and
publishes the GitHub Release. `workflow_dispatch` performs a non-publishing build.
# Bounded desktop tests
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ validate package metadata and scan for secrets. The current whole-package branch
coverage floor is 55%; desktop UI, WebKit and real-device paths remain included
in the denominator.

The current release version is **1.2.0**. A `v1.2.0` tag must match every embedded
The current release version is **1.3.0**. A `v1.3.0` tag must match every embedded
version before CI can publish. Tag releases rebuild both offline app and Omarchy
plugin archives for Python 3.11–3.14 and attach SHA-256 checksums. A manually started
release workflow builds artifacts for inspection but does not publish them. Real
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ timeout --kill-after=5s 50s env PYTHONPATH=src python3 tests/manual/x11.py --run
GTK 测试、代码检查、编译、包元数据校验和密钥扫描。覆盖率按整个源码包的分支覆盖率
计算,当前真实门槛为 55%;GTK 界面、WebKit 和实体设备路径没有从分母中排除。

当前发布版本为 **1.2.0**。只有 `v1.2.0` 标签与所有内置版本完全一致时,CI 才能发布。
当前发布版本为 **1.3.0**。只有 `v1.3.0` 标签与所有内置版本完全一致时,CI 才能发布。
标签发布会为 Python 3.11–3.14 分别构建独立应用与 Omarchy 插件离线包,并附带 SHA-256
校验文件。手动启动发布工作流只构建供检查的产物,不会公开发布。真实登录、麦克风、
全局按键及桌面行为仍需按人工验收清单检查。
Expand Down
88 changes: 16 additions & 72 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,87 +1,31 @@
# Doubao Say 1.2.0
# Doubao Say 1.3.0

豆包说 1.2.0 补齐了硬件控制、设备恢复、语音润色和公开验收报告。下面先列本次新增,再汇总当前版本的完整能力,便于直接用于对外介绍。
豆包说 1.3.0 新增 Deepgram Nova-3 英语语音识别。现在可以在同一应用中选择豆包账号、火山引擎官方 API,或 Deepgram API。

## 1.2.0 新增
## 本次新增

### Vibekey 六个控件都能自定义
### Deepgram Nova-3 英语听写

Vibekey 支持默认关闭,普通键盘用户不会多出扫描和监听开销。启用后,三个按键默认对应录音、确认和取消;旋钮右转、左转和按下默认对应下方向键、上方向键和 Meta+Backspace。六个控件都可以重新录制成任意键盘快捷键。
在「设置 → 语音识别服务」中选择 **Deepgram Nova-3(英语)**,填写自己的 API Key,并先使用「测试 API Key」检查连接。应用会在录音期间流式发送音频,显示中间识别结果,并在录音结束后取得最终文字。当前固定使用美式英语;其他语言和多语种模式尚未开放。Deepgram 按你的账号计费。

接收器现在会完成认证并维持心跳,拔插后可以自动恢复。安装包附带只匹配该设备 VID/PID 的 udev 规则,不新增软件依赖。
API Key 单独保存在本机,仅当前用户可读,不进入设置文件、诊断信息、日志或安装包。切换识别服务时,豆包网页登录与火山引擎 API 仍可继续使用。

### 麦克风和键盘断开后可以恢复
### 输入与安装改进

PipeWire 意外结束时,应用会停止当前录音,不再让悬浮窗卡在录音状态。某个 evdev 设备断开只会移除该设备,其余键盘继续工作;旧录音留下的回调也不会影响新的录音。
- 使用 Fn 听写时,可以用另一个按键结束录音,减少按键操作上的限制。
- 修正 Debian 安装包的离线依赖安装路径;独立应用和 Omarchy 插件的 GitHub Release 压缩包仍是主要分发方式。
- README 增加中英文实际界面截图和三种识别方式的设置说明。

设置页把麦克风选择移到了更靠前的位置。每次打开设置都会重新扫描设备,也可以手动点击“刷新设备”。已经保存但暂时离线的麦克风仍会保留,不会悄悄切换到别的输入源。
## 获取与升级

### 语音润色的状态更清楚
本 Release 提供适配 Python 3.11–3.14 的独立应用与 Omarchy 插件压缩包,并附有 `SHA256SUMS`。按 CPU 架构和 Python 版本选择对应文件,运行包内 `./install.sh`;设置与登录信息会保留。完整步骤见 [安装指南](https://doubao-say.lifeos.md/zh/guide/install)。

润色状态从转写正文中独立出来,悬浮窗会显示单独的状态栏和轻微闪烁的星星。等待三秒后会提示再次按快捷键可立即使用原文;开启“减少动态效果”后,星星保持静止。

模型策略也更明确。官方 DeepSeek 和适配的 Gemini 配置会请求关闭思考;智谱标准 API 与 Coding Plan 在支持时同样关闭思考。GLM-5.3 和 GLM-5.3-Flash 不接受关闭思考参数,因此改用低推理强度。润色仍然遵守五秒总时限,超时就使用原始转写,不会卡住输入。

### 首次引导更利落

上一步和下一步改成了更大的箭头按钮,点击区域保持不变,并保留中英文提示。窄窗口和桌面平铺场景下更容易看清当前操作。

### CI 报告可以直接检查每个用例

Ubuntu 22.04 与 Omarchy 4.0.3 现在运行同一套 Midscene Test 桌面用例。GitHub Actions Summary 会按用例显示结果、真实节点截图,以及对应的 AI 说明或错误;失败项可以直接跳到第一个失败节点。完整 HTML 报告支持回放,并保留最近 30 次历史记录。即使部分用例失败,汇总表仍会生成。

测试已经拆成并行分片,产品用例和 Omarchy shell 验证可以同时执行。取消旧运行后,报告任务也会及时停止,避免继续消耗模型额度。

### Marketplace 分发边界更安全

安装目录不再包含会被编码代理自动加载的仓库级指令文件。发布前检查会拒绝 `AGENTS.md`、`CLAUDE.md`、Copilot instructions 等入口,防止第三方插件目录改变代理的工作区规则。

## 当前版本完整能力

### 两种语音识别模式

- **豆包账号模式**:通过独立网页窗口登录豆包,沿用现有账号完成语音识别。
- **火山引擎官方 API 模式**:使用自己的 Volcengine Seed ASR 2.0 API Key,可在设置页保存并测试连接。音频只在录音期间发送到所选服务,用量由火山引擎按用户账号计费。

两种模式都集成在同一个首次引导和设置页中,可以随时切换。API Key 使用仅当前用户可读的独立文件保存,不写入普通设置和日志。

### OpenAI 兼容语音润色

可以填写 Base URL、API Key、模型,以及独立的中英文 Prompt。应用会根据转写的主要语言自动选择 Prompt,在停顿时流式预览润色结果。继续说话会让旧请求失效;最终请求失败或超过五秒时,会安全使用原始转写。

### Wayland 与原生 X11 输入

支持短按开始或结束、长按说话松开结束,以及双击发送 Enter。Wayland 可以选择剪贴板粘贴或通过 `wtype` 直接输入;原生 X11 支持带目标窗口和焦点保护的自动粘贴。识别成功但无法确认目标窗口时,文字会保留,供用户复制或重试。

### 应用与 Omarchy 插件两种安装方式

Release 同时提供独立应用包和 Omarchy 插件包,并为 Python 3.11、3.12、3.13、3.14 构建离线依赖。统一安装器会先列出缺少的系统包并征求确认。应用会检查 GitHub 上的最新稳定版,但只提示更新,不会自动下载或安装。

## 升级

Git 安装的 Omarchy 插件请先结束录音并停用插件,再执行:

```sh
omarchy plugin update md.lifeos.doubao-say
cd "${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/plugins/md.lifeos.doubao-say"
./install.sh
omarchy plugin enable md.lifeos.doubao-say
```

使用 Release 压缩包安装的用户,请下载与 Python 版本和 CPU 架构匹配的新包,在解压目录运行 `./install.sh` 覆盖安装。设置和登录信息会保留。

豆包说是非官方客户端,不代表豆包或字节跳动的认可或背书。

---
豆包说是非官方客户端,不代表豆包、字节跳动或 Deepgram 的认可或背书。

## English highlights

Doubao Say 1.2.0 adds complete opt-in Vibekey support, cleaner recovery from microphone and keyboard disconnects, live microphone refresh, a dedicated polishing status presentation, provider-aware low-latency reasoning controls, and simpler onboarding navigation.

The current release supports two recognition backends: the default Doubao web-account flow and an optional official Volcengine Seed ASR 2.0 API mode using the user's own key. It also includes OpenAI-compatible voice polishing with bilingual prompts, streaming previews, a five-second original-text fallback, Wayland and native X11 text delivery, and separate standalone-app and Omarchy-plugin archives.

CI now runs shared Midscene Test desktop scenarios on Ubuntu 22.04 and Omarchy 4.0.3. Each case publishes its real node screenshot, AI explanation or error, direct failure link, replayable HTML report, and retained history. Marketplace publication checks also reject files that coding agents automatically load as workspace instructions.
Doubao Say 1.3.0 adds **Deepgram Nova-3 for US English dictation** alongside Doubao web-account recognition and the official Volcengine Seed ASR 2.0 API. Choose a recognition service in Settings, enter your own API key for an API provider, and test it before dictating. Deepgram receives audio only while recording and bills usage to your account. Its key stays in a separate owner-readable local file.

Doubao Say is an unofficial client and is not endorsed by or affiliated with Doubao or ByteDance.
The release also improves Fn-key dictation completion, fixes the Debian offline dependency path, and updates the bilingual README screenshots and setup instructions. App and Omarchy-plugin archives are built for Python 3.11–3.14 with SHA-256 checksums.

See the full comparison: https://github.com/quanru/doubao-say/compare/v1.1.0...v1.2.0
See the full comparison: https://github.com/quanru/doubao-say/compare/v1.2.0...v1.3.0
7 changes: 7 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
doubao-say (1.3.0-1) noble; urgency=medium

* Add optional Deepgram Nova-3 English speech recognition.
* Keep Doubao and Volcengine recognition selectable in Settings.

-- doubao-say contributors <code-dream@qq.com> Sat, 26 Sep 2026 10:00:00 +0800

doubao-say (1.2.0-1) noble; urgency=medium

* Install Python runtime dependencies through apt, with no pip invocation
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"id": "md.lifeos.doubao-say",
"name": "Doubao Say",
"version": "1.2.0",
"version": "1.3.0",
"author": "quanru and contributors",
"description": "Doubao Say turns speech into polished text with live captions and configurable tap or hold triggers on Wayland.",
"kinds": [
Expand Down
2 changes: 1 addition & 1 deletion packaging/INSTALL.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Doubao Say 1.2.0
# Doubao Say 1.3.0

[Website](https://doubao-say.lifeos.md/) · [简体中文安装指南](https://doubao-say.lifeos.md/zh/guide/install)

Expand Down
2 changes: 1 addition & 1 deletion packaging/build-deb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# python3-installer
#
# Usage:
# ./packaging/build-deb.sh # builds ../doubao-say_1.2.0-1_all.deb
# ./packaging/build-deb.sh # builds ../doubao-say_1.3.0-1_all.deb
# ./packaging/build-deb.sh --install # also installs it locally
set -e

Expand Down
2 changes: 1 addition & 1 deletion packaging/runtime-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Complete Python dependency set tested for the 1.2.0 release.
# Complete Python dependency set tested for the 1.3.0 release.
websockets==17.1
sounddevice==0.5.6
evdev==2.0.0
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "doubao-say"
version = "1.2.0"
version = "1.3.0"
description = "Global push-to-hold voice input for Linux/Wayland, backed by Doubao ASR"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion src/doubao_input/product.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Single runtime source for Doubao Say release identity."""

VERSION = "1.2.0"
VERSION = "1.3.0"
REPOSITORY_URL = "https://github.com/quanru/doubao-say"
LATEST_RELEASE_API = "https://api.github.com/repos/quanru/doubao-say/releases/latest"
2 changes: 1 addition & 1 deletion tests/contracts/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_release_workflow_publishes_curated_product_notes(self):
notes = (ROOT / "RELEASE_NOTES.md").read_text()
self.assertIn("--notes-file RELEASE_NOTES.md", workflow)
self.assertNotIn("--generate-notes", workflow)
for expected in ("Doubao Say 1.2.0", "Volcengine Seed ASR 2.0", "Vibekey", "Midscene Test"):
for expected in ("Doubao Say 1.3.0", "Volcengine Seed ASR 2.0", "Deepgram Nova-3"):
self.assertIn(expected, notes)

def test_both_archives_exclude_ignored_source_files(self):
Expand Down
6 changes: 3 additions & 3 deletions tests/contracts/test_version_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@

class VersionCheckTests(unittest.TestCase):
def test_sources_are_aligned_at_current_release(self):
self.assertEqual(VERSION_CHECK.validate(root=ROOT), "1.2.0")
self.assertEqual(VERSION_CHECK.validate(root=ROOT), "1.3.0")

def test_exact_release_tag_is_accepted(self):
self.assertEqual(VERSION_CHECK.validate("v1.2.0", ROOT), "1.2.0")
self.assertEqual(VERSION_CHECK.validate("v1.3.0", ROOT), "1.3.0")

def test_wrong_release_tag_is_rejected(self):
with self.assertRaisesRegex(ValueError, "must be 'v1.2.0'"):
with self.assertRaisesRegex(ValueError, "must be 'v1.3.0'"):
VERSION_CHECK.validate("v1.1.0", ROOT)


Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_diagnostics_do_not_include_device_or_custom_secrets(self):
self.assertNotIn("private-device-identifier", data)
self.assertNotIn("cookies", data)
self.assertNotIn("transcript", data)
self.assertEqual(json.loads(data)["product_version"], "1.2.0")
self.assertEqual(json.loads(data)["product_version"], "1.3.0")
self.assertEqual(json.loads(data)["recognition_provider"], "volcengine")
self.assertEqual(json.loads(data)["recent_stages"], [
{"stage": "first_result", "after_ms": 125}])
Expand Down
32 changes: 16 additions & 16 deletions tests/unit/test_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ def read(self, _limit):

class UpdateTest(TestCase):
def test_semantic_version_comparison_does_not_compare_strings(self):
self.assertTrue(newer_release("v1.10.0", "1.2.0"))
self.assertFalse(newer_release("1.2.0", "1.2.0"))
self.assertFalse(newer_release("1.1.9", "1.2.0"))
self.assertTrue(newer_release("v1.10.0", "1.3.0"))
self.assertFalse(newer_release("1.3.0", "1.3.0"))
self.assertFalse(newer_release("1.1.9", "1.3.0"))

def test_only_new_stable_release_produces_allowlisted_url(self):
self.assertEqual(release_info({"tag_name": "v1.3.0"}),
UpdateInfo("1.3.0", "https://github.com/quanru/doubao-say/releases/tag/v1.3.0"))
self.assertIsNone(release_info({"tag_name": "v1.3.0", "prerelease": True}))
self.assertEqual(release_info({"tag_name": "v1.4.0"}),
UpdateInfo("1.4.0", "https://github.com/quanru/doubao-say/releases/tag/v1.4.0"))
self.assertIsNone(release_info({"tag_name": "v1.4.0", "prerelease": True}))
with self.assertRaises(ValueError):
release_info({"tag_name": "https://malicious.example"})

Expand All @@ -37,15 +37,15 @@ def test_successful_check_is_cached_and_delivered_on_main_dispatch(self):
with tempfile.TemporaryDirectory() as root, patch.dict(
os.environ, {"XDG_CONFIG_HOME": root}), patch(
"doubao_input.updates.request.urlopen",
return_value=Response({"tag_name": "v1.3.0"})):
return_value=Response({"tag_name": "v1.4.0"})):
checker = UpdateChecker(callbacks.append, found.append, clock=lambda: 1000)
checker._fetch()
self.assertEqual(found, [])
callbacks.pop()()
self.assertEqual(found[0].version, "1.3.0")
self.assertEqual(found[0].version, "1.4.0")
cache = json.loads(checker.cache_path().read_text())
self.assertEqual(cache["checked_at"], 1000)
self.assertEqual(cache["current_version"], "1.2.0")
self.assertEqual(cache["current_version"], "1.3.0")
self.assertEqual(checker.cache_path().stat().st_mode & 0o777, 0o600)

def test_fresh_cache_avoids_network_and_repeats_notification(self):
Expand All @@ -55,19 +55,19 @@ def test_fresh_cache_avoids_network_and_repeats_notification(self):
checker = UpdateChecker(callbacks.append, found.append, clock=lambda: 1000)
checker.cache_path().parent.mkdir(parents=True)
checker.cache_path().write_text(json.dumps({
"checked_at": 999, "tag_name": "v1.3.0",
"current_version": "1.2.0",
"checked_at": 999, "tag_name": "v1.4.0",
"current_version": "1.3.0",
"draft": False, "prerelease": False}))
with patch.object(checker, "_fetch") as fetch:
checker.check()
fetch.assert_not_called()
callbacks.pop()()
self.assertEqual(found[0].version, "1.3.0")
self.assertEqual(found[0].version, "1.4.0")

def test_close_suppresses_queued_notification(self):
callbacks, found = [], []
checker = UpdateChecker(callbacks.append, found.append)
checker._deliver(UpdateInfo("1.3.0", "https://example.test"))
checker._deliver(UpdateInfo("1.4.0", "https://example.test"))
checker.close()
callbacks.pop()()
self.assertEqual(found, [])
Expand All @@ -81,7 +81,7 @@ def test_failed_check_is_cached_to_avoid_repeated_requests(self):
checker._fetch()
cache = json.loads(checker.cache_path().read_text())
self.assertEqual(cache["checked_at"], 1000)
self.assertEqual(cache["tag_name"], "1.2.0")
self.assertEqual(cache["tag_name"], "1.3.0")

def test_offline_check_preserves_previous_update(self):
found = []
Expand All @@ -90,9 +90,9 @@ def test_offline_check_preserves_previous_update(self):
"doubao_input.updates.request.urlopen", side_effect=URLError("offline")):
checker = UpdateChecker(lambda callback: callback(), found.append,
clock=lambda: 100000)
checker._save_cache("v1.3.0")
checker._save_cache("v1.4.0")
checker._fetch()
self.assertEqual(found[0].version, "1.3.0")
self.assertEqual(found[0].version, "1.4.0")

def test_cache_from_another_installed_version_is_ignored(self):
callbacks, found = [], []
Expand Down
Loading