Vadym Myroshnyk commited on
Commit ·
83b23d4
1
Parent(s): e19b2fa
save
Browse files- app.py +39 -79
- audio_files.py +12 -0
- check.py +38 -0
- highlighter.py +31 -0
app.py
CHANGED
|
@@ -1,84 +1,27 @@
|
|
| 1 |
import os
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
for word in user_words:
|
| 14 |
-
match, score, idx = process.extractOne(word, original_words, scorer=fuzz.ratio)
|
| 15 |
-
if score > 85 and idx not in used_indices:
|
| 16 |
-
highlighted.append(
|
| 17 |
-
f"<span style='background-color:#f0f0f0; color:#333; padding:4px 10px; "
|
| 18 |
-
f"margin:4px; border-radius:999px; font-weight:500; display:inline-block;'>{word}</span>"
|
| 19 |
-
)
|
| 20 |
-
used_indices.add(idx)
|
| 21 |
-
else:
|
| 22 |
-
highlighted.append(
|
| 23 |
-
f"<span style='background-color:#e6ffe6; color:#006600; padding:4px 10px; "
|
| 24 |
-
f"margin:4px; border-radius:999px; font-weight:500; display:inline-block;'>{word}</span>"
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
for i, word in enumerate(original_words):
|
| 28 |
-
if i not in used_indices:
|
| 29 |
-
highlighted.append(
|
| 30 |
-
f"<span style='background-color:#ffe6e6; color:#990000; padding:4px 10px; "
|
| 31 |
-
f"margin:4px; border-radius:999px; font-weight:500; display:inline-block;'>{word}</span>"
|
| 32 |
-
)
|
| 33 |
-
|
| 34 |
-
return " ".join(highlighted)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def check_transcription(file_name, user_text):
|
| 38 |
-
audio_path = os.path.join("audio", file_name)
|
| 39 |
-
transcript_path = audio_path.replace(".mp3", ".txt").replace("audio", "transcripts")
|
| 40 |
-
|
| 41 |
-
with open(transcript_path, 'r') as f:
|
| 42 |
-
original = f.read().strip()
|
| 43 |
-
|
| 44 |
-
diff_html = highlight_fuzzy_diff(user_text, original)
|
| 45 |
-
score = round(fuzz.ratio(user_text, original))
|
| 46 |
-
|
| 47 |
-
if score > 90:
|
| 48 |
-
emoji = "✅"
|
| 49 |
-
comment = "Great job! Keep it up."
|
| 50 |
-
elif score > 70:
|
| 51 |
-
emoji = "⚠️"
|
| 52 |
-
comment = "Not bad, but pay attention to some words."
|
| 53 |
-
else:
|
| 54 |
-
emoji = "❌"
|
| 55 |
-
comment = "Try again. There are too many mistakes."
|
| 56 |
-
|
| 57 |
-
simple_feedback = f"""
|
| 58 |
-
<div style='font-size: 16px; margin-bottom: 6px;'>
|
| 59 |
-
Similarity: <b>{score}%</b> {emoji}
|
| 60 |
-
</div>
|
| 61 |
-
<div style='margin-top: 4px;'>{comment}</div>
|
| 62 |
-
"""
|
| 63 |
-
|
| 64 |
-
return audio_path, original, (
|
| 65 |
-
"<div style='margin-bottom: 10px;'><b>⬜ Extra / incorrect words</b>, "
|
| 66 |
-
"<b>🟥 Missing words</b>, <b>🟩 Correct words</b></div>" + diff_html
|
| 67 |
-
), simple_feedback
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
# === LOAD AUDIO FILES ===
|
| 71 |
-
audio_dir = "audio"
|
| 72 |
-
audio_list = [f for f in os.listdir(audio_dir) if f.endswith(".mp3")]
|
| 73 |
-
default_file = audio_list[0] if audio_list else None
|
| 74 |
-
|
| 75 |
-
# === GRADIO INTERFACE ===
|
| 76 |
with gr.Blocks(theme="soft") as demo:
|
| 77 |
gr.Markdown("## 🎧 Modals & Conditionals Bootcamp")
|
| 78 |
|
| 79 |
with gr.Row():
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
user_input = gr.Textbox(label="Your transcription", lines=6)
|
| 84 |
btn = gr.Button("Check")
|
|
@@ -88,13 +31,30 @@ with gr.Blocks(theme="soft") as demo:
|
|
| 88 |
feedback_output = gr.HTML(label="Feedback and score")
|
| 89 |
|
| 90 |
|
| 91 |
-
def
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
btn.click(
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
demo.launch()
|
|
|
|
| 1 |
import os
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
+
|
| 5 |
+
from audio_files import get_audio_structure
|
| 6 |
+
from check import check_transcription
|
| 7 |
+
|
| 8 |
+
audio_structure = get_audio_structure()
|
| 9 |
+
week_options = list(audio_structure.keys())
|
| 10 |
+
default_week = week_options[0]
|
| 11 |
+
default_file = audio_structure[default_week][0] if audio_structure[default_week] else ""
|
| 12 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
with gr.Blocks(theme="soft") as demo:
|
| 14 |
gr.Markdown("## 🎧 Modals & Conditionals Bootcamp")
|
| 15 |
|
| 16 |
with gr.Row():
|
| 17 |
+
week_dropdown = gr.Dropdown(label="Select a week", choices=week_options, value=default_week, scale=1)
|
| 18 |
+
lesson_dropdown = gr.Dropdown(
|
| 19 |
+
label="Select a lesson",
|
| 20 |
+
choices=[os.path.basename(f) for f in audio_structure[default_week]],
|
| 21 |
+
value=os.path.basename(default_file),
|
| 22 |
+
scale=1
|
| 23 |
+
)
|
| 24 |
+
audio_player = gr.Audio(label="Audio preview", type="filepath", value=default_file, scale=3)
|
| 25 |
|
| 26 |
user_input = gr.Textbox(label="Your transcription", lines=6)
|
| 27 |
btn = gr.Button("Check")
|
|
|
|
| 31 |
feedback_output = gr.HTML(label="Feedback and score")
|
| 32 |
|
| 33 |
|
| 34 |
+
def update_lessons(week):
|
| 35 |
+
lessons = [os.path.basename(f) for f in audio_structure[week]]
|
| 36 |
+
return gr.update(choices=lessons, value=lessons[0])
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def get_audio_path(week, lesson):
|
| 40 |
+
folder = week.lower().replace(" ", "_") # e.g. "Week 1" -> "week_1"
|
| 41 |
+
return os.path.join("audio", folder, lesson)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def check_transcription_wrapper(week, lesson, user_text):
|
| 45 |
+
full_path = get_audio_path(week, lesson)
|
| 46 |
+
return check_transcription(full_path, user_text)
|
| 47 |
|
| 48 |
|
| 49 |
+
# === EVENTS ===
|
| 50 |
+
week_dropdown.change(fn=update_lessons, inputs=week_dropdown, outputs=lesson_dropdown)
|
| 51 |
+
lesson_dropdown.change(fn=lambda w, l: get_audio_path(w, l), inputs=[week_dropdown, lesson_dropdown],
|
| 52 |
+
outputs=audio_player)
|
| 53 |
|
| 54 |
+
btn.click(
|
| 55 |
+
fn=check_transcription_wrapper,
|
| 56 |
+
inputs=[week_dropdown, lesson_dropdown, user_input],
|
| 57 |
+
outputs=[audio_player, original_output, diff_output, feedback_output]
|
| 58 |
+
)
|
| 59 |
|
| 60 |
demo.launch()
|
audio_files.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def get_audio_structure(audio_dir="audio"):
|
| 5 |
+
structure = {}
|
| 6 |
+
for week_folder in sorted(os.listdir(audio_dir)):
|
| 7 |
+
folder_path = os.path.join(audio_dir, week_folder)
|
| 8 |
+
if os.path.isdir(folder_path) and week_folder.lower().startswith("week_"):
|
| 9 |
+
label = week_folder.replace("_", " ").title()
|
| 10 |
+
files = [f for f in sorted(os.listdir(folder_path)) if f.endswith(".mp3")]
|
| 11 |
+
structure[label] = [os.path.join(folder_path, f) for f in files]
|
| 12 |
+
return structure
|
check.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
from rapidfuzz import fuzz
|
| 4 |
+
|
| 5 |
+
from highlighter import highlight_fuzzy_diff
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def check_transcription(file_name, user_text):
|
| 9 |
+
audio_path = os.path.join("audio", file_name)
|
| 10 |
+
transcript_path = audio_path.replace(".mp3", ".txt").replace("audio", "transcripts")
|
| 11 |
+
|
| 12 |
+
with open(transcript_path, 'r') as f:
|
| 13 |
+
original = f.read().strip()
|
| 14 |
+
|
| 15 |
+
diff_html = highlight_fuzzy_diff(user_text, original)
|
| 16 |
+
score = round(fuzz.ratio(user_text, original))
|
| 17 |
+
|
| 18 |
+
if score > 90:
|
| 19 |
+
emoji = "✅"
|
| 20 |
+
comment = "Great job! Keep it up."
|
| 21 |
+
elif score > 70:
|
| 22 |
+
emoji = "⚠️"
|
| 23 |
+
comment = "Not bad, but pay attention to some words."
|
| 24 |
+
else:
|
| 25 |
+
emoji = "❌"
|
| 26 |
+
comment = "Try again. There are too many mistakes."
|
| 27 |
+
|
| 28 |
+
simple_feedback = f"""
|
| 29 |
+
<div style='font-size: 16px; margin-bottom: 6px;'>
|
| 30 |
+
Similarity: <b>{score}%</b> {emoji}
|
| 31 |
+
</div>
|
| 32 |
+
<div style='margin-top: 4px;'>{comment}</div>
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
return audio_path, original, (
|
| 36 |
+
"<div style='margin-bottom: 10px;'><b>⬜ Extra / incorrect words</b>, "
|
| 37 |
+
"<b>🟥 Missing words</b>, <b>🟩 Correct words</b></div>" + diff_html
|
| 38 |
+
), simple_feedback
|
highlighter.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rapidfuzz import fuzz, process
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def highlight_fuzzy_diff(user_text, original_text):
|
| 5 |
+
user_words = user_text.strip().split()
|
| 6 |
+
original_words = original_text.strip().split()
|
| 7 |
+
highlighted = []
|
| 8 |
+
used_indices = set()
|
| 9 |
+
|
| 10 |
+
for word in user_words:
|
| 11 |
+
match, score, idx = process.extractOne(word, original_words, scorer=fuzz.ratio)
|
| 12 |
+
if score > 85 and idx not in used_indices:
|
| 13 |
+
highlighted.append(
|
| 14 |
+
f"<span style='background-color:#f0f0f0; color:#333; padding:4px 10px; "
|
| 15 |
+
f"margin:4px; border-radius:999px; font-weight:500; display:inline-block;'>{word}</span>"
|
| 16 |
+
)
|
| 17 |
+
used_indices.add(idx)
|
| 18 |
+
else:
|
| 19 |
+
highlighted.append(
|
| 20 |
+
f"<span style='background-color:#e6ffe6; color:#006600; padding:4px 10px; "
|
| 21 |
+
f"margin:4px; border-radius:999px; font-weight:500; display:inline-block;'>{word}</span>"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
for i, word in enumerate(original_words):
|
| 25 |
+
if i not in used_indices:
|
| 26 |
+
highlighted.append(
|
| 27 |
+
f"<span style='background-color:#ffe6e6; color:#990000; padding:4px 10px; "
|
| 28 |
+
f"margin:4px; border-radius:999px; font-weight:500; display:inline-block;'>{word}</span>"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
return " ".join(highlighted)
|