diff --git a/test/src/main/AndroidManifest.xml b/test/src/main/AndroidManifest.xml index 62f5af7..ec2408a 100644 --- a/test/src/main/AndroidManifest.xml +++ b/test/src/main/AndroidManifest.xml @@ -28,7 +28,8 @@ + android:exported="false" + android:theme="@style/Theme.DUIX.Call"> diff --git a/test/src/main/java/ai/guiji/duix/test/model/AvatarModel.kt b/test/src/main/java/ai/guiji/duix/test/model/AvatarModel.kt new file mode 100644 index 0000000..700daa6 --- /dev/null +++ b/test/src/main/java/ai/guiji/duix/test/model/AvatarModel.kt @@ -0,0 +1,15 @@ +package ai.guiji.duix.test.model + +/** + * 数字人形象模型 + * @param name 中文显示名 + * @param voice 语音音色名(传给 Omni Realtime) + * @param modelUrl 模型资源下载地址 + * @param avatarRes 头像资源 ID(mipmap) + */ +data class AvatarModel( + val name: String, + val voice: String, + val modelUrl: String, + val avatarRes: Int +) diff --git a/test/src/main/java/ai/guiji/duix/test/realtime/OmniRealtimeClient.java b/test/src/main/java/ai/guiji/duix/test/realtime/OmniRealtimeClient.java index c09f74b..adeaa73 100644 --- a/test/src/main/java/ai/guiji/duix/test/realtime/OmniRealtimeClient.java +++ b/test/src/main/java/ai/guiji/duix/test/realtime/OmniRealtimeClient.java @@ -30,10 +30,12 @@ public class OmniRealtimeClient { private final OkHttpClient okHttpClient; private WebSocket webSocket; private final Callback callback; + private final String voice; private final AtomicBoolean isConnected = new AtomicBoolean(false); private final AtomicBoolean userSpeaking = new AtomicBoolean(false); - public OmniRealtimeClient(Callback callback) { + public OmniRealtimeClient(String voice, Callback callback) { + this.voice = voice; this.callback = callback; this.okHttpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) @@ -98,7 +100,7 @@ public class OmniRealtimeClient { try { JSONObject session = new JSONObject(); session.put("modalities", new org.json.JSONArray().put("text").put("audio")); - session.put("voice", "Ethan"); + session.put("voice", voice); session.put("input_audio_format", "pcm"); session.put("output_audio_format", "pcm"); session.put("instructions", "你是一名面向中国老年群体的心理陪伴 AI。你的职责是为老人提供温和、耐心、尊重、稳定的情感陪伴、情绪支持、回忆唤起、生活关怀和风险识别服务。你不是医生,也不是心理治疗师,不能替代专业诊疗,但应在必要时建议联系家属、社区、医生或急救资源。\n" + diff --git a/test/src/main/java/ai/guiji/duix/test/ui/activity/CallActivity.kt b/test/src/main/java/ai/guiji/duix/test/ui/activity/CallActivity.kt index 508ebfa..f55d6b0 100644 --- a/test/src/main/java/ai/guiji/duix/test/ui/activity/CallActivity.kt +++ b/test/src/main/java/ai/guiji/duix/test/ui/activity/CallActivity.kt @@ -25,7 +25,6 @@ import android.util.Log import android.view.View import android.widget.CompoundButton import android.widget.Toast -import androidx.core.content.ContextCompat import com.bumptech.glide.Glide import java.io.File import java.io.FileInputStream @@ -39,6 +38,7 @@ class CallActivity : BaseActivity() { } private var modelUrl = "" + private var voice = "Ethan" private var debug = false private var mMessage = "" @@ -82,6 +82,7 @@ class CallActivity : BaseActivity() { setContentView(binding.root) modelUrl = intent.getStringExtra("modelUrl") ?: "" + voice = intent.getStringExtra("voice") ?: "Ethan" debug = intent.getBooleanExtra("debug", false) Glide.with(mContext).load("file:///android_asset/bg/bg1.png").into(binding.ivBg) @@ -311,14 +312,15 @@ class CallActivity : BaseActivity() { applyMessage("AI对话: 连接中...") binding.tvSubtitle.visibility = View.VISIBLE binding.tvSubtitle.text = "正在连接..." - binding.btnAIConversation.text = getString(R.string.ai_fab_stop) + binding.btnAIConversation.setImageResource(R.drawable.ic_mic_off) + binding.btnAIConversation.setBackgroundResource(R.drawable.shape_fab_ai_active) - omniClient = OmniRealtimeClient(object : OmniRealtimeClient.Callback { + omniClient = OmniRealtimeClient(voice, object : OmniRealtimeClient.Callback { override fun onConnected() { mainHandler.post { binding.tvSubtitle.text = "已连接,请对着麦克风说话" binding.btnCameraToggle.visibility = View.VISIBLE - binding.btnCameraToggle.background = ContextCompat.getDrawable(mContext, R.drawable.shape_circle_camera_off) + binding.btnCameraToggle.setBackgroundResource(R.drawable.shape_fab_camera_off) binding.btnCameraToggle.contentDescription = getString(R.string.camera_off) isCameraEnabled = false applyMessage("AI对话: 已连接") @@ -425,7 +427,8 @@ class CallActivity : BaseActivity() { mainHandler.post { if (!isFinishing) { - binding.btnAIConversation.text = getString(R.string.ai_fab_start) + binding.btnAIConversation.setImageResource(R.drawable.ic_mic) + binding.btnAIConversation.setBackgroundResource(R.drawable.shape_fab_ai) binding.btnCameraToggle.visibility = View.GONE binding.tvSubtitle.visibility = View.GONE binding.tvSubtitle.text = "" @@ -451,11 +454,11 @@ class CallActivity : BaseActivity() { } }, binding.cameraPreview) cameraFrameCapture?.start() - binding.btnCameraToggle.background = ContextCompat.getDrawable(mContext, R.drawable.shape_circle_camera_on) + binding.btnCameraToggle.setBackgroundResource(R.drawable.shape_fab_camera_on) binding.btnCameraToggle.contentDescription = getString(R.string.camera_on) } else { stopCamera() - binding.btnCameraToggle.background = ContextCompat.getDrawable(mContext, R.drawable.shape_circle_camera_off) + binding.btnCameraToggle.setBackgroundResource(R.drawable.shape_fab_camera_off) binding.btnCameraToggle.contentDescription = getString(R.string.camera_off) } } diff --git a/test/src/main/java/ai/guiji/duix/test/ui/activity/MainActivity.kt b/test/src/main/java/ai/guiji/duix/test/ui/activity/MainActivity.kt index 15b5ef7..c868f76 100644 --- a/test/src/main/java/ai/guiji/duix/test/ui/activity/MainActivity.kt +++ b/test/src/main/java/ai/guiji/duix/test/ui/activity/MainActivity.kt @@ -1,113 +1,96 @@ package ai.guiji.duix.test.ui.activity -import ai.guiji.duix.sdk.client.BuildConfig import ai.guiji.duix.sdk.client.VirtualModelUtil import ai.guiji.duix.test.R import ai.guiji.duix.test.databinding.ActivityMainBinding +import ai.guiji.duix.test.model.AvatarModel +import ai.guiji.duix.test.ui.adapter.AvatarAdapter import ai.guiji.duix.test.ui.dialog.LoadingDialog -import ai.guiji.duix.test.ui.dialog.ModelSelectorDialog -import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle -import android.text.TextUtils import android.widget.Toast +import androidx.recyclerview.widget.GridLayoutManager import java.io.File class MainActivity : BaseActivity() { private lateinit var binding: ActivityMainBinding - private var mLoadingDialog: LoadingDialog?=null + private var mLoadingDialog: LoadingDialog? = null private var mLastProgress = 0 - val models = arrayListOf( - "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/bendi3_20240518.zip", - "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/airuike_20240409.zip", - "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/675429759852613_7f8d9388a4213080b1820b83dd057cfb_optim_m80.zip", - "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674402003804229_f6e86fb375c4f1f1b82b24f7ee4e7cb4_optim_m80.zip", - "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674400178376773_3925e756433c5a9caa9b9d54147ae4ab_optim_m80.zip", - "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674397294927941_6e297e18a4bdbe35c07a6ae48a1f021f_optim_m80.zip", - "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674393494597701_f49fcf68f5afdb241d516db8a7d88a7b_optim_m80.zip", - "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/651705983152197_ccf3256b2449c76e77f94276dffcb293_optim_m80.zip", - "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/627306542239813_1871244b5e6912efc636ba31ea4c5c6d_optim_m80.zip", + private val baseConfigUrl = "https://dl.xn--876a.net/models/gj_dh_res.zip" + + private val avatarModels = listOf( + AvatarModel("晨煦", "Ethan", "https://dl.xn--876a.net/models/bendi3_20240518.zip", R.mipmap.ethan), + AvatarModel("詹妮弗", "Jennifer", "https://dl.xn--876a.net/models/Emma.zip", R.mipmap.jennifer), + AvatarModel("月白", "Moon", "https://dl.xn--876a.net/models/Kai.zip", R.mipmap.moon), + AvatarModel("艾登", "Aiden", "https://dl.xn--876a.net/models/Leo.zip", R.mipmap.leo), + AvatarModel("四月", "Maia", "https://dl.xn--876a.net/models/Lily.zip", R.mipmap.lily), + AvatarModel("奥利弗", "Andre", "https://dl.xn--876a.net/models/Oliver.zip", R.mipmap.oliver), + AvatarModel("索菲亚", "Sonrisa", "https://dl.xn--876a.net/models/Sofia.zip", R.mipmap.sofia), ) - private var mBaseConfigUrl = "" - private var mModelUrl = "" + private var mSelectedModel: AvatarModel? = null - @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - binding.tvSdkVersion.text = "SDK Version: ${BuildConfig.VERSION_NAME}" - - - binding.btnMoreModel.setOnClickListener { - val modelSelectorDialog = ModelSelectorDialog(mContext, models, object : ModelSelectorDialog.Listener{ - override fun onSelect(url: String) { - binding.etUrl.setText(url) - } - }) - modelSelectorDialog.show() - } - binding.btnPlay.setOnClickListener { - play() - } + binding.rvAvatars.layoutManager = GridLayoutManager(this, 2) + binding.rvAvatars.adapter = AvatarAdapter(avatarModels, object : AvatarAdapter.Callback { + override fun onSelect(model: AvatarModel) { + mSelectedModel = model + play(model) + } + }) } - private fun play(){ - mBaseConfigUrl = binding.etBaseConfig.text.toString() - mModelUrl = binding.etUrl.text.toString() - if (TextUtils.isEmpty(mBaseConfigUrl)){ - Toast.makeText(mContext, R.string.base_config_cannot_be_empty, Toast.LENGTH_SHORT).show() - return - } - if (TextUtils.isEmpty(mModelUrl)){ - Toast.makeText(mContext, R.string.model_url_cannot_be_empty, Toast.LENGTH_SHORT).show() - return - } + private fun play(model: AvatarModel) { + mSelectedModel = model checkBaseConfig() } - private fun checkBaseConfig(){ - if (VirtualModelUtil.checkBaseConfig(mContext)){ + private fun checkBaseConfig() { + if (VirtualModelUtil.checkBaseConfig(mContext)) { checkModel() } else { baseConfigDownload() } } - private fun checkModel(){ - if (VirtualModelUtil.checkModel(mContext, mModelUrl)){ + private fun checkModel() { + val model = mSelectedModel ?: return + if (VirtualModelUtil.checkModel(mContext, model.modelUrl)) { jumpPlayPage() } else { modelDownload() } } - private fun jumpPlayPage(){ + private fun jumpPlayPage() { + val model = mSelectedModel ?: return val intent = Intent(mContext, CallActivity::class.java) - intent.putExtra("modelUrl", mModelUrl) - val debug = binding.switchDebug.isChecked - intent.putExtra("debug", debug) + intent.putExtra("modelUrl", model.modelUrl) + intent.putExtra("voice", model.voice) + intent.putExtra("debug", false) startActivity(intent) } - private fun baseConfigDownload(){ + private fun baseConfigDownload() { mLoadingDialog?.dismiss() - mLoadingDialog = LoadingDialog(mContext, "Start downloading") + mLoadingDialog = LoadingDialog(mContext, "正在准备资源...") mLoadingDialog?.show() - VirtualModelUtil.baseConfigDownload(mContext, mBaseConfigUrl, object : + VirtualModelUtil.baseConfigDownload(mContext, baseConfigUrl, object : VirtualModelUtil.ModelDownloadCallback { override fun onDownloadProgress(url: String?, current: Long, total: Long) { val progress = (current * 100 / total).toInt() - if (progress != mLastProgress){ + if (progress != mLastProgress) { mLastProgress = progress runOnUiThread { - if (mLoadingDialog?.isShowing == true){ - mLoadingDialog?.setContent("Config download(${progress}%)") + if (mLoadingDialog?.isShowing == true) { + mLoadingDialog?.setContent("基础配置下载中(${progress}%)") } } } @@ -115,11 +98,11 @@ class MainActivity : BaseActivity() { override fun onUnzipProgress(url: String?, current: Long, total: Long) { val progress = (current * 100 / total).toInt() - if (progress != mLastProgress){ + if (progress != mLastProgress) { mLastProgress = progress runOnUiThread { - if (mLoadingDialog?.isShowing == true){ - mLoadingDialog?.setContent("Config unzip(${progress}%)") + if (mLoadingDialog?.isShowing == true) { + mLoadingDialog?.setContent("基础配置解压中(${progress}%)") } } } @@ -135,45 +118,37 @@ class MainActivity : BaseActivity() { override fun onDownloadFail(url: String?, code: Int, msg: String?) { runOnUiThread { mLoadingDialog?.dismiss() - Toast.makeText(mContext, "BaseConfig download error: $msg", Toast.LENGTH_SHORT).show() + Toast.makeText(mContext, "资源下载失败: $msg", Toast.LENGTH_SHORT).show() } } - }) } - private fun modelDownload(){ + private fun modelDownload() { + val model = mSelectedModel ?: return mLoadingDialog?.dismiss() - mLoadingDialog = LoadingDialog(mContext, "Start downloading") + mLoadingDialog = LoadingDialog(mContext, "正在下载 ${model.name} ...") mLoadingDialog?.show() - VirtualModelUtil.modelDownload(mContext, mModelUrl, object : VirtualModelUtil.ModelDownloadCallback{ - override fun onDownloadProgress( - url: String?, - current: Long, - total: Long, - ) { + VirtualModelUtil.modelDownload(mContext, model.modelUrl, object : VirtualModelUtil.ModelDownloadCallback { + override fun onDownloadProgress(url: String?, current: Long, total: Long) { val progress = (current * 100 / total).toInt() - if (progress != mLastProgress){ + if (progress != mLastProgress) { mLastProgress = progress runOnUiThread { - if (mLoadingDialog?.isShowing == true){ - mLoadingDialog?.setContent("Model download(${progress}%)") + if (mLoadingDialog?.isShowing == true) { + mLoadingDialog?.setContent("${model.name} 下载中(${progress}%)") } } } } - override fun onUnzipProgress( - url: String?, - current: Long, - total: Long, - ) { + override fun onUnzipProgress(url: String?, current: Long, total: Long) { val progress = (current * 100 / total).toInt() - if (progress != mLastProgress){ + if (progress != mLastProgress) { mLastProgress = progress runOnUiThread { - if (mLoadingDialog?.isShowing == true){ - mLoadingDialog?.setContent("Model unzip(${progress}%)") + if (mLoadingDialog?.isShowing == true) { + mLoadingDialog?.setContent("${model.name} 解压中(${progress}%)") } } } @@ -186,18 +161,12 @@ class MainActivity : BaseActivity() { } } - override fun onDownloadFail( - url: String?, - code: Int, - msg: String?, - ) { + override fun onDownloadFail(url: String?, code: Int, msg: String?) { runOnUiThread { mLoadingDialog?.dismiss() - Toast.makeText(mContext, "Model download error: $msg", Toast.LENGTH_SHORT).show() + Toast.makeText(mContext, "下载失败: $msg", Toast.LENGTH_SHORT).show() } } - }) } - } diff --git a/test/src/main/java/ai/guiji/duix/test/ui/adapter/AvatarAdapter.kt b/test/src/main/java/ai/guiji/duix/test/ui/adapter/AvatarAdapter.kt new file mode 100644 index 0000000..011aecc --- /dev/null +++ b/test/src/main/java/ai/guiji/duix/test/ui/adapter/AvatarAdapter.kt @@ -0,0 +1,38 @@ +package ai.guiji.duix.test.ui.adapter + +import ai.guiji.duix.test.databinding.ItemAvatarCardBinding +import ai.guiji.duix.test.model.AvatarModel +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +class AvatarAdapter( + private val list: List, + private val callback: Callback +) : RecyclerView.Adapter() { + + class ViewHolder(val binding: ItemAvatarCardBinding) : + RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemAvatarCardBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val model = list[position] + holder.binding.ivAvatar.setImageResource(model.avatarRes) + holder.binding.tvName.text = model.name + holder.binding.cardAvatar.setOnClickListener { + callback.onSelect(model) + } + } + + override fun getItemCount(): Int = list.size + + interface Callback { + fun onSelect(model: AvatarModel) + } +} diff --git a/test/src/main/res/drawable/ic_camera.xml b/test/src/main/res/drawable/ic_camera.xml new file mode 100644 index 0000000..a6f4f9c --- /dev/null +++ b/test/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,10 @@ + + + diff --git a/test/src/main/res/drawable/ic_mic.xml b/test/src/main/res/drawable/ic_mic.xml new file mode 100644 index 0000000..d963e13 --- /dev/null +++ b/test/src/main/res/drawable/ic_mic.xml @@ -0,0 +1,10 @@ + + + diff --git a/test/src/main/res/drawable/ic_mic_off.xml b/test/src/main/res/drawable/ic_mic_off.xml new file mode 100644 index 0000000..1aa5ae6 --- /dev/null +++ b/test/src/main/res/drawable/ic_mic_off.xml @@ -0,0 +1,14 @@ + + + + diff --git a/test/src/main/res/drawable/shape_fab_ai.xml b/test/src/main/res/drawable/shape_fab_ai.xml new file mode 100644 index 0000000..942b2f3 --- /dev/null +++ b/test/src/main/res/drawable/shape_fab_ai.xml @@ -0,0 +1,5 @@ + + + + diff --git a/test/src/main/res/drawable/shape_fab_ai_active.xml b/test/src/main/res/drawable/shape_fab_ai_active.xml new file mode 100644 index 0000000..1078fe0 --- /dev/null +++ b/test/src/main/res/drawable/shape_fab_ai_active.xml @@ -0,0 +1,5 @@ + + + + diff --git a/test/src/main/res/drawable/shape_fab_camera_off.xml b/test/src/main/res/drawable/shape_fab_camera_off.xml new file mode 100644 index 0000000..10c91c1 --- /dev/null +++ b/test/src/main/res/drawable/shape_fab_camera_off.xml @@ -0,0 +1,5 @@ + + + + diff --git a/test/src/main/res/drawable/shape_fab_camera_on.xml b/test/src/main/res/drawable/shape_fab_camera_on.xml new file mode 100644 index 0000000..cf47529 --- /dev/null +++ b/test/src/main/res/drawable/shape_fab_camera_on.xml @@ -0,0 +1,5 @@ + + + + diff --git a/test/src/main/res/layout/activity_call.xml b/test/src/main/res/layout/activity_call.xml index 2219ea2..75b721c 100644 --- a/test/src/main/res/layout/activity_call.xml +++ b/test/src/main/res/layout/activity_call.xml @@ -2,7 +2,6 @@ -