优化UI,加入模型/音色选择

This commit is contained in:
feie9456 2026-03-19 07:47:20 +08:00
parent d09d6f1cc0
commit a44ddb6dff
26 changed files with 298 additions and 222 deletions

View File

@ -28,7 +28,8 @@
<activity <activity
android:name=".ui.activity.CallActivity" android:name=".ui.activity.CallActivity"
android:exported="false"> android:exported="false"
android:theme="@style/Theme.DUIX.Call">
</activity> </activity>
</application> </application>

View File

@ -0,0 +1,15 @@
package ai.guiji.duix.test.model
/**
* 数字人形象模型
* @param name 中文显示名
* @param voice 语音音色名传给 Omni Realtime
* @param modelUrl 模型资源下载地址
* @param avatarRes 头像资源 IDmipmap
*/
data class AvatarModel(
val name: String,
val voice: String,
val modelUrl: String,
val avatarRes: Int
)

View File

@ -30,10 +30,12 @@ public class OmniRealtimeClient {
private final OkHttpClient okHttpClient; private final OkHttpClient okHttpClient;
private WebSocket webSocket; private WebSocket webSocket;
private final Callback callback; private final Callback callback;
private final String voice;
private final AtomicBoolean isConnected = new AtomicBoolean(false); private final AtomicBoolean isConnected = new AtomicBoolean(false);
private final AtomicBoolean userSpeaking = 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.callback = callback;
this.okHttpClient = new OkHttpClient.Builder() this.okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
@ -98,7 +100,7 @@ public class OmniRealtimeClient {
try { try {
JSONObject session = new JSONObject(); JSONObject session = new JSONObject();
session.put("modalities", new org.json.JSONArray().put("text").put("audio")); 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("input_audio_format", "pcm");
session.put("output_audio_format", "pcm"); session.put("output_audio_format", "pcm");
session.put("instructions", "你是一名面向中国老年群体的心理陪伴 AI。你的职责是为老人提供温和、耐心、尊重、稳定的情感陪伴、情绪支持、回忆唤起、生活关怀和风险识别服务。你不是医生也不是心理治疗师不能替代专业诊疗但应在必要时建议联系家属、社区、医生或急救资源。\n" + session.put("instructions", "你是一名面向中国老年群体的心理陪伴 AI。你的职责是为老人提供温和、耐心、尊重、稳定的情感陪伴、情绪支持、回忆唤起、生活关怀和风险识别服务。你不是医生也不是心理治疗师不能替代专业诊疗但应在必要时建议联系家属、社区、医生或急救资源。\n" +

View File

@ -25,7 +25,6 @@ import android.util.Log
import android.view.View import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -39,6 +38,7 @@ class CallActivity : BaseActivity() {
} }
private var modelUrl = "" private var modelUrl = ""
private var voice = "Ethan"
private var debug = false private var debug = false
private var mMessage = "" private var mMessage = ""
@ -82,6 +82,7 @@ class CallActivity : BaseActivity() {
setContentView(binding.root) setContentView(binding.root)
modelUrl = intent.getStringExtra("modelUrl") ?: "" modelUrl = intent.getStringExtra("modelUrl") ?: ""
voice = intent.getStringExtra("voice") ?: "Ethan"
debug = intent.getBooleanExtra("debug", false) debug = intent.getBooleanExtra("debug", false)
Glide.with(mContext).load("file:///android_asset/bg/bg1.png").into(binding.ivBg) Glide.with(mContext).load("file:///android_asset/bg/bg1.png").into(binding.ivBg)
@ -311,14 +312,15 @@ class CallActivity : BaseActivity() {
applyMessage("AI对话: 连接中...") applyMessage("AI对话: 连接中...")
binding.tvSubtitle.visibility = View.VISIBLE binding.tvSubtitle.visibility = View.VISIBLE
binding.tvSubtitle.text = "正在连接..." 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() { override fun onConnected() {
mainHandler.post { mainHandler.post {
binding.tvSubtitle.text = "已连接,请对着麦克风说话" binding.tvSubtitle.text = "已连接,请对着麦克风说话"
binding.btnCameraToggle.visibility = View.VISIBLE 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) binding.btnCameraToggle.contentDescription = getString(R.string.camera_off)
isCameraEnabled = false isCameraEnabled = false
applyMessage("AI对话: 已连接") applyMessage("AI对话: 已连接")
@ -425,7 +427,8 @@ class CallActivity : BaseActivity() {
mainHandler.post { mainHandler.post {
if (!isFinishing) { 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.btnCameraToggle.visibility = View.GONE
binding.tvSubtitle.visibility = View.GONE binding.tvSubtitle.visibility = View.GONE
binding.tvSubtitle.text = "" binding.tvSubtitle.text = ""
@ -451,11 +454,11 @@ class CallActivity : BaseActivity() {
} }
}, binding.cameraPreview) }, binding.cameraPreview)
cameraFrameCapture?.start() 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) binding.btnCameraToggle.contentDescription = getString(R.string.camera_on)
} else { } else {
stopCamera() 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) binding.btnCameraToggle.contentDescription = getString(R.string.camera_off)
} }
} }

View File

@ -1,113 +1,96 @@
package ai.guiji.duix.test.ui.activity 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.sdk.client.VirtualModelUtil
import ai.guiji.duix.test.R import ai.guiji.duix.test.R
import ai.guiji.duix.test.databinding.ActivityMainBinding 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.LoadingDialog
import ai.guiji.duix.test.ui.dialog.ModelSelectorDialog
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import java.io.File import java.io.File
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private var mLoadingDialog: LoadingDialog?=null private var mLoadingDialog: LoadingDialog? = null
private var mLastProgress = 0 private var mLastProgress = 0
val models = arrayListOf( private val baseConfigUrl = "https://dl.xn--876a.net/models/gj_dh_res.zip"
"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", private val avatarModels = listOf(
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/675429759852613_7f8d9388a4213080b1820b83dd057cfb_optim_m80.zip", AvatarModel("晨煦", "Ethan", "https://dl.xn--876a.net/models/bendi3_20240518.zip", R.mipmap.ethan),
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674402003804229_f6e86fb375c4f1f1b82b24f7ee4e7cb4_optim_m80.zip", AvatarModel("詹妮弗", "Jennifer", "https://dl.xn--876a.net/models/Emma.zip", R.mipmap.jennifer),
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674400178376773_3925e756433c5a9caa9b9d54147ae4ab_optim_m80.zip", AvatarModel("月白", "Moon", "https://dl.xn--876a.net/models/Kai.zip", R.mipmap.moon),
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674397294927941_6e297e18a4bdbe35c07a6ae48a1f021f_optim_m80.zip", AvatarModel("艾登", "Aiden", "https://dl.xn--876a.net/models/Leo.zip", R.mipmap.leo),
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674393494597701_f49fcf68f5afdb241d516db8a7d88a7b_optim_m80.zip", AvatarModel("四月", "Maia", "https://dl.xn--876a.net/models/Lily.zip", R.mipmap.lily),
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/651705983152197_ccf3256b2449c76e77f94276dffcb293_optim_m80.zip", AvatarModel("奥利弗", "Andre", "https://dl.xn--876a.net/models/Oliver.zip", R.mipmap.oliver),
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/627306542239813_1871244b5e6912efc636ba31ea4c5c6d_optim_m80.zip", AvatarModel("索菲亚", "Sonrisa", "https://dl.xn--876a.net/models/Sofia.zip", R.mipmap.sofia),
) )
private var mBaseConfigUrl = "" private var mSelectedModel: AvatarModel? = null
private var mModelUrl = ""
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.tvSdkVersion.text = "SDK Version: ${BuildConfig.VERSION_NAME}" binding.rvAvatars.layoutManager = GridLayoutManager(this, 2)
binding.rvAvatars.adapter = AvatarAdapter(avatarModels, object : AvatarAdapter.Callback {
override fun onSelect(model: AvatarModel) {
binding.btnMoreModel.setOnClickListener { mSelectedModel = model
val modelSelectorDialog = ModelSelectorDialog(mContext, models, object : ModelSelectorDialog.Listener{ play(model)
override fun onSelect(url: String) { }
binding.etUrl.setText(url) })
}
})
modelSelectorDialog.show()
}
binding.btnPlay.setOnClickListener {
play()
}
} }
private fun play(){ private fun play(model: AvatarModel) {
mBaseConfigUrl = binding.etBaseConfig.text.toString() mSelectedModel = model
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
}
checkBaseConfig() checkBaseConfig()
} }
private fun checkBaseConfig(){ private fun checkBaseConfig() {
if (VirtualModelUtil.checkBaseConfig(mContext)){ if (VirtualModelUtil.checkBaseConfig(mContext)) {
checkModel() checkModel()
} else { } else {
baseConfigDownload() baseConfigDownload()
} }
} }
private fun checkModel(){ private fun checkModel() {
if (VirtualModelUtil.checkModel(mContext, mModelUrl)){ val model = mSelectedModel ?: return
if (VirtualModelUtil.checkModel(mContext, model.modelUrl)) {
jumpPlayPage() jumpPlayPage()
} else { } else {
modelDownload() modelDownload()
} }
} }
private fun jumpPlayPage(){ private fun jumpPlayPage() {
val model = mSelectedModel ?: return
val intent = Intent(mContext, CallActivity::class.java) val intent = Intent(mContext, CallActivity::class.java)
intent.putExtra("modelUrl", mModelUrl) intent.putExtra("modelUrl", model.modelUrl)
val debug = binding.switchDebug.isChecked intent.putExtra("voice", model.voice)
intent.putExtra("debug", debug) intent.putExtra("debug", false)
startActivity(intent) startActivity(intent)
} }
private fun baseConfigDownload(){ private fun baseConfigDownload() {
mLoadingDialog?.dismiss() mLoadingDialog?.dismiss()
mLoadingDialog = LoadingDialog(mContext, "Start downloading") mLoadingDialog = LoadingDialog(mContext, "正在准备资源...")
mLoadingDialog?.show() mLoadingDialog?.show()
VirtualModelUtil.baseConfigDownload(mContext, mBaseConfigUrl, object : VirtualModelUtil.baseConfigDownload(mContext, baseConfigUrl, object :
VirtualModelUtil.ModelDownloadCallback { VirtualModelUtil.ModelDownloadCallback {
override fun onDownloadProgress(url: String?, current: Long, total: Long) { override fun onDownloadProgress(url: String?, current: Long, total: Long) {
val progress = (current * 100 / total).toInt() val progress = (current * 100 / total).toInt()
if (progress != mLastProgress){ if (progress != mLastProgress) {
mLastProgress = progress mLastProgress = progress
runOnUiThread { runOnUiThread {
if (mLoadingDialog?.isShowing == true){ if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Config download(${progress}%)") mLoadingDialog?.setContent("基础配置下载中(${progress}%)")
} }
} }
} }
@ -115,11 +98,11 @@ class MainActivity : BaseActivity() {
override fun onUnzipProgress(url: String?, current: Long, total: Long) { override fun onUnzipProgress(url: String?, current: Long, total: Long) {
val progress = (current * 100 / total).toInt() val progress = (current * 100 / total).toInt()
if (progress != mLastProgress){ if (progress != mLastProgress) {
mLastProgress = progress mLastProgress = progress
runOnUiThread { runOnUiThread {
if (mLoadingDialog?.isShowing == true){ if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Config unzip(${progress}%)") mLoadingDialog?.setContent("基础配置解压中(${progress}%)")
} }
} }
} }
@ -135,45 +118,37 @@ class MainActivity : BaseActivity() {
override fun onDownloadFail(url: String?, code: Int, msg: String?) { override fun onDownloadFail(url: String?, code: Int, msg: String?) {
runOnUiThread { runOnUiThread {
mLoadingDialog?.dismiss() 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?.dismiss()
mLoadingDialog = LoadingDialog(mContext, "Start downloading") mLoadingDialog = LoadingDialog(mContext, "正在下载 ${model.name} ...")
mLoadingDialog?.show() mLoadingDialog?.show()
VirtualModelUtil.modelDownload(mContext, mModelUrl, object : VirtualModelUtil.ModelDownloadCallback{ VirtualModelUtil.modelDownload(mContext, model.modelUrl, object : VirtualModelUtil.ModelDownloadCallback {
override fun onDownloadProgress( override fun onDownloadProgress(url: String?, current: Long, total: Long) {
url: String?,
current: Long,
total: Long,
) {
val progress = (current * 100 / total).toInt() val progress = (current * 100 / total).toInt()
if (progress != mLastProgress){ if (progress != mLastProgress) {
mLastProgress = progress mLastProgress = progress
runOnUiThread { runOnUiThread {
if (mLoadingDialog?.isShowing == true){ if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Model download(${progress}%)") mLoadingDialog?.setContent("${model.name} 下载中(${progress}%)")
} }
} }
} }
} }
override fun onUnzipProgress( override fun onUnzipProgress(url: String?, current: Long, total: Long) {
url: String?,
current: Long,
total: Long,
) {
val progress = (current * 100 / total).toInt() val progress = (current * 100 / total).toInt()
if (progress != mLastProgress){ if (progress != mLastProgress) {
mLastProgress = progress mLastProgress = progress
runOnUiThread { runOnUiThread {
if (mLoadingDialog?.isShowing == true){ if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Model unzip(${progress}%)") mLoadingDialog?.setContent("${model.name} 解压中(${progress}%)")
} }
} }
} }
@ -186,18 +161,12 @@ class MainActivity : BaseActivity() {
} }
} }
override fun onDownloadFail( override fun onDownloadFail(url: String?, code: Int, msg: String?) {
url: String?,
code: Int,
msg: String?,
) {
runOnUiThread { runOnUiThread {
mLoadingDialog?.dismiss() mLoadingDialog?.dismiss()
Toast.makeText(mContext, "Model download error: $msg", Toast.LENGTH_SHORT).show() Toast.makeText(mContext, "下载失败: $msg", Toast.LENGTH_SHORT).show()
} }
} }
}) })
} }
} }

View File

@ -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<AvatarModel>,
private val callback: Callback
) : RecyclerView.Adapter<AvatarAdapter.ViewHolder>() {
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)
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M12,15.2C13.77,15.2 15.2,13.77 15.2,12C15.2,10.23 13.77,8.8 12,8.8C10.23,8.8 8.8,10.23 8.8,12C8.8,13.77 10.23,15.2 12,15.2ZM9,2L7.17,4H4C2.9,4 2,4.9 2,6V18C2,19.1 2.9,20 4,20H20C21.1,20 22,19.1 22,18V6C22,4.9 21.1,4 20,4H16.83L15,2H9ZM12,17C9.24,17 7,14.76 7,12C7,9.24 9.24,7 12,7C14.76,7 17,9.24 17,12C17,14.76 14.76,17 12,17Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
<path
android:fillColor="@android:color/white"
android:pathData="M3.27,3.27L2,4.54l7,7v0.46c0,1.66 1.34,3 3,3c0.23,0 0.44,-0.03 0.65,-0.08l1.66,1.66C13.56,16.5 12.8,16.87 12,17.1V21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9l3.73,3.73 1.27,-1.27L3.27,3.27z"
android:strokeWidth="0" />
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#E8926F" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#CC424242" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#99666666" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#CC2196F3" />
</shape>

View File

@ -2,7 +2,6 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ImageView <ImageView
@ -91,37 +90,34 @@
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
/> />
<Button <ImageButton
android:id="@+id/btnAIConversation" android:id="@+id/btnAIConversation"
android:text="@string/ai_fab_start" android:src="@drawable/ic_mic"
android:textSize="14sp" android:background="@drawable/shape_fab_ai"
android:textColor="@color/white"
android:background="@drawable/shape_circle_fab"
android:elevation="8dp" android:elevation="8dp"
android:layout_width="56dp" android:scaleType="center"
android:layout_height="56dp" android:layout_width="64dp"
android:layout_height="64dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="32dp" android:layout_marginBottom="40dp"
android:contentDescription="@string/ai_conversation" android:contentDescription="@string/ai_conversation"
/> />
<Button <ImageButton
android:id="@+id/btnCameraToggle" android:id="@+id/btnCameraToggle"
android:text="📷" android:src="@drawable/ic_camera"
android:textSize="18sp" android:background="@drawable/shape_fab_camera_off"
android:textColor="@color/white"
android:background="@drawable/shape_circle_camera_off"
android:elevation="8dp" android:elevation="8dp"
android:scaleType="center"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/btnAIConversation" app:layout_constraintBottom_toBottomOf="@+id/btnAIConversation"
app:layout_constraintTop_toTopOf="@+id/btnAIConversation"
app:layout_constraintStart_toEndOf="@+id/btnAIConversation" app:layout_constraintStart_toEndOf="@+id/btnAIConversation"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="24dp"
android:contentDescription="@string/camera_off" android:contentDescription="@string/camera_off"
/> />

View File

@ -3,119 +3,56 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="12dp"> android:fitsSystemWindows="true"
android:background="@color/warm_bg">
<!-- 顶部标题区域 -->
<TextView <TextView
android:id="@+id/tv_title" android:id="@+id/tv_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="120dp" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:text="@string/app_name" android:paddingTop="48dp"
android:textColor="@color/black" android:paddingBottom="4dp"
android:textSize="18dp" android:text="@string/app_title"
android:textStyle="italic|bold" android:textColor="@color/warm_text_primary"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/tv_sdk_version" android:id="@+id/tv_subtitle"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" android:gravity="center"
app:layout_constraintEnd_toEndOf="parent" android:paddingBottom="16dp"
android:text="@string/app_subtitle"
android:textColor="@color/warm_text_secondary"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@+id/tv_title" /> app:layout_constraintTop_toBottomOf="@+id/tv_title" />
<ScrollView <!-- 角色列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvAvatars"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@+id/tv_sdk_version"> android:paddingStart="10dp"
android:paddingEnd="10dp"
android:paddingBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/tvBottomHint"
app:layout_constraintTop_toBottomOf="@+id/tv_subtitle" />
<androidx.constraintlayout.widget.ConstraintLayout <TextView
android:layout_width="match_parent" android:id="@+id/tvBottomHint"
android:layout_height="wrap_content"> android:layout_width="match_parent"
android:layout_height="wrap_content"
<TextView android:gravity="center"
android:id="@+id/tvDownloadTips" android:paddingTop="8dp"
app:layout_constraintTop_toTopOf="parent" android:paddingBottom="16dp"
android:text="@string/main_download_tips" android:text="@string/choose_companion_hint"
android:textSize="13sp" android:textColor="@color/warm_text_hint"
android:textColor="@color/black" android:textSize="12sp"
android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" />
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tvBaseConfig"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/base_config_url"
android:textSize="13sp"
app:layout_constraintTop_toBottomOf="@+id/tvDownloadTips"
app:layout_constraintStart_toStartOf="parent"/>
<EditText
android:id="@+id/etBaseConfig"
android:layout_marginTop="12dp"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/gj_dh_res.zip"
android:textSize="13sp"
app:layout_constraintTop_toBottomOf="@+id/tvBaseConfig"
/>
<TextView
android:id="@+id/tvModelUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/model_url"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etBaseConfig" />
<Button
android:id="@+id/btnMoreModel"
android:text="@string/more"
android:layout_marginTop="12dp"
android:layout_width="wrap_content"
android:layout_height="48dp"
app:layout_constraintTop_toBottomOf="@+id/tvModelUrl"
app:layout_constraintEnd_toEndOf="parent"
/>
<EditText
android:id="@+id/etUrl"
android:layout_width="0dp"
android:layout_height="48dp"
android:text="https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/bendi3_20240518.zip"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/btnMoreModel"
app:layout_constraintEnd_toStartOf="@+id/btnMoreModel"
/>
<Button
android:id="@+id/btnPlay"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="20dp"
android:text="@string/play"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etUrl" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchDebug"
android:text="@string/debug_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="13sp"
android:layout_margin="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnPlay"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardAvatar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:strokeWidth="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
<ImageView
android:id="@+id/ivAvatar"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="12dp"
android:textColor="#333333"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@+id/ivAvatar" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -9,4 +9,11 @@
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="primary">#5a7bbe</color> <color name="primary">#5a7bbe</color>
<color name="primary_99">#995a7bbe</color> <color name="primary_99">#995a7bbe</color>
<!-- 温暖主题色 -->
<color name="warm_bg">#FFF5F0EB</color>
<color name="warm_text_primary">#FF3E2723</color>
<color name="warm_text_secondary">#FF8D6E63</color>
<color name="warm_text_hint">#FFBCAAA4</color>
<color name="warm_accent">#FFE8926F</color>
</resources> </resources>

View File

@ -1,5 +1,8 @@
<resources> <resources>
<string name="app_name">DUIX Demo</string> <string name="app_name">AI 情感陪护</string>
<string name="app_title">AI 情感陪护</string>
<string name="app_subtitle">选一位陪伴者,开始温暖对话</string>
<string name="choose_companion_hint">点击头像即可开始对话</string>
<string name="play">Play</string> <string name="play">Play</string>
<string name="base_config_url">BaseConfig Url:</string> <string name="base_config_url">BaseConfig Url:</string>
<string name="model_url">Model Url:</string> <string name="model_url">Model Url:</string>

View File

@ -1,15 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.DUIX.Test" parent="Theme.AppCompat.Light.NoActionBar"> <style name="Theme.DUIX.Test" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">true</item>
<item name="android:windowBackground">@android:color/white</item> <item name="android:windowBackground">@color/warm_bg</item>
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="android:windowIsTranslucent">false</item> <item name="android:windowIsTranslucent">false</item>
<item name="android:windowIsFloating">false</item> <item name="android:windowIsFloating">false</item>
<item name="colorPrimary">#fff</item> <item name="colorPrimary">@color/warm_bg</item>
<item name="colorPrimaryDark">#4349a9</item> <item name="colorPrimaryDark">@color/warm_accent</item>
<item name="colorAccent">#4349a9</item> <item name="colorAccent">@color/warm_accent</item>
<item name="android:windowTranslucentStatus">true</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
</style>
<!-- CallActivity 全屏沉浸主题 -->
<style name="Theme.DUIX.Call" parent="Theme.DUIX.Test">
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowTranslucentNavigation">true</item>
</style> </style>
</resources> </resources>