Merge remote-tracking branch 'origin/main' into claude/audio-rooms-android-ui-PIpFN
This commit is contained in:
@@ -349,7 +349,6 @@ dependencies {
|
|||||||
// On-device alt-text suggestions: genai image description (preferred, descriptive sentences)
|
// On-device alt-text suggestions: genai image description (preferred, descriptive sentences)
|
||||||
// with image-labeling as a keyword-join fallback for devices without AICore.
|
// with image-labeling as a keyword-join fallback for devices without AICore.
|
||||||
playImplementation libs.google.mlkit.genai.image.description
|
playImplementation libs.google.mlkit.genai.image.description
|
||||||
playImplementation libs.google.mlkit.image.labeling
|
|
||||||
|
|
||||||
// PushNotifications
|
// PushNotifications
|
||||||
playImplementation platform(libs.firebase.bom)
|
playImplementation platform(libs.firebase.bom)
|
||||||
|
|||||||
+14
-10
@@ -48,13 +48,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderC
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatroomListFeedView(
|
fun ChatroomListFeedView(
|
||||||
@@ -139,10 +138,11 @@ private fun FeedLoaded(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stable per-chatroom key — derived from chatroom identity, not the latest
|
// Stable per-chatroom key — derived from chatroom identity, not the latest
|
||||||
// message id, so reorders move the row instead of recreating it. Uses a
|
// message id, so reorders move the row instead of recreating it. Compose
|
||||||
// sealed wrapper around an existing String/RoomId/ChatroomKey to avoid the
|
// stores LazyColumn item keys in a SaveableStateHolder, which on Android
|
||||||
// StringBuilder + concatenation allocations of a typed-prefix string key.
|
// requires Bundle-storable types, so each variant is Serializable and only
|
||||||
private sealed interface ChatroomLazyKey
|
// holds primitives.
|
||||||
|
private sealed interface ChatroomLazyKey : Serializable
|
||||||
|
|
||||||
private data class MarmotChatroomLazyKey(
|
private data class MarmotChatroomLazyKey(
|
||||||
val groupId: HexKey,
|
val groupId: HexKey,
|
||||||
@@ -153,11 +153,12 @@ private data class PublicChannelLazyKey(
|
|||||||
) : ChatroomLazyKey
|
) : ChatroomLazyKey
|
||||||
|
|
||||||
private data class EphemeralChannelLazyKey(
|
private data class EphemeralChannelLazyKey(
|
||||||
val roomId: RoomId,
|
val id: String,
|
||||||
|
val relayUrl: String,
|
||||||
) : ChatroomLazyKey
|
) : ChatroomLazyKey
|
||||||
|
|
||||||
private data class PrivateChatLazyKey(
|
private data class PrivateChatLazyKey(
|
||||||
val key: ChatroomKey,
|
val users: HashSet<HexKey>,
|
||||||
) : ChatroomLazyKey
|
) : ChatroomLazyKey
|
||||||
|
|
||||||
private data class FallbackChatroomLazyKey(
|
private data class FallbackChatroomLazyKey(
|
||||||
@@ -186,12 +187,15 @@ private fun chatroomLazyKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is EphemeralChatEvent -> {
|
is EphemeralChatEvent -> {
|
||||||
event.roomId()?.let { EphemeralChannelLazyKey(it) }
|
event.roomId()?.let { EphemeralChannelLazyKey(it.id, it.relayUrl.url) }
|
||||||
?: FallbackChatroomLazyKey(item.idHex)
|
?: FallbackChatroomLazyKey(item.idHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
is ChatroomKeyable -> {
|
is ChatroomKeyable -> {
|
||||||
PrivateChatLazyKey(event.chatroomKey(myPubKey))
|
// ChatroomKey.users may be a kotlinx PersistentOrderedSet, which
|
||||||
|
// is not Serializable. Copy into a HashSet so the key survives
|
||||||
|
// Bundle round-trips; Set equality stays order-independent.
|
||||||
|
PrivateChatLazyKey(HashSet(event.chatroomKey(myPubKey).users))
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -586,6 +586,8 @@
|
|||||||
<string name="use_direct_url">Közvetlen webcím használata</string>
|
<string name="use_direct_url">Közvetlen webcím használata</string>
|
||||||
<string name="content_description">Tartalmi leírás</string>
|
<string name="content_description">Tartalmi leírás</string>
|
||||||
<string name="content_description_example">Egy kék csónak a fehér homokos tengerparton naplementekor</string>
|
<string name="content_description_example">Egy kék csónak a fehér homokos tengerparton naplementekor</string>
|
||||||
|
<string name="ai_suggested_alt_text_hint">LLM által javasolt, szerkessze bátran</string>
|
||||||
|
<string name="ai_suggested_alt_text_dismiss">LLM-javaslatok eltüntetése</string>
|
||||||
<string name="zap_type">Zap-típus</string>
|
<string name="zap_type">Zap-típus</string>
|
||||||
<string name="zap_type_explainer">Zap-típus minden lehetőséghez</string>
|
<string name="zap_type_explainer">Zap-típus minden lehetőséghez</string>
|
||||||
<string name="zap_type_public">Nyilvános</string>
|
<string name="zap_type_public">Nyilvános</string>
|
||||||
|
|||||||
@@ -586,6 +586,8 @@
|
|||||||
<string name="use_direct_url">使用直接链接</string>
|
<string name="use_direct_url">使用直接链接</string>
|
||||||
<string name="content_description">内容描述</string>
|
<string name="content_description">内容描述</string>
|
||||||
<string name="content_description_example">日落时分,白色沙滩上的蓝色小船</string>
|
<string name="content_description_example">日落时分,白色沙滩上的蓝色小船</string>
|
||||||
|
<string name="ai_suggested_alt_text_hint">AI建议的,编辑我</string>
|
||||||
|
<string name="ai_suggested_alt_text_dismiss">忽略 AI 建议</string>
|
||||||
<string name="zap_type">打闪类型</string>
|
<string name="zap_type">打闪类型</string>
|
||||||
<string name="zap_type_explainer">所有选项的打闪类型</string>
|
<string name="zap_type_explainer">所有选项的打闪类型</string>
|
||||||
<string name="zap_type_public">公开</string>
|
<string name="zap_type_public">公开</string>
|
||||||
|
|||||||
+4
-41
@@ -29,14 +29,8 @@ import com.google.mlkit.genai.imagedescription.ImageDescriber
|
|||||||
import com.google.mlkit.genai.imagedescription.ImageDescriberOptions
|
import com.google.mlkit.genai.imagedescription.ImageDescriberOptions
|
||||||
import com.google.mlkit.genai.imagedescription.ImageDescription
|
import com.google.mlkit.genai.imagedescription.ImageDescription
|
||||||
import com.google.mlkit.genai.imagedescription.ImageDescriptionRequest
|
import com.google.mlkit.genai.imagedescription.ImageDescriptionRequest
|
||||||
import com.google.mlkit.vision.common.InputImage
|
|
||||||
import com.google.mlkit.vision.label.ImageLabeler
|
|
||||||
import com.google.mlkit.vision.label.ImageLabeling
|
|
||||||
import com.google.mlkit.vision.label.defaults.ImageLabelerOptions
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified alt-text suggestion service.
|
* Unified alt-text suggestion service.
|
||||||
@@ -47,15 +41,12 @@ import kotlin.coroutines.resume
|
|||||||
class MLKitImageLabelService(
|
class MLKitImageLabelService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
) {
|
) {
|
||||||
private var labeler: ImageLabeler? = null
|
|
||||||
private var describer: ImageDescriber? = null
|
private var describer: ImageDescriber? = null
|
||||||
|
|
||||||
// FeatureStatus is an Int enum. Cached per-instance — describer availability does not flip
|
// FeatureStatus is an Int enum. Cached per-instance — describer availability does not flip
|
||||||
// mid-session in practice, and one composer mount only needs to ask AICore once.
|
// mid-session in practice, and one composer mount only needs to ask AICore once.
|
||||||
@Volatile private var cachedGenAiStatus: Int? = null
|
@Volatile private var cachedGenAiStatus: Int? = null
|
||||||
|
|
||||||
private fun ensureLabeler(): ImageLabeler = labeler ?: ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS).also { labeler = it }
|
|
||||||
|
|
||||||
private fun ensureDescriber(): ImageDescriber? =
|
private fun ensureDescriber(): ImageDescriber? =
|
||||||
describer
|
describer
|
||||||
?: try {
|
?: try {
|
||||||
@@ -66,26 +57,7 @@ class MLKitImageLabelService(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun labelImage(uri: Uri): List<Pair<String, Float>> =
|
suspend fun suggestAltText(uri: Uri): String? = describeWithGenAi(uri)
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val image = InputImage.fromFilePath(context, uri)
|
|
||||||
val client = ensureLabeler()
|
|
||||||
suspendCancellableCoroutine { cont ->
|
|
||||||
client
|
|
||||||
.process(image)
|
|
||||||
.addOnSuccessListener { labels ->
|
|
||||||
cont.resume(labels.map { it.text to it.confidence })
|
|
||||||
}.addOnFailureListener {
|
|
||||||
cont.resume(emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun suggestAltText(uri: Uri): String? = describeWithGenAi(uri) ?: labelKeywords(uri)
|
|
||||||
|
|
||||||
private suspend fun describeWithGenAi(uri: Uri): String? =
|
private suspend fun describeWithGenAi(uri: Uri): String? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -100,20 +72,13 @@ class MLKitImageLabelService(
|
|||||||
.runInference(request)
|
.runInference(request)
|
||||||
.get()
|
.get()
|
||||||
.description
|
.description
|
||||||
?.trim()
|
.trim()
|
||||||
?.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun labelKeywords(uri: Uri): String? {
|
|
||||||
val labels = labelImage(uri)
|
|
||||||
val confident = labels.filter { it.second >= MIN_CONFIDENCE }.map { it.first }
|
|
||||||
if (confident.isEmpty()) return null
|
|
||||||
return confident.take(MAX_LABELS).joinToString(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Two-pass decode keeps a 12 MP camera shot from blowing past 40 MB of ARGB_8888 — the
|
// Two-pass decode keeps a 12 MP camera shot from blowing past 40 MB of ARGB_8888 — the
|
||||||
// on-device describer downscales internally anyway, so a ~1024 px input is plenty.
|
// on-device describer downscales internally anyway, so a ~1024 px input is plenty.
|
||||||
private fun loadDownscaledBitmap(uri: Uri): Bitmap? =
|
private fun loadDownscaledBitmap(uri: Uri): Bitmap? =
|
||||||
@@ -136,14 +101,12 @@ class MLKitImageLabelService(
|
|||||||
): Int {
|
): Int {
|
||||||
if (width <= 0 || height <= 0) return 1
|
if (width <= 0 || height <= 0) return 1
|
||||||
var sample = 1
|
var sample = 1
|
||||||
var maxDim = maxOf(width, height)
|
val maxDim = maxOf(width, height)
|
||||||
while (maxDim / sample > target) sample *= 2
|
while (maxDim / sample > target) sample *= 2
|
||||||
return sample
|
return sample
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
labeler?.close()
|
|
||||||
labeler = null
|
|
||||||
describer?.close()
|
describer?.close()
|
||||||
describer = null
|
describer = null
|
||||||
cachedGenAiStatus = null
|
cachedGenAiStatus = null
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ genaiProofreading = "1.0.0-beta1"
|
|||||||
genaiPrompt = "1.0.0-beta2"
|
genaiPrompt = "1.0.0-beta2"
|
||||||
genaiRewriting = "1.0.0-beta1"
|
genaiRewriting = "1.0.0-beta1"
|
||||||
genaiImageDescription = "1.0.0-beta1"
|
genaiImageDescription = "1.0.0-beta1"
|
||||||
imageLabeling = "16.0.0"
|
|
||||||
languageId = "17.0.6"
|
languageId = "17.0.6"
|
||||||
lifecycleRuntimeKtx = "2.10.0"
|
lifecycleRuntimeKtx = "2.10.0"
|
||||||
lightcompressor-enhanced = "2.2.1"
|
lightcompressor-enhanced = "2.2.1"
|
||||||
@@ -151,7 +150,6 @@ google-mlkit-genai-proofreading = { group = "com.google.mlkit", name = "genai-pr
|
|||||||
google-mlkit-genai-prompt = { group = "com.google.mlkit", name = "genai-prompt", version.ref = "genaiPrompt" }
|
google-mlkit-genai-prompt = { group = "com.google.mlkit", name = "genai-prompt", version.ref = "genaiPrompt" }
|
||||||
google-mlkit-genai-rewriting = { group = "com.google.mlkit", name = "genai-rewriting", version.ref = "genaiRewriting" }
|
google-mlkit-genai-rewriting = { group = "com.google.mlkit", name = "genai-rewriting", version.ref = "genaiRewriting" }
|
||||||
google-mlkit-genai-image-description = { group = "com.google.mlkit", name = "genai-image-description", version.ref = "genaiImageDescription" }
|
google-mlkit-genai-image-description = { group = "com.google.mlkit", name = "genai-image-description", version.ref = "genaiImageDescription" }
|
||||||
google-mlkit-image-labeling = { group = "com.google.android.gms", name = "play-services-mlkit-image-labeling", version.ref = "imageLabeling" }
|
|
||||||
google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" }
|
google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" }
|
||||||
google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" }
|
google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" }
|
||||||
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
|
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user