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