From fc6d61e41b1b0e0686d1346694445dc119b97872 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 28 Jun 2026 23:58:07 +0800
Subject: [PATCH] Fix hold recording head and tail clipping

---
 privatevoice.src/frontend/package.json      |    2 
 privatevoice.src/app.go                     |   15 +++++--
 privatevoice.src/app_live_caption_test.go   |   44 ++++++++++++++++++++--
 privatevoice.src/frontend/package-lock.json |    4 +-
 CHANGELOG.md                                |   15 +++++++
 privatevoice.src/build/darwin/Info.plist    |    4 +-
 6 files changed, 71 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b2101b..fc7b343 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
 # Changelog
 
+## v2.1.36 (2026-06-28)
+
+### 首尾字稳定性修复
+
+- **Hold 模式首字保护扩展到所有模型**:按下热键后立即启动本地预采集,确认不是组合键后保留这段音频,降低 SenseVoice / Qwen3-ASR / Moonshine / Parakeet 等离线模型开头 1-2 个字丢失的概率。
+- **松键尾部采集保护扩展到所有模型**:松开热键后默认继续采集约 300ms 再停止录音,降低最后 1-2 个字被截断的概率。
+- **保留取消和组合键隔离**:组合键或过早释放仍会丢弃预采集音频,不进入识别流程。
+
+### 构建
+
+- build: `20260628.2354`
+- 说明: 本版本为首尾字稳定性修复的本地验证候选包。
+
+---
+
 ## v2.1.35 (2026-06-23)
 
 ### 输入配置页排版优化
diff --git a/privatevoice.src/app.go b/privatevoice.src/app.go
index c73f9c8..b40cd98 100755
--- a/privatevoice.src/app.go
+++ b/privatevoice.src/app.go
@@ -28,8 +28,8 @@
 )
 
 const (
-	appVersion        = "2.1.35"
-	appBuild          = "20260623.1435"
+	appVersion        = "2.1.36"
+	appBuild          = "20260628.2354"
 	appDisplayVersion = appVersion + " (build " + appBuild + ")"
 	appName           = "PrivateVoice Dictation"
 
@@ -37,6 +37,7 @@
 	silenceTimeoutDuration   = 3 * time.Second // auto-stop after this much silence in free-talk
 	silenceGracePeriod       = 2 * time.Second // don't auto-stop within first 2s of free-talk
 	holdActivationDelay      = 180 * time.Millisecond
+	defaultReleaseTailDelay  = 300 * time.Millisecond
 	doneIndicatorHideDelayMs = 250
 	liveCaptionInterval      = 120 * time.Millisecond
 	liveCaptionMaxBytes      = 108
@@ -985,9 +986,12 @@
 }
 
 func releaseTailCaptureDelayForEngine(eng engine.Engine) time.Duration {
+	if eng == nil {
+		return 0
+	}
 	tailCaptureEng, ok := eng.(engine.ReleaseTailCaptureEngine)
 	if !ok {
-		return 0
+		return defaultReleaseTailDelay
 	}
 	delay := tailCaptureEng.ReleaseTailCaptureDelay()
 	if delay < 0 {
@@ -997,9 +1001,12 @@
 }
 
 func holdPreCaptureEnabledForEngine(eng engine.Engine) bool {
+	if eng == nil {
+		return false
+	}
 	preCaptureEng, ok := eng.(engine.HoldPreCaptureEngine)
 	if !ok {
-		return false
+		return true
 	}
 	return preCaptureEng.HoldPreCaptureEnabled()
 }
diff --git a/privatevoice.src/app_live_caption_test.go b/privatevoice.src/app_live_caption_test.go
index 7dd3a0a..c8380a7 100644
--- a/privatevoice.src/app_live_caption_test.go
+++ b/privatevoice.src/app_live_caption_test.go
@@ -29,12 +29,30 @@
 	}
 }
 
-func TestReleaseTailCaptureDelayDefaultsToZero(t *testing.T) {
+func TestReleaseTailCaptureDelayCanBeDisabledByEngineCapability(t *testing.T) {
 	a := &App{}
-	a.replaceEngine(plainTestEngine{})
+	a.replaceEngine(tailCaptureTestEngine{delay: 0})
 
 	if got := a.releaseTailCaptureDelay(); got != 0 {
 		t.Fatalf("releaseTailCaptureDelay() = %v, want 0", got)
+	}
+}
+
+func TestReleaseTailCaptureDelayDefaultsToProtectionWindow(t *testing.T) {
+	a := &App{}
+	a.replaceEngine(plainTestEngine{})
+
+	if got := a.releaseTailCaptureDelay(); got != defaultReleaseTailDelay {
+		t.Fatalf("releaseTailCaptureDelay() = %v, want %v", got, defaultReleaseTailDelay)
+	}
+}
+
+func TestReleaseTailCaptureDelayDisabledWithoutEngine(t *testing.T) {
+	a := &App{}
+	a.replaceEngine(nil)
+
+	if got := a.releaseTailCaptureDelay(); got != 0 {
+		t.Fatalf("releaseTailCaptureDelay() = %v, want 0 without engine", got)
 	}
 }
 
@@ -47,12 +65,30 @@
 	}
 }
 
-func TestHoldPreCaptureDefaultsToFalse(t *testing.T) {
+func TestHoldPreCaptureCanBeDisabledByEngineCapability(t *testing.T) {
+	a := &App{}
+	a.replaceEngine(holdPreCaptureTestEngine{enabled: false})
+
+	if a.holdPreCaptureEnabled.Load() {
+		t.Fatal("hold pre-capture should respect engine capability false")
+	}
+}
+
+func TestHoldPreCaptureDefaultsToTrueForReadyEngine(t *testing.T) {
 	a := &App{}
 	a.replaceEngine(plainTestEngine{})
 
+	if !a.holdPreCaptureEnabled.Load() {
+		t.Fatal("hold pre-capture should default to true for ready engines")
+	}
+}
+
+func TestHoldPreCaptureDisabledWithoutEngine(t *testing.T) {
+	a := &App{}
+	a.replaceEngine(nil)
+
 	if a.holdPreCaptureEnabled.Load() {
-		t.Fatal("hold pre-capture should default to false")
+		t.Fatal("hold pre-capture should be disabled without an engine")
 	}
 }
 
diff --git a/privatevoice.src/build/darwin/Info.plist b/privatevoice.src/build/darwin/Info.plist
index 409ea47..bbf395a 100755
--- a/privatevoice.src/build/darwin/Info.plist
+++ b/privatevoice.src/build/darwin/Info.plist
@@ -17,9 +17,9 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>2.1.35</string>
+	<string>2.1.36</string>
 	<key>CFBundleVersion</key>
-	<string>20260623.1435</string>
+	<string>20260628.2354</string>
 	<key>ITSAppUsesNonExemptEncryption</key>
 	<false/>
 	<key>LSApplicationCategoryType</key>
diff --git a/privatevoice.src/frontend/package-lock.json b/privatevoice.src/frontend/package-lock.json
index 591e137..ff41baa 100755
--- a/privatevoice.src/frontend/package-lock.json
+++ b/privatevoice.src/frontend/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "privatevoice-dictation-frontend",
-  "version": "2.1.35",
+  "version": "2.1.36",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "privatevoice-dictation-frontend",
-      "version": "2.1.35",
+      "version": "2.1.36",
       "dependencies": {
         "@wailsio/runtime": "latest"
       },
diff --git a/privatevoice.src/frontend/package.json b/privatevoice.src/frontend/package.json
index 45711ef..0863451 100755
--- a/privatevoice.src/frontend/package.json
+++ b/privatevoice.src/frontend/package.json
@@ -1,7 +1,7 @@
 {
   "name": "privatevoice-dictation-frontend",
   "private": true,
-  "version": "2.1.35",
+  "version": "2.1.36",
   "type": "module",
   "scripts": {
     "dev": "vite dev",

--
Gitblit v1.9.3