Decrypt Parity Matrix — TapListenSDK ↔ Speechify SSDK
Purpose. Single living document tracking every Speechify SSDK component (from recon/) against TapListenSDK's implementation status. Drives wave planning. Updated whenever a wave lands or a new gap is discovered.
Sources.
- Speechify recon:
/Users/kohuyn/StudioProjects/DecryptTTS/recon/ (class dumps, strings, pipeline notes)
- TapListenSDK code:
/Users/kohuyn/StudioProjects/TapListenSDK/
- Plan reference:
/Users/kohuyn/StudioProjects/DecryptTTS/plans/260429-1908-bookcast-tts-reader/
Status legend
| Status |
Meaning |
DONE |
Implemented, build clean, behavior verified or 1:1 mapping |
PARTIAL |
Implemented with documented deficits — see Notes |
SCAFFOLDED |
Protocol/types exist but throw notConfigured (cloud features) |
MISSING |
Not implemented, no scaffold |
INTENTIONAL_SKIP |
Anti-goal per plan.md (Tier 3+, CarPlay, AI, multi-device sync, etc.) |
Severity legend
| Severity |
Meaning |
| Critical |
User-visible bug or App Store blocker |
| High |
Feature parity loss, affects UX or correctness |
| Medium |
Polish, nice-to-have visible diff |
| Low |
Edge case or rarely-hit |
| N/A |
Severity not applicable (e.g. INTENTIONAL_SKIP) |
Domain coverage
| Domain |
Scope decision |
Status |
| D1 — Content Pipeline (import/extract/bundle) |
In scope |
Pilot done |
| D2 — Reading Bundle (runtime) |
In scope |
Audited |
| D3 — Reader UI + Block types |
In scope |
Audited |
| D4 — Highlight & Polygon |
In scope |
Audited |
| D5 — TTS Engine + Voice |
In scope |
Audited |
| D6 — Playback / Audio session / CarPlay |
In scope (no CarPlay) |
Audited |
| D7 — Persistence & Library |
In scope |
Audited |
| D8 — User Highlights & Notes |
Tier 2 |
TBD (cross-linked from D4.6 + D7.6) |
| D9 — In-document Search |
Tier 2 |
TBD (cross-linked from D3.8.5 + D4.7 + D7.8) |
| D10 — Skip detection (heuristic + ML) |
Heuristic only; ML INTENTIONAL_SKIP |
Folded into D2.4 |
| D11 — Sync / Cloud accounts |
INTENTIONAL_SKIP (offline-first) |
Skipped |
| D12 — Auth & Account |
INTENTIONAL_SKIP |
Skipped |
| D13 — Onboarding & Paywall |
INTENTIONAL_SKIP |
Skipped |
| D14 — AI features (summarize/quiz/podcast) |
INTENTIONAL_SKIP |
Skipped |
| D15 — SDK platform (DI, factories, plugins) |
In scope |
Audited |
D1 — Content Pipeline
Speechify role: Ingest files from various sources → unified content representation. Entry point SSDKContentImporter, output SSDKContentBundle (sealed).
Key recon files: recon/ssdk-full-pipeline.md §D1, recon/speechify-import-classes.txt, recon/speechify-strings.txt:138315-138969.
D1.1 — Importer state machine
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D1.1.1 |
SSDKContentImporter |
strings:138957 |
Core/Services/ImportService.swift |
DONE |
— |
Same role: state-machine-driven import orchestrator |
| D1.1.2 |
SSDKContentImporterState (sealed) |
strings:138960 |
ImportState enum in ImportService.swift |
PARTIAL |
Medium |
We have 5 cases (idle/starting/importing/finished/failed/duplicate); SSDK has 6 (NotImported/Starting/Importing/Imported/ImportedToLibrary/Failed). Missing: distinct "ImportedToLibrary" stage after Imported (we collapse them) |
| D1.1.3 |
SSDKContentImporterStateInfo |
strings:138959 |
(none) |
MISSING |
Low |
SSDK wraps state with metadata struct; we use enum associated values inline. Functionally equivalent for now |
| D1.1.4 |
SSDKContentImporterStateFailed (with error payload) |
strings:138961 |
.failed(message: String) case |
PARTIAL |
Medium |
We carry only message string; SSDK carries typed Error. Can recover later if we move to typed ImportError in state |
| D1.1.5 |
Per-state class hierarchy (Kotlin sealed → ObjC binding) |
strings:138962-138969 |
Swift enum (more idiomatic) |
DONE |
— |
Idiomatic Swift; no parity issue |
D1.2 — Content providers (input sources)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D1.2.1 |
SSDKFileContentProvider (local file) |
ssdk-full-pipeline §D1 |
ImportService.importFile(at:) + DocumentFormat.detect(from:) |
DONE |
— |
Function-based, not protocol-based; equivalent capability |
| D1.2.2 |
SSDKScanContentProvider (camera scan) |
ssdk-full-pipeline §D1 |
(none) |
INTENTIONAL_SKIP |
N/A |
Camera/scan multi-page OCR is anti-goal per plan.md "Anti-Goals (Defer Past Tier 2)" |
| D1.2.3 |
SSDKSpeechifyBookContentProvider (book store) |
ssdk-full-pipeline §D1 |
(none) |
INTENTIONAL_SKIP |
N/A |
Speechify-internal book catalog; not applicable |
| D1.2.4 |
URL/web content provider |
speechify-import-classes:22 (URLImportService) |
ImportService.importWebURL + WebPageExtractor |
DONE |
— |
Web URL import flow exists |
| D1.2.5 |
WebFileImportService (multi-format web import dispatcher) |
speechify-import-classes:38-49 |
(none — single WebPageExtractor) |
PARTIAL |
Medium |
SSDK has dedicated dispatcher routing web URL to xmlFileImportModule / pdfImportModule / epubImportModule based on detected MIME. We route only HTML; downloading a PDF URL won't trigger PDFExtractor |
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D1.3.1 |
PDFImportModule |
speechify-import-classes:15 |
Core/Services/Extractors/PDFExtractor.swift |
DONE |
— |
PDFKit-based, with OCR fallback |
| D1.3.2 |
XMLFileImportModule (uses ConvertToPDFServiceInterface) |
speechify-import-classes:31 |
DOCXExtractor.swift (different path: native zip+XML walk) |
PARTIAL |
Medium |
SSDK relies on cloud-side PDF conversion for XML/DOC formats; we walk DOCX zip locally. Better for offline; risks coverage gaps on weird docx variants |
| D1.3.3 |
LocalFileImportModule |
speechify-import-classes:53 |
PlainTextExtractor.swift + format dispatch in ImportService |
DONE |
— |
Equivalent role |
| D1.3.4 |
EPUB import module |
(implied via _epubImportModule lazy storage in WebFileImportService:45) |
Core/Services/Extractors/EPUBExtractor.swift |
DONE |
— |
Custom zip+OPF walker, no EPUBKit dep |
| D1.3.5 |
Markdown parser (apple/swift-markdown + cmark-gfm) |
strings-all:50050-50100 + 30+ AST node files |
MarkdownExtractor.swift (AST walker via swift-markdown SPM 0.7.3 + cmark-gfm 0.7.1) |
DONE |
— |
Wave 4 — same library Speechify uses. Closes ~21 gaps: ordered lists, GFM tables, frontmatter strip, intra-word _, escaped chars, autolinks, HTML entities, task list markers, etc. |
| D1.3.6 |
RTF |
(implicit; not separately enumerated in recon) |
RTFExtractor.swift |
DONE |
— |
NSAttributedString native |
| D1.3.7 |
Plain text |
(implicit) |
PlainTextExtractor.swift |
DONE |
— |
|
| D1.3.8 |
Image import module (_imageImportModule) |
speechify-import-classes:46 |
(none — OCR handled inside PDFExtractor only) |
PARTIAL |
Low |
Speechify has dedicated image-import → OCR path. We only do OCR inside PDF flow. Standalone JPG/PNG import → text would need a new path |
| D1.3.9 |
Locale file import module (_localeFileImportModule) |
speechify-import-classes:47 |
(none) |
MISSING |
Low |
Unclear purpose from recon alone — possibly localization-aware fallback. Defer until use case clear |
D1.4 — Cloud conversion service
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D1.4.1 |
ConvertToPDFServiceInterface |
speechify-import-classes:33,40 |
Core/Services/Cloud/CloudConvertService.swift |
SCAFFOLDED |
High |
Protocol exists with CloudConvertStub throwing providerNotConfigured. Real impl needs CloudConvert API key. Used by DOCExtractor for legacy .doc |
| D1.4.2 |
convertToPdfService global injection |
speechify-strings (used in XMLFileImportModule, WebFileImportService) |
(no DI yet — direct instantiation in DOCExtractor) |
PARTIAL |
Medium |
Should inject via DependencyContainer not hard-code stub. Wave 9 candidate (bundler refactor) |
D1.5 — Content bundle (output type)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D1.5.1 |
SSDKContentBundle (sealed parent) |
strings:138881 |
RawDocument + CleanDocument structs |
PARTIAL |
Medium |
We split intermediate (raw) vs cleaned, but no sealed hierarchy. Functionally fine for Tier 1 |
| D1.5.2 |
SSDKContentBundleBookBundle (PDF) |
strings:138882 |
RawDocument from PDFExtractor |
DONE |
— |
Same data captured (blocks + metadata + TOC) |
| D1.5.3 |
SSDKContentBundleEpubBundle |
strings:138883 |
RawDocument from EPUBExtractor |
DONE |
— |
|
| D1.5.4 |
SSDKContentBundleMarkdownBundle |
strings:138884 |
RawDocument from MarkdownExtractor (AST-backed) |
DONE |
— |
Wave 4 — see D1.3.5 |
| D1.5.5 |
SSDKContentBundlePlainTextBundle |
strings:138885 |
RawDocument from PlainTextExtractor |
DONE |
— |
|
| D1.5.6 |
SSDKContentBundleStandardBundle (rich web) |
strings:138886 |
RawDocument from WebPageExtractor |
PARTIAL |
Medium |
Speechify's StandardBundle is the post-bundler "ready-to-listen" form (see D1.6). We don't have a bundler stage at all |
| D1.5.7 |
SSDKContentBundleStandardBundleCompanion |
strings:138887 |
(none) |
MISSING |
Medium |
"Companion" carries display markup alongside TTS text. Speechify ships TWO outputs from bundler: text-for-TTS + markup-for-rendering. We conflate both into TextBlock.text |
| D1.5.8 |
SSDKContentBundleWebPageBundle (raw HTML) |
strings:138888 |
(none — we go straight to cleaned web) |
MISSING |
Low |
SSDK keeps raw HTML alongside cleaned bundle. We don't preserve raw input. Could matter for re-parsing |
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D1.6.1 |
SSDKContentBundler |
strings:138889 |
(no separate stage — ImportService.runImport chains extract+clean+save inline) |
MISSING |
High |
Architecturally significant. Speechify isolates bundling (text-for-TTS vs display markup separation) as a discrete stage. We don't. Affects Markdown rendering and future cloud TTS chunking. Wave 9 refactor candidate |
| D1.6.2 |
SSDKContentBundlerConfig |
strings:138890 |
SkipConfig struct |
PARTIAL |
Medium |
SkipConfig covers heuristic flags but not parsing strategy / OCR fallback config |
| D1.6.3 |
SSDKContentBundlerOptions (11 skip flags + parsing modes) |
ssdk-full-pipeline §D2 + strings:138891 |
SkipConfig (5 flags) |
PARTIAL |
High |
Missing: shouldSkipFootnotes, shouldSkipBraces, shouldSkipCitations, shouldSkipParentheses, shouldSkipBrackets, shouldSkipUrls, shouldSkipCaptions, shouldSkipTables, shouldSkipFormulas. We have only headers/footers/page-numbers/hyphens/wrapped-lines |
| D1.6.4 |
SSDKBundlerFactory + Config + Plugins |
strings:138871-138875 |
(no factory, direct instantiation) |
MISSING |
Low |
Plugin-extensible bundler factory. Over-engineered for Tier 1. Defer until Wave 9 |
| D1.6.5 |
mlParsingMode (on-device ML parsing) |
ssdk-full-pipeline §D2 |
(none — heuristic only) |
INTENTIONAL_SKIP |
N/A |
Plan says "ML là Tier 3" |
| D1.6.6 |
sdkHeuristicParsingMode |
ssdk-full-pipeline §D2 |
TextCleaner |
DONE |
— |
Heuristic noise removal exists |
| D1.6.7 |
ocrFallbackStrategy |
ssdk-full-pipeline §D2 |
PDFExtractor.enableOCRFallback + OCRService.shouldRunOCR |
PARTIAL |
Medium |
Boolean toggle + page-text-density heuristic. SSDK has typed strategy enum (likely .never/.fallback/.always). Our binary toggle is sufficient for Tier 1 |
D1.7 — Supporting types (cursors, boundaries, references)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D1.7.1 |
SSDKContentDescriptor |
strings:138623 |
DocumentFormat enum |
DONE |
— |
Type tag for content; enum is Swift-idiomatic |
| D1.7.2 |
SSDKContentBoundary (START/END markers) |
strings:138315-138318 |
(charOffset, charLength) on TextBlock/CleanBlock |
PARTIAL |
Low |
We use offsets directly. SSDK uses opaque boundary type (probably for cross-format polymorphism). Matters when adding cloud TTS chunking |
| D1.7.3 |
SSDKContentCursor (immutable position pointer) |
strings:138319 |
ReaderCursor struct |
DONE |
— |
(blockIndex, charOffsetInBlock) |
| D1.7.4 |
SSDKContentCursorComparator |
strings:138323 |
(none — Equatable + manual compare) |
MISSING |
Low |
Comparator class for sorting cursors. We can derive from struct compare. Wire when needed for search/highlights ordering |
| D1.7.5 |
SSDKContentCursorCompanion (factory methods) |
strings:138320 |
ReaderCursor.zero + initializers |
DONE |
— |
|
| D1.7.6 |
SSDKContentElementReference (deep link to element) |
strings:138327 |
(none) |
MISSING |
Medium |
Used for bookmark/note targeting and deep linking. Currently we only store block index — can't deep-link to mid-block element. Affects D8 (User Highlights) when implemented |
| D1.7.7 |
SSDKContentAccess (free/preview/paid permission) |
strings:139100 |
(none) |
INTENTIONAL_SKIP |
N/A |
Speechify monetization layer; we're free/offline-first |
D1.8 — Import record schema (audit trail)
| # |
Speechify field |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D1.8.1 |
speechifyUri |
ssdk-full-pipeline §D1 method sig |
Document.id: UUID |
DONE |
— |
Different format, same role |
| D1.8.2 |
primaryFileBlobStorageKey |
ssdk-full-pipeline §D1 |
Document.fileRelativePath |
DONE |
— |
Wave 10 — relative path under Documents/Imports/ resolved at runtime. Sandbox restore safe |
| D1.8.3 |
scannedPages |
ssdk-full-pipeline §D1 |
(none) |
INTENTIONAL_SKIP |
N/A |
See D1.2.2 |
| D1.8.4 |
sourceURL |
ssdk-full-pipeline §D1 |
Document.sourceURLString |
DONE |
— |
|
| D1.8.5 |
importOptions |
ssdk-full-pipeline §D1 |
(none persisted) |
MISSING |
Low |
Skip-config used at import time isn't persisted with the document. If user changes config later, can't replay |
| D1.8.6 |
htmlContentLoadOptions |
ssdk-full-pipeline §D1 |
(none) |
MISSING |
Low |
Web-specific options |
| D1.8.7 |
importType |
ssdk-full-pipeline §D1 |
Document.format |
DONE |
— |
|
| D1.8.8 |
attemptsPerformedCount |
ssdk-full-pipeline §D1 |
(none) |
MISSING |
Low |
Retry telemetry. Not user-visible |
| D1.8.9 |
owner |
ssdk-full-pipeline §D1 |
(none — single user) |
INTENTIONAL_SKIP |
N/A |
No multi-user support |
| D1.8.10 |
lastUpdatedAt |
ssdk-full-pipeline §D1 |
Document.lastReadDate (semantic mismatch) |
PARTIAL |
Low |
We track lastRead, not lastUpdated of the import record itself |
| D1.8.11 |
listeningProgress |
ssdk-full-pipeline §D1 |
Document.lastReadCharOffset + new lastReadBlockOrderIndex/lastReadCharOffsetInBlock |
DONE |
— |
Wave 2 fix |
| D1.8.12 |
mimeType |
ssdk-full-pipeline §D1 |
(derived from Document.format) |
PARTIAL |
Low |
Not persisted as MIME string; derived |
| D1.8.13 |
importStatus |
ssdk-full-pipeline §D1 |
(none persisted — only in-memory ImportState) |
MISSING |
Low |
We don't write status to Document; on success, we just persist the doc. No "Failed" record kept. Acceptable for Tier 1 |
D1 summary
| Severity |
Count |
Top items |
| Critical |
1 |
D1.8.2 absolute file URL (App Store blocker, sandbox restore breaks) |
| High |
4 |
D1.3.5 markdown regex, D1.5.4 markdown bundle, D1.6.1 bundler stage missing, D1.6.3 skip config gaps |
| Medium |
8 |
importer state collapse, web file import dispatcher, content companion, cloud DI, ocr strategy enum, element reference, content boundary type, raw web bundle |
| Low |
6 |
state info wrapper, locale module, image module, attempts count, html load options, mime persistence, import status persistence, content cursor comparator |
| Intentional skip |
6 |
scan provider, speechify book, content access, scanned pages field, owner field, ML parsing |
D1 wave assignment proposal:
- Wave 4 (already proposed) — D1.3.5 + D1.5.4 (Markdown via swift-markdown SPM)
- Wave 9 (architectural refactor) — D1.5.7 + D1.6.1 + D1.6.3 (Bundler stage with text-for-TTS vs display-markup split, expanded skip flags)
- Wave 10 (P0 tech debt) — D1.8.2 (relative file URL)
- Backlog — D1.2.5 (web dispatcher routing), D1.4.2 (DI for convert service), D1.7.6 (element reference for D8 prerequisite)
D2 — Reading Bundle
Speechify role: Convert an imported SSDKContentBundle into a ready-to-listen runtime object (SSDKReadingBundle subtype) via a discrete Bundler factory step. The bundle carries all block data, TOC, metadata, and playback config needed to drive both the Reader UI and the audio controller — separated from the content pipeline by a factory seam.
Key recon files: recon/ssdk-full-pipeline.md §D2 (lines 136–191), recon/speechify-strings.txt:138871–138956, recon/speechify-strings.txt:136398–136969, recon/ssdk-full-pipeline.md Pipeline B (lines 729–814).
D2.1 — ReadingBundle type hierarchy
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D2.1.1 |
SSDKReadingBundle (root interface) |
strings:138923 |
(no explicit type — Document + [TextBlock] loaded by ReaderViewModel.bind) |
MISSING |
Medium |
We have no ReadingBundle concept. ReaderViewModel does inline assembly from Document + sorted textBlocks. Functionally sufficient for Tier 1 but structurally no factory seam |
| D2.1.2 |
SSDKBasicReadingBundle |
strings:138904 |
(none) |
MISSING |
Low |
SSDK root abstract bundle; no direct equivalent. Gap is structural, not user-visible |
| D2.1.3 |
SSDKBasicReadingBundleWithContent |
strings:138905 |
(none) |
MISSING |
Low |
Second abstract level in hierarchy. Same note as D2.1.2 |
| D2.1.4 |
SSDKReadingBundleWithContent |
strings:138924 |
(none) |
MISSING |
Low |
Third abstract level. Carries actual [Block]. Our ReaderViewModel.blocks: [TextBlock] fills the same slot but is not a bundle type |
| D2.1.5 |
SSDKClassicReadingBundle (flowing-text reader) |
strings:138933 |
Document + [TextBlock] assembled by ReaderViewModel.bind(_:modelContext:) |
PARTIAL |
High |
Key gap. SSDK's ClassicReadingBundle is the output of a discrete bundler stage that re-processes ContentBundle blocks at open time. We skip that stage; blocks are persisted directly from TextCleaner. No re-processing at open time. Affects skip-flag changes between imports and opens |
| D2.1.6 |
SSDKBookReadingBundle (fixed-layout PDF) |
strings:138928 |
(none) |
INTENTIONAL_SKIP |
N/A |
Anti-goal: PDF rendered as reflowed text only |
| D2.1.7 |
SSDKEpubReadingBundle |
strings:138956 |
(none — EPUB collapses into same Document/TextBlock path) |
INTENTIONAL_SKIP |
N/A |
EPUB folded into linear TextBlock list. Acceptable for Tier 1 |
| D2.1.8 |
SSDKEmbeddedReadingBundle* |
strings:138951–138952 |
(none) |
INTENTIONAL_SKIP |
N/A |
Snippet/preview embed bundle for share-sheet/widgets. Not a Tier-1 goal |
| D2.1.9 |
SSDKDynamicStandardViewReadingBundle |
strings:138949 |
(none) |
INTENTIONAL_SKIP |
N/A |
Adaptive web reader mode. Anti-goal |
| D2.1.10 |
SSDKListeningBundle |
strings:138895 |
(none — audio payloads assembled inline in ReaderViewModel.buildPlayQueue()) |
MISSING |
Medium |
SSDK pre-stages audio chunks for the audio controller. We build TTS queue on-the-fly |
D2.2 — Bundler factory (ContentBundle → ReadingBundle conversion)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D2.2.1 |
SSDKReadingBundlerBase |
strings:138926 |
(none) |
MISSING |
Medium |
Abstract base for all bundlers. We have no bundler tier — ImportService.runImport chains extract→clean→persist inline |
| D2.2.2 |
SSDKReadingBundler |
strings:138925 |
(none) |
MISSING |
Medium |
Concrete bundler entry point. Closest equivalent is ImportService.runImport, but no typed bundle artifact |
| D2.2.3 |
SSDKClassicReadingBundler |
strings:138936 |
(none — split between TextCleaner.swift and ReaderViewModel.bind) |
MISSING |
High |
Architecturally significant. Runs layout analysis at open time against extracted content. We apply TextCleaner once at import; no re-bundle path if SkipConfig changes |
| D2.2.4 |
SSDKBookReadingBundler |
strings:138929 |
(none) |
INTENTIONAL_SKIP |
N/A |
Fixed-layout PDF |
| D2.2.5 |
SSDKUniversalSourcesReadingBundler |
strings:138927 |
(none) |
MISSING |
Low |
Auto-selects best bundler. We always use the same flat path — acceptable since we have one reader mode |
| D2.2.6 |
SSDKEmbeddedReadingBundler |
strings:138953 |
(none) |
INTENTIONAL_SKIP |
N/A |
Embedded snippet bundle |
| D2.2.7 |
SSDKListeningBundler |
strings:138896 |
(none) |
MISSING |
Medium |
Pre-stages audio chunks for SSDKAudioController. We produce TTSPlayBlock array inline at play time |
| D2.2.8 |
SSDKBundlerFactory + Config + Plugins |
strings:138871, 138873, 138874 |
(none) |
MISSING |
Low |
Plugin-extensible bundler factory. Overengineered for Tier 1 |
| D2.2.9 |
createBundleForLibraryItem:bundleMetadata:callback: (async factory) |
strings:137040 |
ReaderViewModel.bind — synchronous, no callback |
PARTIAL |
Medium |
SSDK creates bundles asynchronously off main thread. Long documents may cause frame drop on open |
D2.3 — Master bundler config (composition)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D2.3.1 |
SSDKBundlerFactoryConfig (master config, 5 sub-configs) |
strings:138873; selector at strings:136919 |
(none — no bundler, no master config) |
MISSING |
Medium |
SSDK composes 5 typed sub-configs into one injectable object |
| D2.3.2 |
SSDKClassicReadingBundlerConfig/Options |
strings:138937–138938 |
SkipConfig in TextCleaner.swift |
PARTIAL |
High |
Covers 5 of 11 skip flags; see D2.4 |
| D2.3.3 |
SSDKBookReadingBundlerConfig/Options |
strings:138930–138931 |
(none) |
INTENTIONAL_SKIP |
N/A |
Fixed-layout PDF config |
| D2.3.4 |
SSDKEmbeddedReadingBundlerConfig/Options |
strings:138954–138955 |
(none) |
INTENTIONAL_SKIP |
N/A |
Embedded snippet config |
| D2.3.5 |
SSDKListeningBundlerConfig/Options |
strings:138898–138899 |
(none) |
MISSING |
Medium |
Audio pre-staging config. Our audio settings scattered across ReaderViewModel.rate, voiceID — not composable |
| D2.3.6 |
SSDKContentBundlerConfig |
strings:138890 |
SkipConfig |
PARTIAL |
High |
Same gap as D2.3.2; additionally not persisted alongside import record |
| D2.3.7 |
SSDKBookReadingContentProcessingOptions |
strings:138932 |
(none) |
INTENTIONAL_SKIP |
N/A |
PDF-specific options |
D2.4 — Content bundler options (skip flags + parsing modes)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D2.4.1 |
shouldSkipHeaders |
strings:136949 |
SkipConfig.skipHeaders |
DONE |
— |
Recurring-text heuristic |
| D2.4.2 |
shouldSkipFooters |
strings:136953 |
SkipConfig.skipFooters |
DONE |
— |
Same heuristic |
| D2.4.3 |
shouldSkipPageNumbers |
(implicit) |
SkipConfig.skipPageNumbers + TextCleaner.isPageNumber |
DONE |
— |
Regex (page\s*)?\d{1,4}(/\d{1,4})? |
| D2.4.4 |
shouldSkipFootnotes |
strings:136957 |
(none) |
MISSING |
High |
High-value for academic PDFs. Wave 4 candidate |
| D2.4.5 |
shouldSkipBraces |
strings:136398 |
(none) |
MISSING |
Medium |
Skips {...} |
| D2.4.6 |
shouldSkipCitations |
strings:136400 |
(none) |
MISSING |
Medium |
Skips [1], (Author, 2023). Academic value |
| D2.4.7 |
shouldSkipParentheses |
strings:136402 |
(none) |
MISSING |
Low |
Can break readability if too aggressive |
| D2.4.8 |
shouldSkipBrackets |
strings:136404 |
(none) |
MISSING |
Medium |
Editorial annotations |
| D2.4.9 |
shouldSkipUrls |
strings:136406 |
(none) |
MISSING |
Medium |
High value — reading raw URLs is jarring |
| D2.4.10 |
shouldSkipCaptions |
strings:136959 |
(none) |
MISSING |
Low |
Lower value for audio-only |
| D2.4.11 |
shouldSkipTables |
strings:136961 |
(none) |
MISSING |
Medium |
Tables read linearly are confusing |
| D2.4.12 |
shouldSkipFormulas |
strings:136963 |
(none) |
MISSING |
High |
LaTeX/MathML symbol strings unlistenable |
| D2.4.13 |
fixHyphens |
(Pipeline B:782) |
SkipConfig.fixHyphens + TextCleaner.fixHyphens |
DONE |
— |
|
| D2.4.14 |
mergeWrappedLines |
(Pipeline B:781) |
SkipConfig.mergeWrappedLines + TextCleaner.mergeWrapped |
DONE |
— |
|
| D2.4.15 |
sdkHeuristicParsingMode |
strings:136967 |
TextCleaner (all heuristics) |
DONE |
— |
|
| D2.4.16 |
mlParsingMode |
strings:136965 |
(none) |
INTENTIONAL_SKIP |
N/A |
"ML là Tier 3" |
| D2.4.17 |
ocrFallbackStrategy (typed enum) |
strings:136969 |
PDFExtractor.enableOCRFallback: Bool + OCRService.shouldRunOCR |
PARTIAL |
Medium |
Boolean toggle vs typed enum. Sufficient for Tier 1 |
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D2.5.1 |
SSDKBundleMetadata |
strings:138908 |
DocumentMetadata (import) + Document fields (runtime) |
PARTIAL |
Low |
SSDK adds analytics fields (contentSubType, importFlow, contentHostname, etc.). We carry only title/author/lang/pageCount/coverImage |
| D2.5.2 |
SSDKEmbeddedBundleMetadata |
strings:138914 |
(none) |
INTENTIONAL_SKIP |
N/A |
|
| D2.5.3 |
SSDKContentProperties/SSDKEventProperties |
strings:138909–138910 |
(none) |
MISSING |
Low |
Telemetry only |
| D2.5.4 |
SSDKPreparedBlock (text + audio + ref) |
strings:138943–138946 |
PreparedBlock struct (index + text + startCharOffset + blockIndex ref) in Core/Services/CloudTTS/PreparedBlock.swift |
DONE |
— |
Wave 5 — same shape. Audio attached via PreparedAudio enum |
| D2.5.5 |
SSDKPreparedAudio* (URL/Data) |
strings:138939–138942 |
PreparedAudio enum: .data(AudioData) / .url(URL) / .fetching(Task<AudioData, Error>) |
DONE |
— |
Wave 5 — sealed hierarchy matching SSDK. .fetching exposes in-flight prefetch task without blocking |
| D2.5.6 |
SSDKMutableObservableContentTitle |
strings:138922 |
Document.title (static) |
MISSING |
Low |
SSDK exposes title as observable Flow for live updates |
D2.6 — ReaderViewModel as bundle consumer
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D2.6.1 |
SSDKAudioController.fromBundle(...) |
Pipeline B:769–791 |
ReaderViewModel.play() builds queue from blocks array |
PARTIAL |
Medium |
SSDK binds audio controller to bundle. We pass raw [TTSPlayBlock] at play time |
| D2.6.2 |
SSDKNavigationIntent (10 cases) |
§D13, Pipeline B:768 |
jumpTo(blockIndex:) + tapWord(...) |
PARTIAL |
Medium |
We have 2 navigation primitives. Missing GoToPage, GoToSearchMatch, GoToUserHighlight |
| D2.6.3 |
TOC backed by bundle |
Pipeline B:755 |
Document.tableOfContents from tocJSON |
DONE |
— |
Same data; different transport |
| D2.6.4 |
Cursor restoration from bundle open intent |
Pipeline B |
ReaderViewModel.restoredCursor(for:) |
DONE |
— |
Wave 2 stable cursor |
| D2.6.5 |
LazyContentProvider (sliding window block access) |
Pipeline B:799–800 |
ReaderViewModel.blocks: [TextBlock] eager load |
PARTIAL |
Low |
Memory pressure for large books |
D2 summary
| Severity |
Count |
Top items |
| Critical |
0 |
— |
| High |
5 |
D2.2.3 no ClassicReadingBundler stage; D2.3.2/D2.3.6 SkipConfig 5/11 flags; D2.4.4 footnote skip; D2.4.12 formula skip |
| Medium |
13 |
D2.1.1 no ReadingBundle type; D2.1.10 no ListeningBundle; D2.2.1/D2.2.2 no bundler tier; D2.2.7 no audio pre-staging; D2.2.9 sync bundle creation; D2.3.1 no master config; D2.3.5 no ListeningBundlerConfig; D2.4.5/6/8/9/11 skip flags; D2.5.4/5 PreparedBlock/Audio; D2.6.1/2 audio binding + navigation |
| Low |
7 |
abstract hierarchy levels; universal bundler; parens/captions skip; metadata analytics; mutable title; eager block load |
| Intentional skip |
8 |
BookReadingBundle; EpubReadingBundle; EmbeddedReadingBundle; DynamicStandardView; book/embedded bundlers; ML parsing; embedded metadata |
D2 wave assignment proposal:
- Wave 4 (alongside Markdown) — D2.4.4 footnote skip + D2.4.9 URL skip + D2.4.6 citation skip (3 high-value flags, pure
TextCleaner extension)
- Wave 9 (architectural refactor with D1.6.1) — D2.2.3 ClassicReadingBundler stage, D2.3.1 master config, D2.3.5 ListeningBundlerConfig, D2.4.11 table skip, D2.4.12 formula skip, D2.5.4/D2.5.5 PreparedBlock/Audio pre-staging, D2.6.1 audio controller bundle binding
- Backlog — D2.2.9 async bundle, D2.4.5/7/8 brace/paren/bracket, D2.6.5 lazy block loading, D2.5.6 observable title
D3 — Reader UI + Block Types
Speechify role: Render a reading bundle as interactive, speech-synchronized scrolling text. Five distinct reader surfaces exist in SSDK (Classic, FixedLayout/PDF, Epub, Kindle/Scan, AudioBrowser). TapListenSDK targets only the Classic-equivalent surface per plan.md. Block and element type hierarchies drive both rendering and TTS chunking fidelity.
Key recon files: recon/ssdk-full-pipeline.md§D3 (lines 194–254), recon/speechify-class-dump-readers.txt:13–95, recon/speechify-strings.txt:122098–124830, 139386–139495, 390611–390620.
D3.1 — Reader surface selection
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D3.1.1 |
ClassicReaderViewModel (UICollectionView+TextKit) |
class-dump-readers:13; strings:123815, 164993 |
ReaderViewModel + ReaderView + BlockTextView |
PARTIAL |
High |
Functional parity in SwiftUI+UITextView, not UICollectionView+TextKit. No virtualization/sliding-window paging — see D3.3 |
| D3.1.2 |
FixedLayoutReaderViewModel (PDF page-curl) |
class-dump-readers:138; strings:123401, 124296 |
(none) |
INTENTIONAL_SKIP |
N/A |
Anti-goal per plan.md |
| D3.1.3 |
EpubReaderViewModel (vertical + horizontal pagination) |
strings:122024, 169619 |
(none) |
INTENTIONAL_SKIP |
N/A |
EPUB extracted and re-rendered in Classic surface |
| D3.1.4 |
KindleReader / ScanReader |
strings:122670, 373411 |
(none) |
INTENTIONAL_SKIP |
N/A |
DRM + camera-scan readers anti-goal |
| D3.1.5 |
AudioBrowserViewModel (browser overlay listener) |
strings:122899, 131403 |
(none) |
INTENTIONAL_SKIP |
N/A |
Tier 3+ |
D3.2 — ClassicBlock type hierarchy
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D3.2.1 |
SSDKClassicBlock (sealed parent) |
strings:139353; pipeline§D3 |
BlockKind enum (9 cases) |
PARTIAL |
High |
SSDK uses sealed class hierarchy; we use flat enum. Subtypes carry rich associated data; our enum carries only level: Int on heading |
| D3.2.2 |
SSDKClassicBlockHeading |
strings:139354 |
BlockKind.heading(level: Int) |
DONE |
— |
|
| D3.2.3 |
SSDKClassicBlockParagraph |
strings:139368 |
BlockKind.paragraph |
DONE |
— |
|
| D3.2.4 |
SSDKClassicBlockImageLocal/Remote |
strings:139355–139357 |
BlockKind.image |
PARTIAL |
Low |
No local/remote distinction; no URL on image blocks |
| D3.2.5 |
SSDKClassicBlockList + 6 subtypes |
strings:139358–139367 |
BlockKind.list (single flat case) |
PARTIAL |
Medium |
Nested list indentation and list-style markers lost |
| D3.2.6 |
SSDKClassicBlockTable + Row/Cell |
strings:139369–139371 |
(none — table text inlined) |
MISSING |
Medium |
No table case; cells flattened into paragraph text |
| D3.2.7 |
SSDKClassicBlockListStyle enum |
strings:139367 |
(none) |
MISSING |
Low |
No bullet/numbered/alpha discriminator |
D3.3 — StandardBlock type hierarchy (web + rich)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D3.3.1 |
SSDKStandardBlock (8 subtypes) |
strings:138426–138438 |
BlockKind partial overlap |
PARTIAL |
Medium |
Missing Formula, TextInsideImage, Caption |
| D3.3.2 |
SSDKStandardBlockCaption |
strings:138427 |
(none) |
MISSING |
Low |
Caption text folded into paragraphs |
| D3.3.3 |
SSDKStandardBlockFooter |
strings:138429 |
BlockKind.footer |
DONE |
— |
|
| D3.3.4 |
SSDKStandardBlockFootnote |
strings:138430 |
(none) |
MISSING |
Low |
Footnotes become paragraphs |
| D3.3.5 |
SSDKStandardBlockFormula |
strings:138431 |
(none) |
MISSING |
Low |
LaTeX as raw text |
| D3.3.6 |
SSDKStandardBlockHeader |
strings:138432 |
BlockKind.header |
DONE |
— |
|
| D3.3.7 |
SSDKStandardBlockHeading |
strings:138433 |
BlockKind.heading(level:) |
DONE |
— |
|
| D3.3.8 |
SSDKStandardBlockList |
strings:138436 |
BlockKind.list |
PARTIAL |
Low |
Same gap as D3.2.5 |
| D3.3.9 |
SSDKStandardBlockTable |
strings:138437 |
(none) |
MISSING |
Medium |
HTML/EPUB tables not preserved |
| D3.3.10 |
SSDKStandardBlockTextInsideImage |
strings:138438 |
(none) |
MISSING |
Low |
OCR'd text inside <img> not extracted |
D3.4 — StandardElement granular inline types
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D3.4.1 |
SSDKStandardElement (21 subtypes) |
strings:138441–138515 |
markdownSource: String? on RawBlock/CleanBlock/TextBlock + NSAttributedString(markdown:) rendering |
PARTIAL |
Low |
Wave 6.1 — bold/italic/code/strike/link runs now preserved across all 6 extractors and rendered with merged font traits. Image inline rendering still missing (D3.4.5); sub-word element seek not exposed |
| D3.4.2 |
SSDKStandardElementAnchor (external + internal) |
strings:138442–138444 |
Markdown link runs preserved; Coordinator.handleTap opens .link URLs via UIApplication.shared.open |
DONE |
— |
Wave 6.1 — external links work. Internal/intra-doc anchors not yet implemented (would need TOC anchor map) |
| D3.4.3 |
SSDKStandardElementBold/Italics/Underlined |
strings:138445, 138451, 138515 |
per-run UIFont symbolic traits merged with base font in BlockTextView.mergedFont |
DONE |
— |
Wave 6.1 — bold + italic + bold-italic rendered for MD/DOCX/RTF/PDF/EPUB/Web. Underline (<u>) dropped (no MD equivalent) |
| D3.4.4 |
SSDKStandardElementCode |
strings:138446 |
inline ` runs → traitMonoSpace font in renderer |
DONE |
— |
Wave 6.1 — both block-level and inline code spans rendered with monospace |
| D3.4.5 |
SSDKStandardElementImage (local + remote inline) |
strings:138448–138450 |
(none) |
MISSING |
Low |
Inline images not rendered |
| D3.4.6 |
SSDKStandardElementTable* |
strings:138509–138512 |
(none) |
MISSING |
Low |
Same as D3.3.9 element-level |
D3.5 — Line-level decomposition
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D3.5.1 |
SSDKHeadingLines |
strings:139029 |
NSLayoutManager.usedRect derived |
PARTIAL |
Low |
We compute geometry on-the-fly; sufficient for word highlight |
| D3.5.2 |
SSDKParagraphLines |
strings:139033 |
(same — layout manager) |
PARTIAL |
Low |
|
| D3.5.3 |
SSDKParagraphParts (text-run decomposition) |
strings:139034 |
(none — block text is one string) |
MISSING |
Medium |
No run metadata. Affects inline formatting (D3.4.3) and seek accuracy |
| D3.5.4 |
SSDKParagraphPartsWithImplicitWhitespace |
strings:139036 |
(none) |
MISSING |
Low |
Whitespace preservation for TTS |
| D3.5.5 |
SSDKTopLevelItem (root container) |
strings:139028 |
RawDocument/CleanDocument struct |
PARTIAL |
Low |
Functionally equivalent |
| D3.5.6 |
SSDKPageParagraph (paragraph + page binding) |
strings:139484 |
TextBlock.pageIndex: Int? |
PARTIAL |
Low |
Sufficient for TOC navigation |
D3.6 — ReaderViewModel architecture
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D3.6.1 |
ClassicReaderViewModel : NSObject <UIScrollViewDelegate> (Combine, 30+ subjects) |
class-dump-readers:13–95 |
@Observable @MainActor ReaderViewModel (Swift Observation) |
PARTIAL |
Medium |
Idiomatic SwiftUI deviation. No interactor decomposition |
| D3.6.2 |
ClassicReader.UserAction (event bus) |
class-dump-readers:25; strings:122376 |
tapWord(blockIndex:range:) direct method call |
PARTIAL |
Low |
No middleware composability |
| D3.6.3 |
ClassicReader.NavigationIntent (10-case rich type) |
class-dump-readers:31; strings:139470–139495 |
jumpTo(blockIndex:) only |
PARTIAL |
High |
Missing GoToPage, GoToSearchMatch, GoToUserHighlight, relative intents (Sentence/Word) |
| D3.6.4 |
ClassicReader.HighlightState (CurrentValueSubject) |
class-dump-readers:45, 62 |
highlightedBlockIndex + highlightedWordRange |
PARTIAL |
High |
No polygon-based cursor; multi-line bounding gap (see D4) |
| D3.6.5 |
ComponentActionBundle (compound gesture results) |
class-dump-readers:26 |
(none) |
MISSING |
Low |
|
| D3.6.6 |
TapActionHandler |
class-dump-readers:71 |
BlockTextView.Coordinator.handleTap |
PARTIAL |
Medium |
Inlined coordinator; not independently testable |
| D3.6.7 |
LongPressActionHandler |
class-dump-readers:72 |
(none) |
MISSING |
Medium |
Long-press selection + context menu absent. isSelectable = false disables system selection |
| D3.6.8 |
DragActionHandler |
class-dump-readers:73 |
(none) |
MISSING |
Low |
No drag-scrub seek |
| D3.6.9 |
SelectionToolbarPresentationManager |
class-dump-readers:80 |
(none) |
MISSING |
Medium |
Floating selection toolbar (copy/define/translate/highlight). Prerequisite for D8 |
| D3.6.10 |
ContentProvider (3 variants: Classic/Lazy/Static) |
class-dump-readers:51 |
(none — VM holds blocks: [TextBlock] directly) |
MISSING |
High |
Memory gap. Static-equivalent only. Large docs (>500 blocks) cause memory pressure. LazyContentProvider needed for production |
| D3.6.11 |
ClassicReaderNavigator + InDocumentNavigationBundle |
class-dump-readers:69–70 |
inline scrollTo in view |
PARTIAL |
Low |
No navigation isolation |
| D3.6.12 |
PreferencesInteractor + TextCustomizations + CursorStyle |
class-dump-readers:74, 55–58 |
deps.preferences.readerTextSize only |
PARTIAL |
Medium |
Missing font picker, line-height, column width, cursor style |
| D3.6.13 |
SelectionInteractor/CopyTextInteractor/UserHighlightInteractor |
class-dump-readers:75–78 |
(none) |
MISSING |
Medium |
Prerequisite for D8 |
| D3.6.14 |
TableLayoutInteractor |
class-dump-readers:75 |
(none) |
MISSING |
Low |
Depends on D3.2.6 |
D3.7 — Rendered view architecture
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D3.7.1 |
ClassicReaderRenderer (UICollectionView+compositional layout+TextKit 1) |
class-dump-readers:42, 60 |
LazyVStack + BlockTextView |
PARTIAL |
High |
Architectural deviation. No cell recycling; LazyVStack keeps each block's UITextView once visible |
| D3.7.2 |
visibleComponentSubject (viewport tracking) |
class-dump-readers:38, 61 |
onChange(of: cursor.blockIndex) + scrollTo |
PARTIAL |
Low |
No explicit visible-viewport tracking |
| D3.7.3 |
ClassicReaderConfiguration (typed config struct) |
class-dump-readers:83 |
scattered deps.preferences reads |
PARTIAL |
Low |
No single config object |
| D3.7.4 |
Word-tap → TTS seek |
class-dump-readers:71 |
Coordinator.handleTap → WordBoundaryFinder → tapWord |
DONE |
— |
Wave 1 fix |
| D3.7.5 |
Sentence + word dual-layer highlight |
class-dump-readers:99 |
TappableTextView.sentenceLayer + wordLayer |
DONE |
— |
Wave 3 |
| D3.7.6 |
Auto-scroll on cursor advance |
strings:137667 (enableAutoscroll) |
scrollTo(..., anchor: .center) |
DONE |
— |
|
D3.8 — Auxiliary panels
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D3.8.1 |
TOC panel |
strings:139471–139472 |
TableOfContentsView.swift |
DONE |
— |
|
| D3.8.2 |
Bookmarks sheet |
strings:139471 |
BookmarksSheet.swift + Bookmark model |
DONE |
— |
|
| D3.8.3 |
Voice / TTS settings |
(implicit) |
VoicePickerView.swift |
DONE |
— |
|
| D3.8.4 |
MiniPlayer |
(Speechify full transport: skip-back, scrubber, AirPlay, sleep timer) |
MiniPlayer.swift (state-aware loading + progress bar + transport + speed) |
PARTIAL |
Medium |
Wave 6.A — added: state-aware status header (loading spinner / "Block N of M %" progress / error / finished), animated transitions, play button disabled while loading. Still missing: skip-back 30s, chapter prev/next, AirPlay target, sleep timer |
| D3.8.5 |
In-document search UI |
strings:139477; class-dump-readers:53 |
(none) |
MISSING |
Medium |
Tier 2 |
| D3.8.6 |
User highlight overlay |
strings:139479; class-dump-readers:78 |
(none) |
MISSING |
Medium |
Tier 2 |
| D3.8.7 |
Quick-back action |
class-dump-readers:81 |
(none) |
MISSING |
Low |
Return-to-prior-position button |
| D3.8.8 |
Full-screen toggle |
class-dump-readers:23 |
(none) |
MISSING |
Low |
|
| D3.8.9 |
Skip-to-main-content |
class-dump-readers:24; strings:139475 |
(none) |
MISSING |
Low |
|
| D3.8.10 |
ReaderEditText (OCR text correction) |
class-dump-readers:97–135 |
(none) |
INTENTIONAL_SKIP |
N/A |
We never render page images |
| D3.8.11 |
Translation overlay |
class-dump-readers:18 |
(none) |
INTENTIONAL_SKIP |
N/A |
Anti-goal |
D3 summary
| Severity |
Count |
Top items |
| Critical |
0 |
— |
| High |
6 |
D3.4.1 inline element tree absent; D3.6.3 navigation intent richness; D3.6.4 HighlightState scalar (no polygon); D3.6.10 no LazyContentProvider; D3.7.1 no cell virtualization; D3.2.1 ClassicBlock sealed vs flat |
| Medium |
10 |
list/table block granularity; inline anchor/bold/italic; VM monolith; long-press; SelectionToolbar; preferences interactor; MiniPlayer gaps |
| Low |
13 |
image local/remote, list-style, caption/footnote/formula/OCR-image blocks; inline code/image/table; paragraph parts; whitespace; ComponentActionBundle; DragActionHandler; navigator isolation; visible-component; reader config; quick-back; full-screen; skip-main |
| Intentional skip |
6 |
FixedLayout, EpubNative, Kindle/Scan, AudioBrowser, ReaderEditText, Translation |
D3 wave assignment proposal:
- Wave 3 (current): D3.7.5 + D3.7.4 + D3.7.6 — DONE
- Wave 5: D3.2.6 + D3.3.9 (table block + basic table rendering), D3.8.4 (skip-back + scrubber)
- Wave 6: D3.4.1–D3.4.3 (inline element tree + bold/italic), D3.6.7 + D3.6.9 (long-press + SelectionToolbar — D8 prereq)
- Wave 7: D3.6.10 (LazyContentProvider — required before production with real corpus)
- Wave 8: D3.5.3 (paragraph parts / run-level model — architectural)
- Tier 2: D3.6.3 rich NavigationIntent, D3.8.5 search, D3.8.6 highlights/notes UI
D4 — Highlight & Polygon
Speechify role: Three-tier polygon rendering (paragraph / sentence / word) driven by per-reader HighlightInteractor subclasses, wired to Combine publishers on ClassicReaderViewModel / FixedLayoutReaderViewModel. Color, blend-mode, and toggle state passed as typed parameters on the EPUB config. User-created highlights live in a parallel UserHighlightInteractor system.
Key recon files: recon/speechify-class-dump-readers.txt:37,45,59,62,78,99, recon/speechify-class-dump-render.txt:206-215, recon/speechify-strings.txt:123089,123173,123286-123288,123428,123566-123567,123868,124202,124290-124291,124799,131430,131692, 137667, 138710-138716, 162751, 385036,385357-385359,389040,391637,391712,393577-393578,393605,437958.
D4.1 — Polygon tier model
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D4.1.1 |
_wordPolygon (@Published CGPolygon?) |
strings:385357, 393578, 439387 |
wordLayer: CAShapeLayer in TappableTextView |
PARTIAL |
Medium |
Word polygon as CAShapeLayer rect-union path, not CGPolygon struct. Loses sub-glyph detail |
| D4.1.2 |
_sentencePolygon (@Published CGPolygon?) |
strings:385359, 391637 |
sentenceLayer: CAShapeLayer |
PARTIAL |
Medium |
Same approach as D4.1.1 |
| D4.1.3 |
_paragraphPolygons (@Published [CGPolygon]) |
strings:391712, 437958 |
(none) |
MISSING |
Low |
Used in OCR-edit view. Not needed for TTS playback in Tier 1 |
| D4.1.4 |
CGPolygon struct |
strings:124201,130098,135187 |
(none — [CGRect] array) |
MISSING |
Low |
Adequate for straight text; breaks on rotated PDF pages |
| D4.1.5 |
_sections (@Published [CGPolygon]) |
strings:391712 |
(none) |
MISSING |
Low |
Section/chapter visual separators |
D4.2 — Word-polygon geometry refinements
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D4.2.1 |
wordPolygonVerticalEdges |
strings:389040, 393605 |
(none) |
MISSING |
Medium |
Edge-handling for word polygons (rounded vs flat caps at line-break boundaries). We apply static cornerRadius: 4 → unnecessary rounding at line-join edges |
| D4.2.2 |
roundedPath(rects:cornerRadius:) |
HighlightShapeBuilder.swift |
same |
PARTIAL |
Low |
Uniform radius vs SSDK's per-edge table |
| D4.2.3 |
wordRange/sentenceRange threading |
strings:385357-385359 |
highlightedWordRange + highlightedBlockIndex |
DONE |
— |
Equivalent data flow |
D4.3 — Blend mode & compositing
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D4.3.1 |
highlightMixBlendMode |
strings:137667 |
(none — alpha compositing only) |
MISSING |
Medium |
Visible defect on dark-mode PDFs. Fix: CAShapeLayer.compositingFilter = "multiplyBlendMode" |
| D4.3.2 |
sentenceHighlightHexColor |
strings:137667 |
hardcoded systemBlue.alpha(0.10) |
PARTIAL |
Medium |
No theme-aware token |
| D4.3.3 |
wordHighlightHexColor |
strings:137667 |
hardcoded systemYellow.alpha(0.55) |
PARTIAL |
Medium |
Harsh in dark theme |
| D4.3.4 |
hoveredSentenceHighlightHexColor |
strings:137667 |
(none) |
INTENTIONAL_SKIP |
N/A |
iPad pointer hover; phone-primary target |
D4.4 — Reader-mode HighlightInteractor variants
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D4.4.1 |
HighlightInteractor (base) |
strings:123089 |
(no protocol — inline applyHighlights) |
MISSING |
Low |
No interactor abstraction |
| D4.4.2 |
EpubReaderVerticalHighlightInteractor |
strings:123173 |
(none) |
INTENTIONAL_SKIP |
N/A |
EPUB out of scope |
| D4.4.3 |
EpubReaderHorizontalHighlightInteractor |
strings:124202 |
(none) |
INTENTIONAL_SKIP |
N/A |
|
| D4.4.4 |
KindleHighlightInteractor |
strings:124799 |
(none) |
INTENTIONAL_SKIP |
N/A |
|
| D4.4.5 |
AudioBrowserHighlightInteractor |
strings:131430 |
(none) |
INTENTIONAL_SKIP |
N/A |
|
| D4.4.6 |
Classic reader highlight path |
class-dump-readers:37,45 |
applyHighlights + HighlightShapeBuilder |
PARTIAL |
Medium |
Refactor when Wave 7 PDF reader starts |
D4.5 — Highlight state & Combine streaming
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D4.5.1 |
currentHighlightStatePublisher |
class-dump-readers:45; strings:384995 |
(none — direct @Observable mutation) |
MISSING |
High |
Wave 1 Bug D root cause. SSDK isolates highlight in dedicated publisher. We collapse with cursor → mid-seek onWord can clobber. lastEventSequence partial mitigation only |
| D4.5.2 |
currentHighlightStateSubject: CurrentValueSubject |
class-dump-readers:62 |
(none) |
MISSING |
High |
Backing store for D4.5.1 |
| D4.5.3 |
HighlightPosition (PassthroughSubject value type) |
strings:123868; class-dump-readers:37 |
(none) |
MISSING |
Medium |
Drives auto-scroll position |
| D4.5.4 |
highlightPositionSubject |
class-dump-readers:37,161 |
(none) |
MISSING |
Medium |
Separate subject from state |
| D4.5.5 |
isSentenceHighlightEnabled toggle |
strings:385533, 386018; class-dump-readers:59 |
(none — always on) |
MISSING |
Medium |
User toggle missing |
| D4.5.6 |
isSentenceHighlightEnabledSubject |
class-dump-readers:59,183 |
(none) |
MISSING |
Medium |
Reactive backing |
D4.6 — UserHighlight system (D8 territory)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D4.6.1 |
UserHighlightInteractor |
strings:123286 |
(none) |
MISSING |
Low |
D8 prereq |
| D4.6.2 |
UserHighlightInteractorIntegration |
strings:123287 |
(none) |
MISSING |
Low |
D8 prereq |
| D4.6.3 |
UserHighlightInteractorError |
strings:123288 |
(none) |
MISSING |
Low |
|
| D4.6.4 |
MutableUserHighlight + SDKBackedMutableUserHighlight |
strings:123566-123567 |
(none) |
MISSING |
Low |
|
| D4.6.5 |
UserHighlightStyle (5-color palette) |
strings:138710-138716 |
(none) |
MISSING |
Low |
|
| D4.6.6 |
UserHighlightStyleState |
strings:124290 |
(none) |
MISSING |
Low |
|
| D4.6.7 |
_userHighlightPolygons |
strings:391638 |
(none) |
MISSING |
Low |
|
| D4.6.8 |
userHighlightColorTokens (EPUB only) |
strings:137667 |
(none) |
MISSING |
Low |
EPUB context |
D4.7 — Selection & search highlight tracks
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D4.7.1 |
selectionBGHexColor |
strings:137667 |
isSelectable = false |
INTENTIONAL_SKIP |
N/A |
Selection disabled |
| D4.7.2 |
searchHighlightConfig |
strings:137667 |
(none) |
MISSING |
High |
Two-track architecture — blocks D9 |
| D4.7.3 |
_focusedSearchMatchPolygon + _searchMatchPolygons |
strings:391638 |
(none) |
MISSING |
Low |
D9 prereq |
| D4.7.4 |
_regionsOfInterestPolygons |
strings |
(none) |
MISSING |
Low |
PDF polish |
D4.8 — SpeechTextRenderer & cursor style
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D4.8.1 |
SpeechTextRenderer |
strings:131692 |
(folded into ReaderViewModel.wireTTS) |
MISSING |
Low |
|
| D4.8.2 |
cursorStyleSubject: CurrentValueSubject<CursorStyle> |
strings:385374; class-dump-readers:58 |
(none — fixed style) |
MISSING |
Medium |
Underline/fill/karaoke style picker missing |
| D4.8.3 |
_isSentenceHighlightEnabled (component iVar) |
strings:385376; class-dump-readers:59 |
(none) |
MISSING |
Medium |
Component shadow of D4.5.5 |
D4 summary
| Severity |
Count |
Top items |
| Critical |
0 |
— |
| High |
3 |
D4.5.1/D4.5.2 currentHighlightStatePublisher (Wave 1 Bug D); D4.7.2 searchHighlightConfig (blocks D9) |
| Medium |
9 |
wordPolygonVerticalEdges; highlightMixBlendMode; hardcoded colors; no interactor abstraction; HighlightPosition; isSentenceHighlightEnabled; CursorStyle |
| Low |
11 |
paragraph polygon tier; CGPolygon struct; sections; uniform corner radius; UserHighlight system (D8); search polygon iVars; regions-of-interest; SpeechTextRenderer |
| Intentional skip |
5 |
hover; Epub vertical/horizontal; Kindle; AudioBrowser; selection BG |
D4 wave assignment proposal:
- Wave 1 Bug D follow-up — D4.5.1/D4.5.2: dedicated
CurrentValueSubject<HighlightState> decoupled from cursor
- Wave 3 polish — D4.3.2/D4.3.3 theme-aware tokens; D4.3.1 blend mode
- Wave 5 UI — D4.5.5/D4.5.6 sentence toggle; D4.8.2 CursorStyle picker
- Wave 6 geometry — D4.2.1 line-boundary flat caps
- Wave 7 PDF — D4.4.1/D4.4.6 HighlightInteractor protocol
- Wave 9 / D9 — D4.7.2/D4.7.3 two-track search renderer
- D8 — D4.6.1–D4.6.7 UserHighlight system
D5 — TTS Engine + Voice
Speechify role: Drive text-to-speech for any reading bundle — on-device via SSDKLocalSpeechSynthesisAdapter or cloud via SSDKLocalStreamingMediaPlayer — with word-level alignment (SSDKSpeechMarks), typed voice catalog (SSDKVoiceSpec sealed hierarchy), and an assembly layer (SSDKListeningBundler / SSDKListeningExperience) that wires TTS+playback+alignment.
Key recon files: recon/ssdk-full-pipeline.md §D5 – §D7, recon/speechify-strings.txt:138095–138277,138896–138903,139039–139082,139468–139469, recon/speechify-recon-and-comparison.md §Pipeline C.
D5.1 — TTS abstraction protocol
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D5.1.1 |
SSDKLocalSpeechSynthesisAdapter |
strings:138095 |
TTSService protocol |
DONE |
— |
Single protocol covers local + cloud |
| D5.1.2 |
SSDKLocalSpeechSynthesisPlayer |
strings:138108 |
AVSpeechTTSService.swift |
DONE |
— |
AVSpeechSynthesizer + delegate; Wave 1 Bug D race fixed |
| D5.1.3 |
SSDKLocalStreamingMediaPlayer (cloud streaming, 3 read modes) |
strings:138132-138133 |
AudioStreamingMode enum + CloudTTSService mode parameter |
PARTIAL |
High |
Wave 5+ — enum scaffolded with 3 cases matching SSDK read-modes (.fullFetch / .chunked / .range). Only .fullFetch implemented. .chunked blocked on backend (probe confirmed api.taplisten.com returns JSON with full Content-Length, no Transfer-Encoding: chunked). .range defers to Wave 7+ (audiobook content). precondition(mode.isImplemented) early-fails on unsupported modes |
| D5.1.4 |
SSDKOnDeviceMediaSynthesisAdapter (neural TTS) |
strings:138153-138154 |
(none) |
INTENTIONAL_SKIP |
N/A |
Tier 3 |
| D5.1.5 |
SSDKSynthesisLocation (.device/.server/.auto) |
strings:138277 |
(none — hardcoded per impl class) |
MISSING |
Medium |
No auto-routing to cloud when backend configured |
| D5.1.6 |
SSDKLocalSpeechSynthesisEvent (5 cases) |
strings:138097–138107 |
5 closure callbacks |
PARTIAL |
Low |
Closures vs sealed event type |
| D5.1.7 |
SSDKOnDeviceSynthesisSupport |
strings:138217–138223 |
(none) |
MISSING |
Low |
No device capability check |
D5.2 — Speech marks / alignment data model
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D5.2.1 |
SSDKSpeechMarks + Impl |
strings:138258, 138264 |
SpeechMarksAdapter.swift (static mapping) |
PARTIAL |
High |
Stateless adapter vs persistent object. Alignment re-derived each chunk |
| D5.2.2 |
SSDKSpeechMarksChunk |
strings:138261 |
[TTSAlignmentItem] keyed per TTSChunk |
PARTIAL |
Medium |
Plain array vs typed chunk; discarded after play |
| D5.2.3 |
SSDKSentenceAndWordLocation (sentenceIdx + wordIdx + range) |
strings:138354 |
TTSWordEvent(sequence, blockIndex, nsRangeInBlock, word) |
PARTIAL |
High |
No sentence-level granularity. Critical for sentence highlight (D4) and sentence-level skip |
| D5.2.4 |
SSDKWordAndSentenceOverlay + OverlayHelper (tick-driven) |
strings:139040, 139042 |
ReaderViewModel.wireTTS() inline closures + BlockTextView |
PARTIAL |
Medium |
No dedicated overlay type; tightly coupled |
| D5.2.5 |
SSDKHighlightedWordPosition (.InFocus/.Left/.Right) |
strings:139039, 139627–139631 |
(none) |
MISSING |
Medium |
No viewport-relative position type for auto-scroll |
| D5.2.6 |
SSDKHighlightMixBlendMode config |
recon-comparison §6 |
(none — solid fill) |
MISSING |
Low |
See D4.3.1 |
| D5.2.7 |
Sentence + paragraph alignment marks |
(SSDKSentenceAndWordLocation) |
Word-level only |
PARTIAL |
High |
Same as D5.2.3 |
| D5.2.8 |
50ms tick rate |
Pipeline C |
50ms Timer in CloudTTSService |
DONE |
— |
Matches |
D5.3 — Voice catalog and voice spec
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D5.3.1 |
SSDKVoiceSpec (sealed: 5 types — Local/Available, Local/SynthesisBacked, Static, VMS/FromAudioServer, VMS/Persisted) |
strings:138292–138311 |
VoiceInfo (local) + CloudVoice (cloud) — flat structs |
PARTIAL |
High |
Type information erased; play() accepts voiceID: String? only |
| D5.3.2 |
SSDKVoiceGender enum |
strings:138286 |
CloudVoice.gender: String? |
PARTIAL |
Low |
Untyped string |
| D5.3.3 |
SSDKVoiceMetadata (rich) |
strings:138287–138291 |
VoiceInfo.quality: String |
PARTIAL |
Medium |
Missing accent, style, sample-rate, engine ID |
| D5.3.4 |
SSDKVoiceRef (opaque handle) |
strings:138118 |
String |
PARTIAL |
Low |
Plain string IDs |
| D5.3.5 |
SSDKVoicePreferences (defaultPremium/Free/Offline) |
strings:138903 |
PreferencesService.ttsVoiceID + ttsRate |
PARTIAL |
High |
Single voice ID; no premium/offline split, no per-language |
| D5.3.6 |
SSDKVoicePreferenceWithStaticValue (fallback chain) |
strings:138900–138901 |
(none) |
MISSING |
Medium |
If voice unavailable, silently falls back without notification |
| D5.3.7 |
SSDKLocalSpeechSynthesisVoicesProvider |
strings:138096 |
AVSpeechTTSService.availableVoices(languageCode:) |
DONE |
— |
|
| D5.3.8 |
Voice preview URL / sample playback |
strings:415710 |
CloudVoice.previewURL (stored, not surfaced) |
PARTIAL |
Medium |
VoicePickerView does not expose play-preview button |
| D5.3.9 |
availableVoices cloud catalog |
strings:388079 |
BackendAPIClient.listVoices() (stub throws) |
SCAFFOLDED |
High |
Real VMS integration absent. Hardcoded "Backend not configured" status |
| D5.3.10 |
SSDKLocalSynthesisOptions (pitch, rate, delays) |
strings:138112 |
AVSpeechTTSService.mappedRate + hardcoded pitch/delay |
PARTIAL |
Medium |
Pitch and delays not configurable |
D5.4 — Voice rate calibration
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D5.4.1 |
Per-voice WPM calibration |
Pipeline C: speedInWordsPerMinute; strings:418709, 418244, 387951 |
mappedRate linear |
PARTIAL |
Medium |
Perceived speed varies by voice |
| D5.4.2 |
currentSpeedFactor + currentVoice observable |
strings:387951-387952 |
PreferencesService.ttsRate + ttsVoiceID |
DONE |
— |
Same role; SSDK additionally stores WPM |
D5.5 — Chunk planning and audio chunking
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D5.5.1 |
Text chunk boundary algorithm |
Pipeline C; strings:136981–136990 |
ChunkPlanner (1500-char ceiling) |
PARTIAL |
Medium |
Fixed ceiling; no sentence-boundary awareness, no network-adaptive |
| D5.5.2 |
SSDKAudioStreamingStrategy |
strings:137918–137926 |
AudioStreamingStrategy enum (.always / .auto(lookahead:) / .never) — wired into ListeningBundler |
DONE |
— |
Wave 5 — enum drives prefetch state machine. Default .auto(lookahead: 2) keeps queue saturated during playback |
| D5.5.3 |
Audio cache (per-voice/rate/text-hash) |
Pipeline §4.4 |
AudioCache.swift SHA-256-keyed disk cache |
DONE |
— |
Solid implementation. SSDK additionally has in-memory layer |
| D5.5.4 |
Gapless / pre-buffered next-chunk |
Pipeline C |
ListeningBundle.audio(at:) + GaplessAudioPlayer.preloadNext(data:) |
DONE |
— |
Wave 5 — pipeline: bundle.audio(at:) awaits prefetch (lookahead=2 ahead) → player.preloadNext schedules next file in AVAudioPlayerNode queue → handleSegmentFinished promotes nextFile when current ends → 0ms transition. Wave 6 client-side prewarm cuts cold-start ~500-900ms |
D5.6 — ListeningBundler / ListeningExperience assembly
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D5.6.1 |
SSDKListeningBundler (assembles AudioConfig + voices + prefs + options) |
strings:138896 |
ListeningBundler class + ListeningBundle runtime in Core/Services/CloudTTS/ListeningBundler.swift |
DONE |
— |
Wave 5 — bundler.bundle(blocks:voice:) returns ListeningBundle owning [Int: Task]. Strategy-driven prefetch state machine. CloudTTSService delegates fully — no longer inline in ReaderViewModel |
| D5.6.2 |
SSDKListeningBundlerConfig/Options |
strings:138898–138899 |
AudioStreamingStrategy + AudioStreamingMode parameters on ListeningBundler.init and CloudTTSService.init |
PARTIAL |
Low |
Wave 5 — strategy + mode are explicit init params (typed, default-friendly). Not wrapped in single config struct; each consumer picks defaults. Acceptable for Tier 1 |
| D5.6.3 |
SSDKListeningExperience (transferable session) |
strings:139468–139469 |
(none — TTS lives in ReaderViewModel) |
MISSING |
Medium |
No transferable session object |
| D5.6.4 |
SSDKListeningBundle (output of bundler) |
strings:138893 |
(none) |
MISSING |
Low |
|
| D5.6.5 |
listeningBundlerConfig as sub-config |
strings:136917-136919 |
(none) |
MISSING |
Low |
Composition layer absent |
D5.7 — Playback control events and state
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D5.7.1 |
SSDKAudioControllerEvent (10 cases) |
strings:138191–138211 |
PlaybackState (6 cases) |
PARTIAL |
Medium |
Missing Seeking, ChangedVoice, ChangedVolume, Buffering detail, Destroyed |
| D5.7.2 |
pause/resume/seekTo(cursor:) symmetry |
strings:136288-136366 |
pause + resume present; no seekTo — replaced by stop+replay |
PARTIAL |
High |
Word tap restarts TTS. Long-form gap |
| D5.7.3 |
SSDKWordsListenedHelper (analytics) |
strings:139058–139064 |
(none) |
INTENTIONAL_SKIP |
N/A |
Analytics anti-goal |
| D5.7.4 |
SSDKPlaybackControls + SSDKPlayPauseButton (6 sub-states) |
strings:139076–139078 |
PlaybackState → MiniPlayer |
DONE |
— |
We collapse to fewer states |
| D5.7.5 |
SSDKPlaybackTime (NotReady/Ready) |
strings:139079–139082 |
.loading for both fetch and decode |
MISSING |
Low |
No timing-readiness signal |
| D5.7.6 |
SSDKNavigableSpeechProvider |
strings:138357 |
(none) |
MISSING |
Medium |
No navigation while preserving stream |
D5.8 — Multi-provider routing and plugin
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D5.8.1 |
SSDKVoiceSpecVMSVoiceSpec (cloud provider abstraction) |
strings:138305, 138310–138312 |
BackendAPIClient protocol + Stub + HTTP impls |
PARTIAL |
Medium |
Right shape but only one invented contract; no voice-selection routing |
| D5.8.2 |
voiceFactory injection |
strings:136923, 443879 |
(none) |
MISSING |
Medium |
No factory pattern |
| D5.8.3 |
SSDKDocumentVoiceInfo (per-doc voice override) |
strings:138893 |
(none — global) |
MISSING |
Medium |
One global voice for all docs |
| D5.8.4 |
Cloud synthesize endpoint contract |
Pipeline C |
HTTPBackendAPIClient.synthesize (invented contract) |
PARTIAL |
High |
Fabricated contract — incompatible with ElevenLabs/Google/Azure/Speechify. No URL-based audio streaming |
D5 summary
| Severity |
Count |
Top items |
| Critical |
0 |
— |
| High |
6 |
D5.1.3 chunked streaming (backend-gated); D5.2.3/D5.2.7 no sentence alignment; D5.3.1 voice spec erasure; D5.3.5 flat preferences; D5.3.9 cloud catalog scaffolded; D5.7.2 no seekTo; D5.8.4 invented contract — Wave 5 closed: D5.5.2, D5.5.4, D5.6.1 |
| Medium |
13 |
synthesis-location auto-routing; overlay helper; viewport position type; voice metadata; preference fallback chain; preview button; rate WPM calibration; chunk ceiling; ListeningBundlerConfig; transferable experience; missing events; NavigableSpeechProvider; multi-provider routing; voice factory; per-doc voice |
| Low |
5 |
closures vs sealed event; device capability check; blend mode; typed VoiceRef; PlaybackTime.NotReady |
| Intentional skip |
2 |
on-device neural TTS; words-listened analytics |
D5 wave assignment proposal:
- Wave 5 (TTS quality) — DONE: D5.5.2 (AudioStreamingStrategy), D5.5.4 (gapless prefetch via ListeningBundler), D5.6.1 (ListeningBundler), D2.5.4 (PreparedBlock), D2.5.5 (PreparedAudio); REMAINING: D5.7.2 (in-chunk seekTo for cloud)
- Wave 6 (voice catalog) — D5.3.8 (preview playback), D5.3.5 (structured prefs with fallback), D5.3.9 (real backend), D5.8.4 (real contract)
- Wave 7 (alignment) — D5.2.3 + D5.2.7 (sentence-level), D5.2.5 (viewport position type)
- Wave 9 (architecture) — D5.6.1 + D5.6.2 + D5.6.3 (extract ListeningBundler), D5.1.5 (auto-routing)
- Backlog — D5.8.3 (per-doc voice), D5.4.1 (WPM calibration), D5.3.1 (typed VoiceSpec)
D6 — Playback / Audio session / CarPlay
Speechify role: Master audio orchestration — audio session lifecycle, OS-level integrations (lock screen, Control Center, headphone remote), interruption handling, Now Playing metadata, platform extensions (CarPlay). Entry: SSDKAudioController; supporting: audioSessionManager, nowPlayingPublisher, nowPlayingCenter, routeChangeObserver, interruptionStream, sleepTimerService.
Key recon files: recon/speechify-strings.txt:384751,412750,415078,440518,450501-505,457353-358,412372-373,418202-204,441477,385250, recon/ssdk-full-pipeline.md §D4, recon/strings-all.txt:63677,67175.
D6.1 — Audio session category & activation
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D6.1.1 |
audioSessionManager (dedicated class) |
strings:384751,440519 |
AudioSessionManager.swift |
DONE |
— |
.playback + .spokenAudio + .duckOthers; activated/deactivated correctly |
| D6.1.2 |
Category/mode/options |
strings:133122-133126 |
matches |
DONE |
— |
|
| D6.1.3 |
UIBackgroundModes: audio Info.plist key |
(App Store requirement) |
Info.plist at project root with UIBackgroundModes [audio] |
DONE |
— |
Wave 10 — verified via plutil -p on built bundle |
| D6.1.4 |
Session deactivation with .notifyOthersOnDeactivation |
(inferred) |
correct |
DONE |
— |
|
| D6.1.5 |
audioSessionSetupFailed error event |
strings:412708,412930 |
print(...) only |
PARTIAL |
Medium |
Errors swallowed; should call onError |
D6.2 — Audio engine (gapless vs gapped)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D6.2.1 |
AVAudioEngine + AVAudioPlayerNode (on-device gapless) |
strings:241690-691; scheduleBuffer at 453654 |
GaplessAudioPlayer in Core/Services/CloudTTS/GaplessAudioPlayer.swift |
DONE |
— |
Wave 5 — full chain: engine.attach(playerNode) → timePitch → mainMixerNode. scheduleFile(file, at: nil, completionCallbackType: .dataPlayedBack) for both current + preloaded next. callbackGeneration / preloadGeneration counters guard stale completions. AVAudioUnitTimePitch.rate for variable-speed (avoids AVAudioPlayer enableRate MP3 hardware-decoder glitches). Wave 6 added prepareEngine() for app-launch pre-warm. Still used only by CloudTTSService — local AVSpeechTTSService keeps direct AVSpeechSynthesizer |
| D6.2.2 |
SSDKLocalStreamingMediaPlayer (cloud chunked HLS/MP3) |
Pipeline C |
CloudTTSService + ListeningBundler lookahead prefetch |
PARTIAL |
Medium |
Wave 5 — chunk boundary gap eliminated via prefetch lookahead=2. True HTTP chunked streaming (AudioStreamingMode.chunked) blocked on backend; see D5.1.3 |
| D6.2.3 |
SSDKAudioStreamingStrategy (Always/Auto/Never) |
§D4 |
AudioStreamingStrategy enum on ListeningBundler.init |
DONE |
— |
Wave 5 — see D5.5.2. Same enum drives both cloud TTS prefetch (D5.5.2) and audio session prefetch behavior (D6.2.3) |
D6.3 — Interruption handling
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D6.3.1 |
interruptionStream / interruptionContinuation |
strings:415160-162,448045-049 |
PlaybackCoordinator.handleInterruption in Core/Services/PlaybackCoordinator.swift |
DONE |
— |
Wave 10 — observes AVAudioSession.interruptionNotification, pauses on .began, auto-resumes on .ended only when shouldResume flag set |
| D6.3.2 |
interruptionResumeEnabled (honour shouldResume hint) |
strings:418282,448047 |
PlaybackCoordinator.handleInterruption checks AVAudioSession.InterruptionOptions.shouldResume |
DONE |
— |
Wave 10 |
| D6.3.3 |
audioSessionInterrupted: legacy ObjC selector |
strings:440518 |
(none) |
MISSING |
Low |
Pre-iOS-15 path |
D6.4 — Route change / headphone disconnect
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D6.4.1 |
routeChangeObserver (AVAudioSession.routeChangeNotification) |
strings:412750-852,415161-163 |
PlaybackCoordinator.handleRouteChange |
DONE |
— |
Wave 10 — pauses on oldDeviceUnavailable (headphone unplug, AirPods removal) |
| D6.4.2 |
NAVAudioSessionRouteChangeReason typed enum |
strings:132092 |
(none) |
MISSING |
Medium |
Minor — raw constants work |
| D6.4.3 |
AirPlay / Bluetooth routing |
(implicit via category) |
implicit (untested without background mode plist) |
PARTIAL |
Low |
Document in integration guide |
D6.5 — Now Playing Info (lock screen / Control Center)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D6.5.1 |
nowPlayingCenter / MPNowPlayingInfoCenter.default() |
strings:387873,450501-505 |
PlaybackCoordinator.updateNowPlayingInfo(...) |
DONE |
— |
Wave 10 — import MediaPlayer, populates MPNowPlayingInfoCenter.default().nowPlayingInfo |
| D6.5.2 |
MPMediaItemPropertyTitle |
strings:450503 |
wired from document.title |
DONE |
— |
Wave 10 |
| D6.5.3 |
MPMediaItemPropertyArtist |
(inferred) |
wired from document.author |
DONE |
— |
Wave 10 |
| D6.5.4 |
MPMediaItemPropertyArtwork |
(inferred) |
Document.coverImageData → UIImage → MPMediaItemArtwork(boundsSize:requestHandler:) |
DONE |
— |
Wave 10 |
| D6.5.5 |
MPNowPlayingInfoPropertyElapsedPlaybackTime |
strings:450503 |
computed from absoluteCharOffsetForCursor() / charsPerSecond |
DONE |
— |
Wave 10 — char-offset estimate; coarse but functional |
| D6.5.6 |
MPNowPlayingInfoPropertyPlaybackRate |
strings:456027 |
set to viewModel.rate when playing, 0.0 when paused |
DONE |
— |
Wave 10 |
| D6.5.7 |
Periodic update of nowPlaying |
(inferred) |
ReaderViewModel.refreshNowPlaying() called on every word event + state change |
DONE |
— |
Wave 10 — sub-second granularity (every word) |
D6.6 — Remote Command Center (headphone / lock / AirPods)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D6.6.1 |
playCommand / pauseCommand / togglePlayPauseCommand |
strings:451507,451314,418202-204 |
PlaybackCoordinator.registerRemoteCommands |
DONE |
— |
Wave 10 — all 3 wired via MPRemoteCommandCenter.shared() to delegate?.coordinatorDidRequestPlay/Pause/Toggle |
| D6.6.2 |
skipForwardCommand (preferred 30s) |
strings:412372-373,457353-357 |
wired with preferredIntervals = [30] |
DONE |
— |
Wave 10 — converts to char-offset delta via 200 WPM × rate |
| D6.6.3 |
skipBackwardCommand (preferred 15s) |
strings:457354 |
wired with preferredIntervals = [15] |
DONE |
— |
Wave 10 |
| D6.6.4 |
nextTrackCommand / previousTrackCommand |
strings:450386,451915 |
both .isEnabled = false |
DONE |
— |
Wave 10 — explicitly disabled rather than silently ignoring |
| D6.6.5 |
changePlaybackPositionCommand (lock-screen scrubber seek) |
strings:441477 |
wired to seekToTime(_:) |
DONE |
— |
Wave 10 — char-offset estimate maps position to block + offset |
| D6.6.6 |
playCommandTask (async back-pressure) |
strings:420892,430976 |
(none) |
MISSING |
Low |
Implementation detail |
D6.7 — Sleep timer
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D6.7.1 |
sleepTimerService / Interactor / Configurator |
strings:385786,457364,163213 |
(none) |
MISSING |
Low |
Wave 7+ |
| D6.7.2 |
sleepTimerStateObserver / sleepTimerRemainingTime |
strings:388047,438825 |
(none) |
MISSING |
Low |
Depends on D6.7.1 |
D6.8 — Mini-player UI parity
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D6.8.1 |
Play / Pause / Stop |
(inferred) |
MiniPlayer.swift |
DONE |
— |
|
| D6.8.2 |
Speed slider (rate control) |
ChangedSpeedFactor event |
continuous 0.3–1.0 slider |
PARTIAL |
Low |
Speechify uses discrete WPM steps (1x/1.5x/2x/3x/4.5x); we cap at 1.0x |
| D6.8.3 |
Voice picker button |
ChangedVoice event |
voice button → VoicePickerView |
DONE |
— |
|
| D6.8.4 |
Buffering indicator |
Buffering event |
hourglass.circle.fill during .loading |
DONE |
— |
|
| D6.8.5 |
Progress bar / scrubber |
(inferred from full mini-player) |
(none) |
MISSING |
Medium |
Add ProgressView(value:total:) tied to progressFraction |
| D6.8.6 |
Chapter / block auto-advance |
(sequential streaming) |
speakNext() / playNextChunk() |
DONE |
— |
|
| D6.8.7 |
Seek-by-tap-on-word |
NavigationIntent |
tapWord → playFromCursor |
DONE |
— |
Wave 1 |
D6.9 — CarPlay
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D6.9.1 |
CPListItem, CPListTemplate, CPNowPlayingTemplate |
strings-all:63677,67175 |
(none) |
INTENTIONAL_SKIP |
N/A |
Anti-goal. Document for host apps |
| D6.9.2 |
CPApplicationDelegate / CPInterfaceController |
(inferred) |
(none) |
INTENTIONAL_SKIP |
N/A |
|
D6 summary (after Wave 5/6/10)
| Severity |
Count |
Top items |
| Critical |
0 |
All 4 closed in Wave 10 P0 |
| High |
2 |
shouldResume flag (still missing); changePlaybackPositionCommand (still missing) — Wave 5 closed: D6.2.1 gapless engine, D6.2.2 cloud prefetch |
| Medium |
4 |
session error swallowed; route-change reason enum; artwork; mini-player skip-back/scrubber polish |
| Low |
6 |
session option; legacy ObjC interruption; AirPlay/BT untested; playback rate; nextTrack/previousTrack disabled; sleep timer |
| Intentional skip |
2 |
CarPlay templates |
D6 wave assignment proposal:
- Wave 10 P0 — DONE ✓ — D6.1.3 (UIBackgroundModes), D6.3.1+D6.3.2 (interruption), D6.4.1 (route-change), D6.6.1+D6.6.2+D6.6.3 (RemoteCommandCenter), D6.5.1-D6.5.7 (Now Playing wiring)
- Wave 5 — DONE ✓ — D6.2.1 (AVAudioEngine + AVAudioPlayerNode via GaplessAudioPlayer), D6.2.2 (cloud prefetch via ListeningBundler), D6.2.3 (AudioStreamingStrategy)
- Wave 6 — DONE ✓ — first-byte client-side optimization (voice catalog disk cache + engine pre-warm + warmUp())
- Wave 6.A — DONE ✓ — MiniPlayer state-aware loading + progress UI; ReaderViewModel state transition fix
- Backlog — D6.6.5 changePlaybackPositionCommand (lock-screen scrubber seek), sleep timer, AirPlay surface, mini-player skip-back 30s
D7 — Persistence & Library
Speechify role: Local persistence via KMP-shared SQLite (GRDB adapter, SSDKDatabaseManager), cloud sync via Firebase Firestore (10 syncers per entity), library browsing (SSDKLibraryItem sealed), full-text search, archive management.
Key recon files: recon/speechify-strings.txt:122808,123247,135373,136725-136728,138638-138705,139048,139114-139247,139312-139545, recon/ssdk-full-pipeline.md §D10, §D11, Pipeline A/G.
D7.1 — Storage technology
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.1.1 |
SSDKDatabase + SSDKDatabaseManager (KMP SQLite via GRDB) |
strings:139114,139201; class-dump:123247 |
SwiftData modelContainer(for:) |
PARTIAL |
Low |
iOS 17+; same guarantees for our single-platform target |
| D7.1.2 |
SSDKDatabaseQuery DSL (9 Where variants) |
strings:139203-139228 |
FetchDescriptor + #Predicate |
PARTIAL |
Low |
Missing compound-key cursor queries |
| D7.1.3 |
SSDKDatabaseTransaction + WriteIntent |
strings:139247,139231-139234 |
modelContext.save() |
DONE |
— |
|
| D7.1.4 |
SSDKDatabaseTableIndexDefinitionFullTextSearch |
strings:139244-139246 |
(none) |
MISSING |
Medium |
No FTS; library search must load all docs into memory |
| D7.1.5 |
Schema migration (DatabaseMigrator) |
strings:135562,137337 |
(none — SwiftData v1, no VersionedSchema) |
MISSING |
High |
Tech-debt H5. Any field rename triggers destructive migration |
D7.2 — Library item model
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.2.1 |
SSDKLibraryItem (sealed: Content/Folder) |
strings:138672-138674 |
flat [Document] |
PARTIAL |
Medium |
No folder concept |
| D7.2.2 |
SSDKLibraryItemFolder |
strings:138674 |
(none) |
MISSING |
Medium |
Tier 2 |
| D7.2.3 |
SSDKFolderQuery + SSDKFolderReference |
strings:138659-138668 |
(none) |
MISSING |
Low |
Folder DSL |
| D7.2.4 |
SSDKContentType |
strings:138646-138647 |
DocumentFormat |
DONE |
— |
|
| D7.2.5 |
SSDKListeningProgress |
strings:139048-139049 |
lastReadCharOffset + lastReadBlockOrderIndex + lastReadCharOffsetInBlock |
DONE |
— |
Wave 2 stable cursor |
D7.3 — Filter and sort
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.3.1 |
SSDKFilterAndSortOptions |
strings:138649 |
(none — fixed sort) |
MISSING |
Medium |
Hardcoded lastReadDate DESC, importDate DESC |
| D7.3.2 |
SSDKFilterTypeANY/RECORDS/FOLDERS/ARCHIVED |
strings:138652-138658 |
(none) |
MISSING |
Medium |
No filter UI |
| D7.3.3 |
7 sort variants (RECENTLY_LISTENED, DATE_ADDED, ALPHABETICAL, LISTENING_PROGRESS, DATE_ARCHIVED) |
strings:138693-138703 |
2 sort keys hardcoded |
PARTIAL |
Low |
No sort picker |
| D7.3.4 |
lastListenedAt vs lastUpdatedAt distinction |
strings:136725 |
lastReadDate (collapsed) |
PARTIAL |
Low |
|
D7.4 — Archived state
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.4.1 |
isArchived |
strings:136727 |
(none — delete only) |
MISSING |
Medium |
Hard-delete on swipe; no recovery |
| D7.4.2 |
Bulk archive ops |
strings:136727-136728,136686-136687 |
(none) |
MISSING |
Low |
Depends on D7.4.1 |
| D7.4.3 |
SortByDATE_ARCHIVED |
strings:138699 |
(none) |
MISSING |
Low |
|
D7.5 — Bookmarks
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.5.1 |
SSDKBookmark |
strings:138638 |
Bookmark @Model |
DONE |
— |
id/createdAt/blockOrderIndex/charOffset/note?/snippet |
| D7.5.2 |
SSDKBookmarkHelper (CRUD service) |
strings:139439 |
inline modelContext.delete in BookmarksSheet |
PARTIAL |
Low |
Refactor into BookmarkService in Wave 8 |
| D7.5.3 |
SSDKBookmarksList + Item |
strings:139440-139441 |
document.bookmarks sorted in view |
DONE |
— |
|
| D7.5.4 |
BookmarkActionState (HasInViewport/NoneInViewport) |
strings:139435-139438 |
(none — always Add) |
MISSING |
Low |
Users may create duplicates at same block |
D7.6 — User highlights and notes (D8 territory)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.6.1 |
SSDKUserHighlight + Coordinates + Style |
strings:138708-138716,139622-139623 |
(none) |
MISSING |
High |
Range-based annotations; D8 scope. Cross-link D4.6 |
| D7.6.2 |
SSDKUserHighlightsHelper + observer |
strings:139542-139545 |
(none) |
MISSING |
High |
No CRUD service |
| D7.6.3 |
Note text on highlight |
(D9 inferred) |
Bookmark.note: String? |
PARTIAL |
Medium |
Notes only on point bookmarks, not ranges |
D7.7 — Import status persistence
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.7.1 |
SSDKImportStatus (Finished/Uploading) |
strings:138669-138671 |
(in-memory ImportState only) |
MISSING |
Low |
Acceptable for offline-first. Cross-link D1.8.13 |
D7.8 — Library search
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.8.1 |
SSDKSearchHelper + Query + State |
strings:139500-139507 |
(none) |
MISSING |
Medium |
Tier 2. Cross-link D7.1.4 + D9 |
D7.9 — Cover image strategy
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.9.1 |
SSDKBookThumbnailsHelper (CDN URLs) |
strings:139640-139643 |
Document.coverImageData: Data? (JPEG embedded in SwiftData) |
PARTIAL |
Low |
Works offline; bloats store. Move to file-backed in Wave 8 |
D7.10 — Per-document settings
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.10.1 |
SSDKVoicePreference (per-doc) |
strings:138900-138903 |
global PreferencesService.ttsVoiceID |
MISSING |
Low |
All docs share one voice |
| D7.10.2 |
Per-doc speed/text-size |
(inferred) |
global only |
MISSING |
Low |
Same gap |
D7.11 — File hash dedup
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.11.1 |
(no explicit symbol found) |
— |
Document.fileHash SHA-256 + #Predicate check |
DONE |
— |
Positive differentiator |
D7.12 — iCloud / CloudKit
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.12.1 |
SSDKSyncProvider (10 syncers) + Firestore |
strings:139313-139331; pipeline G |
(none — local SwiftData only) |
INTENTIONAL_SKIP |
N/A |
Multi-device sync explicit anti-goal |
D7.13 — Backup and file-URL integrity
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D7.13.1 |
Server-canonical blob key (sandbox-agnostic) |
Pipeline A |
Document.fileRelativePath resolved at runtime |
DONE |
— |
Wave 10 — see D1.8.2 |
| D7.13.2 |
OpenConnectionIsBlockingDatabaseMigration event |
strings:139198 |
(none) |
MISSING |
High |
Downstream of D7.1.5 |
D7 summary
| Severity |
Count |
Top items |
| Critical |
1 |
D7.13.1 absolute file URL (Bug B5, cross-links D1.8.2) |
| High |
3 |
D7.1.5 no migration plan (H5); D7.6.1/D7.6.2 user highlights absent (D8); D7.13.2 no migration-blocking UI |
| Medium |
5 |
folder hierarchy; filter/sort UI; archive (soft-delete); range notes; library search bar |
| Low |
8 |
compound-key cursor; sort variants; lastListenedAt split; bulk archive; BookmarkHelper refactor; viewport bookmark state; import status persistence; cover image bloat; per-doc voice/speed |
| Intentional skip |
1 |
CloudKit/multi-device sync |
| Done / positive |
5 |
transactions; content type; listening progress cursor; bookmark model; SHA-256 dedup |
D7 wave assignment proposal:
- Wave 8 (annotations + library polish) — D7.4.1 archive, D7.5.2 BookmarkService, D7.9.1 cover → file-backed
- Wave 8 prereq — D7.1.5
VersionedSchema + SchemaMigrationPlan
- Wave 9 (search + filter UI) — D7.1.4 FTS, D7.8.1 search bar, D7.3.1/D7.3.2 filter+sort picker
- P0 tech debt (Bug B5) — D7.13.1 relative file URL (paired with D1.8.2)
- Tier 2 — D7.2.2 folders, D7.6.1 user highlights (D8), D7.10.1 per-doc voice
Speechify role: Platform scaffolding wiring SSDK services together — FactoryKit DI, config-driven bundler factory, plugin extension point, multi-service root client (SSDKSpeechifyClient), environment/telemetry/analytics infrastructure. Entry: SSDKSpeechifyClientFactory → SSDKSpeechifyClient.
Key recon files: recon/speechify-strings.txt:127345-127360,137903-137948,138871-138875, recon/ssdk-full-pipeline.md §1.1, recon/ssdk-entry-points.txt.
D15.1 — Dependency injection architecture
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D15.1.1 |
FactoryKit (Container, SharedContainer, scopes) |
strings:127345-127352, 424522-424528 |
DependencyContainer singleton (lazy) |
PARTIAL |
Low |
No scope enforcement, no override-for-testing |
| D15.1.2 |
LazyInjected / WeakLazyInjected property wrappers |
strings:127359-127360, class-dump-render:201 |
(none — direct construction) |
MISSING |
Low |
Blocks constructor-injection unit testing |
| D15.1.3 |
Multi-level DI hierarchy (App/MainShell/Service/Listening/SDK) |
strings:123443,124244,124305,124362,123506; DI/SDK/SDKDIContainer.swift at 169596 |
Single flat singleton |
MISSING |
Low |
Adding per-screen lifetimes later requires rewrite |
| D15.1.4 |
Constructor injection vs service-locator mixing |
(FactoryKit Resolving) |
Mix: DependencyContainer.shared + @Environment(\.deps) |
PARTIAL |
Low |
One injection mechanism + one service-locator pull |
| D15.1.5 |
DI override seam for tests |
FactoryKit register mechanism |
(none — private init() singleton) |
MISSING |
Medium |
Tests cannot substitute mocks. Wave 9 candidate |
D15.2 — Root client and environment
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D15.2.1 |
SSDKSpeechifyClient (root service object) |
strings:137945, §1.1 |
DependencyContainer (1 service) |
PARTIAL |
Low |
SSDK owns 7+ named services; we own 1. Will grow |
| D15.2.2 |
SSDKSpeechifyClientFactory |
strings:137948 |
(none — initialized unconditionally in App) |
MISSING |
Low |
No construction phase |
| D15.2.3 |
SSDKAppEnvironment (dev/staging/prod) + config |
strings:137909, 137948; selector at 136081 |
(none — single env) |
MISSING |
Low |
Add when staging/prod split needed |
| D15.2.4 |
SSDKSpeechifyClientServices (typed accessor bag) |
strings:137946 |
flat properties on container |
MISSING |
Low |
Equivalent at current scale |
| D15.2.5 |
SSDKAdaptersProvider (Firebase shim layer) |
strings:137903-137904 |
(none — no Firebase) |
INTENTIONAL_SKIP |
N/A |
Not applicable |
D15.3 — Factory and plugin system
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D15.3.1 |
SSDKBundlerFactory |
strings:138871 |
(none — inline in ImportService) |
MISSING |
Low |
See D1.6.1 |
| D15.3.2 |
SSDKBundlerFactoryConfig (composable) |
strings:138873 |
SkipConfig (5 flags only) |
PARTIAL |
Low |
Documented in D1.6.3 |
| D15.3.3 |
SSDKBundlerPlugins + Companion |
strings:138874-138875 |
(none) |
MISSING |
Low |
Plugin layer for third-party hooks. Single-tenant — not needed for Tier 1 |
D15.4 — Service decomposition
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D15.4.1 |
SSDKDatabase* (KMP cross-platform mini-ORM) |
strings:139114-139225 |
SwiftData (iOS-native) |
PARTIAL |
Low |
No cross-platform portability — intentional |
| D15.4.2 |
SSDKLoggingService (structured logger) |
strings:138732 |
print(...) (1 call) |
MISSING |
Low |
Wave 9: replace with os.Logger |
| D15.4.3 |
SSDKTelemetryOptions / DiagnosticReporter |
strings:138743 |
(none) |
INTENTIONAL_SKIP |
N/A |
OSLog-only per plan |
| D15.4.4 |
20+ *Analytics protocols |
strings:123102, 122698, 127959 |
(none) |
INTENTIONAL_SKIP |
N/A |
Anti-goal |
| D15.4.5 |
Firebase Crashlytics |
strings:122885 |
(none) |
MISSING |
Low |
Revisit before broad rollout |
| D15.4.6 |
SSDKFirebaseAuthService |
strings:138011-138016 |
(none) |
INTENTIONAL_SKIP |
N/A |
D12 anti-goal |
| D15.4.7 |
SSDKFirebaseFirestoreService + Storage |
strings:138018-138030 |
(none) |
INTENTIONAL_SKIP |
N/A |
D11 anti-goal |
D15.5 — Feature flags / A-B
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D15.5.1 |
SSDKNextGenConfig* (16 classes — Firebase Remote Config) |
§4.3 |
(none) |
INTENTIONAL_SKIP |
N/A |
No A/B requirement |
D15.6 — Cloud provider registry (our addition)
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D15.6.1 |
(Speechify uses URLImportService + backend processing) |
— |
CloudProviderRegistry.shared (Dropbox + GDrive registered) |
DONE |
— |
Idiomatic; OneDrive etc. slot in without touching call sites |
D15.7 — App wiring: URL scheme + App Groups
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D15.7.1 |
URL scheme registered in Info.plist |
(Speechify speechify://) |
Info.plist CFBundleURLTypes registered for taplisten scheme |
DONE |
— |
Wave 10 — switched project to manual INFOPLIST_FILE; verified via plutil -p |
| D15.7.2 |
Deep link parser |
— |
DeepLinkHandler.parse(_:) |
DONE |
— |
Logic complete once D15.7.1 registered |
| D15.7.3 |
App Group entitlement |
(Speechify uses Firebase dynamic links) |
TapListenSDK.entitlements at project root with com.apple.security.application-groups = group.com.tap.listen.sdk.shared |
DONE |
— |
Wave 10 — entitlement wired via CODE_SIGN_ENTITLEMENTS build setting. Note: Apple Developer Portal registration needed for paid-team device builds |
| D15.7.4 |
Environment propagation in App scene |
— |
@Environment(coordinator) + @Environment(\.deps) + modelContainer |
DONE |
— |
Clean SwiftUI scene setup |
D15.8 — Module boundaries
| # |
Speechify symbol |
Recon evidence |
Our equivalent |
Status |
Severity |
Notes |
| D15.8.1 |
Multi-module (SSDK / SpeechifyMobile / SpeechifyCommon / FactoryKit / SubscriptionsService) |
strings:169596,169751; class-dump-render:201 |
Single module TapListenSDK |
MISSING |
Low |
No API boundary enforcement. Add SPM local packages when team grows |
D15 summary
| Severity |
Count |
Top items |
| Critical |
2 |
D15.7.1 URL scheme not in Info.plist (deep link dead); D15.7.3 App Group entitlement missing (SharedURLStore broken) |
| High |
0 |
— |
| Medium |
1 |
D15.1.5 no DI override seam (testability) |
| Low |
8 |
FactoryKit vs singleton; LazyInjected; container hierarchy; mixed DI; root client/factory/env config gaps; bundler factory + plugins; structured logger; crash reporting; single-module |
| Intentional skip |
7 |
adapter provider; telemetry; analytics; Crashlytics; Firebase Auth; Firestore/Storage; NextGenConfig |
D15 wave assignment proposal:
- Immediate / pre-ship (P0) — D15.7.1 (register
taplisten://), D15.7.3 (create .entitlements with com.apple.security.application-groups)
- Wave 9 (architectural refactor) — D15.1.5 constructor injection seam, D15.4.2
os.Logger, D15.3.1 BundlerFactory if D1.6.1 lands
- Backlog — FactoryKit / scoped DI (only worthwhile if service count > 10), local SPM packages (team-growth concern)
Cross-domain aggregate
Aggregated severity across all 8 audited domains (D1 + D2 + D3 + D4 + D5 + D6 + D7 + D15).
Updated after Wave 5/6/6.A (2026-04-29).
| Severity |
Open |
Done so far |
% done |
| Critical |
0 |
8 |
100% |
| High |
~17 |
~14 |
~45% |
| Medium |
~52 |
~13 |
~20% |
| Low |
~72 |
~8 |
~10% |
| Intentional skip |
(37 rows, n/a) |
— |
— |
Wave 5/6/6.A closures (~9 High rows + ~4 Medium):
- D2.5.4 PreparedBlock — Wave 5
- D2.5.5 PreparedAudio — Wave 5
- D5.5.2 AudioStreamingStrategy — Wave 5
- D5.5.4 Gapless prefetch — Wave 5
- D5.6.1 ListeningBundler — Wave 5
- D6.2.1 AVAudioEngine + AVAudioPlayerNode — Wave 5
- D6.2.2 Cloud chunk prefetch — Wave 5 (lookahead-based)
- D6.2.3 AudioStreamingStrategy — Wave 5
- D5.6.2 ListeningBundlerConfig — Wave 5 (partial — strategy + mode params)
- D5.1.3 SSDKLocalStreamingMediaPlayer — Wave 5+ (AudioStreamingMode enum scaffold; .fullFetch DONE; .chunked backend-gated)
- D3.8.4 MiniPlayer status header + progress bar — Wave 6.A
- Wave 6 client-side prewarm — voice catalog disk cache + engine pre-warm + warmUp() — first-play ~500-900 ms saved (no specific Speechify symbol; client-only optimization)
Critical-path Wave 10 — ALL CLOSED ✓
| Row |
Item |
Status |
| D6.1.3 |
UIBackgroundModes: audio |
✓ DONE |
| D6.3.1 |
AVAudioSession.interruptionNotification handler |
✓ DONE |
| D6.3.2 |
shouldResume flag handling |
✓ DONE |
| D6.4.1 |
routeChangeNotification handler |
✓ DONE |
| D6.5.1-7 |
MPNowPlayingInfoCenter (title, author, artwork, elapsed, rate, periodic) |
✓ DONE (all 7 sub-items) |
| D6.6.1 |
playCommand / pauseCommand / togglePlayPauseCommand |
✓ DONE |
| D6.6.2-3 |
skipForwardCommand / skipBackwardCommand |
✓ DONE |
| D6.6.4 |
nextTrackCommand / previousTrackCommand (disabled cleanly) |
✓ DONE |
| D6.6.5 |
changePlaybackPositionCommand |
✓ DONE |
| D15.7.1 |
taplisten:// URL scheme registration |
✓ DONE |
| D15.7.3 |
App Group entitlement |
✓ DONE |
| D1.8.2 / D7.13.1 |
Relative file URL in Document |
✓ DONE |
Highest-priority remaining work
| Wave |
Open Critical/High items |
| Wave 4 |
D1.3.5 + D1.5.4 + D2.4.4/6/9 (markdown SPM swap + skip flags) |
| Wave 5 |
D5.5.4 (gapless prefetch) + D5.7.2 (true seek) + D4.5.5 (sentence toggle) + D3.2.6 (table block) |
| Wave 6 |
D5.3.5 + D5.3.9 + D5.8.4 (real backend wiring) + D3.4.1-3 (inline element tree) |
| Wave 7 |
D5.2.3 (sentence alignment) + D3.6.10 (LazyContentProvider) |
| Wave 8 |
D7.1.5 (SchemaMigrationPlan — bomb hẹn giờ) + UserHighlight system |
| Wave 9 |
D1.6.1 + D2.2.3 (Bundler stage) + D5.6.1-3 (ListeningBundler) + D7.1.4 (FTS) |
Tech-debt outside the matrix (still open)
- B4 Share Extension target chưa tạo (high)
- H2 Regex HTML parser (high) — Wave 2 partial fix only
- H3 DOCX
<w:t>-only extraction (high)
- H4 EPUB no NCX TOC parsing (high)
- H7 AudioCache no LRU/size cap (medium)
- 0 unit tests, 0 UI tests, no CI (high — testing infrastructure)
- Localization: hardcoded English UI strings (medium)
- Apple Developer Portal: paid team + App Group registration + provisioning profiles (high)
- Accessibility: VoiceOver labels, Dynamic Type respect (high)
Wave proposals (consolidated across domains)
| Wave |
Focus |
Domain rows |
| Wave 4 |
Markdown via swift-markdown SPM + footnote/citation/URL skip |
D1.3.5, D1.5.4, D2.4.4, D2.4.6, D2.4.9 |
| Wave 5 ✓ |
Cloud TTS strict Speechify pattern (ListeningBundler + GaplessAudioPlayer + PreparedBlock/Audio + AudioStreamingStrategy) |
D2.5.4, D2.5.5, D5.5.2, D5.5.4, D5.6.1, D5.6.2, D6.2.1, D6.2.2, D6.2.3 |
| Wave 6 ✓ |
First-byte latency client-side optimization (voice catalog disk cache + engine pre-warm + warmUp + AudioStreamingMode enum) |
D5.1.3 (partial), Wave 6 client-side gains |
| Wave 6.A ✓ |
MiniPlayer state-aware progress UI + state transition fix |
D3.8.4 (improved) |
| Wave 5 remaining |
Sentence highlight toggle, cursor style, tables, true in-chunk seekTo |
D5.7.2, D4.5.5, D4.8.2, D3.2.6, D3.3.9, D6.6.5, D6.8.5 |
| Wave 6 |
Voice catalog (preview + structured prefs + real backend) + inline element tree + long-press + audio quality |
D5.3.5, D5.3.8, D5.3.9, D5.8.4, D3.4.1-3, D3.6.7, D3.6.9, D6.2.x, D4.2.1 |
| Wave 7 |
Sentence alignment + LazyContentProvider + PDF reader (HighlightInteractor protocol) |
D5.2.3, D5.2.7, D5.2.5, D3.6.10, D4.4.1, D4.4.6 |
| Wave 8 |
Annotations + library polish + schema migration |
D7.4.1, D7.5.2, D7.9.1, D7.1.5, D8 (UserHighlight system from D4.6) |
| Wave 9 |
Architectural refactor (ContentBundler stage + DI seam + structured logger) + library search + filter UI |
D1.6.1, D2.2.3, D2.3.1, D15.1.5, D15.4.2, D7.1.4, D7.8.1, D7.3.1, D4.7.2 (D5.6.1 already done in Wave 5) |
| Wave 10 (P0) |
App Store unblock — Critical row 1-7 above |
(see Critical list) |
| Backlog |
Per-doc voice, WPM calibration, sleep timer, full-screen toggle, etc. |
many Low rows |
| Tier 2 |
D8 search match navigation, in-doc search UI, EPUB native reader (if rev'd) |
D3.6.3, D3.8.5, D3.8.6 |
Update protocol
When a wave lands or a gap is discovered:
- Update the relevant row's Status column.
- If status flipped to
DONE, add the wave/commit reference in Notes.
- Re-run severity counts in domain summary table.
- Cross-link from
plans/260429-1908-bookcast-tts-reader/plan.md Implementation Status section.
When adding a new row:
- Always cite recon evidence (
file:line or section).
- Always specify our equivalent path (or explicit
(none)).
- Severity is judged from a Vietnamese-user-shipping-Tier-1 perspective, not Speechify-feature-completeness perspective.