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
13 changes: 13 additions & 0 deletions Sources/FluidAudio/ModelNames.swift
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,21 @@ public enum Repo: String, CaseIterable, Sendable {
/// Encoder precision for the v3 Parakeet TDT 0.6B encoder.
public enum ParakeetEncoderPrecision: String, Sendable, CaseIterable {
case int8
/// Opt-in int8 per-channel linear re-quantization of the v3 encoder
/// (`Encoder_v2.mlmodelc`, 568M vs 425M). Avoids token corruption the
/// original 6-bit-LUT palettized `Encoder.mlmodelc` exhibits under
/// specific right-context (issue #760). `.int8` remains the default and
/// keeps loading the original file; select this explicitly to use the
/// rebuild.
case int8V2 = "int8-v2"
case int4

public var encoderFileName: String {
switch self {
case .int8:
return ModelNames.ASR.encoderFile
case .int8V2:
return ModelNames.ASR.encoderV2File
case .int4:
return ModelNames.ASR.encoderInt4File
}
Expand Down Expand Up @@ -391,6 +400,10 @@ public enum ModelNames {
/// Joint decoder variant for v3 that exposes top-K outputs
/// (`top_k_ids`, `top_k_logits`) used for language-aware script filtering.
public static let jointV3File = "JointDecisionv3.mlmodelc"
/// v3 encoder re-quantized as int8 per-channel linear (issue #760).
/// Published alongside the immutable original `Encoder.mlmodelc`;
/// HF repo files are never mutated in place, fixes ship as new names.
public static let encoderV2File = "Encoder_v2.mlmodelc"
public static let encoderInt4File = "EncoderInt4.mlmodelc"
public static let ctcHeadFile = ctcHead + ".mlmodelc"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,8 @@ enum TranscribeCommand {
--output-json <file> Save full transcription to JSON
--model-version <v2|v3|110m> ASR model version (default: v3)
--model-dir <path> Local model directory (skips download)
--encoder-precision <int8|int4> Encoder quantization (default: int8)
--encoder-precision <int8|int8-v2|int4> Encoder quantization (default: int8;
int8-v2 = int8-linear rebuild, avoids #760)
--language <code> Language hint (e.g., en, de, fr, es)
--custom-vocab <file> Apply vocabulary boosting in batch mode
--no-mel-context Disable 80ms mel-context prepend for long-form batch ASR
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import XCTest

@testable import FluidAudio

/// Opt-in Encoder_v2 (int8-linear, issue #760) precision selection.
final class AsrModelsEncoderV2Tests: XCTestCase {

func testEncoderFileNames() {
XCTAssertEqual(ParakeetEncoderPrecision.int8.encoderFileName, "Encoder.mlmodelc")
XCTAssertEqual(ParakeetEncoderPrecision.int8V2.encoderFileName, "Encoder_v2.mlmodelc")
XCTAssertEqual(ParakeetEncoderPrecision.int4.encoderFileName, "EncoderInt4.mlmodelc")
}

func testVariantStringRoundTrip() {
// The download variant string must map back to the same precision so
// ModelHub's required-model set matches the file AsrModels loads.
for precision in ParakeetEncoderPrecision.allCases {
XCTAssertEqual(ParakeetEncoderPrecision(rawValue: precision.rawValue), precision)
}
}

func testRequiredModelsV3PerPrecision() {
// int8 stays the default and keeps requiring the original encoder;
// int8-v2 is strictly opt-in.
let int8 = ModelNames.ASR.requiredModelsV3(precision: .int8)
XCTAssertTrue(int8.contains("Encoder.mlmodelc"))
XCTAssertFalse(int8.contains("Encoder_v2.mlmodelc"))

let int8V2 = ModelNames.ASR.requiredModelsV3(precision: .int8V2)
XCTAssertTrue(int8V2.contains("Encoder_v2.mlmodelc"))
XCTAssertFalse(int8V2.contains("Encoder.mlmodelc"))
}

func testModelsExistChecksPrecisionSpecificEncoder() throws {
let parentDir = FileManager.default.temporaryDirectory
.appendingPathComponent("AsrModelsEncoderV2Tests-\(UUID().uuidString)")
let repoDir = parentDir.appendingPathComponent(Repo.parakeetV3.folderName)
defer { try? FileManager.default.removeItem(at: parentDir) }

for file in [
ModelNames.ASR.preprocessorFile,
ModelNames.ASR.decoderFile,
ModelNames.ASR.jointV3File,
ModelNames.ASR.encoderV2File,
] {
try FileManager.default.createDirectory(
at: repoDir.appendingPathComponent(file), withIntermediateDirectories: true)
}
try Data("{}".utf8).write(
to: repoDir.appendingPathComponent(ModelNames.ASR.vocabularyFile))

// Only Encoder_v2 on disk: satisfies an explicit int8-v2 request but
// NOT the int8 default (which still requires Encoder.mlmodelc).
XCTAssertTrue(
AsrModels.modelsExist(at: repoDir, version: .v3, encoderPrecision: .int8V2))
XCTAssertFalse(
AsrModels.modelsExist(at: repoDir, version: .v3, encoderPrecision: .int8))
}
}
Loading