uniapp 微信小程序 同声传译插件

182 阅读7分钟

封装工具代码

/*
  我使用的是:微信同声传译插件
  实现功能:
  1.语音输入
  2.文本翻译
  3.语音合成
*/

import { ref } from 'vue';

const plugin = requirePlugin('WechatSI');

// ------------------------------ 语音输入功能 开始 ------------------------------
// 管理器
const manager = plugin.getRecordRecognitionManager();
// 识别结果
const recognizedText = ref('');
// 是否正在识别
const isRecognizing = ref(false);

// 开始识别
/*
options: 识别配置
duration	Number	否	60000	指定录音的时长,单位ms,最大为60000。如果传入了合法的 duration ,在到达指定的 duration 后会自动停止录音
lang	String	否	zh_CN	识别的语言,目前支持 zh_CN(中国大陆) en_US(英语) sichuanhua(四川话) yue(粤语)
*/
const startRecord = (options) => {
	if (isRecognizing.value) {
		uni.showToast({
			title: '正在识别中,请先停止识别',
			icon: 'none',
		});
		return;
	}
	isRecognizing.value = true;
	manager.start({
		duration: options?.duration || 60000,
		lang: options?.lang || 'zh_CN',
	});
};
manager.onStart = function (res) {
	console.log('成功开始录音识别', res);
};
// 识别结束
const stopRecord = () => {
	isRecognizing.value = false;
	manager.stop();
};
/*
tempFilePath	String	录音临时文件地址
duration	Number	录音总时长,单位: ms
fileSize	Number	文件大小,单位: B
result	String	最终识别结果
*/
manager.onStop = function (res) {
	console.log('识别结束', res);
	recognizedText.value = res.result;
	isRecognizing.value = false;
};
// 识别错误
/*
retcode	Int	错误码
msg	String	错误信息
*/
manager.onError = function (res) {
	console.error('识别错误', res);
	let msg = '';
	switch (res.retcode) {
		case -30001:
			msg = '录音接口出错';
			break;
		case -30002:
			msg = '录音暂停接口被调用,录音终止,识别终止';
			break;
		case -30003:
			msg = '录音帧数据未产生或者发送失败导致的数据传输失败';
			break;
		case -30004:
			msg = '因网络或者其他非正常状态导致的未查询识别结果';
			break;
		case -30005:
			msg = '语音识别服务内部错误';
			break;
		case -30006:
			msg = '语音识别服务未在限定时间内识别完成';
			break;
		case -30007:
			msg = 'start启动参数错误';
			break;
		case -30008:
			msg = '查询请求时网络失败';
			break;
		case -30009:
			msg = '创建鉴权内部失败';
			break;
		case -30010:
			msg = '发送鉴权时网络失败';
			break;
		case -30011:
			msg = '试图在识别正在进行中是再次调用start,返回错误,正在进行的识别任务正常进行';
			break;
		case -30012:
			msg = '当前无识别任务进行时调用stop错误';
			break;
		case -30013:
			msg = '其他未知错误';
			break;
		case -40001:
			msg = '达到接口调用频率限制';
			break;
		default:
			msg = res.msg || '其他未知错误';
			break;
	}
	isRecognizing.value = false;
	uni.showModal({
		title: '提示',
		content: msg,
		showCancel: false,
	});
};
// ------------------------------ 语音输入功能 结束 ------------------------------

const audioContext = uni.createInnerAudioContext();

// ------------------------------ 文本翻译功能 开始 ------------------------------

// 文本翻译结果
const translatedText = ref('');
// 是否正在翻译
const isTranslating = ref(false);
// 文本翻译后合成的语音地址
const translatedVoiceUrl = ref('');

// 开始翻译
/*
参数说明:
lfrom	String	是	文本语言 zh_CN(中国大陆) en_US(英语) yue(粤语)
lto	String	是	目标语言 zh_CN(中国大陆) en_US(英语)yue(粤语)
content	String	是	需要被翻译的文本内容,后台限制200字节大小
tts	Boolean	否	是否对翻译结果进行语音合成,默认为false,不进行语音合成
*/
const translateText = (options) => {
	if (isTranslating.value) {
		uni.showToast({
			title: '正在翻译中,请稍后再试',
			icon: 'none',
		});
		return;
	}
	if (!options.lfrom) {
		uni.showToast({
			title: '请输入文本语言',
			icon: 'none',
		});
		return;
	}
	if (!options.lto) {
		uni.showToast({
			title: '请输入目标语言',
			icon: 'none',
		});
		return;
	}
	if (options.lfrom === options.lto) {
		uni.showToast({
			title: '文本语言和目标语言不能相同',
			icon: 'none',
		});
		return;
	}
	if (!options.content) {
		uni.showToast({
			title: '请输入文本内容',
			icon: 'none',
		});
		return;
	}
	isTranslating.value = true;
	plugin.translate({
		lfrom: options.lfrom,
		lto: options.lto,
		content: options.content,
		tts: options.tts || false,
		success: (res) => {
			/*
      retcode	Int	状态码
        0	翻译合成成功
        -10006	翻译成功,合成内部错误
        -10007	翻译成功,传入了不支持的语音合成语言
        -10008	翻译成功,语音合成达到频率限制
      origin	String	原始文本
      result	String	翻译结果
      filename	String	语音合成返回的语音地址,仅支持合成中文语音
      expired_time	Int	语音合成链接超时时间戳 如1525930552超时后无法播放,可使用时间为3小时
      */
			console.log('翻译结果', res);
			translatedText.value = res.result;
			translatedVoiceUrl.value = res.filename;
			isTranslating.value = false;
		},
		fail: (res) => {
			/*
      retcode	Int	错误码
        -10001	语言检查错误
        -10002	输入的待翻译内容格式不正确
        -10003	传入过长的待翻译文本内容
        -10004	翻译内部逻辑错误
        -10005	请求发送失败,请检查网络
        -40001	接口调用频率达到限制,请联系插件开发者
      msg	String	错误信息
      */
			console.error('翻译失败', res);
			let msg = '';
			switch (res.retcode) {
				case -10001:
					msg = '语言检查错误';
					break;
				case -10002:
					msg = '输入的待翻译内容格式不正确';
					break;
				case -10003:
					msg = '传入过长的待翻译文本内容';
					break;
				case -10004:
					msg = '翻译内部逻辑错误';
					break;
				case -10005:
					msg = '请求发送失败,请检查网络';
					break;
				case -40001:
					msg = '接口调用频率达到限制,请联系插件开发者';
					break;
				default:
					msg = res.msg || '其他未知错误';
					break;
			}
			isTranslating.value = false;
			uni.showModal({
				title: '提示',
				content: msg,
				showCancel: false,
			});
		},
	});
};

const playTranslatedVoice = () => {
	if (isTranslating.value) {
		uni.showToast({
			title: '正在翻译中,请稍后再试',
			icon: 'none',
		});
		return;
	}
	if (!translatedVoiceUrl.value) {
		uni.showToast({
			title: '没有翻译结果音频',
			icon: 'none',
		});
		return;
	}
	audioContext.stop();
	audioContext.src = translatedVoiceUrl.value;
	audioContext.play();
};

// ------------------------------ 文本翻译功能 结束 ------------------------------

// ------------------------------ 语音合成功能 开始 ------------------------------
const voiceUrl = ref('');
// 是否正在语音合成
const isTextToSpeech = ref(false);
/*
参数说明:
lang	String	是	文本语言 zh_CN(中国大陆)en_US(英文)
content	String	是	需要被合成的文本内容,后台限制长度在50个字符内
success	Function		调用成功时触发的callback
fail	Function		调用失败时触发的callback
*/
const textToSpeech = (options) => {
	if (isTextToSpeech.value) {
		uni.showToast({
			title: '正在语音合成中,请稍后再试',
			icon: 'none',
		});
		return;
	}
	if (!options.lang) {
		uni.showToast({
			title: '请输入文本语言',
			icon: 'none',
		});
		return;
	}
	if (!options.content) {
		uni.showToast({
			title: '请输入文本内容',
			icon: 'none',
		});
		return;
	}
	isTextToSpeech.value = true;
	plugin.textToSpeech({
		lang: options.lang,
		tts: true,
		content: options.content,
		success: function (res) {
			/*
      retcode	Int	retcode == 0 时请求成功
      origin	String	原始文本
      filename	String	语音合成返回的语音地址,可自行下载使用
      expired_time	Int	语音合成链接超时时间戳 如1525930552,超时后无法播放,可使用时间为3小时
      */
			console.log('语音合成结果', res);
			voiceUrl.value = res.filename;
			isTextToSpeech.value = false;
		},
		fail: function (res) {
			/*
      retcode	Int	错误码
        -20001	语音合成语言格式出错
        -20002	输入的待合成格式不正确
        -20003	语音合成内部错误
        -20005	网络错误
        -40001	接口调用频率达到限制,请联系插件开发者
      msg	String	错误信息
      */
			console.error('语音合成失败', res);
			let msg = '';
			switch (res.retcode) {
				case -20001:
					msg = '语音合成语言格式出错';
					break;
				case -20002:
					msg = '输入的待合成格式不正确';
					break;
				case -20003:
					msg = '语音合成内部错误';
					break;
				case -20005:
					msg = '网络错误';
					break;
				case -40001:
					msg = '接口调用频率达到限制,请联系插件开发者';
					break;
				default:
					msg = res.msg || '其他未知错误';
					break;
			}
			isTextToSpeech.value = false;
			uni.showModal({
				title: '提示',
				content: msg,
				showCancel: false,
			});
		},
	});
};

const playVoice = () => {
	if (isTextToSpeech.value) {
		uni.showToast({
			title: '正在语音合成中,请稍后再试',
			icon: 'none',
		});
		return;
	}
	if (!voiceUrl.value) {
		uni.showToast({
			title: '没有语音合成结果音频',
			icon: 'none',
		});
		return;
	}
	audioContext.stop();
	audioContext.src = voiceUrl.value;
	audioContext.play();
};
// ------------------------------ 语音合成功能 结束 ------------------------------

export default {
	recognizedText,
	translatedText,
	translatedVoiceUrl,
	voiceUrl,
	startRecord,
	stopRecord,
	translateText,
	playTranslatedVoice,
	textToSpeech,
	playVoice,
};

示例页面

<template>
	<div class="page-box">
		<div class="box">
			<div class="title">语音识别结果 :</div>
			<div>{{ face2FaceTranslator.recognizedText || '暂无结果' }}</div>
		</div>
		<div class="box box-space">
			<div class="btn" @click="startRecord">开始录音</div>
			<div class="btn" @click="stopRecord">结束录音</div>
		</div>
		<div class="line-box">
			<div class="line"></div>
			<div class="text">分割线</div>
			<div class="line"></div>
		</div>
		<div class="box">
			<div class="title">文本语言 : </div>
			<div>
				<radio-group>
					<div class="radio-box">
						<div class="radio-item" v-for="item in translatedLangList" :key="item.id">
							<div class="name">{{ item.name }}</div>
							<radio
								:value="item.id"
								:checked="item.id === translatedForm"
								@click="translatedForm = item.id"
							/>
						</div>
					</div>
				</radio-group>
			</div>
		</div>
		<div class="box">
			<div class="title">目标语言 : </div>
			<div>
				<radio-group>
					<div class="radio-box">
						<div class="radio-item" v-for="item in translatedLangList" :key="item.id">
							<div class="name">{{ item.name }}</div>
							<radio
								:value="item.id"
								:checked="item.id === translatedTo"
								@click="translatedTo = item.id"
							/>
						</div>
					</div>
				</radio-group>
			</div>
		</div>
		<div class="box">
			<textarea v-model="translatedContent" placeholder="请输入翻译文本" />
		</div>
		<div class="box">
			<div class="title">翻译结果 :</div>
			<div>{{ face2FaceTranslator.translatedText || '暂无结果' }}</div>
		</div>
		<div class="box box-space">
			<div class="btn" @click="translateText">翻译</div>
			<div class="btn" @click="playTranslatedVoice">播放翻译结果</div>
		</div>
		<div class="line-box">
			<div class="line"></div>
			<div class="text">分割线</div>
			<div class="line"></div>
		</div>
		<div class="box">
			<div class="title">文本语言 : </div>
			<div>
				<radio-group>
					<div class="radio-box">
						<div class="radio-item" v-for="item in speechLangList" :key="item.id">
							<div class="name">{{ item.name }}</div>
							<radio
								:value="item.id"
								:checked="item.id === speechLang"
								@click="speechLang = item.id"
							/>
						</div>
					</div>
				</radio-group>
			</div>
		</div>
		<div class="box">
			<textarea v-model="speechContent" placeholder="请输入语音合成文本" />
		</div>
		<div class="box box-space">
			<div class="btn" @click="textToSpeech">语音合成</div>
			<div class="btn" @click="playVoice">播放语音合成结果</div>
		</div>
	</div>
</template>

<script setup>
	import { ref, onMounted, computed, watchEffect, getCurrentInstance } from 'vue';
	import { onLoad } from '@dcloudio/uni-app';

	const { proxy } = getCurrentInstance();

	onMounted(() => {});
	onLoad((res) => {});

	import face2FaceTranslator from '@/utils/Face2FaceTranslator.js';

	function startRecord() {
		face2FaceTranslator.startRecord();
	}
	function stopRecord() {
		face2FaceTranslator.stopRecord();
	}

	const translatedLangList = ref([
		{
			id: 'zh_CN',
			name: '中文',
		},
		{
			id: 'en_US',
			name: '英文',
		},
		{
			id: 'yue',
			name: '粤语',
		},
	]);
	const translatedForm = ref('');
	const translatedTo = ref('');
	const translatedContent = ref('');
	function translateText() {
		face2FaceTranslator.translateText({
			lfrom: translatedForm.value,
			lto: translatedTo.value,
			content: translatedContent.value,
			tts: true,
		});
	}
	function playTranslatedVoice() {
		face2FaceTranslator.playTranslatedVoice();
	}

	const speechLangList = ref([
		{
			id: 'zh_CN',
			name: '中文',
		},
		{
			id: 'en_US',
			name: '英文',
		},
	]);
	const speechLang = ref('');
	const speechContent = ref('');
	function textToSpeech() {
		face2FaceTranslator.textToSpeech({
			lang: speechLang.value,
			content: speechContent.value,
		});
	}
	function playVoice() {
		face2FaceTranslator.playVoice();
	}
</script>

<style>
	page {
		background: #f5f5f5;
	}
</style>
<style lang="scss" scoped>
	.page-box {
		padding: 30rpx;
	}
	.box {
		margin-bottom: 20rpx;
		background: #ffffff;
		box-shadow: 0rpx 8rpx 20rpx 0rpx rgba(9, 198, 116, 0.1);
		border-radius: 24rpx 24rpx 24rpx 24rpx;
		padding: 30rpx;
		font-weight: normal;
		font-size: 28rpx;
		color: #000000;
	}
	.title {
		font-weight: normal;
		font-size: 28rpx;
		color: #000000;
		margin-bottom: 20rpx;
	}
	.radio-box {
		display: flex;
		align-items: center;
		.radio-item {
			display: flex;
			align-items: center;
			margin-right: 20rpx;
			.name {
				font-weight: normal;
				font-size: 28rpx;
				color: #000000;
				margin-right: 20rpx;
			}
		}
	}
	textarea {
		width: auto;
		border: 2rpx solid #ccc;
		border-radius: 24rpx;
		padding: 20rpx;
		background: #eee;
		height: 200rpx;
	}
	.box-space {
		display: flex;
		justify-content: space-between;
		align-items: center;
	}
	.btn {
		width: 45%;
		display: flex;
		justify-content: center;
		align-items: center;
		padding: 20rpx;
		border-radius: 50rpx;
		background: #0374f8;
		font-weight: normal;
		font-size: 28rpx;
		color: #fff;
	}
	.line-box {
		margin: 40rpx;
		display: flex;
		justify-content: space-between;
		align-items: center;
		text {
			font-weight: normal;
			font-size: 28rpx;
			color: #000000;
			margin: 0 20rpx;
		}
		.line {
			flex: 1;
			height: 2rpx;
			background: #ccc;
		}
	}
</style>