Merge remote-tracking branch 'origin/main' into claude/audio-rooms-android-ui-PIpFN

This commit is contained in:
Claude
2026-04-27 15:52:35 +00:00
6 changed files with 22 additions and 54 deletions
-1
View File
@@ -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)
@@ -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<HexKey>,
) : 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 -> {
@@ -586,6 +586,8 @@
<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_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_explainer">Zap-típus minden lehetőséghez</string>
<string name="zap_type_public">Nyilvános</string>
@@ -586,6 +586,8 @@
<string name="use_direct_url">使用直接链接</string>
<string name="content_description">内容描述</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_explainer">所有选项的打闪类型</string>
<string name="zap_type_public">公开</string>
@@ -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<Pair<String, Float>> =
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
-2
View File
@@ -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" }