mirror of
https://github.com/Melorise/TamaASR.git
synced 2026-11-07 14:39:32 +00:00
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 项目范围
|
## 项目范围
|
||||||
|
|
||||||
TamaASR 是仅面向 Linux 的 Fcitx5 桌面语音输入工具。项目名为 `TamaASR`,分发包名为 `tama-asr`,当前版本为 `1.0.0`。
|
TamaASR 是仅面向 Linux 的 Fcitx5 桌面语音输入工具。项目名为 `TamaASR`,分发包名为 `tama-asr`,当前版本为 `1.0.2`。
|
||||||
|
|
||||||
不得恢复 Windows、PowerShell 或 Tauri 输出路径。发行版差异只能存在于依赖说明和打包配置,不能进入语音输入核心逻辑。
|
不得恢复 Windows、PowerShell 或 Tauri 输出路径。发行版差异只能存在于依赖说明和打包配置,不能进入语音输入核心逻辑。
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 的格式。
|
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 的格式。
|
||||||
|
|
||||||
|
## [1.0.2] - 2026-09-04
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 修复快速松开快捷键时,网页尚未完成异步启动便再次点击麦克风,导致外部会话已结束但网页仍在录音的问题。
|
||||||
|
|
||||||
## [1.0.1] - 2026-09-01
|
## [1.0.1] - 2026-09-01
|
||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 项目范围
|
## 项目范围
|
||||||
|
|
||||||
TamaASR 是仅面向 Linux 的 Fcitx5 桌面语音输入工具。项目名为 `TamaASR`,分发包名为 `tama-asr`,当前版本为 `1.0.0`。
|
TamaASR 是仅面向 Linux 的 Fcitx5 桌面语音输入工具。项目名为 `TamaASR`,分发包名为 `tama-asr`,当前版本为 `1.0.2`。
|
||||||
|
|
||||||
不得恢复 Windows、PowerShell 或 Tauri 输出路径。发行版差异只能存在于依赖说明和打包配置,不能进入语音输入核心逻辑。
|
不得恢复 Windows、PowerShell 或 Tauri 输出路径。发行版差异只能存在于依赖说明和打包配置,不能进入语音输入核心逻辑。
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
project(tama-asr-fcitx5 VERSION 1.0.1 LANGUAGES CXX)
|
project(tama-asr-fcitx5 VERSION 1.0.2 LANGUAGES CXX)
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
include(CTest)
|
include(CTest)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Comment[zh_CN]=将 TamaASR 网页后端连接到当前输入上下文
|
|||||||
Type=SharedLibrary
|
Type=SharedLibrary
|
||||||
Library=@TAMA_ASR_LIBRARY_PATH@
|
Library=@TAMA_ASR_LIBRARY_PATH@
|
||||||
Category=Module
|
Category=Module
|
||||||
Version=1.0.1
|
Version=1.0.2
|
||||||
OnDemand=False
|
OnDemand=False
|
||||||
Configurable=False
|
Configurable=False
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tama-asr",
|
"name": "tama-asr",
|
||||||
"productName": "TamaASR",
|
"productName": "TamaASR",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@11.21.0",
|
"packageManager": "pnpm@11.21.0",
|
||||||
"author": "TamaASR contributors",
|
"author": "TamaASR contributors",
|
||||||
|
|||||||
+3
-3
@@ -1,13 +1,13 @@
|
|||||||
# Linux 分发打包
|
# Linux 分发打包
|
||||||
|
|
||||||
TamaASR 1.0.1 的系统包由两部分组成:
|
TamaASR 1.0.2 的系统包由两部分组成:
|
||||||
|
|
||||||
- Electron 应用安装到 `/opt/tama-asr`,命令入口为 `/usr/bin/tama-asr`;
|
- Electron 应用安装到 `/opt/tama-asr`,命令入口为 `/usr/bin/tama-asr`;
|
||||||
- Fcitx5 常驻 addon 由目标发行版中的 CMake/Fcitx5 开发包编译,并安装到该环境报告的 addon 目录。
|
- Fcitx5 常驻 addon 由目标发行版中的 CMake/Fcitx5 开发包编译,并安装到该环境报告的 addon 目录。
|
||||||
|
|
||||||
安装包同时提供桌面入口、项目图标和 `/etc/xdg/autostart/tama-asr.desktop`,因此安装后的默认行为是随桌面会话启动。设置页关闭自启动时,应用需要在用户级 autostart 目录写入覆盖项,不能删除系统包所属文件。
|
安装包同时提供桌面入口、项目图标和 `/etc/xdg/autostart/tama-asr.desktop`,因此安装后的默认行为是随桌面会话启动。设置页关闭自启动时,应用需要在用户级 autostart 目录写入覆盖项,不能删除系统包所属文件。
|
||||||
|
|
||||||
`tama-asr` 1.0.1 的 deb、RPM 和 Arch 元数据均声明替换旧 `meloasr` 包;通过对应发行版的包管理器安装新包时,旧应用、Fcitx5 addon 和桌面文件会一并卸载。
|
`tama-asr` 1.0.2 的 deb、RPM 和 Arch 元数据均声明替换旧 `meloasr` 包;通过对应发行版的包管理器安装新包时,旧应用、Fcitx5 addon 和桌面文件会一并卸载。
|
||||||
|
|
||||||
## 统一构建契约
|
## 统一构建契约
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ pnpm run build:linux:dir
|
|||||||
packaging/deb/build-deb.sh
|
packaging/deb/build-deb.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
产物位于 `packaging/out/tama-asr_1.0.1_<arch>.deb`。依赖名称按 Debian/Ubuntu 系列填写,但尚未在各发行版完成安装验证。
|
产物位于 `packaging/out/tama-asr_1.0.2_<arch>.deb`。依赖名称按 Debian/Ubuntu 系列填写,但尚未在各发行版完成安装验证。
|
||||||
|
|
||||||
## rpm
|
## rpm
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=tama-asr
|
pkgname=tama-asr
|
||||||
pkgver=1.0.1
|
pkgver=1.0.2
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc='Web-backed voice input for Fcitx5'
|
pkgdesc='Web-backed voice input for Fcitx5'
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=tama-asr
|
pkgname=tama-asr
|
||||||
pkgver=1.0.1
|
pkgver=1.0.2
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc='Web-backed voice input for Fcitx5'
|
pkgdesc='Web-backed voice input for Fcitx5'
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --fail --location --remote-name \
|
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.2/PKGBUILD
|
||||||
makepkg --syncdeps --cleanbuild
|
makepkg --syncdeps --cleanbuild
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ makepkg --syncdeps --cleanbuild
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --fail --location --remote-name \
|
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.2/PKGBUILD-from-source
|
||||||
makepkg --syncdeps --cleanbuild -p PKGBUILD-from-source
|
makepkg --syncdeps --cleanbuild -p PKGBUILD-from-source
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<binary>tama-asr</binary>
|
<binary>tama-asr</binary>
|
||||||
</provides>
|
</provides>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="1.0.2" date="2026-09-04"/>
|
||||||
<release version="1.0.1" date="2026-09-01"/>
|
<release version="1.0.1" date="2026-09-01"/>
|
||||||
<release version="1.0.0" date="2026-09-01"/>
|
<release version="1.0.0" date="2026-09-01"/>
|
||||||
<release version="0.1.17" date="2026-09-01"/>
|
<release version="0.1.17" date="2026-09-01"/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
version="${TAMA_ASR_VERSION:-1.0.1}"
|
version="${TAMA_ASR_VERSION:-1.0.2}"
|
||||||
arch="${TAMA_ASR_DEB_ARCH:-$(dpkg --print-architecture)}"
|
arch="${TAMA_ASR_DEB_ARCH:-$(dpkg --print-architecture)}"
|
||||||
app_dir="${1:-${TAMA_ASR_ELECTRON_APP_DIR:-${project_dir}/release/linux-unpacked}}"
|
app_dir="${1:-${TAMA_ASR_ELECTRON_APP_DIR:-${project_dir}/release/linux-unpacked}}"
|
||||||
work_dir="${project_dir}/packaging/out/deb-root"
|
work_dir="${project_dir}/packaging/out/deb-root"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
version="${TAMA_ASR_VERSION:-1.0.1}"
|
version="${TAMA_ASR_VERSION:-1.0.2}"
|
||||||
app_dir="${1:-${TAMA_ASR_ELECTRON_APP_DIR:-${project_dir}/release/linux-unpacked}}"
|
app_dir="${1:-${TAMA_ASR_ELECTRON_APP_DIR:-${project_dir}/release/linux-unpacked}}"
|
||||||
stage_dir="${project_dir}/packaging/out/rpm-root"
|
stage_dir="${project_dir}/packaging/out/rpm-root"
|
||||||
top_dir="${project_dir}/packaging/out/rpmbuild"
|
top_dir="${project_dir}/packaging/out/rpmbuild"
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ tar -C %{buildroot} -xzf %{SOURCE0}
|
|||||||
/etc/xdg/autostart/tama-asr.desktop
|
/etc/xdg/autostart/tama-asr.desktop
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Fri Sep 04 2026 TamaASR contributors - 1.0.2-1
|
||||||
|
- 修复快速停止可能使网页录音状态与外部会话不同步的问题。
|
||||||
|
|
||||||
* Tue Sep 01 2026 TamaASR contributors - 1.0.1-1
|
* Tue Sep 01 2026 TamaASR contributors - 1.0.1-1
|
||||||
- Redesign the settings and overlay-position interfaces.
|
- Redesign the settings and overlay-position interfaces.
|
||||||
|
|
||||||
|
|||||||
+16
-7
@@ -1,7 +1,7 @@
|
|||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
import type { BackendControlCommand, BackendPageStatus, BackendTranscriptPayload } from '../backends/contracts';
|
import type { BackendControlCommand, BackendPageStatus, BackendTranscriptPayload } from '../backends/contracts';
|
||||||
import { getBackendWebAdapter } from '../backends/web-registry';
|
import { getBackendWebAdapter } from '../backends/web-registry';
|
||||||
import { shouldClickMicrophoneOnCancel } from './speech-session-state';
|
import { shouldClickMicrophoneOnCancel, shouldClickMicrophoneOnStop } from './speech-session-state';
|
||||||
import { shouldFinishAfterStop } from './stop-completion';
|
import { shouldFinishAfterStop } from './stop-completion';
|
||||||
|
|
||||||
const backendId = process.argv
|
const backendId = process.argv
|
||||||
@@ -180,9 +180,7 @@ function stopSpeech(): void {
|
|||||||
if (!active || stopping || !adapter) return;
|
if (!active || stopping || !adapter) return;
|
||||||
stopping = true;
|
stopping = true;
|
||||||
const stoppedAt = Date.now();
|
const stoppedAt = Date.now();
|
||||||
const wasRecordingAtStop = adapter.isRecording(document);
|
let stopClicked = false;
|
||||||
adapter.findMicrophone(document)?.click();
|
|
||||||
diagnose('stop-clicked', { microphone: controlSnapshot(adapter.findMicrophone(document)) });
|
|
||||||
const finish = (): void => {
|
const finish = (): void => {
|
||||||
emitCurrentEditorText();
|
emitCurrentEditorText();
|
||||||
resetSession();
|
resetSession();
|
||||||
@@ -192,11 +190,22 @@ function stopSpeech(): void {
|
|||||||
const checkCompletion = (): void => {
|
const checkCompletion = (): void => {
|
||||||
if (!active || !stopping || !adapter) return;
|
if (!active || !stopping || !adapter) return;
|
||||||
const now = Date.now();
|
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({
|
if (shouldFinishAfterStop({
|
||||||
elapsedMs: now - stoppedAt,
|
elapsedMs: now - stoppedAt,
|
||||||
quietMs: now - lastTranscriptAt,
|
quietMs: now - lastTranscriptAt,
|
||||||
wasRecordingAtStop,
|
stopClicked,
|
||||||
isRecording: adapter.isRecording(document),
|
isRecording,
|
||||||
...adapter.stopCompletion
|
...adapter.stopCompletion
|
||||||
})) {
|
})) {
|
||||||
diagnose('stop-complete', { elapsedMs: now - stoppedAt });
|
diagnose('stop-complete', { elapsedMs: now - stoppedAt });
|
||||||
@@ -206,7 +215,7 @@ function stopSpeech(): void {
|
|||||||
stopTimer = setTimeout(checkCompletion, 75);
|
stopTimer = setTimeout(checkCompletion, 75);
|
||||||
};
|
};
|
||||||
clearTimeout(stopTimer);
|
clearTimeout(stopTimer);
|
||||||
stopTimer = setTimeout(checkCompletion, 75);
|
checkCompletion();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelSpeech(): void {
|
function cancelSpeech(): void {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
export function shouldClickMicrophoneOnCancel(active: boolean, stopping: boolean): boolean {
|
export function shouldClickMicrophoneOnCancel(active: boolean, stopping: boolean): boolean {
|
||||||
return active && !stopping;
|
return active && !stopping;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldClickMicrophoneOnStop(isRecording: boolean, stopClicked: boolean): boolean {
|
||||||
|
return isRecording && !stopClicked;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export interface StopCompletionInput {
|
export interface StopCompletionInput {
|
||||||
readonly elapsedMs: number;
|
readonly elapsedMs: number;
|
||||||
readonly quietMs: number;
|
readonly quietMs: number;
|
||||||
readonly wasRecordingAtStop: boolean;
|
readonly stopClicked: boolean;
|
||||||
readonly isRecording: boolean;
|
readonly isRecording: boolean;
|
||||||
readonly minimumWaitMs: number;
|
readonly minimumWaitMs: number;
|
||||||
readonly quietWindowMs: number;
|
readonly quietWindowMs: number;
|
||||||
@@ -9,7 +9,7 @@ export interface StopCompletionInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function shouldFinishAfterStop(input: StopCompletionInput): boolean {
|
export function shouldFinishAfterStop(input: StopCompletionInput): boolean {
|
||||||
|
if (!input.stopClicked || input.isRecording) return false;
|
||||||
if (input.elapsedMs >= input.timeoutMs) return true;
|
if (input.elapsedMs >= input.timeoutMs) return true;
|
||||||
if (input.wasRecordingAtStop && input.isRecording) return false;
|
|
||||||
return input.elapsedMs >= input.minimumWaitMs && input.quietMs >= input.quietWindowMs;
|
return input.elapsedMs >= input.minimumWaitMs && input.quietMs >= input.quietWindowMs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { shouldClickMicrophoneOnCancel } from '../src/preload/speech-session-state';
|
import { shouldClickMicrophoneOnCancel, shouldClickMicrophoneOnStop } from '../src/preload/speech-session-state';
|
||||||
|
|
||||||
test('停止命令已经点击过麦克风后,取消不得再次点击并重新开启录音', () => {
|
test('停止命令已经点击过麦克风后,取消不得再次点击并重新开启录音', () => {
|
||||||
assert.equal(shouldClickMicrophoneOnCancel(true, true), false);
|
assert.equal(shouldClickMicrophoneOnCancel(true, true), false);
|
||||||
@@ -13,3 +13,12 @@ test('录音期间直接取消时仍应点击麦克风停止网页录音', () =>
|
|||||||
test('本地会话未激活时取消不得点击麦克风', () => {
|
test('本地会话未激活时取消不得点击麦克风', () => {
|
||||||
assert.equal(shouldClickMicrophoneOnCancel(false, false), false);
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { shouldFinishAfterStop } from '../src/preload/stop-completion';
|
|||||||
const base = {
|
const base = {
|
||||||
elapsedMs: 700,
|
elapsedMs: 700,
|
||||||
quietMs: 500,
|
quietMs: 500,
|
||||||
wasRecordingAtStop: true,
|
stopClicked: true,
|
||||||
isRecording: false,
|
isRecording: false,
|
||||||
minimumWaitMs: 250,
|
minimumWaitMs: 250,
|
||||||
quietWindowMs: 450,
|
quietWindowMs: 450,
|
||||||
@@ -16,6 +16,10 @@ test('网页仍在录音时不得提前提交', () => {
|
|||||||
assert.equal(shouldFinishAfterStop({ ...base, isRecording: true }), false);
|
assert.equal(shouldFinishAfterStop({ ...base, isRecording: true }), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('尚未点击停止时不得提交', () => {
|
||||||
|
assert.equal(shouldFinishAfterStop({ ...base, stopClicked: false }), false);
|
||||||
|
});
|
||||||
|
|
||||||
test('网页结束且最终文本静默窗口满足时立即提交', () => {
|
test('网页结束且最终文本静默窗口满足时立即提交', () => {
|
||||||
assert.equal(shouldFinishAfterStop(base), true);
|
assert.equal(shouldFinishAfterStop(base), true);
|
||||||
});
|
});
|
||||||
@@ -24,6 +28,10 @@ test('网页结束后仍有文本修正时继续等待', () => {
|
|||||||
assert.equal(shouldFinishAfterStop({ ...base, quietMs: 120 }), false);
|
assert.equal(shouldFinishAfterStop({ ...base, quietMs: 120 }), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('网页状态未回切时由超时兜底结束', () => {
|
test('网页仍在录音时超时也不得提交', () => {
|
||||||
assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, isRecording: true }), true);
|
assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, isRecording: true }), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('网页已停止时超时可以结束静默等待', () => {
|
||||||
|
assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, quietMs: 120 }), true);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user