`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 及各子目录 文档同步说明新的位置与边界。
71 lines
3.3 KiB
C++
71 lines
3.3 KiB
C++
#include <metasequoia/personal_dictionary.h>
|
|
#include "../quanpin/quanpin_utils.h"
|
|
#include <utf8.h>
|
|
#include <algorithm>
|
|
|
|
namespace metasequoia
|
|
{
|
|
PersonalDictionaryValidation validate_personal_dictionary_entry(PersonalDictionaryEntry entry)
|
|
{
|
|
const auto invalid = [](const char *message) { return PersonalDictionaryValidation{std::nullopt, message}; };
|
|
if (entry.weight < 1 || entry.weight > 100000000)
|
|
return invalid("Weight must be between 1 and 100000000");
|
|
if (entry.key.empty() || entry.key.size() > 512 || entry.value.empty() || entry.value.size() > 4096 ||
|
|
!utf8::is_valid(entry.value.begin(), entry.value.end()))
|
|
return invalid("A valid, bounded UTF-8 word and input code are required");
|
|
const bool quick = entry.kind == PersonalDictionaryKind::QuickPhrase;
|
|
for (unsigned char ch : entry.value)
|
|
if ((ch < 32 && !(quick && (ch == '\n' || ch == '\t'))) || ch == 127)
|
|
return invalid("The word contains an unsupported control character");
|
|
for (char &ch : entry.key)
|
|
if (ch >= 'A' && ch <= 'Z')
|
|
ch = static_cast<char>(ch - 'A' + 'a');
|
|
auto letters = [](unsigned char ch) { return ch >= 'a' && ch <= 'z'; };
|
|
switch (entry.kind)
|
|
{
|
|
case PersonalDictionaryKind::Pinyin: {
|
|
// Require explicit syllables; guessing a split here could store a different pronunciation.
|
|
std::replace(entry.key.begin(), entry.key.end(), ' ', '\'');
|
|
std::size_t start = 0, count = 0;
|
|
while (start <= entry.key.size())
|
|
{
|
|
const auto end = entry.key.find('\'', start);
|
|
const auto syllable = entry.key.substr(start, end == std::string::npos ? end : end - start);
|
|
if (!quanpin::intact_pinyin_set().count(syllable))
|
|
return invalid("Use complete pinyin syllables separated by apostrophes or spaces");
|
|
++count;
|
|
if (end == std::string::npos)
|
|
break;
|
|
start = end + 1;
|
|
}
|
|
if (count > 64 || count != static_cast<std::size_t>(utf8::distance(entry.value.begin(), entry.value.end())))
|
|
return invalid("Each character must have one pinyin syllable (maximum 64)");
|
|
break;
|
|
}
|
|
case PersonalDictionaryKind::Wubi:
|
|
if (entry.key.size() > 4 || !std::all_of(entry.key.begin(), entry.key.end(), letters))
|
|
return invalid("Wubi codes contain one to four letters");
|
|
break;
|
|
case PersonalDictionaryKind::QuickPhrase:
|
|
if (entry.key.size() > 32 || !std::all_of(entry.key.begin(), entry.key.end(), [&](unsigned char ch) {
|
|
return letters(ch) || (ch >= '0' && ch <= '9');
|
|
}))
|
|
return invalid("Quick-phrase codes contain one to 32 letters or digits");
|
|
break;
|
|
case PersonalDictionaryKind::English: {
|
|
std::string normalized = entry.value;
|
|
for (char &ch : normalized)
|
|
if (ch >= 'A' && ch <= 'Z')
|
|
ch = static_cast<char>(ch - 'A' + 'a');
|
|
if (entry.key.size() > 64 || !std::all_of(entry.key.begin(), entry.key.end(), letters) ||
|
|
normalized != entry.key)
|
|
return invalid("English code must match the word's letters, ignoring case");
|
|
break;
|
|
}
|
|
default:
|
|
return invalid("Unsupported dictionary kind");
|
|
}
|
|
return {std::move(entry), {}};
|
|
}
|
|
} // namespace metasequoia
|