New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

uniapp-text-to-speech

Package Overview
Dependencies
Maintainers
0
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

uniapp-text-to-speech - npm Package Compare versions

Comparing version 1.0.3 to 1.0.4

examples/index.vue

3

dist/index.d.ts

@@ -145,3 +145,3 @@ /**

/**
* 结束计时并返回耗时(毫���)
* 结束计时并返回时(毫秒)
* @private

@@ -189,3 +189,4 @@ */

private handleError;
private calculateProgress;
}
export default SpeechSynthesisUtil;

@@ -242,7 +242,7 @@ /**

return;
// 确保每次播放前都创建新的音频上下文
if (!this.audioContext) {
this.audioContext = uni.createInnerAudioContext();
this.bindAudioEvents();
}
// 销毁旧的音频上下文(如果存在)
this.destroyAudioContext();
// 创建新的音频上下文
this.audioContext = uni.createInnerAudioContext();
this.bindAudioEvents();
this.setState({

@@ -258,8 +258,2 @@ isStartPlayQueue: true,

yield this.safePlay();
// 移除这里的 emit,因为 handleAudioPlay 会处理
// this.emit(EventType.AUDIO_PLAY, {
// currentAudio,
// currentText: currentItem?.text || '',
// remainingCount: this.audioQueue.length - 1
// });
}

@@ -275,3 +269,2 @@ catch (error) {

this.pendingAudioUrl = currentAudio;
// 播放失败时销毁音频上下文
this.destroyAudioContext();

@@ -348,9 +341,13 @@ }

const currentItem = this.audioTextQueue[0];
this.setState({ isPlaying: true });
this.emit(EventType.AUDIO_PLAY, {
currentAudio,
currentText: (currentItem === null || currentItem === void 0 ? void 0 : currentItem.text) || '',
remainingCount: this.audioQueue.length - 1, // 减1是因为不包括当前正在播放的
totalCount: this.totalAudioCount
});
// 确保只有在真正开始播放时才更新状态和触发事件
if (currentAudio && currentItem) {
this.setState({ isPlaying: true, isPaused: false });
this.emit(EventType.AUDIO_PLAY, {
currentAudio,
currentText: currentItem.text,
remainingCount: this.audioQueue.length - 1,
totalCount: this.totalAudioCount,
progress: this.calculateProgress()
});
}
}

@@ -364,34 +361,42 @@ /**

const finishedItem = this.audioTextQueue[0];
if (!finishedAudio || !finishedItem)
return;
// 先设置播放状态为 false
this.setState({ isPlaying: false });
// 移除已播放的音频
this.audioQueue.shift();
this.audioTextQueue.shift();
// 计算当前进度
const playedCount = this.totalAudioCount - this.audioQueue.length;
const progress = this.totalAudioCount > 0 ? (playedCount / this.totalAudioCount) * 100 : 0;
// 发送进度事件
this.emit(EventType.PROGRESS, {
progress: Math.round(progress), // 四舍五入到整数
playedCount,
totalCount: this.totalAudioCount,
currentText: (finishedItem === null || finishedItem === void 0 ? void 0 : finishedItem.text) || '',
remainingCount: this.audioQueue.length // 剩余数量就是队列的长度
});
// 检查是否还有待播放的音频
if (this.audioQueue.length > 0) {
if (this.audioQueue.length > 1) {
// 移除当前音频
this.audioQueue.shift();
this.audioTextQueue.shift();
// 发送进度事件
this.emit(EventType.PROGRESS, {
progress: this.calculateProgress(),
playedCount: this.totalAudioCount - this.audioQueue.length,
totalCount: this.totalAudioCount,
currentText: finishedItem.text,
remainingCount: this.audioQueue.length - 1
});
// 确保在开始下一个播放前销毁当前上下文
this.destroyAudioContext();
this.playAudioQueue();
// 使用 setTimeout 确保异步执行下一个播放
setTimeout(() => {
this.playAudioQueue();
}, 0);
}
else {
// 最后一个音频播放完成
this.audioQueue.shift();
this.audioTextQueue.shift();
this.setState({
isEnded: true,
isStartPlayQueue: false,
isPlaying: false
isPlaying: false,
isPaused: false
});
this.emit(EventType.AUDIO_END, {
finishedAudio,
finishedText: (finishedItem === null || finishedItem === void 0 ? void 0 : finishedItem.text) || '',
finishedText: finishedItem.text,
remainingCount: 0,
isComplete: true,
progress: 100, // 确保最后进度为100%
progress: 100,
totalCount: this.totalAudioCount,

@@ -401,2 +406,3 @@ playedCount: this.totalAudioCount

this.destroyAudioContext();
this.resetTextProcessor();
}

@@ -506,3 +512,3 @@ }

/**
* 结束计时并返回耗时(毫���)
* 结束计时并返回时(毫秒)
* @private

@@ -654,3 +660,10 @@ */

}
// 添加进度计算辅助方法
calculateProgress() {
if (this.totalAudioCount === 0)
return 0;
const playedCount = this.totalAudioCount - this.audioQueue.length;
return Math.round((playedCount / this.totalAudioCount) * 100);
}
}
export default SpeechSynthesisUtil;
{
"name": "uniapp-text-to-speech",
"version": "1.0.3",
"version": "1.0.4",
"description": "uniapp 文本转语音",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

# uniapp 文本转语音
- 基于 Minimax API 的 UniApp 文本转语音工具,支持文本分段、队列播放、暂停恢复等功能。
- 基于 Minimax API 的 UniApp 文本转语音工具,支持文本分段、队列播放、暂停恢复等功能。
- 目前只内置了 [Minimax](https://platform.minimaxi.com/document/T2A%20V2?key=66719005a427f0c8a5701643)文本转语音
- Minimax的语音生成技术以其自然、情感丰富和实时性强而著称
- Minimax 的语音生成技术以其自然、情感丰富和实时性强而著称
#### API_KEY、GroupId获取方法
#### API_KEY、GroupId 获取方法
[https://platform.minimaxi.com/user-center/basic-information/interface-key](https://platform.minimaxi.com/user-center/basic-information/interface-key)
[NPM地址](https://www.npmjs.com/package/uniapp-text-to-speech)
[NPM 地址](https://www.npmjs.com/package/uniapp-text-to-speech)

@@ -22,3 +23,4 @@ ## 特性

## 安装 (推荐npm安装,而不是导入)
## 安装 (推荐 npm 安装,而不是导入)
```typescript

@@ -58,25 +60,22 @@ npm install uniapp-text-to-speech

## 分段使用
- 模拟AI大模型流式返回数据
- 模拟 AI 大模型流式返回数据,自动会处理合成分段:["你好","我是一个ai机器人","我的名字叫做阿强"]
```typescript
import SpeechSynthesisUtil from 'uniapp-text-to-speech';
import SpeechSynthesisUtil from "uniapp-text-to-speech";
// 初始化
const tts = new SpeechSynthesisUtil({
API_KEY: 'your_minimax_api_key', // Minimax API密钥
GroupId: 'your_group_id', // Minimax 组ID
API_KEY: "your_minimax_api_key", // Minimax API密钥
GroupId: "your_group_id", // Minimax 组ID
});
const mockArray = ['你好,', '我是', '一个ai','机器人', ',我的名', '字', '叫做', '阿强']
const mockTexts = ['你好,', '我是', '人工智能助手,', '很高兴认识你!'];
// 自动会处理合成分段:["你好","我是一个ai机器人","我的名字叫做阿强"]
// 基础播放
try {
mockArray.map(item => {
await tts.textToSpeech(item);
})
await tts.flushRemainingText()
for (const text of mockTexts) {
await tts.processText(text);
}
await tts.flushRemainingText();
} catch (error) {
console.error('语音合成失败:', error);
addLog(`分段播放失败: ${error.message}`);
}

@@ -90,21 +89,42 @@ ```

```typescript
import { EventType } from 'uniapp-text-to-speech';
import { EventType } from "uniapp-text-to-speech";
// 监听合成开始
tts.on(EventType.SYNTHESIS_START, ({ text }) => {
console.log('开始合成文本:', text);
console.log(`开始合成文本: ${text}`);
});
// 监听播放开始
tts.on(EventType.AUDIO_PLAY, ({ currentText, remainingCount }) => {
console.log('正在播放:', currentText);
console.log('剩余数量:', remainingCount);
tts.on(EventType.AUDIO_PLAY, ({ currentText }) => {
console.log(`正在播放: ${currentText}`);
status.value = "播放中";
});
// 监听播放结束
tts.on(EventType.AUDIO_END, ({ finishedText }) => {
console.log('播放完成:', finishedText);
console.log(`播放完成: ${finishedText}`);
status.value = "就绪";
progress.value = 100;
});
// 监听错误
tts.on(EventType.ERROR, ({ error }) => {
console.error('发生错误:', error);
console.log(`错误: ${error.message}`);
status.value = "错误";
});
// 监听暂停
tts.on(EventType.PAUSE, () => {
console.log("播放已暂停");
status.value = "已暂停";
isPaused.value = true;
});
// 监听恢复
tts.on(EventType.RESUME, () => {
console.log("播放已恢复");
status.value = "播放中";
isPaused.value = false;
});
```
### 2. 暂停和恢复

@@ -120,8 +140,8 @@

```
### 3. 长文本分段处理
```typescript
// 自动按标点符号分段处理长文本
await tts.processText('这是第一句话。这是第二句话!这是第三句话?');
await tts.processText("这是第一句话。这是第二句话!这是第三句话?");
// 强制处理剩余未播放的文本

@@ -132,2 +152,3 @@ await tts.flushRemainingText();

```
### 4. 状态管理

@@ -138,4 +159,4 @@

const state = tts.getState();
console.log('是否正在播放:', state.isPlaying);
console.log('是否已暂停:', state.isPaused);
console.log("是否正在播放:", state.isPlaying);
console.log("是否已暂停:", state.isPaused);
// 重置所有状态

@@ -145,3 +166,2 @@ tts.reset();

## API 文档

@@ -151,34 +171,60 @@

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| API_KEY | string | 是 | Minimax API密钥 |
| GroupId | string | 是 | Minimax 组ID |
| MAX_QUEUE_LENGTH | number | 否 | 音频队列最大长度,默认为3 |
|modelConfig|object|否|合成语音配置,参考[minimaxi](https://platform.minimaxi.com/document/T2A%20V2?key=66719005a427f0c8a5701643#YqSh1KAoyms1WH4XJrdeIrrb)
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------- |
| API_KEY | string | 是 | Minimax API 密钥 |
| GroupId | string | 是 | Minimax 组 ID |
| MAX_QUEUE_LENGTH | number | 否 | 音频队列最大长度,默认为 3 |
| modelConfig | object | 否 | 合成语音配置,参考[minimaxi](https://platform.minimaxi.com/document/T2A%20V2?key=66719005a427f0c8a5701643#YqSh1KAoyms1WH4XJrdeIrrb) |
### 事件类型
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| SYNTHESIS_START | 开始合成 | { text: string } |
| SYNTHESIS_END | 合成结束 | { text: string } |
| AUDIO_PLAY | 开始播放 | { currentText: string, remainingCount: number } |
| AUDIO_END | 播放结束 | { finishedText: string } |
| PAUSE | 暂停播放 | - |
| RESUME | 恢复播放 | - |
| ERROR | 发生错误 | { error: Error } |
| 事件名 | 说明 | 回调参数 |
| --------------- | -------------------- | ---------------- |
| SYNTHESIS_START | 开始合成 | { text: string } |
| SYNTHESIS_END | 合成结束 | { text: string } |
| AUDIO_PLAY | 开始播放单个音频片段 | { text: string } |
| AUDIO_END | 所有音频播放完成 | { text: string } |
| PAUSE | 暂停播放 | - |
| RESUME | 恢复播放 | - |
| ERROR | 发生错误 | { error: Error } |
### 主要方法
### 事件说明
| 方法名 | 说明 | 参数 | 返回值 |
|--------|------|------|--------|
| textToSpeech | 文本转语音 | text: string | Promise<void> |
| processText | 处理长文本 | text: string | Promise<void> |
| pause | 暂停播放 | - | void |
| resume | 恢复播放 | - | void |
| togglePlay | 切换播放状态 | - | void |
| reset | 重置所有状态 | - | void |
| on | 添加事件监听 | event: EventType, callback: Function | void |
| off | 移除事件监听 | event: EventType, callback: Function | void |
- `AUDIO_PLAY`: 每个音频片段开始播放时触发
- `AUDIO_END`: 仅在所有音频片段都播放完成后触发一次
## 使用示例
```typescript
import SpeechSynthesisUtil, { EventType } from "uniapp-text-to-speech";
const tts = new SpeechSynthesisUtil({
API_KEY: "your_minimax_api_key",
GroupId: "your_group_id",
modelConfig: {
model: "speech-01-240228",
voice_setting: {
voice_id: "female-yujie", // 默认使用悦姐声音
speed: 1.2,
vol: 1,
},
},
});
// 监听播放完成事件
tts.on(EventType.AUDIO_END, ({ text }) => {
console.log("所有音频播放完成,最后播放的文本:", text);
});
// 分段播放示例
async function playMultipleTexts() {
await tts.processText("第一段文本");
await tts.processText("第二段文本");
await tts.flushRemainingText(); // 确保所有文本都被处理
}
// 重置播放状态
tts.reset();
```
## 注意事项

@@ -191,5 +237,19 @@

- 低优先级:,、
3. 音频队列最大长度默认为3,可以通过构造函数参数修改
4. 播放失败时会自动重试,如需手动触发可调用 manualPlay 方法
3. 音频队列最大长度默认为 3,可以通过构造函数参数修改
4. `AUDIO_END` 事件只会在所有音频片段播放完成后触发一次
5. 使用 `reset()` 方法可以重置所有播放状态和计数器
## 主要的方
| 方法名 | 说明 | 参数 | 返回值 |
| ------------ | ------------ | ------------------------------------ | ------------- |
| textToSpeech | 文本转语音 | text: string | Promise<void> |
| processText | 处理长文本 | text: string | Promise<void> |
| pause | 暂停播放 | - | void |
| resume | 恢复播放 | - | void |
| togglePlay | 切换播放状态 | - | void |
| reset | 重置所有状态 | - | void |
| on | 添加事件监听 | event: EventType, callback: Function | void |
| off | 移除事件监听 | event: EventType, callback: Function | void |
## 完整的示例代码

@@ -199,249 +259,245 @@

<template>
<div class="speech-container">
<!-- 文本输入区域 -->
<textarea
v-model="inputText"
placeholder="请输入要转换的文本"
:disabled="isProcessing"
></textarea>
<div class="speech-demo">
<!-- 基础演示区域 -->
<section class="demo-section">
<h3>基础演示</h3>
<textarea v-model="basicText" placeholder="请输入要转换的文本"></textarea>
<button @click="handleBasicSpeech">开始播放</button>
</section>
<!-- 控制按钮区域 -->
<div class="control-panel">
<button
@click="startPlayback"
:disabled="isProcessing || !inputText"
>
{{ isProcessing ? '处理中...' : '开始播放' }}
</button>
<!-- 分段演示区域 -->
<section class="demo-section">
<h3>分段播放演示</h3>
<div class="segment-container">
<div v-for="(text, index) in mockTexts" :key="index" class="segment">
<span>{{ text }}</span>
</div>
</div>
<button @click="handleSegmentSpeech">分段播放</button>
</section>
<button
@click="handlePlayPause"
:disabled="!canTogglePlay"
>
{{ isPaused ? '继续' : '暂停' }}
</button>
<!-- 高级功能演示区域 -->
<section class="demo-section">
<h3>高级功能演示</h3>
<div class="controls">
<button @click="handleTogglePlay">{{ isPaused ? '继续' : '暂停' }}</button>
<button @click="handleReset">重置</button>
</div>
<div class="status">
<p>当前状态: {{ status }}</p>
<p>播放进度: {{ progress }}%</p>
</div>
</section>
<button
@click="handleStop"
:disabled="!isPlaying && !isPaused"
>
停止
</button>
</div>
<!-- 事件日志区域 -->
<section class="demo-section">
<h3>事件日志</h3>
<div class="log-container">
<div v-for="(log, index) in eventLogs" :key="index" class="log-item">
{{ log }}
</div>
</div>
</section>
</div>
</template>
<!-- 状态显示区域 -->
<div class="status-panel">
<div class="status-item">
<span>状态:</span>
<span :class="statusClass">{{ status }}</span>
</div>
<div class="status-item">
<span>进度:</span>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${progress}%` }"
></div>
<span>{{ progress }}%</span>
</div>
</div>
</div>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import SpeechSynthesisUtil, { EventType } from 'uniapp-text-to-speech';
<!-- 错误信息显示 -->
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
</div>
</template>
// 响应式状态
const basicText = ref('你好,这是一个基础示例。');
const mockTexts = ref(['你好,', '我是', '人工智能助手,', '很高兴认识你!']);
const status = ref('就绪');
const progress = ref(0);
const isPaused = ref(false);
const eventLogs = ref<string[]>([]);
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import SpeechSynthesisUtil, { EventType } from 'uniapp-text-to-speech';
// 初始化语音工具
const tts = new SpeechSynthesisUtil({
API_KEY: 'your_minimax_api_key', // Minimax API密钥
GroupId: 'your_group_id', // Minimax 组ID
modelConfig: {
model: 'speech-01-240228',
voice_setting: {
voice_id: "female-yujie",
speed: 1,
vol: 1
}
}
});
// 响应式状态
const inputText = ref('');
const isProcessing = ref(false);
const isPaused = ref(false);
const isPlaying = ref(false);
const status = ref('就绪');
const progress = ref(0);
const errorMessage = ref('');
// 添加日志
const addLog = (message : string) => {
eventLogs.value.unshift(`${new Date().toLocaleTimeString()}: ${message}`);
if (eventLogs.value.length > 10) {
eventLogs.value.pop();
}
};
// 计算属性
const canTogglePlay = computed(() => {
return isPlaying.value || isPaused.value;
});
// 设置事件监听
const setupEventListeners = () => {
// 监听合成开始
tts.on(EventType.SYNTHESIS_START, ({ text }) => {
addLog(`开始合成文本: ${text}`);
});
const statusClass = computed(() => {
return {
'status-ready': status.value === '就绪',
'status-playing': status.value === '播放中',
'status-paused': status.value === '已暂停',
'status-error': status.value.includes('错误')
};
});
// 监听播放开始
tts.on(EventType.AUDIO_PLAY, ({ currentText }) => {
addLog(`正在播放: ${currentText}`);
status.value = '播放中';
});
// 初始化语音工具
const speechUtil = new SpeechSynthesisUtil({
API_KEY: 'your_api_key',
GroupId: 'your_group_id'
});
// 监听播放结束
tts.on(EventType.AUDIO_END, ({ finishedText }) => {
addLog(`播放完成: ${finishedText}`);
status.value = '就绪';
progress.value = 100;
});
// 设置事件监听
const setupEventListeners = () => {
// 状态变化监听
speechUtil.on(EventType.STATE_CHANGE, ({ newState }) => {
isPaused.value = newState.isPaused;
isPlaying.value = newState.isPlaying;
status.value = newState.isPlaying ? '播放中' :
newState.isPaused ? '已暂停' :
newState.isError ? '错误' : '就绪';
});
// 监听错误
tts.on(EventType.ERROR, ({ error }) => {
addLog(`错误: ${error.message}`);
status.value = '错误';
});
// 进度监听
speechUtil.on(EventType.PROGRESS, ({ currentIndex, totalChunks }) => {
progress.value = Math.round(((currentIndex + 1) / totalChunks) * 100);
});
// 监听暂停
tts.on(EventType.PAUSE, () => {
addLog('播放已暂停');
status.value = '已暂停';
isPaused.value = true;
});
// 错误监听
speechUtil.on(EventType.ERROR, ({ error }) => {
errorMessage.value = error.message;
status.value = '错误';
});
// 监听恢复
tts.on(EventType.RESUME, () => {
addLog('播放已恢复');
status.value = '播放中';
isPaused.value = false;
});
};
// 播放结束监听
speechUtil.on(EventType.AUDIO_END, () => {
if (progress.value === 100) {
resetState();
}
});
};
// 基础播放示例
const handleBasicSpeech = async () => {
try {
await tts.textToSpeech(basicText.value);
} catch (error) {
addLog(`播放失败: ${error.message}`);
}
};
// 开始播放
const startPlayback = async () => {
if (!inputText.value) return;
isProcessing.value = true;
errorMessage.value = '';
try {
await speechUtil.processText(inputText.value);
await speechUtil.flushRemainingText();
} catch (error) {
errorMessage.value = `处理失败: ${error.message}`;
} finally {
isProcessing.value = false;
}
};
// 分段播放示例
const handleSegmentSpeech = async () => {
try {
for (const text of mockTexts.value) {
await tts.processText(text);
}
await tts.flushRemainingText();
} catch (error) {
addLog(`分段播放失败: ${error.message}`);
}
};
// 处理播放/暂停
const handlePlayPause = () => {
speechUtil.togglePlay();
};
// 切换播放/暂停
const handleTogglePlay = () => {
tts.togglePlay();
};
// 处理停止
const handleStop = () => {
speechUtil.reset();
resetState();
};
// 重置播放
const handleReset = () => {
tts.reset();
status.value = '就绪';
progress.value = 0;
isPaused.value = false;
addLog('已重置所有状态');
};
// 重置状态
const resetState = () => {
isProcessing.value = false;
isPaused.value = false;
isPlaying.value = false;
status.value = '就绪';
progress.value = 0;
errorMessage.value = '';
};
// 生命周期钩子
onMounted(() => {
setupEventListeners();
});
// 生命周期钩子
onMounted(() => {
setupEventListeners();
});
onBeforeUnmount(() => {
speechUtil.reset();
});
onBeforeUnmount(() => {
tts.reset();
});
</script>
<style scoped>
.speech-container {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.speech-demo {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
textarea {
width: 100%;
height: 150px;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
}
.demo-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
}
.control-panel {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #4CAF50;
color: white;
cursor: pointer;
}
textarea {
width: 100%;
height: 100px;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
button {
padding: 8px 16px;
margin-right: 10px;
border: none;
border-radius: 4px;
background-color: #4CAF50;
color: white;
cursor: pointer;
}
.status-panel {
background-color: #f5f5f5;
padding: 15px;
border-radius: 4px;
}
button:disabled {
background-color: #cccccc;
}
.status-item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.segment-container {
margin-bottom: 15px;
}
.progress-bar {
flex: 1;
height: 20px;
background-color: #ddd;
border-radius: 10px;
overflow: hidden;
margin-left: 10px;
position: relative;
}
.segment {
display: inline-block;
padding: 5px 10px;
margin: 5px;
background-color: #f5f5f5;
border-radius: 4px;
}
.progress-fill {
height: 100%;
background-color: #4CAF50;
transition: width 0.3s ease;
}
.controls {
margin-bottom: 15px;
}
.error-message {
margin-top: 20px;
padding: 10px;
background-color: #ffebee;
color: #c62828;
border-radius: 4px;
}
.status {
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}
.status-ready { color: #2196F3; }
.status-playing { color: #4CAF50; }
.status-paused { color: #FF9800; }
.status-error { color: #F44336; }
.log-container {
height: 200px;
overflow-y: auto;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}
.log-item {
padding: 5px;
border-bottom: 1px solid #eee;
font-family: monospace;
}
</style>

@@ -456,2 +512,2 @@ ```

乔振 <qiaozhenleve@gmail.com>
乔振 <qiaozhenleve@gmail.com>

@@ -295,7 +295,8 @@ /**

// 确保每次播放前都创建新的音频上下文
if (!this.audioContext) {
this.audioContext = uni.createInnerAudioContext();
this.bindAudioEvents();
}
// 销毁旧的音频上下文(如果存在)
this.destroyAudioContext();
// 创建新的音频上下文
this.audioContext = uni.createInnerAudioContext();
this.bindAudioEvents();

@@ -315,8 +316,2 @@ this.setState({

await this.safePlay();
// 移除这里的 emit,因为 handleAudioPlay 会处理
// this.emit(EventType.AUDIO_PLAY, {
// currentAudio,
// currentText: currentItem?.text || '',
// remainingCount: this.audioQueue.length - 1
// });
} catch (error) {

@@ -331,3 +326,2 @@ console.warn('播放失败:', error);

this.pendingAudioUrl = currentAudio;
// 播放失败时销毁音频上下文
this.destroyAudioContext();

@@ -402,9 +396,13 @@ }

this.setState({ isPlaying: true });
this.emit(EventType.AUDIO_PLAY, {
currentAudio,
currentText: currentItem?.text || '',
remainingCount: this.audioQueue.length - 1, // 减1是因为不包括当前正在播放的
totalCount: this.totalAudioCount
});
// 确保只有在真正开始播放时才更新状态和触发事件
if (currentAudio && currentItem) {
this.setState({ isPlaying: true, isPaused: false });
this.emit(EventType.AUDIO_PLAY, {
currentAudio,
currentText: currentItem.text,
remainingCount: this.audioQueue.length - 1,
totalCount: this.totalAudioCount,
progress: this.calculateProgress()
});
}
}

@@ -420,30 +418,39 @@

if (!finishedAudio || !finishedItem) return;
// 先设置播放状态为 false
this.setState({ isPlaying: false });
// 移除已播放的音频
this.audioQueue.shift();
this.audioTextQueue.shift();
// 计算当前进度
const playedCount = this.totalAudioCount - this.audioQueue.length;
const progress = this.totalAudioCount > 0 ? (playedCount / this.totalAudioCount) * 100 : 0;
// 发送进度事件
this.emit(EventType.PROGRESS, {
progress: Math.round(progress), // 四舍五入到整数
playedCount,
totalCount: this.totalAudioCount,
currentText: finishedItem?.text || '',
remainingCount: this.audioQueue.length // 剩余数量就是队列的长度
});
// 检查是否还有待播放的音频
if (this.audioQueue.length > 0) {
if (this.audioQueue.length > 1) {
// 移除当前音频
this.audioQueue.shift();
this.audioTextQueue.shift();
// 发送进度事件
this.emit(EventType.PROGRESS, {
progress: this.calculateProgress(),
playedCount: this.totalAudioCount - this.audioQueue.length,
totalCount: this.totalAudioCount,
currentText: finishedItem.text,
remainingCount: this.audioQueue.length - 1
});
// 确保在开始下一个播放前销毁当前上下文
this.destroyAudioContext();
this.playAudioQueue();
// 使用 setTimeout 确保异步执行下一个播放
setTimeout(() => {
this.playAudioQueue();
}, 0);
} else {
// 最后一个音频播放完成
this.audioQueue.shift();
this.audioTextQueue.shift();
this.setState({
isEnded: true,
isStartPlayQueue: false,
isPlaying: false
isPlaying: false,
isPaused: false
});

@@ -453,6 +460,6 @@

finishedAudio,
finishedText: finishedItem?.text || '',
finishedText: finishedItem.text,
remainingCount: 0,
isComplete: true,
progress: 100, // 确保最后进度为100%
progress: 100,
totalCount: this.totalAudioCount,

@@ -463,2 +470,3 @@ playedCount: this.totalAudioCount

this.destroyAudioContext();
this.resetTextProcessor();
}

@@ -579,3 +587,3 @@ }

/**
* 结束计时并返回耗时(毫���)
* 结束计时并返回时(毫秒)
* @private

@@ -735,4 +743,11 @@ */

}
// 添加进度计算辅助方法
private calculateProgress(): number {
if (this.totalAudioCount === 0) return 0;
const playedCount = this.totalAudioCount - this.audioQueue.length;
return Math.round((playedCount / this.totalAudioCount) * 100);
}
}
export default SpeechSynthesisUtil;
declare namespace UniApp {
interface SpeakOptions {
text?: string;
lang?: string;
pitch?: number;
volume?: number;
rate?: number;
}
interface CallbackOptions {
success?(res: any): void;
fail?(err: any): void;
complete?(res: any): void;
}
interface InnerAudioContext {

@@ -22,15 +8,15 @@ src: string;

destroy(): void;
onPlay(callback: () => void): void;
offPlay(callback: () => void): void;
onEnded(callback: () => void): void;
onError(callback: (res: any) => void): void;
onPlay(callback: Function): void;
onEnded(callback: Function): void;
onError(callback: Function): void;
offPlay(callback: Function): void;
}
interface FileSystemManager {
writeFile(options: {
writeFile(option: {
filePath: string;
data: string | ArrayBuffer;
encoding?: string;
success?(res: any): void;
fail?(error: any): void;
encoding: string;
success?: Function;
fail?: Function;
}): void;

@@ -44,17 +30,6 @@ unlinkSync(filePath: string): void;

getFileSystemManager(): UniApp.FileSystemManager;
createSpeechSynthesizer(): {
speak(options: UniApp.SpeakOptions & UniApp.CallbackOptions): void;
stop(options: UniApp.CallbackOptions): void;
};
request(options: any): void;
env: {
USER_DATA_PATH: string;
};
request(options: {
url: string;
method: string;
header: Record<string, string>;
data: any;
success(res: any): void;
fail(error: any): void;
}): void;
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc