-
Notifications
You must be signed in to change notification settings - Fork 2
Architecture
Candela is a multi-module Gradle project — thirty-four modules as of v0.5.51 (up from 13 at v0.4.83, 29 at v0.5.38). Modules in rough dependency order:
:app ← single AndroidApp module; Hilt entry point, navigation, MainActivity
:feature ← screens (Compose UI, ViewModels) — Library, Browse, Reader,
Detail, Settings, AI Chat, TechEmpower Home, Voices
:core-data ← Room DB, repositories, FictionSource contract, plugin registry
:core-plugin-ksp ← KSP SymbolProcessor — emits Hilt @IntoSet factory per
@SourcePlugin-annotated FictionSource (v0.5.27)
:core-llm ← LLM router (provider-agnostic chat; ChatStreamEvent flow type
carries text deltas + tool-call + tool-result events;
ToolCatalog powers function calling; ImageContentBlock for
multi-modal images)
:core-playback ← TTS engine wrapper, EnginePlayer, PcmCache, MediaSession service,
Tier 3 multi-engine producer, SentenceTracker
:core-sync ← InstantDB cross-device sync (#360 v0.5.12) — library, follows,
positions, bookmarks, pronunciation, encrypted secrets
:core-ui ← Library Nocturne theme, brass tokens, shared composables
:source-royalroad ← FictionSource for royalroad.com (HTML scraping, WebView login)
: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-epub-writer ← EPUB export — downloaded chapters → portable .epub
:source-outline ← FictionSource for self-hosted Outline wikis
:source-gutenberg ← Project Gutenberg via Gutendex
:source-ao3 ← Archive of Our Own (per-tag feeds + EPUBs); auth PR1 v0.5.51 #426
:source-standard-ebooks ← Standard Ebooks classics
:source-wikipedia / :source-wikisource ← Heading-split chapters
:source-radio ← Audio-stream backend — curated stations + Radio Browser search
:source-notion ← Notion pages and databases (beautiful covers v0.5.51 #514)
:source-hackernews / :source-arxiv / :source-plos ← News + research backends
:source-discord ← Discord channels (bot-token auth)
:source-telegram ← v0.5.51 — public Telegram channels via Bot API (#462)
:source-palace ← v0.5.51 — Palace Project OPDS catalog walker (#502)
:source-slack ← v0.5.51 — Slack channels via Web API (#454)
:source-matrix ← v0.5.51 — federated Matrix rooms via Client-Server API (#457)
:source-readability ← Catch-all — Readability4J for any HTTP(S) URL (#472)
:source-azure ← VoiceEngine for Azure HD cloud voices (BYOK, remote)
:baselineprofile ← UI Automator hot-path producer; emits baseline-prof.txt
(#409 v0.5.46 — cold launch 6.7 s → 0.8 s on Tab A7 Lite)
:wear ← Wear OS companion (experimental)
The TTS engine itself is not a module in this repo — it's the :engine-lib AAR from techempower-org/VoxSherpa-TTS pulled in via JitPack as a transitive dependency on :core-playback. Piper, Kokoro, and KittenTTS all live behind one AAR.
Every fiction backend implements FictionSource from :core-data and is annotated with @SourcePlugin:
@SourcePlugin
class TelegramSource @Inject constructor(…) : FictionSource {
override val key: BrowseSourceKey = BrowseSourceKey.TELEGRAM
override suspend fun list(query, filter): List<Fiction> = …
override suspend fun chapter(fictionId, chapterId): ChapterContent = …
}The :core-plugin-ksp KSP processor emits a Hilt @IntoSet factory per annotated class at compile time, and SourcePluginRegistry discovers backends at startup. No manual Hilt module needed; no central enum to edit. Adding a backend is ~4 touchpoints (down from ~17 pre-Phase-2). RSS, EPUB, Outline, Notion, Telegram, Palace, Slack, Matrix, and the readability catch-all all landed against this contract without changes elsewhere. The Plugin manager in Settings → Plugins iterates the registry — new backends auto-surface with on/off toggle and details modal.
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. Candela 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