`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是 product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次 搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、 miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。 Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、 macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。 词库仍按 product-lock.json 在构建时取发布产物。 引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游 commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。 verify-contracts / verify-checkout 随之移除,verify-published 保留。 CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除 列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job; engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily 改为 monthly。 README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录 文档同步说明新的位置与边界。
350 lines
12 KiB
C++
350 lines
12 KiB
C++
#include "ime_session.h"
|
|
#include "../schemes/quanpin_scheme.h"
|
|
#include "../schemes/shuangpin_scheme.h"
|
|
#include "../schemes/wubi_scheme.h"
|
|
#include "../schemes/japanese_romaji_scheme.h"
|
|
#include "../quanpin/quanpin_utils.h"
|
|
#include "../shuangpin/shuangpin_query.h"
|
|
#include <stdexcept>
|
|
|
|
namespace
|
|
{
|
|
void ApplyShuangpinHelpcodeSegmentation(QueryRequest &request, const ShuangpinProfile &profile)
|
|
{
|
|
if (request.scheme != SchemeType::Shuangpin || !request.enable_shuangpin_helpcode ||
|
|
shuangpin::detect_active_double_helpcode_length(request.raw_input, request.raw_input_with_cases, profile) != 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const size_t helpcode_length = 2;
|
|
// The detector locates the help codes in delimiter-stripped space, so the split has to be made there too: slicing
|
|
// the last raw bytes would push a pinyin letter into the base and a manual delimiter into the help codes.
|
|
const std::string base_raw_input =
|
|
shuangpin::trim_trailing_letters_preserve_delimiters(request.raw_input, helpcode_length);
|
|
const std::string base_raw_input_with_cases =
|
|
shuangpin::trim_trailing_letters_preserve_delimiters(request.raw_input_with_cases, helpcode_length);
|
|
const std::string base_segmentation = shuangpin::segment_input(base_raw_input, profile);
|
|
const std::string effective_input_with_cases = shuangpin::remove_manual_delimiters(request.raw_input_with_cases);
|
|
const std::string help_codes =
|
|
effective_input_with_cases.substr(effective_input_with_cases.size() - helpcode_length);
|
|
|
|
request.raw_segmentation =
|
|
shuangpin::apply_segmentation_cases(base_segmentation, base_raw_input_with_cases) + "'" + help_codes;
|
|
request.normalized_segmentation = shuangpin::to_quanpin_segmentation(base_segmentation, profile) + "'" + help_codes;
|
|
request.segmentation = request.normalized_segmentation;
|
|
}
|
|
} // namespace
|
|
|
|
ImeSession::ImeSession(SchemeType scheme_type, const ShuangpinProfile &shuangpin_profile,
|
|
metasequoia::RuntimePaths paths)
|
|
: provider_registry_(shuangpin_profile, std::move(paths)), shuangpin_profile_(shuangpin_profile),
|
|
scheme_(create_scheme(scheme_type))
|
|
{
|
|
bind_wubi_scheme();
|
|
}
|
|
|
|
void ImeSession::bind_wubi_scheme()
|
|
{
|
|
wubi_scheme_ = dynamic_cast<WubiScheme *>(scheme_.get());
|
|
if (wubi_scheme_ != nullptr)
|
|
{
|
|
wubi_scheme_->set_mixed_pinyin_allowed(wubi_options_.mixed_pinyin);
|
|
}
|
|
}
|
|
|
|
void ImeSession::set_wubi_input_options(metasequoia::WubiInputOptions options)
|
|
{
|
|
wubi_options_ = options;
|
|
if (wubi_scheme_ != nullptr)
|
|
{
|
|
wubi_scheme_->set_mixed_pinyin_allowed(wubi_options_.mixed_pinyin);
|
|
}
|
|
}
|
|
|
|
void ImeSession::handle_key(ImeKeyCode vk, ImeModifierMask modifiers_down, ImeCharacter wch)
|
|
{
|
|
scheme_->handle_key(vk, modifiers_down, wch);
|
|
// 查询并更新候选词列表
|
|
refresh_candidates();
|
|
}
|
|
|
|
void ImeSession::switch_scheme(SchemeType scheme_type)
|
|
{
|
|
scheme_ = create_scheme(scheme_type);
|
|
bind_wubi_scheme();
|
|
state_ = CompositionState{};
|
|
}
|
|
|
|
void ImeSession::set_shuangpin_helpcode_enabled(bool enabled)
|
|
{
|
|
enable_shuangpin_helpcode_ = enabled;
|
|
}
|
|
|
|
void ImeSession::set_quanpin_helpcode_enabled(bool enabled)
|
|
{
|
|
enable_quanpin_helpcode_ = enabled;
|
|
}
|
|
|
|
void ImeSession::set_quanpin_autocorrect_types(unsigned autocorrect_types)
|
|
{
|
|
quanpin_autocorrect_types_ = autocorrect_types;
|
|
}
|
|
|
|
void ImeSession::replace_shuangpin_raw_input(const std::string &raw_input, const std::string &raw_input_with_cases)
|
|
{
|
|
if (scheme_->type() != SchemeType::Shuangpin)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto *shuangpin_scheme = dynamic_cast<ShuangpinScheme *>(scheme_.get());
|
|
if (!shuangpin_scheme)
|
|
{
|
|
return;
|
|
}
|
|
|
|
shuangpin_scheme->set_raw_input(raw_input, raw_input_with_cases);
|
|
refresh_candidates();
|
|
}
|
|
|
|
void ImeSession::replace_quanpin_raw_input(const std::string &raw_input, const std::string &raw_input_with_cases)
|
|
{
|
|
if (scheme_->type() != SchemeType::Quanpin)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto *quanpin_scheme = dynamic_cast<QuanpinScheme *>(scheme_.get());
|
|
if (!quanpin_scheme)
|
|
{
|
|
return;
|
|
}
|
|
|
|
quanpin_scheme->set_raw_input(raw_input, raw_input_with_cases);
|
|
refresh_candidates();
|
|
}
|
|
|
|
void ImeSession::replace_wubi_raw_input(const std::string &raw_input, const std::string &raw_input_with_cases)
|
|
{
|
|
if (scheme_->type() != SchemeType::Wubi)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (wubi_scheme_ == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// A replacement is host-driven editing of text the composition already holds, so the length it
|
|
// is clipped to comes from the setting rather than from whatever the previous query happened to
|
|
// answer. Without this, moving the caret through a mixed composition silently drops everything
|
|
// past the fourth letter. refresh_candidates puts the query-derived value back.
|
|
wubi_scheme_->set_extended_length_allowed(wubi_options_.mixed_pinyin);
|
|
wubi_scheme_->set_raw_input(raw_input, raw_input_with_cases);
|
|
refresh_candidates();
|
|
}
|
|
|
|
void ImeSession::reset()
|
|
{
|
|
scheme_->reset();
|
|
composition_uses_pinyin_fallback_ = false;
|
|
state_ = CompositionState{};
|
|
}
|
|
|
|
SchemeType ImeSession::candidate_scheme() const
|
|
{
|
|
// The dictionary that actually produced the candidates on screen. A wubi code answered by the
|
|
// quanpin fallback is cached, looked up and learnt in the pinyin dictionary; routing it by the
|
|
// typed scheme would clear the wrong cache and write the weight into the wubi table.
|
|
return state_.answered_by_pinyin_fallback ? SchemeType::Quanpin : current_scheme_type();
|
|
}
|
|
|
|
void ImeSession::reset_cache()
|
|
{
|
|
provider_registry_.reset_cache(candidate_scheme());
|
|
refresh_candidates();
|
|
}
|
|
|
|
int ImeSession::create_word(std::string pinyin, std::string word)
|
|
{
|
|
return provider_registry_.create_word(current_scheme_type(), std::move(pinyin), std::move(word));
|
|
}
|
|
|
|
int ImeSession::update_weight_by_pinyin_and_word(std::string pinyin, std::string word)
|
|
{
|
|
return provider_registry_.update_weight_by_pinyin_and_word(candidate_scheme(), std::move(pinyin), std::move(word));
|
|
}
|
|
|
|
int ImeSession::delete_by_pinyin_and_word(std::string pinyin, std::string word)
|
|
{
|
|
return provider_registry_.delete_by_pinyin_and_word(current_scheme_type(), std::move(pinyin), std::move(word));
|
|
}
|
|
|
|
int ImeSession::cache_dynamic_candidate(const std::string &pinyin, const std::string &word, CandidateSource source)
|
|
{
|
|
return provider_registry_.cache_dynamic_candidate(current_scheme_type(), pinyin, word, source);
|
|
}
|
|
|
|
void ImeSession::replace_japanese_raw_input(const std::string &raw_input, const std::string &raw_input_with_cases)
|
|
{
|
|
if (scheme_->type() != SchemeType::JapaneseRomaji)
|
|
return;
|
|
auto *japanese_scheme = dynamic_cast<JapaneseRomajiScheme *>(scheme_.get());
|
|
if (!japanese_scheme)
|
|
return;
|
|
japanese_scheme->set_raw_input(raw_input, raw_input_with_cases);
|
|
refresh_candidates();
|
|
}
|
|
|
|
void ImeSession::replace_active_raw_input(const std::string &raw_input, const std::string &raw_input_with_cases)
|
|
{
|
|
switch (scheme_->type())
|
|
{
|
|
case SchemeType::Quanpin:
|
|
replace_quanpin_raw_input(raw_input, raw_input_with_cases);
|
|
return;
|
|
case SchemeType::Shuangpin:
|
|
replace_shuangpin_raw_input(raw_input, raw_input_with_cases);
|
|
return;
|
|
case SchemeType::Wubi:
|
|
replace_wubi_raw_input(raw_input, raw_input_with_cases);
|
|
return;
|
|
case SchemeType::JapaneseRomaji:
|
|
replace_japanese_raw_input(raw_input, raw_input_with_cases);
|
|
return;
|
|
}
|
|
}
|
|
|
|
int ImeSession::cache_dynamic_candidate_for_current_request(const std::string &word, CandidateSource source)
|
|
{
|
|
return provider_registry_.cache_dynamic_candidate_for_request(state_.request, word, source);
|
|
}
|
|
|
|
int ImeSession::apply_dynamic_candidate(const std::string &word, CandidateSource source)
|
|
{
|
|
const int result = cache_dynamic_candidate_for_current_request(word, source);
|
|
if (result == 0)
|
|
{
|
|
refresh_candidates();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
SchemeType ImeSession::current_scheme_type() const
|
|
{
|
|
return scheme_->type();
|
|
}
|
|
|
|
const std::string &ImeSession::get_preedit() const
|
|
{
|
|
return state_.preedit;
|
|
}
|
|
|
|
const QueryRequest &ImeSession::get_request() const
|
|
{
|
|
return state_.request;
|
|
}
|
|
|
|
const std::vector<WordItem> &ImeSession::get_candidates() const
|
|
{
|
|
return state_.candidates;
|
|
}
|
|
|
|
std::optional<WordItem> ImeSession::find_candidate(const std::string &key, const std::string &value)
|
|
{
|
|
return provider_registry_.find_candidate(candidate_scheme(), key, value);
|
|
}
|
|
|
|
bool ImeSession::expand_initial_candidates()
|
|
{
|
|
return provider_registry_.expand_initial_candidates(state_.request, state_.candidates);
|
|
}
|
|
|
|
void ImeSession::refresh_candidates()
|
|
{
|
|
state_.preedit = scheme_->get_preedit();
|
|
state_.request = scheme_->build_request();
|
|
state_.request.enable_shuangpin_helpcode = enable_shuangpin_helpcode_;
|
|
state_.request.enable_quanpin_helpcode = enable_quanpin_helpcode_;
|
|
state_.request.enable_quanpin_autocorrect_transposition =
|
|
(quanpin_autocorrect_types_ & quanpin::kAutocorrectTransposition) != 0;
|
|
state_.request.enable_quanpin_autocorrect_neighbor =
|
|
(quanpin_autocorrect_types_ & quanpin::kAutocorrectNeighbor) != 0;
|
|
state_.request.fuzzy_pinyin = fuzzy_pinyin_;
|
|
ApplyShuangpinHelpcodeSegmentation(state_.request, shuangpin_profile_);
|
|
|
|
state_.answered_by_pinyin_fallback = false;
|
|
|
|
if (!state_.request.valid)
|
|
{
|
|
state_.candidates.clear();
|
|
return;
|
|
}
|
|
|
|
if (state_.request.raw_input.empty())
|
|
{
|
|
composition_uses_pinyin_fallback_ = false;
|
|
}
|
|
|
|
state_.candidates = provider_registry_.resolve(state_.request.scheme).query(state_.request);
|
|
const bool wubi_table_answered = !state_.candidates.empty() && !composition_uses_pinyin_fallback_;
|
|
|
|
if (wubi_scheme_ != nullptr)
|
|
{
|
|
// Once the table has failed the code in hand, mixed input lets the composition grow past
|
|
// four letters so a full spelling can be finished. A code the table answers keeps the limit.
|
|
wubi_scheme_->set_extended_length_allowed(wubi_options_.mixed_pinyin && !wubi_table_answered);
|
|
}
|
|
|
|
if (wubi_scheme_ != nullptr && !wubi_table_answered && wubi_options_.mixed_pinyin)
|
|
{
|
|
// The wubi table knows nothing for this code, so the same letters are offered to quanpin.
|
|
// A code the table does know never reaches here, which is what keeps this out of the way of
|
|
// someone typing wubi fluently: it only speaks up where nothing could be typed at all.
|
|
QuanpinScheme pinyin;
|
|
pinyin.set_raw_input(state_.request.raw_input, state_.request.raw_input_with_cases);
|
|
QueryRequest fallback = pinyin.build_request();
|
|
fallback.enable_quanpin_helpcode = enable_quanpin_helpcode_;
|
|
fallback.enable_quanpin_autocorrect_transposition =
|
|
(quanpin_autocorrect_types_ & quanpin::kAutocorrectTransposition) != 0;
|
|
fallback.enable_quanpin_autocorrect_neighbor =
|
|
(quanpin_autocorrect_types_ & quanpin::kAutocorrectNeighbor) != 0;
|
|
fallback.fuzzy_pinyin = fuzzy_pinyin_;
|
|
// The same physical keys produced these letters, so the strokes carry over rather than
|
|
// reaching the provider empty.
|
|
fallback.key_strokes = state_.request.key_strokes;
|
|
if (fallback.valid)
|
|
{
|
|
state_.candidates = provider_registry_.resolve(fallback.scheme).query(fallback);
|
|
state_.answered_by_pinyin_fallback = !state_.candidates.empty();
|
|
composition_uses_pinyin_fallback_ = composition_uses_pinyin_fallback_ || state_.answered_by_pinyin_fallback;
|
|
if (state_.answered_by_pinyin_fallback)
|
|
{
|
|
// The candidates are pinyin, so the request describing them has to be the pinyin one:
|
|
// everything downstream reads the segmentation from here, and the wubi request carries
|
|
// the letters unsplit. The preedit keeps showing the letters as typed either way.
|
|
state_.request = std::move(fallback);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<IInputScheme> ImeSession::create_scheme(SchemeType scheme_type) const
|
|
{
|
|
switch (scheme_type)
|
|
{
|
|
case SchemeType::Shuangpin:
|
|
return std::make_unique<ShuangpinScheme>(shuangpin_profile_);
|
|
case SchemeType::Quanpin:
|
|
return std::make_unique<QuanpinScheme>();
|
|
case SchemeType::Wubi:
|
|
return std::make_unique<WubiScheme>();
|
|
case SchemeType::JapaneseRomaji:
|
|
return std::make_unique<JapaneseRomajiScheme>();
|
|
default:
|
|
throw std::runtime_error("Unknown scheme type.");
|
|
}
|
|
}
|