@mingxy/ocosay 1.0.0
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/README.md +556 -0
- package/TECH_PLAN.md +352 -0
- package/__mocks__/@opencode-ai/plugin.ts +32 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +95 -0
- package/dist/config.js.map +1 -0
- package/dist/core/backends/afplay-backend.d.ts +33 -0
- package/dist/core/backends/afplay-backend.d.ts.map +1 -0
- package/dist/core/backends/afplay-backend.js +144 -0
- package/dist/core/backends/afplay-backend.js.map +1 -0
- package/dist/core/backends/aplay-backend.d.ts +33 -0
- package/dist/core/backends/aplay-backend.d.ts.map +1 -0
- package/dist/core/backends/aplay-backend.js +142 -0
- package/dist/core/backends/aplay-backend.js.map +1 -0
- package/dist/core/backends/base.d.ts +94 -0
- package/dist/core/backends/base.d.ts.map +1 -0
- package/dist/core/backends/base.js +6 -0
- package/dist/core/backends/base.js.map +1 -0
- package/dist/core/backends/index.d.ts +29 -0
- package/dist/core/backends/index.d.ts.map +1 -0
- package/dist/core/backends/index.js +114 -0
- package/dist/core/backends/index.js.map +1 -0
- package/dist/core/backends/naudiodon-backend.d.ts +52 -0
- package/dist/core/backends/naudiodon-backend.d.ts.map +1 -0
- package/dist/core/backends/naudiodon-backend.js +123 -0
- package/dist/core/backends/naudiodon-backend.js.map +1 -0
- package/dist/core/backends/powershell-backend.d.ts +34 -0
- package/dist/core/backends/powershell-backend.d.ts.map +1 -0
- package/dist/core/backends/powershell-backend.js +154 -0
- package/dist/core/backends/powershell-backend.js.map +1 -0
- package/dist/core/player.d.ts +97 -0
- package/dist/core/player.d.ts.map +1 -0
- package/dist/core/player.js +268 -0
- package/dist/core/player.js.map +1 -0
- package/dist/core/speaker.d.ts +97 -0
- package/dist/core/speaker.d.ts.map +1 -0
- package/dist/core/speaker.js +218 -0
- package/dist/core/speaker.js.map +1 -0
- package/dist/core/stream-player.d.ts +107 -0
- package/dist/core/stream-player.d.ts.map +1 -0
- package/dist/core/stream-player.js +272 -0
- package/dist/core/stream-player.js.map +1 -0
- package/dist/core/stream-reader.d.ts +86 -0
- package/dist/core/stream-reader.d.ts.map +1 -0
- package/dist/core/stream-reader.js +172 -0
- package/dist/core/stream-reader.js.map +1 -0
- package/dist/core/streaming-synthesizer.d.ts +51 -0
- package/dist/core/streaming-synthesizer.d.ts.map +1 -0
- package/dist/core/streaming-synthesizer.js +103 -0
- package/dist/core/streaming-synthesizer.js.map +1 -0
- package/dist/core/types.d.ts +141 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +37 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +179 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +151 -0
- package/dist/plugin.js.map +1 -0
- package/dist/providers/base.d.ts +55 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +95 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/minimax.d.ts +84 -0
- package/dist/providers/minimax.d.ts.map +1 -0
- package/dist/providers/minimax.js +387 -0
- package/dist/providers/minimax.js.map +1 -0
- package/dist/tools/tts.d.ts +147 -0
- package/dist/tools/tts.d.ts.map +1 -0
- package/dist/tools/tts.js +232 -0
- package/dist/tools/tts.js.map +1 -0
- package/jest.config.js +15 -0
- package/package.json +49 -0
- package/src/config.ts +121 -0
- package/src/core/backends/afplay-backend.ts +162 -0
- package/src/core/backends/aplay-backend.ts +160 -0
- package/src/core/backends/base.ts +117 -0
- package/src/core/backends/index.ts +128 -0
- package/src/core/backends/naudiodon-backend.ts +164 -0
- package/src/core/backends/powershell-backend.ts +173 -0
- package/src/core/player.ts +322 -0
- package/src/core/speaker.ts +283 -0
- package/src/core/stream-player.ts +326 -0
- package/src/core/stream-reader.ts +190 -0
- package/src/core/streaming-synthesizer.ts +123 -0
- package/src/core/types.ts +185 -0
- package/src/index.ts +233 -0
- package/src/plugin.ts +166 -0
- package/src/providers/base.ts +150 -0
- package/src/providers/minimax.ts +515 -0
- package/src/tools/tts.ts +277 -0
- package/src/types/naudiodon.d.ts +19 -0
- package/tests/__mocks__/@opencode-ai/plugin.ts +32 -0
- package/tests/backends.test.ts +831 -0
- package/tests/index.test.ts +201 -0
- package/tests/integration-test.d.ts +6 -0
- package/tests/integration-test.d.ts.map +1 -0
- package/tests/integration-test.js +84 -0
- package/tests/integration-test.js.map +1 -0
- package/tests/integration-test.ts +93 -0
- package/tests/p1-fixes.test.ts +160 -0
- package/tests/plugin.test.ts +311 -0
- package/tests/provider.test.d.ts +2 -0
- package/tests/provider.test.d.ts.map +1 -0
- package/tests/provider.test.js +69 -0
- package/tests/provider.test.js.map +1 -0
- package/tests/provider.test.ts +87 -0
- package/tests/speaker.test.d.ts +2 -0
- package/tests/speaker.test.d.ts.map +1 -0
- package/tests/speaker.test.js +63 -0
- package/tests/speaker.test.js.map +1 -0
- package/tests/speaker.test.ts +232 -0
- package/tests/stream-player.test.ts +303 -0
- package/tests/stream-reader.test.ts +269 -0
- package/tests/streaming-synthesizer.test.ts +225 -0
- package/tests/tts-tools.test.ts +270 -0
- package/tests/types.test.d.ts +2 -0
- package/tests/types.test.d.ts.map +1 -0
- package/tests/types.test.js +61 -0
- package/tests/types.test.js.map +1 -0
- package/tests/types.test.ts +63 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamReader - 流式文本缓冲与句子边界检测
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 订阅 TuiEventBus 的 message.part.delta 事件
|
|
6
|
+
* - 缓冲区满或遇到句子结束符时触发 textReady 事件
|
|
7
|
+
* - 支持超时强制发送
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { EventEmitter } from 'events'
|
|
11
|
+
import { StreamState, TTSError, TTSErrorCode } from './types'
|
|
12
|
+
|
|
13
|
+
export class StreamReader extends EventEmitter {
|
|
14
|
+
private state: StreamState = StreamState.IDLE
|
|
15
|
+
private buffer: string = ''
|
|
16
|
+
private sessionID?: string
|
|
17
|
+
private messageID?: string
|
|
18
|
+
private partID?: string
|
|
19
|
+
private timeoutHandle?: NodeJS.Timeout
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private bufferSize: number = 30,
|
|
23
|
+
private bufferTimeout: number = 2000
|
|
24
|
+
) {
|
|
25
|
+
super()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 启动流式监听
|
|
30
|
+
* 将状态从 IDLE 切换到 BUFFERING,开始监听事件
|
|
31
|
+
*/
|
|
32
|
+
start(): void {
|
|
33
|
+
if (this.state === StreamState.IDLE) {
|
|
34
|
+
this.state = StreamState.BUFFERING
|
|
35
|
+
this.emit('streamStart')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 处理 message.part.delta 事件
|
|
41
|
+
*/
|
|
42
|
+
handleDelta(sessionID: string, messageID: string, partID: string, delta: string): void {
|
|
43
|
+
if (this.state === StreamState.IDLE) {
|
|
44
|
+
this.state = StreamState.BUFFERING
|
|
45
|
+
this.sessionID = sessionID
|
|
46
|
+
this.messageID = messageID
|
|
47
|
+
this.partID = partID
|
|
48
|
+
this.emit('streamStart')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.buffer += delta
|
|
52
|
+
this.resetTimeout()
|
|
53
|
+
|
|
54
|
+
if (this.shouldFlush()) {
|
|
55
|
+
this.flushBuffer()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 处理流结束
|
|
61
|
+
*/
|
|
62
|
+
handleEnd(): void {
|
|
63
|
+
if (this.state === StreamState.ENDED) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
if (this.buffer.length > 0) {
|
|
67
|
+
this.flushBuffer()
|
|
68
|
+
}
|
|
69
|
+
this.state = StreamState.ENDED
|
|
70
|
+
this.clearTimeout()
|
|
71
|
+
this.emit('streamEnd')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 处理错误
|
|
76
|
+
*/
|
|
77
|
+
handleError(error: TTSError): void {
|
|
78
|
+
this.clearTimeout()
|
|
79
|
+
this.state = StreamState.IDLE
|
|
80
|
+
this.buffer = ''
|
|
81
|
+
this.emit('streamError', error)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 重置缓冲器
|
|
86
|
+
*/
|
|
87
|
+
reset(): void {
|
|
88
|
+
this.state = StreamState.IDLE
|
|
89
|
+
this.buffer = ''
|
|
90
|
+
this.sessionID = undefined
|
|
91
|
+
this.messageID = undefined
|
|
92
|
+
this.partID = undefined
|
|
93
|
+
this.clearTimeout()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 判断是否应该刷新缓冲区
|
|
98
|
+
* 条件:
|
|
99
|
+
* 1. 包含句子结束符(任何长度)
|
|
100
|
+
* 2. 缓冲区长度 >= bufferSize
|
|
101
|
+
*/
|
|
102
|
+
private shouldFlush(): boolean {
|
|
103
|
+
// 句子结束标记:。!?.!?……(中文句号、感叹号、问号、省略号)
|
|
104
|
+
const sentenceEnd = /[。!?.!?]|……/
|
|
105
|
+
if (sentenceEnd.test(this.buffer)) {
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
// 缓冲区达到阈值
|
|
109
|
+
if (this.buffer.length >= this.bufferSize) {
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 刷新缓冲区,发送textReady事件
|
|
117
|
+
*/
|
|
118
|
+
private flushBuffer(): void {
|
|
119
|
+
const text = this.buffer.trim()
|
|
120
|
+
if (text.length > 0) {
|
|
121
|
+
this.emit('textReady', text)
|
|
122
|
+
}
|
|
123
|
+
this.buffer = ''
|
|
124
|
+
this.resetTimeout()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 重置超时计时器
|
|
129
|
+
*/
|
|
130
|
+
private resetTimeout(): void {
|
|
131
|
+
this.clearTimeout()
|
|
132
|
+
this.timeoutHandle = setTimeout(() => {
|
|
133
|
+
if (this.buffer.length > 0) {
|
|
134
|
+
this.flushBuffer()
|
|
135
|
+
}
|
|
136
|
+
}, this.bufferTimeout)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 清除超时计时器
|
|
141
|
+
*/
|
|
142
|
+
private clearTimeout(): void {
|
|
143
|
+
if (this.timeoutHandle) {
|
|
144
|
+
clearTimeout(this.timeoutHandle)
|
|
145
|
+
this.timeoutHandle = undefined
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 获取当前状态
|
|
151
|
+
*/
|
|
152
|
+
getState(): StreamState {
|
|
153
|
+
return this.state
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 检查流是否处于活跃状态
|
|
158
|
+
*/
|
|
159
|
+
isActive(): boolean {
|
|
160
|
+
return this.state === StreamState.BUFFERING
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 获取当前缓冲区内容
|
|
165
|
+
*/
|
|
166
|
+
getBuffer(): string {
|
|
167
|
+
return this.buffer
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 获取当前会话ID
|
|
172
|
+
*/
|
|
173
|
+
getSessionID(): string | undefined {
|
|
174
|
+
return this.sessionID
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 获取当前消息ID
|
|
179
|
+
*/
|
|
180
|
+
getMessageID(): string | undefined {
|
|
181
|
+
return this.messageID
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 获取当前分块ID
|
|
186
|
+
*/
|
|
187
|
+
getPartID(): string | undefined {
|
|
188
|
+
return this.partID
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamingSynthesizer - 流式合成器
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 接收 StreamReader 发来的文本(通过 synthesize 方法)
|
|
6
|
+
* - 调用 TTSProvider 的流式合成接口
|
|
7
|
+
* - 将返回的音频 chunk 传递给下游(StreamPlayer)
|
|
8
|
+
*
|
|
9
|
+
* 数据流:
|
|
10
|
+
* StreamReader.textReady → StreamingSynthesizer.synthesize() → StreamPlayer (边收边播)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { EventEmitter } from 'events'
|
|
14
|
+
import { TTSProvider, TTSError, TTSErrorCode, StreamingSynthesizerOptions, AudioResult } from './types'
|
|
15
|
+
|
|
16
|
+
export interface StreamingSynthesizerEvents {
|
|
17
|
+
on(event: 'chunk', handler: (chunk: Buffer) => void): void
|
|
18
|
+
on(event: 'error', handler: (error: TTSError) => void): void
|
|
19
|
+
on(event: 'done', handler: () => void): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class StreamingSynthesizer extends EventEmitter {
|
|
23
|
+
private audioChunks: Buffer[] = []
|
|
24
|
+
|
|
25
|
+
constructor(private options: StreamingSynthesizerOptions) {
|
|
26
|
+
super()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 发送文本片段进行合成
|
|
31
|
+
* 调用 provider.speak() 并处理返回的音频流
|
|
32
|
+
*/
|
|
33
|
+
async synthesize(text: string): Promise<void> {
|
|
34
|
+
if (!text || text.trim().length === 0) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const result = await this.options.provider.speak(text, {
|
|
40
|
+
model: 'stream',
|
|
41
|
+
voice: this.options.voice,
|
|
42
|
+
speed: this.options.speed,
|
|
43
|
+
volume: this.options.volume,
|
|
44
|
+
pitch: this.options.pitch
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
await this.processAudioResult(result)
|
|
48
|
+
|
|
49
|
+
this.emit('done')
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const ttsError = error instanceof TTSError
|
|
52
|
+
? error
|
|
53
|
+
: new TTSError(
|
|
54
|
+
error instanceof Error ? error.message : 'Synthesis failed',
|
|
55
|
+
'UNKNOWN' as TTSErrorCode,
|
|
56
|
+
this.options.provider.name,
|
|
57
|
+
error
|
|
58
|
+
)
|
|
59
|
+
this.emit('error', ttsError)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 处理 AudioResult,根据 audioData 类型进行相应处理
|
|
65
|
+
*/
|
|
66
|
+
private async processAudioResult(result: AudioResult): Promise<void> {
|
|
67
|
+
if (result.isStream && result.audioData instanceof ReadableStream) {
|
|
68
|
+
// 流式数据:ReadableStream
|
|
69
|
+
await this.processReadableStream(result.audioData)
|
|
70
|
+
} else if (Buffer.isBuffer(result.audioData)) {
|
|
71
|
+
// 非流式数据:Buffer
|
|
72
|
+
this.emitChunk(result.audioData)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 处理 ReadableStream,逐chunk emit
|
|
78
|
+
*/
|
|
79
|
+
private async processReadableStream(stream: ReadableStream): Promise<void> {
|
|
80
|
+
const reader = stream.getReader()
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
while (true) {
|
|
84
|
+
const { done, value } = await reader.read()
|
|
85
|
+
|
|
86
|
+
if (done) {
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (value) {
|
|
91
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value)
|
|
92
|
+
this.emitChunk(chunk)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
reader.releaseLock()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* emit chunk 并累积
|
|
102
|
+
*/
|
|
103
|
+
private emitChunk(chunk: Buffer): void {
|
|
104
|
+
this.audioChunks.push(chunk)
|
|
105
|
+
this.emit('chunk', chunk)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 重置状态
|
|
110
|
+
* 清空累积的音频数据
|
|
111
|
+
*/
|
|
112
|
+
reset(): void {
|
|
113
|
+
this.audioChunks = []
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 获取累积的音频数据
|
|
118
|
+
* 返回所有已接收的 chunk
|
|
119
|
+
*/
|
|
120
|
+
getAudioChunks(): Buffer[] {
|
|
121
|
+
return [...this.audioChunks]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTS Core Types
|
|
3
|
+
* 核心类型定义
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export enum TTSErrorCode {
|
|
7
|
+
NETWORK = 'NETWORK',
|
|
8
|
+
AUTH = 'AUTH',
|
|
9
|
+
QUOTA = 'QUOTA',
|
|
10
|
+
INVALID_VOICE = 'INVALID_VOICE',
|
|
11
|
+
INVALID_PARAMS = 'INVALID_PARAMS',
|
|
12
|
+
PLAYER_ERROR = 'PLAYER_ERROR',
|
|
13
|
+
UNKNOWN = 'UNKNOWN'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class TTSError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
message: string,
|
|
19
|
+
code: TTSErrorCode,
|
|
20
|
+
provider: string,
|
|
21
|
+
details?: unknown
|
|
22
|
+
) {
|
|
23
|
+
super(message)
|
|
24
|
+
this.name = 'TTSError'
|
|
25
|
+
this.code = code
|
|
26
|
+
this.provider = provider
|
|
27
|
+
this.details = details
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
code: TTSErrorCode
|
|
31
|
+
provider: string
|
|
32
|
+
details?: unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Voice {
|
|
36
|
+
id: string
|
|
37
|
+
name: string
|
|
38
|
+
language?: string
|
|
39
|
+
gender?: 'male' | 'female' | 'neutral'
|
|
40
|
+
previewUrl?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TTSCapabilities {
|
|
44
|
+
speak: true
|
|
45
|
+
voiceClone?: boolean
|
|
46
|
+
stream?: boolean
|
|
47
|
+
voiceList?: boolean
|
|
48
|
+
sync?: boolean
|
|
49
|
+
async?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type SynthesisModel = 'sync' | 'async' | 'stream'
|
|
53
|
+
|
|
54
|
+
export interface SpeakOptions {
|
|
55
|
+
voice?: string
|
|
56
|
+
model?: SynthesisModel
|
|
57
|
+
speed?: number
|
|
58
|
+
volume?: number
|
|
59
|
+
pitch?: number
|
|
60
|
+
sourceVoice?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AudioResult {
|
|
64
|
+
audioData: Buffer | ReadableStream
|
|
65
|
+
sampleRate?: number
|
|
66
|
+
channels?: number
|
|
67
|
+
duration?: number
|
|
68
|
+
format: string
|
|
69
|
+
isStream: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type TTSEvent =
|
|
73
|
+
| 'start'
|
|
74
|
+
| 'end'
|
|
75
|
+
| 'error'
|
|
76
|
+
| 'progress'
|
|
77
|
+
| 'pause'
|
|
78
|
+
| 'resume'
|
|
79
|
+
| 'stop'
|
|
80
|
+
|
|
81
|
+
export interface SpeakerEvents {
|
|
82
|
+
on(event: 'start', handler: (text: string) => void): void
|
|
83
|
+
on(event: 'end', handler: (text: string) => void): void
|
|
84
|
+
on(event: 'error', handler: (error: TTSError) => void): void
|
|
85
|
+
on(event: 'progress', handler: (progress: { current: number; total: number }) => void): void
|
|
86
|
+
on(event: 'pause', handler: () => void): void
|
|
87
|
+
on(event: 'resume', handler: () => void): void
|
|
88
|
+
on(event: 'stop', handler: () => void): void
|
|
89
|
+
off(event: TTSEvent, handler: Function): void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface TTSProvider {
|
|
93
|
+
name: string
|
|
94
|
+
capabilities: TTSCapabilities
|
|
95
|
+
|
|
96
|
+
initialize(): Promise<void>
|
|
97
|
+
destroy(): Promise<void>
|
|
98
|
+
|
|
99
|
+
speak(text: string, options?: SpeakOptions): Promise<AudioResult>
|
|
100
|
+
pause(): Promise<void>
|
|
101
|
+
resume(): Promise<void>
|
|
102
|
+
stop(): Promise<void>
|
|
103
|
+
listVoices(): Promise<Voice[]>
|
|
104
|
+
|
|
105
|
+
getCapabilities(): TTSCapabilities
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// 配置相关类型
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
export interface GlobalConfig {
|
|
113
|
+
defaultProvider: string
|
|
114
|
+
defaultModel?: SynthesisModel
|
|
115
|
+
defaultVoice?: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ProviderConfig {
|
|
119
|
+
enabled?: boolean
|
|
120
|
+
apiKey?: string
|
|
121
|
+
[key: string]: unknown
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface OcosayConfig {
|
|
125
|
+
enabled?: boolean
|
|
126
|
+
autoPlay?: boolean
|
|
127
|
+
autoRead?: boolean
|
|
128
|
+
streamMode?: boolean
|
|
129
|
+
streamBufferSize?: number
|
|
130
|
+
streamBufferTimeout?: number
|
|
131
|
+
provider?: string
|
|
132
|
+
ttsModel?: string
|
|
133
|
+
baseURL?: string
|
|
134
|
+
speed?: number
|
|
135
|
+
volume?: number
|
|
136
|
+
pitch?: number
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// 流式朗读相关类型
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
export enum StreamState {
|
|
144
|
+
IDLE = 'idle',
|
|
145
|
+
BUFFERING = 'buffering',
|
|
146
|
+
STREAMING = 'streaming',
|
|
147
|
+
ENDED = 'ended'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface OcosayStreamConfig {
|
|
151
|
+
enabled: boolean
|
|
152
|
+
autoPlay: boolean
|
|
153
|
+
autoRead: boolean
|
|
154
|
+
streamMode: boolean
|
|
155
|
+
streamBufferSize: number
|
|
156
|
+
streamBufferTimeout: number
|
|
157
|
+
provider: string
|
|
158
|
+
voiceId?: string
|
|
159
|
+
ttsModel?: string
|
|
160
|
+
baseURL?: string
|
|
161
|
+
speed?: number
|
|
162
|
+
volume?: number
|
|
163
|
+
pitch?: number
|
|
164
|
+
apiKey?: string
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface StreamReaderEvents {
|
|
168
|
+
onTextReady: (text: string) => void
|
|
169
|
+
onStreamStart: () => void
|
|
170
|
+
onStreamEnd: () => void
|
|
171
|
+
onStreamError: (error: TTSError) => void
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface StreamingSynthesizerOptions {
|
|
175
|
+
provider: TTSProvider
|
|
176
|
+
voice?: string
|
|
177
|
+
speed?: number
|
|
178
|
+
volume?: number
|
|
179
|
+
pitch?: number
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface StreamPlayerOptions {
|
|
183
|
+
format?: 'mp3' | 'wav' | 'flac'
|
|
184
|
+
onProgress?: (bytesReceived: number) => void
|
|
185
|
+
}
|