@xdfnet/ispeak 1.6.11 → 1.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/main.go +65 -179
  2. package/main_test.go +0 -378
  3. package/package.json +1 -1
package/main.go CHANGED
@@ -47,193 +47,81 @@ type StreamPlayer interface {
47
47
  Abort() error
48
48
  }
49
49
 
50
- // 任务状态
51
- // 生命周期:pending -> running -> delete
52
- type TaskStatus int
53
-
54
- const (
55
- TaskStatusPending TaskStatus = iota
56
- TaskStatusRunning
57
- )
58
-
59
- // 单个 TTS 任务
60
- type Task struct {
61
- ID uint64
62
- Text string
63
- Status TaskStatus
64
- Voice VoiceInfo
65
- Cfg Config
66
- }
67
-
68
- // 任务引擎:任务仓库 + 单事务 worker
69
- type TaskEngine struct {
70
- mu sync.Mutex
71
-
72
- nextID uint64
73
- latestID uint64
74
- tasks map[uint64]*Task
75
- pending []uint64
76
-
77
- wake chan struct{}
78
-
79
- synthesizeStreamFn func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error
80
- newStreamPlayerFn func() (StreamPlayer, error)
81
- }
82
-
83
- func NewTaskEngine() *TaskEngine {
84
- return &TaskEngine{
85
- tasks: make(map[uint64]*Task),
86
- wake: make(chan struct{}, 1),
87
- synthesizeStreamFn: synthesizeStream,
88
- newStreamPlayerFn: newDefaultStreamPlayer,
89
- }
90
- }
91
-
92
- func (e *TaskEngine) Start() {
93
- go e.transactionWorker()
94
- }
95
-
96
- func (e *TaskEngine) Submit(text string, voice VoiceInfo, cfg Config) uint64 {
97
- e.mu.Lock()
98
-
99
-
100
- e.nextID++
101
- task := &Task{
102
- ID: e.nextID,
103
- Text: text,
104
- Status: TaskStatusPending,
105
- Voice: voice,
106
- Cfg: cfg,
107
- }
108
- e.tasks[task.ID] = task
109
- e.latestID = task.ID
110
- e.pending = append(e.pending, task.ID)
111
- log.Printf("任务创建: id=%d text=%s", task.ID, text)
112
-
113
- notify(e.wake)
114
- e.mu.Unlock()
115
-
116
- return task.ID
50
+ // 单播放器:新的打断旧的,不用队列
51
+ type Player struct {
52
+ mu sync.Mutex
53
+ gen int64
54
+ currentGen int64
55
+ player StreamPlayer
117
56
  }
118
57
 
119
- func (e *TaskEngine) transactionWorker() {
120
- for {
121
- id := e.claimPending()
122
- if id == 0 {
123
- <-e.wake
124
- continue
125
- }
126
-
127
- e.processTransaction(id)
128
- }
58
+ func NewPlayer() *Player {
59
+ return &Player{}
129
60
  }
130
61
 
131
- func (e *TaskEngine) processTransaction(id uint64) {
132
- defer func() {
133
- if r := recover(); r != nil {
134
- log.Printf("播报任务崩溃并删除: id=%d err=%v", id, r)
135
- e.deleteTask(id)
136
- }
137
- }()
62
+ func (p *Player) Submit(text string, voice VoiceInfo, cfg Config) {
63
+ p.mu.Lock()
64
+ p.gen++
65
+ currentGen := p.gen
66
+ p.player = nil
138
67
 
139
- task, ok := e.getTask(id)
140
- if !ok {
141
- return
142
- }
143
- if !e.isLatestTask(id) {
144
- log.Printf("跳过过期播报任务: id=%d", id)
145
- e.deleteTask(id)
146
- return
147
- }
148
-
149
- if err := e.runTransaction(task); err != nil {
150
- log.Printf("播报失败并删除任务: id=%d err=%v", id, err)
151
- e.deleteTask(id)
68
+ player, err := newDefaultStreamPlayer()
69
+ if err != nil {
70
+ log.Printf("启动播放器失败: %v", err)
71
+ p.mu.Unlock()
152
72
  return
153
73
  }
74
+ p.player = player
75
+ log.Printf("TTS: %s", text)
154
76
 
155
- log.Printf("播报完成并删除任务: id=%d", id)
156
- e.deleteTask(id)
157
- }
158
-
159
- func (e *TaskEngine) runTransaction(task *Task) error {
160
77
  startedAt := time.Now()
161
- player, err := e.newStreamPlayerFn()
162
- if err != nil {
163
- return fmt.Errorf("启动播放器失败: %w", err)
164
- }
165
-
166
- firstChunkLogged := false
167
- onAudio := func(audio []byte) error {
168
- if len(audio) > 0 && !firstChunkLogged {
169
- firstChunkLogged = true
170
- log.Printf("首个音频 chunk: id=%d elapsed=%s bytes=%d", task.ID, time.Since(startedAt).Round(time.Millisecond), len(audio))
78
+ go func() {
79
+ defer func() {
80
+ if r := recover(); r != nil {
81
+ log.Printf("播报崩溃: %v", r)
82
+ }
83
+ }()
84
+
85
+ onAudio := func(audio []byte) error {
86
+ p.mu.Lock()
87
+ stale := currentGen != p.gen
88
+ p.mu.Unlock()
89
+ if stale {
90
+ return errors.New("stale")
91
+ }
92
+ if err := player.Write(audio); err != nil {
93
+ return err
94
+ }
95
+ if len(audio) > 0 {
96
+ log.Printf("首个音频 chunk elapsed=%s bytes=%d", time.Since(startedAt).Round(time.Millisecond), len(audio))
97
+ }
98
+ return nil
171
99
  }
172
- return player.Write(audio)
173
- }
174
-
175
- if err := e.synthesizeStreamFn(context.Background(), task.Cfg, task.Text, &task.Voice, onAudio); err != nil {
176
- _ = player.Abort()
177
- return fmt.Errorf("TTS 合成失败: id=%d: %w", task.ID, err)
178
- }
179
- log.Printf("TTS 流结束: id=%d elapsed=%s", task.ID, time.Since(startedAt).Round(time.Millisecond))
180
100
 
181
- if err := player.CloseAndWait(); err != nil {
182
- _ = player.Abort()
183
- return fmt.Errorf("播放器失败: id=%d: %w", task.ID, err)
184
- }
185
- return nil
186
- }
187
-
188
- func (e *TaskEngine) claimPending() uint64 {
189
- e.mu.Lock()
190
- defer e.mu.Unlock()
101
+ if err := synthesizeStream(context.Background(), cfg, text, &voice, onAudio); err != nil {
102
+ if !strings.Contains(err.Error(), "stale") {
103
+ log.Printf("TTS 合成失败: %v", err)
104
+ }
105
+ _ = player.Abort()
106
+ p.mu.Lock()
107
+ if p.player == player {
108
+ p.player = nil
109
+ }
110
+ p.mu.Unlock()
111
+ return
112
+ }
191
113
 
192
- for len(e.pending) > 0 {
193
- id := e.pending[0]
194
- e.pending = e.pending[1:]
195
- task, ok := e.tasks[id]
196
- if !ok {
197
- continue
114
+ log.Printf("TTS 流结束 elapsed=%s", time.Since(startedAt).Round(time.Millisecond))
115
+ if err := player.CloseAndWait(); err != nil {
116
+ log.Printf("播放器失败: %v", err)
198
117
  }
199
- if task.Status != TaskStatusPending {
200
- continue
118
+ p.mu.Lock()
119
+ if p.player == player {
120
+ p.player = nil
201
121
  }
202
- task.Status = TaskStatusRunning
203
- return id
204
- }
205
- return 0
206
- }
207
-
208
- func (e *TaskEngine) getTask(id uint64) (*Task, bool) {
209
- e.mu.Lock()
210
- defer e.mu.Unlock()
211
-
212
- task, ok := e.tasks[id]
213
- if !ok {
214
- return nil, false
215
- }
216
- clone := *task
217
- return &clone, true
218
- }
219
-
220
- func (e *TaskEngine) deleteTask(id uint64) {
221
- e.mu.Lock()
222
- defer e.mu.Unlock()
223
- delete(e.tasks, id)
224
- }
225
-
226
- func (e *TaskEngine) isLatestTask(id uint64) bool {
227
- e.mu.Lock()
228
- defer e.mu.Unlock()
229
- return e.latestID == id
230
- }
231
-
232
- func notify(ch chan struct{}) {
233
- select {
234
- case ch <- struct{}{}:
235
- default:
236
- }
122
+ p.mu.Unlock()
123
+ }()
124
+ p.mu.Unlock()
237
125
  }
238
126
 
239
127
  // 音色信息
@@ -595,8 +483,7 @@ func main() {
595
483
  listener.Close()
596
484
  }()
597
485
 
598
- engine := NewTaskEngine()
599
- engine.Start()
486
+ player := NewPlayer()
600
487
 
601
488
  log.Printf("iSpeak 已启动,监听 %s", socketPath)
602
489
  for {
@@ -607,7 +494,7 @@ func main() {
607
494
  }
608
495
  continue
609
496
  }
610
- go handleConnection(conn, engine)
497
+ go handleConnection(conn, player)
611
498
  }
612
499
  }
613
500
 
@@ -688,7 +575,7 @@ func validateVoiceInfo(name string, voice *VoiceInfo) error {
688
575
  return nil
689
576
  }
690
577
 
691
- func handleConnection(conn net.Conn, engine *TaskEngine) {
578
+ func handleConnection(conn net.Conn, player *Player) {
692
579
  defer func() {
693
580
  if r := recover(); r != nil {
694
581
  log.Printf("连接处理崩溃: %v", r)
@@ -735,8 +622,7 @@ func handleConnection(conn net.Conn, engine *TaskEngine) {
735
622
  return
736
623
  }
737
624
 
738
- log.Printf("TTS: %s", cleaned)
739
- engine.Submit(cleaned, *voice, cfg)
625
+ player.Submit(cleaned, *voice, cfg)
740
626
  }
741
627
 
742
628
  // 解析消息中的音色前缀,返回 VoiceInfo
package/main_test.go CHANGED
@@ -1,229 +1,16 @@
1
1
  package main
2
2
 
3
3
  import (
4
- "context"
5
4
  "encoding/base64"
6
5
  "errors"
7
6
  "net"
8
7
  "os"
9
8
  "path/filepath"
10
9
  "strings"
11
- "sync"
12
10
  "testing"
13
11
  "time"
14
12
  )
15
13
 
16
- func TestSubmitKeepsAllPendingTasks(t *testing.T) {
17
- e := NewTaskEngine()
18
- e.synthesizeStreamFn = func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error {
19
- return onAudio([]byte("ok"))
20
- }
21
- e.newStreamPlayerFn = newFakeStreamPlayerFactory()
22
-
23
- cfg := Config{}
24
- voice := VoiceInfo{VoiceType: "v", ResourceID: "r"}
25
-
26
- e.Submit("a", voice, cfg)
27
- e.Submit("b", voice, cfg)
28
-
29
- e.mu.Lock()
30
- defer e.mu.Unlock()
31
- if len(e.pending) != 2 {
32
- t.Fatalf("expected 2 pending, got %d", len(e.pending))
33
- }
34
- if e.latestID != 2 {
35
- t.Fatalf("expected latestID 2, got %d", e.latestID)
36
- }
37
- }
38
-
39
- func TestTransactionDeletesOnSynthesisFailureWithoutRetry(t *testing.T) {
40
- e := NewTaskEngine()
41
- var mu sync.Mutex
42
- calls := 0
43
- e.synthesizeStreamFn = func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error {
44
- mu.Lock()
45
- defer mu.Unlock()
46
- calls++
47
- return errors.New("fail")
48
- }
49
- e.newStreamPlayerFn = newFakeStreamPlayerFactory()
50
- e.Start()
51
-
52
- cfg := Config{}
53
- voice := VoiceInfo{VoiceType: "v", ResourceID: "r"}
54
- id := e.Submit("x", voice, cfg)
55
-
56
- waitFor(t, 2*time.Second, func() bool {
57
- e.mu.Lock()
58
- defer e.mu.Unlock()
59
- _, ok := e.tasks[id]
60
- return !ok
61
- })
62
-
63
- mu.Lock()
64
- defer mu.Unlock()
65
- if calls != 1 {
66
- t.Fatalf("expected synth attempts=1, got %d", calls)
67
- }
68
- }
69
-
70
- func TestTransactionDeletesOnPlayerWriteFailureWithoutRetry(t *testing.T) {
71
- e := NewTaskEngine()
72
- e.synthesizeStreamFn = func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error {
73
- return onAudio([]byte("audio"))
74
- }
75
- var mu sync.Mutex
76
- calls := 0
77
- e.newStreamPlayerFn = func() (StreamPlayer, error) {
78
- mu.Lock()
79
- defer mu.Unlock()
80
- calls++
81
- return &fakeStreamPlayer{writeErr: calls == 1}, nil
82
- }
83
- e.Start()
84
-
85
- cfg := Config{}
86
- voice := VoiceInfo{VoiceType: "v", ResourceID: "r"}
87
- id := e.Submit("x", voice, cfg)
88
-
89
- waitFor(t, 2*time.Second, func() bool {
90
- e.mu.Lock()
91
- defer e.mu.Unlock()
92
- _, ok := e.tasks[id]
93
- return !ok
94
- })
95
-
96
- mu.Lock()
97
- defer mu.Unlock()
98
- if calls != 1 {
99
- t.Fatalf("expected player attempts=1, got %d", calls)
100
- }
101
- }
102
-
103
- func TestSubmitDoesNotInterruptRunningTask(t *testing.T) {
104
- e := NewTaskEngine()
105
- start := make(chan struct{}, 1)
106
- release := make(chan struct{})
107
- var mu sync.Mutex
108
- processed := make([]string, 0, 2)
109
-
110
- e.synthesizeStreamFn = func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error {
111
- if text == "a" {
112
- start <- struct{}{}
113
- }
114
- if text == "a" {
115
- <-release
116
- }
117
- mu.Lock()
118
- processed = append(processed, text)
119
- mu.Unlock()
120
- return onAudio([]byte(text))
121
- }
122
- e.newStreamPlayerFn = newFakeStreamPlayerFactory()
123
- e.Start()
124
-
125
- cfg := Config{}
126
- voice := VoiceInfo{VoiceType: "v", ResourceID: "r"}
127
- e.Submit("a", voice, cfg)
128
- <-start // a 已进入 running
129
- secondID := e.Submit("b", voice, cfg)
130
-
131
- e.mu.Lock()
132
- if len(e.pending) != 1 || e.pending[0] != secondID {
133
- t.Fatalf("expected second task pending while first keeps running, got %#v", e.pending)
134
- }
135
- e.mu.Unlock()
136
-
137
- close(release)
138
-
139
- waitFor(t, 3*time.Second, func() bool {
140
- e.mu.Lock()
141
- defer e.mu.Unlock()
142
- return len(e.tasks) == 0
143
- })
144
-
145
- mu.Lock()
146
- defer mu.Unlock()
147
- if strings.Join(processed, "") != "ab" {
148
- t.Fatalf("expected processed [a b], got %#v", processed)
149
- }
150
- }
151
-
152
- func TestClaimedStaleTaskIsSkippedBeforeTransaction(t *testing.T) {
153
- e := NewTaskEngine()
154
- calls := 0
155
- e.synthesizeStreamFn = func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error {
156
- calls++
157
- return onAudio([]byte(text))
158
- }
159
- e.newStreamPlayerFn = newFakeStreamPlayerFactory()
160
-
161
- cfg := Config{}
162
- voice := VoiceInfo{VoiceType: "v", ResourceID: "r"}
163
- firstID := e.Submit("a", voice, cfg)
164
- claimedID := e.claimPending()
165
- if claimedID != firstID {
166
- t.Fatalf("expected claimed first task %d, got %d", firstID, claimedID)
167
- }
168
- e.Submit("b", voice, cfg)
169
-
170
- e.processTransaction(firstID)
171
-
172
- if calls != 0 {
173
- t.Fatalf("expected stale task skipped before transaction, got calls=%d", calls)
174
- }
175
- e.mu.Lock()
176
- _, firstExists := e.tasks[firstID]
177
- e.mu.Unlock()
178
- if firstExists {
179
- t.Fatalf("expected stale task deleted")
180
- }
181
- }
182
-
183
- func TestSynthesisPanicDeletesTaskAndWorkerContinues(t *testing.T) {
184
- e := NewTaskEngine()
185
- e.synthesizeStreamFn = func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error {
186
- if text == "panic" {
187
- panic("boom")
188
- }
189
- return onAudio([]byte(text))
190
- }
191
- e.newStreamPlayerFn = newFakeStreamPlayerFactory()
192
- e.Start()
193
-
194
- cfg := Config{}
195
- voice := VoiceInfo{VoiceType: "v", ResourceID: "r"}
196
- panicID := e.Submit("panic", voice, cfg)
197
- waitForTaskDeleted(t, e, panicID)
198
-
199
- okID := e.Submit("ok", voice, cfg)
200
- waitForTaskDeleted(t, e, okID)
201
- }
202
-
203
- func TestPlaybackPanicDeletesTaskAndWorkerContinues(t *testing.T) {
204
- e := NewTaskEngine()
205
- e.synthesizeStreamFn = func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error {
206
- return onAudio([]byte(text))
207
- }
208
- var mu sync.Mutex
209
- calls := 0
210
- e.newStreamPlayerFn = func() (StreamPlayer, error) {
211
- mu.Lock()
212
- defer mu.Unlock()
213
- calls++
214
- return &fakeStreamPlayer{panicOnWrite: calls == 1}, nil
215
- }
216
- e.Start()
217
-
218
- cfg := Config{}
219
- voice := VoiceInfo{VoiceType: "v", ResourceID: "r"}
220
- panicID := e.Submit("panic", voice, cfg)
221
- waitForTaskDeleted(t, e, panicID)
222
-
223
- okID := e.Submit("ok", voice, cfg)
224
- waitForTaskDeleted(t, e, okID)
225
- }
226
-
227
14
  func TestParseSSEStreamWritesChunksInOrder(t *testing.T) {
228
15
  stream := strings.NewReader(
229
16
  "data: {\"audio\":\"YQ==\"}\n\n" +
@@ -469,96 +256,6 @@ func TestCleanTextSingleLineArtifactDoesNotSwallowFollowingText(t *testing.T) {
469
256
  }
470
257
  }
471
258
 
472
- func TestHandleConnectionPreservesMultilineBeforeCleaning(t *testing.T) {
473
- oldConfigDir := configDir
474
- oldCacheValid := configCacheValid
475
- oldCachePath := configCachePath
476
- oldCacheModTime := configCacheModTime
477
- oldCache := configCache
478
- t.Cleanup(func() {
479
- configDir = oldConfigDir
480
- configCacheValid = oldCacheValid
481
- configCachePath = oldCachePath
482
- configCacheModTime = oldCacheModTime
483
- configCache = oldCache
484
- })
485
-
486
- dir := t.TempDir()
487
- configDir = dir
488
- configCacheValid = false
489
- cfg := `{
490
- "apiKey": "key",
491
- "endpoint": "https://example.com/tts",
492
- "defaultVoice": {"voice_type": "voice", "resourceId": "resource"}
493
- }`
494
- if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(cfg), 0644); err != nil {
495
- t.Fatalf("write config: %v", err)
496
- }
497
-
498
- server, client := net.Pipe()
499
- e := NewTaskEngine()
500
- done := make(chan struct{})
501
- go func() {
502
- handleConnection(server, e)
503
- close(done)
504
- }()
505
-
506
- msg := strings.Join([]string{
507
- "不是,飞哥。",
508
- "",
509
- "| 部分 | 是否常驻 |",
510
- "|---|---|",
511
- "| ispeakd | 是 |",
512
- "",
513
- "也就是说:daemon 常驻,播放器不是常驻。",
514
- }, "\n")
515
- if _, err := client.Write([]byte(msg)); err != nil {
516
- t.Fatalf("write client: %v", err)
517
- }
518
- if err := client.Close(); err != nil {
519
- t.Fatalf("close client: %v", err)
520
- }
521
- select {
522
- case <-done:
523
- case <-time.After(time.Second):
524
- t.Fatalf("handleConnection did not return")
525
- }
526
-
527
- e.mu.Lock()
528
- defer e.mu.Unlock()
529
- if len(e.pending) != 1 {
530
- t.Fatalf("expected one pending task, got %d", len(e.pending))
531
- }
532
- task := e.tasks[e.pending[0]]
533
- if !strings.Contains(task.Text, "不是,飞哥。") ||
534
- !strings.Contains(task.Text, "也就是说:daemon 常驻,播放器不是常驻。") {
535
- t.Fatalf("expected surrounding text preserved, got %q", task.Text)
536
- }
537
- if strings.Contains(task.Text, "是否常驻") || strings.Contains(task.Text, "ispeakd | 是") {
538
- t.Fatalf("expected table removed, got %q", task.Text)
539
- }
540
- }
541
-
542
- func TestInvalidSSEAudioDeletesTaskAndWorkerContinues(t *testing.T) {
543
- e := NewTaskEngine()
544
- e.synthesizeStreamFn = func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error {
545
- if text == "bad" {
546
- return parseSSEStream(strings.NewReader("data: {\"audio\":\"***\"}\n\n"), onAudio)
547
- }
548
- return onAudio([]byte(text))
549
- }
550
- e.newStreamPlayerFn = newFakeStreamPlayerFactory()
551
- e.Start()
552
-
553
- cfg := Config{}
554
- voice := VoiceInfo{VoiceType: "v", ResourceID: "r"}
555
- badID := e.Submit("bad", voice, cfg)
556
- waitForTaskDeleted(t, e, badID)
557
-
558
- okID := e.Submit("ok", voice, cfg)
559
- waitForTaskDeleted(t, e, okID)
560
- }
561
-
562
259
  func TestValidateConfigRequiresDefaultVoiceResourceID(t *testing.T) {
563
260
  cfg := Config{
564
261
  APIKey: "key",
@@ -595,59 +292,6 @@ func TestValidateConfigRequiresSourceVoiceResourceID(t *testing.T) {
595
292
  }
596
293
  }
597
294
 
598
- type fakeStreamPlayer struct {
599
- writeErr bool
600
- closeErr bool
601
- panicOnWrite bool
602
- chunks [][]byte
603
- aborted bool
604
- closed bool
605
- closeBlock chan struct{}
606
- closeStarted chan struct{}
607
- closeOnce sync.Once
608
- }
609
-
610
- func newFakeStreamPlayerFactory() func() (StreamPlayer, error) {
611
- return func() (StreamPlayer, error) {
612
- return &fakeStreamPlayer{}, nil
613
- }
614
- }
615
-
616
- func (p *fakeStreamPlayer) Write(audio []byte) error {
617
- if p.panicOnWrite {
618
- panic("boom")
619
- }
620
- if p.writeErr {
621
- return errors.New("write failed")
622
- }
623
- p.chunks = append(p.chunks, append([]byte(nil), audio...))
624
- return nil
625
- }
626
-
627
- func (p *fakeStreamPlayer) CloseAndWait() error {
628
- p.closed = true
629
- if p.closeStarted != nil {
630
- p.closeStarted <- struct{}{}
631
- }
632
- if p.closeBlock != nil {
633
- <-p.closeBlock
634
- }
635
- if p.closeErr {
636
- return errors.New("close failed")
637
- }
638
- return nil
639
- }
640
-
641
- func (p *fakeStreamPlayer) Abort() error {
642
- p.aborted = true
643
- if p.closeBlock != nil {
644
- p.closeOnce.Do(func() {
645
- close(p.closeBlock)
646
- })
647
- }
648
- return nil
649
- }
650
-
651
295
  func TestListenUnixSocketRemovesStalePath(t *testing.T) {
652
296
  socketPath := shortSocketPath(t)
653
297
  if err := os.WriteFile(socketPath, []byte("stale"), 0644); err != nil {
@@ -722,25 +366,3 @@ func shortSocketPath(t *testing.T) string {
722
366
  })
723
367
  return filepath.Join(dir, "sock")
724
368
  }
725
-
726
- func waitForTaskDeleted(t *testing.T, e *TaskEngine, id uint64) {
727
- t.Helper()
728
- waitFor(t, 2*time.Second, func() bool {
729
- e.mu.Lock()
730
- defer e.mu.Unlock()
731
- _, ok := e.tasks[id]
732
- return !ok
733
- })
734
- }
735
-
736
- func waitFor(t *testing.T, timeout time.Duration, fn func() bool) {
737
- t.Helper()
738
- deadline := time.Now().Add(timeout)
739
- for time.Now().Before(deadline) {
740
- if fn() {
741
- return
742
- }
743
- time.Sleep(10 * time.Millisecond)
744
- }
745
- t.Fatalf("condition not met within %s", timeout)
746
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdfnet/ispeak",
3
- "version": "1.6.11",
3
+ "version": "1.6.12",
4
4
  "description": "Local macOS TTS daemon for AI coding assistants, powered by Volcengine streaming TTS.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/xdfnet/iSpeak#readme",