-
Notifications
You must be signed in to change notification settings - Fork 2
Architecture
storyvox is a multi-module Gradle project — thirteen modules as of v0.4.83. Modules in rough dependency order:
:app ← single AndroidApp module; Hilt entry point, navigation, MainActivity
:feature ← screens (Compose UI, ViewModels)
:core-data ← Room DB, repositories, FictionSource contract
:core-llm ← LLM router (provider-agnostic chat + sessions)
:core-playback ← TTS engine wrapper, PlaybackController, MediaSession service,
Tier 3 multi-engine producer
:core-ui ← Library Nocturne theme, brass tokens, shared composables
:source-royalroad ← FictionSource for royalroad.com (HTML scraping)
:source-github ← FictionSource for GitHub (book.toml + bare-repo + OAuth Device Flow)
:source-mempalace ← FictionSource for self-hosted Memory Palace (LAN-only)
:source-rss ← FictionSource for RSS / Atom feeds + storyvox-feeds suggestions
:source-epub ← FictionSource for local EPUB folders (SAF + OPF parser)
:source-outline ← FictionSource for self-hosted Outline wikis
:source-azure ← VoiceEngine for Azure HD cloud voices (BYOK, remote)
:wear ← Wear OS companion (experimental)
The TTS engine itself is not a module in this repo — it's the :engine-lib AAR from jphein/VoxSherpa-TTS pulled in via JitPack as a transitive dependency on :core-playback.
Every fiction backend implements FictionSource:
interface FictionSource {
val key: BrowseSourceKey
suspend fun list(query, filter): List<Fiction>
suspend fun get(fictionId): Fiction
suspend fun chapter(fictionId, chapterId): ChapterContent
// ...
}Hilt multibinds them as a Set<FictionSource>; FictionRepository resolves the right one by key. RSS, EPUB, and Outline all landed against this contract without changes elsewhere. Adding the next backend (e.g. Notion #233) is a new module + a new entry in the multibinding set.
The same plug-in pattern applied to TTS engines. Local Piper/Kokoro and remote Azure HD both implement VoiceEngine and bind into a Map<EngineKind, VoiceEngine> in :core-playback. EnginePlayer routes synthesis to whichever engine the currently-selected voice belongs to, with offline fallback to the local engine if Azure errors mid-chapter.
Single source of truth for playback. Lives in core-playback. The EnginePlayer actually speaks; the controller orchestrates chapter fetch, position persistence, sleep timer, MediaSession callbacks.
StoryvoxPlaybackService : MediaSessionService hosts the EnginePlayer. Foreground via Media3 once a controller binds. Notification rendering fully delegated to DefaultMediaNotificationProvider.
Feature-API-facing wrapper around DataStore. The implementation in app/SettingsRepositoryUiImpl.kt is the only place that touches DataStore directly. Test fakes implement the interface for ViewModel unit tests.
LLM router — LlmRepository.chat(provider, model, messages) returns a Flow of tokens. Provider implementations: ClaudeApiProvider, OpenAiApiProvider, OllamaProvider, VertexApiProvider, BedrockApiProvider, FoundryApiProvider, AnthropicTeamsProvider. Session storage lives in LlmMessageDao / LlmSessionDao (Room).
- User taps "Listen" →
FictionDetailScreencallsplaybackUi.play(fictionId, chapterId). -
RealPlaybackControllerUitranslates →controller.play(...)in core-playback. - Controller fetches chapter via
ChapterRepository. -
EnginePlayer.speak(text)chunks into sentences (viaSentenceChunker), feeds VoxSherpa, writes PCM toAudioTrack. -
SentenceTrackeremits sentence-range updates that the reader subscribes to for highlighting. -
BrowseUiStateandPlaybackStateflow upward to the UI via StateFlow.
- DataStore — settings (preferences_pb).
- Room — fictions, chapters, follows, downloads, LLM sessions/messages.
-
Filesystem — voice models (
/files/voices/), PCM cache (/files/pcm-cache/), chapter cache. - No SharedPreferences — DataStore only.
Every project under ~/Projects/ uses realm-sigil for unified versioning. storyvox carries the fantasy realm. The version string + git hash + dirty flag + build timestamp are baked at configure-time via BuildConfig. Settings → About surfaces them.
-
JVM unit tests — feature ViewModels, source parsers, repository logic. Robolectric for
android.net.Uricalls. - Instrumented tests — minimal surface; smoke tests for navigation.
- Real-tablet QA — every release installed on a Tab A7 Lite (R83W80CAFZB) and a Z Flip3 (R5CRB0W66MK) before tagging.
The signing keystore is checked into the repo (app/storyvox-debug.keystore) so every contributor / CI runner signs with the same cert. This matters for sideload upgrades — without it, fresh keystore on each environment would produce "package conflicts" errors when one APK lands over another. Pre-Play-Store, this is fine; the security trade-off is acknowledged in app/build.gradle.kts.
CI (android.yml) runs on every push + tag. Tagged releases attach the debug-signed APK to the GitHub release.
Start here
Using Candela
- Fiction sources
- Reading & library
- Export & extras
- Voices
- Voice catalog
- AI chat
- Cloud sync
- Accessibility
- Settings reference
- Troubleshooting
Developer
Project