- Python 74.8%
- JavaScript 15.9%
- HTML 6.3%
- Shell 3%
The unit pointed at `make render`; this repo has no Makefile and never had one. The build was documented as taking minutes and needing 0.5 GB — measured on the VPS it is under 20 s (extract 6.8 s, parse 2.3 s, audit 0.3 s, render 4.1 s, precompress 4.0 s) and writes 250 MB (site 229 MB, database 17 MB, work/ 4.4 MB, beside the 4.3 MB PDF). ADR 0002 contradicted itself. The 2026-10-16 amendment took binding out of the decision; the Consequences list still asserted that loopback-only binding was part of it. Restated as what the amendment actually requires: port 18188 stays off the internet, and production meets that by passing --host 127.0.0.1 rather than by relying on serve.py's default. Step 5 gains the check that cost 20 restarts here. A hand-run serve.py holding 18188 makes the unit start, fail to bind, and be retried (Restart=on-failure, RestartSec=3) — while the site keeps answering, from the wrong process. The install looks like it worked and NRestarts is the only thing that says otherwise. Troubleshooting gains the two silent failures this deployment produced: a build that is green at every stage over the one-page extract (the database guard passes, entries.json is [], and only step 2's checksum catches it), and a unit that starts while something else serves. The third pitfall records the rule they both come from — one artefact, one manager — and the three things that make this service hold to it. |
||
|---|---|---|
| deploy | ||
| docs | ||
| tests | ||
| tools | ||
| .gitignore | ||
| .python-version | ||
| README.md | ||
Diccionario Español de Sinónimos y Antónimos — search interface
A searchable web interface over the 568-page Diccionario Español de Sinónimos y Antónimos (Federico Carlos Saínz de Robles, Cuban edition, 2012/2014), hosted on a small Debian VPS.
Words in the interface are clickable. A word is a link only if this dictionary has an article for it; the RAE's DLE is a separate, secondary control beside it.
Status: the site is built; the deploy is ready. The extraction and parsing pipeline is complete and validated end-to-end over all 568 pages, producing 24,092 entries and 224,089 relations, and the interface is pre-rendered and searchable. What remains is the manual step on the VPS. See docs/roadmap.md and docs/deploy.md.
Serves on the VPS as sinonimos.ijuanes.ovh, a subdomain proxied by the
existing Caddy, with a static file server on port 18188. In production the
service binds loopback only, because Caddy is its only client; locally it
binds all interfaces by default so a phone on the same network can test it.
Source
| File | Diccionario-Español-de-Sinónimos-y-Antónimos.pdf |
| Pages | 568 (dictionary body: pp. 11–565) |
| Origin | Adobe InDesign CS4 → Acrobat Distiller 9 (2014) |
| SHA-256 | a549f48fdfd1c34f4d67b51e007a0c37dcb4ac71b09f9ef13231b2b8297d7a19 |
The PDF is not committed — it lives outside this repo and is referenced by path. Only its checksum is recorded, in docs/source.md.
Quick start
# 1. extract (PDF -> one text file per page, reading order restored)
python3 tools/extract.py --pdf /path/to/book.pdf --out work/pages
# 2. parse (pages -> SQLite)
python3 tools/parse.py --pages work/pages --db data/dictionary.db
# 3. audit the result
python3 tools/verify.py --db data/dictionary.db --pages work/pages
# 4. serve the built site. Binds 0.0.0.0 by default so a phone on the LAN can
# load it; --precompress writes the .gz siblings it will then send instead
# of the raw bytes. (Production writes those at build time, not at startup
# — the service is sandboxed read-only. See docs/deploy.md.)
python3 tools/serve.py --root site --port 18188 --precompress
All tools are dependency-free (standard library + poppler-utils). The Mistral
OCR path is not used — see ADR 0001.
Tests
python3 tests/run.py # 152 tests, stdlib unittest
node tests/search_harness.mjs # 78 search checks, running the real search.js
python3 tests/mutants.py # revert each fix; confirm the suite catches it
Seven pipeline bugs were found during development and none of them raised an
exception — every one produced plausible, wrong text. tests/mutants.py
exists because a test that cannot fail proves nothing: it reverts each fix in
turn and asserts the suite goes red. Currently 15/15 mutations caught.
Each test maps to a documented bug in docs/pipeline.md, including two that were found by writing the tests: construction synonyms leaking into the headword, and headwords with parenthetical qualifiers being swallowed into the previous article.
Layout
tools/extract.py PDF -> work/pages/NNNN.txt (reading order, dehyphenated)
tools/parse.py pages -> data/dictionary.db (SQLite)
tools/verify.py audit invariants on the result
tools/probe_static.py re-measures the static-site figures below
tools/serve.py serves the built site on 18188, behind Caddy
tests/ 152 tests + a search harness + a mutation check
docs/ design, ADRs, roadmap, deploy, notation reference
deploy/ systemd unit, Caddy block, preflight script
site/ generated static site (gitignored)
data/ generated database (gitignored)
work/ intermediate page text (gitignored)
What is extracted
| Metric | Value |
|---|---|
| Entries (articles) | 24,092 |
| Synonym relations | 210,412 |
| Antonym relations | 7,316 |
Cross-references (V.) |
6,361 |
Construction synonyms (||) |
1,264 rows / 1,034 locutions |
| Distinct related terms | ~54,800 |
Terms flagged as loanwords ((v.)) |
14,032 |
| Entries with more than one sense | 7,470 (max 23) |
| Mean synonyms per entry | 8.7 |
The (v.) flag marks cursiva in the original edition — terms that are
loanwords, and which the dictionary deliberately marks so they are not
confused with native equivalents. The web interface should render these
distinctly.
Documentation
| Document | Contents |
|---|---|
| docs/notation.md | The dictionary's markup language, and how the parser decodes it |
| docs/pipeline.md | Extraction and parsing, and why each step exists |
| docs/data-model.md | SQLite schema and rationale |
| docs/frontend-backend.md | Where the line sits, and the measurements behind it |
| docs/architecture.md | Intended system design for the VPS |
| docs/roadmap.md | Stages, and what is deliberately deferred |
| docs/decisions.md | Settled design decisions, and why |
| docs/open-questions.md | What is still undecided, incl. the RAE-link UX |
| docs/deploy.md | Installing it on the VPS: the artefact, the unit, the Caddy block |
| docs/source.md | Provenance and checksums |
| docs/adr/ | Architecture decision records |
How it works
Measured, not assumed (docs/architecture.md):
| 24,092 articles rendered | 2.5–2.9 s |
| Whole site | 90.3 MB raw / 27.4 MB gzipped |
| Search index | 0.76 MB gzipped |
| …compared to the database | 17 MB |
The dictionary pre-renders to static files. On the VPS, Python is a
build tool and the only running process is a static file server behind the
existing Caddy reverse proxy — the same subdomain + automatic-certificate +
reverse_proxy shape as every other service. The only JavaScript is search.
Re-check with python3 tools/probe_static.py --db data/dictionary.db. The full
deploy procedure is in docs/deploy.md.
Settled
Recorded in full in docs/decisions.md.
- Backend: Python, build-time only. Go/Rust are unnecessary — there is nothing left running to be fast in.
- No API. HTML is the only contract between build and browser.
- Pre-render everything. Smaller than the database it comes from.
- Vanilla JS, no framework, no build step. A classic
deferscript, not an ES module: a module fails silently, while thedata-statehandshake makes adeferscript's failure visible (ADR 0003, amended). htmx is a category error without a server; Alpine quadruples a 5 KB JS budget. Plain JS with JSDoc types rather than TypeScript, because TS forces a build step and loses immutable asset caching. Every page stays readable with JavaScript off. - Mobile-first, one codebase. One set of pages, one stylesheet, two layouts. Not two frontends.
- SQLite — 17 MB, 2 cores, 4 GB. Anything heavier is overkill.
- Sense grouping — synonyms grouped by numbered sense, not listed flat.
- Fuzzy search — quality of life only, limited to unambiguous tolerance
(diacritics,
ñ/n). - Loanwords — the
(v.)flag renders as a badge. - Desktop clients — an idea only.
- Other dictionaries — someday, off the roadmap.
Word clicks are settled — a local article when one exists, with the RAE as a
small secondary control (↗, modifier-click) — as is homograph handling. See
decisions.md and the
remaining open questions.
Licence and attribution
The source text is © Editorial José Martí / Instituto Cubano del Libro, based on Federico Carlos Saínz de Robles (1968). This repository contains tooling and a derived database for personal use; the book itself is not redistributed here.