File size: 6,882 Bytes
64f7370 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | import ASRKit
import Foundation
import SpeechCore
// SpeechKit regression harness (macOS).
// Usage:
// asrkit-cli <iphone-asr-dir> [--stress]
// asrkit-cli <iphone-asr-dir> --file path/to/audio.wav
// asrkit-cli <iphone-asr-dir> --file path/to/audio.wav --repeat 10
// Verifies each stage against Python references, exercising the SAME
// public API integrators use (ASRTranscriber + AssetBundle).
setvbuf(stdout, nil, _IONBF, 0)
let args = Array(CommandLine.arguments.dropFirst())
func value(after flag: String) -> String? {
guard let i = args.firstIndex(of: flag), i + 1 < args.count else { return nil }
return args[i + 1]
}
let fileArg = value(after: "--file")
let repeatCount = value(after: "--repeat").flatMap(Int.init) ?? 1
let rootArg = args.first { arg in
!arg.hasPrefix("--") && arg != fileArg && arg != value(after: "--repeat")
}
let root = URL(fileURLWithPath: rootArg ?? FileManager.default.currentDirectoryPath)
let refs = root.appendingPathComponent("ios_refs")
let bundleURL = root.appendingPathComponent("dist/ASRModels.bundle")
func loadFloats(_ url: URL) throws -> [Float] {
try Data(contentsOf: url).withUnsafeBytes { Array($0.bindMemory(to: Float.self)) }
}
do {
if let fileArg {
print("loading transcriber ...")
let transcriber = try ASRTranscriber(bundleURL: bundleURL)
transcriber.warmUp()
let url = URL(fileURLWithPath: fileArg)
let samples = try AudioResampler.loadFile(url)
func footprintMB() -> Double {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(
MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size)
_ = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
return Double(info.phys_footprint) / 1_048_576
}
print(String(format: "[file] start footprint %.0f MB", footprintMB()))
var result: ASRTranscriber.Result?
let passes = max(repeatCount, 1)
for i in 1...passes {
let r = try transcriber.transcribe(samples)
result = r
if passes > 1 {
print(String(format: "[file] pass %d/%d footprint %.0f MB transcript: %@",
i, passes, footprintMB(), r.text))
}
}
guard let result else { exit(1) }
let t = result.timings
print("[file] transcript: \(result.text)")
print("[file] tokenIDs: \(result.tokenIDs)")
print("[file] hitStop: \(result.hitStop)")
print(String(format: "[file] %.0fms: mel %.0f + tower %.0f + decode %.0f",
t.total * 1000, t.mel * 1000, t.tower * 1000, t.decode * 1000))
exit(0)
}
let refAudio = try loadFloats(refs.appendingPathComponent("ref_audio.bin"))
let refMel = try loadFloats(refs.appendingPathComponent("ref_mel.bin"))
let refProjected = try loadFloats(refs.appendingPathComponent("ref_projected.bin"))
let refTranscript = try String(contentsOf: refs.appendingPathComponent("ref_transcript.txt"))
.trimmingCharacters(in: .whitespacesAndNewlines)
print("loading transcriber (verifyAssets: true) ...")
var options = ASRTranscriber.Options()
options.verifyAssets = true
let transcriber = try ASRTranscriber(bundleURL: bundleURL, options: options)
transcriber.warmUp()
print("[assets] bundle verified PASS")
// stage 1: mel parity
let (melOut, encLen, bucketFrames) = transcriber.mel.extract(refAudio)
var melDiff: Float = 0
for t in 0..<bucketFrames {
for m in 0..<128 {
melDiff = max(melDiff, abs(melOut[m * bucketFrames + t] - refMel[m * 3000 + t]))
}
}
print(String(format: "[mel] max_abs_diff=%.6f encLen=%d bucket=%d %@",
melDiff, encLen, bucketFrames, melDiff < 1e-3 ? "PASS" : "FAIL"))
// stage 2: tower parity
let masks = try transcriber.decoder.generateMasks(encLen: encLen)
let audioEmbeds = try transcriber.tower.embed(
mel: melOut, bucketFrames: bucketFrames, attnMask390: masks.attn,
validMask390: masks.valid, sampleCount: refAudio.count)
let flat = audioEmbeds.flatMap { $0 }
var dot = 0.0, na = 0.0, nb = 0.0
for (x, y) in zip(flat, refProjected) {
dot += Double(x) * Double(y)
na += Double(x) * Double(x)
nb += Double(y) * Double(y)
}
let cos = dot / (na.squareRoot() * nb.squareRoot() + 1e-12)
print(String(format: "[tower] tokens=%d cosine=%.6f %@",
audioEmbeds.count, cos, cos > 0.995 ? "PASS" : "FAIL"))
// stage 3: e2e via public API
let result = try transcriber.transcribe(refAudio)
let t = result.timings
print("[e2e] transcript: \(result.text)")
print("[e2e] tokenIDs: \(result.tokenIDs)")
print("[e2e] reference: \(refTranscript)")
print(String(format: "[e2e] %@ (%.0fms: mel %.0f + tower %.0f + decode %.0f)",
result.text == refTranscript ? "PASS" : "FAIL",
t.total * 1000, t.mel * 1000, t.tower * 1000, t.decode * 1000))
// error contract checks
do {
_ = try transcriber.transcribe([Float](repeating: 0, count: 100))
print("[errors] audioTooShort FAIL (no throw)")
} catch let e as SpeechError {
print("[errors] audioTooShort -> \(e) PASS")
}
let token = ASRTranscriber.CancellationToken()
token.cancel()
do {
_ = try transcriber.transcribe(refAudio, cancellation: token)
print("[errors] cancellation FAIL (no throw)")
} catch SpeechError.cancelled {
print("[errors] cancellation -> cancelled PASS")
}
if CommandLine.arguments.contains("--stress") {
func footprintMB() -> Double {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(
MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size)
_ = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
return Double(info.phys_footprint) / 1_048_576
}
print(String(format: "[stress] start footprint %.0f MB", footprintMB()))
for i in 1...100 {
_ = try transcriber.transcribe(refAudio)
if i % 20 == 0 {
print(String(format: "[stress] pass %3d footprint %.0f MB", i, footprintMB()))
}
}
print(String(format: "[stress] end footprint %.0f MB", footprintMB()))
}
} catch {
print("ERROR: \(error)")
exit(1)
}
|