Upload / ingestion (multi-format parse → contentHash)
Parent: key-designs
core/src/upload/. Ingest is 100% client-side — the server stores the parsed result, never parses. parseFile dispatches by extension to parseEpub / parsePdf / parseText (+ Gutenberg import, epubMetadata, epubToc).
- EPUB (
parseEpub.ts): JSZip opens the archive →container.xml→ OPF spine → each spine item HTML-stripped and joined with\n\n; rejects scanned/thin books (<200 chars). TOC from NCX first, EPUB3 nav second (epubToc.ts), each entry mapped to a cumulative char offset viacomputeChapterOffsets(so chapter breaks are content-positions, feeding dynamic-pagination). - PDF / OCR: text PDFs parse client-side; scanned PDFs route to the Python
nlp-api/parse-pdf+/ocr-images(Tesseract) — the one server-side ingestion path. - Language detect + ISBN/metadata provenance (
isbnSource= epub_meta / manual / fuzzy_match).
contentHash (MD5) is the keystone. upload/contentHash.ts MD5s the full text — deliberately MD5, to stay relink-compatible with legacy rows. It is the join key everything anchors to: vocab, notes, the sentence cache, the search index, and the TTS audiobook are all keyed by content hash, so re-uploading the same book relinks existing vocabulary/notes/highlights instead of orphaning them. Upload posts structured JSON (POST /api/books {action:"create"}) — text + language + contentHash + TOC + base64 cover — and returns a bookId.
Why it's a pillar: multi-format parsing is real engineering, and contentHash is the identity on which the whole content-anchored design (raw-display-cursor) depends — get the hash wrong and every downstream anchor detaches.