Add iOS client for WhisperLive (Audio-Transcription-iOS)
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// RecordingViewModel.swift
|
||||
// Lecture2Quiz
|
||||
//
|
||||
// Created by ParkMazorika on 4/27/25.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Combine
|
||||
|
||||
/// Represents a segment of transcribed audio with start/end timestamps and completion flag.
|
||||
struct TranscriptionSegment: Identifiable, Equatable {
|
||||
var id = UUID()
|
||||
var start: Double
|
||||
var end: Double
|
||||
var text: String
|
||||
var completed: Bool
|
||||
}
|
||||
|
||||
/// ViewModel responsible for managing audio recording and transcription logic.
|
||||
class AudioViewModel: ObservableObject {
|
||||
@Published var isRecording = false // Indicates if recording is active
|
||||
@Published var isPaused = false // Indicates if recording is currently paused
|
||||
@Published var timeLabel = "00:00" // Timer label formatted as mm:ss
|
||||
@Published var transcriptionList: [String] = [] // Live transcription output
|
||||
@Published var isLoading = false // True while waiting for server response
|
||||
@Published var finalScript: String = "" // Final script from completed segments
|
||||
|
||||
private var timer: Timer?
|
||||
private var elapsedTime: Int = 0
|
||||
|
||||
private var audioStreamer: AudioStreamer? // Handles audio capture and streaming
|
||||
private var audioWebSocket: AudioWebSocket? // Manages WebSocket communication
|
||||
|
||||
private var segments: [TranscriptionSegment] = [] // Stores all transcription segments
|
||||
|
||||
init() {}
|
||||
|
||||
/// Starts audio recording and initializes WebSocket + AVAudioEngine.
|
||||
func startRecording() {
|
||||
let audioAPIUrl = "your server url"
|
||||
audioWebSocket = AudioWebSocket(host: audioAPIUrl, port: 443)
|
||||
audioStreamer = AudioStreamer(webSocket: audioWebSocket!)
|
||||
|
||||
isLoading = true
|
||||
|
||||
// Handle server transcription message
|
||||
audioWebSocket?.onTranscriptionReceived = { [weak self] text in
|
||||
self?.handleRawTranscriptionJSON(text)
|
||||
}
|
||||
|
||||
// When server sends SERVER_READY
|
||||
audioWebSocket?.onServerReady = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
self.isRecording = true
|
||||
self.isPaused = false
|
||||
self.timeLabel = "00:00"
|
||||
self.elapsedTime = 0
|
||||
self.startTimer()
|
||||
self.audioStreamer?.startStreaming()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pauses the recording and stops the timer.
|
||||
func pauseRecording() {
|
||||
isPaused = true
|
||||
audioStreamer?.pauseStreaming()
|
||||
timer?.invalidate()
|
||||
}
|
||||
|
||||
/// Resumes recording and restarts the timer.
|
||||
func resumeRecording() {
|
||||
isPaused = false
|
||||
audioStreamer?.resumeStreaming()
|
||||
startTimer()
|
||||
}
|
||||
|
||||
/// Stops recording and finalizes connection to server.
|
||||
func stopRecording() {
|
||||
isRecording = false
|
||||
isPaused = false
|
||||
timer?.invalidate()
|
||||
|
||||
audioStreamer?.stopStreaming()
|
||||
audioWebSocket?.sendEndOfAudio()
|
||||
audioWebSocket?.onTranscriptionReceived = nil
|
||||
audioWebSocket?.closeConnection()
|
||||
}
|
||||
|
||||
/// Starts the recording timer (1-second interval).
|
||||
private func startTimer() {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
self.elapsedTime += 1
|
||||
let minutes = self.elapsedTime / 60
|
||||
let seconds = self.elapsedTime % 60
|
||||
self.timeLabel = String(format: "%02d:%02d", minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
/// Finalizes the transcription by joining all completed segments into one string.
|
||||
func finalizeTranscription() {
|
||||
isLoading = false
|
||||
let completedText = segments
|
||||
.filter { $0.completed }
|
||||
.map { $0.text.trimmingCharacters(in: .whitespaces) }
|
||||
.joined(separator: " ")
|
||||
finalScript = completedText
|
||||
print("Final transcript:\n\(finalScript)")
|
||||
}
|
||||
|
||||
/// Handles incoming JSON from the server and updates UI state.
|
||||
/// Supports both full JSON and raw string cases.
|
||||
func handleRawTranscriptionJSON(_ jsonString: String) {
|
||||
let trimmed = jsonString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let data = trimmed.data(using: .utf8) else { return }
|
||||
|
||||
if trimmed.hasPrefix("{") {
|
||||
// Parse JSON containing segment list
|
||||
do {
|
||||
if let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let segmentDicts = dict["segments"] as? [[String: Any]] {
|
||||
|
||||
for item in segmentDicts {
|
||||
guard let startStr = item["start"] as? String,
|
||||
let endStr = item["end"] as? String,
|
||||
let text = item["text"] as? String,
|
||||
let completed = item["completed"] as? Bool,
|
||||
let start = Double(startStr),
|
||||
let end = Double(endStr) else { continue }
|
||||
|
||||
let newSegment = TranscriptionSegment(start: start, end: end, text: text, completed: completed)
|
||||
|
||||
// Overwrite if already exists, else append
|
||||
if let index = self.segments.firstIndex(where: { $0.start == start }) {
|
||||
self.segments[index] = newSegment
|
||||
} else {
|
||||
self.segments.append(newSegment)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the UI
|
||||
DispatchQueue.main.async {
|
||||
let completedTexts = self.segments
|
||||
.filter { $0.completed }
|
||||
.sorted(by: { $0.start < $1.start })
|
||||
.map { $0.text.trimmingCharacters(in: .whitespaces) }
|
||||
|
||||
let pendingText = self.segments
|
||||
.filter { !$0.completed }
|
||||
.sorted(by: { $0.start < $1.start })
|
||||
.map { $0.text.trimmingCharacters(in: .whitespaces) }
|
||||
.last ?? ""
|
||||
|
||||
self.transcriptionList = completedTexts + (pendingText.isEmpty ? [] : [pendingText])
|
||||
self.finalScript = self.transcriptionList.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("JSON parsing error: \(error)")
|
||||
}
|
||||
} else {
|
||||
// Handle raw text line
|
||||
DispatchQueue.main.async {
|
||||
if self.transcriptionList.last != trimmed {
|
||||
self.transcriptionList.append(trimmed)
|
||||
self.finalScript = self.transcriptionList.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user