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)
|
||||
// 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)
|
||||
|
||||
+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.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>
|
||||
|
||||
+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.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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user