llm testing
This commit is contained in:
@@ -42,6 +42,7 @@ import com.digitalperson.data.AppDatabase
|
||||
import com.digitalperson.data.entity.ChatMessage
|
||||
import com.digitalperson.interaction.ConversationBufferMemory
|
||||
import com.digitalperson.interaction.ConversationSummaryMemory
|
||||
|
||||
import java.io.File
|
||||
import android.graphics.BitmapFactory
|
||||
import org.json.JSONObject
|
||||
@@ -56,6 +57,9 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
import com.digitalperson.onboard_testing.FaceRecognitionTest
|
||||
import com.digitalperson.onboard_testing.LLMSummaryTest
|
||||
|
||||
class Live2DChatActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val TAG_ACTIVITY = "Live2DChatActivity"
|
||||
@@ -116,6 +120,9 @@ class Live2DChatActivity : AppCompatActivity() {
|
||||
private var lastFaceIdentityId: String? = null
|
||||
private var lastFaceRecognizedName: String? = null
|
||||
|
||||
private lateinit var faceRecognitionTest: FaceRecognitionTest
|
||||
private lateinit var llmSummaryTest: LLMSummaryTest
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
@@ -400,6 +407,8 @@ class Live2DChatActivity : AppCompatActivity() {
|
||||
// 显示本地 LLM 开关,并同步状态
|
||||
uiManager.showLLMSwitch(false)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -457,12 +466,21 @@ class Live2DChatActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
// 测试人脸识别(延迟执行,确保所有组件初始化完成)
|
||||
// ioScope.launch {
|
||||
// kotlinx.coroutines.delay(10000) // 等待3秒,确保所有组件初始化完成
|
||||
// runOnUiThread {
|
||||
// runFaceRecognitionTest()
|
||||
// }
|
||||
// }
|
||||
if (AppConfig.OnboardTesting.FACE_REGONITION) {
|
||||
faceRecognitionTest = FaceRecognitionTest(this)
|
||||
faceRecognitionTest.setFaceDetectionPipeline(faceDetectionPipeline)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
kotlinx.coroutines.delay(10000)
|
||||
runOnUiThread {
|
||||
faceRecognitionTest.runTest("http://192.168.1.19:5000/api/face_test_images") { message ->
|
||||
Log.i(AppConfig.TAG, message)
|
||||
uiManager.appendToUi("\n$message\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,185 +497,8 @@ class Live2DChatActivity : AppCompatActivity() {
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行人脸识别相似度测试
|
||||
* 使用网络服务器上的测试图片
|
||||
*/
|
||||
private fun runFaceRecognitionTest() {
|
||||
Log.i(TAG_ACTIVITY, "Starting face recognition test...")
|
||||
uiManager.appendToUi("\n[测试] 开始人脸识别相似度测试...\n")
|
||||
|
||||
// 从服务器获取目录下的所有图片文件列表
|
||||
ioScope.launch {
|
||||
try {
|
||||
val imageUrls = fetchImageListFromServer("http://192.168.1.19:5000/api/face_test_images")
|
||||
|
||||
if (imageUrls.isEmpty()) {
|
||||
Log.e(AppConfig.TAG, "No images found in server directory")
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n[测试] 服务器目录中没有找到图片文件\n")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
Log.i(AppConfig.TAG, "[测试]Found ${imageUrls.size} images: $imageUrls")
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n[测试] 发现 ${imageUrls.size} 张测试图片\n")
|
||||
}
|
||||
|
||||
val bitmaps = mutableListOf<Pair<String, Bitmap>>()
|
||||
|
||||
// 下载所有图片
|
||||
for (url in imageUrls) {
|
||||
Log.d(AppConfig.TAG, "[测试]Downloading test image: $url")
|
||||
val bitmap = downloadImage(url)
|
||||
if (bitmap != null) {
|
||||
val fileName = url.substringAfterLast("/")
|
||||
bitmaps.add(fileName to bitmap)
|
||||
Log.d(AppConfig.TAG, "[测试]Downloaded image $fileName successfully")
|
||||
} else {
|
||||
Log.e(AppConfig.TAG, "[测试]Failed to download image: $url")
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmaps.size < 2) {
|
||||
Log.e(AppConfig.TAG, "[测试]Not enough test images downloaded")
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n[测试] 测试图片下载失败,无法进行测试\n")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
// 对所有图片两两比较
|
||||
Log.i(AppConfig.TAG, "[测试]Starting similarity comparison for ${bitmaps.size} images...")
|
||||
for (i in 0 until bitmaps.size) {
|
||||
for (j in i + 1 until bitmaps.size) {
|
||||
val (fileName1, bitmap1) = bitmaps[i]
|
||||
val (fileName2, bitmap2) = bitmaps[j]
|
||||
|
||||
Log.d(AppConfig.TAG, "[测试]Comparing $fileName1 with $fileName2")
|
||||
|
||||
// 检测人脸
|
||||
val face1 = detectFace(bitmap1)
|
||||
val face2 = detectFace(bitmap2)
|
||||
|
||||
Log.d(AppConfig.TAG, "[测试]Face detection result: face1=$face1, face2=$face2")
|
||||
|
||||
if (face1 != null && face2 != null) {
|
||||
// 计算相似度
|
||||
Log.d(AppConfig.TAG, "[测试]Detected faces, calculating similarity...")
|
||||
val similarity = faceDetectionPipeline?.getRecognizer()?.testSimilarityBetween(
|
||||
bitmap1, face1, bitmap2, face2
|
||||
)
|
||||
val similarityRaw = faceDetectionPipeline?.getRecognizer()?.run {
|
||||
val emb1 = extractEmbedding(bitmap1, face1)
|
||||
val emb2 = extractEmbedding(bitmap2, face2)
|
||||
if (emb1.isNotEmpty() && emb2.isNotEmpty()) {
|
||||
var dot = 0f
|
||||
var n1 = 0f
|
||||
var n2 = 0f
|
||||
for (k in emb1.indices) {
|
||||
dot += emb1[k] * emb2[k]
|
||||
n1 += emb1[k] * emb1[k]
|
||||
n2 += emb2[k] * emb2[k]
|
||||
}
|
||||
if (n1 > 1e-12f && n2 > 1e-12f) {
|
||||
(dot / (kotlin.math.sqrt(n1) * kotlin.math.sqrt(n2))).coerceIn(-1f, 1f)
|
||||
} else -1f
|
||||
} else -1f
|
||||
}
|
||||
|
||||
Log.d(AppConfig.TAG, "[测试]Similarity result: $similarity")
|
||||
|
||||
if (similarity != null && similarity >= 0) {
|
||||
val message = "[测试] 图片 $fileName1 与 $fileName2 的相似度: $similarity"
|
||||
val compareMessage = "[测试] 对齐后=$similarity, 原始裁剪=$similarityRaw"
|
||||
Log.i(AppConfig.TAG, message)
|
||||
Log.i(AppConfig.TAG, compareMessage)
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n$message\n")
|
||||
uiManager.appendToUi("$compareMessage\n")
|
||||
}
|
||||
} else {
|
||||
Log.w(AppConfig.TAG, "[测试]Failed to calculate similarity: $similarity")
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n[测试] 计算相似度失败: $similarity\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val message = "[测试] 无法检测到人脸: $fileName1 或 $fileName2"
|
||||
Log.w(AppConfig.TAG, message)
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n$message\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(AppConfig.TAG, "[测试]Face recognition test completed")
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n[测试] 人脸识别相似度测试完成\n")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Error during face recognition test: ${e.message}", e)
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n[测试] 测试过程中发生错误: ${e.message}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器获取目录下的图片文件列表
|
||||
* 调用 API 接口获取图片列表
|
||||
*/
|
||||
private fun fetchImageListFromServer(apiUrl: String): List<String> {
|
||||
val imageUrls = mutableListOf<String>()
|
||||
|
||||
return try {
|
||||
// 调用 API 接口
|
||||
val connection = java.net.URL(apiUrl).openConnection() as java.net.HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 10000
|
||||
connection.readTimeout = 10000
|
||||
connection.setRequestProperty("Accept", "application/json")
|
||||
|
||||
try {
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode == 200) {
|
||||
connection.inputStream.use { input ->
|
||||
val content = input.bufferedReader().use { it.readText() }
|
||||
Log.d(AppConfig.TAG, "API response: $content")
|
||||
|
||||
// 解析 JSON 响应
|
||||
val jsonObject = org.json.JSONObject(content)
|
||||
val imagesArray = jsonObject.getJSONArray("images")
|
||||
|
||||
// 构建完整的图片 URL
|
||||
val baseUrl = apiUrl.replace("/api/face_test_images", "/shared_files/face_test")
|
||||
for (i in 0 until imagesArray.length()) {
|
||||
val fileName = imagesArray.getString(i)
|
||||
val fullUrl = "$baseUrl/$fileName"
|
||||
imageUrls.add(fullUrl)
|
||||
Log.d(AppConfig.TAG, "Added image URL: $fullUrl")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(AppConfig.TAG, "API request failed with code: $responseCode")
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
|
||||
imageUrls
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to fetch image list: ${e.message}", e)
|
||||
// 如果获取失败,返回空列表
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检查 URL 是否存在
|
||||
*/
|
||||
@@ -674,87 +515,8 @@ class Live2DChatActivity : AppCompatActivity() {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从网络下载图片
|
||||
*/
|
||||
private fun downloadImage(url: String): Bitmap? {
|
||||
return try {
|
||||
// 使用与大模型相同的下载方式
|
||||
val tempFile = File(cacheDir, "temp_test_image_${System.currentTimeMillis()}.jpg")
|
||||
val success = FileHelper.downloadTestImage(url, tempFile)
|
||||
|
||||
if (success && tempFile.exists()) {
|
||||
val bitmap = BitmapFactory.decodeFile(tempFile.absolutePath)
|
||||
tempFile.delete() // 删除临时文件
|
||||
bitmap
|
||||
} else {
|
||||
Log.e(AppConfig.TAG, "Failed to download image: $url")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to download image: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测图片中的人脸
|
||||
*/
|
||||
private fun detectFace(bitmap: Bitmap): FaceBox? {
|
||||
Log.d(AppConfig.TAG, "[测试]Detecting face in bitmap: ${bitmap.width}x${bitmap.height}")
|
||||
return try {
|
||||
val engine = RetinaFaceEngineRKNN()
|
||||
Log.d(AppConfig.TAG, "[测试]Initializing RetinaFace engine...")
|
||||
if (engine.initialize(applicationContext)) {
|
||||
Log.d(AppConfig.TAG, "[测试]RetinaFace engine initialized successfully")
|
||||
val raw = engine.detect(bitmap)
|
||||
Log.d(AppConfig.TAG, "[测试]Face detection result: ${raw.joinToString(", ")}")
|
||||
engine.release()
|
||||
|
||||
if (raw.isNotEmpty()) {
|
||||
val stride = when {
|
||||
raw.size % 15 == 0 -> 15
|
||||
raw.size % 5 == 0 -> 5
|
||||
else -> 0
|
||||
}
|
||||
|
||||
Log.d(AppConfig.TAG, "[测试]Stride: $stride, raw size: ${raw.size}")
|
||||
|
||||
if (stride > 0) {
|
||||
val faceCount = raw.size / stride
|
||||
Log.d(AppConfig.TAG, "[测试]Detected $faceCount faces")
|
||||
if (faceCount > 0) {
|
||||
val i = 0
|
||||
val lm = if (stride >= 15) raw.copyOfRange(i + 5, i + 15) else null
|
||||
val hasLm = lm?.all { it >= 0f } == true
|
||||
|
||||
val faceBox = FaceBox(
|
||||
left = raw[i],
|
||||
top = raw[i + 1],
|
||||
right = raw[i + 2],
|
||||
bottom = raw[i + 3],
|
||||
score = raw[i + 4],
|
||||
hasLandmarks = hasLm,
|
||||
landmarks = if (hasLm) lm else null
|
||||
)
|
||||
Log.d(AppConfig.TAG, "[测试]Created face box: $faceBox")
|
||||
return faceBox
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(AppConfig.TAG, "[测试]No faces detected in bitmap")
|
||||
}
|
||||
} else {
|
||||
Log.e(AppConfig.TAG, "[测试]Failed to initialize RetinaFace engine")
|
||||
}
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "[测试]Failed to detect face: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun createAsrCallback() = object : AsrManager.AsrCallback {
|
||||
override fun onAsrStarted() {
|
||||
currentTrace?.markASRStart()
|
||||
@@ -1247,27 +1009,30 @@ class Live2DChatActivity : AppCompatActivity() {
|
||||
Log.d(AppConfig.TAG, "Generated conversation summary for $activeUserId: $summary")
|
||||
}
|
||||
|
||||
// 使用 conversationBufferMemory 的对话记录提取用户信息
|
||||
// 使用多角度提问方式提取用户信息
|
||||
val dialogue = messages.joinToString("\n") { "${it.role}: ${it.content}" }
|
||||
|
||||
requestLocalProfileExtraction(dialogue) { raw ->
|
||||
requestMultiAngleProfileExtraction(dialogue) { profileData ->
|
||||
try {
|
||||
val json = parseFirstJsonObject(raw)
|
||||
val name = json.optString("name", "").trim().ifBlank { null }
|
||||
val age = json.optString("age", "").trim().ifBlank { null }
|
||||
val gender = json.optString("gender", "").trim().ifBlank { null }
|
||||
val hobbies = json.optString("hobbies", "").trim().ifBlank { null }
|
||||
val summary = json.optString("summary", "").trim().ifBlank { null }
|
||||
if (name != null) {
|
||||
userMemoryStore.updateDisplayName(activeUserId, name)
|
||||
}
|
||||
userMemoryStore.updateProfile(activeUserId, age, gender, hobbies, summary)
|
||||
|
||||
// 清空已处理的对话记录
|
||||
conversationBufferMemory.clear(activeUserId)
|
||||
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n[Memory] 已更新用户画像: $activeUserId\n")
|
||||
val nameToUpdate = profileData["name"]?.trim()?.ifBlank { null }
|
||||
val ageToUpdate = profileData["age"]?.trim()?.ifBlank { null }
|
||||
val genderToUpdate = profileData["gender"]?.trim()?.ifBlank { null }
|
||||
val hobbiesToUpdate = profileData["hobbies"]?.trim()?.ifBlank { null }
|
||||
val summaryToUpdate = profileData["summary"]?.trim()?.ifBlank { null }
|
||||
Log.d(TAG_LLM, "profileData: $profileData")
|
||||
if (nameToUpdate != null || ageToUpdate != null || genderToUpdate != null || hobbiesToUpdate != null || summaryToUpdate != null) {
|
||||
if (nameToUpdate != null) {
|
||||
userMemoryStore.updateDisplayName(activeUserId, nameToUpdate)
|
||||
Log.i(TAG_LLM, "Updated display name to $nameToUpdate")
|
||||
}
|
||||
userMemoryStore.updateProfile(activeUserId, ageToUpdate, genderToUpdate, hobbiesToUpdate, summaryToUpdate)
|
||||
|
||||
// 清空已处理的对话记录
|
||||
conversationBufferMemory.clear(activeUserId)
|
||||
|
||||
runOnUiThread {
|
||||
uiManager.appendToUi("\n[Memory] 已更新用户画像: $activeUserId\n")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG_LLM, "Profile parse failed: ${e.message}")
|
||||
@@ -1275,27 +1040,77 @@ class Live2DChatActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestLocalProfileExtraction(dialogue: String, onResult: (String) -> Unit) {
|
||||
private fun requestMultiAngleProfileExtraction(dialogue: String, onResult: (Map<String, String>) -> Unit) {
|
||||
try {
|
||||
val local = llmManager
|
||||
if (local == null) {
|
||||
onResult("{}")
|
||||
onResult(emptyMap())
|
||||
return
|
||||
}
|
||||
localThoughtSilentMode = true
|
||||
pendingLocalProfileCallback = onResult
|
||||
Log.i(TAG_LLM, "Routing profile extraction to LOCAL")
|
||||
local.generateResponseWithSystem(
|
||||
"你是信息抽取器。仅输出JSON对象,不要其他文字。字段为name,age,gender,hobbies,summary。",
|
||||
"请从以下对话提取用户信息,未知填空字符串,注意不需要:\n$dialogue"
|
||||
|
||||
val questions = listOf(
|
||||
"请从对话中提取用户的姓名,只返回姓名,如果没有提到姓名,请返回未知",
|
||||
"请从对话中提取用户的年龄,只返回年龄,如果没有提到年龄,请返回未知",
|
||||
"请从对话中提取用户的性别,只返回性别,如果没有提到性别,请返回未知",
|
||||
"请从对话中提取用户的爱好,只返回爱好,如果没有提到爱好,请返回未知",
|
||||
"请总结对话,只返回总结的内容"
|
||||
)
|
||||
|
||||
var completed = 0
|
||||
val results = mutableMapOf<String, String>()
|
||||
|
||||
questions.forEach { question ->
|
||||
val prompt = buildMultiAnglePrompt(dialogue, question)
|
||||
local.generate(prompt) { answer ->
|
||||
val processedAnswer = processProfileAnswer(answer)
|
||||
|
||||
when {
|
||||
question.contains("姓名") -> results["name"] = processedAnswer
|
||||
question.contains("年龄") -> results["age"] = processedAnswer
|
||||
question.contains("性别") -> results["gender"] = processedAnswer
|
||||
question.contains("爱好") -> results["hobbies"] = processedAnswer
|
||||
question.contains("总结") -> results["summary"] = processedAnswer
|
||||
}
|
||||
|
||||
completed++
|
||||
|
||||
if (completed == questions.size) {
|
||||
onResult(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
pendingLocalProfileCallback = null
|
||||
localThoughtSilentMode = false
|
||||
Log.e(TAG_LLM, "requestLocalProfileExtraction failed: ${e.message}", e)
|
||||
onResult("{}")
|
||||
Log.e(TAG_LLM, "requestMultiAngleProfileExtraction failed: ${e.message}", e)
|
||||
onResult(emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMultiAnglePrompt(dialogue: String, question: String): String {
|
||||
return """
|
||||
请根据以下对话回答问题:
|
||||
|
||||
对话内容:
|
||||
$dialogue
|
||||
|
||||
问题:$question
|
||||
|
||||
回答:
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun processProfileAnswer(answer: String): String {
|
||||
var processed = answer.replace("<", "").replace(">", "")
|
||||
if (processed.contains("unknown", ignoreCase = true) ||
|
||||
processed.contains("null", ignoreCase = true) ||
|
||||
processed.contains("未知")) {
|
||||
return ""
|
||||
}
|
||||
if (processed.contains(":")) {
|
||||
processed = processed.substringAfter(":").trim()
|
||||
}
|
||||
processed = processed.replace(".", "").trim()
|
||||
return processed
|
||||
}
|
||||
|
||||
private fun parseFirstJsonObject(text: String): JSONObject {
|
||||
val raw = text.trim()
|
||||
@@ -1363,6 +1178,22 @@ class Live2DChatActivity : AppCompatActivity() {
|
||||
})
|
||||
Log.i(TAG_LLM, "LOCAL memory LLM initialized")
|
||||
useLocalLLM = true
|
||||
|
||||
if (AppConfig.OnboardTesting.LOCAL_LLM_SUMMARY) {
|
||||
llmSummaryTest = LLMSummaryTest(this)
|
||||
ioScope.launch {
|
||||
kotlinx.coroutines.delay(5000) // 等待5秒,确保LLMManager初始化完成
|
||||
runOnUiThread {
|
||||
if (llmManager != null) {
|
||||
llmSummaryTest.setLLMManager(llmManager!!)
|
||||
llmSummaryTest.runTest { message ->
|
||||
Log.i(AppConfig.TAG, message)
|
||||
uiManager.appendToUi("\n$message\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to initialize LLM: ${e.message}", e)
|
||||
Log.e(TAG_LLM, "LOCAL init failed: ${e.message}", e)
|
||||
|
||||
@@ -90,4 +90,13 @@ object AppConfig {
|
||||
// 模型文件大小估计(字节)
|
||||
const val MODEL_SIZE_ESTIMATE = 500L * 1024 * 1024 // 500MB
|
||||
}
|
||||
|
||||
object OnboardTesting {
|
||||
// 测试人脸识别
|
||||
const val FACE_REGONITION = false
|
||||
// 测试本地大模型
|
||||
const val LOCAL_LLM_SUMMARY = false
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,16 @@ class UserMemoryStore(context: Context) {
|
||||
fun updateDisplayName(userId: String, displayName: String?) {
|
||||
if (displayName.isNullOrBlank()) return
|
||||
upsertUserSeen(userId, displayName)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val existing = memoryCache[userId]
|
||||
if (existing != null) {
|
||||
memoryCache[userId] = existing.copy(
|
||||
displayName = displayName ?: existing.displayName,
|
||||
lastSeenAt = now
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateThought(userId: String, thought: String) {
|
||||
@@ -73,7 +83,7 @@ class UserMemoryStore(context: Context) {
|
||||
|
||||
fun updateProfile(userId: String, age: String?, gender: String?, hobbies: String?, summary: String?) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
upsertUserSeen(userId, null)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
userMemoryDao.updateProfile(userId, age, gender, hobbies, summary, now)
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package com.digitalperson.onboard_testing
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import com.digitalperson.config.AppConfig
|
||||
import com.digitalperson.engine.RetinaFaceEngineRKNN
|
||||
import com.digitalperson.face.FaceBox
|
||||
import com.digitalperson.face.FaceDetectionPipeline
|
||||
import com.digitalperson.face.FaceRecognizer
|
||||
import com.digitalperson.util.FileHelper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class FaceRecognitionTest(private val context: Context) {
|
||||
|
||||
private lateinit var faceDetectionPipeline: FaceDetectionPipeline
|
||||
private lateinit var faceRecognizer: FaceRecognizer
|
||||
|
||||
fun setFaceDetectionPipeline(pipeline: FaceDetectionPipeline) {
|
||||
this.faceDetectionPipeline = pipeline
|
||||
this.faceRecognizer = pipeline.getRecognizer()
|
||||
}
|
||||
|
||||
fun runTest(apiUrl: String, onResult: (String) -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val imageUrls = fetchImageListFromServer(apiUrl)
|
||||
|
||||
if (imageUrls.isEmpty()) {
|
||||
onResult("[测试] 服务器目录中没有找到图片文件")
|
||||
return@launch
|
||||
}
|
||||
|
||||
onResult("[测试] 发现 ${imageUrls.size} 张测试图片")
|
||||
|
||||
val bitmaps = mutableListOf<Pair<String, Bitmap>>()
|
||||
|
||||
// 下载所有图片
|
||||
for (url in imageUrls) {
|
||||
val bitmap = downloadImage(url)
|
||||
if (bitmap != null) {
|
||||
val fileName = url.substringAfterLast("/")
|
||||
bitmaps.add(fileName to bitmap)
|
||||
onResult("[测试] 下载图片 $fileName 成功")
|
||||
} else {
|
||||
onResult("[测试] 下载图片 $url 失败")
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmaps.size < 2) {
|
||||
onResult("[测试] 测试图片下载失败,无法进行测试")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// 对所有图片两两比较(只比较不同图片)
|
||||
for (i in 0 until bitmaps.size) {
|
||||
for (j in i + 1 until bitmaps.size) {
|
||||
val (fileName1, bitmap1) = bitmaps[i]
|
||||
val (fileName2, bitmap2) = bitmaps[j]
|
||||
|
||||
// 检测人脸
|
||||
val face1 = detectFace(bitmap1)
|
||||
val face2 = detectFace(bitmap2)
|
||||
|
||||
if (face1 != null && face2 != null) {
|
||||
// 计算相似度
|
||||
val similarity = faceRecognizer.testSimilarityBetween(
|
||||
bitmap1, face1, bitmap2, face2
|
||||
)
|
||||
|
||||
if (similarity >= 0) {
|
||||
onResult("[测试] 图片 $fileName1 与 $fileName2 的相似度: $similarity")
|
||||
} else {
|
||||
onResult("[测试] 计算相似度失败: $similarity")
|
||||
}
|
||||
} else {
|
||||
onResult("[测试] 无法检测到人脸: $fileName1 或 $fileName2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onResult("[测试] 人脸识别相似度测试完成")
|
||||
|
||||
} catch (e: Exception) {
|
||||
onResult("[测试] 测试过程中发生错误: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchImageListFromServer(apiUrl: String): List<String> {
|
||||
val imageUrls = mutableListOf<String>()
|
||||
|
||||
return try {
|
||||
// 调用 API 接口
|
||||
val connection = java.net.URL(apiUrl).openConnection() as java.net.HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 10000
|
||||
connection.readTimeout = 10000
|
||||
connection.setRequestProperty("Accept", "application/json")
|
||||
|
||||
try {
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode == 200) {
|
||||
connection.inputStream.use { input ->
|
||||
val content = input.bufferedReader().use { it.readText() }
|
||||
Log.d(AppConfig.TAG, "API response: $content")
|
||||
|
||||
// 解析 JSON 响应
|
||||
val jsonObject = org.json.JSONObject(content)
|
||||
val imagesArray = jsonObject.getJSONArray("images")
|
||||
|
||||
// 构建完整的图片 URL
|
||||
val baseUrl = apiUrl.replace("/api/face_test_images", "/shared_files/face_test")
|
||||
for (i in 0 until imagesArray.length()) {
|
||||
val fileName = imagesArray.getString(i)
|
||||
val fullUrl = "$baseUrl/$fileName"
|
||||
imageUrls.add(fullUrl)
|
||||
Log.d(AppConfig.TAG, "Added image URL: $fullUrl")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(AppConfig.TAG, "API request failed with code: $responseCode")
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
|
||||
imageUrls
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to fetch image list: ${e.message}", e)
|
||||
// 如果获取失败,返回空列表
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadImage(url: String): Bitmap? {
|
||||
return try {
|
||||
// 使用与大模型相同的下载方式
|
||||
val tempFile = File(context.cacheDir, "temp_test_image_${System.currentTimeMillis()}.jpg")
|
||||
val success = FileHelper.downloadTestImage(url, tempFile)
|
||||
|
||||
if (success && tempFile.exists()) {
|
||||
val bitmap = BitmapFactory.decodeFile(tempFile.absolutePath)
|
||||
tempFile.delete() // 删除临时文件
|
||||
bitmap
|
||||
} else {
|
||||
Log.e(AppConfig.TAG, "Failed to download image: $url")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to download image: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun detectFace(bitmap: Bitmap): FaceBox? {
|
||||
Log.d(AppConfig.TAG, "[测试]Detecting face in bitmap: ${bitmap.width}x${bitmap.height}")
|
||||
return try {
|
||||
val engine = RetinaFaceEngineRKNN()
|
||||
Log.d(AppConfig.TAG, "[测试]Initializing RetinaFace engine...")
|
||||
if (engine.initialize(context)) {
|
||||
Log.d(AppConfig.TAG, "[测试]RetinaFace engine initialized successfully")
|
||||
val raw = engine.detect(bitmap)
|
||||
Log.d(AppConfig.TAG, "[测试]Face detection result: ${raw.joinToString(", ")}")
|
||||
engine.release()
|
||||
|
||||
if (raw.isNotEmpty()) {
|
||||
val stride = when {
|
||||
raw.size % 15 == 0 -> 15
|
||||
raw.size % 5 == 0 -> 5
|
||||
else -> 0
|
||||
}
|
||||
|
||||
Log.d(AppConfig.TAG, "[测试]Stride: $stride, raw size: ${raw.size}")
|
||||
|
||||
if (stride > 0) {
|
||||
val faceCount = raw.size / stride
|
||||
Log.d(AppConfig.TAG, "[测试]Detected $faceCount faces")
|
||||
if (faceCount > 0) {
|
||||
val i = 0
|
||||
val lm = if (stride >= 15) raw.copyOfRange(i + 5, i + 15) else null
|
||||
val hasLm = lm?.all { it >= 0f } == true
|
||||
|
||||
val faceBox = FaceBox(
|
||||
left = raw[i],
|
||||
top = raw[i + 1],
|
||||
right = raw[i + 2],
|
||||
bottom = raw[i + 3],
|
||||
score = raw[i + 4],
|
||||
hasLandmarks = hasLm,
|
||||
landmarks = if (hasLm) lm else null
|
||||
)
|
||||
Log.d(AppConfig.TAG, "[测试]Created face box: $faceBox")
|
||||
return faceBox
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(AppConfig.TAG, "[测试]No faces detected in bitmap")
|
||||
}
|
||||
} else {
|
||||
Log.e(AppConfig.TAG, "[测试]Failed to initialize RetinaFace engine")
|
||||
}
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "[测试]Failed to detect face: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.digitalperson.onboard_testing
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.digitalperson.config.AppConfig
|
||||
import com.digitalperson.llm.LLMManager
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LLMSummaryTest(private val context: Context) {
|
||||
|
||||
private lateinit var llmManager: LLMManager
|
||||
|
||||
fun setLLMManager(manager: LLMManager) {
|
||||
this.llmManager = manager
|
||||
}
|
||||
|
||||
fun runTest(onResult: (String) -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
onResult("[测试] 开始本地大模型总结能力测试...")
|
||||
|
||||
// 测试用例 1: 简单对话总结
|
||||
val testCase1 = listOf(
|
||||
"助手: 下午好,小朋友,请问你叫什么名字?",
|
||||
"用户: 你好,我是张三",
|
||||
"助手: 你好张三,有什么可以帮你的吗?",
|
||||
"用户: 我想了解一下今天的天气",
|
||||
"助手: 今天天气晴朗,温度25度,适合户外活动"
|
||||
)
|
||||
|
||||
// 测试用例 2: 复杂对话总结
|
||||
val testCase2 = listOf(
|
||||
"用户: 我想学习编程",
|
||||
"助手: 好的,你想学习哪种编程语言?",
|
||||
"用户: 我对Python感兴趣",
|
||||
"助手: Python是一种很好的入门语言,你可以从基础语法开始学习",
|
||||
"用户: 有什么推荐的学习资源吗?",
|
||||
"助手: 你可以看官方文档,或者一些在线教程,比如Codecademy或Coursera"
|
||||
)
|
||||
|
||||
// 测试用例 3: 多轮对话总结
|
||||
val testCase3 = listOf(
|
||||
"用户: 你能帮我做一个番茄炒蛋吗?",
|
||||
"助手: 当然可以,需要准备番茄、鸡蛋、盐、糖等材料",
|
||||
"用户: 具体步骤是什么?",
|
||||
"助手: 1. 番茄切块,鸡蛋打散;2. 热锅倒油,炒鸡蛋;3. 加入番茄翻炒;4. 加盐糖调味",
|
||||
"用户: 需要多长时间?",
|
||||
"助手: 大约15-20分钟"
|
||||
)
|
||||
|
||||
// 执行测试
|
||||
testMultiAngleSummary("简单对话", testCase1, onResult)
|
||||
testMultiAngleSummary("复杂对话", testCase2, onResult)
|
||||
testMultiAngleSummary("多轮对话", testCase3, onResult)
|
||||
|
||||
onResult("[测试] 本地大模型总结能力测试完成")
|
||||
|
||||
} catch (e: Exception) {
|
||||
onResult("[测试] 测试过程中发生错误: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun testMultiAngleSummary(testName: String, messages: List<String>, onResult: (String) -> Unit) {
|
||||
try {
|
||||
onResult("[测试] 测试 $testName 多角度总结...")
|
||||
|
||||
val questions = listOf(
|
||||
"请从对话中提取用户的姓名,只返回姓名,如果没有提到姓名,请返回未知",
|
||||
"请总结对话,只返回总结的内容",
|
||||
"请从对话中提取用户的性别,只返回性别,如果没有提到性别,请返回未知"
|
||||
)
|
||||
|
||||
var completed = 0
|
||||
val results = mutableMapOf<String, String>()
|
||||
|
||||
questions.forEach { question ->
|
||||
val prompt = buildMultiAnglePrompt(messages, question)
|
||||
llmManager.generate(prompt) { answer ->
|
||||
results[question] = answer
|
||||
completed++
|
||||
|
||||
if (completed == questions.size) {
|
||||
// 所有问题都回答完成
|
||||
val summary = buildFinalSummary(results)
|
||||
onResult("[测试] $testName 多角度总结结果: $summary")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
onResult("[测试] $testName 测试失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMultiAnglePrompt(messages: List<String>, question: String): String {
|
||||
val messageText = messages.joinToString("\n")
|
||||
return """
|
||||
请根据以下对话回答问题:
|
||||
|
||||
对话内容:
|
||||
$messageText
|
||||
|
||||
问题:$question
|
||||
|
||||
回答:
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun buildFinalSummary(results: Map<String, String>): String {
|
||||
val summary = StringBuilder()
|
||||
results.forEach { (question, answer) ->
|
||||
summary.append("${question.substringAfter("提取")}: $answer\n")
|
||||
}
|
||||
return summary.toString()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user