@xdfnet/ispeak 1.6.11 → 1.6.13
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 +30 -174
- package/main_test.go +0 -378
- package/package.json +1 -1
package/main.go
CHANGED
|
@@ -47,193 +47,51 @@ type StreamPlayer interface {
|
|
|
47
47
|
Abort() error
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
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)
|
|
50
|
+
// 最简单的播放器:channel 队列,串行播报
|
|
51
|
+
type Player struct {
|
|
52
|
+
ch chan job
|
|
81
53
|
}
|
|
82
54
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
synthesizeStreamFn: synthesizeStream,
|
|
88
|
-
newStreamPlayerFn: newDefaultStreamPlayer,
|
|
89
|
-
}
|
|
55
|
+
type job struct {
|
|
56
|
+
text string
|
|
57
|
+
voice VoiceInfo
|
|
58
|
+
cfg Config
|
|
90
59
|
}
|
|
91
60
|
|
|
92
|
-
func (
|
|
93
|
-
|
|
61
|
+
func NewPlayer() *Player {
|
|
62
|
+
p := &Player{ch: make(chan job, 256)}
|
|
63
|
+
go p.loop()
|
|
64
|
+
return p
|
|
94
65
|
}
|
|
95
66
|
|
|
96
|
-
func (
|
|
97
|
-
|
|
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
|
|
67
|
+
func (p *Player) Submit(text string, voice VoiceInfo, cfg Config) {
|
|
68
|
+
log.Printf("TTS: %s", text)
|
|
69
|
+
p.ch <- job{text, voice, cfg}
|
|
117
70
|
}
|
|
118
71
|
|
|
119
|
-
func (
|
|
120
|
-
for {
|
|
121
|
-
|
|
122
|
-
if
|
|
123
|
-
|
|
72
|
+
func (p *Player) loop() {
|
|
73
|
+
for j := range p.ch {
|
|
74
|
+
player, err := newDefaultStreamPlayer()
|
|
75
|
+
if err != nil {
|
|
76
|
+
log.Printf("启动播放器失败: %v", err)
|
|
124
77
|
continue
|
|
125
78
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
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
|
-
}()
|
|
138
|
-
|
|
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)
|
|
152
|
-
return
|
|
79
|
+
p.play(j, player)
|
|
80
|
+
_ = player.CloseAndWait()
|
|
153
81
|
}
|
|
154
|
-
|
|
155
|
-
log.Printf("播报完成并删除任务: id=%d", id)
|
|
156
|
-
e.deleteTask(id)
|
|
157
82
|
}
|
|
158
83
|
|
|
159
|
-
func (
|
|
84
|
+
func (p *Player) play(j job, player StreamPlayer) {
|
|
160
85
|
startedAt := time.Now()
|
|
161
|
-
player, err := e.newStreamPlayerFn()
|
|
162
|
-
if err != nil {
|
|
163
|
-
return fmt.Errorf("启动播放器失败: %w", err)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
firstChunkLogged := false
|
|
167
86
|
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))
|
|
171
|
-
}
|
|
172
87
|
return player.Write(audio)
|
|
173
88
|
}
|
|
174
89
|
|
|
175
|
-
if err :=
|
|
176
|
-
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
log.Printf("TTS 流结束: id=%d elapsed=%s", task.ID, time.Since(startedAt).Round(time.Millisecond))
|
|
180
|
-
|
|
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()
|
|
191
|
-
|
|
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
|
|
198
|
-
}
|
|
199
|
-
if task.Status != TaskStatusPending {
|
|
200
|
-
continue
|
|
201
|
-
}
|
|
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:
|
|
90
|
+
if err := synthesizeStream(context.Background(), j.cfg, j.text, &j.voice, onAudio); err != nil {
|
|
91
|
+
log.Printf("TTS 合成失败: %v", err)
|
|
92
|
+
return
|
|
236
93
|
}
|
|
94
|
+
log.Printf("TTS: 完成 elapsed=%s", time.Since(startedAt).Round(time.Millisecond))
|
|
237
95
|
}
|
|
238
96
|
|
|
239
97
|
// 音色信息
|
|
@@ -595,8 +453,7 @@ func main() {
|
|
|
595
453
|
listener.Close()
|
|
596
454
|
}()
|
|
597
455
|
|
|
598
|
-
|
|
599
|
-
engine.Start()
|
|
456
|
+
player := NewPlayer()
|
|
600
457
|
|
|
601
458
|
log.Printf("iSpeak 已启动,监听 %s", socketPath)
|
|
602
459
|
for {
|
|
@@ -607,7 +464,7 @@ func main() {
|
|
|
607
464
|
}
|
|
608
465
|
continue
|
|
609
466
|
}
|
|
610
|
-
go handleConnection(conn,
|
|
467
|
+
go handleConnection(conn, player)
|
|
611
468
|
}
|
|
612
469
|
}
|
|
613
470
|
|
|
@@ -688,7 +545,7 @@ func validateVoiceInfo(name string, voice *VoiceInfo) error {
|
|
|
688
545
|
return nil
|
|
689
546
|
}
|
|
690
547
|
|
|
691
|
-
func handleConnection(conn net.Conn,
|
|
548
|
+
func handleConnection(conn net.Conn, player *Player) {
|
|
692
549
|
defer func() {
|
|
693
550
|
if r := recover(); r != nil {
|
|
694
551
|
log.Printf("连接处理崩溃: %v", r)
|
|
@@ -735,8 +592,7 @@ func handleConnection(conn net.Conn, engine *TaskEngine) {
|
|
|
735
592
|
return
|
|
736
593
|
}
|
|
737
594
|
|
|
738
|
-
|
|
739
|
-
engine.Submit(cleaned, *voice, cfg)
|
|
595
|
+
player.Submit(cleaned, *voice, cfg)
|
|
740
596
|
}
|
|
741
597
|
|
|
742
598
|
// 解析消息中的音色前缀,返回 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