Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package ai.guiji.duix.test.model

import ai.guiji.duix.test.R

/**
* 数字人形象数据模型
*/
data class AvatarModel(
val name: String,
val description: String,
val url: String,
val avatarResId: Int = 0 // 头像资源 ID,0 表示无头像
) {
companion object {
/**
* 官方公开的 4 个数字人形象 (v2.0.1) - 推荐使用
*/
val OFFICIAL_AVATARS = arrayListOf(
AvatarModel(
name = "Leo",
description = "年轻男性 · 休闲风格",
url = "https://github.com/duixcom/Duix.mobile/releases/download/v2.0.1/Leo.zip",
avatarResId = R.drawable.avatar_leo
),
AvatarModel(
name = "Oliver",
description = "成熟男性 · 商务正装",
url = "https://github.com/duixcom/Duix.mobile/releases/download/v2.0.1/Oliver.zip",
avatarResId = R.drawable.avatar_oliver
),
AvatarModel(
name = "Sofia",
description = "知性女性 · 白衬衫",
url = "https://github.com/duixcom/Duix.mobile/releases/download/v2.0.1/Sofia.zip",
avatarResId = R.drawable.avatar_sofia
),
AvatarModel(
name = "Lily",
description = "年轻女性 · 亚洲面孔",
url = "https://github.com/duixcom/Duix.mobile/releases/download/v2.0.1/Lily.zip",
avatarResId = R.drawable.avatar_lily
)
)

/**
* 更多可用模型 (v1.0.0)
*/
val MORE_MODELS = arrayListOf(
AvatarModel(
name = "bendi3",
description = "通用形象",
url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/bendi3_20240518.zip"
),
AvatarModel(
name = "airuike",
description = "通用形象",
url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/airuike_20240409.zip"
),
AvatarModel(
name = "ddzh",
description = "数字人形象",
url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/ddzh.zip"
),
AvatarModel(
name = "shuziren_v2",
description = "数字人 v2",
url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/shuziren_v2.zip"
),
AvatarModel(
name = "St_yxm",
description = "加密优化版",
url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/St_yxm_encrypted_optim.zip"
),
AvatarModel(
name = "Zyynv",
description = "1080p 高清",
url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/Zyynv_0515_1080p.zip"
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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.dialog.LoadingDialog
import ai.guiji.duix.test.ui.dialog.ModelSelectorDialog
import android.annotation.SuppressLint
Expand All @@ -17,21 +18,9 @@ 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 var mBaseConfigUrl = ""
private var mModelUrl = ""

Expand All @@ -43,70 +32,80 @@ class MainActivity : BaseActivity() {

binding.tvSdkVersion.text = "SDK Version: ${BuildConfig.VERSION_NAME}"

// 默认选中第一个官方形象
val defaultAvatar = AvatarModel.OFFICIAL_AVATARS[0]
binding.etUrl.setText(defaultAvatar.url)
binding.tvSelectedAvatar.text = "当前形象: ${defaultAvatar.name} (${defaultAvatar.description})"

binding.btnMoreModel.setOnClickListener {
val modelSelectorDialog = ModelSelectorDialog(mContext, models, object : ModelSelectorDialog.Listener{
override fun onSelect(url: String) {
binding.etUrl.setText(url)
val modelSelectorDialog = ModelSelectorDialog(
mContext,
AvatarModel.OFFICIAL_AVATARS,
AvatarModel.MORE_MODELS,
object : ModelSelectorDialog.Listener {
override fun onSelect(avatar: AvatarModel) {
binding.etUrl.setText(avatar.url)
binding.tvSelectedAvatar.text = "当前形象: ${avatar.name} (${avatar.description})"
}
}
})
)
modelSelectorDialog.show()
}
binding.btnPlay.setOnClickListener {
play()
}
}

private fun play(){
private fun play() {
mBaseConfigUrl = binding.etBaseConfig.text.toString()
mModelUrl = binding.etUrl.text.toString()
if (TextUtils.isEmpty(mBaseConfigUrl)){
if (TextUtils.isEmpty(mBaseConfigUrl)) {
Toast.makeText(mContext, R.string.base_config_cannot_be_empty, Toast.LENGTH_SHORT).show()
return
}
if (TextUtils.isEmpty(mModelUrl)){
if (TextUtils.isEmpty(mModelUrl)) {
Toast.makeText(mContext, R.string.model_url_cannot_be_empty, Toast.LENGTH_SHORT).show()
return
}
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() {
if (VirtualModelUtil.checkModel(mContext, mModelUrl)) {
jumpPlayPage()
} else {
modelDownload()
}
}

private fun jumpPlayPage(){
private fun jumpPlayPage() {
val intent = Intent(mContext, CallActivity::class.java)
intent.putExtra("modelUrl", mModelUrl)
val debug = binding.switchDebug.isChecked
intent.putExtra("debug", debug)
startActivity(intent)
}

private fun baseConfigDownload(){
private fun baseConfigDownload() {
mLoadingDialog?.dismiss()
mLoadingDialog = LoadingDialog(mContext, "Start downloading")
mLoadingDialog?.show()
VirtualModelUtil.baseConfigDownload(mContext, mBaseConfigUrl, 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){
if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Config download(${progress}%)")
}
}
Expand All @@ -115,10 +114,10 @@ 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){
if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Config unzip(${progress}%)")
}
}
Expand All @@ -142,21 +141,21 @@ class MainActivity : BaseActivity() {
})
}

private fun modelDownload(){
private fun modelDownload() {
mLoadingDialog?.dismiss()
mLoadingDialog = LoadingDialog(mContext, "Start downloading")
mLoadingDialog?.show()
VirtualModelUtil.modelDownload(mContext, mModelUrl, object : VirtualModelUtil.ModelDownloadCallback{
VirtualModelUtil.modelDownload(mContext, mModelUrl, 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){
if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Model download(${progress}%)")
}
}
Expand All @@ -169,10 +168,10 @@ class MainActivity : BaseActivity() {
total: Long,
) {
val progress = (current * 100 / total).toInt()
if (progress != mLastProgress){
if (progress != mLastProgress) {
mLastProgress = progress
runOnUiThread {
if (mLoadingDialog?.isShowing == true){
if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Model unzip(${progress}%)")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package ai.guiji.duix.test.ui.adapter

import ai.guiji.duix.test.databinding.ItemModelSelectorBinding
import ai.guiji.duix.test.model.AvatarModel
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView


class ModelSelectorAdapter(
private val mList: ArrayList<String>,
private val mList: ArrayList<AvatarModel>,
private val callback: Callback
) : RecyclerView.Adapter<ModelSelectorAdapter.ItemHolder>() {

private var selectedPosition = -1

class ItemHolder(val itemBinding: ItemModelSelectorBinding) :
RecyclerView.ViewHolder(itemBinding.root)

Expand All @@ -23,9 +27,28 @@ class ModelSelectorAdapter(

@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ItemHolder, position: Int) {
holder.itemBinding.tvModelUrl.text = mList[position]
holder.itemBinding.tvModelUrl.setOnClickListener {
callback.onClick(mList[position])
val avatar = mList[position]
holder.itemBinding.tvModelName.text = avatar.name
holder.itemBinding.tvModelDesc.text = avatar.description

// 显示头像(如果有)
if (avatar.avatarResId != 0) {
holder.itemBinding.ivAvatar.visibility = View.VISIBLE
holder.itemBinding.ivAvatar.setImageResource(avatar.avatarResId)
} else {
holder.itemBinding.ivAvatar.visibility = View.GONE
}

// 高亮选中项
val isSelected = position == selectedPosition
holder.itemBinding.root.alpha = if (isSelected) 1.0f else 0.85f

holder.itemBinding.root.setOnClickListener {
val oldPos = selectedPosition
selectedPosition = holder.adapterPosition
if (oldPos >= 0) notifyItemChanged(oldPos)
notifyItemChanged(selectedPosition)
callback.onClick(avatar)
}
}

Expand All @@ -34,6 +57,6 @@ class ModelSelectorAdapter(
}

interface Callback {
fun onClick(url: String)
fun onClick(avatar: AvatarModel)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,60 @@ package ai.guiji.duix.test.ui.dialog

import ai.guiji.duix.test.R
import ai.guiji.duix.test.databinding.DialogModelSelectorBinding
import ai.guiji.duix.test.model.AvatarModel
import ai.guiji.duix.test.ui.adapter.ModelSelectorAdapter
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager


class ModelSelectorDialog(mContext: Context, val models: ArrayList<String>, private val listener: Listener) :
Dialog(mContext, R.style.dialog_center) {
class ModelSelectorDialog(
mContext: Context,
private val officialModels: ArrayList<AvatarModel>,
private val moreModels: ArrayList<AvatarModel>,
private val listener: Listener
) : Dialog(mContext, R.style.dialog_center) {

private lateinit var binding: DialogModelSelectorBinding
private var mAdapter: ModelSelectorAdapter?=null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogModelSelectorBinding.inflate(layoutInflater)
super.setContentView(binding.root)

mAdapter = ModelSelectorAdapter(models, object : ModelSelectorAdapter.Callback{
override fun onClick(url: String) {
// 设置 Dialog 宽度为屏幕 90%
window?.setLayout(
(context.resources.displayMetrics.widthPixels * 0.9).toInt(),
WindowManager.LayoutParams.WRAP_CONTENT
)

// 推荐形象 - 网格布局
val officialAdapter = ModelSelectorAdapter(officialModels, object : ModelSelectorAdapter.Callback {
override fun onClick(avatar: AvatarModel) {
dismiss()
listener.onSelect(avatar)
}
})
binding.rvOfficialModels.adapter = officialAdapter

// 更多模型 - 列表布局
val moreAdapter = ModelSelectorAdapter(moreModels, object : ModelSelectorAdapter.Callback {
override fun onClick(avatar: AvatarModel) {
dismiss()
listener.onSelect(url)
listener.onSelect(avatar)
}
})
binding.rvModels.adapter = mAdapter
binding.rvLegacyModels.adapter = moreAdapter

setCancelable(true)
setCanceledOnTouchOutside(true)
}

interface Listener {
fun onSelect(url: String)
fun onSelect(avatar: AvatarModel)
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading