EuropaLex / export /csv_export.py
Takosaga's picture
feat: expand ISO 639-1 language mapping to 23 EU languages
99d7b50
Raw
History Blame Contribute Delete
5.92 kB
"""EuropaLex CSV Export β€” creates a zipped folder containing CSV + media files."""
import csv
import re
import shutil
from pathlib import Path
from typing import Any
# ISO 639-1 language abbreviation mapping
_LANGUAGE_ABBREVS: dict[str, str] = {
"Bulgarian": "BG",
"Croatian": "HR",
"Czech": "CS",
"Danish": "DA",
"Dutch": "NL",
"Estonian": "ET",
"Finnish": "FI",
"French": "FR",
"German": "DE",
"Greek": "EL",
"Hungarian": "HU",
"Irish": "GA",
"Italian": "IT",
"Latvian": "LV",
"Lithuanian": "LT",
"Maltese": "MT",
"Polish": "PL",
"Portuguese": "PT",
"Romanian": "RO",
"Slovak": "SK",
"Slovenian": "SL",
"Spanish": "ES",
"Swedish": "SV",
}
# Project root for resolving relative paths
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
def _sanitize_folder_name(scenario: str) -> str:
"""Convert scenario text to a filesystem-safe folder name slug.
Rules: lowercase, remove special characters (keep alphanumeric, spaces, underscores),
replace spaces with underscores, collapse multiple underscores, strip leading/trailing underscores.
Args:
scenario: Free-form scenario/topic string from the user.
Returns:
Sanitized slug suitable for use as a directory name.
"""
slug = scenario.strip().lower()
slug = re.sub(r'[^a-z0-9\s_]', '', slug) # remove special chars
slug = re.sub(r'\s+', '_', slug) # spaces β†’ underscores
slug = re.sub(r'_+', '_', slug) # collapse multiple underscores
return slug.strip('_')
def _get_language_abbrev(language: str) -> str:
"""Return the ISO 639-1 abbreviation for a language name.
Args:
language: Language name (e.g., 'Latvian', 'Spanish').
Returns:
Two-letter ISO 639-1 code.
Raises:
ValueError: If the language is not in the mapping.
"""
if language not in _LANGUAGE_ABBREVS:
raise ValueError(
f"Unknown language '{language}'. "
f"Supported: {', '.join(sorted(_LANGUAGE_ABBREVS.keys()))}"
)
return _LANGUAGE_ABBREVS[language]
def export_csv_zip(
cards: list[dict[str, Any]],
scenario: str,
cefr_level: str,
target_language: str,
) -> str:
"""Export cards as a zipped folder containing CSV + media files.
Creates a folder under {models_dir}/output/export/ with the following flat structure:
{folder_name}/
cards.csv (CSV with 7 columns, one row per card)
{scenario_slug}_{CEFR}_{LANG}_0.wav (copied from TTS output)
{scenario_slug}_{CEFR}_{LANG}_0.png (copied from image generation)
{folder_name}.zip (zipped archive of the above)
CSV columns: scenario, cefr_level, target_language, english_text, translated_text,
audio_filename, image_filename
Media filenames are bare filenames in the export folder (e.g., 'ordering_coffee_A2_LV_0.wav').
Missing media files are silently skipped β€” CSV entries remain empty strings.
Args:
cards: List of card dicts with keys: 'text', 'translation',
'audio_path' (str or None), 'image_path' (str or None).
scenario: Free-form scenario/topic string.
cefr_level: CEFR level string (e.g., 'A2', 'B1').
target_language: Target language name (e.g., 'Latvian').
Returns:
Absolute path to the generated .zip file.
Raises:
ValueError: If target_language is not in the supported mapping.
RuntimeError: If zip creation fails.
"""
# Resolve output directory from project root
export_base = _PROJECT_ROOT / ".local" / "models" / "output" / "export"
export_base.mkdir(parents=True, exist_ok=True)
# Build folder name
scenario_slug = _sanitize_folder_name(scenario)
lang_abbrev = _get_language_abbrev(target_language)
folder_name = f"{scenario_slug}_{cefr_level}_{lang_abbrev}"
export_dir = export_base / folder_name
export_dir.mkdir(parents=True, exist_ok=True)
# Copy media files and build CSV rows (flat folder β€” no subfolders)
csv_path = export_dir / "cards.csv"
with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile, quoting=csv.QUOTE_ALL)
# Header row
writer.writerow([
'scenario', 'cefr_level', 'target_language',
'english_text', 'translated_text',
'audio_filename', 'image_filename'
])
for i, card in enumerate(cards):
audio_path = card.get('audio_path')
image_path = card.get('image_path')
# Build the common prefix: {scenario_slug}_{CEFR}_{LANG_ABBREV}
base_name = f"{scenario_slug}_{cefr_level}_{lang_abbrev}"
# Copy audio file if it exists β€” flat naming
audio_filename = ''
if audio_path and Path(audio_path).exists():
audio_dst = export_dir / f"{base_name}_{i}.wav"
shutil.copy2(audio_path, audio_dst)
audio_filename = f"{base_name}_{i}.wav"
# Copy image file if it exists β€” flat naming
image_filename = ''
if image_path and Path(image_path).exists():
image_dst = export_dir / f"{base_name}_{i}.png"
shutil.copy2(image_path, image_dst)
image_filename = f"{base_name}_{i}.png"
writer.writerow([
scenario,
cefr_level,
target_language,
card.get('text', ''),
card.get('translation', ''),
audio_filename,
image_filename,
])
# Create zip archive
zip_path = shutil.make_archive(
str(export_base / folder_name),
'zip',
export_dir,
)
return str(zip_path)