Files
vorleser/VorleserMac/Services/MacLibraryViewModel.swift

141 lines
5.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import AppKit
@MainActor
final class MacLibraryViewModel: ObservableObject {
@Published private(set) var selectedURL: URL?
@Published private(set) var chapters: [EPUBChapter] = []
@Published private(set) var chunks: [String] = []
@Published var selectedChapterIndex: Int = 0
@Published var selectedChunkIndex: Int?
@Published private(set) var chunkedCount: Int = 0
@Published private(set) var statusMessage: String = ""
private let epubService = EPUBService()
private let chunker = TextChunker(maxCharacters: 80)
private let audioPlayer = AudioPlaybackService()
private let synthesisWorker = SynthesisWorker()
func pickEPUB() {
let panel = NSOpenPanel()
panel.allowedContentTypes = [.init(filenameExtension: "epub")].compactMap { $0 }
panel.allowsMultipleSelection = false
panel.canChooseDirectories = false
panel.canChooseFiles = true
panel.begin { [weak self] result in
guard result == .OK, let url = panel.urls.first else { return }
self?.loadEPUB(url: url)
}
}
func loadTestText() {
selectedURL = nil
let chapter = EPUBChapter(
title: "Test Chapter",
rawText: """
When magic returns to the Earth, its power calls Sam Verner. As Sam searches for his sister through the slick and scary streets of 2050, his quest leads him across the ocean to England, where druids guard secrets older than cities.
He follows rumors into crowded markets and dark alleyways, bargaining with strangers and tracing clues in a language he barely understands. Every answer creates another question, and every friend might also be a rival. In a world where technology and spellcraft collide, survival requires equal parts courage and caution.
"""
)
chapters = [chapter]
selectedChapterIndex = 0
chunks = chunker.chunk(chapter.rawText)
selectedChunkIndex = chunks.isEmpty ? nil : 0
chunkedCount = chunks.count
statusMessage = "Loaded test text (1 chapter, \(chunkedCount) chunks)"
}
func loadDebugPhonemes() {
selectedURL = nil
let phonemes = "[PHONEMES]hˌW ɑɹ ju tədˈA? ˌI ɐm dˈuɪŋ ɹˈizənəbli wˈɛl, θˈæŋk ju fɔɹ ˈæskɪŋ"
let chapter = EPUBChapter(
title: "Debug Phonemes",
rawText: phonemes
)
chapters = [chapter]
selectedChapterIndex = 0
chunks = chunker.chunk(chapter.rawText)
selectedChunkIndex = chunks.isEmpty ? nil : 0
chunkedCount = chunks.count
statusMessage = "Loaded debug phonemes (1 chapter, \(chunkedCount) chunks)"
}
func updateStatus(_ message: String) {
statusMessage = message
}
private func loadEPUB(url: URL) {
selectedURL = url
statusMessage = "Parsing EPUB…"
Task { [weak self] in
do {
guard let self else { return }
let chapters = try self.epubService.extractChapters(from: url)
let chunkCount = chapters.reduce(0) { total, chapter in
total + self.chunker.chunk(chapter.rawText).count
}
self.chapters = chapters
self.selectedChapterIndex = 0
self.chunks = chapters.first.map { self.chunker.chunk($0.rawText) } ?? []
self.selectedChunkIndex = self.chunks.isEmpty ? nil : 0
self.chunkedCount = chunkCount
self.statusMessage = "Parsed \(chapters.count) chapters, \(chunkCount) chunks"
} catch {
self?.statusMessage = "Failed: \(error.localizedDescription)"
}
}
}
func selectChapter(index: Int) {
guard index >= 0 && index < chapters.count else { return }
selectedChapterIndex = index
chunks = chunker.chunk(chapters[index].rawText)
selectedChunkIndex = chunks.isEmpty ? nil : 0
}
func synthesizeSelectedChunk() {
guard !chunks.isEmpty else {
statusMessage = "No chunks to synthesize."
return
}
let index = min(selectedChunkIndex ?? 0, chunks.count - 1)
let text = chunks[index]
print("DEBUG: Playing chunk \(index + 1)/\(chunks.count), text: \(text)")
statusMessage = "Phonemizing and synthesizing chunk \(index + 1)/\(chunks.count)"
Task {
do {
let samples = try await synthesisWorker.synthesize(text: text)
audioPlayer.play(samples: samples)
statusMessage = "Playing synthesized audio."
} catch {
statusMessage = "Synthesis failed: \(error.localizedDescription) (\(String(describing: error)))"
}
}
}
func playFirstChunk() {
selectedChunkIndex = 0
synthesizeSelectedChunk()
}
func playChunk(at index: Int) {
guard index >= 0 && index < chunks.count else { return }
selectedChunkIndex = index
synthesizeSelectedChunk()
}
func exportLastAudio() {
if let url = audioPlayer.exportLastWav() {
statusMessage = "Exported last audio to \(url.path)"
} else {
statusMessage = "No audio to export yet."
}
}
}