Skip to content

Commit a77bf7e

Browse files
committed
ToolNeuron v2.0.0 — Character Cards, AI Memory, and RAG Pipeline Overhaul
1 parent fcfb3bf commit a77bf7e

5 files changed

Lines changed: 50 additions & 19 deletions

File tree

app/src/main/java/com/dark/tool_neuron/service/ModelDownloadService.kt

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.content.Intent
77
import android.content.pm.ServiceInfo
88
import android.os.Build
99
import android.os.IBinder
10+
import android.util.Log
1011
import androidx.core.app.NotificationCompat
1112
import androidx.core.app.ServiceCompat
1213
import com.dark.tool_neuron.data.AppSettingsDataStore
@@ -21,7 +22,6 @@ import com.dark.tool_neuron.models.table_schema.Model
2122
import com.dark.tool_neuron.models.table_schema.ModelConfig
2223
import com.dark.tool_neuron.worker.DiffusionConfig
2324
import com.dark.tool_neuron.worker.DiffusionInferenceParams
24-
import com.dark.tool_neuron.worker.ModelDataParser
2525
import kotlinx.coroutines.CoroutineScope
2626
import kotlinx.coroutines.Dispatchers
2727
import kotlinx.coroutines.Job
@@ -44,7 +44,7 @@ class ModelDownloadService : Service() {
4444

4545
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
4646
private val downloadJobs = ConcurrentHashMap<String, Job>()
47-
private var notificationIdCounter = NOTIFICATION_ID
47+
private val notificationIdCounter = java.util.concurrent.atomic.AtomicInteger(NOTIFICATION_ID)
4848

4949
private val notificationManager by lazy {
5050
getSystemService(NOTIFICATION_SERVICE) as NotificationManager
@@ -157,10 +157,14 @@ class ModelDownloadService : Service() {
157157
runOnCpu: Boolean,
158158
textEmbeddingSize: Int
159159
) {
160-
// Cancel existing download for this model if any
160+
// Skip if this model is already downloading
161+
if (downloadJobs[modelId]?.isActive == true) {
162+
Log.w("DownloadService", "Download already in progress for $modelId, skipping duplicate")
163+
return
164+
}
161165
downloadJobs[modelId]?.cancel()
162166

163-
val notificationId = ++notificationIdCounter
167+
val notificationId = notificationIdCounter.incrementAndGet()
164168
val job = serviceScope.launch {
165169
var tempFile: File? = null
166170
var extractTempDir: File? = null
@@ -562,10 +566,10 @@ class ModelDownloadService : Service() {
562566
textEmbeddingSize: Int
563567
) = withContext(Dispatchers.IO) {
564568
val repository = AppContainer.getModelRepository()
565-
val parser = ModelDataParser()
566-
567-
val checksum = parser.checksumSHA256(modelPath)
568569

570+
// Use the store model ID as primary key so the UI can match
571+
// installed models against store listings. SHA256 is still computed
572+
// for integrity but not used as the DB key.
569573
val providerType = when (modelType) {
570574
"SD" -> ProviderType.DIFFUSION
571575
"GGUF" -> ProviderType.GGUF
@@ -589,7 +593,7 @@ class ModelDownloadService : Service() {
589593
}
590594

591595
val model = Model(
592-
id = checksum,
596+
id = modelId,
593597
modelName = modelName,
594598
modelPath = modelPath,
595599
pathType = pathType,
@@ -614,7 +618,7 @@ class ModelDownloadService : Service() {
614618
)
615619
val inferenceParams = DiffusionInferenceParams()
616620
ModelConfig(
617-
modelId = checksum,
621+
modelId = modelId,
618622
modelLoadingParams = diffusionConfig.toJson(),
619623
modelInferenceParams = inferenceParams.toJson()
620624
)
@@ -633,16 +637,15 @@ class ModelDownloadService : Service() {
633637
}
634638
val ggufSchema = GgufEngineSchema(loadingParams = loadingParams)
635639
ModelConfig(
636-
modelId = checksum,
640+
modelId = modelId,
637641
modelLoadingParams = ggufSchema.toLoadingJson(),
638642
modelInferenceParams = ggufSchema.toInferenceJson()
639643
)
640644
}
641645

642646
ProviderType.TTS -> {
643-
// TTS models use simple config with voice/speed/language
644647
ModelConfig(
645-
modelId = checksum,
648+
modelId = modelId,
646649
modelLoadingParams = """{"type":"tts","useNNAPI":false}""",
647650
modelInferenceParams = """{"voice":"F1","speed":1.05,"steps":2,"language":"en"}"""
648651
)

app/src/main/java/com/dark/tool_neuron/ui/components/ModeToggleSwitch.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ fun ModeToggleSwitch(
8787
isSelected = !isImageMode,
8888
isEnabled = textModelLoaded,
8989
icon = TnIcons.Code,
90-
contentDescription = "Text mode",
90+
contentDescription = if (textModelLoaded) "Switch to text chat" else "Load a text model to enable chat",
9191
onClick = {
9292
if (!isImageMode) return@IconButton
9393
onModeChange(false)
@@ -99,7 +99,7 @@ fun ModeToggleSwitch(
9999
isSelected = isImageMode,
100100
isEnabled = imageModelLoaded,
101101
icon = TnIcons.Photo,
102-
contentDescription = "Image mode",
102+
contentDescription = if (imageModelLoaded) "Switch to image generation" else "Load an image model to enable",
103103
onClick = {
104104
if (isImageMode) return@IconButton
105105
onModeChange(true)

app/src/main/java/com/dark/tool_neuron/viewmodel/ChatViewModel.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,11 @@ class ChatViewModel @Inject constructor(
384384

385385
fun sendChat(prompt: String) {
386386
if (!isAnyTextModelLoaded) {
387-
reportError("Please load a text generation model first")
387+
val hint = if (LlmModelWorker.isDiffusionModelLoaded.value)
388+
"You have an image model loaded — switch to image mode, or load a text model for chat"
389+
else
390+
"Please load a text generation model first"
391+
reportError(hint)
388392
return
389393
}
390394
if (_isGenerating.value) return
@@ -2036,7 +2040,7 @@ class ChatViewModel @Inject constructor(
20362040
val modelDir = TTSManager.getModelDirectory()
20372041
if (modelDir == null) {
20382042
withContext(Dispatchers.Main) {
2039-
Toast.makeText(appContext, "Install the TTS model from Settings", Toast.LENGTH_SHORT).show()
2043+
Toast.makeText(appContext, "Download the TTS voice model from Model Store to enable speech", Toast.LENGTH_LONG).show()
20402044
}
20412045
return@launch
20422046
}

app/src/main/java/com/dark/tool_neuron/viewmodel/LLMModelViewModel.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,12 @@ class LLMModelViewModel @Inject constructor(
6767
private val _needsQnnSetup = MutableStateFlow(false)
6868
val needsQnnSetup: StateFlow<Boolean> = _needsQnnSetup.asStateFlow()
6969

70-
private fun isQnnRuntimeReady(): Boolean {
70+
private fun isQnnRuntimeReady(): Boolean = try {
7171
val runtimeDir = File(getApplication<Application>().filesDir, "runtime_libs/qnnlibs")
7272
val marker = File(runtimeDir, ".extracted")
73-
Log.d("QNN", runtimeDir.exists().toString())
74-
return marker.exists() && (runtimeDir.listFiles()?.size ?: 0) > 1
73+
marker.exists() && (runtimeDir.listFiles()?.size ?: 0) > 1
74+
} catch (_: Exception) {
75+
false
7576
}
7677

7778
fun onQnnSetupComplete() {

app/src/main/java/com/dark/tool_neuron/viewmodel/ModelStoreViewModel.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,19 @@ class ModelStoreViewModel @Inject constructor(
433433

434434
fun downloadModel(model: HuggingFaceModel) {
435435
val context = getApplication<Application>()
436+
437+
// Warn user if model is likely too large for their device
438+
val approxSizeMB = parseApproxSizeMB(model.approximateSize)
439+
if (approxSizeMB > 0) {
440+
val activityManager = context.getSystemService(android.content.Context.ACTIVITY_SERVICE) as android.app.ActivityManager
441+
val memInfo = android.app.ActivityManager.MemoryInfo()
442+
activityManager.getMemoryInfo(memInfo)
443+
val totalRamMB = (memInfo.totalMem / (1024 * 1024)).toInt()
444+
if (approxSizeMB > totalRamMB * 0.8) {
445+
_error.value = "Warning: This model (~${approxSizeMB}MB) may be too large for your device (${totalRamMB}MB RAM). It might fail to load."
446+
}
447+
}
448+
436449
val fileUrl = "https://huggingface.co/${model.fileUri}"
437450

438451
val intent = Intent(context, ModelDownloadService::class.java).apply {
@@ -449,6 +462,16 @@ class ModelStoreViewModel @Inject constructor(
449462
androidx.core.content.ContextCompat.startForegroundService(context, intent)
450463
}
451464

465+
private fun parseApproxSizeMB(sizeStr: String): Int {
466+
val cleaned = sizeStr.trim().uppercase()
467+
val number = cleaned.filter { it.isDigit() || it == '.' }.toDoubleOrNull() ?: return 0
468+
return when {
469+
cleaned.endsWith("GB") -> (number * 1024).toInt()
470+
cleaned.endsWith("MB") -> number.toInt()
471+
else -> 0
472+
}
473+
}
474+
452475
fun cancelDownload(modelId: String) {
453476
val context = getApplication<Application>()
454477
val intent = Intent(context, ModelDownloadService::class.java).apply {

0 commit comments

Comments
 (0)