Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,20 @@ function renderCategories(categories) {
card.setAttribute("tabindex", "0");

const emoji = CATEGORY_EMOJI[cat.name] || "📚";
card.innerHTML = `<span class="emoji">${emoji}</span> <span>${cat.display_name}</span><br><span class="small">${cat.word_count} word${cat.word_count !== 1 ? "s" : ""}</span>`;
const emojiSpan = document.createElement("span");
emojiSpan.className = "emoji";
emojiSpan.textContent = emoji;
const nameSpan = document.createElement("span");
nameSpan.textContent = cat.display_name;
const lineBreak = document.createElement("br");
const countSpan = document.createElement("span");
countSpan.className = "small";
countSpan.textContent = `${cat.word_count} word${cat.word_count !== 1 ? "s" : ""}`;
card.appendChild(emojiSpan);
card.appendChild(document.createTextNode(" "));
card.appendChild(nameSpan);
card.appendChild(lineBreak);
card.appendChild(countSpan);

const go = () => {
const { lang, difficulty } = getState();
Expand Down
22 changes: 11 additions & 11 deletions backend/SCORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ If your voice's fundamental frequency differs from the reference speaker's by 40

### A. Feature weights

**File:** `feature_comparator.py`, lines 14–20
**File:** `feature_comparator.py`, lines 13–19

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reference points to feature_comparator.py, but the actual module in this repo appears to be backend/app/services/feature_comparator.py (consistent with earlier sections of this doc). Update the path here to avoid sending readers to a non-existent file name.

Suggested change
**File:** `feature_comparator.py`, lines 13–19
**File:** `backend/app/services/feature_comparator.py`, lines 13–19

Copilot uses AI. Check for mistakes.

```python
WEIGHTS = {
Expand All @@ -359,16 +359,16 @@ Must sum to 1.0.

| Feature | Component | Sigma | File location | More lenient suggestion |
|---------|-----------|-------|---------------|------------------------|
| Pitch | Contour (DTW) | 50 | `_compare_pitch`, line ~143 | 80–100 |
| Pitch | Mean | 30 | `_compare_pitch`, line ~146 | 50–60 |
| Pitch | Range | 40 | `_compare_pitch`, line ~150 | 40 (fine) |
| Formants | F1/F2/F3 (DTW) | 100 | `_compare_formants`, line ~163 | 200–300 |
| Intensity | Contour (DTW) | 10 | `_compare_intensity`, line ~186 | 15–20 |
| Intensity | Mean | 5 | `_compare_intensity`, line ~190 | 10–15 |
| Duration | Time ratio | 0.3 | `_compare_duration`, line ~208 | 0.3 (fine) |
| Duration | Voiced fraction | 0.2 | `_compare_duration`, line ~218 | 0.2 (fine) |
| Voice quality | Jitter | 0.01 | `_compare_voice_quality`, line ~231 | 0.02–0.03 |
| Voice quality | Shimmer | 0.05 | `_compare_voice_quality`, line ~232 | 0.08–0.10 |
| Pitch | Contour (DTW) | 50 | `_compare_pitch` | 80–100 |
| Pitch | Mean | 30 | `_compare_pitch` | 50–60 |
| Pitch | Range | 40 | `_compare_pitch` | 40 (fine) |
| Formants | F1/F2/F3 (DTW) | 100 | `_compare_formants` | 200–300 |
| Intensity | Contour (DTW) | 10 | `_compare_intensity` | 15–20 |
| Intensity | Mean | 5 | `_compare_intensity` | 10–15 |
| Duration | Time ratio | 0.3 | `_compare_duration` | 0.3 (fine) |
| Duration | Voiced fraction | 0.2 | `_compare_duration` | 0.2 (fine) |
| Voice quality | Jitter | 0.01 | `_compare_voice_quality` | 0.02–0.03 |
| Voice quality | Shimmer | 0.05 | `_compare_voice_quality` | 0.08–0.10 |

### C. Sub-feature weights

Expand Down
11 changes: 10 additions & 1 deletion backend/app/routes/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ async def stream_audio(word_id: int, db: aiosqlite.Connection = Depends(get_db))
if not audio_file:
raise HTTPException(404, f"No audio file for word {word_id}")

path = settings.AUDIO_DIR / audio_file
# Resolve path safely and ensure it stays within the configured audio directory
try:
base_dir = settings.AUDIO_DIR.resolve()
path = (base_dir / audio_file).resolve()
except Exception:
# Any failure resolving the path is treated as an invalid path
raise HTTPException(400, "Invalid audio file path")
# Prevent path traversal by ensuring the resolved path is inside base_dir
if path == base_dir or base_dir not in path.parents:
raise HTTPException(400, "Invalid audio file path")
if not path.is_file():
raise HTTPException(404, f"Audio file not found on disk: {audio_file}")

Expand Down
18 changes: 12 additions & 6 deletions backend/app/routes/words.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

router = APIRouter(tags=["words"])

LANG_COLUMNS = {"en": "translation_en", "fr": "translation_fr", "de": "translation_de"}


@router.get("/categories/{category_name}/words", response_model=list[WordOut])
async def list_words(
Expand All @@ -25,15 +23,23 @@ async def list_words(
if not cat:
raise HTTPException(404, f"Category '{category_name}' not found")

col = LANG_COLUMNS[lang]
cursor = await db.execute(
f"""
SELECT id, word_lb, {col} AS translation, gender, audio_filename
"""
SELECT
id,
word_lb,
CASE ?
WHEN 'en' THEN translation_en
WHEN 'fr' THEN translation_fr
WHEN 'de' THEN translation_de
END AS translation,
gender,
audio_filename
FROM words
WHERE category_id = ?
ORDER BY id
""",
(cat["id"],),
(lang, cat["id"]),
)
rows = await cursor.fetchall()
return [
Expand Down
2 changes: 1 addition & 1 deletion topic.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function updateUI() {
const labels = { en: "English", fr: "French", de: "German" };
const label = labels[TARGET_LANG] || "English";
const translation = word.translation || "";
meaningText.innerHTML = `Translation (${label}): <b>${translation}</b>`;
meaningText.textContent = `Translation (${label}): ${translation}`;
}

// Audio-only mode hides word + meaning
Expand Down