Skip to content

Commit fcfb3bf

Browse files
Siddhesh2377claude
andcommitted
fix: SIGILL crashes, theme crashes, 16KB page support, Android 10 compat
- Rebuild gguf_lib AAR with KleidiAI OFF (fixes SIGILL on non-SVE devices) - Fix MODULE target 16KB page alignment (add_link_options instead of CMAKE_SHARED_LINKER_FLAGS) - Add 16KB page flag to ums and system_encryptor CMakeLists (Pixel 10 compat) - Wrap dynamicColorScheme in try-catch for broken OEM ROMs (fixes Resources$NotFoundException) - Lazy singleton ManropeTypography (eliminates 16 allocations per recomposition) - Lower minSdk 30→29 (Android 10 support, fixes #74) - Rebuild ai_supertonic_tts AAR with 16KB alignment - Bump to v2.0.3 (versionCode 30) Closes #74 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2bbb494 commit fcfb3bf

File tree

12 files changed

+224
-34
lines changed

12 files changed

+224
-34
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ android {
1717

1818
defaultConfig {
1919
applicationId = "com.dark.tool_neuron"
20-
minSdk = 30
20+
minSdk = 29
2121
targetSdk = 36
22-
versionCode = 29
23-
versionName = "2.0.2-model-loading-fix"
22+
versionCode = 30
23+
versionName = "2.0.3"
2424
ndk {
2525
abiFilters += listOf("arm64-v8a", "x86_64")
2626
}

app/src/main/aidl/com/dark/tool_neuron/service/ILLMService.aidl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ interface ILLMService {
4242
void setThinkingEnabledGguf(boolean enabled);
4343
float getContextUsageGguf();
4444

45+
// Context window tracking
46+
String getContextInfoGguf(String prompt);
47+
48+
// Character engine
49+
boolean setPersonalityGguf(String personalityJson);
50+
boolean setMoodGguf(int mood);
51+
boolean setCustomMoodGguf(float tempMod, float topPMod, float repPenaltyMod);
52+
String getCharacterContextGguf();
53+
String buildPromptGguf(String userPrompt);
54+
boolean setUncensoredGguf(boolean enabled);
55+
boolean isUncensoredGguf();
56+
4557
// Upscaler
4658
void loadUpscaler(String modelPath, IModelLoadCallback callback);
4759
void releaseUpscaler();

app/src/main/java/com/dark/tool_neuron/activity/RagDataReaderActivity.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,8 @@ fun PasswordInputDialog(
732732
)
733733
}
734734

735+
private const val MAX_RAG_FILE_SIZE = 512L * 1024 * 1024 // 512 MB
736+
735737
suspend fun loadRagFile(
736738
filePath: String,
737739
password: String?,
@@ -744,6 +746,12 @@ suspend fun loadRagFile(
744746
return@withContext null
745747
}
746748

749+
// Guard against OOM — RAG files are loaded entirely into memory
750+
if (file.length() > MAX_RAG_FILE_SIZE) {
751+
android.util.Log.e("RagDataReader", "RAG file too large: ${file.length()} bytes (max $MAX_RAG_FILE_SIZE)")
752+
return@withContext null
753+
}
754+
747755
val graph = NeuronGraph(embeddingEngine, GraphSettings.DEFAULT)
748756

749757
if (isEncrypted && password != null) {
@@ -767,6 +775,9 @@ suspend fun loadRagFile(
767775
}
768776

769777
graph
778+
} catch (e: OutOfMemoryError) {
779+
android.util.Log.e("RagDataReader", "OOM loading RAG file (${File(filePath).length()} bytes)", e)
780+
null
770781
} catch (e: Exception) {
771782
android.util.Log.e("RagDataReader", "Failed to load RAG: ${e.message}", e)
772783
null

app/src/main/java/com/dark/tool_neuron/engine/GGUFEngine.kt

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,108 @@ class GGUFEngine {
339339
} catch (_: Exception) { 0f }
340340
}
341341

342+
// ── Context Window Tracking ──
343+
344+
fun getContextInfo(prompt: String? = null): com.dark.gguf_lib.ContextInfo {
345+
if (!engine.isLoaded) return com.dark.gguf_lib.ContextInfo(0, 0, 0, -1, -1)
346+
return try {
347+
engine.getContextInfo(prompt)
348+
} catch (_: Exception) { com.dark.gguf_lib.ContextInfo(0, 0, 0, -1, -1) }
349+
}
350+
351+
// ── Character Engine ──
352+
353+
private val characterEngine by lazy { com.dark.gguf_lib.CharacterEngine(engine) }
354+
355+
fun setPersonality(personalityJson: String): Boolean {
356+
if (!engine.isLoaded) return false
357+
return try {
358+
val j = org.json.JSONObject(personalityJson)
359+
characterEngine.setPersonality(com.dark.gguf_lib.Personality(
360+
name = j.optString("name", ""),
361+
persona = j.optString("persona", ""),
362+
temperature = j.optDouble("temperature", 0.7).toFloat(),
363+
topP = j.optDouble("topP", 0.9).toFloat(),
364+
repetitionPenalty = j.optDouble("repetitionPenalty", 1.1).toFloat(),
365+
creativity = j.optDouble("creativity", 0.5).toFloat(),
366+
verbosity = j.optDouble("verbosity", 0.5).toFloat(),
367+
formality = j.optDouble("formality", 0.5).toFloat(),
368+
topK = j.optInt("topK", -1),
369+
minP = j.optDouble("minP", -1.0).toFloat(),
370+
))
371+
true
372+
} catch (_: Exception) { false }
373+
}
374+
375+
fun setMood(mood: Int): Boolean {
376+
if (!engine.isLoaded) return false
377+
return try {
378+
characterEngine.setMood(com.dark.gguf_lib.Mood.entries[mood])
379+
true
380+
} catch (_: Exception) { false }
381+
}
382+
383+
fun setCustomMood(tempMod: Float, topPMod: Float, repPenaltyMod: Float): Boolean {
384+
if (!engine.isLoaded) return false
385+
return try {
386+
characterEngine.setCustomMood(tempMod, topPMod, repPenaltyMod)
387+
true
388+
} catch (_: Exception) { false }
389+
}
390+
391+
fun getCharacterContext(): String {
392+
if (!engine.isLoaded) return ""
393+
return try {
394+
characterEngine.getContext()
395+
} catch (_: Exception) { "" }
396+
}
397+
398+
fun buildPrompt(userPrompt: String): String {
399+
if (!engine.isLoaded) return userPrompt
400+
return try {
401+
characterEngine.buildPrompt(userPrompt)
402+
} catch (_: Exception) { userPrompt }
403+
}
404+
405+
fun setUncensored(enabled: Boolean): Boolean {
406+
if (!engine.isLoaded) return false
407+
return try {
408+
characterEngine.setUncensored(enabled)
409+
true
410+
} catch (_: Exception) { false }
411+
}
412+
413+
fun isUncensored(): Boolean {
414+
if (!engine.isLoaded) return false
415+
return try {
416+
characterEngine.isUncensored
417+
} catch (_: Exception) { false }
418+
}
419+
420+
// ── Activation Steering ──
421+
422+
fun calcVectors(prompt: String, onProgress: ((Float) -> Unit)? = null): FloatArray? {
423+
if (!engine.isLoaded) return null
424+
return try {
425+
characterEngine.calcVectors(prompt, onProgress)
426+
} catch (_: Exception) { null }
427+
}
428+
429+
fun applyVectors(data: FloatArray, strength: Float = 1.0f, ilStart: Int = -1, ilEnd: Int = -1): Boolean {
430+
if (!engine.isLoaded) return false
431+
return try {
432+
characterEngine.applyVectors(data, strength, ilStart, ilEnd)
433+
} catch (_: Exception) { false }
434+
}
435+
436+
fun clearVectors(): Boolean {
437+
if (!engine.isLoaded) return false
438+
return try {
439+
characterEngine.clearVectors()
440+
true
441+
} catch (_: Exception) { false }
442+
}
443+
342444
// ── VLM (Vision Language Model) ──
343445

344446
fun loadVlmProjector(path: String, threads: Int = 0): Boolean {

app/src/main/java/com/dark/tool_neuron/models/engine_schema/DecodingMetrics.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ data class DecodingMetrics(
1717
val modelSizeMB: Float = 0f,
1818
val contextSizeMB: Float = 0f,
1919
val peakMemoryMB: Float = 0f,
20-
val memoryUsagePercent: Float = 0f
20+
val memoryUsagePercent: Float = 0f,
21+
val contextTokensUsed: Int = 0,
22+
val contextTokensMax: Int = 0,
23+
val contextUsagePercent: Float = 0f,
2124
)
2225

2326
/** Convert library DecodingMetrics to local serializable version */

app/src/main/java/com/dark/tool_neuron/ui/screen/home/StatusStates.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,33 @@ internal fun GeneratingTextContent(state: AppState.GeneratingText, chatViewModel
287287
)
288288
}
289289
}
290+
// ── Context usage bar ──
291+
val contextUsage by chatViewModel.contextUsagePercent.collectAsStateWithLifecycle()
292+
if (contextUsage > 0f) {
293+
Row(
294+
modifier = Modifier.fillMaxWidth(),
295+
horizontalArrangement = Arrangement.spacedBy(Standards.SpacingSm),
296+
verticalAlignment = Alignment.CenterVertically
297+
) {
298+
LinearProgressIndicator(
299+
progress = { contextUsage },
300+
modifier = Modifier
301+
.weight(1f)
302+
.height(3.dp)
303+
.clip(RoundedCornerShape(2.dp)),
304+
color = if (contextUsage > 0.85f) MaterialTheme.colorScheme.error
305+
else MaterialTheme.colorScheme.tertiary,
306+
trackColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
307+
)
308+
Text(
309+
text = "${(contextUsage * 100).toInt()}%",
310+
style = MaterialTheme.typography.labelSmall,
311+
color = if (contextUsage > 0.85f) MaterialTheme.colorScheme.error
312+
else MaterialTheme.colorScheme.onSurfaceVariant
313+
)
314+
}
315+
}
316+
290317
LinearProgressIndicator(
291318
modifier = Modifier
292319
.fillMaxWidth()

app/src/main/java/com/dark/tool_neuron/ui/theme/Theme.kt

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,49 @@
11
package com.dark.tool_neuron.ui.theme
22

3+
import android.os.Build
34
import androidx.compose.foundation.isSystemInDarkTheme
45
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
56
import androidx.compose.material3.MaterialExpressiveTheme
67
import androidx.compose.material3.MotionScheme
78
import androidx.compose.material3.Typography
9+
import androidx.compose.material3.darkColorScheme
810
import androidx.compose.material3.dynamicDarkColorScheme
911
import androidx.compose.material3.dynamicLightColorScheme
12+
import androidx.compose.material3.lightColorScheme
1013
import androidx.compose.runtime.Composable
1114
import androidx.compose.ui.platform.LocalContext
1215

16+
// ── Static fallback schemes ──
17+
// Used for API < 31 and as a safety net for OEM ROMs that report API 31+
18+
// but don't implement the Monet color engine (some MIUI, custom ROMs, AOSP forks).
19+
20+
private val FallbackDarkColorScheme = darkColorScheme()
21+
private val FallbackLightColorScheme = lightColorScheme()
22+
23+
// ── Typography ──
24+
// Single instance with Manrope applied to all text styles (avoids 16 allocations per recomposition).
25+
26+
private val ManropeTypography: Typography by lazy {
27+
val base = Typography()
28+
base.copy(
29+
displayLarge = base.displayLarge.copy(fontFamily = ManropeFontFamily),
30+
displayMedium = base.displayMedium.copy(fontFamily = ManropeFontFamily),
31+
displaySmall = base.displaySmall.copy(fontFamily = ManropeFontFamily),
32+
headlineLarge = base.headlineLarge.copy(fontFamily = ManropeFontFamily),
33+
headlineMedium = base.headlineMedium.copy(fontFamily = ManropeFontFamily),
34+
headlineSmall = base.headlineSmall.copy(fontFamily = ManropeFontFamily),
35+
titleLarge = base.titleLarge.copy(fontFamily = ManropeFontFamily),
36+
titleMedium = base.titleMedium.copy(fontFamily = ManropeFontFamily),
37+
titleSmall = base.titleSmall.copy(fontFamily = ManropeFontFamily),
38+
bodyLarge = base.bodyLarge.copy(fontFamily = ManropeFontFamily),
39+
bodyMedium = base.bodyMedium.copy(fontFamily = ManropeFontFamily),
40+
bodySmall = base.bodySmall.copy(fontFamily = ManropeFontFamily),
41+
labelLarge = base.labelLarge.copy(fontFamily = ManropeFontFamily),
42+
labelMedium = base.labelMedium.copy(fontFamily = ManropeFontFamily),
43+
labelSmall = base.labelSmall.copy(fontFamily = ManropeFontFamily),
44+
)
45+
}
46+
1347
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
1448
@Composable
1549
fun NeuroVerseTheme(
@@ -18,34 +52,22 @@ fun NeuroVerseTheme(
1852
) {
1953
val context = LocalContext.current
2054

21-
val colorScheme = if (darkTheme) {
22-
dynamicDarkColorScheme(context)
55+
// Dynamic color requires Android 12+ (API 31). Older devices get Material3 defaults.
56+
// Try-catch guards against OEM ROMs that report API 31+ but lack Monet resources —
57+
// dynamicDarkColorScheme() throws Resources$NotFoundException on these devices.
58+
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
59+
try {
60+
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
61+
} catch (_: Exception) {
62+
if (darkTheme) FallbackDarkColorScheme else FallbackLightColorScheme
63+
}
2364
} else {
24-
dynamicLightColorScheme(context)
65+
if (darkTheme) FallbackDarkColorScheme else FallbackLightColorScheme
2566
}
2667

27-
// Define typography with Manrope as the base/default family
28-
val typography = Typography().copy(
29-
displayLarge = Typography().displayLarge.copy(fontFamily = ManropeFontFamily),
30-
displayMedium = Typography().displayMedium.copy(fontFamily = ManropeFontFamily),
31-
displaySmall = Typography().displaySmall.copy(fontFamily = ManropeFontFamily),
32-
headlineLarge = Typography().headlineLarge.copy(fontFamily = ManropeFontFamily),
33-
headlineMedium = Typography().headlineMedium.copy(fontFamily = ManropeFontFamily),
34-
headlineSmall = Typography().headlineSmall.copy(fontFamily = ManropeFontFamily),
35-
titleLarge = Typography().titleLarge.copy(fontFamily = ManropeFontFamily),
36-
titleMedium = Typography().titleMedium.copy(fontFamily = ManropeFontFamily),
37-
titleSmall = Typography().titleSmall.copy(fontFamily = ManropeFontFamily),
38-
bodyLarge = Typography().bodyLarge.copy(fontFamily = ManropeFontFamily),
39-
bodyMedium = Typography().bodyMedium.copy(fontFamily = ManropeFontFamily),
40-
bodySmall = Typography().bodySmall.copy(fontFamily = ManropeFontFamily),
41-
labelLarge = Typography().labelLarge.copy(fontFamily = ManropeFontFamily),
42-
labelMedium = Typography().labelMedium.copy(fontFamily = ManropeFontFamily),
43-
labelSmall = Typography().labelSmall.copy(fontFamily = ManropeFontFamily),
44-
)
45-
4668
MaterialExpressiveTheme(
4769
colorScheme = colorScheme,
48-
typography = typography,
70+
typography = ManropeTypography,
4971
motionScheme = MotionScheme.expressive(),
5072
content = content
5173
)

app/src/main/java/com/dark/tool_neuron/worker/SystemBackupManager.kt

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ class SystemBackupManager(private val context: Context) {
4747
private const val IV_LENGTH = 12
4848
private const val GCM_TAG_LENGTH = 128
4949
private const val DB_NAME = "llm_models_database"
50-
private const val MAX_MODEL_FILE_SIZE = 2L * 1024 * 1024 * 1024 // 2GB
50+
// Cap per-file size for in-memory backup. GGUF model files are typically
51+
// 1-10 GB and cannot be safely held in the Java heap. Diffusion config
52+
// files and small adapters (< 256 MB) are fine.
53+
private const val MAX_MODEL_FILE_SIZE = 256L * 1024 * 1024 // 256MB
5154
}
5255

5356
sealed class BackupProgress {
@@ -196,7 +199,7 @@ class SystemBackupManager(private val context: Context) {
196199
sizeBytes = if (modelFile.isDirectory) dirSize(modelFile) else modelFile.length()
197200
if (sizeBytes > MAX_MODEL_FILE_SIZE) {
198201
canBackup = false
199-
reason = "File too large (>${MAX_MODEL_FILE_SIZE / (1024 * 1024 * 1024)}GB)"
202+
reason = "File too large (>${MAX_MODEL_FILE_SIZE / (1024 * 1024)}MB)"
200203
} else {
201204
canBackup = true
202205
reason = ""
@@ -774,20 +777,28 @@ class SystemBackupManager(private val context: Context) {
774777
}
775778

776779
if (modelFile.isDirectory) {
777-
// Diffusion/TTS: walk directory
780+
// Diffusion/TTS: walk directory — only small config/adapter files
778781
modelFile.walkTopDown().filter { it.isFile }.forEach { file ->
779782
if (file.length() <= MAX_MODEL_FILE_SIZE) {
780-
val relativePath = file.relativeTo(modelsDir).path
781-
results.add(relativePath to file.readBytes())
783+
try {
784+
val relativePath = file.relativeTo(modelsDir).path
785+
results.add(relativePath to file.readBytes())
786+
} catch (e: OutOfMemoryError) {
787+
Log.e(TAG, "OOM reading model file: ${file.name} (${file.length()} bytes), skipping")
788+
}
782789
} else {
783790
Log.w(TAG, "Skipping oversized model file: ${file.name} (${file.length()} bytes)")
784791
}
785792
}
786793
} else {
787794
// GGUF: single file
788795
if (modelFile.length() <= MAX_MODEL_FILE_SIZE) {
789-
val relativePath = modelFile.relativeTo(modelsDir).path
790-
results.add(relativePath to modelFile.readBytes())
796+
try {
797+
val relativePath = modelFile.relativeTo(modelsDir).path
798+
results.add(relativePath to modelFile.readBytes())
799+
} catch (e: OutOfMemoryError) {
800+
Log.e(TAG, "OOM reading model file: ${modelFile.name} (${modelFile.length()} bytes), skipping")
801+
}
791802
} else {
792803
Log.w(TAG, "Skipping oversized model file: ${modelFile.name} (${modelFile.length()} bytes)")
793804
}

libs/ai_supertonic_tts-release.aar

-105 Bytes
Binary file not shown.

libs/gguf_lib-release.aar

7.19 MB
Binary file not shown.

0 commit comments

Comments
 (0)