[Feat]新增支持多语言的TTS

[Feat]新增支持多语言的TTS
This commit is contained in:
chenfufeng
2023-02-24 14:11:40 +08:00
parent 5d378b833b
commit 1e7b13f151
19 changed files with 564 additions and 2 deletions

View File

@@ -426,6 +426,6 @@
-keep class com.squareup.haha.guava.collect.*{*;}
-keep class **.zego.**{*;}
#-----科大讯飞语音合成-----
-keep class com.iflytek.**{*;}

View File

@@ -142,6 +142,7 @@ ext {
ttszhi : "com.mogo.tts:tts-zhi:${TTS_ZHI_VERSION}",
ttspad : "com.mogo.tts:tts-pad:${TTS_ZHI_VERSION}",
ttsnoop : "com.mogo.tts:tts-noop:${TTS_NOOP_VERSION}",
ttsiflytek : "com.mogo.tts:tts-iflytek :${TTS_IFLYTEK_VERSION}",
//========================= 网约车 Maven 版本管理 =========================
mogooch : "com.mogo.och:och:${MOGO_OCH_VERSION}",

View File

@@ -6,6 +6,7 @@ import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger;
import com.mogo.eagle.core.utilcode.util.ToastUtils;
import com.mogo.tts.base.IMogoTTS;
import com.mogo.tts.base.IMogoTTSCallback;
import com.mogo.tts.base.MultiLangTtsEntity;
import com.mogo.tts.base.PreemptType;
/**
@@ -52,7 +53,9 @@ public class AIAssist {
private AIAssist(Context context) {
try {
// 暂时换成反射,解决死锁问题
// TODO:("支持切换思必驰和科大讯飞")
Class<?> clazz = Class.forName("com.mogo.tts.pad.PadTTS");
// Class<?> clazz = Class.forName("com.mogo.tts.iflytek.IFlyTekTts");
mTTS = (IMogoTTS) clazz.getConstructor().newInstance();
mTTS.init(context);
// mTTS = (IMogoTTS) ARouter.getInstance().build(MogoTTSConstants.API_PATH).navigation(context.getApplicationContext());
@@ -122,6 +125,18 @@ public class AIAssist {
}
}
/**
* 支持多语言的Tts
* @param ttsEntity: 多语言Entity
* @param level: 等级由低到高为0、1、2、3分别对应p3、p2、p1、p0
* @param callback
*/
public void speakMultiLangTTSWithLevel(MultiLangTtsEntity ttsEntity, int level, IMogoTTSCallback callback) {
if (mTTS != null) {
mTTS.speakMultiLangTTSWithLevel(ttsEntity, level, callback);
}
}
/**
* 语音播报
*

View File

@@ -123,6 +123,7 @@ TTS_DI_VERSION=2.1.16.10
TTS_ZHI_VERSION=2.1.16.10
TTS_PAD_VERSION=2.1.16.10
TTS_NOOP_VERSION=2.1.16.10
TTS_IFLYTEK_VERSION=2.1.16.10
# 自研地图
MAP_CUSTOM_VERSION=2.1.16.10

View File

@@ -50,6 +50,7 @@ include ':libraries:mogo-obu'
// 语音
include ':tts:tts-base'
include ':tts:tts-pad'
include ':tts:tts-iflytek'
// 测试DEBUG
include ':test:crashreport'

View File

@@ -64,6 +64,12 @@ interface IMogoTTS extends IProvider {
*/
void speakTTSVoice( String tts, PreemptType type, IMogoTTSCallback callback );
void speakMultiLangTTSWithLevel(
MultiLangTtsEntity ttsEntity,
int level,
IMogoTTSCallback callback
);
void stopSpeakTts(String text);
void stopTts();

View File

@@ -0,0 +1,7 @@
package com.mogo.tts.base
enum class LanguageType(val langName: String) {
CHINESE("chinese"),
ENGLISH("english"),
KOREAN("korean")
}

View File

@@ -0,0 +1,42 @@
package com.mogo.tts.base
data class MultiLangTtsEntity(
private var ttsList: List<LangTtsEntity>
) {
private val stringBuffer by lazy {
StringBuffer()
}
private var ttsIndex = 0
fun ttsNext(): LangTtsEntity? {
return if (ttsIndex in ttsList.indices) {
ttsList[ttsIndex++]
} else {
null
}
}
override fun toString(): String {
return stringBuffer.let {
it.setLength(0)
ttsList.forEachIndexed { index, langTtsEntity ->
if (index != ttsList.size - 1) {
it.append("${langTtsEntity};")
} else {
it.append(langTtsEntity)
}
}
it.toString()
}
}
}
data class LangTtsEntity(
var ttsContent: String,
var language: LanguageType
) {
override fun toString(): String {
return "ttsContent is:$ttsContent,language is:${language.langName}"
}
}

1
tts/tts-iflytek/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,62 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
id 'com.alibaba.arouter'
}
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
// buildToolsVersion rootProject.ext.android.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode Integer.valueOf(VERSION_CODE)
versionName getValueFromRootProperties("${project.name.replace("-", "_").toUpperCase()}_VERSION")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
//ARouter apt 参数
kapt {
useBuildCache = false
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation rootProject.ext.dependencies.androidxappcompat
implementation rootProject.ext.dependencies.arouter
kapt rootProject.ext.dependencies.aroutercompiler
api rootProject.ext.dependencies.aiassist
api rootProject.ext.dependencies.aiassistReplace
if (Boolean.valueOf(USE_MAVEN_PACKAGE)) {
implementation rootProject.ext.dependencies.ttsbase
implementation rootProject.ext.dependencies.mogo_core_utils
} else {
implementation project(":tts:tts-base")
implementation project(':core:mogo-core-utils')
}
}
apply from: new File(rootProject.rootDir, "gradle/upload.gradle").toString()

View File

View File

@@ -0,0 +1,3 @@
GROUP=com.mogo.tts
POM_ARTIFACT_ID=tts-iflytek
VERSION_CODE=1

Binary file not shown.

21
tts/tts-iflytek/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mogo.tts.iflytek">
</manifest>

View File

@@ -0,0 +1,375 @@
package com.mogo.tts.iflytek
import android.content.Context
import android.os.Bundle
import android.os.Looper
import android.util.Pair
import androidx.annotation.Keep
import com.iflytek.cloud.*
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger.d
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger.i
import com.mogo.eagle.core.utilcode.util.UiThreadHandler
import com.mogo.tts.base.*
import java.util.*
@Keep
class IFlyTekTts : IMogoTTS, InitListener {
companion object {
const val TAG = "IFlyTekTts"
}
private var context: Context? = null
private var ttsEngine: SpeechSynthesizer? = null
private var engineType = SpeechConstant.TYPE_CLOUD
private var voicer: String = "xiaoyan"
private var voicerEntries: Array<String>? = null
// 等级由低到高为0、1、2、3默认为-1表示没有正在tts的
private var curTtsLevel = -1
// 由于主动打断不会有回调事件所以主动打断时清掉map中被打断的text和callback
private var curTtsContent = ""
private var curTtsEntity: MultiLangTtsEntity? = null
private val linkedList = LinkedList<Pair<MultiLangTtsEntity, Int>>()
// 上一次语音合成的语言
private var lastLang = LanguageType.CHINESE
private val speakVoiceMap by lazy {
HashMap<String, IMogoTTSCallback>()
}
override fun init(context: Context) {
this.context = context
// 应用程序入口处调用避免手机内存过小杀死后台进程后通过历史intent进入Activity造成SpeechUtility对象为null
// 如在Application中调用初始化需要在Mainifest中注册该Applicaiton
// 注意此接口在非主进程调用会返回null对象如需在非主进程使用语音功能请增加参数SpeechConstant.FORCE_LOGIN+"=true"
// 参数间使用半角“,”分隔。
// 设置你申请的应用appid,请勿在'='与appid之间添加空格及空转义符
// 注意: appid 必须和下载的SDK保持一致否则会出现10407错误
SpeechUtility.createUtility(context, "appid=0c498b42")
ttsEngine = SpeechSynthesizer.createSynthesizer(context, this)
voicerEntries = context.resources?.getStringArray(R.array.voicer_cloud_values)
}
override fun initTts(sn: String?) {}
override fun onInit(code: Int) {
if (code != ErrorCode.SUCCESS) {
CallerLogger.e(
TAG,
"初始化失败,错误码为:$code,请点击网址https://www.xfyun.cn/document/error-code查询解决方案"
)
}
}
override fun release() {
ttsEngine?.stopSpeaking()
ttsEngine?.destroy()
}
override fun flush() {
}
override fun speakTTSVoice(tts: String?) {
}
override fun speakTTSVoice(tts: String?, callback: IMogoTTSCallback?) {
}
override fun speakTTSVoice(tts: String?, type: PreemptType?, callback: IMogoTTSCallback?) {
}
override fun speakTTSVoiceWithLevel(tts: String?, level: Int) {
}
override fun speakTTSVoiceWithLevel(tts: String?, level: Int, callBack: IMogoTTSCallback?) {
}
override fun speakMultiLangTTSWithLevel(
ttsEntity: MultiLangTtsEntity,
level: Int,
callBack: IMogoTTSCallback?
) {
if (callBack != null) {
speakVoiceMap[ttsEntity.toString()] = callBack
}
speakMultiLangTTSWithLevel(ttsEntity, level)
}
override fun stopSpeakTts(text: String?) {
stopTts()
}
override fun stopTts() {
curTtsEntity?.let {
val string = it.toString()
if (speakVoiceMap.containsKey(string)) {
speakVoiceMap.remove(string)
}
curTtsEntity = null
}
curTtsContent = ""
curTtsLevel = -1
ttsEngine?.stopSpeaking()
}
override fun speakQAndACmd(tts: String?, callback: IMogoTTSCallback?) {
}
override fun speakQAndACmd(
tts: String?,
okWords: Array<out String>?,
cancelWords: Array<out String>?,
callback: IMogoTTSCallback?
) {
}
override fun registerUnWakeupCommand(
cmd: String?,
cmdWords: Array<out String>?,
callback: IMogoTTSCallback?
) {
}
override fun unregisterUnWakeupCommand(cmd: String?) {
}
override fun unregisterUnWakeupCommand(cmd: String?, callback: IMogoTTSCallback?) {
}
override fun startAIAssist(context: Context?) {
}
override fun startAIAssist(context: Context?, status: Int) {
}
override fun breakOffSpeak() {
}
// 降序插入Tts(目前Level0、1可排队)
private fun insertTts(ttsEntity: MultiLangTtsEntity, level: Int) {
var index = -1
for (i in linkedList.indices.reversed()) {
val nodeLevel = linkedList[i].second
// 只有高优先级才插入到前面,等于的情况下是插到后面
if (level > nodeLevel) {
index = i
}
}
if (index >= 0) {
linkedList.add(index, Pair(ttsEntity, level))
} else {
linkedList.addLast(Pair(ttsEntity, level))
}
for (ttsPair in linkedList) {
d(TAG, "tts文本为" + ttsPair.first + ",level为" + ttsPair.second)
}
d(TAG, "===================")
}
private fun speakMultiLangTTSWithLevel(ttsEntity: MultiLangTtsEntity, ttsLevel: Int) {
ttsEngine?.let {
if (ttsLevel == curTtsLevel) {
// 对应p3、p2级别的排队
if (ttsLevel == 0 || ttsLevel == 1) {
d(TAG, "===================")
d(TAG, "插入消息:$ttsEntity,level为$ttsLevel")
insertTts(ttsEntity, ttsLevel)
return
} else {
// 打断并合成新的
stopTts()
d(TAG, "非Level1同级别打断!")
}
} else {
// 将要TTS的比现在正在TTS的优先级高
if (ttsLevel > curTtsLevel) {
if (curTtsLevel >= 0) {
// 打断并合成高优先级的
stopTts()
}
d(TAG, "高优先级打断低级别的!")
} else {
if (ttsLevel == 0 || ttsLevel == 1) {
d(TAG, "===================")
d(TAG, "插入消息:$ttsEntity,level为$ttsLevel")
insertTts(ttsEntity, ttsLevel)
}
return
}
}
curTtsLevel = ttsLevel
curTtsEntity = ttsEntity
// 合成并播放
d(TAG, "tts准备合成$ttsEntity,curTtsLevel为$curTtsLevel")
startSpeak(ttsEntity.ttsNext())
}
}
private fun startSpeak(langTtsEntity: LangTtsEntity?) {
langTtsEntity?.let {
if (it.language != lastLang) {
updateVoicer(it.language)
updateTtsParam()
lastLang = it.language
}
ttsEngine?.startSpeaking(it.ttsContent, ttsListener)
}
}
private fun handleCompleteEvent() {
curTtsLevel = -1
if (curTtsEntity != null) {// 多语言
val langTtsEntity = curTtsEntity!!.ttsNext()
if (langTtsEntity != null) {
speakVoiceMap[curTtsEntity.toString()]?.onSpeakEnd(curTtsEntity.toString())
ttsNextLanguage(langTtsEntity)
} else {
speakVoiceMap.remove(curTtsEntity.toString())?.onSpeakEnd(curTtsEntity.toString())
curTtsEntity = null
ttsNextMultiLangEntity()
}
} else {// 单语言
speakVoiceMap.remove(curTtsContent)?.onSpeakEnd(curTtsContent)
curTtsContent = ""
ttsNextMultiLangEntity()
}
}
private fun handleErrorEvent(error: SpeechError) {
curTtsLevel = -1
if (curTtsEntity != null) {
speakVoiceMap.remove(curTtsEntity.toString())
?.onSpeakError(curTtsEntity.toString(), error.message)
} else {
speakVoiceMap.remove(curTtsContent)?.onSpeakError(curTtsContent, error.message)
}
curTtsEntity = null
curTtsContent = ""
}
private val ttsListener = object : SynthesizerListener {
override fun onSpeakBegin() {
}
override fun onBufferProgress(p0: Int, p1: Int, p2: Int, p3: String?) {
}
override fun onSpeakPaused() {
}
override fun onSpeakResumed() {
}
override fun onSpeakProgress(p0: Int, p1: Int, p2: Int) {
}
override fun onEvent(p0: Int, p1: Int, p2: Int, p3: Bundle?) {
}
/**
* error - 错误信息若为null则没有出现错误。
* 在音频播放完成,或会话出现错误时,将回调此函数。
* 若应用主动调用SpeechSynthesizer.stopSpeaking()停止会话,则不会回调此函数。
*/
override fun onCompleted(error: SpeechError?) {
if (Thread.currentThread() == Looper.getMainLooper().thread) {
if (error == null) {// 播放完成
handleCompleteEvent()
} else {// 播放出错
handleErrorEvent(error)
}
} else {
UiThreadHandler.post {
if (error == null) {
handleCompleteEvent()
} else {
handleErrorEvent(error)
}
}
}
}
}
/**
* 语音合成下一个MultiLangTtsEntity
*/
private fun ttsNextMultiLangEntity() {
if (!linkedList.isEmpty()) {
val ttsPair = linkedList.removeFirst()
i(TAG, "排队播放的下一条文本为:" + ttsPair.first + ",级别为:" + ttsPair.second)
curTtsLevel = ttsPair.second
startSpeak(ttsPair.first.ttsNext())
} else {
i(TAG, "队列为空")
}
}
/**
* 语音合成下一个MultiLangTtsEntity中的语言
*/
private fun ttsNextLanguage(langTtsEntity: LangTtsEntity) {
startSpeak(langTtsEntity)
}
private fun updateVoicer(language: LanguageType) {
voicerEntries?.let {
voicer = when (language) {
LanguageType.ENGLISH -> it[5]
LanguageType.KOREAN -> it[6]
else -> it[0]
}
}
}
private fun updateTtsParam() {
// 清空参数
ttsEngine?.setParameter(SpeechConstant.PARAMS, null)
// 根据合成引擎设置相应参数
if (engineType == SpeechConstant.TYPE_CLOUD) {
ttsEngine?.setParameter(SpeechConstant.ENGINE_TYPE, SpeechConstant.TYPE_CLOUD)
// 支持实时音频返回,仅在 synthesizeToUri 条件下支持
ttsEngine?.setParameter(SpeechConstant.TTS_DATA_NOTIFY, "1")
// ttsEngine?.setParameter(SpeechConstant.TTS_BUFFER_TIME,"1");
// 设置在线合成发音人
ttsEngine?.setParameter(SpeechConstant.VOICE_NAME, voicer)
//设置合成语速
ttsEngine?.setParameter(
SpeechConstant.SPEED, "50"
)
//设置合成音调
ttsEngine?.setParameter(
SpeechConstant.PITCH, "50"
)
//设置合成音量
ttsEngine?.setParameter(
SpeechConstant.VOLUME, "50"
)
} else {
ttsEngine?.setParameter(SpeechConstant.ENGINE_TYPE, SpeechConstant.TYPE_LOCAL)
ttsEngine?.setParameter(SpeechConstant.VOICE_NAME, "")
}
//设置播放器音频流类型
ttsEngine?.setParameter(
SpeechConstant.STREAM_TYPE, "3"
)
// 设置播放合成音频打断音乐播放默认为true
ttsEngine?.setParameter(SpeechConstant.KEY_REQUEST_FOCUS, "false")
// 设置音频保存路径保存音频格式支持pcm、wav设置路径为sd卡请注意WRITE_EXTERNAL_STORAGE权限
ttsEngine?.setParameter(SpeechConstant.AUDIO_FORMAT, "pcm")
ttsEngine?.setParameter(
SpeechConstant.TTS_AUDIO_PATH,
context?.getExternalFilesDir("msc")?.absolutePath + "/tts.pcm"
)
}
}

Binary file not shown.

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="voicer_cloud_values">
<!-- 汉语发音人 -->
<item>xiaoyan</item>
<item>aisjiuxu</item>
<item>aisxping</item>
<item>aisjinger</item>
<item>aisbabyxu</item>
<!-- 英语发音人 -->
<item>x2_enus_catherine</item>
<!-- 韩语发音人 -->
<item>zhimin</item>
</string-array>
</resources>

View File

@@ -29,6 +29,7 @@ import com.mogo.eagle.core.utilcode.util.ThreadUtils;
import com.mogo.eagle.core.utilcode.util.ToastUtils;
import com.mogo.tts.base.IMogoTTS;
import com.mogo.tts.base.IMogoTTSCallback;
import com.mogo.tts.base.MultiLangTtsEntity;
import com.mogo.tts.base.PreemptType;
import com.zhidao.auto.platform.voice.VoiceClient;
import com.zhidao.voicesdk.MogoVoiceManager;
@@ -505,6 +506,11 @@ public class PadTTS implements IMogoTTS, VoiceClient.VoiceCmdCallBack, OnTtsList
}
}
@Override
public void speakMultiLangTTSWithLevel(MultiLangTtsEntity ttsEntity, int level, IMogoTTSCallback callback) {
}
/**
* 问答类型语音注册:默认确认和取消
*