phrase manager added
This commit is contained in:
parent
bb6dcb7630
commit
31de93dad3
6 changed files with 220 additions and 9 deletions
BIN
aula_int_plus_1_glos_en_alfa.pdf
Normal file
BIN
aula_int_plus_1_glos_en_alfa.pdf
Normal file
Binary file not shown.
78
core/bulk_importer.py
Normal file
78
core/bulk_importer.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# core/bulk_importer.py
|
||||||
|
import re
|
||||||
|
from database.connection import get_connection
|
||||||
|
|
||||||
|
class BulkImporter:
|
||||||
|
def __init__(self):
|
||||||
|
# Matches typical patterns: "Spanish word", "English translation", "Unit metadata"
|
||||||
|
self.row_regex = re.compile(r'"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"')
|
||||||
|
|
||||||
|
def clean_text(self, text: str) -> str:
|
||||||
|
"""Strips newlines and extra spaces from extracted data fields."""
|
||||||
|
return text.replace('\n', ' ').strip()
|
||||||
|
|
||||||
|
def parse_unit(self, context_str: str) -> int:
|
||||||
|
"""
|
||||||
|
Extracts the unit integer from codes like 'U5_3D', 'U7 4A', or 'UG_11B'.
|
||||||
|
Returns None if it's a general marker like 'UT LEX'.
|
||||||
|
"""
|
||||||
|
match = re.search(r'U([0-9])', context_str)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def import_glossary_file(self, file_path: str, textbook_name: str):
|
||||||
|
"""Reads the structural text lines and inserts them into the translation database."""
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print(f"📖 Starting bulk ingestion for '{file_path}'...")
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find all matching row structures inside the text
|
||||||
|
matches = self.row_regex.findall(content)
|
||||||
|
|
||||||
|
for es_raw, en_raw, ctx_raw in matches:
|
||||||
|
spanish_text = self.clean_text(es_raw)
|
||||||
|
english_text = self.clean_text(en_raw)
|
||||||
|
context_tag = self.clean_text(ctx_raw)
|
||||||
|
unit_number = self.parse_unit(context_tag)
|
||||||
|
|
||||||
|
# Skip header rows or structural markers
|
||||||
|
if spanish_text.lower() in ["alphabetical glossary", "spanish", "word"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 1. Insert Spanish Term
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO phrases (text, language, textbook, unit, source_context)
|
||||||
|
VALUES (?, 'es', ?, ?, ?)
|
||||||
|
""", (spanish_text, textbook_name, unit_number, context_tag))
|
||||||
|
es_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# 2. Insert English Term
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO phrases (text, language, textbook, unit, source_context)
|
||||||
|
VALUES (?, 'en', ?, ?, ?)
|
||||||
|
""", (english_text, textbook_name, unit_number, context_tag))
|
||||||
|
en_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# 3. Create Bidirectional Bridges
|
||||||
|
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))
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f"🎉 Successfully imported {count} linked glossary pairs into SQLite!")
|
||||||
|
return count
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
print(f"❌ Error during bulk data ingestion: {e}")
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
71
core/phrase_manager.py
Normal file
71
core/phrase_manager.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
from database.connection import get_connection
|
||||||
|
from core.asset_generator import AssetGenerator
|
||||||
|
|
||||||
|
class PhraseManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.asset_gen = AssetGenerator()
|
||||||
|
|
||||||
|
def _clean_filename(self, text: str) -> str:
|
||||||
|
"""Filters forbidden system characters but preserves Spanish diacritics."""
|
||||||
|
safe_text = re.sub(r'[/\\?%*:|"<>]', '', text)
|
||||||
|
return safe_text.strip().replace(" ", "_")
|
||||||
|
|
||||||
|
async def add_translation_pair(self, spanish_text: str, english_text: str,
|
||||||
|
textbook: str = None, unit: int = None,
|
||||||
|
context: str = None, voice_gender: str = "male"):
|
||||||
|
"""
|
||||||
|
Takes a multi-lingual pair, automates audio generation,
|
||||||
|
and securely links them together in the SQLite database.
|
||||||
|
"""
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Insert Spanish Phrase Row
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO phrases (text, language, textbook, unit, source_context)
|
||||||
|
VALUES (?, 'es', ?, ?, ?)
|
||||||
|
""", (spanish_text, textbook, unit, context))
|
||||||
|
es_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# 2. Insert English Phrase Row
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO phrases (text, language, textbook, unit, source_context)
|
||||||
|
VALUES (?, 'en', ?, ?, ?)
|
||||||
|
""", (english_text, textbook, unit, context))
|
||||||
|
en_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# 3. Create Bidirectional Translation Bridges
|
||||||
|
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))
|
||||||
|
|
||||||
|
# 4. Automate Castilian Audio Generation
|
||||||
|
safe_filename = self._clean_filename(spanish_text)
|
||||||
|
audio_filename = f"{safe_filename}_{voice_gender}.mp3"
|
||||||
|
output_path = os.path.join("media", audio_filename)
|
||||||
|
|
||||||
|
# Fire our cloud TTS utility
|
||||||
|
await self.asset_gen.generate_speech(spanish_text, output_path, gender=voice_gender)
|
||||||
|
selected_voice_name = self.asset_gen.voices.get(voice_gender.lower(), "es-ES-AlvaroNeural")
|
||||||
|
|
||||||
|
# 5. Log Audio Track to Database
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO audio_tracks (phrase_id, voice_gender, voice_name, file_path, is_reference)
|
||||||
|
VALUES (?, ?, ?, ?, 1)
|
||||||
|
""", (es_id, voice_gender.lower(), selected_voice_name, output_path))
|
||||||
|
|
||||||
|
# Commit everything at once securely
|
||||||
|
conn.commit()
|
||||||
|
print(f"✨ Successfully integrated: '{spanish_text}' ⇄ '{english_text}'")
|
||||||
|
print(f" Audio generated safely at: {output_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
print(f"❌ Error during phrase ingestion transaction: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
49
doc/Notes.md
49
doc/Notes.md
|
|
@ -23,6 +23,10 @@
|
||||||
- [12.2. Cask Extension (--cask)](#122-cask-extension---cask)
|
- [12.2. Cask Extension (--cask)](#122-cask-extension---cask)
|
||||||
- [12.3. Why This Is Useful](#123-why-this-is-useful)
|
- [12.3. Why This Is Useful](#123-why-this-is-useful)
|
||||||
- [13. Beekeeper Studio](#13-beekeeper-studio)
|
- [13. Beekeeper Studio](#13-beekeeper-studio)
|
||||||
|
- [14. Does edge-tts always connect to the Microsoft Cloud ?](#14-does-edge-tts-always-connect-to-the-microsoft-cloud-)
|
||||||
|
- [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.3. Future Proofing: Going Fully Offline Down the Track](#143-future-proofing-going-fully-offline-down-the-track)
|
||||||
|
|
||||||
# 1. spanish-voice-trainer
|
# 1. spanish-voice-trainer
|
||||||
# 2. Project Summary:
|
# 2. Project Summary:
|
||||||
|
|
@ -332,4 +336,47 @@ Furthermore, whenever you run your system updates down the road using brew upgra
|
||||||
Has an introduction YouTube video
|
Has an introduction YouTube video
|
||||||
|
|
||||||
The main thing is you double click apon the data base.db we created.
|
The main thing is you double click apon the data base.db we created.
|
||||||

|

|
||||||
|
|
||||||
|
# 14. Does edge-tts always connect to the Microsoft Cloud ?
|
||||||
|
|
||||||
|
The short answer is yes.
|
||||||
|
|
||||||
|
edge-tts is a Python library that wraps around the communication protocol used by Microsoft Edge's built-in "Read Aloud" feature. Because it doesn't run neural voice models locally on your MacBook, it must make an active network connection to the Microsoft cloud servers every time you request audio synthesis.
|
||||||
|
|
||||||
|
Here is a closer look at what is happening under the hood when you call await communicate.save() on Scotty:
|
||||||
|
|
||||||
|
## 14.1. How edge-tts Works (The Cloud Pipeline)
|
||||||
|
Instead of keeping massive gigabyte-sized neural networks running in your local system memory, edge-tts leverages Microsoft’s cloud infrastructure using secure WebSockets.
|
||||||
|
|
||||||
|
The Request: Your Python script establishes an asymmetrical WebSocket connection to a Microsoft Azure speech endpoint.
|
||||||
|
|
||||||
|
The Handshake: It passes your text string along with configuration meta-data (like requesting the specific es-ES-AlvaroNeural Castilian voice).
|
||||||
|
|
||||||
|
The Synthesis: Microsoft's remote cloud supercomputers synthesize the text into human-like phonemes in real time.
|
||||||
|
|
||||||
|
The Stream: The server streams raw audio binary packets back down to edge-tts, which chunks them cleanly together and writes them to disk as your target .mp3 file.
|
||||||
|
|
||||||
|
Because this relies entirely on a remote API endpoint, if Scotty is completely offline or disconnected from Wi-Fi, the edge-tts script will throw a connection error and fail to generate audio.
|
||||||
|
|
||||||
|
## 14.2. Why This is Highly Advantageous for Phase 1
|
||||||
|
While running things offline is fantastic for complete privacy, using a cloud-backed pipeline for your audio assets right now offers three massive benefits for your project development:
|
||||||
|
|
||||||
|
Zero Hardware Strain: Deep-learning voice synthesis is computationally heavy. Running a true high-fidelity audio model locally requires significant CPU/GPU processing. edge-tts offloads 100% of that heavy lifting to the cloud, meaning your script runs instantly in seconds without draining your MacBook's battery or spinning up the fans.
|
||||||
|
|
||||||
|
Instant, Production-Ready Audio: You don't have to download, compile, or tune massive multi-gigabyte voice weights (like tortoise-tts or Bark) inside your Python environment. You get pristine, studio-quality Castilian speech with two lines of code.
|
||||||
|
|
||||||
|
## 14.3. Future Proofing: Going Fully Offline Down the Track
|
||||||
|
Because you prefer data privacy and local-first software engineering, it's worth noting that your current architecture is completely future-proofed for offline migration when you reach an advanced phase of development.
|
||||||
|
|
||||||
|
Because we explicitly decoupled your code—separating the asset creation logic into core/asset_generator.py and the application logic into main.py—you can swap out the backend audio engine later without changing a single line of your GUI code.
|
||||||
|
|
||||||
|
If you want to make your voice trainer run 100% locally in the future without hitting the cloud, you can replace the internal logic of AssetGenerator with an offline framework like:
|
||||||
|
|
||||||
|
Piper TTS: A fast, local, neural text-to-speech system optimized to run beautifully on local hardware architectures.
|
||||||
|
|
||||||
|
Kokoro: A highly lightweight, open-source local voice model that produces incredibly realistic human inflections.
|
||||||
|
|
||||||
|
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
|
||||||
BIN
doc/Notes.pdf
BIN
doc/Notes.pdf
Binary file not shown.
31
main.py
31
main.py
|
|
@ -1,18 +1,33 @@
|
||||||
# main.py
|
# main.py
|
||||||
import sys
|
import sys
|
||||||
|
import asyncio
|
||||||
from database.connection import init_db
|
from database.connection import init_db
|
||||||
|
from core.phrase_manager import PhraseManager
|
||||||
|
|
||||||
def main():
|
async def test_pipeline():
|
||||||
print("🚀 Booting Castilian Voice Trainer...")
|
print("🚀 Booting Castilian Voice Trainer Ingestion Engine...")
|
||||||
|
|
||||||
# Step 1: Ensure infrastructure is present before doing anything else
|
# Ensure database is present
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
# Step 2: Initialize Core Engines (AudioEngine, AssetGenerator)
|
# Initialize the phrase controller manager
|
||||||
# Step 3: Launch your PyQt6 Graphical User Interface
|
manager = PhraseManager()
|
||||||
print("🖥️ Launching GUI...")
|
|
||||||
|
|
||||||
# (PyQt6 window exec loops will go here)
|
# Test Entry: Let's log an authentic textbook phrase from Aula Internacional
|
||||||
|
print("\n📥 Processing sample entry...")
|
||||||
|
success = await manager.add_translation_pair(
|
||||||
|
spanish_text="¿Cómo se pronuncia esta palabra?",
|
||||||
|
english_text="How do you pronounce this word?",
|
||||||
|
textbook="Aula Internacional 1 Plus",
|
||||||
|
unit=2,
|
||||||
|
context="Glossary / Lesson Terms",
|
||||||
|
voice_gender="female" # Let's verify Elvira's voice on this one!
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n🎉 Verification Phase 1 Pipeline Test complete!")
|
||||||
|
else:
|
||||||
|
print("\n⚠️ Pipeline processing failed.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
asyncio.run(test_pipeline())
|
||||||
Loading…
Reference in a new issue