UNPKG

@coze/uniapp-api

Version:

Official Coze UniApp SDK for seamless AI integration into your applications | 扣子官方 UniApp SDK,助您轻松集成 AI 能力到应用中

429 lines (428 loc) 16.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WsSpeechClient = void 0; const api_1 = require("@coze/api"); const pcm_stream_player_1 = require("../pcm-stream-player"); const api_2 = require("../../api"); // Import types directly from @coze/api /** * WsSpeechClient for UniApp/WeChat Mini Program * Handles text-to-speech streaming through WebSockets * @class */ class WsSpeechClient { /** * Creates a new WsSpeechClient instance * @param {WsToolsOptions} config - Configuration options */ constructor(config) { Object.defineProperty(this, "ws", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "listeners", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "pcmStreamPlayer", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "trackId", { enumerable: true, configurable: true, writable: true, value: 'default' }); Object.defineProperty(this, "api", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "totalDuration", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "playbackStartTime", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "playbackPauseTime", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "playbackTimeout", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "elapsedBeforePause", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "audioDeltaList", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "sampleRate", { enumerable: true, configurable: true, writable: true, value: pcm_stream_player_1.PcmStreamPlayer.getSampleRate() }); Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Process audio data from the queue * @private */ Object.defineProperty(this, "handleAudioMessage", { enumerable: true, configurable: true, writable: true, value: () => __awaiter(this, void 0, void 0, function* () { const message = this.audioDeltaList[0]; // Use UniApp's base64ToArrayBuffer instead of atob for Mini Program compatibility const arrayBuffer = uni.base64ToArrayBuffer(message); // Calculate duration in seconds const bytesPerSecond = Number(this.sampleRate) * 1 * (16 / 8); // sampleRate * channels * (bitDepth/8) const duration = arrayBuffer.byteLength / bytesPerSecond; this.totalDuration += duration; try { yield this.pcmStreamPlayer.add16BitPCM(arrayBuffer, this.trackId); // Start or update the playback timer if (!this.playbackStartTime && !this.playbackPauseTime) { this.playbackStartTime = Date.now(); this.elapsedBeforePause = 0; } // Remove the processed message and process the next one if available this.audioDeltaList.shift(); if (this.audioDeltaList.length > 0) { this.handleAudioMessage(); } } catch (error) { console.warn(`[speech] pcmStreamPlayer error ${error === null || error === void 0 ? void 0 : error.message}`, error); } }) }); this.api = new api_2.CozeAPI(Object.assign({ baseWsURL: api_1.COZE_CN_BASE_WS_URL }, config)); this.pcmStreamPlayer = new pcm_stream_player_1.PcmStreamPlayer({ sampleRate: this.sampleRate, }); this.config = config; } /** * Initialize the WebSocket connection * @returns {Promise<WebSocketAPI>} - The WebSocket API instance */ init() { return __awaiter(this, void 0, void 0, function* () { if (this.ws) { return this.ws; } const ws = yield this.api.websockets.audio.speech.create(undefined, this.config.websocketOptions); this.ws = ws; let isResolved = false; // Generate a unique track ID using timestamp instead of uuid for Mini Program this.trackId = `my-track-id-${Date.now()}`; this.totalDuration = 0; if (this.playbackTimeout) { clearTimeout(this.playbackTimeout); this.playbackTimeout = null; } this.playbackStartTime = null; return new Promise((resolve, reject) => { ws.onopen = () => { console.debug('[speech] ws open'); }; ws.onmessage = data => { var _a, _b, _c, _d, _e; // Trigger all registered event listeners this.emit('data', data); this.emit(data.event_type, data); if (data.event_type === api_1.WebsocketsEventType.ERROR) { this.closeWs(); if (isResolved) { return; } isResolved = true; // Handle error with type casting to access data properties safely reject(new api_1.APIError((_a = data.data) === null || _a === void 0 ? void 0 : _a.code, { code: (_b = data.data) === null || _b === void 0 ? void 0 : _b.code, msg: (_c = data.data) === null || _c === void 0 ? void 0 : _c.msg, detail: data.detail, }, (_d = data.data) === null || _d === void 0 ? void 0 : _d.msg, undefined)); return; } else if (data.event_type === api_1.WebsocketsEventType.SPEECH_CREATED) { resolve(ws); isResolved = true; } else if (data.event_type === api_1.WebsocketsEventType.SPEECH_AUDIO_UPDATE) { // Push audio data to queue for sequential processing // Use type assertion to access data property safely this.audioDeltaList.push((_e = data.data) === null || _e === void 0 ? void 0 : _e.delta); if (this.audioDeltaList.length === 1) { this.handleAudioMessage(); } } else if (data.event_type === api_1.WebsocketsEventType.SPEECH_AUDIO_COMPLETED) { console.debug('[speech] totalDuration', this.totalDuration); if (this.playbackStartTime) { // Calculate remaining time = total duration - played time - paused time const now = Date.now(); const remaining = this.totalDuration - (now - this.playbackStartTime) / 1000 - this.elapsedBeforePause; this.playbackTimeout = setTimeout(() => { this.emit('completed', undefined); this.playbackStartTime = null; this.elapsedBeforePause = 0; }, remaining * 1000); } this.closeWs(); } }; ws.onerror = (error, event) => { var _a, _b; console.error('[speech] WebSocket error', error, event); this.emit('data', error); this.emit(api_1.WebsocketsEventType.ERROR, error); this.closeWs(); if (isResolved) { return; } isResolved = true; // Use type assertion to safely access error properties reject(new api_1.APIError((_a = error.data) === null || _a === void 0 ? void 0 : _a.code, error, (_b = error.data) === null || _b === void 0 ? void 0 : _b.msg, undefined)); }; ws.onclose = () => { console.debug('[speech] ws closed'); }; }); }); } /** * Connect to the speech service and configure audio output * @param {Object} options - Connection options * @param {string} [options.voiceId] - Voice ID to use * @param {number} [options.speechRate] - Speech rate (-50 to 100, default 0) * @returns {Promise<void>} */ connect() { return __awaiter(this, arguments, void 0, function* ({ voiceId, speechRate, } = {}) { var _a; yield this.init(); (_a = this.ws) === null || _a === void 0 ? void 0 : _a.send({ id: `event-${Date.now()}`, event_type: api_1.WebsocketsEventType.SPEECH_UPDATE, data: { output_audio: { codec: 'pcm', voice_id: voiceId || undefined, speech_rate: speechRate || undefined, pcm_config: { sample_rate: this.sampleRate, }, }, }, }); }); } /** * Disconnect from the speech service and stop audio playback * @returns {Promise<void>} */ disconnect() { return __awaiter(this, void 0, void 0, function* () { if (this.playbackTimeout) { clearTimeout(this.playbackTimeout); } this.audioDeltaList.length = 0; yield this.pcmStreamPlayer.interrupt(); this.closeWs(); }); } /** * Append text to the speech buffer * @param {string} message - Text message to convert to speech */ append(message) { var _a; (_a = this.ws) === null || _a === void 0 ? void 0 : _a.send({ id: `event-${Date.now()}`, event_type: api_1.WebsocketsEventType.INPUT_TEXT_BUFFER_APPEND, data: { delta: message, }, }); } /** * Complete the speech buffer and start processing */ complete() { var _a; (_a = this.ws) === null || _a === void 0 ? void 0 : _a.send({ id: `event-${Date.now()}`, event_type: api_1.WebsocketsEventType.INPUT_TEXT_BUFFER_COMPLETE, }); } /** * Append text and complete in a single call * @param {string} message - Text message to convert to speech */ appendAndComplete(message) { this.append(message); this.complete(); } /** * Interrupt playback and disconnect * @returns {Promise<void>} */ interrupt() { return __awaiter(this, void 0, void 0, function* () { yield this.disconnect(); this.emit('completed', undefined); console.debug('[speech] playback completed', this.totalDuration); }); } /** * Pause audio playback * @returns {Promise<void>} */ pause() { return __awaiter(this, void 0, void 0, function* () { if (this.playbackTimeout) { clearTimeout(this.playbackTimeout); this.playbackTimeout = null; } if (this.playbackStartTime && !this.playbackPauseTime) { this.playbackPauseTime = Date.now(); this.elapsedBeforePause += (this.playbackPauseTime - this.playbackStartTime) / 1000; } yield this.pcmStreamPlayer.pause(); }); } /** * Resume audio playback * @returns {Promise<void>} */ resume() { return __awaiter(this, void 0, void 0, function* () { if (this.playbackPauseTime) { this.playbackStartTime = Date.now(); this.playbackPauseTime = null; // Update the timeout with remaining duration if (this.playbackTimeout) { clearTimeout(this.playbackTimeout); } const remaining = this.totalDuration - this.elapsedBeforePause; this.playbackTimeout = setTimeout(() => { this.emit('completed', undefined); console.debug('[speech] playback completed', this.totalDuration); this.playbackStartTime = null; this.elapsedBeforePause = 0; }, remaining * 1000); } yield this.pcmStreamPlayer.resume(); }); } /** * Toggle between play and pause states * @returns {Promise<void>} */ togglePlay() { return __awaiter(this, void 0, void 0, function* () { if (this.isPlaying()) { yield this.pause(); } else { yield this.resume(); } }); } /** * Check if audio is currently playing * @returns {boolean} */ isPlaying() { return this.pcmStreamPlayer.isPlaying(); } /** * Register an event listener * @param {string} event - Event name to listen for * @param {Function} callback - Callback function */ on(event, callback) { var _a; if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } (_a = this.listeners.get(event)) === null || _a === void 0 ? void 0 : _a.add(callback); } /** * Remove an event listener * @param {string} event - Event name * @param {Function} callback - Callback function to remove */ off(event, callback) { var _a; (_a = this.listeners.get(event)) === null || _a === void 0 ? void 0 : _a.delete(callback); } /** * Close the WebSocket connection * @private */ closeWs() { var _a, _b; if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === 1) { (_b = this.ws) === null || _b === void 0 ? void 0 : _b.close(); } this.ws = null; } /** * Emit an event to all registered listeners * @param {string} event - Event name * @param {CreateSpeechWsRes|undefined} data - Event data * @private */ emit(event, data) { var _a; (_a = this.listeners.get(event)) === null || _a === void 0 ? void 0 : _a.forEach(callback => callback(data)); } } exports.WsSpeechClient = WsSpeechClient; exports.default = WsSpeechClient;