141 lines
5.3 KiB
Swift
141 lines
5.3 KiB
Swift
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."
|
||
}
|
||
}
|
||
}
|