`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 及各子目录 文档同步说明新的位置与边界。
239 lines
7.0 KiB
C++
239 lines
7.0 KiB
C++
#include "../contracts/assets/assets.h"
|
|
#include "jianpin_query.h"
|
|
|
|
#include "../core/data_path.h"
|
|
#include "../quanpin/quanpin_query.h"
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace metasequoia::local_modes
|
|
{
|
|
namespace
|
|
{
|
|
struct DatabaseCloser
|
|
{
|
|
void operator()(sqlite3 *database) const
|
|
{
|
|
if (database != nullptr)
|
|
{
|
|
sqlite3_close(database);
|
|
}
|
|
}
|
|
};
|
|
|
|
struct StatementCloser
|
|
{
|
|
void operator()(sqlite3_stmt *statement) const
|
|
{
|
|
if (statement != nullptr)
|
|
{
|
|
sqlite3_finalize(statement);
|
|
}
|
|
}
|
|
};
|
|
|
|
using Database = std::unique_ptr<sqlite3, DatabaseCloser>;
|
|
using Statement = std::unique_ptr<sqlite3_stmt, StatementCloser>;
|
|
|
|
std::string normalize_code(const std::string &code)
|
|
{
|
|
std::string normalized;
|
|
normalized.reserve(code.size());
|
|
for (unsigned char character : code)
|
|
{
|
|
if (character >= 'A' && character <= 'Z')
|
|
{
|
|
character = static_cast<unsigned char>(character - 'A' + 'a');
|
|
}
|
|
if (character < 'a' || character > 'z')
|
|
{
|
|
return {};
|
|
}
|
|
normalized.push_back(static_cast<char>(character));
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
std::string decode_shuangpin_initial(char code, const ShuangpinProfile &profile)
|
|
{
|
|
const std::string mapped_key(1, code);
|
|
for (const auto &[initial, key] : profile.initials)
|
|
{
|
|
if (key == mapped_key)
|
|
{
|
|
return initial;
|
|
}
|
|
}
|
|
return mapped_key;
|
|
}
|
|
|
|
quanpin::Segments expand_code(const std::string &normalized, SchemeType scheme, const ShuangpinProfile &profile)
|
|
{
|
|
quanpin::Segments segments;
|
|
segments.reserve(normalized.size());
|
|
for (const char character : normalized)
|
|
{
|
|
segments.push_back(scheme == SchemeType::Shuangpin ? decode_shuangpin_initial(character, profile)
|
|
: std::string(1, character));
|
|
}
|
|
return segments;
|
|
}
|
|
|
|
std::vector<std::string> split_key(const std::string &key)
|
|
{
|
|
std::vector<std::string> parts;
|
|
std::size_t start = 0;
|
|
while (true)
|
|
{
|
|
const std::size_t separator = key.find('\'', start);
|
|
if (separator == std::string::npos)
|
|
{
|
|
parts.push_back(key.substr(start));
|
|
return parts;
|
|
}
|
|
parts.push_back(key.substr(start, separator - start));
|
|
start = separator + 1;
|
|
}
|
|
}
|
|
|
|
std::string syllable_initial(const std::string &syllable)
|
|
{
|
|
if (syllable.size() >= 2)
|
|
{
|
|
const std::string prefix = syllable.substr(0, 2);
|
|
if (prefix == "zh" || prefix == "ch" || prefix == "sh")
|
|
{
|
|
return prefix;
|
|
}
|
|
}
|
|
return syllable.empty() ? std::string{} : syllable.substr(0, 1);
|
|
}
|
|
|
|
bool key_matches_initials(const std::string &key, const quanpin::Segments &initials)
|
|
{
|
|
const auto syllables = split_key(key);
|
|
if (syllables.size() != initials.size())
|
|
{
|
|
return false;
|
|
}
|
|
for (std::size_t index = 0; index < initials.size(); ++index)
|
|
{
|
|
if (syllable_initial(syllables[index]) != initials[index])
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int scan_limit(int limit, bool filter_initials)
|
|
{
|
|
if (!filter_initials || limit > 100000000)
|
|
{
|
|
return limit;
|
|
}
|
|
return std::max(limit * 32, 512);
|
|
}
|
|
|
|
LocalQueryResult query_failure(const char *diagnostic)
|
|
{
|
|
return {{}, std::string(diagnostic)};
|
|
}
|
|
} // namespace
|
|
|
|
std::string jianpin_ranking_context(const std::string &code, SchemeType scheme, const ShuangpinProfile &profile)
|
|
{
|
|
const std::string normalized = normalize_code(code);
|
|
return normalized.empty() ? std::string{} : quanpin::join_segments(expand_code(normalized, scheme, profile));
|
|
}
|
|
|
|
LocalQueryResult query_jianpin(const std::string &code, SchemeType scheme, int limit, const ShuangpinProfile &profile)
|
|
{
|
|
return query_jianpin(code, scheme, data_file_path(metasequoia::assets::main_dictionary), limit, profile);
|
|
}
|
|
|
|
LocalQueryResult query_jianpin(const std::string &code, SchemeType scheme, const std::filesystem::path &database_path,
|
|
int limit, const ShuangpinProfile &profile)
|
|
{
|
|
if (limit <= 0)
|
|
{
|
|
return {};
|
|
}
|
|
const std::string normalized = normalize_code(code);
|
|
if (normalized.empty())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
const auto segments = expand_code(normalized, scheme, profile);
|
|
const std::string table = quanpin::build_table_name(segments);
|
|
if (table.empty())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
sqlite3 *raw_database = nullptr;
|
|
if (database_path.empty() || sqlite3_open_v2(path_to_utf8(database_path).c_str(), &raw_database,
|
|
SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr) != SQLITE_OK)
|
|
{
|
|
if (raw_database != nullptr)
|
|
{
|
|
sqlite3_close(raw_database);
|
|
}
|
|
return query_failure("Super-jianpin database is unavailable.");
|
|
}
|
|
Database database(raw_database);
|
|
sqlite3_busy_timeout(database.get(), 1000);
|
|
|
|
const std::string jianpin = quanpin::segments_to_jianpin(segments);
|
|
const bool filter_initials = scheme == SchemeType::Shuangpin;
|
|
const std::string sql =
|
|
"SELECT \"key\",\"value\",\"weight\" FROM \"" + table + "\" WHERE \"jp\"=?1 ORDER BY \"weight\" DESC LIMIT ?2";
|
|
sqlite3_stmt *raw_statement = nullptr;
|
|
if (sqlite3_prepare_v2(database.get(), sql.c_str(), -1, &raw_statement, nullptr) != SQLITE_OK)
|
|
{
|
|
return query_failure("Super-jianpin database could not be queried.");
|
|
}
|
|
Statement statement(raw_statement);
|
|
if (sqlite3_bind_text(statement.get(), 1, jianpin.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
|
sqlite3_bind_int(statement.get(), 2, scan_limit(limit, filter_initials)) != SQLITE_OK)
|
|
{
|
|
return query_failure("Super-jianpin database could not be queried.");
|
|
}
|
|
|
|
const std::string matched_code = quanpin::join_segments(segments);
|
|
LocalQueryResult result;
|
|
int step_result = SQLITE_ROW;
|
|
while ((step_result = sqlite3_step(statement.get())) == SQLITE_ROW)
|
|
{
|
|
const auto *key = reinterpret_cast<const char *>(sqlite3_column_text(statement.get(), 0));
|
|
const auto *value = reinterpret_cast<const char *>(sqlite3_column_text(statement.get(), 1));
|
|
if (value == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
const std::string canonical = key == nullptr ? std::string{} : std::string(key);
|
|
if (filter_initials && !key_matches_initials(canonical, segments))
|
|
{
|
|
continue;
|
|
}
|
|
result.candidates.emplace_back(matched_code, value, sqlite3_column_int64(statement.get(), 2),
|
|
CandidateSource::Database, canonical);
|
|
if (static_cast<int>(result.candidates.size()) >= limit)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (step_result != SQLITE_DONE && step_result != SQLITE_ROW)
|
|
{
|
|
return query_failure("Super-jianpin database could not be queried.");
|
|
}
|
|
return result;
|
|
}
|
|
} // namespace metasequoia::local_modes
|