From 7ed4f2e0fd0ca359e54c7a8bbdc2b76a3292909e Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Mon, 12 Oct 2026 03:01:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(engine):=20=E5=9B=9E=E6=BA=AF=E5=88=87?= =?UTF-8?q?=E5=88=86=E5=88=A4=E5=AE=9A=E5=90=88=E6=B3=95=E5=85=A8=E6=8B=BC?= =?UTF-8?q?=EF=BC=8C=E5=90=88=E6=B3=95=E8=BE=93=E5=85=A5=E9=80=89=E8=AF=8D?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=8C=89=E7=BA=A0=E9=94=99=E5=88=87=E5=88=86?= =?UTF-8?q?=E6=B6=88=E8=80=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit is_complete_pinyin_input 用贪心最长匹配判定合法性:zaijinuta 取了 jin 就剩下切不动的 uta,合法的 zai'ji'nu'ta 被当成手误。查询侧用回溯切分认它合法,预编辑和选词却去走纠错: 预编辑显示成 zai'ji'nut'a,选「再」按 zaij -> zai 的纠错切分吞掉 j,只剩 inuta。 - is_complete_pinyin_part 与 looks_like_jianpin_spelling 改用回溯切分,与查询侧同一个算法 - corrected_reading_raw_length:读音是原始字母前缀、整串合法(可带一个末尾声母)时按读音长度消耗 --- engine/quanpin/quanpin_utils.cpp | 57 +++++++++---------------- engine/tests/src/test_input_session.cpp | 26 ++++++++++- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/engine/quanpin/quanpin_utils.cpp b/engine/quanpin/quanpin_utils.cpp index 5d3e7adb..6e8aa37a 100644 --- a/engine/quanpin/quanpin_utils.cpp +++ b/engine/quanpin/quanpin_utils.cpp @@ -88,7 +88,9 @@ bool is_complete_pinyin_part(const std::string &part) return false; } - return !cut_one_piece_greedy(part, true).empty(); + // 要能回溯:贪心在 zaijinuta 上取了 jin 就剩下切不动的 uta,把合法的 zai'ji'nu'ta + // 判成手误,预编辑和选词随之去走纠错。与查询侧的切分同一个算法。 + return !cut_one_piece_min_segments(part, true).empty(); } Segments append_rest(const Segments &head, const Segments &segments) @@ -208,44 +210,21 @@ bool looks_like_jianpin_spelling(const std::string &pinyin) { return true; } - static const size_t kMaxSyllableLength = - std::max_element(intact_pinyin_list().begin(), intact_pinyin_list().end(), - [](const std::string &lhs, const std::string &rhs) { return lhs.size() < rhs.size(); }) - ->size(); - - // Greedy longest-match scan: the input must reduce to one or more legal - // syllables plus at most one trailing letter ("zheg" = zhe + g) to count - // as jianpin intent. All-consonant strings match zero syllables and - // return false on purpose: the engine has no multi-letter jianpin, so - // correction is the only useful reading of e.g. "bqng" -> bang. - size_t pos = 0; - while (pos < pinyin.size()) - { - const size_t max_len = std::min(kMaxSyllableLength, pinyin.size() - pos); - size_t matched = 0; - for (size_t len = max_len; len >= 1; --len) - { - if (valid_pinyin.find(pinyin.substr(pos, len)) != valid_pinyin.end()) - { - matched = len; - break; - } - } - if (matched == 0) - { - break; - } - pos += matched; - } - if (pos == 0 || pinyin.size() - pos > 1) - { - return false; - } - if (pinyin.size() == pos) + // The input must reduce to one or more legal syllables plus at most one + // trailing letter ("zheg" = zhe + g) to count as jianpin intent. The cut + // backtracks like the query-side segmentation ("zaijinut" = zai'ji'nu + t). + // All-consonant strings match zero syllables and return false on purpose: + // the engine has no multi-letter jianpin, so correction is the only useful + // reading of e.g. "bqng" -> bang. + if (is_complete_pinyin_input(pinyin)) { // Fully reduced to legal syllables: a legitimate spelling, not a typo. return true; } + if (pinyin.size() < 2 || !is_complete_pinyin_input(pinyin.substr(0, pinyin.size() - 1))) + { + return false; + } // Exactly one trailing letter. It reads as jianpin intent only when it is a // plausible jianpin initial -- an initial consonant, as in "zheg" = zhe + g. // A bare vowel can never begin a jianpin syllable, so a complete syllable @@ -1209,10 +1188,12 @@ std::optional corrected_reading_raw_length(const std::string &raw_letter { reading_letters += syllable; } - // 读音是原始字母的前缀,且整串本身是合法拼音:没有纠错在起作用,按读音长度即可。 + // 读音是原始字母的前缀,且整串本身是合法拼音(可带一个末尾声母,与查询侧不进纠错的 + // 判据一致):没有纠错在起作用,按读音长度即可。合法输入上的纠错读法另有原读法领衔或 + // 第 2 位兜底,选字面候选不该被它带偏(zaijinuta 选「再」曾按 zaij -> zai 吞掉 j)。 // 只看前缀不够:buuhui 里单选「不」,bu 碰巧也是原始前缀,可它实际盖住的是 buu。 - if (reading.empty() || - (raw_letters.compare(0, reading_letters.size(), reading_letters) == 0 && is_complete_pinyin_input(raw_letters))) + if (reading.empty() || (raw_letters.compare(0, reading_letters.size(), reading_letters) == 0 && + (is_complete_pinyin_input(raw_letters) || looks_like_jianpin_spelling(raw_letters)))) { return std::nullopt; } diff --git a/engine/tests/src/test_input_session.cpp b/engine/tests/src/test_input_session.cpp index 162ed650..7b870576 100644 --- a/engine/tests/src/test_input_session.cpp +++ b/engine/tests/src/test_input_session.cpp @@ -1263,6 +1263,11 @@ void run_autocorrect_selection_consumption_tests(const std::filesystem::path &da "A deletion correction must cover only the letters actually typed."); require(!quanpin::corrected_reading_raw_length("nihao", {"ni"}, all_types).has_value(), "A plain prefix of a legal input must keep the reading-length consumption."); + // 贪心切分在 jin 上卡死、要回溯才切得开的合法输入:曾被判成手误,「再」按 zaij -> zai 吞掉 j。 + require(quanpin::is_complete_pinyin_input("zaijinuta") && quanpin::looks_like_jianpin_spelling("zaijinut"), + "A legal input that needs backtracking must still count as legal."); + require(!quanpin::corrected_reading_raw_length("zaijinuta", {"zai"}, all_types).has_value(), + "A plain prefix of a backtracking-only legal input must keep the reading-length consumption."); const std::filesystem::path directory = data_directory / "autocorrect-selection-consumption"; std::filesystem::create_directories(directory); @@ -1276,11 +1281,18 @@ void run_autocorrect_selection_consumption_tests(const std::filesystem::path &da "INSERT INTO tbl_1_h VALUES('hui', 'h', '会', 900000);" "INSERT INTO tbl_1_h VALUES('hao', 'h', '好', 900000);"); database.execute("CREATE TABLE tbl_1_z(key TEXT,jp TEXT,value TEXT,weight INTEGER);" - "INSERT INTO tbl_1_z VALUES('zhang', 'z', '张', 900000);"); + "INSERT INTO tbl_1_z VALUES('zhang', 'z', '张', 900000);" + "INSERT INTO tbl_1_z VALUES('zai', 'z', '再', 900000);"); + database.execute("CREATE TABLE tbl_1_j(key TEXT,jp TEXT,value TEXT,weight INTEGER);" + "INSERT INTO tbl_1_j VALUES('ji', 'j', '激', 900000);"); + database.execute("CREATE TABLE tbl_1_t(key TEXT,jp TEXT,value TEXT,weight INTEGER);" + "INSERT INTO tbl_1_t VALUES('ta', 't', '他', 900000);"); database.execute("CREATE TABLE tbl_1_g(key TEXT,jp TEXT,value TEXT,weight INTEGER);" "INSERT INTO tbl_1_g VALUES('guo', 'g', '国', 900000);"); database.execute("CREATE TABLE tbl_1_n(key TEXT,jp TEXT,value TEXT,weight INTEGER);" - "INSERT INTO tbl_1_n VALUES('ni', 'n', '你', 900000);"); + "INSERT INTO tbl_1_n VALUES('ni', 'n', '你', 900000);" + "INSERT INTO tbl_1_n VALUES('nu', 'n', '怒', 900000);" + "INSERT INTO tbl_1_n VALUES('niu', 'n', '牛', 900000);"); database.execute("CREATE TABLE tbl_2_n(key TEXT,jp TEXT,value TEXT,weight INTEGER);" "INSERT INTO tbl_2_n VALUES('ni''hao', 'nh', '你好', 1000);"); } @@ -1324,6 +1336,16 @@ void run_autocorrect_selection_consumption_tests(const std::filesystem::path &da require(session.has_composition() && session.preedit() == "hao", "A plain prefix of a legal input must keep its consumption."); } + // 要回溯才切得开的合法输入不进纠错:预编辑按字面切分,选「再」剩 jinuta。 + { + metasequoia::InputSession session(SchemeType::Quanpin, all_types, true, true, true, paths); + type(session, "zaijinuta"); + require(session.get_pinyin_segmentation_with_cases() == "zai'ji'nu'ta", + "A legal input must be displayed by its literal segmentation."); + (void)session.select_candidate(candidate_index(session, "再")); + require(session.has_composition() && session.preedit() == "jinuta", + "Selecting a literal prefix must not consume letters of the next syllable."); + } std::filesystem::remove_all(directory); }