`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 及各子目录 文档同步说明新的位置与边界。
160 lines
5.1 KiB
C++
160 lines
5.1 KiB
C++
#include "../contracts/assets/assets.h"
|
|
#include "kaomoji_query.h"
|
|
|
|
#include "../core/data_path.h"
|
|
#include "../shuangpin/shuangpin_query.h"
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <memory>
|
|
#include <unordered_set>
|
|
#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>;
|
|
|
|
bool valid_code(const std::string &code)
|
|
{
|
|
return !code.empty() && std::all_of(code.begin(), code.end(), [](unsigned char character) {
|
|
return (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || character == '\'';
|
|
});
|
|
}
|
|
|
|
std::string lower_ascii(std::string text)
|
|
{
|
|
std::transform(text.begin(), text.end(), text.begin(),
|
|
[](unsigned char character) { return static_cast<char>(std::tolower(character)); });
|
|
return text;
|
|
}
|
|
|
|
LocalQueryResult query_failure(const char *diagnostic)
|
|
{
|
|
return {{}, std::string(diagnostic)};
|
|
}
|
|
} // namespace
|
|
|
|
LocalQueryResult query_kaomoji(const std::string &code, SchemeType scheme, int limit, const ShuangpinProfile &profile)
|
|
{
|
|
return query_kaomoji(code, scheme, data_file_path(metasequoia::assets::other_dictionary), limit, profile);
|
|
}
|
|
|
|
LocalQueryResult query_kaomoji(const std::string &code, SchemeType scheme, const std::filesystem::path &database_path,
|
|
int limit, const ShuangpinProfile &profile)
|
|
{
|
|
if (!valid_code(code) || limit <= 0)
|
|
{
|
|
return {};
|
|
}
|
|
if (database_path.empty())
|
|
{
|
|
return query_failure("Kaomoji database is unavailable.");
|
|
}
|
|
|
|
const std::string lower = lower_ascii(code);
|
|
std::vector<std::string> prefixes{lower};
|
|
if (scheme == SchemeType::Shuangpin)
|
|
{
|
|
const std::string quanpin = shuangpin::normalize_input(lower, profile);
|
|
if (!quanpin.empty() && quanpin != lower)
|
|
{
|
|
prefixes.push_back(quanpin);
|
|
}
|
|
}
|
|
|
|
sqlite3 *raw_database = nullptr;
|
|
if (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("Kaomoji database is unavailable.");
|
|
}
|
|
Database database(raw_database);
|
|
sqlite3_busy_timeout(database.get(), 1000);
|
|
|
|
struct Entry
|
|
{
|
|
std::string text;
|
|
int sort_order;
|
|
};
|
|
std::vector<Entry> entries;
|
|
std::unordered_set<std::string> seen;
|
|
constexpr const char *kSql = "SELECT kaomoji,sort_order FROM kaomoji "
|
|
"WHERE (pinyin>=?1 AND pinyin<?2) OR (jianpin>=?1 AND jianpin<?2) "
|
|
"ORDER BY sort_order LIMIT ?3";
|
|
for (const std::string &prefix : prefixes)
|
|
{
|
|
sqlite3_stmt *raw_statement = nullptr;
|
|
if (sqlite3_prepare_v2(database.get(), kSql, -1, &raw_statement, nullptr) != SQLITE_OK)
|
|
{
|
|
return query_failure("Kaomoji database could not be queried.");
|
|
}
|
|
Statement statement(raw_statement);
|
|
std::string upper_bound = prefix;
|
|
upper_bound.push_back(static_cast<char>(0x7f));
|
|
if (sqlite3_bind_text(statement.get(), 1, prefix.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
|
sqlite3_bind_text(statement.get(), 2, upper_bound.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
|
sqlite3_bind_int(statement.get(), 3, limit) != SQLITE_OK)
|
|
{
|
|
return query_failure("Kaomoji database could not be queried.");
|
|
}
|
|
|
|
int step_result = SQLITE_ROW;
|
|
while ((step_result = sqlite3_step(statement.get())) == SQLITE_ROW)
|
|
{
|
|
const auto *text = reinterpret_cast<const char *>(sqlite3_column_text(statement.get(), 0));
|
|
if (text != nullptr && seen.insert(text).second)
|
|
{
|
|
entries.push_back({text, sqlite3_column_int(statement.get(), 1)});
|
|
}
|
|
}
|
|
if (step_result != SQLITE_DONE)
|
|
{
|
|
return query_failure("Kaomoji database could not be queried.");
|
|
}
|
|
}
|
|
|
|
std::stable_sort(entries.begin(), entries.end(),
|
|
[](const Entry &left, const Entry &right) { return left.sort_order < right.sort_order; });
|
|
const std::size_t count = std::min(static_cast<std::size_t>(limit), entries.size());
|
|
LocalQueryResult result;
|
|
result.candidates.reserve(count);
|
|
for (std::size_t index = 0; index < count; ++index)
|
|
{
|
|
result.candidates.emplace_back(lower, entries[index].text, static_cast<std::int64_t>(count - index),
|
|
CandidateSource::Kaomoji);
|
|
}
|
|
return result;
|
|
}
|
|
} // namespace metasequoia::local_modes
|