diff --git a/amethyst/build.gradle b/amethyst/build.gradle index fb4ddb6d8..ae6d0f09e 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -349,7 +349,6 @@ dependencies { // On-device alt-text suggestions: genai image description (preferred, descriptive sentences) // with image-labeling as a keyword-join fallback for devices without AICore. playImplementation libs.google.mlkit.genai.image.description - playImplementation libs.google.mlkit.image.labeling // PushNotifications playImplementation platform(libs.firebase.bom) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index b852165f1..300f36eda 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -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.FeedPadding 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.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import java.io.Serializable @Composable fun ChatroomListFeedView( @@ -139,10 +138,11 @@ private fun FeedLoaded( } // Stable per-chatroom key — derived from chatroom identity, not the latest -// message id, so reorders move the row instead of recreating it. Uses a -// sealed wrapper around an existing String/RoomId/ChatroomKey to avoid the -// StringBuilder + concatenation allocations of a typed-prefix string key. -private sealed interface ChatroomLazyKey +// message id, so reorders move the row instead of recreating it. Compose +// stores LazyColumn item keys in a SaveableStateHolder, which on Android +// requires Bundle-storable types, so each variant is Serializable and only +// holds primitives. +private sealed interface ChatroomLazyKey : Serializable private data class MarmotChatroomLazyKey( val groupId: HexKey, @@ -153,11 +153,12 @@ private data class PublicChannelLazyKey( ) : ChatroomLazyKey private data class EphemeralChannelLazyKey( - val roomId: RoomId, + val id: String, + val relayUrl: String, ) : ChatroomLazyKey private data class PrivateChatLazyKey( - val key: ChatroomKey, + val users: HashSet, ) : ChatroomLazyKey private data class FallbackChatroomLazyKey( @@ -186,12 +187,15 @@ private fun chatroomLazyKey( } is EphemeralChatEvent -> { - event.roomId()?.let { EphemeralChannelLazyKey(it) } + event.roomId()?.let { EphemeralChannelLazyKey(it.id, it.relayUrl.url) } ?: FallbackChatroomLazyKey(item.idHex) } 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 -> { diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index ceb91302b..ccbba56bb 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -586,6 +586,8 @@ Közvetlen webcím használata Tartalmi leírás Egy kék csónak a fehér homokos tengerparton naplementekor + LLM által javasolt, szerkessze bátran + LLM-javaslatok eltüntetése Zap-típus Zap-típus minden lehetőséghez Nyilvános diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index e3515de0e..0ecb77c47 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -586,6 +586,8 @@ 使用直接链接 内容描述 日落时分,白色沙滩上的蓝色小船 + AI建议的,编辑我 + 忽略 AI 建议 打闪类型 所有选项的打闪类型 公开 diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt index 25d044b34..57d734845 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt @@ -29,14 +29,8 @@ import com.google.mlkit.genai.imagedescription.ImageDescriber import com.google.mlkit.genai.imagedescription.ImageDescriberOptions import com.google.mlkit.genai.imagedescription.ImageDescription 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.suspendCancellableCoroutine import kotlinx.coroutines.withContext -import kotlin.coroutines.resume /** * Unified alt-text suggestion service. @@ -47,15 +41,12 @@ import kotlin.coroutines.resume class MLKitImageLabelService( private val context: Context, ) { - private var labeler: ImageLabeler? = null private var describer: ImageDescriber? = null // 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. @Volatile private var cachedGenAiStatus: Int? = null - private fun ensureLabeler(): ImageLabeler = labeler ?: ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS).also { labeler = it } - private fun ensureDescriber(): ImageDescriber? = describer ?: try { @@ -66,26 +57,7 @@ class MLKitImageLabelService( null } - suspend fun labelImage(uri: Uri): List> = - 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) + suspend fun suggestAltText(uri: Uri): String? = describeWithGenAi(uri) private suspend fun describeWithGenAi(uri: Uri): String? = withContext(Dispatchers.IO) { @@ -100,20 +72,13 @@ class MLKitImageLabelService( .runInference(request) .get() .description - ?.trim() - ?.takeIf { it.isNotEmpty() } + .trim() + .takeIf { it.isNotEmpty() } } catch (_: Exception) { 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 // on-device describer downscales internally anyway, so a ~1024 px input is plenty. private fun loadDownscaledBitmap(uri: Uri): Bitmap? = @@ -136,14 +101,12 @@ class MLKitImageLabelService( ): Int { if (width <= 0 || height <= 0) return 1 var sample = 1 - var maxDim = maxOf(width, height) + val maxDim = maxOf(width, height) while (maxDim / sample > target) sample *= 2 return sample } fun close() { - labeler?.close() - labeler = null describer?.close() describer = null cachedGenAiStatus = null diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1dc29b301..c51f30efd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,7 +41,6 @@ genaiProofreading = "1.0.0-beta1" genaiPrompt = "1.0.0-beta2" genaiRewriting = "1.0.0-beta1" genaiImageDescription = "1.0.0-beta1" -imageLabeling = "16.0.0" languageId = "17.0.6" lifecycleRuntimeKtx = "2.10.0" 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-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-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-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" }