Skip to content

Commit f8832cb

Browse files
authored
Add language identification swiftui demo (#729)
1 parent fabd30e commit f8832cb

File tree

16 files changed

+1057
-0
lines changed

16 files changed

+1057
-0
lines changed

ios-swiftui/SherpaOnnx/SherpaOnnx/Model.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@ func getBilingualStreamingZhEnParaformer() -> SherpaOnnxOnlineModelConfig {
4848
)
4949
}
5050

51+
// https://k2-fsa.github.io/sherpa/onnx/pretrained_models/whisper/tiny.en.html#tiny-en
52+
//
53+
func getLanguageIdentificationTiny() -> SherpaOnnxSpokenLanguageIdentificationConfig
54+
{
55+
let encoder = getResource("tiny-encoder.int8", "onnx")
56+
let decoder = getResource("tiny-decoder.int8", "onnx")
57+
58+
let whisperConfig = sherpaOnnxSpokenLanguageIdentificationWhisperConfig(
59+
encoder: encoder,
60+
decoder: decoder
61+
)
62+
63+
let config = sherpaOnnxSpokenLanguageIdentificationConfig(
64+
whisper: whisperConfig,
65+
numThreads: 1,
66+
debug: 1,
67+
provider: "cpu"
68+
)
69+
return config
70+
}
71+
72+
5173
/// Please refer to
5274
/// https://k2-fsa.github.io/sherpa/onnx/pretrained_models/index.html
5375
/// to add more models if you need

ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.pbxproj

Lines changed: 667 additions & 0 deletions
Large diffs are not rendered by default.

ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"colors" : [
3+
{
4+
"idiom" : "universal"
5+
}
6+
],
7+
"info" : {
8+
"author" : "xcode",
9+
"version" : 1
10+
}
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "k2-1024x1024.png",
5+
"idiom" : "universal",
6+
"platform" : "ios",
7+
"size" : "1024x1024"
8+
}
9+
],
10+
"info" : {
11+
"author" : "xcode",
12+
"version" : 1
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"platform" : "ios",
6+
"size" : "1024x1024"
7+
}
8+
],
9+
"info" : {
10+
"author" : "xcode",
11+
"version" : 1
12+
}
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// ContentView.swift
3+
// SherpaOnnxLangID
4+
//
5+
// Created by knight on 2024/4/1.
6+
//
7+
8+
import SwiftUI
9+
10+
struct ContentView: View {
11+
@StateObject var viewModel = ViewModel()
12+
13+
var body: some View {
14+
VStack {
15+
Text("ASR with Next-gen Kaldi")
16+
.font(.title)
17+
if viewModel.status == .stop {
18+
Text("See https://github.com/k2-fsa/sherpa-onnx")
19+
Text("Press the Start button to run!")
20+
}
21+
if viewModel.status == .recording {
22+
Text("Stop will show recording language.")
23+
}
24+
Spacer()
25+
Text("Recording language is: \(viewModel.language)")
26+
.frame(maxWidth: .infinity)
27+
Spacer()
28+
Button {
29+
toggleRecorder()
30+
} label: {
31+
Text(viewModel.status == .stop ? "Start" : "Stop")
32+
}
33+
}
34+
.padding()
35+
}
36+
37+
private func toggleRecorder() {
38+
Task {
39+
await viewModel.toggleRecorder()
40+
}
41+
}
42+
}
43+
44+
#Preview {
45+
ContentView()
46+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// SherpaOnnxLangIDApp.swift
3+
// SherpaOnnxLangID
4+
//
5+
// Created by knight on 2024/4/1.
6+
//
7+
8+
import SwiftUI
9+
10+
@main
11+
struct SherpaOnnxLangIDApp: App {
12+
var body: some Scene {
13+
WindowGroup {
14+
ContentView()
15+
}
16+
}
17+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//
2+
// ViewModel.swift
3+
// SherpaOnnxLangID
4+
//
5+
// Created by knight on 2024/4/1.
6+
//
7+
8+
import SwiftUI
9+
import AVFoundation
10+
11+
enum Status {
12+
case stop
13+
case recording
14+
}
15+
16+
@MainActor
17+
class ViewModel:ObservableObject {
18+
@Published var status: Status = .stop
19+
20+
@Published var language: String = ""
21+
22+
var languageIdentifier: SherpaOnnxSpokenLanguageIdentificationWrapper? = nil
23+
var audioEngine: AVAudioEngine? = nil
24+
25+
var voices: [Float] = []
26+
27+
init() {
28+
initRecorder()
29+
initRecognizer()
30+
}
31+
32+
private func initRecognizer() {
33+
var config = getLanguageIdentificationTiny()
34+
self.languageIdentifier = SherpaOnnxSpokenLanguageIdentificationWrapper(config: &config)
35+
}
36+
37+
private func initRecorder() {
38+
print("init recorder")
39+
audioEngine = AVAudioEngine()
40+
let inputNode = self.audioEngine?.inputNode
41+
let bus = 0
42+
let inputFormat = inputNode?.outputFormat(forBus: bus)
43+
let outputFormat = AVAudioFormat(
44+
commonFormat: .pcmFormatFloat32,
45+
sampleRate: 16000, channels: 1,
46+
interleaved: false)!
47+
48+
let converter = AVAudioConverter(from: inputFormat!, to: outputFormat)!
49+
50+
inputNode!.installTap(
51+
onBus: bus,
52+
bufferSize: 1024,
53+
format: inputFormat
54+
) {
55+
(buffer: AVAudioPCMBuffer, when: AVAudioTime) in
56+
var newBufferAvailable = true
57+
58+
let inputCallback: AVAudioConverterInputBlock = {
59+
inNumPackets, outStatus in
60+
if newBufferAvailable {
61+
outStatus.pointee = .haveData
62+
newBufferAvailable = false
63+
64+
return buffer
65+
} else {
66+
outStatus.pointee = .noDataNow
67+
return nil
68+
}
69+
}
70+
71+
let convertedBuffer = AVAudioPCMBuffer(
72+
pcmFormat: outputFormat,
73+
frameCapacity:
74+
AVAudioFrameCount(outputFormat.sampleRate)
75+
* buffer.frameLength
76+
/ AVAudioFrameCount(buffer.format.sampleRate))!
77+
78+
var error: NSError?
79+
let _ = converter.convert(
80+
to: convertedBuffer,
81+
error: &error, withInputFrom: inputCallback)
82+
83+
// TODO(fangjun): Handle status != haveData
84+
85+
let array = convertedBuffer.array()
86+
if !array.isEmpty {
87+
self.voices.append(contentsOf: array)
88+
}
89+
}
90+
}
91+
92+
public func toggleRecorder() async{
93+
if status == .stop {
94+
await startRecorder()
95+
} else {
96+
await stopRecorder()
97+
}
98+
}
99+
100+
private func startRecorder() async {
101+
await MainActor.run {
102+
self.language = ""
103+
}
104+
if !self.voices.isEmpty {
105+
self.voices = []
106+
}
107+
do {
108+
try self.audioEngine?.start()
109+
status = .recording
110+
print("started")
111+
} catch let error as NSError {
112+
print("Got an error starting audioEngine: \(error.domain), \(error)")
113+
}
114+
}
115+
116+
private func stopRecorder() async {
117+
audioEngine?.stop()
118+
print("stopped, and begin identify language")
119+
await self.identify()
120+
status = .stop
121+
}
122+
123+
private func identify() async {
124+
let result = self.languageIdentifier? .decode(samples: self.voices)
125+
if let language = result?.lang {
126+
await MainActor.run {
127+
self.language = language
128+
}
129+
}
130+
}
131+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// SherpaOnnxLangIDTests.swift
3+
// SherpaOnnxLangIDTests
4+
//
5+
// Created by knight on 2024/4/1.
6+
//
7+
8+
import XCTest
9+
@testable import SherpaOnnxLangID
10+
11+
final class SherpaOnnxLangIDTests: XCTestCase {
12+
13+
override func setUpWithError() throws {
14+
// Put setup code here. This method is called before the invocation of each test method in the class.
15+
}
16+
17+
override func tearDownWithError() throws {
18+
// Put teardown code here. This method is called after the invocation of each test method in the class.
19+
}
20+
21+
func testExample() throws {
22+
// This is an example of a functional test case.
23+
// Use XCTAssert and related functions to verify your tests produce the correct results.
24+
// Any test you write for XCTest can be annotated as throws and async.
25+
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
26+
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
27+
}
28+
29+
func testPerformanceExample() throws {
30+
// This is an example of a performance test case.
31+
self.measure {
32+
// Put the code you want to measure the time of here.
33+
}
34+
}
35+
36+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// SherpaOnnxLangIDUITests.swift
3+
// SherpaOnnxLangIDUITests
4+
//
5+
// Created by knight on 2024/4/1.
6+
//
7+
8+
import XCTest
9+
10+
final class SherpaOnnxLangIDUITests: XCTestCase {
11+
12+
override func setUpWithError() throws {
13+
// Put setup code here. This method is called before the invocation of each test method in the class.
14+
15+
// In UI tests it is usually best to stop immediately when a failure occurs.
16+
continueAfterFailure = false
17+
18+
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19+
}
20+
21+
override func tearDownWithError() throws {
22+
// Put teardown code here. This method is called after the invocation of each test method in the class.
23+
}
24+
25+
func testExample() throws {
26+
// UI tests must launch the application that they test.
27+
let app = XCUIApplication()
28+
app.launch()
29+
30+
// Use XCTAssert and related functions to verify your tests produce the correct results.
31+
}
32+
33+
func testLaunchPerformance() throws {
34+
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
35+
// This measures how long it takes to launch your application.
36+
measure(metrics: [XCTApplicationLaunchMetric()]) {
37+
XCUIApplication().launch()
38+
}
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)