11 Commits
Author SHA1 Message Date
MeloriseandCodex 3e04e30b24 fix: 适配新版千问录音状态并升级至 1.0.3
AI-assisted-by: Codex
AI-model: gpt-5.6-sol
AI-reasoning-effort: medium
Co-authored-by: Codex <codex@openai.com>
2026-10-07 00:37:24 +08:00
github-actions[bot] 5b55b0eaf9 build: 更新 flake.lock 2026-09-29 21:24:38 +00:00
github-actions[bot] 18ed2eb43a build: 更新 flake.lock 2026-09-22 20:12:04 +00:00
github-actions[bot] 58be580578 build: 更新 flake.lock 2026-09-15 20:15:52 +00:00
github-actions[bot] 6c940939b1 build: 更新 flake.lock 2026-09-08 19:55:01 +00:00
MeloriseandCodex fd3ab43b94 fix: Nix 包固定 Electron 42
将 Nix 包运行时固定到 Electron 42,避免默认 Electron 43 的 KDE StatusNotifierItem 兼容问题。

AI-assisted-by: Codex
AI-model: gpt-5.6-sol
AI-reasoning-effort: medium
Co-authored-by: Codex <codex@openai.com>
2026-09-08 00:24:32 +08:00
MeloriseandCodex 6b0a9716fa fix: 强制 Nix 包使用 XWayland
AI-assisted-by: Codex
AI-model: gpt-5.6-sol
AI-reasoning-effort: medium
Co-authored-by: Codex <codex@openai.com>
2026-09-07 10:13:49 +08:00
Melorise 3a5b5d84ff Merge pull request #1 from Melorise/dev-1.0.2
Dev 1.0.2
2026-09-04 22:23:32 +08:00
MeloriseandCodex 857953dd1f chore: 更新版本至 1.0.2
同步应用、Fcitx5 addon、AppStream、发行版打包元数据、维护约定和更新日志。

AI-assisted-by: Codex
AI-model: gpt-5.6-sol
AI-reasoning-effort: medium
Co-authored-by: Codex <codex@openai.com>
2026-09-04 17:36:03 +08:00
MeloriseandCodex 29809d9cd3 fix: 避免快速停止重新开启网页录音
停止请求早于网页录音状态落地时,等待实际进入录音后再点击一次停止;网页仍在录音或尚未点击停止时不再完成外部会话。

AI-assisted-by: Codex
AI-model: gpt-5.6-sol
AI-reasoning-effort: medium
Co-authored-by: Codex <codex@openai.com>
2026-09-04 17:32:12 +08:00
github-actions[bot] 34f682b53d build: 更新 flake.lock 2026-09-01 19:57:15 +00:00
23 changed files with 123 additions and 32 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
## 项目范围
TamaASR 是仅面向 Linux 的 Fcitx5 桌面语音输入工具。项目名为 `TamaASR`,分发包名为 `tama-asr`,当前版本为 `1.0.0`。
TamaASR 是仅面向 Linux 的 Fcitx5 桌面语音输入工具。项目名为 `TamaASR`,分发包名为 `tama-asr`,当前版本为 `1.0.3`。
不得恢复 Windows、PowerShell 或 Tauri 输出路径。发行版差异只能存在于依赖说明和打包配置,不能进入语音输入核心逻辑。
+13
View File
@@ -2,6 +2,19 @@
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 的格式。
## [1.0.3] - 2026-10-07
### 修复
- 适配新版千问网页的录音状态标记,修复松开快捷键后未点击停止按钮、悬浮球持续停留在黄色终止状态的问题。
- Nix 包启动时显式使用 XWayland,与其他发行版的启动参数保持一致。
## [1.0.2] - 2026-09-04
### 修复
- 修复快速松开快捷键时,网页尚未完成异步启动便再次点击麦克风,导致外部会话已结束但网页仍在录音的问题。
## [1.0.1] - 2026-09-01
### 变更
+1 -1
View File
@@ -2,7 +2,7 @@
## 项目范围
TamaASR 是仅面向 Linux 的 Fcitx5 桌面语音输入工具。项目名为 `TamaASR`,分发包名为 `tama-asr`,当前版本为 `1.0.0`。
TamaASR 是仅面向 Linux 的 Fcitx5 桌面语音输入工具。项目名为 `TamaASR`,分发包名为 `tama-asr`,当前版本为 `1.0.3`。
不得恢复 Windows、PowerShell 或 Tauri 输出路径。发行版差异只能存在于依赖说明和打包配置,不能进入语音输入核心逻辑。
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1788107362,
"narHash": "sha256-pKxqcA71wmW6eMh3kFpW5NHIdbrgYOhtW2aOwqQ28GQ=",
"lastModified": 1790707128,
"narHash": "sha256-IB1Qi718DOScdq/skqHOKIApsAznqa8TkdaWX/MGh4w=",
"owner": "Melorise",
"repo": "nixpkgs",
"rev": "e1c33116d631fcc7987a1a6b298622523ce1d66a",
"rev": "521f999a323ecd2bee19a0e93d133c27712cbee1",
"type": "github"
},
"original": {
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.16)
project(tama-asr-fcitx5 VERSION 1.0.1 LANGUAGES CXX)
project(tama-asr-fcitx5 VERSION 1.0.3 LANGUAGES CXX)
include(GNUInstallDirs)
include(CTest)
+1 -1
View File
@@ -6,7 +6,7 @@ Comment[zh_CN]=将 TamaASR 网页后端连接到当前输入上下文
Type=SharedLibrary
Library=@TAMA_ASR_LIBRARY_PATH@
Category=Module
Version=1.0.1
Version=1.0.3
OnDemand=False
Configurable=False
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "tama-asr",
"productName": "TamaASR",
"version": "1.0.1",
"version": "1.0.3",
"private": true,
"packageManager": "pnpm@11.21.0",
"author": "TamaASR contributors",
+3 -3
View File
@@ -1,13 +1,13 @@
# Linux 分发打包
TamaASR 1.0.1 的系统包由两部分组成:
TamaASR 1.0.3 的系统包由两部分组成:
- Electron 应用安装到 `/opt/tama-asr`,命令入口为 `/usr/bin/tama-asr`;
- Fcitx5 常驻 addon 由目标发行版中的 CMake/Fcitx5 开发包编译,并安装到该环境报告的 addon 目录。
安装包同时提供桌面入口、项目图标和 `/etc/xdg/autostart/tama-asr.desktop`,因此安装后的默认行为是随桌面会话启动。设置页关闭自启动时,应用需要在用户级 autostart 目录写入覆盖项,不能删除系统包所属文件。
`tama-asr` 1.0.1 的 deb、RPM 和 Arch 元数据均声明替换旧 `meloasr` 包;通过对应发行版的包管理器安装新包时,旧应用、Fcitx5 addon 和桌面文件会一并卸载。
`tama-asr` 1.0.3 的 deb、RPM 和 Arch 元数据均声明替换旧 `meloasr` 包;通过对应发行版的包管理器安装新包时,旧应用、Fcitx5 addon 和桌面文件会一并卸载。
## 统一构建契约
@@ -42,7 +42,7 @@ pnpm run build:linux:dir
packaging/deb/build-deb.sh
```
产物位于 `packaging/out/tama-asr_1.0.1_<arch>.deb`。依赖名称按 Debian/Ubuntu 系列填写,但尚未在各发行版完成安装验证。
产物位于 `packaging/out/tama-asr_1.0.3_<arch>.deb`。依赖名称按 Debian/Ubuntu 系列填写,但尚未在各发行版完成安装验证。
## rpm
+1 -1
View File
@@ -1,5 +1,5 @@
pkgname=tama-asr
pkgver=1.0.1
pkgver=1.0.3
pkgrel=1
pkgdesc='Web-backed voice input for Fcitx5'
arch=('x86_64' 'aarch64')
+1 -1
View File
@@ -1,5 +1,5 @@
pkgname=tama-asr
pkgver=1.0.1
pkgver=1.0.3
pkgrel=1
pkgdesc='Web-backed voice input for Fcitx5'
arch=('x86_64')
+2 -2
View File
@@ -8,7 +8,7 @@
```bash
curl --fail --location --remote-name \
https://github.com/Melorise/TamaASR/releases/download/v1.0.1/PKGBUILD
https://github.com/Melorise/TamaASR/releases/download/v1.0.3/PKGBUILD
makepkg --syncdeps --cleanbuild
```
@@ -20,7 +20,7 @@ makepkg --syncdeps --cleanbuild
```bash
curl --fail --location --remote-name \
https://github.com/Melorise/TamaASR/releases/download/v1.0.1/PKGBUILD-from-source
https://github.com/Melorise/TamaASR/releases/download/v1.0.3/PKGBUILD-from-source
makepkg --syncdeps --cleanbuild -p PKGBUILD-from-source
```
+2
View File
@@ -16,6 +16,8 @@
<binary>tama-asr</binary>
</provides>
<releases>
<release version="1.0.3" date="2026-10-07"/>
<release version="1.0.2" date="2026-09-04"/>
<release version="1.0.1" date="2026-09-01"/>
<release version="1.0.0" date="2026-09-01"/>
<release version="0.1.17" date="2026-09-01"/>
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
version="${TAMA_ASR_VERSION:-1.0.1}"
version="${TAMA_ASR_VERSION:-1.0.3}"
arch="${TAMA_ASR_DEB_ARCH:-$(dpkg --print-architecture)}"
app_dir="${1:-${TAMA_ASR_ELECTRON_APP_DIR:-${project_dir}/release/linux-unpacked}}"
work_dir="${project_dir}/packaging/out/deb-root"
+3 -2
View File
@@ -4,7 +4,7 @@
, nodejs
, pnpm_11
, pnpmConfigHook
, electron
, electron_42
, cmake
, pkg-config
, makeWrapper
@@ -84,7 +84,8 @@ stdenv.mkDerivation (finalAttrs: {
"$out/share/pixmaps/tama-asr.png"
cmake --install build/fcitx5
makeWrapper ${electron}/bin/electron "$out/bin/tama-asr" \
makeWrapper ${electron_42}/bin/electron "$out/bin/tama-asr" \
--add-flags "--ozone-platform=x11" \
--add-flags "$appDir"
runHook postInstall
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
version="${TAMA_ASR_VERSION:-1.0.1}"
version="${TAMA_ASR_VERSION:-1.0.3}"
app_dir="${1:-${TAMA_ASR_ELECTRON_APP_DIR:-${project_dir}/release/linux-unpacked}}"
stage_dir="${project_dir}/packaging/out/rpm-root"
top_dir="${project_dir}/packaging/out/rpmbuild"
+7
View File
@@ -38,6 +38,13 @@ tar -C %{buildroot} -xzf %{SOURCE0}
/etc/xdg/autostart/tama-asr.desktop
%changelog
* Wed Oct 07 2026 TamaASR contributors - 1.0.3-1
- 适配新版千问录音状态,修复松开快捷键后无法终止的问题。
- Nix 启动参数与其他发行版保持一致,显式使用 XWayland。
* Fri Sep 04 2026 TamaASR contributors - 1.0.2-1
- 修复快速停止可能使网页录音状态与外部会话不同步的问题。
* Tue Sep 01 2026 TamaASR contributors - 1.0.1-1
- Redesign the settings and overlay-position interfaces.
+1
View File
@@ -21,6 +21,7 @@ export const qianwenAdapter: BackendWebAdapter = {
isRecording(document) {
const microphone = this.findMicrophone(document);
return microphone?.getAttribute('aria-pressed') === 'true' ||
microphone?.classList.contains('bg-tag') === true ||
/停止语音|stop\s*(voice|recording|input)/i.test(microphone ? controlDescription(microphone) : '');
},
+16 -7
View File
@@ -1,7 +1,7 @@
import { ipcRenderer } from 'electron';
import type { BackendControlCommand, BackendPageStatus, BackendTranscriptPayload } from '../backends/contracts';
import { getBackendWebAdapter } from '../backends/web-registry';
import { shouldClickMicrophoneOnCancel } from './speech-session-state';
import { shouldClickMicrophoneOnCancel, shouldClickMicrophoneOnStop } from './speech-session-state';
import { shouldFinishAfterStop } from './stop-completion';
const backendId = process.argv
@@ -180,9 +180,7 @@ function stopSpeech(): void {
if (!active || stopping || !adapter) return;
stopping = true;
const stoppedAt = Date.now();
const wasRecordingAtStop = adapter.isRecording(document);
adapter.findMicrophone(document)?.click();
diagnose('stop-clicked', { microphone: controlSnapshot(adapter.findMicrophone(document)) });
let stopClicked = false;
const finish = (): void => {
emitCurrentEditorText();
resetSession();
@@ -192,11 +190,22 @@ function stopSpeech(): void {
const checkCompletion = (): void => {
if (!active || !stopping || !adapter) return;
const now = Date.now();
const isRecording = adapter.isRecording(document);
if (shouldClickMicrophoneOnStop(isRecording, stopClicked)) {
const microphone = adapter.findMicrophone(document);
if (microphone) {
microphone.click();
stopClicked = true;
diagnose('stop-clicked', { microphone: controlSnapshot(adapter.findMicrophone(document)) });
}
stopTimer = setTimeout(checkCompletion, 75);
return;
}
if (shouldFinishAfterStop({
elapsedMs: now - stoppedAt,
quietMs: now - lastTranscriptAt,
wasRecordingAtStop,
isRecording: adapter.isRecording(document),
stopClicked,
isRecording,
...adapter.stopCompletion
})) {
diagnose('stop-complete', { elapsedMs: now - stoppedAt });
@@ -206,7 +215,7 @@ function stopSpeech(): void {
stopTimer = setTimeout(checkCompletion, 75);
};
clearTimeout(stopTimer);
stopTimer = setTimeout(checkCompletion, 75);
checkCompletion();
}
function cancelSpeech(): void {
+4
View File
@@ -1,3 +1,7 @@
export function shouldClickMicrophoneOnCancel(active: boolean, stopping: boolean): boolean {
return active && !stopping;
}
export function shouldClickMicrophoneOnStop(isRecording: boolean, stopClicked: boolean): boolean {
return isRecording && !stopClicked;
}
+2 -2
View File
@@ -1,7 +1,7 @@
export interface StopCompletionInput {
readonly elapsedMs: number;
readonly quietMs: number;
readonly wasRecordingAtStop: boolean;
readonly stopClicked: boolean;
readonly isRecording: boolean;
readonly minimumWaitMs: number;
readonly quietWindowMs: number;
@@ -9,7 +9,7 @@ export interface StopCompletionInput {
}
export function shouldFinishAfterStop(input: StopCompletionInput): boolean {
if (!input.stopClicked || input.isRecording) return false;
if (input.elapsedMs >= input.timeoutMs) return true;
if (input.wasRecordingAtStop && input.isRecording) return false;
return input.elapsedMs >= input.minimumWaitMs && input.quietMs >= input.quietWindowMs;
}
+37
View File
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { qianwenAdapter } from '../src/backends/qianwen';
import { shouldClickMicrophoneOnStop } from '../src/preload/speech-session-state';
function microphone(className: string, attributes: Record<string, string | undefined> = {}): HTMLElement {
return {
classList: { contains: (token: string) => className.split(/\s+/).includes(token) },
getAttribute: (name: string) => name === 'class' ? className : attributes[name] ?? null,
querySelector: () => null,
textContent: ''
} as unknown as HTMLElement;
}
test('千问新版录音样式允许点击停止,恢复空闲样式后不再判为录音', () => {
let control = microphone('cursor-pointer hover:bg-tag');
const adapter = { ...qianwenAdapter, findMicrophone: () => control };
const document = {} as Document;
assert.equal(adapter.isRecording(document), false);
control = microphone('pointer-events-auto cursor-pointer bg-tag');
assert.equal(adapter.isRecording(document), true);
assert.equal(shouldClickMicrophoneOnStop(adapter.isRecording(document), false), true);
assert.equal(shouldClickMicrophoneOnStop(adapter.isRecording(document), true), false);
control = microphone('cursor-pointer hover:bg-tag');
assert.equal(adapter.isRecording(document), false);
});
test('千问旧版 aria 和停止文案保持兼容', () => {
for (const attributes of [{ 'aria-pressed': 'true' }, { 'aria-label': '停止语音' }]) {
const adapter = { ...qianwenAdapter, findMicrophone: () => microphone('', attributes) };
assert.equal(adapter.isRecording({} as Document), true);
}
const adapter = { ...qianwenAdapter, findMicrophone: () => null };
assert.equal(adapter.isRecording({} as Document), false);
});
+10 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { shouldClickMicrophoneOnCancel } from '../src/preload/speech-session-state';
import { shouldClickMicrophoneOnCancel, shouldClickMicrophoneOnStop } from '../src/preload/speech-session-state';
test('停止命令已经点击过麦克风后,取消不得再次点击并重新开启录音', () => {
assert.equal(shouldClickMicrophoneOnCancel(true, true), false);
@@ -13,3 +13,12 @@ test('录音期间直接取消时仍应点击麦克风停止网页录音', () =>
test('本地会话未激活时取消不得点击麦克风', () => {
assert.equal(shouldClickMicrophoneOnCancel(false, false), false);
});
test('停止到达但网页尚未开始录音时不得再次点击开始按钮', () => {
assert.equal(shouldClickMicrophoneOnStop(false, false), false);
});
test('网页异步进入录音后只点击一次停止按钮', () => {
assert.equal(shouldClickMicrophoneOnStop(true, false), true);
assert.equal(shouldClickMicrophoneOnStop(true, true), false);
});
+11 -3
View File
@@ -5,7 +5,7 @@ import { shouldFinishAfterStop } from '../src/preload/stop-completion';
const base = {
elapsedMs: 700,
quietMs: 500,
wasRecordingAtStop: true,
stopClicked: true,
isRecording: false,
minimumWaitMs: 250,
quietWindowMs: 450,
@@ -16,6 +16,10 @@ test('网页仍在录音时不得提前提交', () => {
assert.equal(shouldFinishAfterStop({ ...base, isRecording: true }), false);
});
test('尚未点击停止时不得提交', () => {
assert.equal(shouldFinishAfterStop({ ...base, stopClicked: false }), false);
});
test('网页结束且最终文本静默窗口满足时立即提交', () => {
assert.equal(shouldFinishAfterStop(base), true);
});
@@ -24,6 +28,10 @@ test('网页结束后仍有文本修正时继续等待', () => {
assert.equal(shouldFinishAfterStop({ ...base, quietMs: 120 }), false);
});
test('网页状态未回切时由超时兜底结束', () => {
assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, isRecording: true }), true);
test('网页仍在录音时超时也不得提交', () => {
assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, isRecording: true }), false);
});
test('网页已停止时超时可以结束静默等待', () => {
assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, quietMs: 120 }), true);
});