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." } } }