Files
msime-windows/engine/core/input_session_composition.cpp
T
Lu Fan(SonnyCalcr) 8a524aa498 feat(engine): 全拼纠错扩展到多字/漏字,贵档读法按整句分领衔 (#623)
* feat(engine): 合法输入纠错扩展到多字、漏字,纠错读音按原始区间消耗字母

jiuzheeyang 切成 jiu'zhe'e'yang 全合法,多打的 e 原本纠不回「就这样」;多字纠错的
读音又与原始字母不等长,buuhui 选「不会」后会剩下一个 i。

- legal_input_correction_cuts(原 legal_input_transposition_cuts)认换位、多字、漏字
  三类静态表读法,不认邻键与生成式空间;末尾音节按漏字读出的(nia -> nian)视为
  还没打完,跳过;k-best 先取 9 条再过滤,长句里纠正不被别处的读法挤掉
- 改变字母数的纠正代价 3.0(换位 2.0);字面切分本身是真词时不认改变字母数的读法,
  少一个音节的句子在语言模型里天然分高(邪恶 -> 些、答案 -> 但)
- 新增 correction_cut_for_reading / corrected_reading_raw_length:选词按纠错切分
  记录的原始区间消耗字母,预编辑也按它分隔(jiu'zhee'yang)
- AutocorrectCut 恢复 has_generated,供合法输入路径排除生成式读法

* fix(engine): 被裁掉的合法输入纠错读法不再误标原读法的候选

合法输入上的纠错读法登记进了全局纠错标记表,即使裁决时被丢掉也照样参与按读音字母
的标记匹配:xiee 按多一个 e 读成 xie 被丢掉后,邪恶后面的前缀单字 写/些/血 读音
也是 xie,全被标成纠错。

改为不登记,只在 arbitrate_legal_corrections 里给真正用上的那条纠错读法的整读音
候选打标,标记随结果进缓存。

* feat(engine): 结构性贵档纠错读法可按整句分领衔

shiideya 的换位读法 shi + ide->die(权重 10,是爹呀)比多字读法 shii->shi(权重 12,
是的呀)便宜一档。#592 只在同档内做上下文消解,贵档读法一律排在最后,「是的呀」
连首页都进不了。

- 上下文消解把前 2 条贵档读法也解码,不要求词库有整词;总分 = 路径分 - 手误代价,
  代价按纠错表权重换算(权重 x 0.2:换位 2.0、漏字 2.2、多字 2.4)
- 只收由结构性手误(换位、漏字、多字)构成的贵档读法:邻键只比漏字贵 0.4 分,
  常用词的语言模型优势会把 zhowei 的「周围」挤成「作为」
- AutocorrectCutSegment 记录每段的纠正权重,用来区分纠正类型
- 预编辑跟随领衔读法分隔也覆盖切不成合法音节的输入(shii'de'ya)
- eval_quanpin_autocorrect 新增 --word-lattice,量产品默认开词格时的排序

评测(--word-lattice,300 样本,seed 42)各档 R@1/R@3 不低于改动前:mixed R@1
79.7/76.7/78.3/70.3 -> 82.3/81.7/81.3/78.0,insertion 83.3/74.7/72.7/52.0 ->
91.3/89.3/90.0/83.7,deletion 持平或上升,ambiguous 持平。

* fix(engine): 纠错上下文消解在任一整句来源开启时生效

上下文消解只认 word_lattice 开关。只开万象重排、不显示 Trigram 整句时(词格在内部
照样解码),整段被跳过,shiideya 退回静态排序,「是爹呀」又领衔。

- 新增 SentenceAssociationOptions::any_sentence_source,门控改看它;全关时仍是纯
  静态排序,#592 的「关联关闭 = 静态路径」不变
- 贵档胜者没有整词也没有整句行时(重排模型未载入),由它的最长前缀词领衔,不让
  主切的行顶回首位
2026-10-05 01:41:37 +08:00

1131 lines
45 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "input_session.h"
#include "../common/helpcode_utils.h"
#include "../quanpin/quanpin_query.h"
#include "../quanpin/quanpin_utils.h"
#include "../shuangpin/shuangpin_query.h"
#include "../shuangpin/shuangpin_utils.h"
#include "../japanese/romaji_converter.h"
#include <algorithm>
#include <cctype>
namespace metasequoia
{
namespace
{
std::string remove_delimiters(const std::string &segmented)
{
std::string normalized;
normalized.reserve(segmented.size());
for (const char ch : segmented)
{
if (ch != '\'')
{
normalized.push_back(ch);
}
}
return normalized;
}
void remove_consumed_leading_separators(std::string &raw_input, std::string &raw_input_with_cases)
{
size_t count = 0;
while (count < raw_input.size() && raw_input[count] == '\'')
{
++count;
}
raw_input.erase(0, count);
count = 0;
while (count < raw_input_with_cases.size() && raw_input_with_cases[count] == '\'')
{
++count;
}
raw_input_with_cases.erase(0, count);
}
// 整句落库的长度上限。词库里的短语本身就不超过这个音节数(对标
// quanpin::WordLatticeOptions::max_phrase_syllables),再长的整句只是这一次输入
// 的产物,落库除了撑大用户词库没有别的作用。
constexpr size_t kMaxLearnedSentenceSyllables = 7;
std::string normalize_canonical_pinyin_for_word(const std::string &pinyin, const std::string &word)
{
if (pinyin.empty())
{
return {};
}
const auto segments = quanpin::split_segments(pinyin);
if (segments.empty() || segments.size() != HelpcodeUtils::count_han_chars(word))
{
return {};
}
for (const auto &segment : segments)
{
if (segment.empty() || !quanpin::is_complete_pinyin_input(segment))
{
return {};
}
}
return quanpin::join_segments(segments);
}
std::string append_canonical_pinyin(const std::string &prefix, const std::string &suffix)
{
if (prefix.empty())
{
return suffix;
}
if (suffix.empty())
{
return {};
}
return prefix + "'" + suffix;
}
// 句中辅助码:选词推进是在去掉反引号段的串上算的,剩下的部分要换回原串,后面音节上的约束才
// 留得住。[clean_begin, clean_end) 是剩余部分在去段串里的范围;开头的分隔符连同挂在已上屏音节上
// 的段一起算进已消耗部分,跟普通推进丢掉开头分隔符的做法一致。
struct MidSentenceRest
{
std::string consumed;
std::string rest;
};
MidSentenceRest RemapMidSentenceRest(const QueryRequest &request, const std::string &typed, std::size_t clean_begin,
std::size_t clean_end, const ShuangpinProfile &profile)
{
// 方案层或直接辅助码的解析器已经把映射填进请求(见 QueryRequest::has_syllable_helpcode_layout),
// 直接用它;原串的段规则取决于当时的触发设置,这里不再反推。
shuangpin::MidSentenceHelpcodeInput parsed;
if (request.has_syllable_helpcode_layout)
{
parsed.input = request.raw_input_with_cases;
parsed.source_index = request.syllable_helpcode_source_index;
}
else
{
parsed = shuangpin::parse_mid_sentence_helpcodes(typed, profile);
}
const std::string &clean = parsed.input;
clean_end = (std::min)(clean_end, clean.size());
while (clean_begin < clean_end && clean[clean_begin] == '\'')
{
++clean_begin;
}
MidSentenceRest result;
if (clean_begin >= clean_end)
{
result.consumed = typed;
return result;
}
const std::size_t full_begin = parsed.source_index[clean_begin];
const std::size_t full_end = clean_end >= clean.size() ? typed.size() : parsed.source_index[clean_end];
result.consumed = typed.substr(0, full_begin);
result.rest = typed.substr(full_begin, full_end - full_begin);
return result;
}
std::string LowercaseLetters(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
return text;
}
struct ShuangpinCompositionBase
{
std::string raw_input;
std::string raw_input_with_cases;
std::string effective_raw_input;
std::string effective_raw_input_with_cases;
size_t helpcode_length = 0;
};
ShuangpinCompositionBase ResolveShuangpinCompositionBase(const QueryRequest &request, const ShuangpinProfile &profile)
{
ShuangpinCompositionBase base{
request.raw_input, request.raw_input_with_cases.empty() ? request.raw_input : request.raw_input_with_cases};
base.effective_raw_input = shuangpin::remove_manual_delimiters(base.raw_input);
base.effective_raw_input_with_cases = shuangpin::remove_manual_delimiters(base.raw_input_with_cases);
if (!request.enable_shuangpin_helpcode || base.effective_raw_input.empty())
{
return base;
}
const auto has_complete_unseparated_base = [&](size_t helpcode_length) {
if (base.effective_raw_input.size() <= helpcode_length)
{
return false;
}
const size_t pure_length = base.effective_raw_input.size() - helpcode_length;
const size_t raw_prefix_length = shuangpin::raw_length_for_effective_prefix(base.raw_input, pure_length);
// An apostrophe immediately before the suffix makes that suffix a
// user-defined pinyin segment, not an auxiliary code.
if (raw_prefix_length < base.raw_input.size() && base.raw_input[raw_prefix_length] == '\'')
{
return false;
}
return shuangpin::is_complete_input(base.raw_input.substr(0, raw_prefix_length), profile);
};
if (shuangpin::detect_active_double_helpcode_length(base.raw_input, base.raw_input_with_cases, profile) == 2)
{
base.helpcode_length = 2;
return base;
}
if (base.effective_raw_input.size() % 2 == 1 && base.effective_raw_input.size() > 1)
{
if (has_complete_unseparated_base(1))
{
base.helpcode_length = 1;
}
}
return base;
}
bool HasActiveQuanpinHelpcode(const QueryRequest &request)
{
return request.enable_quanpin_helpcode &&
quanpin::detect_active_helpcode_length(request.raw_input, request.raw_input_with_cases) > 0;
}
std::string ResolveShuangpinCloudCacheKey(const QueryRequest &request, const ShuangpinProfile &profile)
{
const auto base = ResolveShuangpinCompositionBase(request, profile);
if (base.helpcode_length > 0 && base.effective_raw_input.size() >= base.helpcode_length)
{
const size_t base_length = base.effective_raw_input.size() - base.helpcode_length;
return base.raw_input.substr(0, shuangpin::raw_length_for_effective_prefix(base.raw_input, base_length));
}
return base.raw_input;
}
std::string ResolveQuanpinCloudCacheKey(const QueryRequest &request)
{
return quanpin::strip_active_helpcodes(request.raw_input, request.raw_input_with_cases);
}
// Same four-bit mapping as autocorrect_types_from_request (engine.cpp): the
// request only carries the two legacy bools, and either one on also enables
// deletion and insertion so the preedit rebuild sees the same correction space
// as the query.
unsigned QuanpinAutocorrectTypes(const QueryRequest &request)
{
const unsigned legacy =
(request.enable_quanpin_autocorrect_transposition ? quanpin::kAutocorrectTransposition : 0u) |
(request.enable_quanpin_autocorrect_neighbor ? quanpin::kAutocorrectNeighbor : 0u);
return legacy == 0 ? 0u : legacy | quanpin::kAutocorrectDeletion | quanpin::kAutocorrectInsertion;
}
std::string QuanpinLettersWithoutDelimiters(const std::string &text)
{
std::string letters;
letters.reserve(text.size());
for (const char ch : text)
{
if (ch != '\'')
{
letters.push_back(ch);
}
}
return letters;
}
// Folds letters for autocorrect display comparisons: lowercases and strips
// manual delimiters, and maps the u-umlaut style 'v' spelling onto 'u'. This
// equivalence is what classifies length-preserving spelling aliases (jv->ju,
// nue->nve) as "explainable by the cut" so the preedit can be rebuilt with
// separators; removing it would drop separators for alias-typed input. It only
// ever compares two internally derived strings, never gates a user-facing
// mark — the dictionary-side corrected_from contract lives in
// quanpin_dictionary.cpp and treats v/u as distinct letters on purpose.
std::string FoldQuanpinAutocorrectLetters(const std::string &text)
{
std::string folded;
folded.reserve(text.size());
for (const char ch : text)
{
if (ch == '\'')
{
continue;
}
const char lower = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
folded.push_back(lower == 'v' ? 'u' : lower);
}
return folded;
}
// Folded letters of a cut's syllable sequence, for comparing the BFS reading
// against the scheme segmentation reading.
std::string CutSyllableLetters(const quanpin::AutocorrectCut &cut)
{
std::string letters;
for (const auto &segment : cut.segments)
{
letters += segment.syllable;
}
return FoldQuanpinAutocorrectLetters(letters);
}
// Rebuilds the preedit from the cased input letters with a separator after
// each listed letter count (ascending; the final segment end is excluded).
// Letters (case included) are preserved verbatim; manual delimiters are
// replaced by the given boundaries.
std::string RebuildQuanpinDisplayAtBoundaries(const std::string &cased_input, const std::vector<size_t> &boundaries)
{
std::string display;
display.reserve(cased_input.size() + boundaries.size());
size_t letter_index = 0;
size_t boundary_index = 0;
for (const char ch : cased_input)
{
if (ch == '\'')
{
continue;
}
display.push_back(ch);
++letter_index;
if (boundary_index < boundaries.size() && letter_index == boundaries[boundary_index])
{
display.push_back('\'');
++boundary_index;
}
}
return display;
}
// Separators at the raw spans the correction BFS reports.
std::string RebuildQuanpinDisplayFromCut(const std::string &cased_input, const quanpin::AutocorrectCut &cut)
{
std::vector<size_t> boundaries;
for (size_t i = 0; i + 1 < cut.segments.size(); ++i)
{
boundaries.push_back(cut.segments[i].start + cut.segments[i].raw_text.size());
}
return RebuildQuanpinDisplayAtBoundaries(cased_input, boundaries);
}
// Separators for a spelling that passed the jianpin guard, read the way the
// guard reads it: complete syllables plus at most one trailing letter
// ("dongan" -> dong'an, "haoyongg" -> hao'yong'g). Only the typed letters are
// cut, so the result is independent of any alias reading that won the query.
// Returns an empty string when no such reading exists.
std::string RebuildQuanpinDisplayFromLegalSpelling(const std::string &cased_input, const std::string &raw_input)
{
auto segments = quanpin::cut_one_piece_min_segments(raw_input, true);
if (segments.empty() && raw_input.size() > 1)
{
segments = quanpin::cut_one_piece_min_segments(raw_input.substr(0, raw_input.size() - 1), true);
if (!segments.empty())
{
segments.push_back(raw_input.substr(raw_input.size() - 1));
}
}
if (segments.empty())
{
return {};
}
std::vector<size_t> boundaries;
size_t end = 0;
for (size_t i = 0; i + 1 < segments.size(); ++i)
{
end += segments[i].size();
boundaries.push_back(end);
}
return RebuildQuanpinDisplayAtBoundaries(cased_input, boundaries);
}
// The preedit must always show the letters the user actually typed (PRD R5).
// Two layers can rewrite them into canonical pinyin: the scheme alias table
// (sahng -> shang, baked into raw_segmentation) and the dictionary correction
// BFS (shabg -> shang, which only re-separates). Both are rebuilt here from
// the raw letters with separators at the actual cut positions; when the BFS
// cannot explain a rewrite (length-changing aliases such as mihng -> ming) the
// input is shown as typed, with no scheme separators. The rebuild is deliberately
// switch-independent: the alias layer rewrites letters regardless of the
// autocorrect switches, and AC1 only constrains the candidate list.
std::string BuildQuanpinAutocorrectDisplay(const QueryRequest &request)
{
const std::string &cased = request.raw_input_with_cases.empty() ? request.raw_input : request.raw_input_with_cases;
const std::string base = request.raw_segmentation.empty() ? cased : request.raw_segmentation;
if (request.raw_input.empty() || cased.empty())
{
return base;
}
const unsigned types = QuanpinAutocorrectTypes(request);
const std::string folded_input = FoldQuanpinAutocorrectLetters(cased);
const bool letters_rewritten = FoldQuanpinAutocorrectLetters(QuanpinLettersWithoutDelimiters(base)) != folded_input;
// Fast path: the scheme kept the typed letters and either no correction
// type is enabled or the input is already a complete pinyin spelling, so
// no correction interpretation exists to draw separators from.
if (!letters_rewritten && (types == 0 || quanpin::is_complete_pinyin_input(request.raw_input)))
{
return base;
}
// Jianpin guard, mirroring resolve_series_query in the dictionary layer: a
// legal syllable plus at most one trailing letter is user intent, never a
// typo. The deletion table explains shapes like "zheg" -> zheng, so without
// this guard the preedit would lose the scheme separators once the deletion
// bit rides along with the legacy switches.
if (quanpin::looks_like_syllable_with_jianpin_tail(request.raw_input))
{
// The typed spelling is legal, but the scheme alias table may still
// have re-segmented it into a reading with copied letters (dongan ->
// dong'gan, haoyonga -> hao'yong'ga). The dictionary ranks those
// readings against the exact one by frequency, yet the preedit must
// show the typed letters (PRD R5), so a rewritten base may never
// reach it verbatim. Separate the typed letters by their own legal
// reading instead, so one more letter does not drop every separator
// (hao'yong -> hao'yong'a, not haoyonga).
if (!letters_rewritten)
{
return base;
}
const std::string display = RebuildQuanpinDisplayFromLegalSpelling(cased, request.raw_input);
return display.empty() ? cased : display;
}
const auto cut = quanpin::autocorrect_cut_detail(folded_input, types);
// When the scheme rewrote the letters, the query resolved through the alias
// layer, so the preedit may only draw separators from the BFS when both
// layers explain the letters identically ("sahnghao" -> shang'hao).
// Otherwise the deletion bit would re-separate "sahng" as sa'hng while the
// candidates actually come from the alias reading shang -- the old code
// never noticed because the cut happened to be empty without it.
if (!cut.empty() && (!letters_rewritten || CutSyllableLetters(cut) == FoldQuanpinAutocorrectLetters(
QuanpinLettersWithoutDelimiters(base))))
{
return RebuildQuanpinDisplayFromCut(cased, cut);
}
// The BFS cannot explain the input (e.g. a length-changing alias such as
// mihng -> ming): fall back to the input exactly as typed when the letters
// were rewritten, otherwise keep the scheme segmentation untouched. Manual
// delimiters are part of what was typed, so they stay ("mihng'").
return letters_rewritten ? cased : base;
}
// 合法输入上的纠错领衔时(ziazheliya 出「在这里呀」、jiuzheeyang 出「就这样」),预编辑
// 按领衔读法分隔成 zia'zhe'li'ya、jiu'zhee'yang,而不是字面切分 zi'a'zhe'li'ya——后者
// 和首选对不上。边界取纠错切分记录的原始区间(多字、漏字纠错和原始字母不等长);
// 罕见音节替换(lia -> lai)不在 k-best 里,它是换位、字母数不变,按读法音节长度分。
// 切不成合法音节的输入同理:上下文把贵档读法提到首位时(shiideya 出「是的呀」),
// 预编辑要跟着它分成 shii'de'ya,而不是 k-best 首条的 shi'ide'ya。
// 只在首选带纠错标记、读法恰好盖住整串时生效(辅助码、手动分隔符都不满足),否则
// 返回空串。
std::string BuildQuanpinDisplayFromLeadingCorrection(const QueryRequest &request, const WordItem &head)
{
const std::string &cased = request.raw_input_with_cases.empty() ? request.raw_input : request.raw_input_with_cases;
if (head.corrected_from.empty() || request.raw_input.find('\'') != std::string::npos ||
cased.size() != request.raw_input.size())
{
return {};
}
const auto syllables = quanpin::split_segments(head.pinyin);
if (syllables.empty())
{
return {};
}
std::vector<size_t> boundaries;
size_t end = 0;
const auto cut =
quanpin::correction_cut_for_reading(request.raw_input, syllables, QuanpinAutocorrectTypes(request));
if (cut.has_value() && cut->segments.size() == syllables.size())
{
for (const auto &segment : cut->segments)
{
end = segment.start + segment.raw_text.size();
boundaries.push_back(end);
}
}
else
{
for (const auto &syllable : syllables)
{
end += syllable.size();
boundaries.push_back(end);
}
}
if (end != request.raw_input.size())
{
return {};
}
boundaries.pop_back();
return RebuildQuanpinDisplayAtBoundaries(cased, boundaries);
}
} // namespace
void InputSession::handle_engine_key(ImeKeyCode vk, ImeModifierMask modifiers_down, ImeCharacter wch)
{
engine_.handle_key(vk, modifiers_down, wch);
online_requests_.invalidate();
update_mixed_candidates();
}
void InputSession::recompute_candidates()
{
if (has_pending_pinyin_sequence_ || has_pending_pinyin_sequence_with_cases_)
{
apply_pending_sequence();
return;
}
engine_.handle_key(0, 0, 0);
update_mixed_candidates();
}
SchemeType InputSession::current_scheme_type() const
{
return engine_.current_scheme_type();
}
void InputSession::reset_state()
{
clear_pending_sequence();
reset_composition();
}
void InputSession::reset_cache()
{
engine_.reset_cache();
if (canonical_phrase_engine_)
canonical_phrase_engine_->reset_cache();
// 前缀候选是按文本缓存的,不跟着引擎缓存失效:只清缓存键,让下一次
// refresh_prefix_candidates 按新权重/选项重查;保留当前列表,避免 caret
// 激活期间出现空候选窗。
prefix_query_input_.clear();
}
void InputSession::reset_sentence_cache()
{
engine_.reset_sentence_cache();
if (canonical_phrase_engine_)
canonical_phrase_engine_->reset_sentence_cache();
// 前缀候选同样可能带整句,理由同 reset_cache。
prefix_query_input_.clear();
}
const std::vector<WordItem> &InputSession::get_candidates() const
{
return candidates();
}
bool InputSession::expand_initial_candidates()
{
return engine_.expand_initial_candidates();
}
std::optional<WordItem> InputSession::find_candidate(const std::string &key, const std::string &value)
{
return engine_.find_candidate(scheme(), key, value);
}
const QueryRequest &InputSession::request() const
{
return engine_.get_request();
}
const std::string &InputSession::get_pinyin_sequence() const
{
return request().raw_input;
}
const std::string &InputSession::get_pinyin_sequence_with_cases() const
{
// 句中辅助码的反引号段只在原串里,宿主回读的必须是用户敲的原样。
if (!request().raw_input_with_syllable_helpcodes.empty())
{
return request().raw_input_with_syllable_helpcodes;
}
return request().raw_input_with_cases.empty() ? request().raw_input : request().raw_input_with_cases;
}
const std::string &InputSession::get_pure_pinyin_sequence() const
{
return request().normalized_input;
}
const std::string &InputSession::get_pinyin_segmentation() const
{
return request().normalized_segmentation.empty() ? request().segmentation : request().normalized_segmentation;
}
std::string InputSession::get_pinyin_segmentation_with_cases() const
{
return build_pinyin_segmentation_with_cases(shuangpin_preedit_uses_raw_);
}
InputSession::ShuangpinPreeditForms InputSession::get_shuangpin_preedit_forms() const
{
if (!is_shuangpin())
{
return {};
}
return {build_pinyin_segmentation_with_cases(true), build_pinyin_segmentation_with_cases(false)};
}
std::string InputSession::build_pinyin_segmentation_with_cases(bool shuangpin_raw) const
{
if (is_wubi())
{
return request().raw_input;
}
if (is_japanese())
{
return request().raw_input_with_cases.empty() ? request().raw_input : request().raw_input_with_cases;
}
// 句中辅助码的反引号段在切分串里只是一个 ',显示时按原样接回对应音节后面;
// 末尾补 ' 也要看用户敲的原串,否则 ulpb`x 会显示成 ul'pb`x'。
const std::string &typed = get_pinyin_sequence_with_cases();
// 段文本由方案层或直接辅助码的解析器随请求给出(见 QueryRequest::has_syllable_helpcode_layout)。
const auto decorate = [&](const std::string &segmentation) {
return request().has_syllable_helpcode_layout
? shuangpin::decorate_segmentation(segmentation, request().syllable_helpcode_decorations)
: shuangpin::decorate_mid_sentence_segmentation(segmentation, typed, shuangpin_profile_);
};
if (is_shuangpin() && shuangpin_raw)
{
std::string preedit = request().raw_segmentation.empty() ? request().raw_input : request().raw_segmentation;
preedit = decorate(preedit);
if (!typed.empty() && typed.back() == '\'' && (preedit.empty() || preedit.back() != '\''))
{
preedit.push_back('\'');
}
return preedit;
}
if (current_scheme_type() == SchemeType::Quanpin)
{
if (!candidates().empty())
{
std::string display = BuildQuanpinDisplayFromLeadingCorrection(request(), candidates().front());
if (!display.empty())
{
return display;
}
}
return BuildQuanpinAutocorrectDisplay(request());
}
std::string preedit =
request().normalized_segmentation.empty() ? request().segmentation : request().normalized_segmentation;
if (is_shuangpin())
{
preedit = decorate(preedit);
}
if (!typed.empty() && typed.back() == '\'' && (preedit.empty() || preedit.back() != '\''))
{
preedit.push_back('\'');
}
return preedit;
}
std::string InputSession::get_quanpin() const
{
return request().normalized_input;
}
bool InputSession::is_all_complete_pure_pinyin() const
{
if (is_wubi())
{
return request().valid;
}
if (is_japanese())
{
return japanese::ConvertRomaji(request().raw_input).complete;
}
if (is_shuangpin())
{
const auto base = ResolveShuangpinCompositionBase(request(), shuangpin_profile_);
if (base.helpcode_length > 0 && base.effective_raw_input.size() >= base.helpcode_length)
{
const size_t base_length = base.effective_raw_input.size() - base.helpcode_length;
return shuangpin::is_complete_input(
base.raw_input.substr(0, shuangpin::raw_length_for_effective_prefix(base.raw_input, base_length)),
shuangpin_profile_);
}
return shuangpin::is_complete_input(base.raw_input, shuangpin_profile_);
}
const auto &segmentation =
request().normalized_segmentation.empty() ? request().segmentation : request().normalized_segmentation;
return !segmentation.empty() && quanpin::is_complete_pinyin_input(segmentation);
}
bool InputSession::reads_as_pinyin() const
{
if (has_active_helpcode())
return true;
std::string raw = remove_delimiters(request().raw_input);
std::transform(raw.begin(), raw.end(), raw.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (raw.empty())
return false;
if (is_shuangpin())
{
// 双拼两键一个音节,打到一半时末尾会剩一个声母键。
return raw.size() == 1 || shuangpin::is_complete_input(raw, shuangpin_profile_) ||
shuangpin::is_complete_input(raw.substr(0, raw.size() - 1), shuangpin_profile_);
}
if (current_scheme_type() == SchemeType::Quanpin)
return !quanpin::cut_one_piece_min_segments(raw, false).empty();
return false;
}
bool InputSession::wubi_unique_four_code() const
{
// Both modes spell words rather than codes; a wubi code is not composed inside them. No host
// sets either one on a wubi session today, but the guard keeps the pair meaningful if one does.
if (dedicated_english_mode_ || local_input_mode_ != LocalInputMode::None)
{
return false;
}
// A code holding z is a wildcard guess, not a code the typist committed to: the "only
// candidate" behind it is one arbitrary letter away from several others, and committing it
// silently takes away the one thing the key exists for -- seeing what matched. Same reason
// mixed input never reaches here (no table row contains z, so its z codes stay empty).
if (engine_.get_request().raw_input.find('z') != std::string::npos)
{
return false;
}
// is_wubi() covers "this is a wubi session", and engine_.wubi_code_is_complete() is false for
// every other scheme.
if (!is_wubi() || !engine_.wubi_code_is_complete())
{
return false;
}
// Only the code's own rows count: the pinyin candidates mixed input appends are not the answer
// the auto-commit looks for. A code the table answered with exactly one row is genuinely unique.
return wubi_native_candidate_count() == 1;
}
bool InputSession::wubi_four_code_is_complete() const
{
// Same guards as wubi_unique_four_code minus the candidate count: hosts commit the first
// candidate on the next key whether or not the code has one candidate or many. The pinyin
// candidates mixed input appends do not make a code the table never answered complete.
if (dedicated_english_mode_ || local_input_mode_ != LocalInputMode::None)
{
return false;
}
// Wildcard codes neither auto-commit nor top-commit: see wubi_unique_four_code.
if (engine_.get_request().raw_input.find('z') != std::string::npos)
{
return false;
}
return is_wubi() && engine_.wubi_code_is_complete() && wubi_native_candidate_count() > 0;
}
bool InputSession::has_active_helpcode() const
{
if (is_wubi() || is_japanese())
{
return false;
}
if (is_shuangpin())
{
return ResolveShuangpinCompositionBase(request(), shuangpin_profile_).helpcode_length > 0;
}
return HasActiveQuanpinHelpcode(request());
}
void InputSession::set_pinyin_sequence(const std::string &pinyin_sequence)
{
pending_pinyin_sequence_ = pinyin_sequence;
has_pending_pinyin_sequence_ = true;
}
void InputSession::set_pinyin_sequence_with_cases(const std::string &pinyin_sequence)
{
pending_pinyin_sequence_with_cases_ = pinyin_sequence;
has_pending_pinyin_sequence_with_cases_ = true;
}
int InputSession::store_user_phrase(std::string pinyin, std::string word)
{
return engine_.create_word(std::move(pinyin), std::move(word));
}
int InputSession::store_user_phrase_from_canonical_pinyin(std::string pinyin, std::string word)
{
// Both quanpin and shuangpin ultimately share the canonical quanpin
// dictionary. Do not feed a complete quanpin key back through the active
// shuangpin profile a second time.
if (!canonical_phrase_engine_)
canonical_phrase_engine_ = std::make_unique<QuanpinEngine>(paths_);
return canonical_phrase_engine_->create_word_from_canonical_pinyin(std::move(pinyin), std::move(word));
}
// 整句候选(词格 CandidateSource::Generated、Google 解码器 CandidateSource::Fallback、
// 神经整句 NeuralDesktop / NeuralKeyboard)
// 是猜出来的,词库里没有它那一行,所以调频对它无效:update_weight_by_pinyin_and_word
// 改的是 SQLite 里已存在的行,找不到行就什么也不做,用户于是发现自己选多少次都提不上来。
// 选中即落库才是对的处理:存成用户词组之后,下次同样的输入它以 UserDatabase 候选出现,
// 按 quanpin::generated_sentence_insert_position 的约定排在两条整句之前,之后再选还能
// 走正常调频。
std::optional<std::string> InputSession::learn_sentence_candidate(const WordItem &selected)
{
// 本地模式(U/K/E/M/J/Y/R、日期时间)和英文模式也用 Generated 装自己的候选,
// 那些不是拼音整句,不能往拼音用户词库里塞。五笔码表候选也不进这里:混输追加的整句
// 候选自带 Quanpin 方案,按候选自己的方案判断,不看会话方案。
if (local_input_mode_ != LocalInputMode::None || dedicated_english_mode_ || is_japanese() ||
is_wubi_native_candidate(selected))
{
return std::nullopt;
}
// 读音必须完整且音节数与字数对得上,否则落库的是个读音残缺的词条。双拼的整句
// 候选同样带 canonical quanpin,这里统一按全拼键处理。
const std::string canonical = normalize_canonical_pinyin_for_word(selected.canonical_pinyin, selected.word);
if (canonical.empty() || quanpin::split_segments(canonical).size() > kMaxLearnedSentenceSyllables)
{
return std::nullopt;
}
if (store_user_phrase_from_canonical_pinyin(canonical, selected.word) != 0)
{
return "Unable to persist the selected sentence.";
}
return std::nullopt;
}
int InputSession::remove_candidate(std::string pinyin, std::string word, SchemeType scheme)
{
if (!is_wubi() && remove_delimiters(request().raw_input).size() == 1)
{
return -1;
}
return engine_.delete_by_pinyin_and_word(scheme, std::move(pinyin), std::move(word));
}
int InputSession::cache_dynamic_candidate(const std::string &pinyin, const std::string &word, CandidateSource source)
{
const int cache_result = engine_.cache_dynamic_candidate(pinyin, word, source);
(void)engine_.cache_dynamic_candidate_for_current_request(word, source);
return cache_result;
}
InputSession::SelectionTransition InputSession::advance_composition_after_selection(
const std::string &selected_pinyin, const std::string &selected_word, const std::string &selected_canonical_pinyin,
SchemeType selected_scheme)
{
SelectionTransition transition;
transition.selected_canonical_pinyin = selected_canonical_pinyin;
transition.wubi_native = selected_scheme == SchemeType::Wubi;
if (is_japanese())
{
transition.full_pure_pinyin = request().raw_input;
transition.current_segmentation = request().segmentation;
transition.current_segmentation_with_cases = request().raw_input_with_cases;
return transition;
}
if (transition.wubi_native)
{
transition.full_pure_pinyin = request().normalized_input;
transition.current_segmentation = request().normalized_input;
transition.current_segmentation_with_cases = request().raw_input;
return transition;
}
if (is_shuangpin())
{
const auto base = ResolveShuangpinCompositionBase(request(), shuangpin_profile_);
const size_t word_pinyin_length = HelpcodeUtils::count_han_chars(selected_word) * 2;
const size_t total_input_length = base.effective_raw_input.size();
transition.full_pure_pinyin =
base.helpcode_length > 0 && total_input_length >= base.helpcode_length
? base.effective_raw_input.substr(0, total_input_length - base.helpcode_length)
: base.effective_raw_input;
size_t consumed_length = remove_delimiters(selected_pinyin).size();
// 带句中辅助码时 base 是去掉反引号段的串,推进结果要映射回原串,见 RemapMidSentenceRest。
const std::string typed = request().raw_input_with_syllable_helpcodes;
if (base.helpcode_length > 0)
{
const size_t rest_start =
shuangpin::raw_length_for_effective_prefix(base.raw_input_with_cases, word_pinyin_length);
transition.consumed_raw_input_with_cases = base.raw_input_with_cases.substr(0, rest_start);
const size_t required_length = word_pinyin_length + base.helpcode_length;
transition.continues_composition =
required_length < total_input_length && word_pinyin_length < total_input_length;
if (transition.continues_composition)
{
const size_t rest_end = shuangpin::raw_length_for_effective_prefix(
base.raw_input_with_cases, total_input_length - base.helpcode_length);
const std::string rest_pinyin_sequence = base.raw_input.substr(rest_start, rest_end - rest_start);
std::string normalized_rest = rest_pinyin_sequence;
std::string cased_rest = base.raw_input_with_cases.substr(rest_start, rest_end - rest_start);
remove_consumed_leading_separators(normalized_rest, cased_rest);
if (!typed.empty())
{
auto remapped = RemapMidSentenceRest(request(), typed, rest_start, rest_end, shuangpin_profile_);
transition.consumed_raw_input_with_cases = std::move(remapped.consumed);
cased_rest = std::move(remapped.rest);
normalized_rest = LowercaseLetters(cased_rest);
}
engine_.replace_shuangpin_raw_input(normalized_rest, cased_rest);
online_requests_.invalidate();
update_mixed_candidates();
}
}
else
{
if (consumed_length == 0 || consumed_length > base.effective_raw_input.size())
{
consumed_length = (std::min)(word_pinyin_length, base.effective_raw_input.size());
}
const size_t consumed_raw_length =
shuangpin::raw_length_for_effective_prefix(base.raw_input_with_cases, consumed_length);
transition.consumed_raw_input_with_cases = base.raw_input_with_cases.substr(0, consumed_raw_length);
transition.continues_composition = consumed_length < transition.full_pure_pinyin.size();
if (transition.continues_composition)
{
const std::string rest_pinyin_sequence =
base.raw_input.substr(consumed_raw_length, base.raw_input.size() - consumed_raw_length);
const std::string rest_pinyin_sequence_with_cases = base.raw_input_with_cases.substr(
consumed_raw_length, base.raw_input_with_cases.size() - consumed_raw_length);
std::string normalized_rest = rest_pinyin_sequence;
std::string cased_rest = rest_pinyin_sequence_with_cases;
remove_consumed_leading_separators(normalized_rest, cased_rest);
if (!typed.empty())
{
auto remapped = RemapMidSentenceRest(request(), typed, consumed_raw_length,
base.raw_input_with_cases.size(), shuangpin_profile_);
transition.consumed_raw_input_with_cases = std::move(remapped.consumed);
cased_rest = std::move(remapped.rest);
normalized_rest = LowercaseLetters(cased_rest);
}
engine_.replace_shuangpin_raw_input(normalized_rest, cased_rest);
online_requests_.invalidate();
update_mixed_candidates();
}
}
if (!typed.empty() && !transition.continues_composition)
{
// 整串上屏:撤销时要还原的是用户敲的原串,连同反引号段。
transition.consumed_raw_input_with_cases = typed;
}
transition.current_segmentation = get_pinyin_segmentation();
transition.current_segmentation_with_cases = get_pinyin_segmentation_with_cases();
return transition;
}
transition.full_pure_pinyin = request().normalized_input;
const std::string current_segmentation =
request().normalized_segmentation.empty() ? request().segmentation : request().normalized_segmentation;
const std::string current_segmentation_with_cases = get_pinyin_segmentation_with_cases();
const std::string selected_pure_pinyin = remove_delimiters(selected_pinyin);
const std::string raw_input_without_helpcodes =
quanpin::strip_active_helpcodes(request().raw_input, request().raw_input_with_cases);
const std::string raw_input_with_cases_without_helpcodes =
quanpin::strip_active_helpcodes_with_cases(request().raw_input, request().raw_input_with_cases);
// 纠错读音按它在原始输入里盖住的字母消耗:多字、漏字纠错和原始字母不等长,按读音
// 长度消耗会剩下或多吞字母(buuhui 选「不会」剩个 i)。
size_t consumed_letters = selected_pure_pinyin.size();
const unsigned autocorrect_types = QuanpinAutocorrectTypes(request());
if (autocorrect_types != 0 && raw_input_without_helpcodes.find('\'') == std::string::npos)
{
if (const auto aligned = quanpin::corrected_reading_raw_length(
raw_input_without_helpcodes, quanpin::split_segments(selected_pinyin), autocorrect_types))
{
consumed_letters = *aligned;
}
}
size_t consumed_raw_length =
shuangpin::raw_length_for_effective_prefix(raw_input_with_cases_without_helpcodes, consumed_letters);
transition.consumed_raw_input_with_cases = raw_input_with_cases_without_helpcodes.substr(0, consumed_raw_length);
transition.continues_composition = !selected_pure_pinyin.empty() &&
consumed_letters < transition.full_pure_pinyin.size() &&
consumed_raw_length < raw_input_without_helpcodes.size();
if (transition.continues_composition)
{
std::string rest_raw_input = raw_input_without_helpcodes.substr(consumed_raw_length);
std::string rest_raw_input_with_cases = raw_input_with_cases_without_helpcodes.substr(consumed_raw_length);
remove_consumed_leading_separators(rest_raw_input, rest_raw_input_with_cases);
engine_.replace_active_raw_input(rest_raw_input, rest_raw_input_with_cases);
online_requests_.invalidate();
update_mixed_candidates();
transition.current_segmentation = get_pinyin_segmentation();
transition.current_segmentation_with_cases = get_pinyin_segmentation_with_cases();
return transition;
}
transition.current_segmentation = current_segmentation;
transition.current_segmentation_with_cases = current_segmentation_with_cases;
return transition;
}
InputSession::CloudQueryState InputSession::get_cloud_query_state() const
{
CloudQueryState state;
if (is_japanese())
{
state.cache_key = request().raw_input;
state.committed_pinyin = request().raw_input;
state.should_query = !request().raw_input.empty();
state.query_text = state.should_query ? request().raw_input : std::string{};
return state;
}
if (is_wubi())
{
state.cache_key = request().normalized_input;
state.committed_pinyin = request().normalized_input;
return state;
}
if (is_shuangpin())
{
const auto base = ResolveShuangpinCompositionBase(request(), shuangpin_profile_);
state.cache_key = ResolveShuangpinCloudCacheKey(request(), shuangpin_profile_);
state.committed_pinyin = shuangpin::remove_manual_delimiters(state.cache_key);
if (has_active_helpcode())
{
return state;
}
const char last =
base.effective_raw_input_with_cases.empty() ? '\0' : base.effective_raw_input_with_cases.back();
const bool ends_with_input_key = (last >= 'a' && last <= 'z') || last == ';';
state.should_query =
ends_with_input_key && shuangpin::is_complete_input(base.effective_raw_input, shuangpin_profile_);
if (state.should_query)
{
// 云输入拿的是带撇号的全拼,但 ü 要换成 inputtools 认的写法:它和本地的
// Google 解码器一样只认 nue/lue,nve'dai'dong'wu 会被它拆成 nv + e,
// 「虐待动物」于是变成「女蛾黛动物」。committed_pinyin / cache_key 不受
// 影响,仍是双拼原串。
const std::string quanpin_segmentation =
shuangpin::normalize_input_with_delimiters(state.cache_key, shuangpin_profile_);
state.query_text = quanpin::to_google_spelling(quanpin_segmentation);
}
return state;
}
state.committed_pinyin = request().normalized_input;
state.cache_key = ResolveQuanpinCloudCacheKey(request());
if (has_active_helpcode())
{
return state;
}
state.should_query = !request().normalized_input.empty();
// 云端要按用户看到的分词来查:手动把 qi'e'huan 分成三段,就该带着音节边界发给云接口,
// 否则云端自行贪心断句会当成 qie'huan(切换)。normalized_segmentation 保留了用户的手动
// 撇号(cut_pinyin_with_corrections 先按撇号切分),既定住了分词,也让 to_google_spelling
// 能逐音节把 nve / lve 换成云端认的 nue / lue 写法。分词缺失时退回裸串。
const std::string &segmentation =
request().normalized_segmentation.empty() ? request().normalized_input : request().normalized_segmentation;
state.query_text = quanpin::to_google_spelling(segmentation);
return state;
}
InputSession::CreatingWordProgress InputSession::update_creating_word_progress(
const std::string &current_pinyin, const std::string &current_word, const std::string &selected_word,
const SelectionTransition &selection_transition) const
{
CreatingWordProgress progress;
if (selection_transition.wubi_native)
{
progress.pinyin = current_pinyin.empty() ? selection_transition.full_pure_pinyin : current_pinyin;
progress.word = current_word + selected_word;
progress.preedit = progress.word;
progress.completed = true;
progress.can_store = false;
return progress;
}
const std::string selected_canonical =
normalize_canonical_pinyin_for_word(selection_transition.selected_canonical_pinyin, selected_word);
const bool prior_parts_are_storeable = current_word.empty() || !current_pinyin.empty();
if (prior_parts_are_storeable && !selected_canonical.empty())
{
progress.pinyin = append_canonical_pinyin(current_pinyin, selected_canonical);
}
progress.word = current_word + selected_word;
progress.preedit = progress.word + selection_transition.current_segmentation_with_cases;
progress.completed = !selection_transition.continues_composition;
progress.can_store =
progress.completed && !normalize_canonical_pinyin_for_word(progress.pinyin, progress.word).empty();
return progress;
}
bool InputSession::is_shuangpin() const
{
return current_scheme_type() == SchemeType::Shuangpin;
}
bool InputSession::is_wubi() const
{
return current_scheme_type() == SchemeType::Wubi;
}
bool InputSession::is_wubi_native_candidate(const WordItem &item)
{
return item.scheme == SchemeType::Wubi;
}
std::size_t InputSession::wubi_native_candidate_count() const
{
return static_cast<std::size_t>(std::count_if(candidates().begin(), candidates().end(),
[](const WordItem &item) { return is_wubi_native_candidate(item); }));
}
bool InputSession::is_japanese() const
{
return current_scheme_type() == SchemeType::JapaneseRomaji;
}
void InputSession::clear_pending_sequence()
{
pending_pinyin_sequence_.clear();
pending_pinyin_sequence_with_cases_.clear();
has_pending_pinyin_sequence_ = false;
has_pending_pinyin_sequence_with_cases_ = false;
}
void InputSession::apply_pending_sequence()
{
caret_.reset();
const std::string raw_input = has_pending_pinyin_sequence_ ? pending_pinyin_sequence_ : request().raw_input;
const std::string raw_input_with_cases =
has_pending_pinyin_sequence_with_cases_ ? pending_pinyin_sequence_with_cases_ : raw_input;
switch (current_scheme_type())
{
case SchemeType::Shuangpin:
engine_.replace_shuangpin_raw_input(raw_input, raw_input_with_cases);
break;
case SchemeType::Quanpin:
engine_.replace_quanpin_raw_input(raw_input, raw_input_with_cases);
break;
case SchemeType::Wubi:
engine_.replace_wubi_raw_input(raw_input, raw_input_with_cases);
break;
case SchemeType::JapaneseRomaji:
engine_.replace_japanese_raw_input(raw_input, raw_input_with_cases);
break;
}
clear_pending_sequence();
online_requests_.invalidate();
update_mixed_candidates();
}
} // namespace metasequoia