snapshot current state before gitea sync

This commit is contained in:
2026-02-18 10:59:55 +01:00
commit 1dd0676fca
53 changed files with 3108 additions and 0 deletions
@@ -0,0 +1,139 @@
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: 220)
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]
statusMessage = "Phonemizing and synthesizing…"
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."
}
}
}