Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@ updates:
schedule:
interval: "weekly"

- package-ecosystem: "npm"
- package-ecosystem: "bun"
directory: "/bridge"
schedule:
interval: "weekly"

- package-ecosystem: "pip"
directory: "/scripts/voxtral"
schedule:
interval: "weekly"

- package-ecosystem: "pre-commit"
directory: "/"
schedule:
interval: "weekly"

- package-ecosystem: "swift"
directory: "/apps/ValarCLI"
schedule:
Expand All @@ -25,6 +35,16 @@ updates:
schedule:
interval: "weekly"

- package-ecosystem: "swift"
directory: "/Packages/ValarCore"
schedule:
interval: "weekly"

- package-ecosystem: "swift"
directory: "/Packages/ValarModelKit"
schedule:
interval: "weekly"

- package-ecosystem: "swift"
directory: "/Packages/ValarAudio"
schedule:
Expand Down
21 changes: 8 additions & 13 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,17 @@ jobs:
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4
with:
languages: swift,javascript-typescript
languages: javascript-typescript

- name: Ensure build helpers
run: |
command -v jq >/dev/null 2>&1 || arch -arm64 brew install jq
command -v rg >/dev/null 2>&1 || arch -arm64 brew install ripgrep
jq --version
rg --version | head -n 1

- name: Bootstrap dependencies
run: bash ./tools/bootstrap.sh native --with-bridge
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.3.14"

- name: Build Swift targets
- name: Install bridge dependencies
run: |
swift build --package-path apps/ValarCLI
swift build --package-path apps/ValarDaemon
cd bridge
bun install --frozen-lockfile --ignore-scripts

- name: Analyze
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4
2 changes: 1 addition & 1 deletion .github/workflows/native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.2.13"
bun-version: "1.3.14"

- name: Ensure validation helpers
run: |
Expand Down
4 changes: 2 additions & 2 deletions Packages/ValarCore/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 58 additions & 7 deletions Packages/ValarCore/Sources/ValarCore/ValarCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -887,14 +887,33 @@ public struct ModelInstallValidationReport: Sendable, Equatable {
}
}

public enum ModelInstallerError: Error, Equatable {
public enum ModelInstallerError: Error, Equatable, LocalizedError {
case validationFailed([String])
case installedRecordMissing(String)
case installedPackMissing(String)
case invalidRemoteSourceLocation(String)
case downloadFailed(String)
case checksumMismatch(artifactPath: String, expected: String, actual: String)
case missingChecksum(artifactPath: String)

public var errorDescription: String? {
switch self {
case .validationFailed(let messages):
return "Model manifest validation failed: \(messages.joined(separator: "; "))"
case .installedRecordMissing(let modelID):
return "Installed model record was not created for \(modelID)."
case .installedPackMissing(let path):
return "Installed model pack is missing at \(path)."
case .invalidRemoteSourceLocation(let location):
return "Invalid remote model source: \(location)."
case .downloadFailed(let message):
return message
case .checksumMismatch(let artifactPath, let expected, let actual):
return "Checksum mismatch for \(artifactPath): expected \(expected), got \(actual)."
case .missingChecksum(let artifactPath):
return "Remote artifact '\(artifactPath)' is missing a SHA-256 checksum."
}
}
}

public enum ModelInstallMode: Sendable, Equatable {
Expand Down Expand Up @@ -1063,7 +1082,7 @@ public actor ModelInstaller {
for artifact in uncheckedArtifacts {
issues.append(.init(
severity: .warning,
message: "\(Self.checksumWarningLabel(for: artifact.kind)) artifact '\(artifact.id)' is missing a SHA-256 checksum; Valar can install it, but cannot locally verify the downloaded file"
message: "\(Self.checksumWarningLabel(for: artifact.kind)) artifact '\(artifact.id)' is missing a SHA-256 checksum; Valar will not install the file from a remote source until a checksum is declared"
))
}
do {
Expand Down Expand Up @@ -1216,6 +1235,37 @@ public actor ModelInstaller {
return record
}

public func verifyInstalledArtifacts(manifest: ValarPersistence.ModelPackManifest) throws {
let packDirectory = try paths.modelPackDirectory(familyID: manifest.familyID, modelID: manifest.modelID)

for artifact in manifest.artifactSpecs where !artifact.relativePath.hasSuffix("/") {
let artifactURL = packDirectory.appendingPathComponent(artifact.relativePath, isDirectory: false)
try ValarAppPaths.validateContainment(artifactURL, within: packDirectory)

guard fileManager.fileExists(atPath: artifactURL.path) else {
if artifact.required {
throw ModelInstallerError.downloadFailed(
"Installed artifact '\(artifact.relativePath)' is missing from the current model pack."
)
}
continue
}

if let checksum = artifact.checksum {
let actualChecksum = try sha256Hex(for: artifactURL)
guard actualChecksum.caseInsensitiveCompare(checksum) == .orderedSame else {
throw ModelInstallerError.checksumMismatch(
artifactPath: artifact.relativePath,
expected: checksum,
actual: actualChecksum
)
}
} else if Self.remoteChecksumRequiredKinds.contains(artifact.kind) {
throw ModelInstallerError.missingChecksum(artifactPath: artifact.relativePath)
}
}
}

public func purgeSharedCaches(for modelID: ModelIdentifier) throws -> [String] {
let hubRoot = Self.resolveHFHubCacheRoot(fileManager: fileManager, hfCacheRoot: hfCacheRoot)
let standardDirectory = hubRoot.appendingPathComponent(Self.hfHubRepoDirectoryName(for: modelID.rawValue), isDirectory: true)
Expand Down Expand Up @@ -1286,6 +1336,10 @@ public actor ModelInstaller {
let weight = 1 / totalArtifacts
let destinationURL = stagingDirectory.appendingPathComponent(artifact.relativePath, isDirectory: false)
try ValarAppPaths.validateContainment(destinationURL, within: stagingDirectory)
let requiresChecksum = Self.remoteChecksumRequiredKinds.contains(artifact.kind)
guard !artifact.required || artifact.checksum != nil else {
throw ModelInstallerError.missingChecksum(artifactPath: artifact.relativePath)
}

try fileManager.createDirectory(
at: destinationURL.deletingLastPathComponent(),
Expand Down Expand Up @@ -1347,12 +1401,9 @@ public actor ModelInstaller {
actual: actualChecksum
)
}
} else if artifact.checksum != nil && hfCached == nil {
// Catalog declared a checksum but it wasn't verified above —
// this shouldn't happen, but guard against it.
} else if requiresChecksum {
try? removeIfPresent(destinationURL)
throw ModelInstallerError.missingChecksum(artifactPath: artifact.relativePath)
// Note: models without pre-computed checksums (checksum == nil)
// are trusted when downloaded directly from HuggingFace.
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,14 @@ public extension ValarRuntime {
if sourceKind == .remoteURL, !allowDownload {
throw RouteModelError.refreshRequiresDownload(id)
}
_ = try await modelInstaller.uninstall(modelID: identifier)
if sourceKind == .remoteURL,
try await modelPackRegistry.installedRecord(for: identifier.rawValue) != nil {
do {
try await modelInstaller.verifyInstalledArtifacts(manifest: manifest)
} catch {
_ = try await modelInstaller.uninstall(modelID: identifier)
}
}
_ = try await modelInstaller.purgeSharedCaches(for: identifier)
}

Expand Down
4 changes: 2 additions & 2 deletions Packages/ValarMLX/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,61 @@ public enum VibeVoiceCatalog {
VibeVoiceSurface.vibeVoiceRealtime05B4Bit.supportedLanguages
}

private struct ArtifactIntegrity {
let sha256: String
let sizeBytes: Int
}

private static let artifactIntegrity: [String: ArtifactIntegrity] = [
"config.json": .init(sha256: "ef672aca9e7deb835925970492a10e606d1b2c7fc741dd30cf36e3efe2886717", sizeBytes: 2655),
"model.safetensors": .init(sha256: "d4e33a2daca2dd866b472e42210701fe9e28dc6fcec649b2a1fd05e5885b30bd", sizeBytes: 632_644_595),
"tokenizer.json": .init(sha256: "c0382117ea329cdf097041132f6d735924b697924d6f6fc3945713e96ce87539", sizeBytes: 7_031_645),
"tokenizer_config.json": .init(sha256: "c91efca15ceff6e9ee9424db58a6f59cd41294e550a86cbd07e3c1fb500b34f9", sizeBytes: 7_228),
"preprocessor_config.json": .init(sha256: "ebf514b5d30a012e5ae00d9a19d01e735e35b27768c3926d980815db8fa742e5", sizeBytes: 360),
"voices/de-Spk0_man.safetensors": .init(sha256: "2e34ddef90b8585c6298c2545841ef73c67e6adf8f728376107e8088244c1463", sizeBytes: 7_017_168),
"voices/de-Spk1_woman.safetensors": .init(sha256: "7a6c9efd03b06a2a6c2cb4fe88a43ec5b4ea5f3fe46d597ed36056f324ab415c", sizeBytes: 5_268_176),
"voices/en-Carter_man.safetensors": .init(sha256: "1b3efb89bc26bc14d86095da9b26b0aaf5989e8ed75e39efa958088bf301160c", sizeBytes: 4_241_352),
"voices/en-Davis_man.safetensors": .init(sha256: "6d689ac3f6f630fd1617814a15ab165544772a208ce305d3779feb08f033f1e0", sizeBytes: 2_456_752),
"voices/en-Emma_woman.safetensors": .init(sha256: "8572620ccf3384529c8fce7b211871482cfa1fc8e3068f80576b7ea15257e819", sizeBytes: 3_328_696),
"voices/en-Frank_man.safetensors": .init(sha256: "869af2fd5e83b3f70cdc23b28fdc1ef82c11e7122c387adb8bc56902b323efb2", sizeBytes: 3_345_080),
"voices/en-Grace_woman.safetensors": .init(sha256: "7b0cb4438eb8a2cc0d45eb8c4d724d27fb459f999a8ea0bf9ab221e67cb92ba6", sizeBytes: 2_758_064),
"voices/en-Mike_man.safetensors": .init(sha256: "10524823aa1f90cd4cec05828f16d20d090d83ac3eb682646aa43e45c1f9dc0a", sizeBytes: 1_993_376),
"voices/fr-Spk0_man.safetensors": .init(sha256: "8d6d1df2b70d05680bf2da34bb606b991eefa35068f091e2f17c603af8c8726e", sizeBytes: 4_363_984),
"voices/fr-Spk1_woman.safetensors": .init(sha256: "b9ce6e695df9bef90ce926b6c570ce211c456d969164208c3445a4ac257bd1e4", sizeBytes: 4_249_296),
"voices/in-Samuel_man.safetensors": .init(sha256: "3a22a118b02f1d2dbafe02284152bd83413dad344c7c65a66dc83ae2b528bc64", sizeBytes: 3_768_000),
"voices/it-Spk0_woman.safetensors": .init(sha256: "87ba3fcecc31c1d639a86bf5631096e06297bcdff2b455756b014bd4ccad9672", sizeBytes: 2_529_456),
"voices/it-Spk1_man.safetensors": .init(sha256: "b5c7cb194cca41d43e15e6cadbd7a7add10bc3d5fe53ef287979129f8fa90c21", sizeBytes: 2_832_056),
"voices/jp-Spk0_man.safetensors": .init(sha256: "ab53e74e00e87577506db3817985bd3584fab079acfc80615dccd160435b3476", sizeBytes: 4_645_840),
"voices/jp-Spk1_woman.safetensors": .init(sha256: "b331ec494ce24227ee4bd8a834aa407f3fb6d0fde9fd2ad6ac739f5df7b96a44", sizeBytes: 4_615_120),
"voices/kr-Spk0_woman.safetensors": .init(sha256: "2ac9224493d510782c7ca2294c4309edbd74e5871a76fa6b8fe0c408c2ddba01", sizeBytes: 4_131_024),
"voices/kr-Spk1_man.safetensors": .init(sha256: "67ccf3f76b5be71609d4c26ea64c6ce850ff8cfcf03ee623568a6bca916e40e9", sizeBytes: 5_842_640),
"voices/nl-Spk0_man.safetensors": .init(sha256: "8645c3a0fd62e94609527c066fece280998824ebd6a4dae169d024ad3de085fc", sizeBytes: 3_681_992),
"voices/nl-Spk1_woman.safetensors": .init(sha256: "cc8ac9607e1c61e3347ef3a25a0599886bf7c06dc69ab9a7633facfc80e6cbb4", sizeBytes: 5_073_104),
"voices/pl-Spk0_man.safetensors": .init(sha256: "b8f61efaf59ea95f520b4231646f4d04dea367fdf39633e80106d20abc3873b8", sizeBytes: 3_728_328),
"voices/pl-Spk1_woman.safetensors": .init(sha256: "7c3220f7ef26e8a06bfbb9f8acd0201139a9ee7be8a7b83df46bf28324e9677c", sizeBytes: 4_955_856),
"voices/pt-Spk0_woman.safetensors": .init(sha256: "0828216576aae51b2ada2b542a393edf1177a4e2cd5338b230187162ae4fdcc3", sizeBytes: 2_245_544),
"voices/pt-Spk1_man.safetensors": .init(sha256: "370345ebe6209bfea8084290104060297414e0d1015a9c7e5e9e3ce532bb17e2", sizeBytes: 3_532_488),
"voices/sp-Spk0_woman.safetensors": .init(sha256: "7c05318ca1f3c94ba533d4f7c2fb4694332a7de67de68051018fbd97f158070e", sizeBytes: 4_221_128),
"voices/sp-Spk1_man.safetensors": .init(sha256: "90c928352e59070b7a41c6d7ad76943f18e0f9d8dfc58d430d25e005c2287d79", sizeBytes: 5_107_920),
]

private static func artifactSpec(
id: String,
role: ArtifactRole,
relativePath: String,
required: Bool = true
) -> ArtifactSpec {
let integrity = artifactIntegrity[relativePath]
return ArtifactSpec(
id: id,
role: role,
relativePath: relativePath,
sha256: integrity?.sha256,
sizeBytes: integrity?.sizeBytes,
required: required
)
}

public static func primaryLanguage(for preset: PresetVoiceSpec) -> String? {
preset.languageAffinity.first?.lowercased()
}
Expand Down Expand Up @@ -231,7 +286,7 @@ public enum VibeVoiceCatalog {

static var voiceCacheArtifacts: [ArtifactSpec] {
presetVoices.map { preset in
ArtifactSpec(
artifactSpec(
id: "voice-cache-\(preset.name)",
role: .voiceAsset,
relativePath: "voices/\(preset.name).safetensors"
Expand Down Expand Up @@ -261,13 +316,13 @@ public enum VibeVoiceCatalog {
),
],
artifacts: [
ArtifactSpec(id: "model-config", role: .config, relativePath: "config.json"),
ArtifactSpec(id: "model-weights", role: .weights, relativePath: "model.safetensors"),
ArtifactSpec(id: "tokenizer", role: .tokenizer, relativePath: "tokenizer.json"),
ArtifactSpec(id: "tokenizer-config", role: .tokenizer, relativePath: "tokenizer_config.json"),
ArtifactSpec(id: "special-tokens-map", role: .auxiliary, relativePath: "special_tokens_map.json", required: false),
ArtifactSpec(id: "added-tokens", role: .auxiliary, relativePath: "added_tokens.json", required: false),
ArtifactSpec(id: "preprocessor-config", role: .config, relativePath: "preprocessor_config.json"),
artifactSpec(id: "model-config", role: .config, relativePath: "config.json"),
artifactSpec(id: "model-weights", role: .weights, relativePath: "model.safetensors"),
artifactSpec(id: "tokenizer", role: .tokenizer, relativePath: "tokenizer.json"),
artifactSpec(id: "tokenizer-config", role: .tokenizer, relativePath: "tokenizer_config.json"),
artifactSpec(id: "special-tokens-map", role: .auxiliary, relativePath: "special_tokens_map.json", required: false),
artifactSpec(id: "added-tokens", role: .auxiliary, relativePath: "added_tokens.json", required: false),
artifactSpec(id: "preprocessor-config", role: .config, relativePath: "preprocessor_config.json"),
] + voiceCacheArtifacts,
tokenizer: TokenizerSpec(
kind: "huggingface",
Expand Down
Loading