iPhone app ──(your login)──► your server ──(NAMU_API_KEY)──► api.namuai-lab.com
1. Record
AVAudioRecorder writes AAC in an M4A file, which the API reads as it is. Add NSMicrophoneUsageDescription to your Info.plist.
Recorder.swift
import AVFoundation
final class Recorder {
private var recorder: AVAudioRecorder?
let url = FileManager.default.temporaryDirectory.appendingPathComponent("note.m4a")
func start() throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .spokenAudio, options: [.defaultToSpeaker])
try session.setActive(true)
recorder = try AVAudioRecorder(url: url, settings: [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 16_000,
AVNumberOfChannelsKey: 1, // mono: half the bytes, same words
AVEncoderBitRateKey: 32_000,
])
recorder?.record()
}
func stop() { recorder?.stop() }
}
2. Send it to your server
VoiceAPI.swift
import Foundation
struct VoiceAPI {
let baseURL: URL
let token: String // your user's token, not the Namu key
private var session: URLSession {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 300 // a model waking up can take a minute
config.waitsForConnectivity = true // wait for the network instead of failing at once
return URLSession(configuration: config)
}
func transcribe(_ file: URL, language: String = "ha") async throws -> String {
let boundary = UUID().uuidString
var body = Data()
func field(_ name: String, _ value: String) {
body.append("--\(boundary)\r\nContent-Disposition: form-data; name=\"\(name)\"\r\n\r\n\(value)\r\n".data(using: .utf8)!)
}
field("language", language)
body.append("--\(boundary)\r\nContent-Disposition: form-data; name=\"file\"; filename=\"note.m4a\"\r\nContent-Type: audio/mp4\r\n\r\n".data(using: .utf8)!)
body.append(try Data(contentsOf: file))
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
var request = URLRequest(url: baseURL.appendingPathComponent("api/transcribe"))
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let (data, response) = try await session.upload(for: request, from: body)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
guard (response as? HTTPURLResponse)?.statusCode == 200, let text = json["text"] as? String else {
throw URLError(.badServerResponse)
}
return text
}
func speak(_ text: String) async throws -> Data {
var request = URLRequest(url: baseURL.appendingPathComponent("api/speak"))
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: ["text": text, "voice": "nadia"])
let (data, response) = try await session.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) }
return data // MP3
}
}
3. Play the answer
import AVFoundation
var player: AVAudioPlayer? // keep a reference, or playback stops at once
let mp3 = try await api.speak("Na gode! Mun karɓi saƙonku.")
player = try AVAudioPlayer(data: mp3)
player?.play()
note.m4a until the transcription succeeds, so a dropped connection never means asking the person to speak again.