diff --git a/installer/default_config/config.default.toml b/installer/default_config/config.default.toml index a5f3f431..c181785f 100644 --- a/installer/default_config/config.default.toml +++ b/installer/default_config/config.default.toml @@ -54,6 +54,23 @@ cloud_candidates = true # TODO: 极简模式。极简模式下,候选窗口使用 d2d 绘制,界面只作展示候选项,无其他功能。 clean_mode = false +# +# 整句联想:多音节输入时把整串拼音解码成一句,四个来源各一个开关,另有去重补位开关,默认全关, +# 打开后才会在候选里出现对应整句。词格/Google 是传统 ngram 来源;神经两档是 +# chinese-ime-lm 的 Transformer,还需模型文件在数据目录里,desktop 更准也更慢。 +# +[association] +# 词格整句联想(kenlm 三元模型打分) +sentence_wordlattice = false +# Google 解码器整句联想 +sentence_google = false +# 神经网络模型整句(效果优先 / desktop):内部生成最多 12 条 Trigram 备选并挑选,不依赖 sentence_wordlattice 开关 +sentence_neural_desktop = false +# 神经网络模型整句(速度优先 / keyboard):同上;与 desktop 同时开启时各自独立给出结果 +sentence_neural_keyboard = false +# 某个模型的首选与已有候选重复时,改为显示该模型排名最高的下一条不同结果 +sentence_show_next_on_duplicate = false + # # 腾讯云机器翻译(竖排候选项本地词库未命中时的补全) # diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 0c7392e8..23ea5940 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -54,6 +54,23 @@ cloud_candidates = true # TODO: 极简模式。极简模式下,候选窗口使用 d2d 绘制,界面只作展示候选项,无其他功能。 clean_mode = false +# +# 整句联想:多音节输入时把整串拼音解码成一句,四个来源各一个开关,另有去重补位开关,默认全关, +# 打开后才会在候选里出现对应整句。词格/Google 是传统 ngram 来源;神经两档是 +# chinese-ime-lm 的 Transformer,还需模型文件在数据目录里,desktop 更准也更慢。 +# +[association] +# 词格整句联想(kenlm 三元模型打分) +sentence_wordlattice = false +# Google 解码器整句联想 +sentence_google = false +# 神经网络模型整句(效果优先 / desktop):内部生成最多 12 条 Trigram 备选并挑选,不依赖 sentence_wordlattice 开关 +sentence_neural_desktop = false +# 神经网络模型整句(速度优先 / keyboard):同上;与 desktop 同时开启时各自独立给出结果 +sentence_neural_keyboard = false +# 某个模型的首选与已有候选重复时,改为显示该模型排名最高的下一条不同结果 +sentence_show_next_on_duplicate = false + # # 腾讯云机器翻译(候选项本地词库未命中时的补全) # diff --git a/server/src/config/ime_config.cpp b/server/src/config/ime_config.cpp index 3b08b4da..52e3ef60 100644 --- a/server/src/config/ime_config.cpp +++ b/server/src/config/ime_config.cpp @@ -122,6 +122,12 @@ bool g_english_candidates_enabled = false; bool g_candidate_translations_enabled = true; int g_english_mixed_input_min_chars = kEnglishMixedInputMinCharsDefault; bool g_cloud_candidates_enabled = true; +// 整句候选来源与去重补位开关,默认全关。 +bool g_assoc_sentence_wordlattice = false; +bool g_assoc_sentence_google = false; +bool g_assoc_sentence_neural_desktop = false; +bool g_assoc_sentence_neural_keyboard = false; +bool g_assoc_sentence_show_next_on_duplicate = false; bool g_emoji_mixed_input_enabled = false; bool g_kaomoji_mixed_input_enabled = false; bool g_unicode_mode_enabled = true; @@ -1057,6 +1063,11 @@ bool LoadImeConfig() : kEnglishMixedInputMinCharsDefault; } g_cloud_candidates_enabled = tbl["general"]["cloud_candidates"].value_or(true); + g_assoc_sentence_wordlattice = tbl["association"]["sentence_wordlattice"].value_or(false); + g_assoc_sentence_google = tbl["association"]["sentence_google"].value_or(false); + g_assoc_sentence_neural_desktop = tbl["association"]["sentence_neural_desktop"].value_or(false); + g_assoc_sentence_neural_keyboard = tbl["association"]["sentence_neural_keyboard"].value_or(false); + g_assoc_sentence_show_next_on_duplicate = tbl["association"]["sentence_show_next_on_duplicate"].value_or(false); g_emoji_mixed_input_enabled = tbl["general"]["emoji_mixed_input"].value_or(false); g_kaomoji_mixed_input_enabled = tbl["general"]["kaomoji_mixed_input"].value_or(false); g_unicode_mode_enabled = tbl["utility"]["unicode_mode"].value_or(true); @@ -3387,6 +3398,77 @@ bool SetConfiguredCloudCandidatesEnabled(bool enabled) return true; } +// 整句候选来源与去重补位开关的 getter / setter。写入 [association] 段,值域为布尔。 +bool GetConfiguredAssocSentenceWordLattice() +{ + return g_assoc_sentence_wordlattice; +} +bool SetConfiguredAssocSentenceWordLattice(bool enabled) +{ + if (!WriteConfiguredValue("association", "sentence_wordlattice", enabled ? "true" : "false")) + { + return false; + } + g_assoc_sentence_wordlattice = enabled; + return true; +} + +bool GetConfiguredAssocSentenceGoogle() +{ + return g_assoc_sentence_google; +} +bool SetConfiguredAssocSentenceGoogle(bool enabled) +{ + if (!WriteConfiguredValue("association", "sentence_google", enabled ? "true" : "false")) + { + return false; + } + g_assoc_sentence_google = enabled; + return true; +} + +bool GetConfiguredAssocSentenceNeuralDesktop() +{ + return g_assoc_sentence_neural_desktop; +} +bool SetConfiguredAssocSentenceNeuralDesktop(bool enabled) +{ + if (!WriteConfiguredValue("association", "sentence_neural_desktop", enabled ? "true" : "false")) + { + return false; + } + g_assoc_sentence_neural_desktop = enabled; + return true; +} + +bool GetConfiguredAssocSentenceNeuralKeyboard() +{ + return g_assoc_sentence_neural_keyboard; +} +bool SetConfiguredAssocSentenceNeuralKeyboard(bool enabled) +{ + if (!WriteConfiguredValue("association", "sentence_neural_keyboard", enabled ? "true" : "false")) + { + return false; + } + g_assoc_sentence_neural_keyboard = enabled; + return true; +} + +bool GetConfiguredAssocSentenceShowNextOnDuplicate() +{ + return g_assoc_sentence_show_next_on_duplicate; +} +bool SetConfiguredAssocSentenceShowNextOnDuplicate(bool enabled) +{ + if (!WriteConfiguredValue("association", "sentence_show_next_on_duplicate", enabled ? "true" : "false")) + { + return false; + } + g_assoc_sentence_show_next_on_duplicate = enabled; + return true; +} + bool GetConfiguredUnicodeModeEnabled() { return g_unicode_mode_enabled; diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index b9b5bdbd..afe49937 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -277,6 +277,17 @@ bool GetConfiguredKaomojiMixedInputEnabled(); bool SetConfiguredKaomojiMixedInputEnabled(bool enabled); bool GetConfiguredCloudCandidatesEnabled(); bool SetConfiguredCloudCandidatesEnabled(bool enabled); +// 整句候选来源与去重补位开关([association] 段),默认全关。 +bool GetConfiguredAssocSentenceWordLattice(); +bool SetConfiguredAssocSentenceWordLattice(bool enabled); +bool GetConfiguredAssocSentenceGoogle(); +bool SetConfiguredAssocSentenceGoogle(bool enabled); +bool GetConfiguredAssocSentenceNeuralDesktop(); +bool SetConfiguredAssocSentenceNeuralDesktop(bool enabled); +bool GetConfiguredAssocSentenceNeuralKeyboard(); +bool SetConfiguredAssocSentenceNeuralKeyboard(bool enabled); +bool GetConfiguredAssocSentenceShowNextOnDuplicate(); +bool SetConfiguredAssocSentenceShowNextOnDuplicate(bool enabled); bool GetConfiguredUnicodeModeEnabled(); bool SetConfiguredUnicodeModeEnabled(bool enabled); bool GetConfiguredQuickPhraseEnabled(); diff --git a/server/src/ipc/candidate_selection_policy.h b/server/src/ipc/candidate_selection_policy.h index 7860bd18..3a84686a 100644 --- a/server/src/ipc/candidate_selection_policy.h +++ b/server/src/ipc/candidate_selection_policy.h @@ -31,14 +31,21 @@ inline size_t CountCanonicalSyllables(const std::string &canonical_pinyin) noexc return 1 + static_cast(std::count(canonical_pinyin.begin(), canonical_pinyin.end(), '\'')); } -// 整句候选(词格 Generated / Google 解码器 Fallback)独立上屏——不接在造词前缀 -// 后面、自己就是整条输入——时是否该落库。该落:整句是猜出来的,词库里没有它那一行, -// 调频改的是已有的行,改不到它。不落库的话用户选多少次,下次它仍然要靠猜,也仍然 -// 排在词库里那条同音短语后面。 +// 猜出来的整句来源:词格 Generated、Google 解码器 Fallback,以及神经整句 +// NeuralDesktop / NeuralKeyboard。它们都不是词库里已有的行,落库/学习判定同类处理。 +constexpr bool IsGuessedSentenceSource(CandidateSource source) noexcept +{ + return source == CandidateSource::Generated || source == CandidateSource::Fallback || + source == CandidateSource::NeuralDesktop || source == CandidateSource::NeuralKeyboard; +} + +// 整句候选独立上屏——不接在造词前缀后面、自己就是整条输入——时是否该落库。该落:整句是 +// 猜出来的,词库里没有它那一行,调频改的是已有的行,改不到它。不落库的话用户选多少次, +// 下次它仍然要靠猜,也仍然排在词库里那条同音短语后面。 inline bool ShouldStoreStandaloneSentence(CandidateSource source, const std::string &candidate_canonical_pinyin) noexcept { - if (source != CandidateSource::Generated && source != CandidateSource::Fallback) + if (!IsGuessedSentenceSource(source)) { return false; } @@ -57,7 +64,11 @@ inline bool ShouldStoreEarlyReturnPhrase(CandidateSource source, bool creating_w const std::string &prefix_canonical_pinyin, const std::string &candidate_canonical_pinyin) noexcept { - if (source != CandidateSource::Generated || candidate_canonical_pinyin.empty()) + // 与词格 Generated 同一支:神经整句同样带 canonical quanpin,可以结束一段造词或独立 + // 上屏。Google Fallback 沿用旧语义仍不走这条造词落库路径。 + const bool early_return_source = source == CandidateSource::Generated || source == CandidateSource::NeuralDesktop || + source == CandidateSource::NeuralKeyboard; + if (!early_return_source || candidate_canonical_pinyin.empty()) { return false; } diff --git a/server/src/ipc/event_listener.cpp b/server/src/ipc/event_listener.cpp index 959344b6..15f5dca4 100644 --- a/server/src/ipc/event_listener.cpp +++ b/server/src/ipc/event_listener.cpp @@ -717,6 +717,10 @@ void AppendAiContext(const std::string &committed_word) ++cut; g_ai_context.erase(0, cut); } + // 同一份上屏历史也是神经整句重排的前文(青简那边叫 InputHistory):下一次查询会随 + // QueryRequest 下发到词典层。词典层自己按 RerankOptions::context_chars 取末尾若干字。 + if (g_inputSession) + g_inputSession->set_rescoring_context(g_ai_context); } std::wstring BuildCreateWordPipePayload(const std::string &remaining_raw_input_with_cases, @@ -1207,6 +1211,16 @@ std::string BuildCurrentCandidatePage() view.badge = " ☁️"; else if (item.source == CandidateSource::AiSuggestion) view.badge = " 🤖"; + // 整句来源标签与设置页名称保持一致,方便同时比较四个来源。两家选中同一句时只剩一行, + // 标签归先保留下来的来源;开启去重补位后,其余来源会改为显示自己的下一条不同结果。 + else if (item.source == CandidateSource::Generated) + view.badge = " 〔Trigram〕"; + else if (item.source == CandidateSource::Fallback) + view.badge = " 〔Unigram〕"; + else if (item.source == CandidateSource::NeuralDesktop) + view.badge = " 〔神经D〕"; + else if (item.source == CandidateSource::NeuralKeyboard) + view.badge = " 〔神经K〕"; view.fixed_position = item.fixed_position > 0; EnglishIme::TranslationQuery translation_query; if (!translation_page && BuildTranslationQuery(item, translation_query)) @@ -1600,6 +1614,8 @@ enum class TaskType RefreshCandidatePage, ResetInputSessionCache, ExitEnglishInputMode, + // 神经整句重排在后台线程里算完了,候选顺序需要就地更新一次。 + ApplyRescoredOrder, }; struct Task @@ -1830,6 +1846,7 @@ void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_e void WaitForCandidateRenderSync(UINT keycode); void ApplyCloudCandidate(const std::string &candidate, const std::string &pinyin, uint64_t generation, const std::optional &query); +void ApplyRescoredOrder(); void ApplyAiCandidate(const std::string &candidate, const std::string &identity, uint64_t generation, const std::optional &query); void ApplyEnglishCandidates(std::vector candidates, const std::string &input, uint64_t generation); @@ -2408,6 +2425,11 @@ void WorkerThread() } break; } + + case TaskType::ApplyRescoredOrder: { + ApplyRescoredOrder(); + break; + } } } @@ -2467,6 +2489,19 @@ void EnqueueCloudCandidate(const std::string &candidate, const std::string &piny pipe_queueCv.notify_one(); } +// 后台线程算完了一批整句重排。它不知道自己算的是不是当前这次输入——那要在任务线程上、拿着 +// g_inputSession 才判断得了——所以这里只负责把「去看一眼」排进队列,判断留给 ApplyRescoredOrder。 +void EnqueueRescoredCandidates() +{ + { + std::lock_guard lock(queueMutex); + Task task; + task.type = TaskType::ApplyRescoredOrder; + taskQueue.push(std::move(task)); + } + pipe_queueCv.notify_one(); +} + void EnqueueAiCandidate(const std::string &candidate, const std::string &identity, uint64_t generation) { const AsyncRequestOrigin origin = FindAiRequestOrigin(identity, generation); @@ -3866,6 +3901,53 @@ void ApplyCloudCandidate(const std::string &candidate, const std::string &pinyin RefreshCandidatePageUi(true); } +// 候选列表当初是按词格的静态顺序发出去的,因为打分那会儿还没算完。现在算完了:把引擎的候选缓存 +// 丢掉重查一次,这一次 quanpin::make_neural_reranker 能在结果表里查到顺序,整句就落到它该在的位 +// 置上。重查本身不碰模型,走的还是词格那条快路。 +// +// 后台线程算的可能已经是上一次输入的了(用户没停手),所以这里不认「哪一批」,只看重查出来的词 +// 序有没有真的变:没变就一个字节都不动 UI。这既挡掉了过期结果,也挡掉了模型弃权的情况。 +void ApplyRescoredOrder() +{ + if (!g_inputSession || g_translation_candidates_active) + return; + // 造词界面和译文页各自占着 items,重排不该去动它们。 + if (GlobalIme::composition.creating_word.active) + return; + if (Global::candidate_ui.items.empty()) + return; + const FanyImeIpc::CandidateUiOwner owner = SnapshotCandidateUiOwner(); + if (!owner || !IsPipeActivationCurrent(owner.client_id, owner.activation_epoch)) + return; + + const std::vector before = g_inputSession->get_candidates(); + // 云/AI 候选只活在 series cache 里,reset_cache 会把它们一起清掉,而它们的请求早已回来、不会 + // 再发一次——不补回来,重排一落地它们就从列表里消失了。重查不推进在线请求的 generation, + // 所以用当前的 online_query 原样回填即可;回填时若同一句已被整句候选占了,引擎自己会拒绝。 + std::vector online_items; + std::copy_if(before.begin(), before.end(), std::back_inserter(online_items), [](const WordItem &item) { + return item.source == CandidateSource::CloudSuggestion || item.source == CandidateSource::AiSuggestion; + }); + g_inputSession->reset_cache(); + g_inputSession->recompute_candidates(); + if (!online_items.empty()) + { + if (const auto query = g_inputSession->online_query()) + { + for (const WordItem &item : online_items) + g_inputSession->apply_online_candidate(*query, item.word, item.source); + } + } + const std::vector &after = g_inputSession->get_candidates(); + if (after.size() == before.size() && + std::equal(before.begin(), before.end(), after.begin(), + [](const WordItem &a, const WordItem &b) { return a.word == b.word; })) + return; + + PrepareCandidateList(owner.client_id, owner.activation_epoch); + RequestShowCandidateWindow(); +} + void ApplyAiCandidate(const std::string &candidate, const std::string &identity, uint64_t generation, const std::optional &engine_query) { diff --git a/server/src/ipc/event_listener.h b/server/src/ipc/event_listener.h index 9fb7377b..ff2228db 100644 --- a/server/src/ipc/event_listener.h +++ b/server/src/ipc/event_listener.h @@ -49,6 +49,8 @@ void ClearState(); void RegisterStatusSnapshotWindow(HWND toolbar_window); void EnqueueCloudCandidate(const std::string &candidate, const std::string &pinyin, uint64_t generation); void EnqueueAiCandidate(const std::string &candidate, const std::string &identity, uint64_t generation); +// 神经整句重排的后台线程算完一批后调这个;见 event_listener.cpp 的 ApplyRescoredOrder。 +void EnqueueRescoredCandidates(); void CancelCloudCandidateRequest(); void EnqueueEnglishCandidates(std::vector candidates, const std::string &input, uint64_t generation); void EnqueueCandidateTranslations(std::vector results, uint64_t generation, diff --git a/server/src/main.cpp b/server/src/main.cpp index 4cb7a344..4c716e7d 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -18,6 +18,7 @@ #include "english/english_ime.h" #include "emoji/emoji_ime.h" #include "kaomoji/kaomoji_ime.h" +#include "engine/neural/rescore_worker.h" #include "utils/common_utils.h" #include "utils/single_instance.h" #include "session/session_factory.h" @@ -176,6 +177,9 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE /*hPrevInstance*/, AiAssistant::Start([](const std::string &candidate, const std::string &identity, uint64_t generation) { FanyNamedPipe::EnqueueAiCandidate(candidate, identity, generation); }); + // 神经整句重排:打分在引擎自己的后台线程上跑,算完通过这个回调回到任务队列,由队列去重查候选。 + // 不 Start 什么线程——没开这个开关的用户不会平白多一条。 + neural::RescoreWorker::instance().set_ready_callback([] { FanyNamedPipe::EnqueueRescoredCandidates(); }); EnglishIme::Start( CommonUtils::get_ime_data_path() + "\\english.db", [](std::vector candidates, const std::string &input, uint64_t generation) { diff --git a/server/src/session/engine_input_session.cpp b/server/src/session/engine_input_session.cpp index 7129a7c0..7fd87919 100644 --- a/server/src/session/engine_input_session.cpp +++ b/server/src/session/engine_input_session.cpp @@ -1,6 +1,7 @@ #include "engine_input_session.h" #include "config/ime_config.h" #include "engine/common/helpcode_utils.h" +#include "engine/core/sentence_association_options.h" #include "engine/quanpin/quanpin_utils.h" EngineInputSession::EngineInputSession(SchemeType scheme, const ShuangpinProfile &profile) @@ -38,6 +39,14 @@ void EngineInputSession::ApplyConfiguration() // converted quanpin syllables for shuangpin. Re-read on every key so setting // changes take effect immediately. session_.set_fuzzy_pinyin_options(GetConfiguredFuzzyPinyinOptions()); + // 整句候选来源与去重补位选项,每次击键重读,改设置立即生效。 + SentenceAssociationOptions association; + association.word_lattice = GetConfiguredAssocSentenceWordLattice(); + association.google = GetConfiguredAssocSentenceGoogle(); + association.neural_desktop = GetConfiguredAssocSentenceNeuralDesktop(); + association.neural_keyboard = GetConfiguredAssocSentenceNeuralKeyboard(); + association.show_next_on_duplicate = GetConfiguredAssocSentenceShowNextOnDuplicate(); + session_.set_sentence_association(association); session_.set_shuangpin_preedit_uses_raw(GetConfiguredShuangpinPreeditMode() == "shuangpin"); } @@ -144,6 +153,11 @@ bool EngineInputSession::has_active_helpcode() const return session_.has_active_helpcode(); } +void EngineInputSession::set_rescoring_context(std::string context) +{ + return session_.set_rescoring_context(std::move(context)); +} + void EngineInputSession::set_pinyin_sequence(const std::string &pinyin_sequence) { return session_.set_pinyin_sequence(pinyin_sequence); diff --git a/server/src/session/engine_input_session.h b/server/src/session/engine_input_session.h index f75c5496..bb5a7f40 100644 --- a/server/src/session/engine_input_session.h +++ b/server/src/session/engine_input_session.h @@ -35,6 +35,7 @@ class EngineInputSession : public IInputSession bool wubi_four_code_is_complete() const override; bool has_active_helpcode() const override; + void set_rescoring_context(std::string context) override; void set_pinyin_sequence(const std::string &pinyin_sequence) override; void set_pinyin_sequence_with_cases(const std::string &pinyin_sequence) override; diff --git a/server/src/session/input_session.h b/server/src/session/input_session.h index ead19ff5..62fe77ba 100644 --- a/server/src/session/input_session.h +++ b/server/src/session/input_session.h @@ -53,6 +53,11 @@ class IInputSession virtual bool wubi_four_code_is_complete() const = 0; virtual bool has_active_helpcode() const = 0; + // 本会话最近上屏的文本,给神经整句重排当前文。空实现:除引擎会话外没人需要前文。 + virtual void set_rescoring_context(std::string) + { + } + virtual void set_pinyin_sequence(const std::string &pinyin_sequence) = 0; virtual void set_pinyin_sequence_with_cases(const std::string &pinyin_sequence) = 0; diff --git a/server/src/settings/settings_app.cpp b/server/src/settings/settings_app.cpp index 514b0cf6..b923da2d 100644 --- a/server/src/settings/settings_app.cpp +++ b/server/src/settings/settings_app.cpp @@ -374,6 +374,12 @@ std::wstring BuildConfigMessage(bool refresh_skin_catalog) {"paging_page_up_down", GetConfiguredPagingPageUpDownEnabled()}, {"paging_mouse_wheel", GetConfiguredPagingMouseWheelEnabled()}, {"candidate_arrow_navigation", GetConfiguredCandidateArrowNavigationEnabled()}}}, + {"association", + {{"sentence_wordlattice", GetConfiguredAssocSentenceWordLattice()}, + {"sentence_google", GetConfiguredAssocSentenceGoogle()}, + {"sentence_neural_desktop", GetConfiguredAssocSentenceNeuralDesktop()}, + {"sentence_neural_keyboard", GetConfiguredAssocSentenceNeuralKeyboard()}, + {"sentence_show_next_on_duplicate", GetConfiguredAssocSentenceShowNextOnDuplicate()}}}, {"keybindings", {{"switch_language_shift", GetConfiguredSwitchLanguageShiftEnabled()}, {"switch_language_ctrl", GetConfiguredSwitchLanguageCtrlEnabled()}, @@ -681,6 +687,16 @@ bool ApplyConfigUpdate(const json::object &data) return SetConfiguredKaomojiMixedInputEnabled(json::value_to(data.at("value"))); if (path == "general.cloud_candidates") return SetConfiguredCloudCandidatesEnabled(json::value_to(data.at("value"))); + if (path == "association.sentence_wordlattice") + return SetConfiguredAssocSentenceWordLattice(json::value_to(data.at("value"))); + if (path == "association.sentence_google") + return SetConfiguredAssocSentenceGoogle(json::value_to(data.at("value"))); + if (path == "association.sentence_neural_desktop") + return SetConfiguredAssocSentenceNeuralDesktop(json::value_to(data.at("value"))); + if (path == "association.sentence_neural_keyboard") + return SetConfiguredAssocSentenceNeuralKeyboard(json::value_to(data.at("value"))); + if (path == "association.sentence_show_next_on_duplicate") + return SetConfiguredAssocSentenceShowNextOnDuplicate(json::value_to(data.at("value"))); if (path == "utility.unicode_mode") return SetConfiguredUnicodeModeEnabled(json::value_to(data.at("value"))); if (path == "utility.quick_phrase") diff --git a/server/src/webview2/windows_webview2.cpp b/server/src/webview2/windows_webview2.cpp index a26ecd27..437c56d6 100644 --- a/server/src/webview2/windows_webview2.cpp +++ b/server/src/webview2/windows_webview2.cpp @@ -4267,6 +4267,46 @@ HRESULT OnControllerCreatedSettingsWnd( // PostSettingsConfig(); } } + else if (path == "association.sentence_wordlattice") + { + const bool value = json::value_to(data.at("value")); + if (SetConfiguredAssocSentenceWordLattice(value)) + { + PostSettingsConfig(); + } + } + else if (path == "association.sentence_google") + { + const bool value = json::value_to(data.at("value")); + if (SetConfiguredAssocSentenceGoogle(value)) + { + PostSettingsConfig(); + } + } + else if (path == "association.sentence_neural_desktop") + { + const bool value = json::value_to(data.at("value")); + if (SetConfiguredAssocSentenceNeuralDesktop(value)) + { + PostSettingsConfig(); + } + } + else if (path == "association.sentence_neural_keyboard") + { + const bool value = json::value_to(data.at("value")); + if (SetConfiguredAssocSentenceNeuralKeyboard(value)) + { + PostSettingsConfig(); + } + } + else if (path == "association.sentence_show_next_on_duplicate") + { + const bool value = json::value_to(data.at("value")); + if (SetConfiguredAssocSentenceShowNextOnDuplicate(value)) + { + PostSettingsConfig(); + } + } else if (path == "utility.unicode_mode") { const bool value = json::value_to(data.at("value")); @@ -4636,6 +4676,12 @@ void PostSettingsConfig() {"paging_page_up_down", GetConfiguredPagingPageUpDownEnabled()}, {"paging_mouse_wheel", GetConfiguredPagingMouseWheelEnabled()}, {"candidate_arrow_navigation", GetConfiguredCandidateArrowNavigationEnabled()}}}, + {"association", + {{"sentence_wordlattice", GetConfiguredAssocSentenceWordLattice()}, + {"sentence_google", GetConfiguredAssocSentenceGoogle()}, + {"sentence_neural_desktop", GetConfiguredAssocSentenceNeuralDesktop()}, + {"sentence_neural_keyboard", GetConfiguredAssocSentenceNeuralKeyboard()}, + {"sentence_show_next_on_duplicate", GetConfiguredAssocSentenceShowNextOnDuplicate()}}}, {"keybindings", {{"switch_language_shift", GetConfiguredSwitchLanguageShiftEnabled()}, {"switch_language_ctrl", GetConfiguredSwitchLanguageCtrlEnabled()}, diff --git a/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts b/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts index c5e8c7f4..4044866c 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts @@ -181,6 +181,21 @@ function applyConfigData(data: Record, target?: string): void { if (typeof data?.general?.cloud_candidates === 'boolean') { applyToggleState('cloudCandidatesToggleBtn', data.general.cloud_candidates); } + if (typeof data?.association?.sentence_wordlattice === 'boolean') { + applyToggleState('sentenceWordLatticeToggleBtn', data.association.sentence_wordlattice); + } + if (typeof data?.association?.sentence_google === 'boolean') { + applyToggleState('sentenceGoogleToggleBtn', data.association.sentence_google); + } + if (typeof data?.association?.sentence_neural_desktop === 'boolean') { + applyToggleState('sentenceNeuralDesktopToggleBtn', data.association.sentence_neural_desktop); + } + if (typeof data?.association?.sentence_neural_keyboard === 'boolean') { + applyToggleState('sentenceNeuralKeyboardToggleBtn', data.association.sentence_neural_keyboard); + } + if (typeof data?.association?.sentence_show_next_on_duplicate === 'boolean') { + applyToggleState('sentenceShowNextOnDuplicateToggleBtn', data.association.sentence_show_next_on_duplicate); + } if (typeof data?.utility?.unicode_mode === 'boolean') { applyToggleState('unicodeModeToggleBtn', data.utility.unicode_mode); } diff --git a/ui-html/webview2/settings/ime-settings/src/modules/input.ts b/ui-html/webview2/settings/ime-settings/src/modules/input.ts index f9dbce7b..c5565114 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/input.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/input.ts @@ -345,6 +345,23 @@ export function setupInput(): void { setupToggleButton('cloudCandidatesToggleBtn', (active) => { updateConfig('general.cloud_candidates', active); }); + // 四个整句来源和一个去重补位开关,默认全关。两个神经来源会自行在词格中生成最多 12 条 + // 内部备选;补位打开后,某来源的首选重复时沿该来源自己的排名寻找下一条不同结果。 + setupToggleButton('sentenceWordLatticeToggleBtn', (active) => { + updateConfig('association.sentence_wordlattice', active); + }); + setupToggleButton('sentenceGoogleToggleBtn', (active) => { + updateConfig('association.sentence_google', active); + }); + setupToggleButton('sentenceNeuralDesktopToggleBtn', (active) => { + updateConfig('association.sentence_neural_desktop', active); + }); + setupToggleButton('sentenceNeuralKeyboardToggleBtn', (active) => { + updateConfig('association.sentence_neural_keyboard', active); + }); + setupToggleButton('sentenceShowNextOnDuplicateToggleBtn', (active) => { + updateConfig('association.sentence_show_next_on_duplicate', active); + }); } function setupFrequencyOptions(): void { diff --git a/ui-html/webview2/settings/ime-settings/src/partials/input.html b/ui-html/webview2/settings/ime-settings/src/partials/input.html index 835023e0..4782f075 100644 --- a/ui-html/webview2/settings/ime-settings/src/partials/input.html +++ b/ui-html/webview2/settings/ime-settings/src/partials/input.html @@ -558,6 +558,62 @@ +
+
+
+
整句候选
+
输入多个拼音音节时,提供整句候选
+
+
+
+
+
+
Trigram 模型整句
+
使用三元语言模型生成整句候选
+
+
+
+
+
+
+
+
Unigram 模型整句
+
使用 Google 拼音的一元语言模型生成整句候选
+
+
+
+
+
+
+
+
神经网络模型整句(效果优先)
+
结合已上屏的前文,从内部生成的最多 12 个备选句中选择,处理时间较长
+
+
+
+
+
+
+
+
神经网络模型整句(速度优先)
+
使用轻量模型从最多 12 个备选句中选择;两种模式可同时独立给出结果
+
+
+
+
+
+
+
+
去重后补充次选
+
某个模型的首选与已有候选重复时,显示该模型排名最高的下一条不同结果
+
+
+
+
+
+
+
+
拼音方案调频