Files
Lu Fan(SonnyCalcr) 649747f8cd feat(server): 神经网络整句效果优先模型改为按需下载 (#838)
效果优先模型(sentence-model-desktop.safetensors,约 25 MB)不再随安装包分发,
改为和八股文模型一样在设置页下载后才能使用。

- engine:SentenceAssociationOptions 新增 neural_desktop_model 路径,留空即模型缺席、
  静默不重排;词典不再从资源目录固定加载效果档。load_file 按 UTF-8 打开路径,
  数据目录在非 ASCII 用户目录下也能读到模型
- server:新增 settings/neural_model,下载到 <数据目录>\models\neural-desktop\,
  下载与导入都按 neural-model/lock.json 的字节数和 SHA256 校验后原子落位;
  会话层只在开关打开时解析模型路径
- ui-html:设置页增加下载 / 导入 / 删除 / 浏览器下载,模型未就绪时开关置灰
- installer:不再打包效果档模型;旧版装进数据目录根下的那份在覆盖安装与卸载时清掉

原有配置项 sentence_neural_desktop 保持不变,用户的开关状态升级后保留。
2026-10-11 18:08:33 +08:00

107 lines
5.0 KiB
C++

#pragma once
// Neural reranking of whole-sentence candidates.
//
// The model does not produce sentences. The word lattice (engine/quanpin/word_lattice.*) already
// decodes the typed pinyin into its n-best sentences with a kenlm trigram; this layer asks the
// character-level Transformer how likely each of those sentences is given what the user has already
// committed, and reorders them by
//
// score = static + lambda * (neural - static)
//
// both terms in nats. That is a log-linear interpolation of the two language models, with lambda
// controlling how much the neural model is allowed to override the lattice. It cannot invent a
// sentence the lattice did not propose, which is the point: the lattice is good at coverage and bad
// at long-range choices, and the Transformer is the reverse.
//
// This replaces an earlier design that ran a per-syllable beam search over the model to generate
// sentences directly. That needed one full forward pass per beam per syllable with no K/V reuse --
// on the order of 10^10 multiply-accumulates for a dozen syllables, seconds of wall clock on the
// typing path. Scoring a small batch of finished sentences against a cached context is far less work, because
// the sentences come from the lattice for free and the batch streams the weights once.
//
// The word lattice always supplies the paths when neural reranking is enabled. Its own candidate
// does not have to be enabled for display: in that mode the lattice is only an internal candidate
// generator and the user sees the neural model's pick.
#include <cstddef>
#include <string>
#include <utility>
#include <vector>
namespace neural
{
class SentenceModel;
struct RerankOptions
{
// How much of the model's opinion to apply. 0 leaves the lattice order untouched; 1 replaces
// the lattice score outright. 0.5 keeps the lattice's own evidence (user dictionary weights,
// reading priors) in play, which matters because the neural model has never seen them.
double lambda = 0.5;
// Paths to rescore. The lattice's own ordering past the first handful is already noise, and
// every extra path is another row in the batch.
std::size_t max_paths = 12;
// Characters of committed text shown to the model as context. Bounded by the model's own
// window; 64 matches what the model was trained to use.
std::size_t context_chars = 64;
};
// Last `count` Unicode characters of `text`, not bytes: the context bound is in characters because
// the model's window is in characters. Exposed so a caller that caches results keyed by context can
// trim to exactly what will be scored -- two histories with the same tail are the same context, and
// keying on the untrimmed text would miss the cache on every commit.
std::string last_characters(const std::string &text, std::size_t count);
// Loads (once, process-wide) and returns the model at `path`, or nullptr if it is missing or
// invalid. The returned pointer is owned by the cache and lives for the process; a failed load is
// remembered so it is not retried on every keystroke.
const SentenceModel *shared_sentence_model(const std::string &path);
// A handle that defers shared_sentence_model() until the model is first asked for. The two models
// together are ~28 MB of heap once loaded; resolving them in the dictionary constructors paid for
// both even when the settings leave them off (the desktop model is off by default). Not thread-safe:
// resolve it on the thread that owns the dictionary and hand the raw pointer to worker threads.
class LazySentenceModel
{
public:
LazySentenceModel() = default;
explicit LazySentenceModel(std::string path) : path_(std::move(path))
{
}
const SentenceModel *get() const
{
if (!resolved_)
{
model_ = path_.empty() ? nullptr : shared_sentence_model(path_);
resolved_ = true;
}
return model_;
}
private:
std::string path_;
mutable const SentenceModel *model_ = nullptr;
mutable bool resolved_ = false;
};
// The order `sentences` should be shown in, as indices into it.
//
// `static_scores` are the lattice's own path scores in log10, as LatticePath::log_prob reports
// them; they are converted to nats here. `context` is the text already committed before the
// composition, and is truncated to the last `options.context_chars` characters.
//
// Returns an empty vector when the model could not be consulted at all (fewer than two sentences,
// lambda of zero, mismatched inputs). The caller must then leave its own order alone: a partial
// reorder is worse than none, because it mixes two incomparable rankings.
//
// Only the first `options.max_paths` sentences are rescored; any beyond that keep their relative
// order at the end.
std::vector<std::size_t> rerank_order(const SentenceModel &model, const std::string &context,
const std::vector<std::string> &sentences,
const std::vector<double> &static_scores, const RerankOptions &options = {});
} // namespace neural