mmap21-final: loadModel() in AssistantEngine, Modell-Picker, Thinking-Indikator

This commit is contained in:
ODY Build 2026-07-21 09:21:29 +00:00
parent 274e73cb4d
commit 2dbd4f895b
2 changed files with 35 additions and 21 deletions

View file

@ -22,22 +22,7 @@ import kotlinx.coroutines.launch
import java.io.File
/**
* ODY AssistantEngine mmap12 / ODY-RUNTIME-STATEFLOW-BRIDGE-FIX-1
*
* Root Cause des bisherigen Bugs:
* val runtimeState: StateFlow<RuntimeState>
* get() = runtime.state
*
* collectAsState() in Compose abonniert den Flow beim ersten Compose-Lauf.
* Wenn runtime danach intern ausgetauscht wird (SimulationRuntime LlamaCppRuntime),
* weiß Compose nicht, dass es einen anderen Flow sammeln muss.
* Ergebnis: UI lauscht auf altem SimulationRuntime.state, Engine nutzt neue Runtime.
*
* Fix:
* - Stabiler engine-owned _runtimeState MutableStateFlow wird NIEMALS ausgetauscht
* - bindRuntime() forwarded newRuntime.state _runtimeState via runtimeStateJob
* - runtimeStateJob wird bei jedem Runtime-Wechsel gecancelt und neu gestartet
* - Lifecycle-Cleanup in release(): runtimeStateJob canceln, Runtime freigeben
* ODY AssistantEngine mmap21
*/
class AssistantEngine(private val context: Context) {
@ -57,8 +42,10 @@ class AssistantEngine(private val context: Context) {
private val _runtimeState = MutableStateFlow<RuntimeState>(RuntimeState.UNLOADED)
val runtimeState: StateFlow<RuntimeState> = _runtimeState.asStateFlow()
private val _currentModelId = MutableStateFlow<String?>(null)
val currentModelId: StateFlow<String?> = _currentModelId.asStateFlow()
// Job der newRuntime.state → _runtimeState forwarded
// Wird bei jedem Runtime-Wechsel gecancelt und neu gestartet
private var runtimeStateJob: Job? = null
val isSimulation: Boolean
@ -95,6 +82,7 @@ class AssistantEngine(private val context: Context) {
contextLength = 4096
)
)
_currentModelId.value = modelFile.nameWithoutExtension
}
} else {
Log.i(TAG, "start: Kein Modell → SimulationRuntime aktiv")
@ -107,6 +95,34 @@ class AssistantEngine(private val context: Context) {
scope.launch { runtime.unload() }
}
/**
* Wechselt das aktive Modell zur Laufzeit.
* Entlädt die aktuelle Runtime, bindet eine neue LlamaCppRuntime und lädt das neue Modell.
*/
fun loadModel(modelFile: File) {
scope.launch {
Log.i(TAG, "loadModel: ${modelFile.name}")
try {
_currentModelId.value = null
runtime.unload()
val llamaRuntime = LlamaCppRuntime(context)
bindRuntime(llamaRuntime)
llamaRuntime.loadModel(
modelPath = modelFile.absolutePath,
config = ModelConfig(
modelPath = modelFile.absolutePath,
modelId = modelFile.nameWithoutExtension,
contextLength = 4096
)
)
_currentModelId.value = modelFile.nameWithoutExtension
Log.i(TAG, "loadModel: ${modelFile.name} erfolgreich geladen")
} catch (e: Throwable) {
Log.e(TAG, "loadModel: Fehler beim Laden von ${modelFile.name}", e)
}
}
}
suspend fun generate(
request: GenerationRequest,
onToken: (String) -> Unit
@ -118,9 +134,7 @@ class AssistantEngine(private val context: Context) {
fun cancel() = runtime.cancel()
/**
* Vollständiges Lifecycle-Cleanup:
* - runtimeStateJob canceln (kein weiteres Forwarding)
* - Runtime via unload() freigeben
* Vollständiges Lifecycle-Cleanup.
*/
fun release() {
runtimeStateJob?.cancel()

View file

@ -58,7 +58,7 @@ class ModelManager(private val context: Context) {
fileName: String,
onProgress: (Long, Long) -> Unit = { _, _ -> },
): File? {
val safeName = fileName.replace(Regex("[^a-zA-Z0-9._\-]"), "_")
val safeName = fileName.replace(Regex("[^a-zA-Z0-9._ -]"), "_")
.let { if (it.endsWith(".gguf")) it else "$it.gguf" }
val partFile = File(modelsDir(), "$safeName.part")
val finalFile = File(modelsDir(), safeName)