微信小程序语音转文字
要在微信中实现语音转文字,可通过官方提供的「微信同声传译插件」或第三方工具完成。以下是具体方案
1. 接入流程
添加插件:登录微信公众平台(https://mp.weixin.qq.com),进入「设置 → 第三方服务 → 插件管理」,搜索「微信同声传译」并添加,获取其 AppID(如wx069ba97219f66d99)和最新版本号
配置小程序:在app.json中配置插件信息,示例代码如下
{
"plugins": {
"WechatSI": {
"version": "0.3.4", // 填写实际版本号
"provider": "wx069ba97219f66d99"
}
}
}
- 需确保版本号与插件详情页一致,否则可能导致功能异常
2. 代码实现
引入插件:在 JS 文件中引入插件并获取语音识别管理器
const plugin = requirePlugin('WechatSI');
const manager = plugin.getRecordRecognitionManager();
录音控制:通过按钮绑定录音事件,示例代码
// 开始录音
touchStart() {
this.setData({ recordState: true });
manager.start({ lang: 'zh_CN' }); // 支持zh_CN、en_US、zh_HK等语言
}
// 结束录音
touchEnd() {
this.setData({ recordState: false });
manager.stop();
}
监听识别结果:通过回调函数获取实时或最终识别文本
manager.onRecognize = (res) => {
this.setData({ content: res.result }); // 实时更新文本
};
manager.onStop = (res) => {
console.log('最终识别结果:', res.result); // 结束后完整文本
};
- 需注意,onRecognize可能包含重复内容,需结合onStop结果进行去重或合并
3. 注意事项
权限申请:需在小程序后台的「用户隐私保护指引」中声明使用录音功能,否则无法获取麦克风权限
以下完整代码
-
app.json
{ "pages": [ "pages/voiceToText/voiceToText" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "微信同声传译示例", "navigationBarTextStyle": "black" }, "plugins": { "WechatSI": { "version": "0.3.4", "provider": "wx069ba97219f66d99" } }, "sitemapLocation": "sitemap.json" } -
pages/voiceToText/voiceToText.json
{ "navigationBarTitleText": "语音转文字", "usingComponents": {} } -
pages/voiceToText/voiceToText.ts
// 引入微信同声传译插件 const plugin = requirePlugin('WechatSI') // 获取全局唯一的语音识别管理器 const manager = plugin.getRecordRecognitionManager() Page({ /** * 页面的初始数据 */ data: { // 录音状态:false-未录音,true-正在录音 isRecording: false, // 识别结果文本 resultText: '', // 历史记录列表 historyList: [] as string[], // 录音时长(秒) recordDuration: 0, // 定时器ID timer: 0 as number, // 支持的语言列表 languages: [ { code: 'zh_CN', name: '中文' }, { code: 'en_US', name: '英语' }, { code: 'zh_HK', name: '粤语' } ], // 当前选中的语言 currentLang: 'zh_CN' }, /** * 生命周期函数--监听页面加载 */ onLoad() { this.initRecordManager() // 从本地存储加载历史记录 const history = wx.getStorageSync('voiceToTextHistory') if (history && Array.isArray(history)) { this.setData({ historyList: history }) } }, /** * 初始化录音管理器 */ initRecordManager() { // 识别中事件 manager.onRecognize = (res: { result: string }) => { console.log('实时识别结果:', res.result) this.setData({ resultText: res.result }) } // 识别结束事件 manager.onStop = (res: { result: string }) => { console.log('识别结束:', res) let text = res.result // 处理可能的空结果 if (!text) { wx.showToast({ title: '未识别到内容', icon: 'none', duration: 2000 }) text = '' } else { // 保存到历史记录 this.addToHistory(text) } this.setData({ resultText: text, isRecording: false, recordDuration: 0 }) // 清除定时器 if (this.data.timer) { clearInterval(this.data.timer) } } // 识别错误事件 manager.onError = (err: { errMsg: string }) => { console.error('识别错误:', err) wx.showToast({ title: '识别失败: ' + err.errMsg, icon: 'none', duration: 3000 }) this.setData({ isRecording: false, recordDuration: 0 }) if (this.data.timer) { clearInterval(this.data.timer) } } }, /** * 开始录音 */ startRecord() { // 检查录音权限 wx.getSetting({ success: (res) => { if (!res.authSetting['scope.record']) { // 申请录音权限 wx.authorize({ scope: 'scope.record', success: () => { this.startRecording() }, fail: () => { wx.showModal({ title: '权限不足', content: '需要录音权限才能使用语音识别功能,请在设置中开启', confirmText: '去设置', success: (modalRes) => { if (modalRes.confirm) { wx.openSetting() } } }) } }) } else { this.startRecording() } } }) }, /** * 实际开始录音的逻辑 */ startRecording() { // 重置结果 this.setData({ resultText: '', isRecording: true, recordDuration: 0 }) // 开始录音 manager.start({ lang: this.data.currentLang, duration: 60000 // 最长录音时间,单位ms }) // 开始计时 const timer = setInterval(() => { this.setData({ recordDuration: this.data.recordDuration + 1 }) }, 1000) this.setData({ timer }) }, /** * 停止录音 */ stopRecord() { if (this.data.isRecording) { manager.stop() } }, /** * 添加到历史记录 */ addToHistory(text: string) { const newHistory = [text, ...this.data.historyList] // 限制历史记录数量为20条 if (newHistory.length > 20) { newHistory.pop() } this.setData({ historyList: newHistory }) // 保存到本地存储 wx.setStorageSync('voiceToTextHistory', newHistory) }, /** * 清空当前结果 */ clearResult() { this.setData({ resultText: '' }) }, /** * 复制结果到剪贴板 */ copyResult() { if (!this.data.resultText) { wx.showToast({ title: '没有可复制的内容', icon: 'none' }) return } wx.setClipboardData({ data: this.data.resultText, success: () => { wx.showToast({ title: '复制成功', icon: 'success' }) } }) }, /** * 清空历史记录 */ clearHistory() { if (this.data.historyList.length === 0) return wx.showModal({ title: '确认', content: '确定要清空所有历史记录吗?', success: (res) => { if (res.confirm) { this.setData({ historyList: [] }) wx.removeStorageSync('voiceToTextHistory') } } }) }, /** * 选择语言 */ selectLanguage(e: { currentTarget: { dataset: { code: string } } }) { const langCode = e.currentTarget.dataset.code this.setData({ currentLang: langCode }) }, /** * 生命周期函数--监听页面卸载 */ onUnload() { // 页面卸载时停止录音并清除定时器 if (this.data.isRecording) { manager.stop() } if (this.data.timer) { clearInterval(this.data.timer) } } }) -
pages/voiceToText/voiceToText.wxml
<view class="container"> <view class="title">语音转文字</view> <!-- 语言选择 --> <view class="language-selector"> <view class="lang-item" wx:for="{{languages}}" wx:key="code" data-code="{{item.code}}" bindtap="selectLanguage" class="{{currentLang === item.code ? 'lang-selected' : ''}}"> {{item.name}} </view> </view> <!-- 结果显示区域 --> <view class="result-container"> <textarea class="result-text" value="{{resultText}}" placeholder="点击下方按钮开始说话..." disabled ></textarea> <!-- 操作按钮 --> <view class="action-buttons"> <button class="btn clear-btn" bindtap="clearResult" disabled="{{!resultText}}" > 清空 </button> <button class="btn copy-btn" bindtap="copyResult" disabled="{{!resultText}}" > 复制 </button> </view> </view> <!-- 录音控制 --> <view class="record-control"> <view class="record-button" bindtouchstart="startRecord" bindtouchend="stopRecord" class="{{isRecording ? 'recording' : ''}}" > <text class="record-icon">{{isRecording ? '⏹️' : '🎤'}}</text> <text class="record-text">{{isRecording ? '松开停止' : '按住说话'}}</text> <text class="record-time" wx:if="{{isRecording}}">{{recordDuration}}s</text> </view> </view> <!-- 历史记录 --> <view class="history-section" wx:if="{{historyList.length > 0}}"> <view class="history-header"> <text>历史记录</text> <button class="clear-history-btn" bindtap="clearHistory">清空</button> </view> <view class="history-list"> <view class="history-item" wx:for="{{historyList}}" wx:key="index"> <text>{{item}}</text> </view> </view> </view> </view> -
pages/voiceToText/voiceToText.wxss
.container { display: flex; flex-direction: column; min-height: 100vh; background-color: #f5f5f5; padding: 20rpx; } .title { font-size: 36rpx; font-weight: bold; text-align: center; margin: 30rpx 0; color: #333; } .language-selector { display: flex; justify-content: center; margin-bottom: 20rpx; flex-wrap: wrap; } .lang-item { padding: 10rpx 20rpx; margin: 0 10rpx 10rpx; background-color: #fff; border-radius: 20rpx; font-size: 28rpx; border: 1px solid #eee; } .lang-selected { background-color: #07c160; color: white; border-color: #07c160; } .result-container { background-color: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 30rpx; box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05); } .result-text { width: 100%; min-height: 200rpx; font-size: 30rpx; line-height: 1.6; padding: 10rpx; border: none; resize: none; color: #333; } .result-text::placeholder { color: #ccc; } .action-buttons { display: flex; justify-content: flex-end; margin-top: 10rpx; } .btn { padding: 10rpx 20rpx; margin-left: 15rpx; font-size: 26rpx; border-radius: 8rpx; } .clear-btn { background-color: #f5f5f5; color: #666; } .copy-btn { background-color: #07c160; color: white; } .record-control { display: flex; justify-content: center; margin: 40rpx 0 60rpx; } .record-button { width: 160rpx; height: 160rpx; border-radius: 50%; background-color: #ff4d4f; display: flex; flex-direction: column; justify-content: center; align-items: center; color: white; box-shadow: 0 5rpx 15rpx rgba(255,77,79,0.4); touch-action: manipulation; } .record-button.recording { background-color: #faad14; box-shadow: 0 5rpx 15rpx rgba(250,173,20,0.4); } .record-icon { font-size: 60rpx; margin-bottom: 5rpx; } .record-text { font-size: 24rpx; } .record-time { font-size: 22rpx; margin-top: 5rpx; } .history-section { margin-top: auto; background-color: #fff; border-radius: 16rpx; padding: 20rpx; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05); } .history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15rpx; padding-bottom: 10rpx; border-bottom: 1px solid #f5f5f5; } .history-header text { font-size: 30rpx; font-weight: 500; color: #666; } .clear-history-btn { font-size: 24rpx; color: #07c160; background: none; padding: 0; } .history-list { max-height: 300rpx; overflow-y: auto; } .history-item { padding: 15rpx 0; border-bottom: 1px solid #f5f5f5; font-size: 26rpx; color: #333; line-height: 1.5; } .history-item:last-child { border-bottom: none; }

浙公网安备 33010602011771号