Files
hope-voice-api/Sources/LiveKit/TranscriptHandler.swift
T

79 lines
2.7 KiB
Swift

import Foundation
import LiveKit
struct TranscriptEvent: Equatable, Identifiable {
enum Role: String {
case user
case agent
}
let id: String
let role: Role
var text: String
var isFinal: Bool
let timestamp: Date
}
/// Merges the primary transcription segments (Room delegate) with the
/// secondary `transcript` data topic (finals only). Dedupe rule: a final for a
/// role closes any open partial row for that role.
@MainActor
final class TranscriptHandler: ObservableObject {
@Published private(set) var events: [TranscriptEvent] = []
let history = TranscriptHistory()
func receiveSegment(id: String, role: TranscriptEvent.Role, text: String, isFinal: Bool) {
guard !text.isEmpty else { return }
upsert(id: id, role: role, text: text, isFinal: isFinal)
if isFinal {
history.add(role: role.rawValue, text: text)
}
}
func receiveDataTranscript(role: TranscriptEvent.Role, text: String) {
closePartial(for: role)
upsert(id: "final-\(role.rawValue)-\(UUID().uuidString)", role: role, text: text, isFinal: true)
history.add(role: role.rawValue, text: text)
}
func clear() {
events.removeAll()
}
func clearHistory() {
history.clear()
}
private func closePartial(for role: TranscriptEvent.Role) {
for index in events.indices where !events[index].isFinal && events[index].role == role {
events[index].isFinal = true
}
}
private func upsert(id: String, role: TranscriptEvent.Role, text: String, isFinal: Bool) {
if let index = events.firstIndex(where: { $0.id == id }) {
events[index].text = text
events[index].isFinal = isFinal
} else {
events.append(TranscriptEvent(id: id, role: role, text: text, isFinal: isFinal, timestamp: Date()))
}
}
}
extension TranscriptHandler: RoomDelegate {
nonisolated func room(_ room: Room, participant: Participant, trackPublication: TrackPublication, didReceiveTranscriptionSegments segments: [TranscriptionSegment]) {
let role: TranscriptEvent.Role = participant.isAgent ? .agent : .user
let isLocal = participant.identity?.stringValue == room.localParticipant.identity?.stringValue
let resolvedRole: TranscriptEvent.Role = isLocal ? .user : role
let events = segments.map {
TranscriptEvent(id: $0.id, role: resolvedRole, text: $0.text, isFinal: $0.isFinal, timestamp: $0.lastReceivedTime)
}
Task { @MainActor [weak self] in
for event in events {
self?.receiveSegment(id: event.id, role: event.role, text: event.text, isFinal: event.isFinal)
}
}
}
}