Files
msime-windows/scripts/build-language-model.ps1
fanlusky d30d2946e4 feat(installer): 打包时生成并安装词格语言模型 sc.lm
sc.lm 此前只能手工投放,重装即失效,发布包里也没有它。这里把它变成一个被钉住、
可复现、自动参与打包的产品输入。

它不走词库那条路:词库 release 由本仓库之外的构建器产出,资产集合和摘要在产品锁
里逐一钉死,加一项要对方先发新版。改为独立钉住上游语料,与 libime 自己的做法一致。

- language-model/lock.json 钉住上游 URL、语料摘要、转换参数和产物摘要。转换是
  确定性的(已在 MSVC 14.44 与 14.50 两套工具链上验证产物摘要一致),所以产物
  摘要可以像下载摘要一样钉死。
- scripts/build-language-model.ps1 下载、校验、转换、再校验,写临时文件后改名,
  中途失败不会留下摘要对不上的 sc.lm。幂等,摘要对得上直接返回。
- 它只 configure engine/ngram 一个目录,不拉 vcpkg 和 SQLite:转换工具只依赖
  kenlm 自己,而打包机上未必装得起引擎的全部依赖。
- Invoke-LocalTest.ps1 打完整包前自动调用它,轻量包本就不带数据文件,不调。
- 打包脚本只断言文件存在,摘要由构建脚本负责,与词库交给 product_lock.py 校验
  的分工一致。缺模型让打包失败而不是跳过:引擎对缺模型是静默降级的,运行期不会
  暴露,打包是唯一拦得住的地方,package-files.ps1 为此加了反向用例。

模型是 LGPL-2.1-or-later 的第三方数据,声明随之安装为 libime-lm-NOTICE.md。
安装脚本无需改动,它已按通配收集资源目录。产物与语料不进仓库,只进锁与声明。
2026-09-19 10:08:39 +08:00

104 lines
5.4 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 生成词格整句打分用的 sc.lm。
#
# 词库是从钉住的发布下载的,语言模型不是:它的上游是 libime 的 ARPA 语料,我们按
# libime 自己那组参数现场转换。转换是确定性的,所以 language-model\lock.json 既钉
# 下载的摘要,也钉产物的摘要 —— 两头都校验过,本地和 CI 才会得到同一个文件。
#
# 幂等:sc.lm 已在且摘要正确时直接返回。因此 Invoke-LocalTest.ps1 可以在每次打完整
# 包前无条件调用它,日常 test.ps1 不会为此多花时间。
[CmdletBinding()]
param(
[string]$RepoRoot = (Split-Path -Parent $PSScriptRoot),
# 下载的压缩包与解出来的 ARPA 都留在这里。ARPA 有 ~190 MB,保留它是为了改转换参数
# 时不必重下;删掉也只是下次多下一次。
[string]$CacheDirectory,
# 即使摘要已经对上也重新转换一次。改了 lock.json 的 build 参数时用。
[switch]$Force
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$modelDirectory = Join-Path $RepoRoot 'language-model'
$lockPath = Join-Path $modelDirectory 'lock.json'
if (-not (Test-Path -LiteralPath $lockPath)) {
throw "语言模型的来源锁不存在:$lockPath"
}
$lock = Get-Content -LiteralPath $lockPath -Raw | ConvertFrom-Json
if ($lock.schema_version -ne 1) {
throw "不支持的 lock.json schema_version:$($lock.schema_version)"
}
if (-not $CacheDirectory) { $CacheDirectory = Join-Path $modelDirectory 'cache' }
$modelPath = Join-Path $modelDirectory $lock.output.path
function Test-Digest {
param([Parameter(Mandatory)][string]$LiteralPath, [Parameter(Mandatory)][string]$Expected)
if (-not (Test-Path -LiteralPath $LiteralPath)) { return $false }
return (Get-FileHash -Algorithm SHA256 -LiteralPath $LiteralPath).Hash.ToLower() -eq $Expected.ToLower()
}
function Assert-Digest {
param([Parameter(Mandatory)][string]$LiteralPath, [Parameter(Mandatory)][string]$Expected,
[Parameter(Mandatory)][string]$Description)
$actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $LiteralPath).Hash.ToLower()
if ($actual -ne $Expected.ToLower()) {
throw "$Description 的 SHA256 与 lock.json 不符:期望 $($Expected.ToLower()),实得 $actual($LiteralPath)"
}
}
if (-not $Force -and (Test-Digest -LiteralPath $modelPath -Expected $lock.output.sha256)) {
Write-Host "语言模型已就绪:$modelPath"
return
}
New-Item -ItemType Directory -Path $CacheDirectory -Force | Out-Null
$archiveName = Split-Path -Leaf ([uri]$lock.source.url).AbsolutePath
$archivePath = Join-Path $CacheDirectory $archiveName
$arpaPath = Join-Path $CacheDirectory $lock.source.member
if (-not (Test-Digest -LiteralPath $archivePath -Expected $lock.source.archive_sha256)) {
Write-Host "下载语料模型:$($lock.source.url)"
# 摘要没对上的残留必须先删:curl 的断点续传会把两次不同的下载接在一起。
if (Test-Path -LiteralPath $archivePath) { Remove-Item -LiteralPath $archivePath -Force }
curl.exe -L --fail --retry 3 -o $archivePath $lock.source.url
if ($LASTEXITCODE -ne 0) { throw "下载语料模型失败($LASTEXITCODE):$($lock.source.url)" }
}
Assert-Digest -LiteralPath $archivePath -Expected $lock.source.archive_sha256 -Description '语料模型压缩包'
# Windows 自带的 tar 是 bsdtar,直接认 zstd,不需要另装 zstd。
if (Test-Path -LiteralPath $arpaPath) { Remove-Item -LiteralPath $arpaPath -Force }
tar.exe -xf $archivePath -C $CacheDirectory
if ($LASTEXITCODE -ne 0) { throw "解压语料模型失败($LASTEXITCODE):$archivePath" }
if (-not (Test-Path -LiteralPath $arpaPath)) {
throw "压缩包里没有 $($lock.source.member):$archivePath"
}
# 只 configure engine\ngram 这一个目录,不走 engine\CMakeLists.txt。转换工具只依赖
# kenlm 自己(零 Boost、零 SQLite),为它拉起整个引擎的依赖没有意义,在只下载了
# 二进制产物的打包机上还未必装得起来。
$toolBuild = Join-Path $CacheDirectory 'build-tool'
$toolPath = Join-Path $toolBuild 'Release\metasequoia_ime_build_binary.exe'
if (-not (Test-Path -LiteralPath $toolPath)) {
cmake -S (Join-Path $RepoRoot 'engine\ngram') -B $toolBuild -DMETASEQUOIA_IME_BUILD_NGRAM_TOOLS=ON
if ($LASTEXITCODE -ne 0) { throw "配置转换工具失败($LASTEXITCODE)" }
cmake --build $toolBuild --config Release --target metasequoia_ime_build_binary --parallel
if ($LASTEXITCODE -ne 0) { throw "构建转换工具失败($LASTEXITCODE)" }
}
if (-not (Test-Path -LiteralPath $toolPath)) {
# 单配置生成器(Ninja、Makefiles)不套 Release 子目录。
$toolPath = Join-Path $toolBuild 'metasequoia_ime_build_binary.exe'
if (-not (Test-Path -LiteralPath $toolPath)) { throw "没有找到转换工具:$toolBuild" }
}
# 先写到临时文件再改名:转换中途失败不应留下一个摘要对不上的 sc.lm,让下次运行
# 以为只是缓存脏了。
$stagingPath = "$modelPath.incoming"
if (Test-Path -LiteralPath $stagingPath) { Remove-Item -LiteralPath $stagingPath -Force }
& $toolPath @($lock.build.flags) $arpaPath $stagingPath
if ($LASTEXITCODE -ne 0) { throw "转换语言模型失败($LASTEXITCODE)" }
Assert-Digest -LiteralPath $stagingPath -Expected $lock.output.sha256 -Description '语言模型 sc.lm'
Move-Item -LiteralPath $stagingPath -Destination $modelPath -Force
Write-Host "语言模型生成完成:$modelPath($((Get-Item -LiteralPath $modelPath).Length) 字节)"