Skip to content

Commit 2b1d550

Browse files
authored
Merge pull request #51 from david0154/feat/vision-search-multilang-fixes-8062784426717246400
feat: Add image processing, web search, and improve multilingual support
2 parents c183a94 + 1fd2fc3 commit 2b1d550

5 files changed

Lines changed: 140 additions & 63 deletions

File tree

app/src/main/kotlin/com/davidstudioz/david/MainActivity.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import androidx.compose.ui.text.font.FontWeight
3535
import androidx.compose.ui.unit.dp
3636
import androidx.compose.ui.unit.sp
3737
import androidx.lifecycle.lifecycleScope
38+
import com.davidstudioz.david.ai.ImageProcessor
3839
import com.davidstudioz.david.chat.ChatManager
3940
import com.davidstudioz.david.chat.ScriptureDownloadManager
4041
import com.davidstudioz.david.device.DeviceAccessManager
@@ -89,6 +90,7 @@ class MainActivity : ComponentActivity() {
8990

9091
// NEW: Scripture download manager
9192
private var scriptureDownloadManager: ScriptureDownloadManager? = null
93+
private var imageProcessor: ImageProcessor? = null
9294

9395
// UI State
9496
private var isListening by mutableStateOf(false)
@@ -106,6 +108,17 @@ class MainActivity : ComponentActivity() {
106108
// NEW: Chat & Download UI state
107109
private var showChatScreen by mutableStateOf(false)
108110
private var showDownloadDialog by mutableStateOf(false)
111+
private val imagePickerLauncher =
112+
registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
113+
uri?.let {
114+
lifecycleScope.launch {
115+
val result = imageProcessor?.processImage(it)
116+
if (result != null) {
117+
chatManager?.sendMessage(result)
118+
}
119+
}
120+
}
121+
}
109122

110123
private val permissionLauncher = registerForActivityResult(
111124
ActivityResultContracts.RequestMultiplePermissions()
@@ -201,6 +214,7 @@ class MainActivity : ComponentActivity() {
201214

202215
private fun initCoreComponents() {
203216
Log.d(TAG, "Initializing core components...")
217+
imageProcessor = ImageProcessor(this)
204218
resourceManager = DeviceResourceManager(this)
205219
resourceStatus = resourceManager?.getResourceStatus()
206220

@@ -487,7 +501,8 @@ class MainActivity : ComponentActivity() {
487501
onClearChat = {
488502
chatManager?.clearHistory()
489503
chatHistory = emptyList()
490-
}
504+
},
505+
onImageUpload = { imagePickerLauncher.launch("image/*") }
491506
)
492507
}
493508

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.davidstudioz.david.ai
2+
3+
import android.content.Context
4+
import android.graphics.BitmapFactory
5+
import android.net.Uri
6+
import android.util.Log
7+
import com.google.mlkit.vision.common.InputImage
8+
import com.google.mlkit.vision.label.ImageLabeling
9+
import com.google.mlkit.vision.label.defaults.ImageLabelerOptions
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.withContext
12+
13+
class ImageProcessor(private val context: Context) {
14+
15+
private val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)
16+
17+
suspend fun processImage(uri: Uri): String = withContext(Dispatchers.IO) {
18+
try {
19+
val inputStream = context.contentResolver.openInputStream(uri)
20+
val bitmap = BitmapFactory.decodeStream(inputStream)
21+
val image = InputImage.fromBitmap(bitmap, 0)
22+
23+
val result = labeler.process(image).continueWith { task ->
24+
if (task.isSuccessful) {
25+
val labels = task.result
26+
if (labels.isNotEmpty()) {
27+
labels.joinToString(", ") { it.text }
28+
} else {
29+
"I'm not sure what this is."
30+
}
31+
} else {
32+
"I couldn't analyze the image."
33+
}
34+
}
35+
36+
result.result
37+
} catch (e: Exception) {
38+
Log.e(TAG, "Error processing image", e)
39+
"I couldn't process the image."
40+
}
41+
}
42+
43+
companion object {
44+
private const val TAG = "ImageProcessor"
45+
}
46+
}

app/src/main/kotlin/com/davidstudioz/david/chat/ChatManager.kt

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import com.davidstudioz.david.voice.VoiceCommandProcessor
99
import com.davidstudioz.david.web.WebSearchEngine
1010
import com.davidstudioz.david.features.WeatherService
1111
import com.davidstudioz.david.features.NewsService
12+
import kotlinx.coroutines.CoroutineScope
1213
import kotlinx.coroutines.Dispatchers
13-
import kotlinx.coroutines.GlobalScope
14+
import kotlinx.coroutines.SupervisorJob
1415
import kotlinx.coroutines.launch
1516
import kotlinx.coroutines.withContext
1617
import java.io.File
@@ -32,8 +33,9 @@ data class ChatMessage(
3233
* ✅ No generic "I understand you're asking" for non-English
3334
* ✅ All features work in all languages
3435
*/
35-
class ChatManager(private val context: Context) {
36-
36+
class ChatManager(private val context:Context) {
37+
private val job = SupervisorJob()
38+
private val coroutineScope = CoroutineScope(Dispatchers.IO + job)
3739
private val messages = mutableListOf<ChatMessage>()
3840
private val modelsDir = File(context.filesDir, "david_models")
3941

@@ -56,7 +58,7 @@ class ChatManager(private val context: Context) {
5658
}
5759

5860
private fun loadBestAvailableModel() {
59-
GlobalScope.launch(Dispatchers.IO) {
61+
coroutineScope.launch {
6062
try {
6163
if (!modelsDir.exists()) {
6264
modelsDir.mkdirs()
@@ -110,8 +112,15 @@ class ChatManager(private val context: Context) {
110112
isCommand(correctedMessage) -> executeCommand(correctedMessage)
111113
isWeatherQuery(correctedMessage) -> getWeatherInfo(correctedMessage)
112114
isMotivationQuery(correctedMessage) -> getMotivation(correctedMessage, detectedLang)
113-
webSearch.needsWebSearch(correctedMessage) -> searchWeb(correctedMessage)
114-
isModelReady() -> generateWithModel(correctedMessage)
115+
isSearchQuery(correctedMessage) -> searchWeb(correctedMessage)
116+
isModelReady() -> {
117+
val modelResponse = generateWithModel(correctedMessage)
118+
if (modelResponse.isNotBlank() && modelResponse.length > 5) {
119+
modelResponse
120+
} else {
121+
searchWeb(correctedMessage)
122+
}
123+
}
115124
else -> generateSmartFallback(correctedMessage, detectedLang)
116125
}
117126

@@ -133,6 +142,13 @@ class ChatManager(private val context: Context) {
133142
)
134143
}
135144
}
145+
private fun isSearchQuery(message: String): Boolean {
146+
val lower = message.lowercase()
147+
return lower.startsWith("search for") || lower.startsWith("google") ||
148+
lower.startsWith("what is") || lower.startsWith("who is") ||
149+
lower.startsWith("where is") || lower.startsWith("when is") ||
150+
lower.startsWith("how to")
151+
}
136152

137153
private suspend fun generateWithModel(input: String): String = withContext(Dispatchers.IO) {
138154
return@withContext try {
@@ -437,6 +453,7 @@ class ChatManager(private val context: Context) {
437453

438454
fun release() {
439455
universalLoader.release()
456+
job.cancel()
440457
}
441458

442459
companion object {

app/src/main/kotlin/com/davidstudioz/david/chat/LanguageDetector.kt

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,50 +9,60 @@ import android.util.Log
99
* ✅ Keyword-based fallback
1010
*/
1111
class LanguageDetector {
12-
12+
private val languageKeywords = mapOf(
13+
"hi" to listOf("क्या", "है", "और", "में", "से", "नमस्ते"),
14+
"bn" to listOf("কি", "এবং", "থেকে", "নমস্কার"),
15+
"ta" to listOf("என்ன", "மற்றும்", "இருந்து", "வணக்கம்"),
16+
"te" to listOf("ఏమిటి", "మరియు", "నుండి", "నమస్కారం"),
17+
"mr" to listOf("काय", "आणि", "पासून", "नमस्कार"),
18+
"gu" to listOf("શું", "અને", "માંથી", "નમસ્તે"),
19+
"kn" to listOf("ಏನು", "ಮತ್ತು", "ಇಂದ", "ನಮಸ್ಕಾರ"),
20+
"ml" to listOf("എന്ത്", "കൂടാതെ", "നിന്ന്", "നമസ്കാരം"),
21+
"pa" to listOf("ਕੀ", "ਅਤੇ", "ਤੋਂ", "ਸਤ ਸ੍ਰੀ ਅਕਾਲ"),
22+
"or" to listOf("କଣ", "ଏବଂ", "ଠାରୁ", "ନମସ୍କାର"),
23+
"ur" to listOf("کیا", "اور", "سے", "آداب"),
24+
"as" to listOf("কি", "আৰু", "পৰা", "নমস্কাৰ"),
25+
"ks" to listOf("کیاہ", " تہٕ", "پؠٹھ", "آدا̄ب"),
26+
"sa" to listOf("किम्", "", "तः", "नमस्ते")
27+
)
28+
1329
fun detectLanguage(text: String): String {
1430
if (text.isBlank()) return "en"
15-
31+
1632
// Check for specific scripts
17-
val language = when {
18-
// Devanagari script (Hindi, Marathi, Sanskrit)
33+
val scriptBasedLanguage = when {
1934
text.any { it in '\u0900'..'\u097F' } -> detectDevanagari(text)
20-
21-
// Bengali script
2235
text.any { it in '\u0980'..'\u09FF' } -> "bn"
23-
24-
// Tamil script
2536
text.any { it in '\u0B80'..'\u0BFF' } -> "ta"
26-
27-
// Telugu script
2837
text.any { it in '\u0C00'..'\u0C7F' } -> "te"
29-
30-
// Gujarati script
3138
text.any { it in '\u0A80'..'\u0AFF' } -> "gu"
32-
33-
// Kannada script
3439
text.any { it in '\u0C80'..'\u0CFF' } -> "kn"
35-
36-
// Malayalam script
3740
text.any { it in '\u0D00'..'\u0D7F' } -> "ml"
38-
39-
// Punjabi script (Gurmukhi)
4041
text.any { it in '\u0A00'..'\u0A7F' } -> "pa"
41-
42-
// Odia script
4342
text.any { it in '\u0B00'..'\u0B7F' } -> "or"
44-
45-
// Urdu (uses Arabic script)
4643
text.any { it in '\u0600'..'\u06FF' } -> "ur"
47-
48-
// English (default)
4944
else -> "en"
5045
}
51-
52-
Log.d(TAG, "Detected language: $language for text: ${text.take(50)}")
53-
return language
46+
47+
val keywordBasedLanguage = detectLanguageWithKeywords(text)
48+
if (keywordBasedLanguage != "en") {
49+
return keywordBasedLanguage
50+
}
51+
52+
Log.d(TAG, "Detected language: $scriptBasedLanguage for text: ${text.take(50)}")
53+
return scriptBasedLanguage
5454
}
55-
55+
56+
private fun detectLanguageWithKeywords(text: String): String {
57+
val lowerText = text.lowercase()
58+
for ((lang, keywords) in languageKeywords) {
59+
if (keywords.any { it in lowerText }) {
60+
return lang
61+
}
62+
}
63+
return "en"
64+
}
65+
5666
private fun detectDevanagari(text: String): String {
5767
val lower = text.lowercase()
5868
return when {
@@ -64,7 +74,7 @@ class LanguageDetector {
6474
else -> "hi"
6575
}
6676
}
67-
77+
6878
fun getLanguageName(code: String): String {
6979
return when (code) {
7080
"en" -> "English"

app/src/main/kotlin/com/davidstudioz/david/voice/VoiceManager.kt

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -193,35 +193,24 @@ class VoiceManager(private val context: Context) {
193193
* Set language
194194
*/
195195
fun setLanguage(languageCode: String): Boolean {
196-
return try {
197-
currentLanguage = languageCode
198-
val locale = when (languageCode) {
199-
"hi" -> Locale("hi", "IN")
200-
"ta" -> Locale("ta", "IN")
201-
"te" -> Locale("te", "IN")
202-
"bn" -> Locale("bn", "IN")
203-
"mr" -> Locale("mr", "IN")
204-
"gu" -> Locale("gu", "IN")
205-
"kn" -> Locale("kn", "IN")
206-
"ml" -> Locale("ml", "IN")
207-
"pa" -> Locale("pa", "IN")
208-
else -> Locale.US
209-
}
210-
211-
val result = tts?.setLanguage(locale)
212-
val success = result != TextToSpeech.LANG_MISSING_DATA && result != TextToSpeech.LANG_NOT_SUPPORTED
213-
214-
if (success) {
215-
Log.d(TAG, "Language set to: $languageCode")
216-
} else {
217-
Log.e(TAG, "Language not supported: $languageCode")
196+
currentLanguage = languageCode
197+
val locale = Locale(languageCode)
198+
var ttsSuccess = false
199+
if (isTTSReady) {
200+
try {
201+
val result = tts?.setLanguage(locale)
202+
if (result != TextToSpeech.LANG_MISSING_DATA && result != TextToSpeech.LANG_NOT_SUPPORTED) {
203+
ttsSuccess = true
204+
Log.d(TAG, "TTS language set to: $languageCode")
205+
} else {
206+
Log.e(TAG, "TTS language not supported: $languageCode")
207+
}
208+
} catch (e: Exception) {
209+
Log.e(TAG, "Error setting TTS language", e)
218210
}
219-
220-
success
221-
} catch (e: Exception) {
222-
Log.e(TAG, "Error setting language", e)
223-
false
224211
}
212+
213+
return ttsSuccess
225214
}
226215

227216
fun isListening(): Boolean = isListening

0 commit comments

Comments
 (0)