| Metadata | Value |
|---|---|
| Status | Active |
| Version | 1.0.0 |
| Last Updated | 2026-09-10 |
| Author | Claude Opus 4.6 (requested by Seshadri) |
| Document Type | Evidence record |
[!NOTE] Historical evidence: results, counts, commands, and observations below belong to the original work described here. The editorial update date is not a new test or corpus verification. For present behavior, use current feature map.
The Kotlin backend currently contains ~1,200 lines of extraction, parsing, and scraping logic spread across 7 files (WebScrapingService, DeterministicWebScraper, KrithiStructureParser, HtmlTextExtractor, GeminiApiClient, TempleScrapingService, ScrapeJsonSanitizer). The Python service (tools/krithi-extract-enrich-worker/) contains ~800 lines of parallel extraction logic for PDFs.
The two codebases duplicate domain heuristics — section detection regexes, metadata parsing, diacritic normalisation — in different languages, leading to the persistent MADHYAMAKALA bug and other divergences documented in the remediation retrospective.
This document recommends consolidating all extraction (PDF + HTML) into the Python service, leaving the Kotlin backend focused on domain logic, persistence, API routing, and orchestration.
| File | Lines | Responsibility | External Deps |
|---|---|---|---|
WebScrapingService.kt |
~460 | HTML fetch, Jsoup extraction, Gemini LLM prompt construction, response parsing, temple enrichment | Ktor HTTP, Jsoup, GeminiApiClient |
DeterministicWebScraper.kt |
~150 | LLM-free HTML scraping fallback, metadata hints from URL/title patterns | Ktor HTTP, Jsoup |
KrithiStructureParser.kt |
~535 | Section detection (Pallavi/Anupallavi/Charanam + 10 variants), language header detection, lyric variant extraction, metadata hint extraction, boilerplate filtering | Pure Kotlin regex |
HtmlTextExtractor.kt |
~115 | Jsoup DOM traversal, block-element-aware text extraction, link annotation | Jsoup |
GeminiApiClient.kt |
~470 | Gemini API HTTP client with rate limiting, retry, 429/503 handling, schema mode, JSON sanitisation | Ktor HTTP |
TempleScrapingService.kt |
~100 | Temple/kshetra details scraping from nested URLs | Ktor HTTP, Jsoup |
ScrapeJsonSanitizer.kt |
~50 | Gemini response JSON repair (trailing commas, markdown blocks) | Pure Kotlin |
| Total | ~1,880 |
| File | Lines | Responsibility | External Deps |
|---|---|---|---|
extractor.py |
~180 | PyMuPDF text extraction with font metadata, OCR fallback trigger | PyMuPDF, pytesseract |
page_segmenter.py |
~200 | PDF page-level Krithi boundary detection (bold/font-size heuristic) | None (pure Python) |
structure_parser.py |
~215 | Section label detection (same domain patterns as KrithiStructureParser.kt) | None (pure Python) |
metadata_parser.py |
~200 | Raga/tala/composer/deity/temple extraction from header text | diacritic_normalizer |
diacritic_normalizer.py |
~140 | PDF garbled diacritic repair (Rules 1-8) | None (pure Python) |
velthuis_decoder.py |
~290 | Velthuis Devanagari font glyph mapping | None (pure Python) |
worker.py |
~200 | Queue polling, extraction orchestration | psycopg, httpx |
db.py |
~100 | PostgreSQL queue operations | psycopg |
| Total | ~1,525 |
| File | Responsibility |
|---|---|
ExtractionResultProcessor.kt |
Reads completed extraction results, dedup, Krithi creation/matching |
KrithiCreationFromExtractionService.kt |
Creates Krithi records from canonical extractions |
VariantMatchingService.kt |
Cross-language variant matching and confidence scoring |
NameNormalizationService.kt |
Transliteration-aware normalisation for matching keys |
DeduplicationService.kt |
Fuzzy title matching with metadata-aware thresholds |
ImportService.kt |
Import orchestration, approval workflow |
| All DAL repositories | Database persistence |
| All API routes | REST endpoints |
The retrospective identified this as the root cause of the MADHYAMAKALA bug. Here’s the concrete evidence:
Kotlin (KrithiStructureParser.kt:362-461) — 100 regex patterns covering:
pallavi, anupallavi, charanam, chittaswaram, madhyama kala, etc.P, A, C, Chपल्लवि, अनुपल्लवि, चरणम्, समष्टि चरणम्, मध्यम काल साहित्यम्பல்லவி, அனுபல்லவி, சரணம்పల్లవి, అనుపల్లవి, చరణంಪಲ್ಲವಿ, ಅನುಪಲ್ಲವಿ, ಚರಣപല്ലവി, അനുപല്ലവി, ചരണംPython (structure_parser.py:34-93) — 7 pattern groups covering:
pallavi, anupallavi, charanam, caran.amपल्लवि, अनुपल्लवि, चरणम्, समष्टिThe Kotlin version has far more patterns (Indic script abbreviations, 5 Indic scripts). The Python version handles garbled PDF forms (caran.am). Neither is a superset of the other.
Kotlin (KrithiStructureParser.extractMetadataHints()) — Parses blogspot-style meta lines:
abhayAmbA jagadambA - rAgaM kalyANi - tALaM Adi
Python (MetadataParser.parse()) — Parses guruguha.org PDF-style headers:
Rāga: Kalyāṇi — Tāla: Ādi
Also handles garbled Utopia font patterns.
Again, neither is a superset. Both have domain-specific patterns the other lacks.
When the Kotlin MADHYAMAKALA regex was fixed to handle (madhyama kAla sAhityam), the Python structure_parser.py was not updated. When the Python diacritic_normalizer.py was enhanced with Rule 8, the Kotlin NameNormalizationService was not aware. Improvements in one language rotted the other.
┌─────────────────────────┐
│ Kotlin Backend (Ktor) │
│ │
│ - API Routes │
│ - Import Orchestration │
│ - Deduplication │
│ - Variant Matching │
│ - Krithi CRUD │
│ - Persistence (DAL) │
└────────┬────────────────┘
│ writes to extraction_queue
│ reads from extraction_queue (DONE)
▼
┌─────────────────────────┐
│ PostgreSQL │
│ extraction_queue │
└────────┬────────────────┘
│ polls queue
▼
┌─────────────────────────┐
│ Python Extraction Svc │
│ │
│ - PDF extraction │
│ - HTML extraction (NEW) │
│ - Gemini LLM calls (NEW)│
│ - Section detection │
│ - Metadata parsing │
│ - Diacritic normalisation│
│ - Script detection │
│ - Lyric variant extract │
│ - Temple scraping (NEW) │
│ │
│ Output: CanonicalExtractionDto │
└─────────────────────────┘
| Current Kotlin Component | Python Equivalent | Notes |
|---|---|---|
HtmlTextExtractor (Jsoup) |
BeautifulSoup4 or lxml |
Direct replacement — BS4 is the Python standard |
KrithiStructureParser (regex) |
Merge into structure_parser.py |
Unify all 100+ patterns into one file |
WebScrapingService Gemini prompt |
google-generativeai SDK |
Native Python SDK, simpler than Ktor HTTP wrapper |
GeminiApiClient (HTTP + retry) |
google-generativeai SDK |
SDK handles retries, rate limiting natively |
DeterministicWebScraper |
Built into the unified extractor | LLM-free path becomes a mode flag |
TempleScrapingService |
httpx + beautifulsoup4 |
Straightforward port |
ScrapeJsonSanitizer |
Not needed — Python json + SDK handle this |
Gemini Python SDK returns structured objects |
| Component | Reason |
|---|---|
ExtractionResultProcessor |
Orchestration — reads queue results, triggers dedup/creation |
KrithiCreationFromExtractionService |
Domain logic — creates Krithi records with FK resolution |
VariantMatchingService |
Domain logic — cross-language matching with confidence scoring |
NameNormalizationService |
Used by persistence layer for matching keys |
DeduplicationService |
Domain logic — Levenshtein with metadata-aware thresholds |
ImportService |
Orchestration — approval workflow, batch management |
| All DAL repositories | Persistence layer |
| All API routes | REST API surface |
TransliterationCollapse.kt |
Shared domain logic for matching keys |
| Library | Maturity | Fit |
|---|---|---|
| BeautifulSoup4 | 20+ years, universal standard | Excellent — handles malformed HTML (blogspot), CSS selectors, DOM traversal. Direct replacement for Jsoup. |
| lxml | 15+ years, C-based speed | Excellent — 10-50x faster than BS4 for large documents. Can be used as BS4 backend. |
| selectolax | Newer, Modest ecosystem | Good performance but less mature for edge cases. |
Recommendation: beautifulsoup4 with lxml as parser backend. This is the exact equivalent of Jsoup in the Python world, with arguably better handling of malformed HTML.
| Library | Fit |
|---|---|
| httpx | Already in pyproject.toml. Async-capable, HTTP/2 support, excellent timeout handling. Direct replacement for Ktor HTTP client. |
Recommendation: Already using httpx. No change needed.
The current Kotlin implementation is a hand-rolled 470-line HTTP client (GeminiApiClient.kt) that:
Python equivalent: google-generativeai SDK (already in pyproject.toml):
import google.generativeai as genai
model = genai.GenerativeModel("gemini-2.0-flash")
response = model.generate_content(
prompt,
generation_config=genai.GenerationConfig(
response_mime_type="application/json",
response_schema=schema_dict,
),
)
result = response.text # Already parsed, no sanitisation needed
Advantages:
| Library | Already Used? | Capability |
|---|---|---|
indic-transliteration |
Yes (in Python) | Script conversion between Devanagari, Tamil, Telugu, Kannada, Malayalam, IAST, Harvard-Kyoto, SLP1 |
regex (PyPI) |
No (uses stdlib re) |
Unicode-aware regex with \p{Devanagari} property support — better than stdlib re for Indic script detection |
unicodedata (stdlib) |
Available | NFD/NFC normalisation — same as java.text.Normalizer |
IndicNLP |
Not yet | Sentence tokenisation, script detection — useful but not critical |
Key point: The indic-transliteration library is Python-native with no JVM equivalent. Currently the Kotlin backend doesn’t do script conversion at all — it relies on the Python service for that. Moving HTML extraction to Python means ALL transliteration can use this library directly.
Python has stronger text processing primitives for this workload:
| Capability | Python | Kotlin/JVM |
|---|---|---|
| Unicode normalisation | unicodedata.normalize('NFD', ...) |
java.text.Normalizer.normalize(NFD) |
| Regex Unicode properties | \p{Devanagari} via regex library |
\p{IsDevanagari} — works but verbose |
| String manipulation | First-class, concise | Verbose, Java-style |
| Script detection | unicodedata.name(), regex \p{Script=...} |
Manual Unicode range checks |
| Diacritic stripping | One-liner with NFD + category filter | Same, but more verbose |
| Pattern matching | Structural match statements (3.10+) | when expressions — comparable |
For text-heavy, regex-heavy, Unicode-heavy workloads like music lyric extraction, Python is the more natural fit.
New file: tools/krithi-extract-enrich-worker/src/html_extractor.py
Port HtmlTextExtractor.kt logic to Python using BeautifulSoup4:
div.post-body > div.post > article > div.post-content > body<a> link annotationTest: Extract text from 5 blogspot URLs, compare output with current Kotlin extraction.
Merge into: tools/krithi-extract-enrich-worker/src/structure_parser.py
Consolidate all 100+ section detection patterns from KrithiStructureParser.kt into the Python StructureParser:
P, A, C, Ch)DEVANAGARI, TAMIL, etc.)Shared pattern config (optional but recommended): Extract section patterns to a YAML file that both languages can reference during the transition period.
Test: Run against 20 blogspot pages + 20 PDF pages, verify section detection matches or exceeds current Kotlin output.
New file: tools/krithi-extract-enrich-worker/src/llm_extractor.py
Replace the 470-line GeminiApiClient.kt + prompt construction with:
google-generativeai SDK for API callsTest: Extract metadata from 10 blogspot URLs using both Kotlin and Python paths, compare results.
Modify: tools/krithi-extract-enrich-worker/src/worker.py
Extend the existing queue polling worker to handle source_format = 'HTML' in addition to 'PDF':
Both paths output the same CanonicalExtractionDto — the Kotlin consumer doesn’t need to change.
Remove from Kotlin:
WebScrapingService.kt (460 lines)DeterministicWebScraper.kt (150 lines)KrithiStructureParser.kt (535 lines)HtmlTextExtractor.kt (115 lines)GeminiApiClient.kt (470 lines)TempleScrapingService.kt (100 lines)ScrapeJsonSanitizer.kt (50 lines)ScrapeCache.kt (~80 lines)build.gradle.ktsTotal Kotlin reduction: ~1,960 lines removed
Update the import/scraping routes to submit extraction requests to the queue instead of calling WebScrapingService directly.
compose.yaml to add volume mount for tools/krithi-extract-enrich-worker/src (development mode)httpx and beautifulsoup4[lxml] are in pyproject.toml (httpx already is)Dockerfile if new system dependencies are needed (none expected)| Risk | Mitigation |
|---|---|
| BS4 can’t handle blogspot HTML | BS4 + lxml handles malformed HTML better than Jsoup |
| Python regex can’t handle Indic scripts | Python regex library has full Unicode property support |
| Gemini SDK missing features | SDK is maintained by Google, covers all current features |
| Performance for HTML extraction | HTML extraction is I/O-bound (network fetch), not CPU-bound. Python is fine. |
| Risk | Mitigation |
|---|---|
| Queue latency for HTML extraction | Currently HTML scraping is synchronous in the Kotlin request path. Moving to queue-based means the caller gets a job ID, not immediate results. May need to adjust import orchestration to poll for completion. |
| Two-service deployment complexity | Already running both services. No new operational burden. |
Losing the in-memory ScrapeCache |
Implement Redis or DB-based caching in Python, or accept cache-miss cost (HTTP fetch is ~1s, Gemini is ~5-10s). |
| Risk | Mitigation |
|---|---|
| Breaking existing bulk import flow | The bulk import currently calls IWebScraper.scrapeKrithi() synchronously during import. Moving this to queue-based means rearchitecting the import flow to: (1) submit URLs to extraction queue, (2) wait for results, (3) then process. This is the biggest change. |
Mitigation for the bulk import flow: The import service can be modified to:
ExtractionResultProcessor pattern)This actually simplifies the import flow — instead of two code paths (bulk import vs extraction pipeline), there’s one: everything goes through the extraction queue.
indic-transliteration — Script conversion available for HTML extraction, not just PDFs.CanonicalExtractionDto. No more ScrapedKrithiMetadata vs CanonicalExtractionDto duality.The following should remain in Kotlin:
NameNormalizationService — Used at persistence time for matching keys. Moving it would require cross-service calls for every Krithi creation.TransliterationCollapse — Shared domain module used by DAL. Keep in Kotlin.DeduplicationService — Needs database access for findDuplicateCandidates. Keep in Kotlin.ExtractionResultProcessor — Orchestration that reads queue results and writes to domain tables. Keep in Kotlin.VariantMatchingService — Domain logic that needs DB access for Krithi lookup. Keep in Kotlin.The principle: extraction (raw content -> structured DTO) moves to Python. Resolution (structured DTO -> domain entities in the database) stays in Kotlin.
| Phase | Effort | Dependencies |
|---|---|---|
| Phase 1: HTML fetch + text extraction | 1-2 days | None |
| Phase 2: Unified structure parser | 2-3 days | Phase 1 |
| Phase 3: Gemini LLM integration | 1-2 days | Phase 1 |
| Phase 4: Worker queue extension | 1 day | Phases 1-3 |
| Phase 5: Kotlin cleanup | 1 day | Phase 4 validated |
| Phase 6: Docker + deployment | 0.5 day | Phase 5 |
| Total | 6.5-9.5 days |
With the retrospective’s recommended approach of vertical slices and validation at each step, the realistic timeline is ~2 weeks including testing and edge case fixes.
Proceed with consolidation. The duplicated heuristics problem is a structural flaw that will keep causing bugs as the extraction logic evolves. Python is the stronger platform for text extraction, Unicode processing, and LLM integration. The CanonicalExtractionDto contract already exists as the bridge between the two services.
Start with Phase 1 (HTML text extraction) as a vertical slice: extract text from one blogspot URL in Python, produce a CanonicalExtractionDto, have Kotlin consume it from the queue. Validate end-to-end before proceeding to Phase 2.
Apply the retrospective’s key lesson: one working slice first, then expand.
Analysis conducted 2026-02-12 by Claude Opus 4.6.