Skip to content

Commit 125553a

Browse files
authored
Merge pull request #46 from Picovoice/ios-profile
2 parents bac27f0 + 912333e commit 125553a

File tree

4 files changed

+107
-1
lines changed

4 files changed

+107
-1
lines changed

recipes/llm-voice-assistant/ios/LLMVoiceAssistantDemo.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
02A1194B268D39A700A2AC99 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A1194A268D39A700A2AC99 /* ContentView.swift */; };
1212
02A1194D268D39AB00A2AC99 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 02A1194C268D39AB00A2AC99 /* Assets.xcassets */; };
1313
02A1195F268D3FD600A2AC99 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A1195E268D3FD600A2AC99 /* ViewModel.swift */; };
14+
02B5889E2DD41233007E7026 /* TPSProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B5889D2DD41233007E7026 /* TPSProfiler.swift */; };
15+
02B5889F2DD41233007E7026 /* RTFProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B5889C2DD41233007E7026 /* RTFProfiler.swift */; };
1416
34C504D02D271C5E0003405B /* ios_voice_processor in Frameworks */ = {isa = PBXBuildFile; productRef = 34C504CF2D271C5E0003405B /* ios_voice_processor */; };
1517
34C504D32D271E4B0003405B /* ios_voice_processor in Frameworks */ = {isa = PBXBuildFile; productRef = 34C504D22D271E4B0003405B /* ios_voice_processor */; };
1618
34C504D62D2728300003405B /* Porcupine in Frameworks */ = {isa = PBXBuildFile; productRef = 34C504D52D2728300003405B /* Porcupine */; };
@@ -32,6 +34,8 @@
3234
02A1194C268D39AB00A2AC99 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3335
02A11951268D39AB00A2AC99 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3436
02A1195E268D3FD600A2AC99 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
37+
02B5889C2DD41233007E7026 /* RTFProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTFProfiler.swift; sourceTree = "<group>"; };
38+
02B5889D2DD41233007E7026 /* TPSProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TPSProfiler.swift; sourceTree = "<group>"; };
3539
C789D24A2BEA8E5D005FDB10 /* LoadModelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadModelView.swift; sourceTree = "<group>"; };
3640
C789D24C2BEA8EAE005FDB10 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
3741
C789D2502BEAD752005FDB10 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
@@ -76,6 +80,8 @@
7680
02A11947268D39A700A2AC99 /* LLMVoiceAssistantDemo */ = {
7781
isa = PBXGroup;
7882
children = (
83+
02B5889C2DD41233007E7026 /* RTFProfiler.swift */,
84+
02B5889D2DD41233007E7026 /* TPSProfiler.swift */,
7985
C7E87DC02C07C65C00C32367 /* resources */,
8086
02A11948268D39A700A2AC99 /* LLMVoiceAssistantDemoApp.swift */,
8187
02A1194A268D39A700A2AC99 /* ContentView.swift */,
@@ -178,6 +184,8 @@
178184
buildActionMask = 2147483647;
179185
files = (
180186
C789D24D2BEA8EAE005FDB10 /* ChatView.swift in Sources */,
187+
02B5889E2DD41233007E7026 /* TPSProfiler.swift in Sources */,
188+
02B5889F2DD41233007E7026 /* RTFProfiler.swift in Sources */,
181189
C789D24B2BEA8E5D005FDB10 /* LoadModelView.swift in Sources */,
182190
02A1194B268D39A700A2AC99 /* ContentView.swift in Sources */,
183191
C789D2512BEAD752005FDB10 /* Constants.swift in Sources */,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// Copyright 2025 Picovoice Inc.
3+
// You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
4+
// file accompanying this source.
5+
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
6+
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
7+
// specific language governing permissions and limitations under the License.
8+
//
9+
10+
import Foundation
11+
12+
class RTFProfiler {
13+
private let sampleRate: Int32
14+
private var computeSec: Double
15+
private var audioSec: Double
16+
private var tickSec: Double
17+
18+
init(sampleRate: Int32) {
19+
self.sampleRate = sampleRate
20+
self.computeSec = 0.0
21+
self.audioSec = 0.0
22+
self.tickSec = 0.0
23+
}
24+
25+
func tick() {
26+
self.tickSec = Double(DispatchTime.now().uptimeNanoseconds) / 1e9
27+
}
28+
29+
func tock(pcm: [Int16]?) {
30+
let nowSec = Double(DispatchTime.now().uptimeNanoseconds) / 1e9
31+
self.computeSec += nowSec - self.tickSec
32+
33+
if let pcm = pcm, !pcm.isEmpty {
34+
self.audioSec += Double(pcm.count) / Double(self.sampleRate)
35+
}
36+
}
37+
38+
func rtf() -> Double {
39+
let rtf = audioSec > 0.0 ? computeSec / audioSec : 0.0
40+
self.computeSec = 0.0
41+
self.audioSec = 0.0
42+
return rtf
43+
}
44+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// Copyright 2025 Picovoice Inc.
3+
// You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
4+
// file accompanying this source.
5+
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
6+
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
7+
// specific language governing permissions and limitations under the License.
8+
//
9+
10+
import Foundation
11+
12+
class TPSProfiler {
13+
private var numTokens: Int
14+
private var startSec: UInt64
15+
private var endSec: UInt64
16+
17+
init() {
18+
self.numTokens = 0
19+
self.startSec = 0
20+
self.endSec = 0
21+
}
22+
23+
func tock() {
24+
if self.startSec == 0 {
25+
self.startSec = DispatchTime.now().uptimeNanoseconds
26+
} else {
27+
self.endSec = DispatchTime.now().uptimeNanoseconds
28+
self.numTokens += 1
29+
}
30+
}
31+
32+
func tps() -> Double {
33+
let elapsedSec = Double(self.endSec - self.startSec) / 1e9
34+
let tps = elapsedSec > 0 ? Double(self.numTokens) / elapsedSec : 0.0
35+
36+
self.numTokens = 0
37+
self.startSec = 0
38+
self.endSec = 0
39+
40+
return tps
41+
}
42+
}

recipes/llm-voice-assistant/ios/LLMVoiceAssistantDemo/ViewModel.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 Picovoice Inc.
2+
// Copyright 2024-2025 Picovoice Inc.
33
// You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
44
// file accompanying this source.
55
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
@@ -34,6 +34,8 @@ class ViewModel: ObservableObject {
3434
private var picollm: PicoLLM?
3535
private var dialog: PicoLLMDialog?
3636

37+
private let picoLLMProfiler = TPSProfiler()
38+
3739
private var chatState: ChatState = .WAKEWORD
3840

3941
private var audioStream: AudioPlayerStream?
@@ -193,6 +195,9 @@ You can download directly to your device or airdrop from a Mac.
193195

194196
private func streamCallback(completion: String) {
195197
DispatchQueue.main.async { [self] in
198+
199+
picoLLMProfiler.tock()
200+
196201
if self.stopPhrases.contains(completion) || chatState != .GENERATE {
197202
return
198203
}
@@ -223,6 +228,7 @@ You can download directly to your device or airdrop from a Mac.
223228
streamCallback: streamCallback)
224229

225230
try dialog!.addLLMResponse(content: result.completion)
231+
print(String(format: "TPS: %.2f", picoLLMProfiler.tps()))
226232

227233
DispatchQueue.main.async { [self] in
228234
if result.endpoint == .interrupted {
@@ -251,6 +257,7 @@ You can download directly to your device or airdrop from a Mac.
251257
do {
252258
audioStream!.resetAudioPlayer()
253259
let orcaStream = try self.orca!.streamOpen()
260+
let orcaProfiler = RTFProfiler(sampleRate: self.orca!.sampleRate!)
254261

255262
var warmup = true
256263
var warmupBuffer: [Int16] = []
@@ -268,7 +275,9 @@ You can download directly to your device or airdrop from a Mac.
268275
completionArray.removeFirst()
269276
}
270277

278+
orcaProfiler.tick()
271279
let pcm = try orcaStream.synthesize(text: token)
280+
orcaProfiler.tock(pcm: pcm)
272281
if pcm != nil {
273282
if warmup {
274283
warmupBuffer.append(contentsOf: pcm!)
@@ -288,10 +297,13 @@ You can download directly to your device or airdrop from a Mac.
288297
try audioStream!.playStreamPCM(warmupBuffer)
289298
}
290299

300+
orcaProfiler.tick()
291301
let pcm = try orcaStream.flush()
302+
orcaProfiler.tock(pcm: pcm)
292303
if pcm != nil {
293304
try audioStream!.playStreamPCM(pcm!)
294305
}
306+
print(String(format: "RTF: %.2f", orcaProfiler.rtf()))
295307
orcaStream.close()
296308
} catch {
297309
DispatchQueue.main.async { [self] in

0 commit comments

Comments
 (0)