2026-09-23·by Sijie Wang#lucerna#software#architecture

upload-ingestion

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 via computeChapterOffsets (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.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →