@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.
- package/main.go +65 -185
- package/main_test.go +0 -383
- 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
|
-
|
|
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
|
-
}
|
|
50
|
+
// 单播放器:新的打断旧的,不用队列
|
|
51
|
+
type Player struct {
|
|
52
|
+
mu sync.Mutex
|
|
53
|
+
gen int64
|
|
54
|
+
currentGen int64
|
|
55
|
+
player StreamPlayer
|
|
90
56
|
}
|
|
91
57
|
|
|
92
|
-
func (
|
|
93
|
-
|
|
58
|
+
func NewPlayer() *Player {
|
|
59
|
+
return &Player{}
|
|
94
60
|
}
|
|
95
61
|
|
|
96
|
-
func (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
206
|
-
|
|
118
|
+
p.mu.Lock()
|
|
119
|
+
if p.player == player {
|
|
120
|
+
p.player = nil
|
|
207
121
|
}
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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