@mingxy/ocosay 1.0.35 → 1.1.1
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/dist/config.js +4 -1
- package/dist/core/backends/index.js +8 -3
- package/dist/core/notification.d.ts +27 -0
- package/dist/core/notification.js +86 -0
- package/dist/core/speaker.js +6 -35
- package/dist/core/stream-reader.js +2 -1
- package/dist/core/streaming-synthesizer.js +2 -0
- package/dist/index.d.ts +1 -13
- package/dist/index.js +4 -23
- package/dist/package.json +2 -2
- package/dist/plugin.js +990 -586
- package/dist/providers/minimax.js +43 -20
- package/dist/services/notification-service.d.ts +17 -0
- package/dist/services/notification-service.js +74 -0
- package/dist/services/speaker-service.d.ts +34 -0
- package/dist/services/speaker-service.js +74 -0
- package/dist/services/streaming-service.d.ts +109 -0
- package/dist/services/streaming-service.js +281 -0
- package/dist/tools/tts.js +32 -19
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.js +42 -6
- package/package.json +2 -2
- package/.idea/UniappTool.xml +0 -10
- package/.idea/inspectionProfiles/profiles_settings.xml +0 -5
- package/.idea/modules.xml +0 -8
- package/.idea/ocosay.iml +0 -12
- package/.idea/vcs.xml +0 -6
- package/.sisyphus/boulder.json +0 -23
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/core/backends/afplay-backend.d.ts.map +0 -1
- package/dist/core/backends/afplay-backend.js.map +0 -1
- package/dist/core/backends/aplay-backend.d.ts.map +0 -1
- package/dist/core/backends/aplay-backend.js.map +0 -1
- package/dist/core/backends/base.d.ts.map +0 -1
- package/dist/core/backends/base.js.map +0 -1
- package/dist/core/backends/howler-backend.d.ts.map +0 -1
- package/dist/core/backends/howler-backend.js.map +0 -1
- package/dist/core/backends/index.d.ts.map +0 -1
- package/dist/core/backends/index.js.map +0 -1
- package/dist/core/backends/naudiodon-backend.d.ts.map +0 -1
- package/dist/core/backends/naudiodon-backend.js.map +0 -1
- package/dist/core/backends/powershell-backend.d.ts.map +0 -1
- package/dist/core/backends/powershell-backend.js.map +0 -1
- package/dist/core/logger.d.ts.map +0 -1
- package/dist/core/logger.js.map +0 -1
- package/dist/core/player.d.ts.map +0 -1
- package/dist/core/player.js.map +0 -1
- package/dist/core/speaker.d.ts.map +0 -1
- package/dist/core/speaker.js.map +0 -1
- package/dist/core/stream-player.d.ts.map +0 -1
- package/dist/core/stream-player.js.map +0 -1
- package/dist/core/stream-reader.d.ts.map +0 -1
- package/dist/core/stream-reader.js.map +0 -1
- package/dist/core/streaming-synthesizer.d.ts.map +0 -1
- package/dist/core/streaming-synthesizer.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js.map +0 -7
- package/dist/providers/base.d.ts.map +0 -1
- package/dist/providers/base.js.map +0 -1
- package/dist/providers/minimax.d.ts.map +0 -1
- package/dist/providers/minimax.js.map +0 -1
- package/dist/tools/tts.d.ts.map +0 -1
- package/dist/tools/tts.js.map +0 -1
- package/dist/types/config.d.ts.map +0 -1
- package/dist/types/config.js.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js.map +0 -1
|
@@ -98,36 +98,59 @@ export class MiniMaxProvider extends BaseTTSProvider {
|
|
|
98
98
|
});
|
|
99
99
|
const stream = response.data;
|
|
100
100
|
const audioChunks = [];
|
|
101
|
+
let lineBuffer = '';
|
|
101
102
|
return new Promise((resolve, reject) => {
|
|
102
103
|
stream.on('data', (chunk) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
lineBuffer += chunk.toString();
|
|
105
|
+
while (true) {
|
|
106
|
+
const lineEnd = lineBuffer.indexOf('\n');
|
|
107
|
+
if (lineEnd === -1)
|
|
108
|
+
break;
|
|
109
|
+
const line = lineBuffer.slice(0, lineEnd).trim();
|
|
110
|
+
lineBuffer = lineBuffer.slice(lineEnd + 1);
|
|
111
|
+
if (!line || !line.startsWith('data:'))
|
|
112
|
+
continue;
|
|
113
|
+
const jsonStr = line.slice(5).trim();
|
|
114
|
+
if (!jsonStr)
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
const data = JSON.parse(jsonStr);
|
|
118
|
+
if (data.audio) {
|
|
119
|
+
audioChunks.push(Buffer.from(data.audio, 'hex'));
|
|
120
|
+
}
|
|
121
|
+
if (data.is_final === true) {
|
|
122
|
+
const fullAudio = Buffer.concat(audioChunks);
|
|
123
|
+
resolve({
|
|
124
|
+
audioData: fullAudio,
|
|
125
|
+
format: this.audioFormat,
|
|
126
|
+
isStream: true,
|
|
127
|
+
duration: this.estimateDuration(fullAudio.length)
|
|
128
|
+
});
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
catch (e) {
|
|
132
|
+
// ignore
|
|
133
|
+
}
|
|
125
134
|
}
|
|
126
135
|
});
|
|
127
136
|
stream.on('error', (err) => {
|
|
128
137
|
reject(new TTSError('Stream error', TTSErrorCode.NETWORK, this.name, err));
|
|
129
138
|
});
|
|
130
139
|
stream.on('end', () => {
|
|
140
|
+
if (lineBuffer.trim() && lineBuffer.startsWith('data:')) {
|
|
141
|
+
const jsonStr = lineBuffer.slice(5).trim();
|
|
142
|
+
if (jsonStr) {
|
|
143
|
+
try {
|
|
144
|
+
const data = JSON.parse(jsonStr);
|
|
145
|
+
if (data.audio) {
|
|
146
|
+
audioChunks.push(Buffer.from(data.audio, 'hex'));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
// ignore
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
131
154
|
if (audioChunks.length > 0) {
|
|
132
155
|
const fullAudio = Buffer.concat(audioChunks);
|
|
133
156
|
resolve({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
2
|
+
export declare class NotificationService {
|
|
3
|
+
private constructor();
|
|
4
|
+
static getInstance(): NotificationService;
|
|
5
|
+
initialize(tuiInstance: any): void;
|
|
6
|
+
isReady(): boolean;
|
|
7
|
+
showToast(message: string, type?: ToastType): void;
|
|
8
|
+
success(message: string): void;
|
|
9
|
+
error(message: string): void;
|
|
10
|
+
warning(message: string): void;
|
|
11
|
+
info(message: string): void;
|
|
12
|
+
private getTitleForType;
|
|
13
|
+
private fallbackLog;
|
|
14
|
+
}
|
|
15
|
+
export declare function showToast(message: string, type?: ToastType): void;
|
|
16
|
+
export declare function initializeNotificationService(tuiInstance: any): void;
|
|
17
|
+
//# sourceMappingURL=notification-service.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.js';
|
|
2
|
+
let instance;
|
|
3
|
+
let tui = null;
|
|
4
|
+
let initialized = false;
|
|
5
|
+
export class NotificationService {
|
|
6
|
+
constructor() { }
|
|
7
|
+
static getInstance() {
|
|
8
|
+
if (!instance) {
|
|
9
|
+
instance = new NotificationService();
|
|
10
|
+
}
|
|
11
|
+
return instance;
|
|
12
|
+
}
|
|
13
|
+
initialize(tuiInstance) {
|
|
14
|
+
if (initialized)
|
|
15
|
+
return;
|
|
16
|
+
tui = tuiInstance;
|
|
17
|
+
initialized = true;
|
|
18
|
+
logger.debug('NotificationService initialized');
|
|
19
|
+
}
|
|
20
|
+
isReady() {
|
|
21
|
+
return initialized && tui !== null;
|
|
22
|
+
}
|
|
23
|
+
showToast(message, type = 'info') {
|
|
24
|
+
const title = this.getTitleForType(type);
|
|
25
|
+
if (tui?.showToast) {
|
|
26
|
+
try {
|
|
27
|
+
tui.showToast({
|
|
28
|
+
title,
|
|
29
|
+
message,
|
|
30
|
+
variant: type,
|
|
31
|
+
duration: type === 'error' ? 8000 : 5000
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
logger.warn({ err }, 'tui.showToast failed');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.fallbackLog(type, title, message);
|
|
40
|
+
}
|
|
41
|
+
success(message) { this.showToast(message, 'success'); }
|
|
42
|
+
error(message) { this.showToast(message, 'error'); }
|
|
43
|
+
warning(message) { this.showToast(message, 'warning'); }
|
|
44
|
+
info(message) { this.showToast(message, 'info'); }
|
|
45
|
+
getTitleForType(type) {
|
|
46
|
+
const titles = {
|
|
47
|
+
success: 'Success',
|
|
48
|
+
error: 'Error',
|
|
49
|
+
warning: 'Warning',
|
|
50
|
+
info: 'Info'
|
|
51
|
+
};
|
|
52
|
+
return titles[type];
|
|
53
|
+
}
|
|
54
|
+
fallbackLog(type, title, message) {
|
|
55
|
+
const timestamp = new Date().toISOString();
|
|
56
|
+
switch (type) {
|
|
57
|
+
case 'error':
|
|
58
|
+
logger.error({ title, message, timestamp }, 'Toast (fallback)');
|
|
59
|
+
break;
|
|
60
|
+
case 'warning':
|
|
61
|
+
logger.warn({ title, message, timestamp }, 'Toast (fallback)');
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
logger.info({ title, message, timestamp }, 'Toast (fallback)');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function showToast(message, type = 'info') {
|
|
69
|
+
NotificationService.getInstance().showToast(message, type);
|
|
70
|
+
}
|
|
71
|
+
export function initializeNotificationService(tuiInstance) {
|
|
72
|
+
NotificationService.getInstance().initialize(tuiInstance);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=notification-service.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Voice, SpeakOptions } from '../core/types';
|
|
2
|
+
export interface speakerServiceOptions {
|
|
3
|
+
defaultProvider?: string;
|
|
4
|
+
defaultModel?: 'sync' | 'async' | 'stream';
|
|
5
|
+
defaultVoice?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class SpeakerService {
|
|
8
|
+
private options;
|
|
9
|
+
private speaker;
|
|
10
|
+
constructor(options?: speakerServiceOptions);
|
|
11
|
+
speak(text: string, options?: SpeakOptions & {
|
|
12
|
+
provider?: string;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
pause(): void;
|
|
15
|
+
resume(): void;
|
|
16
|
+
stop(): Promise<void>;
|
|
17
|
+
listVoices(providerName?: string): Promise<Voice[]>;
|
|
18
|
+
getCapabilities(providerName?: string): import("../core/types").TTSCapabilities;
|
|
19
|
+
getProviders(): string[];
|
|
20
|
+
isPlaying(): boolean;
|
|
21
|
+
isPausedState(): boolean;
|
|
22
|
+
destroy(): Promise<void>;
|
|
23
|
+
private getTimestamp;
|
|
24
|
+
}
|
|
25
|
+
export declare function getDefaultSpeakerService(): SpeakerService;
|
|
26
|
+
export declare function speak(text: string, options?: SpeakOptions & {
|
|
27
|
+
provider?: string;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
export declare function stop(): Promise<void>;
|
|
30
|
+
export declare function pause(): void;
|
|
31
|
+
export declare function resume(): void;
|
|
32
|
+
export declare function listVoices(providerName?: string): Promise<Voice[]>;
|
|
33
|
+
export default SpeakerService;
|
|
34
|
+
//# sourceMappingURL=speaker-service.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getDefaultSpeaker, speak as coreSpeak, stop as coreStop, pause as corePause, resume as coreResume, listVoices as coreListVoices } from '../core/speaker';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
export class SpeakerService {
|
|
4
|
+
options;
|
|
5
|
+
speaker;
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.speaker = getDefaultSpeaker();
|
|
9
|
+
}
|
|
10
|
+
async speak(text, options) {
|
|
11
|
+
const timestamp = this.getTimestamp();
|
|
12
|
+
logger.info(`[Ocosay][${timestamp}][INFO][Speaker] 对应事件{播放开始} - 文本长度: ${text.length}`);
|
|
13
|
+
return this.speaker.speak(text, options);
|
|
14
|
+
}
|
|
15
|
+
pause() {
|
|
16
|
+
this.speaker.pause();
|
|
17
|
+
}
|
|
18
|
+
resume() {
|
|
19
|
+
this.speaker.resume();
|
|
20
|
+
}
|
|
21
|
+
async stop() {
|
|
22
|
+
return this.speaker.stop();
|
|
23
|
+
}
|
|
24
|
+
async listVoices(providerName) {
|
|
25
|
+
return this.speaker.listVoices(providerName);
|
|
26
|
+
}
|
|
27
|
+
getCapabilities(providerName) {
|
|
28
|
+
return this.speaker.getCapabilities(providerName);
|
|
29
|
+
}
|
|
30
|
+
getProviders() {
|
|
31
|
+
return this.speaker.getProviders();
|
|
32
|
+
}
|
|
33
|
+
isPlaying() {
|
|
34
|
+
return this.speaker.isPlaying();
|
|
35
|
+
}
|
|
36
|
+
isPausedState() {
|
|
37
|
+
return this.speaker.isPausedState();
|
|
38
|
+
}
|
|
39
|
+
async destroy() {
|
|
40
|
+
return this.speaker.destroy();
|
|
41
|
+
}
|
|
42
|
+
getTimestamp() {
|
|
43
|
+
const now = new Date();
|
|
44
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
45
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let defaultSpeakerService;
|
|
49
|
+
export function getDefaultSpeakerService() {
|
|
50
|
+
if (!defaultSpeakerService) {
|
|
51
|
+
defaultSpeakerService = new SpeakerService();
|
|
52
|
+
}
|
|
53
|
+
return defaultSpeakerService;
|
|
54
|
+
}
|
|
55
|
+
export async function speak(text, options) {
|
|
56
|
+
if (options) {
|
|
57
|
+
return coreSpeak(text, options);
|
|
58
|
+
}
|
|
59
|
+
return coreSpeak(text);
|
|
60
|
+
}
|
|
61
|
+
export async function stop() {
|
|
62
|
+
return coreStop();
|
|
63
|
+
}
|
|
64
|
+
export function pause() {
|
|
65
|
+
corePause();
|
|
66
|
+
}
|
|
67
|
+
export function resume() {
|
|
68
|
+
coreResume();
|
|
69
|
+
}
|
|
70
|
+
export async function listVoices(providerName) {
|
|
71
|
+
return coreListVoices(providerName);
|
|
72
|
+
}
|
|
73
|
+
export default SpeakerService;
|
|
74
|
+
//# sourceMappingURL=speaker-service.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamingService - 流式TTS服务(Service层)
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 调用Provider层获取流式TTS
|
|
6
|
+
* - 调用Backend层播放音频
|
|
7
|
+
* - 支持边接收边播放(豆包模式)
|
|
8
|
+
*
|
|
9
|
+
* 数据流:
|
|
10
|
+
* stream(text) → MiniMaxProvider.speak(stream) → StreamPlayer (边收边播)
|
|
11
|
+
*/
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
import { BackendType } from '../core/backends';
|
|
14
|
+
export interface StreamingServiceOptions {
|
|
15
|
+
provider?: string;
|
|
16
|
+
voice?: string;
|
|
17
|
+
speed?: number;
|
|
18
|
+
volume?: number;
|
|
19
|
+
pitch?: number;
|
|
20
|
+
backendType?: BackendType;
|
|
21
|
+
}
|
|
22
|
+
export interface StreamingServiceStatus {
|
|
23
|
+
isActive: boolean;
|
|
24
|
+
bytesWritten: number;
|
|
25
|
+
state: string;
|
|
26
|
+
}
|
|
27
|
+
export declare class StreamingService extends EventEmitter {
|
|
28
|
+
private player;
|
|
29
|
+
private providerName;
|
|
30
|
+
private voice?;
|
|
31
|
+
private speed?;
|
|
32
|
+
private volume?;
|
|
33
|
+
private pitch?;
|
|
34
|
+
private backendType;
|
|
35
|
+
private _isActive;
|
|
36
|
+
private _bytesWritten;
|
|
37
|
+
constructor(options?: StreamingServiceOptions);
|
|
38
|
+
/**
|
|
39
|
+
* 获取时间戳
|
|
40
|
+
*/
|
|
41
|
+
private getTimestamp;
|
|
42
|
+
/**
|
|
43
|
+
* 初始化播放器
|
|
44
|
+
*/
|
|
45
|
+
private initPlayer;
|
|
46
|
+
/**
|
|
47
|
+
* 流式播放文本
|
|
48
|
+
* 边接收边播放(豆包模式)
|
|
49
|
+
*/
|
|
50
|
+
stream(text: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* 处理音频结果
|
|
53
|
+
*/
|
|
54
|
+
private processAudioResult;
|
|
55
|
+
/**
|
|
56
|
+
* 流式处理音频chunk
|
|
57
|
+
*/
|
|
58
|
+
private streamAudioChunks;
|
|
59
|
+
/**
|
|
60
|
+
* 停止流式播放
|
|
61
|
+
*/
|
|
62
|
+
stop(): void;
|
|
63
|
+
/**
|
|
64
|
+
* 暂停流式播放
|
|
65
|
+
*/
|
|
66
|
+
pause(): void;
|
|
67
|
+
/**
|
|
68
|
+
* 恢复流式播放
|
|
69
|
+
*/
|
|
70
|
+
resume(): void;
|
|
71
|
+
/**
|
|
72
|
+
* 获取流式播放状态
|
|
73
|
+
*/
|
|
74
|
+
getStatus(): StreamingServiceStatus;
|
|
75
|
+
/**
|
|
76
|
+
* 是否处于活跃状态
|
|
77
|
+
*/
|
|
78
|
+
isActive(): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* 销毁服务
|
|
81
|
+
*/
|
|
82
|
+
destroy(): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 获取默认流式服务实例
|
|
86
|
+
*/
|
|
87
|
+
export declare function getDefaultStreamingService(): StreamingService;
|
|
88
|
+
/**
|
|
89
|
+
* 导出 stream 方法
|
|
90
|
+
*/
|
|
91
|
+
export declare function stream(text: string, options?: StreamingServiceOptions): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* 导出 stop 方法
|
|
94
|
+
*/
|
|
95
|
+
export declare function streamStop(): void;
|
|
96
|
+
/**
|
|
97
|
+
* 导出 pause 方法
|
|
98
|
+
*/
|
|
99
|
+
export declare function streamPause(): void;
|
|
100
|
+
/**
|
|
101
|
+
* 导出 resume 方法
|
|
102
|
+
*/
|
|
103
|
+
export declare function streamResume(): void;
|
|
104
|
+
/**
|
|
105
|
+
* 导出 getStreamStatus 方法
|
|
106
|
+
*/
|
|
107
|
+
export declare function getStreamStatus(): StreamingServiceStatus;
|
|
108
|
+
export default StreamingService;
|
|
109
|
+
//# sourceMappingURL=streaming-service.d.ts.map
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamingService - 流式TTS服务(Service层)
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 调用Provider层获取流式TTS
|
|
6
|
+
* - 调用Backend层播放音频
|
|
7
|
+
* - 支持边接收边播放(豆包模式)
|
|
8
|
+
*
|
|
9
|
+
* 数据流:
|
|
10
|
+
* stream(text) → MiniMaxProvider.speak(stream) → StreamPlayer (边收边播)
|
|
11
|
+
*/
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
import { getProvider } from '../providers/base';
|
|
14
|
+
import { TTSError, TTSErrorCode } from '../core/types';
|
|
15
|
+
import { StreamPlayer } from '../core/stream-player';
|
|
16
|
+
import { BackendType } from '../core/backends';
|
|
17
|
+
import { logger } from '../utils/logger';
|
|
18
|
+
export class StreamingService extends EventEmitter {
|
|
19
|
+
player = null;
|
|
20
|
+
providerName;
|
|
21
|
+
voice;
|
|
22
|
+
speed;
|
|
23
|
+
volume;
|
|
24
|
+
pitch;
|
|
25
|
+
backendType;
|
|
26
|
+
_isActive = false;
|
|
27
|
+
_bytesWritten = 0;
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
super();
|
|
30
|
+
this.providerName = options.provider || 'minimax';
|
|
31
|
+
this.voice = options.voice;
|
|
32
|
+
this.speed = options.speed;
|
|
33
|
+
this.volume = options.volume;
|
|
34
|
+
this.pitch = options.pitch;
|
|
35
|
+
this.backendType = options.backendType || BackendType.NAUDIODON;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 获取时间戳
|
|
39
|
+
*/
|
|
40
|
+
getTimestamp() {
|
|
41
|
+
const now = new Date();
|
|
42
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
43
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 初始化播放器
|
|
47
|
+
*/
|
|
48
|
+
initPlayer() {
|
|
49
|
+
if (this.player) {
|
|
50
|
+
this.player.stop();
|
|
51
|
+
this.player = null;
|
|
52
|
+
}
|
|
53
|
+
const playerOptions = {
|
|
54
|
+
format: 'mp3',
|
|
55
|
+
backendType: this.backendType,
|
|
56
|
+
events: {
|
|
57
|
+
onStart: () => {
|
|
58
|
+
const timestamp = this.getTimestamp();
|
|
59
|
+
logger.info(`[Ocosay][${timestamp}][INFO][Streaming] 对应事件{流式播放开始}`);
|
|
60
|
+
this.emit('start');
|
|
61
|
+
},
|
|
62
|
+
onEnd: () => {
|
|
63
|
+
const timestamp = this.getTimestamp();
|
|
64
|
+
logger.info(`[Ocosay][${timestamp}][INFO][Streaming] 对应事件{流式播放结束}`);
|
|
65
|
+
this._isActive = false;
|
|
66
|
+
this.emit('end');
|
|
67
|
+
},
|
|
68
|
+
onError: (error) => {
|
|
69
|
+
const timestamp = this.getTimestamp();
|
|
70
|
+
logger.error(`[Ocosay][${timestamp}][ERROR][Streaming] 对应事件{流式播放错误} - ${error.message}`);
|
|
71
|
+
this._isActive = false;
|
|
72
|
+
this.emit('error', error);
|
|
73
|
+
},
|
|
74
|
+
onProgress: (bytes) => {
|
|
75
|
+
this._bytesWritten = bytes;
|
|
76
|
+
this.emit('progress', bytes);
|
|
77
|
+
},
|
|
78
|
+
onPause: () => {
|
|
79
|
+
const timestamp = this.getTimestamp();
|
|
80
|
+
logger.info(`[Ocosay][${timestamp}][INFO][Streaming] 对应事件{流式播放暂停}`);
|
|
81
|
+
this.emit('pause');
|
|
82
|
+
},
|
|
83
|
+
onResume: () => {
|
|
84
|
+
const timestamp = this.getTimestamp();
|
|
85
|
+
logger.info(`[Ocosay][${timestamp}][INFO][Streaming] 对应事件{流式播放恢复}`);
|
|
86
|
+
this.emit('resume');
|
|
87
|
+
},
|
|
88
|
+
onStop: () => {
|
|
89
|
+
const timestamp = this.getTimestamp();
|
|
90
|
+
logger.info(`[Ocosay][${timestamp}][INFO][Streaming] 对应事件{流式播放停止}`);
|
|
91
|
+
this._isActive = false;
|
|
92
|
+
this._bytesWritten = 0;
|
|
93
|
+
this.emit('stop');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
this.player = new StreamPlayer(playerOptions);
|
|
98
|
+
return this.player;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 流式播放文本
|
|
102
|
+
* 边接收边播放(豆包模式)
|
|
103
|
+
*/
|
|
104
|
+
async stream(text) {
|
|
105
|
+
if (!text || text.trim().length === 0) {
|
|
106
|
+
const timestamp = this.getTimestamp();
|
|
107
|
+
logger.warn(`[Ocosay][${timestamp}][WARNING][Streaming] 对应事件{空文本跳过}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const timestamp = this.getTimestamp();
|
|
111
|
+
logger.info(`[Ocosay][${timestamp}][INFO][Streaming] 对应事件{流式播放开始} - 文本长度: ${text.length}`);
|
|
112
|
+
try {
|
|
113
|
+
// 获取 Provider
|
|
114
|
+
const provider = getProvider(this.providerName);
|
|
115
|
+
if (!provider) {
|
|
116
|
+
throw new TTSError(`Provider ${this.providerName} not found`, TTSErrorCode.UNKNOWN, this.providerName);
|
|
117
|
+
}
|
|
118
|
+
// 初始化播放器
|
|
119
|
+
const player = this.initPlayer();
|
|
120
|
+
player.start();
|
|
121
|
+
this._isActive = true;
|
|
122
|
+
this._bytesWritten = 0;
|
|
123
|
+
// 调用 Provider 的流式合成
|
|
124
|
+
const result = await provider.speak(text, {
|
|
125
|
+
voice: this.voice,
|
|
126
|
+
model: 'stream',
|
|
127
|
+
speed: this.speed,
|
|
128
|
+
volume: this.volume,
|
|
129
|
+
pitch: this.pitch
|
|
130
|
+
});
|
|
131
|
+
// 处理音频结果
|
|
132
|
+
await this.processAudioResult(result, player);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const ts = this.getTimestamp();
|
|
136
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
137
|
+
logger.error(`[Ocosay][${ts}][ERROR][Streaming] 对应事件{流式播放错误} - ${errorMsg}`);
|
|
138
|
+
this._isActive = false;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 处理音频结果
|
|
144
|
+
*/
|
|
145
|
+
async processAudioResult(result, player) {
|
|
146
|
+
if (result.isStream && result.audioData instanceof ReadableStream) {
|
|
147
|
+
// 流式数据:边收边播
|
|
148
|
+
await this.streamAudioChunks(result.audioData, player);
|
|
149
|
+
}
|
|
150
|
+
else if (Buffer.isBuffer(result.audioData)) {
|
|
151
|
+
// 非流式数据:直接写入
|
|
152
|
+
player.write(result.audioData);
|
|
153
|
+
player.end();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 流式处理音频chunk
|
|
158
|
+
*/
|
|
159
|
+
async streamAudioChunks(stream, player) {
|
|
160
|
+
const reader = stream.getReader();
|
|
161
|
+
try {
|
|
162
|
+
while (true) {
|
|
163
|
+
const { done, value } = await reader.read();
|
|
164
|
+
if (done) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (value) {
|
|
168
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
169
|
+
player.write(chunk);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
reader.releaseLock();
|
|
175
|
+
player.end();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 停止流式播放
|
|
180
|
+
*/
|
|
181
|
+
stop() {
|
|
182
|
+
if (this.player) {
|
|
183
|
+
this.player.stop();
|
|
184
|
+
}
|
|
185
|
+
this._isActive = false;
|
|
186
|
+
this._bytesWritten = 0;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 暂停流式播放
|
|
190
|
+
*/
|
|
191
|
+
pause() {
|
|
192
|
+
if (this.player) {
|
|
193
|
+
this.player.pause();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 恢复流式播放
|
|
198
|
+
*/
|
|
199
|
+
resume() {
|
|
200
|
+
if (this.player) {
|
|
201
|
+
this.player.resume();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 获取流式播放状态
|
|
206
|
+
*/
|
|
207
|
+
getStatus() {
|
|
208
|
+
return {
|
|
209
|
+
isActive: this._isActive,
|
|
210
|
+
bytesWritten: this._bytesWritten,
|
|
211
|
+
state: this.player?.isStopped() ? 'stopped' :
|
|
212
|
+
this.player?.isPaused() ? 'paused' :
|
|
213
|
+
this._isActive ? 'playing' : 'idle'
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 是否处于活跃状态
|
|
218
|
+
*/
|
|
219
|
+
isActive() {
|
|
220
|
+
return this._isActive;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 销毁服务
|
|
224
|
+
*/
|
|
225
|
+
async destroy() {
|
|
226
|
+
if (this.player) {
|
|
227
|
+
this.player.stop();
|
|
228
|
+
this.player = null;
|
|
229
|
+
}
|
|
230
|
+
this._isActive = false;
|
|
231
|
+
this._bytesWritten = 0;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// 单例实例
|
|
235
|
+
let defaultStreamingService;
|
|
236
|
+
/**
|
|
237
|
+
* 获取默认流式服务实例
|
|
238
|
+
*/
|
|
239
|
+
export function getDefaultStreamingService() {
|
|
240
|
+
if (!defaultStreamingService) {
|
|
241
|
+
defaultStreamingService = new StreamingService();
|
|
242
|
+
}
|
|
243
|
+
return defaultStreamingService;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 导出 stream 方法
|
|
247
|
+
*/
|
|
248
|
+
export async function stream(text, options) {
|
|
249
|
+
const service = options ? new StreamingService(options) : getDefaultStreamingService();
|
|
250
|
+
return service.stream(text);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* 导出 stop 方法
|
|
254
|
+
*/
|
|
255
|
+
export function streamStop() {
|
|
256
|
+
const service = getDefaultStreamingService();
|
|
257
|
+
service.stop();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 导出 pause 方法
|
|
261
|
+
*/
|
|
262
|
+
export function streamPause() {
|
|
263
|
+
const service = getDefaultStreamingService();
|
|
264
|
+
service.pause();
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 导出 resume 方法
|
|
268
|
+
*/
|
|
269
|
+
export function streamResume() {
|
|
270
|
+
const service = getDefaultStreamingService();
|
|
271
|
+
service.resume();
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 导出 getStreamStatus 方法
|
|
275
|
+
*/
|
|
276
|
+
export function getStreamStatus() {
|
|
277
|
+
const service = getDefaultStreamingService();
|
|
278
|
+
return service.getStatus();
|
|
279
|
+
}
|
|
280
|
+
export default StreamingService;
|
|
281
|
+
//# sourceMappingURL=streaming-service.js.map
|