diff --git a/app/src/main/java/de/ody/app/engine/AssistantEngine.kt b/app/src/main/java/de/ody/app/engine/AssistantEngine.kt index 0b8edbb..42eecd6 100644 --- a/app/src/main/java/de/ody/app/engine/AssistantEngine.kt +++ b/app/src/main/java/de/ody/app/engine/AssistantEngine.kt @@ -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 - * 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.UNLOADED) val runtimeState: StateFlow = _runtimeState.asStateFlow() + private val _currentModelId = MutableStateFlow(null) + val currentModelId: StateFlow = _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() diff --git a/app/src/main/java/de/ody/model/ModelManager.kt b/app/src/main/java/de/ody/model/ModelManager.kt index 008c155..2734e60 100644 --- a/app/src/main/java/de/ody/model/ModelManager.kt +++ b/app/src/main/java/de/ody/model/ModelManager.kt @@ -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)