Skip to content

Architecture

JP Hein edited this page May 10, 2026 · 9 revisions

Architecture

storyvox is a multi-module Gradle project — thirteen modules as of v0.4.83. Modules in rough dependency order:

Module graph

: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.

Key abstractions

FictionSource (multi-source backbone)

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.

VoiceEngine (multi-backend TTS)

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.

PlaybackController

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.

SettingsRepositoryUi

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.

LlmRepository + LlmSessionRepository

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).

Data flow (chapter playback)

  1. User taps "Listen" → FictionDetailScreen calls playbackUi.play(fictionId, chapterId).
  2. RealPlaybackControllerUi translates → controller.play(...) in core-playback.
  3. Controller fetches chapter via ChapterRepository.
  4. EnginePlayer.speak(text) chunks into sentences (via SentenceChunker), feeds VoxSherpa, writes PCM to AudioTrack.
  5. SentenceTracker emits sentence-range updates that the reader subscribes to for highlighting.
  6. BrowseUiState and PlaybackState flow upward to the UI via StateFlow.

State persistence

  • 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.

Realm sigil

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.

Testing

  • JVM unit tests — feature ViewModels, source parsers, repository logic. Robolectric for android.net.Uri calls.
  • 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.

Build + signing

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.

Clone this wiki locally