@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.
- package/main.go +65 -179
- package/main_test.go +0 -378
- 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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 (
|
|
120
|
-
|
|
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 (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
if
|
|
141
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
200
|
-
|
|
118
|
+
p.mu.Lock()
|
|
119
|
+
if p.player == player {
|
|
120
|
+
p.player = nil
|
|
201
121
|
}
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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