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。 安装脚本无需改动,它已按通配收集资源目录。产物与语料不进仓库,只进锁与声明。
104 lines
5.4 KiB
PowerShell
104 lines
5.4 KiB
PowerShell
# 生成词格整句打分用的 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) 字节)"
|