working version of bulk importer

This commit is contained in:
stephen 2026-06-13 17:02:06 +10:00
parent 9392d8d9bc
commit 18e46e7cec
4 changed files with 129 additions and 73 deletions

View file

@ -5,30 +5,65 @@ from docling.document_converter import DocumentConverter
class BulkImporter: class BulkImporter:
def __init__(self): def __init__(self):
# Converter engine for handling multi-column documents
self.converter = DocumentConverter() self.converter = DocumentConverter()
# Matches textbook unit codes (e.g., U5_3D, U7 4A, UG_11B)
self.unit_regex = re.compile(r'U([0-9]+)') self.unit_regex = re.compile(r'U([0-9]+)')
def clean_text(self, text: str) -> str: def clean_field(self, text: str) -> str:
"""Cleans syntax breaks, structural commas, and quotes from strings.""" """Removes layout spacing, structural artifacts, and noise."""
if not text: if not text:
return "" return ""
return text.replace('\n', ' ').replace('"', '').replace("'", "").strip() # Remove markdown bold/italics markers if any, and compress spaces
cleaned = text.replace('*', '').replace('_', '').replace('\\', '')
cleaned = re.sub(r'\s+', ' ', cleaned)
return cleaned.strip()
def parse_unit(self, context_str: str) -> int: def parse_unit(self, context_str: str) -> int:
"""Extracts the exact Unit integer from codes like U5_3D or U2 1A.""" """Extracts the integer unit ID out of strings like 'U3_8B' or 'U7'."""
match = self.unit_regex.search(context_str) match = self.unit_regex.search(context_str)
if match: if match:
return int(match.group(1)) return int(match.group(1))
return None # General reference markers like 'UT LEX' or 'UG' return None
def insert_pair(self, cursor, es_text, en_text, context_tag, textbook_name):
"""Helper to cleanly insert a single Spanish-English translation pair."""
es_clean = self.clean_field(es_text)
en_clean = self.clean_field(en_text)
tag_clean = self.clean_field(context_tag)
# Guard clause against column headers or empty layout cells
if not es_clean or not en_clean:
return False
if es_clean.lower() in ["spanish", "word", "alphabetical glossary", "es", "en"] or len(es_clean) <= 1:
return False
unit_number = self.parse_unit(tag_clean)
try:
# 1. Insert Spanish Phrase
cursor.execute("""
INSERT INTO phrases (text, language, textbook, unit, source_context)
VALUES (?, 'es', ?, ?, ?)
""", (es_clean, textbook_name, unit_number, tag_clean))
es_id = cursor.lastrowid
# 2. Insert English Translation
cursor.execute("""
INSERT INTO phrases (text, language, textbook, unit, source_context)
VALUES (?, 'en', ?, ?, ?)
""", (en_clean, textbook_name, unit_number, tag_clean))
en_id = cursor.lastrowid
# 3. Create Bidirectional Cross-Reference Entries
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (es_id, en_id))
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (en_id, es_id))
return True
except Exception:
return False
def import_pdf_glossary(self, file_path: str, textbook_name: str): def import_pdf_glossary(self, file_path: str, textbook_name: str):
"""Converts multi-column PDF via Docling and maps items to SQLite."""
print(f"🔄 Docling is analyzing structural layout for '{file_path}'...") print(f"🔄 Docling is analyzing structural layout for '{file_path}'...")
try: try:
# Render layout-aware structural conversion
result = self.converter.convert(file_path) result = self.converter.convert(file_path)
document_text = result.document.export_to_markdown() document_text = result.document.export_to_markdown()
except Exception as e: except Exception as e:
@ -38,52 +73,49 @@ class BulkImporter:
conn = get_connection() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
print("📥 Parsing text structures and writing to database...") print("📥 Parsing 6-column text grid structure and writing to database...")
count = 0 count = 0
# Regex targeted to catch both standard blocks and tabular lines safely # Process the markdown line-by-line
# Matches patterns like: "Spanish Word", "English Translation", "Unit Code" lines = document_text.split('\n')
pattern = re.compile(r'([^,\n"\[]+?)\s*,\s*([^,\n"\[]+?)\s*,\s*(U[G|T|0-9][^\n,]+)') for line in lines:
matches = pattern.findall(document_text) line_str = line.strip()
try: # Target lines containing table formatting row data
for es_raw, en_raw, ctx_raw in matches: if not line_str.startswith('|') or not line_str.endswith('|'):
spanish_text = self.clean_text(es_raw) continue
english_text = self.clean_text(en_raw)
context_tag = self.clean_text(ctx_raw)
unit_number = self.parse_unit(context_tag)
# Avoid table headers or non-lexical entries # Split by markdown table pipes
if spanish_text.lower() in ["spanish", "word", "alphabetical glossary"] or len(spanish_text) <= 1: # e.g., "| años60 m | 1960s | U8_7A | bañ osmpl | bath | U1_1A |"
continue parts = [p.strip() for p in line_str.split('|')]
# 1. Insert Spanish Term Entry # A valid row split will include empty items at the ends due to leading/trailing pipes
cursor.execute(""" # For a 6-column table, len(parts) should be at least 8 elements
INSERT INTO phrases (text, language, textbook, unit, source_context) if len(parts) < 7:
VALUES (?, 'es', ?, ?, ?) continue
""", (spanish_text, textbook_name, unit_number, context_tag))
es_id = cursor.lastrowid
# 2. Insert English Translation Entry # Skip Markdown table header separator rows: |---|---|...
cursor.execute(""" if '---' in parts[1]:
INSERT INTO phrases (text, language, textbook, unit, source_context) continue
VALUES (?, 'en', ?, ?, ?)
""", (english_text, textbook_name, unit_number, context_tag))
en_id = cursor.lastrowid
# 3. Create Bidirectional Cross-Reference Records # Extract Left-Hand Column Group (Columns 1, 2, 3)
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (es_id, en_id)) es_left = parts[1]
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (en_id, es_id)) en_left = parts[2]
unit_left = parts[3] if len(parts) >= 4 else ""
if self.insert_pair(cursor, es_left, en_left, unit_left, textbook_name):
count += 1 count += 1
conn.commit() # Extract Right-Hand Column Group (Columns 4, 5, 6)
print(f"🎉 Bulk ingestion complete! Registered {count} matched glossary items.") if len(parts) >= 7:
return count es_right = parts[4]
en_right = parts[5]
unit_right = parts[6]
except Exception as e: if self.insert_pair(cursor, es_right, en_right, unit_right, textbook_name):
conn.rollback() count += 1
print(f"❌ Error during database transaction: {e}")
return 0 conn.commit()
finally: conn.close()
conn.close() print(f"🎉 Bulk ingestion complete! Registered {count} matched glossary items.")
return count

View file

@ -27,6 +27,9 @@
- [14.1. How edge-tts Works (The Cloud Pipeline)](#141-how-edge-tts-works-the-cloud-pipeline) - [14.1. How edge-tts Works (The Cloud Pipeline)](#141-how-edge-tts-works-the-cloud-pipeline)
- [14.2. Why This is Highly Advantageous for Phase 1](#142-why-this-is-highly-advantageous-for-phase-1) - [14.2. Why This is Highly Advantageous for Phase 1](#142-why-this-is-highly-advantageous-for-phase-1)
- [14.3. Future Proofing: Going Fully Offline Down the Track](#143-future-proofing-going-fully-offline-down-the-track) - [14.3. Future Proofing: Going Fully Offline Down the Track](#143-future-proofing-going-fully-offline-down-the-track)
- [Steps of app creation](#steps-of-app-creation)
- [Gemini Reposne](#gemini-reposne)
- [So we fix the bulk upload file](#so-we-fix-the-bulk-upload-file)
# 1. spanish-voice-trainer # 1. spanish-voice-trainer
# 2. Project Summary: # 2. Project Summary:
@ -380,3 +383,31 @@ Kokoro: A highly lightweight, open-source local voice model that produces incred
For now, leveraging Microsoft's cloud via edge-tts gives you native-speaker accuracy with zero setup friction, letting you focus entirely on mastering your Aula Internacional curriculum! For now, leveraging Microsoft's cloud via edge-tts gives you native-speaker accuracy with zero setup friction, letting you focus entirely on mastering your Aula Internacional curriculum!
I suppose this OK in phase 1 , I may want to migrate Kokoro next I suppose this OK in phase 1 , I may want to migrate Kokoro next
# Steps of app creation
```zsh
(139_spanish-voice-trainer) stephenlohning@Scotty 139_spanish-voice-trainer % git tag -n
list basic framework created
v1.0 Tested text speak with 2 files added to the root directory
v2.0 added db creation, to project, also loaded installed Bee Keeper Studio on Scotty
v3.0 phase manager added main modified to test this, bulk_importer drafted not tested
v4.0 bulk importer did not work using docling, so we going to export the markdown into a text file as an intermediate step
v5.0 modified to export the markdown file, to be used to modify bulk importer
```
v5.0 docling gave us
```txt
## alphabetical GLOSSARY
| años60 m | 1960s | U8_7A | bañ osmpl | bath | U1_1A |
|---------------------|------------------|-------------|-----------------------------|-------------------------|-------------|
| aparecer (zc) | toappear | U3_6A | bar m | bar | U1_LEX |
| apasionado/a | passionate | U5_15C | barato/a | cheap | U4_6C |
| apellido m | surname | U1_4A | barba f | beard | U5_9A |
| aprender | tolearn | U2_2C | Barcelona | Barcelona | U3_8A |
```
# Gemini Reposne
```zsh
| Col 1 (ES) | Col 2 (EN) | Col 3 (Unit) | Col 4 (ES) | Col 5 (EN) | Col 6 (Unit) |
```
# So we fix the bulk upload file

Binary file not shown.

31
main.py
View file

@ -1,27 +1,20 @@
# main.py # main.py
import asyncio import asyncio
from docling.document_converter import DocumentConverter from database.connection import init_db
from core.bulk_importer import BulkImporter
async def dump_docling_output(): async def main():
file_path = "aula_int_plus_1_glos_en_alfa.pdf" # Verify tables exist or run schemas
output_path = "docling_output.md" init_db()
print(f"🔄 Docling is analyzing structural layout for '{file_path}'...") # Initialize the importer pipeline
importer = BulkImporter()
try: # Execute structural parse and database write operations
converter = DocumentConverter() pdf_file = "aula_int_plus_1_glos_en_alfa.pdf"
result = converter.convert(file_path) textbook = "Aula Internacional Plus 1"
markdown_text = result.document.export_to_markdown()
# Save the exact output to a file in your project root importer.import_pdf_glossary(pdf_file, textbook)
with open(output_path, "w", encoding="utf-8") as f:
f.write(markdown_text)
print(f"📝 Success! Raw Docling layout exported cleanly to: {output_path}")
print("👀 Open this file in VS Code to see exactly how the text flows.")
except Exception as e:
print(f"❌ Docling processing failed: {e}")
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(dump_docling_output()) asyncio.run(main())