Files
FluidAudio/Sources/FluidAudioCLI/FluidAudioCLI.swift
Alex 2593f55415 Add Japanese ASR support with JSUT and Common Voice datasets (#478)
## Summary

Adds comprehensive Japanese ASR support to FluidAudio with benchmark
datasets and CLI commands.

## Changes

### Core Japanese ASR Support
- **CtcJaManager.swift** - Japanese CTC transcription manager
(actor-based)
- **CtcJaModels.swift** - Japanese model loading and management
- **ModelNames.swift** - Added Japanese model registry (`parakeetCtcJa`,
`CTCJa` enum)
- **AsrModels.swift** - Added `.ctcJa` model version (3,072 vocab, 1,024
hidden, blank_id=3072)
- **AsrManager.swift** - Added `.ctcJa` case with error directing to
`CtcJaManager`

### CLI Commands
- **JapaneseAsrBenchmark.swift** (459 lines) - New `ja-benchmark`
command
  - JSUT basic5000 dataset support
  - Mozilla Common Voice (MCV) test set support
  - Auto-download capability
  - CER (Character Error Rate) evaluation
- **DownloadCommand.swift** - Added JSUT and MCV Japanese dataset
downloads
- **TranscribeCommand.swift** - Added `.ctcJa` model version support
- **AsrBenchmark.swift** - Added `.ctcJa` switch case

### Dataset Support
- **JapaneseDatasetDownloader.swift** (387 lines) - Dataset download and
parsing
  - JSUT basic5000 (5,000 sentences, clean studio recordings)
  - Mozilla Common Voice Japanese test split
  - Efficient streaming downloads
  - Metadata extraction and validation

## Usage

### CLI Commands
```bash
# Benchmark on JSUT basic5000 (100 samples)
swift run fluidaudiocli ja-benchmark --dataset jsut --samples 100

# Benchmark on Common Voice test (500 samples, auto-download)
swift run fluidaudiocli ja-benchmark --dataset cv-test --samples 500 --auto-download

# Download datasets
swift run fluidaudiocli download --dataset jsut
swift run fluidaudiocli download --dataset cv-ja-test
```

### Swift API
```swift
// Load and use Japanese CTC transcription
let manager = try await CtcJaManager.load()
let text = try manager.transcribe(audioURL: japaneseAudioFile)
```

## Model Info
- **Repo**: `FluidInference/parakeet-ctc-0.6b-ja-coreml`
- **Architecture**: 600M parameter CTC-only
- **Vocabulary**: 3,072 Japanese SentencePiece tokens + 1 blank (id:
3072)
- **Encoder**: 1,024 hidden size
- **Expected CER**: 6.5% on JSUT basic5000, 13.3% on MCV 16.1 test

## Testing
- ✅ Builds successfully (`swift build`)
- ✅ Model loading integration tested
- ✅ CLI commands compile and link correctly
- ⏳ Runtime benchmark testing pending (requires model download)

## Related
- Mobius PR #39: Japanese CTC CoreML conversion
(https://github.com/FluidInference/mobius/pull/39)

🤖 Generated with Claude Code
<!-- devin-review-badge-begin -->

---

<a href="https://app.devin.ai/review/fluidinference/fluidaudio/pull/478"
target="_blank">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1">
<img
src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1"
alt="Open with Devin">
  </picture>
</a>
<!-- devin-review-badge-end -->

---------
2026-04-04 12:57:32 -04:00

194 lines
7.9 KiB
Swift

#if os(macOS)
import AVFoundation
import FluidAudio
import Foundation
import MachTaskSelfWrapper
// Using @main instead of main.swift for Swift 6 compatibility.
// This provides an explicit async context and clear isolation semantics.
@main
struct FluidAudioCLI {
static let cliLogger = AppLogger(category: "Main")
static func main() async {
let arguments = CommandLine.arguments
guard arguments.count > 1 else {
printUsage()
exitWithPeakMemory(1)
}
// Log system information once at application startup
await SystemInfo.logOnce(using: cliLogger)
let command = arguments[1]
defer {
logPeakMemoryUsage()
}
switch command {
case "vad-benchmark":
await VadBenchmark.runVadBenchmark(arguments: Array(arguments.dropFirst(2)))
case "vad-analyze":
await VadAnalyzeCommand.run(arguments: Array(arguments.dropFirst(2)))
case "asr-benchmark":
await ASRBenchmark.runASRBenchmark(arguments: Array(arguments.dropFirst(2)))
case "fleurs-benchmark":
await FLEURSBenchmark.runCLI(arguments: Array(arguments.dropFirst(2)))
case "transcribe":
await TranscribeCommand.run(arguments: Array(arguments.dropFirst(2)))
case "multi-stream":
await MultiStreamCommand.run(arguments: Array(arguments.dropFirst(2)))
case "tts":
await TTS.run(arguments: Array(arguments.dropFirst(2)))
case "diarization-benchmark":
await StreamDiarizationBenchmark.run(arguments: Array(arguments.dropFirst(2)))
case "process":
await ProcessCommand.run(arguments: Array(arguments.dropFirst(2)))
case "download":
await DownloadCommand.run(arguments: Array(arguments.dropFirst(2)))
case "parakeet-eou":
await ParakeetEouCommand.main(Array(arguments.dropFirst(2)))
case "ctc-earnings-benchmark":
await CtcEarningsBenchmark.runCLI(arguments: Array(arguments.dropFirst(2)))
case "sortformer":
await SortformerCommand.run(arguments: Array(arguments.dropFirst(2)))
case "sortformer-benchmark":
await SortformerBenchmark.run(arguments: Array(arguments.dropFirst(2)))
case "lseend":
await LSEENDCommand.run(arguments: Array(arguments.dropFirst(2)))
case "lseend-benchmark":
await LSEENDBenchmark.run(arguments: Array(arguments.dropFirst(2)))
case "qwen3-benchmark":
await Qwen3AsrBenchmark.runCLI(arguments: Array(arguments.dropFirst(2)))
case "qwen3-transcribe":
await Qwen3TranscribeCommand.run(arguments: Array(arguments.dropFirst(2)))
case "g2p-benchmark":
await G2PBenchmark.run(arguments: Array(arguments.dropFirst(2)))
case "nemotron-benchmark":
await NemotronBenchmark.run(arguments: Array(arguments.dropFirst(2)))
case "nemotron-transcribe":
await NemotronTranscribe.run(arguments: Array(arguments.dropFirst(2)))
case "ctc-zh-cn-transcribe":
await CtcZhCnTranscribeCommand.run(arguments: Array(arguments.dropFirst(2)))
case "ctc-zh-cn-benchmark":
await CtcZhCnBenchmark.run(arguments: Array(arguments.dropFirst(2)))
case "ja-benchmark":
await JapaneseAsrBenchmark.run(arguments: Array(arguments.dropFirst(2)))
case "help", "--help", "-h":
printUsage()
default:
cliLogger.error("Unknown command: \(command)")
printUsage()
exit(1)
}
}
static func printUsage() {
cliLogger.info(
"""
FluidAudio CLI
Usage: fluidaudio <command> [options]
Commands:
process Process a single audio file for diarization
diarization-benchmark Run diarization benchmark
vad-benchmark Run VAD-specific benchmark
vad-analyze Inspect VAD segmentation and streaming events
asr-benchmark Run ASR benchmark on LibriSpeech
fleurs-benchmark Run multilingual ASR benchmark on FLEURS dataset
transcribe Transcribe audio file using streaming ASR
multi-stream Transcribe multiple audio files in parallel
tts Synthesize speech from text using Kokoro TTS
parakeet-eou Run Parakeet EOU Streaming ASR on a single file
ctc-earnings-benchmark Run CTC keyword spotting benchmark on Earnings22
sortformer Run Sortformer streaming diarization
sortformer-benchmark Run Sortformer benchmark on AMI dataset
lseend Run LS-EEND diarization on a single file
lseend-benchmark Run LS-EEND benchmark on AMI dataset
qwen3-benchmark Run Qwen3 ASR benchmark
qwen3-transcribe Transcribe using Qwen3 ASR
g2p-benchmark Run multilingual G2P benchmark
nemotron-benchmark Run Nemotron 0.6B streaming ASR benchmark
nemotron-transcribe Transcribe custom audio files with Nemotron
ctc-zh-cn-transcribe Transcribe Mandarin Chinese audio with Parakeet CTC
ctc-zh-cn-benchmark Run CTC zh-CN benchmark on THCHS-30 dataset
ja-benchmark Run Japanese ASR benchmark on JSUT/Common Voice
download Download evaluation datasets
help Show this help message
Run 'fluidaudio <command> --help' for command-specific options.
Examples:
fluidaudio process audio.wav --output results.json
fluidaudio diarization-benchmark --single-file ES2004a
fluidaudio asr-benchmark --subset test-clean --max-files 100
fluidaudio fleurs-benchmark --languages en_us,fr_fr --samples 10
fluidaudio transcribe audio.wav --low-latency
fluidaudio multi-stream audio1.wav audio2.wav
fluidaudio tts "Hello world" --output hello.wav
fluidaudio vad-analyze audio.wav --streaming
fluidaudio download --dataset ami-sdm
fluidaudio ja-benchmark --dataset jsut --samples 100
fluidaudio ja-benchmark --dataset cv-test --samples 500 --auto-download
"""
)
}
static func fetchPeakMemoryUsageBytes() -> UInt64? {
var info = task_vm_info_data_t()
var count =
mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.size)
/ mach_msg_type_number_t(MemoryLayout<natural_t>.size)
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(
get_current_task_port(),
task_flavor_t(TASK_VM_INFO),
$0,
&count)
}
}
guard result == KERN_SUCCESS else {
return nil
}
return info.resident_size_peak
}
static func logPeakMemoryUsage() {
guard let peakBytes = fetchPeakMemoryUsageBytes() else {
cliLogger.error("Unable to determine peak memory usage")
return
}
let peakGigabytes = Double(peakBytes) / 1024.0 / 1024.0 / 1024.0
let formatted = String(format: "%.3f", peakGigabytes)
cliLogger.info(
"Peak memory usage (process-wide): \(formatted) GB"
)
}
static func exitWithPeakMemory(_ code: Int32) -> Never {
logPeakMemoryUsage()
exit(code)
}
}
#else
#error("FluidAudioCLI is only supported on macOS")
#endif