Skip to content

Commit 7b04398

Browse files
committed
fix(ai): resolve offline custom key token loss
Prevent token loss and hallucination in local models due to formatting and JNI bugs.
1 parent 57f47cb commit 7b04398

10 files changed

Lines changed: 76 additions & 35 deletions

File tree

app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ public void initialize() { // needs to be delayed for access to EmojiTabStrip, w
311311
androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, false));
312312
mSearchAdapter = new EmojiSearchAdapter(emoji -> {
313313
mKeyboardActionListener.onTextInput(emoji);
314+
addRecentKey(emoji);
314315
// Optionally close search or keep it open for multiple inputs?
315316
// restore standard behavior: stop search
316317
stopSearchMode();

app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import java.util.concurrent.TimeUnit
6262
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
6363
class DictionaryFacilitatorImpl : DictionaryFacilitator {
6464
private var mPrefs: SharedPreferences? = null
65+
private var mContext: Context? = null
6566
private var mEnabledDictionariesState: Map<String, Boolean> = emptyMap()
6667
private var dictionaryGroups = listOf(DictionaryGroup())
6768

@@ -78,6 +79,9 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator {
7879
private var changeFrom = ""
7980
private var changeTo = ""
8081

82+
private var mLoadedSuggestEmojis: Boolean = false
83+
private var mLoadedEmojiDictExists: Boolean = false
84+
8185
private val SPELLING_DICTIONARY_TYPES = arrayOf(
8286
Dictionary.TYPE_MAIN,
8387
Dictionary.TYPE_CONTACTS,
@@ -144,6 +148,12 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator {
144148
return false
145149
}
146150
}
151+
val ctx = mContext ?: return false
152+
val currentSuggestEmojis = Settings.getValues().mSuggestEmojis
153+
val currentEmojiDictExists = locales.any { helium314.keyboard.latin.utils.DictionaryInfoUtils.getCachedDictForLocaleAndType(it, Dictionary.TYPE_EMOJI, ctx) != null }
154+
if (currentSuggestEmojis != mLoadedSuggestEmojis || currentEmojiDictExists != mLoadedEmojiDictExists) {
155+
return false
156+
}
147157
val dictGroup = dictionaryGroups[0] // settings are the same for all groups
148158
return contacts == dictGroup.hasDict(Dictionary.TYPE_CONTACTS)
149159
&& apps == dictGroup.hasDict(Dictionary.TYPE_APPS)
@@ -165,6 +175,7 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator {
165175
listener: DictionaryInitializationListener?
166176
) {
167177
Log.i(TAG, "resetDictionaries, force reloading main dictionary: $forceReloadMainDictionary")
178+
mContext = context.applicationContext
168179
val prefs = context.prefs()
169180
mPrefs = prefs
170181
mEnabledDictionariesState = prefs.all.filterKeys { it.startsWith("pref_dict_enabled_") }
@@ -237,7 +248,11 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator {
237248

238249
// create new or re-use already loaded main dict
239250
val mainDict: Dictionary?
240-
if (forceReload || oldDictGroupForLocale == null
251+
val currentSuggestEmojis = Settings.getValues().mSuggestEmojis
252+
val currentEmojiDictExists = helium314.keyboard.latin.utils.DictionaryInfoUtils.getCachedDictForLocaleAndType(locale, Dictionary.TYPE_EMOJI, context) != null
253+
val forceReloadMain = forceReload || (currentSuggestEmojis != mLoadedSuggestEmojis) || (currentEmojiDictExists != mLoadedEmojiDictExists)
254+
255+
if (forceReloadMain || oldDictGroupForLocale == null
241256
|| !oldDictGroupForLocale.hasDict(Dictionary.TYPE_MAIN)
242257
) {
243258
mainDict = null // null main dicts will be loaded later in asyncReloadUninitializedMainDictionaries
@@ -276,6 +291,8 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator {
276291
scope.launch {
277292
try {
278293
val useEmojiDict = Settings.getValues().mSuggestEmojis
294+
mLoadedSuggestEmojis = useEmojiDict
295+
mLoadedEmojiDictExists = locales.any { helium314.keyboard.latin.utils.DictionaryInfoUtils.getCachedDictForLocaleAndType(it, Dictionary.TYPE_EMOJI, context) != null }
279296
val dictGroupsWithNewMainDict = locales.mapNotNull {
280297
val dictionaryGroup = findDictionaryGroupWithLocale(dictionaryGroups, it)
281298
if (dictionaryGroup == null) {

app/src/main/java/helium314/keyboard/latin/LatinIME.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,8 +1714,8 @@ public void pickSuggestionManually(final SuggestedWordInfo suggestionInfo) {
17141714
mKeyboardSwitcher.clearHandwritingCanvas();
17151715
}
17161716

1717-
if (suggestionInfo.mSourceDict != null && helium314.keyboard.latin.dictionary.Dictionary.TYPE_EMOJI
1718-
.equals(suggestionInfo.mSourceDict.mDictType)) {
1717+
if (suggestionInfo.isEmoji() || (suggestionInfo.mSourceDict != null && helium314.keyboard.latin.dictionary.Dictionary.TYPE_EMOJI
1718+
.equals(suggestionInfo.mSourceDict.mDictType))) {
17191719
final helium314.keyboard.keyboard.emoji.EmojiPalettesView emojiView = mKeyboardSwitcher
17201720
.getEmojiPalettesView();
17211721
if (emojiView != null) {

app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3489,35 +3489,43 @@ private void handleCustomAIKey(int index) {
34893489
final android.content.SharedPreferences prefs = helium314.keyboard.latin.utils.DeviceProtectedUtils
34903490
.getSharedPreferences(mLatinIME);
34913491
String prompt = prefs.getString("pref_custom_ai_prompt_" + index, "");
3492-
String systemInstruction = "";
3492+
StringBuilder systemInstructionBuilder = new StringBuilder();
34933493
boolean shouldAppend = false;
34943494

34953495
// Keyword parsing for system instructions / personas
34963496
if (prompt.contains("#editor")) {
3497-
systemInstruction = " You are a text editor tool. Output ONLY the edited text. Do not add any conversational filler.";
3497+
systemInstructionBuilder.append(" You are a text editor tool. Output ONLY the edited text. Do not add any conversational filler.");
34983498
prompt = prompt.replace("#editor", "").trim();
3499-
} else if (prompt.contains("#outputonly")) {
3500-
systemInstruction = " Output ONLY the result. Do not add introductions or explanations.";
3499+
}
3500+
if (prompt.contains("#outputonly")) {
3501+
systemInstructionBuilder.append(" Output ONLY the result. Do not add introductions or explanations.");
35013502
prompt = prompt.replace("#outputonly", "").trim();
3502-
} else if (prompt.contains("#proofread")) {
3503-
systemInstruction = " You are a proofreader. Fix grammar and spelling errors. Output ONLY the fixed text.";
3503+
}
3504+
if (prompt.contains("#proofread")) {
3505+
systemInstructionBuilder.append(" You are a proofreader. Fix grammar and spelling errors. Output ONLY the fixed text.");
35043506
prompt = prompt.replace("#proofread", "").trim();
3505-
} else if (prompt.contains("#paraphrase")) {
3506-
systemInstruction = " You are a paraphrasing tool. Rewrite the text using different words while keeping the meaning. Output ONLY the result.";
3507+
}
3508+
if (prompt.contains("#paraphrase")) {
3509+
systemInstructionBuilder.append(" You are a paraphrasing tool. Rewrite the text using different words while keeping the meaning. Output ONLY the result.");
35073510
prompt = prompt.replace("#paraphrase", "").trim();
3508-
} else if (prompt.contains("#summarize")) {
3509-
systemInstruction = " You are a summarizer. Provide a concise summary of the text. Output ONLY the summary.";
3511+
}
3512+
if (prompt.contains("#summarize")) {
3513+
systemInstructionBuilder.append(" You are a summarizer. Provide a concise summary of the text. Output ONLY the summary.");
35103514
prompt = prompt.replace("#summarize", "").trim();
3511-
} else if (prompt.contains("#expand")) {
3512-
systemInstruction = " You are a creative writing assistant. Expand on the text with more details. Output ONLY the result.";
3515+
}
3516+
if (prompt.contains("#expand")) {
3517+
systemInstructionBuilder.append(" You are a creative writing assistant. Expand on the text with more details. Output ONLY the result.");
35133518
prompt = prompt.replace("#expand", "").trim();
3514-
} else if (prompt.contains("#toneshift")) {
3515-
systemInstruction = " You are a tone modifier. Adjust the tone as requested. Output ONLY the result.";
3519+
}
3520+
if (prompt.contains("#toneshift")) {
3521+
systemInstructionBuilder.append(" You are a tone modifier. Adjust the tone as requested. Output ONLY the result.");
35163522
prompt = prompt.replace("#toneshift", "").trim();
3517-
} else if (prompt.contains("#generate")) {
3518-
systemInstruction = " You are a creative content generator. Output ONLY the generated content.";
3523+
}
3524+
if (prompt.contains("#generate")) {
3525+
systemInstructionBuilder.append(" You are a creative content generator. Output ONLY the generated content.");
35193526
prompt = prompt.replace("#generate", "").trim();
35203527
}
3528+
String systemInstruction = systemInstructionBuilder.toString();
35213529

35223530
// Input handling keywords
35233531
if (prompt.contains("#append")) {

app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,20 +272,22 @@ enum class ToolbarMode {
272272
val toolbarKeyStrings = entries.associateWithTo(EnumMap(ToolbarKey::class.java)) { it.toString().lowercase(Locale.US) }
273273

274274
private val excludedKeys by lazy {
275-
val customAiKeys = if (BuildConfig.FLAVOR != "standard" && BuildConfig.FLAVOR != "standardOptimised")
275+
val customAiKeys = if (BuildConfig.FLAVOR != "standard" && BuildConfig.FLAVOR != "standardOptimised" && BuildConfig.FLAVOR != "offline")
276276
ToolbarKey.entries.filter { it.name.startsWith("CUSTOM_AI_") }
277277
else emptyList()
278278
val otherKeys = if (BuildConfig.FLAVOR == "offlinelite")
279-
listOf(CLOSE_HISTORY, PROOFREAD, TRANSLATE, CLIPBOARD_SEARCH)
279+
listOf(CLOSE_HISTORY, PROOFREAD, TRANSLATE, CLIPBOARD_SEARCH, HANDWRITING)
280+
else if (BuildConfig.FLAVOR == "offline")
281+
listOf(CLOSE_HISTORY, CLIPBOARD_SEARCH, HANDWRITING)
280282
else
281283
listOf(CLOSE_HISTORY, CLIPBOARD_SEARCH)
282284
customAiKeys + otherKeys
283285
}
284286

285287
val defaultToolbarPref by lazy {
286288
val default = when (helium314.keyboard.latin.BuildConfig.FLAVOR) {
287-
"offline" -> listOf(SETTINGS, VOICE, CLIPBOARD, HANDWRITING, UNDO, INCOGNITO, COPY, PASTE, PROOFREAD, TRANSLATE)
288-
"offlinelite" -> listOf(SETTINGS, VOICE, CLIPBOARD, HANDWRITING, UNDO, INCOGNITO, COPY, PASTE)
289+
"offline" -> listOf(SETTINGS, VOICE, CLIPBOARD, CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, UNDO, INCOGNITO, COPY, PASTE, PROOFREAD, TRANSLATE)
290+
"offlinelite" -> listOf(SETTINGS, VOICE, CLIPBOARD, UNDO, INCOGNITO, COPY, PASTE)
289291
else -> listOf(SETTINGS, VOICE, CLIPBOARD, HANDWRITING, CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, UNDO, PROOFREAD, TRANSLATE, INCOGNITO, TOUCHPAD, FLOATING, NUMPAD, COPY, PASTE, SELECT_ALL)
290292
}
291293

@@ -377,7 +379,8 @@ private fun getEnabledToolbarKeys(prefs: SharedPreferences, pref: String, defaul
377379
val split = it.split(Separators.KV)
378380
if (split.last() == "true") {
379381
try {
380-
ToolbarKey.valueOf(split.first())
382+
val key = ToolbarKey.valueOf(split.first())
383+
if (key in excludedKeys) null else key
381384
} catch (_: IllegalArgumentException) {
382385
null
383386
}

app/src/main/java/helium314/keyboard/settings/screens/AIIntegrationScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ private fun StandardAIIntegrationScreen(onClickBack: () -> Unit) {
9696
@Composable
9797
private fun OfflineAIIntegrationScreen(onClickBack: () -> Unit) {
9898
val items = listOf(
99+
SettingsWithoutKey.CUSTOM_AI_KEYS,
99100
SettingsWithoutKey.OFFLINE_MODEL_PATH,
100101
SettingsWithoutKey.OFFLINE_KEEP_MODEL_LOADED
101102
)

app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ fun createAdvancedSettings(context: Context) = listOfNotNull(
537537
)
538538
}
539539
},
540-
if (BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardOptimised") Setting(context, SettingsWithoutKey.CUSTOM_AI_KEYS, R.string.custom_ai_keys_title, R.string.custom_ai_keys_summary) {
540+
if (BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardOptimised" || BuildConfig.FLAVOR == "offline") Setting(context, SettingsWithoutKey.CUSTOM_AI_KEYS, R.string.custom_ai_keys_title, R.string.custom_ai_keys_summary) {
541541
Preference(
542542
name = it.title,
543543
description = it.description,

app/src/main/java/helium314/keyboard/settings/screens/LibrariesHubScreen.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import helium314.keyboard.settings.preferences.LoadGestureLibPreference
2929
import helium314.keyboard.settings.preferences.LoadEmojiLibPreference
3030
import helium314.keyboard.settings.preferences.LoadHandwritingPluginPreference
3131
import helium314.keyboard.latin.handwriting.HandwritingLoader
32+
import helium314.keyboard.latin.BuildConfig
3233
import helium314.keyboard.latin.common.Links
3334
import helium314.keyboard.settings.preferences.Preference
3435
import androidx.compose.ui.platform.LocalUriHandler
@@ -95,13 +96,15 @@ fun LibrariesHubScreen(
9596
)
9697

9798
// Handwriting Input Plugin
98-
var handwritingInstalled by remember { mutableStateOf(HandwritingLoader.hasPlugin(context)) }
99-
LoadHandwritingPluginPreference(
100-
title = stringResource(R.string.libraries_hub_handwriting_title),
101-
summary = if (handwritingInstalled) stringResource(R.string.libraries_status_active) else stringResource(R.string.libraries_status_not_installed),
102-
icon = R.drawable.ic_edit,
103-
onSuccess = { handwritingInstalled = HandwritingLoader.hasPlugin(context) }
104-
)
99+
if (BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardOptimised") {
100+
var handwritingInstalled by remember { mutableStateOf(HandwritingLoader.hasPlugin(context)) }
101+
LoadHandwritingPluginPreference(
102+
title = stringResource(R.string.libraries_hub_handwriting_title),
103+
summary = if (handwritingInstalled) stringResource(R.string.libraries_status_active) else stringResource(R.string.libraries_status_not_installed),
104+
icon = R.drawable.ic_edit,
105+
onSuccess = { handwritingInstalled = HandwritingLoader.hasPlugin(context) }
106+
)
107+
}
105108

106109
// Documentation & Features
107110
val uriHandler = LocalUriHandler.current

app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ fun createToolbarSettings(context: Context): List<Setting> {
9797
val filter = { name: String ->
9898
val lowerName = name.lowercase()
9999
when {
100-
lowerName.startsWith("custom_ai_") -> BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardOptimised"
100+
lowerName.startsWith("custom_ai_") -> BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardOptimised" || BuildConfig.FLAVOR == "offline"
101+
lowerName == "handwriting" -> BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardOptimised"
101102
lowerName in listOf("proofread", "translate", "clipboard_search") -> BuildConfig.FLAVOR != "offlinelite"
102103
else -> true
103104
}

app/src/offline/java/helium314/keyboard/latin/utils/ProofreadService.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ class ProofreadService(private val context: Context) {
381381
builder += "Input: $text\nOutput:"
382382
builder
383383
} else {
384-
"Instruction: ${systemPrompt.trim()}\nInput: $text\nOutput:"
384+
"Instruction: ${systemPrompt.trim()}\n\nInput: $text\nOutput:"
385385
}
386386
} else {
387387
// Default proofreading with few-shot examples for better local model guidance
@@ -456,6 +456,13 @@ class ProofreadService(private val context: Context) {
456456
}
457457
}
458458

459+
// Also truncate at any newline followed by a potential template header (e.g., "\nDraft email:", "\nCorrection:")
460+
val headerRegex = Regex("\\n[a-zA-Z0-9 ]+:")
461+
val match = headerRegex.find(cleanedOutput)
462+
if (match != null) {
463+
cleanedOutput = cleanedOutput.substring(0, match.range.first).trim()
464+
}
465+
459466
// Also strip common prefixes that the model might generate or echo
460467
val prefixesToStrip = listOf(
461468
"Output:", "Corrected:", "Translation:", "Response:", "Result:",
@@ -544,7 +551,7 @@ class ProofreadService(private val context: Context) {
544551
// Build parameters map
545552
val params = mutableMapOf<String, Any>(
546553
"prompt" to prompt,
547-
"emit_partial_completion" to showThinking,
554+
"emit_partial_completion" to true,
548555
"temperature" to temp.toDouble(),
549556
"top_p" to topP.toDouble(),
550557
"top_k" to topK,

0 commit comments

Comments
 (0)