matheetharanadshyan's picture
Update
3a72c54 verified
Raw
History Blame Contribute Delete
14.1 kB
import warnings
warnings.filterwarnings("ignore")
import os
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
import gradio as gr
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torchaudio
from captum.attr import Saliency
from transformers import Wav2Vec2ForCTC, Wav2Vec2Processor, pipeline, logging as hf_logging
hf_logging.set_verbosity_error()
MODEL_ID = "matheetharanadshyan/ipd-submission-model"
LLM_ID = "Qwen/Qwen2.5-0.5B-Instruct"
SAMPLE_RATE = 16000
PRACTICE_PHRASES = {
"Simple Words": [
"Hello", "Carrot", "Apple", "Water", "Happy", "Good Morning"
],
}
ALL_PHRASES = []
for category, phrases in PRACTICE_PHRASES.items():
ALL_PHRASES.extend(phrases)
ALL_PHRASES.append("Custom Phrase...")
device = torch.device(
"mps" if torch.backends.mps.is_available()
else "cuda" if torch.cuda.is_available()
else "cpu"
)
print(f"Loading Models On {device}...")
processor = Wav2Vec2Processor.from_pretrained(MODEL_ID)
model = Wav2Vec2ForCTC.from_pretrained(MODEL_ID).to(device).eval()
llm_pipe = pipeline("text-generation", model=LLM_ID, device=device)
print("Models Loaded Successfully!")
def preprocess_audio(audio_path):
speech, sr = torchaudio.load(audio_path)
if speech.shape[0] > 1:
speech = torch.mean(speech, dim=0, keepdim=True)
if sr != SAMPLE_RATE:
speech = torchaudio.transforms.Resample(sr, SAMPLE_RATE)(speech)
speech = trim_silence(speech)
speech = speech / (torch.max(torch.abs(speech)) + 1e-8)
speech = torch.cat([speech[:, :1], speech[:, 1:] - 0.97 * speech[:, :-1]], dim=1)
return speech
def trim_silence(audio, threshold=0.01, frame_length=1024):
audio_abs = torch.abs(audio.squeeze())
start_idx, end_idx = 0, len(audio_abs)
for i in range(0, len(audio_abs) - frame_length, frame_length // 2):
if torch.mean(audio_abs[i:i+frame_length]) > threshold:
start_idx = max(0, i - frame_length)
break
for i in range(len(audio_abs) - frame_length, 0, -frame_length // 2):
if torch.mean(audio_abs[i:i+frame_length]) > threshold:
end_idx = min(len(audio_abs), i + frame_length * 2)
break
return audio[:, start_idx:end_idx]
def get_alignment(target, actual):
n, m = len(target), len(actual)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n + 1):
dp[i][0] = i
for j in range(m + 1):
dp[0][j] = j
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = 0 if target[i-1] == actual[j-1] else 1
dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + cost)
align, i, j = [], n, m
while i > 0 or j > 0:
if i > 0 and j > 0 and target[i-1] == actual[j-1]:
align.append((target[i-1], actual[j-1], "✓ Correct"))
i, j = i - 1, j - 1
elif i > 0 and j > 0 and dp[i][j] == dp[i-1][j-1] + 1:
align.append((target[i-1], actual[j-1], "✗ Substitution"))
i, j = i - 1, j - 1
elif j > 0 and (i == 0 or dp[i][j] == dp[i][j-1] + 1):
align.append(("—", actual[j-1], "⊕ Insertion"))
j -= 1
else:
align.append((target[i-1], "—", "⊖ Deletion"))
i -= 1
return align[::-1]
def calculate_accuracy(alignment):
total = len(alignment)
correct = sum(1 for _, _, s in alignment if "Correct" in s)
subs = sum(1 for _, _, s in alignment if "Substitution" in s)
ins = sum(1 for _, _, s in alignment if "Insertion" in s)
dels = sum(1 for _, _, s in alignment if "Deletion" in s)
return {
"accuracy": (correct / total * 100) if total > 0 else 0,
"wer": min(((subs + ins + dels) / max(total - ins, 1)) * 100, 100),
"correct": correct,
"total": total
}
def create_saliency_plot(grads, transcription=""):
grads_norm = grads / (np.max(grads) + 1e-8)
threshold = np.percentile(grads_norm, 85)
grads_smooth = np.convolve(grads_norm, np.ones(5)/5, mode='same')
fig, ax = plt.subplots(figsize=(10, 3), facecolor='#ffffff')
time_axis = np.arange(len(grads_norm)) / 50
ax.fill_between(time_axis, grads_smooth, color='#3b82f6', alpha=0.15)
ax.plot(time_axis, grads_smooth, color='#3b82f6', alpha=0.9, linewidth=2)
important_mask = grads_smooth > threshold
ax.fill_between(time_axis, grads_smooth, where=important_mask,
color='#3b82f6', alpha=0.4, label='High importance')
ax.set_facecolor('#ffffff')
for spine in ax.spines.values():
spine.set_visible(False)
ax.tick_params(colors='#6b7280', which='both', labelsize=9, length=0)
ax.set_xlabel('Time (seconds)', color='#374151', fontsize=10, fontweight='500')
ax.set_ylabel('Model Attention', color='#374151', fontsize=10, fontweight='500')
ax.set_xlim(0, time_axis[-1] if len(time_axis) > 0 else 1)
ax.set_ylim(0, 1.1)
ax.grid(True, axis='y', alpha=0.2, linestyle='-', linewidth=0.5, color='#d1d5db')
if transcription:
ax.set_title(f'Speech Pattern Analysis', fontsize=11, fontweight='600',
color='#111827', pad=10)
plt.tight_layout(pad=1.5)
return fig
def generate_feedback(target_txt, alignment, metrics):
errors = []
for target, actual, status in alignment:
if "Substitution" in status:
errors.append(f"said '{actual}' instead of '{target}'")
elif "Deletion" in status:
errors.append(f"omitted '{target}'")
elif "Insertion" in status:
errors.append(f"added extra '{actual}'")
accuracy = metrics['accuracy']
if accuracy >= 90:
score_color = "excellent"
elif accuracy >= 70:
score_color = "good"
elif accuracy >= 50:
score_color = "improving"
else:
score_color = "keep practicing"
score_text = f"Score: {accuracy:.0f}%\n**{metrics['correct']}/{metrics['total']}** words correct — {score_color}!\n\n"
if not errors:
return score_text + "**Perfect pronunciation!** All words were clearly articulated. Great job!"
prompt = f"""<|im_start|>system
You are a friendly speech coach. Provide ONE specific, actionable tip to improve pronunciation. Focus on mouth position, tongue placement, or breathing. Keep under 40 words. Be encouraging.<|im_end|>
<|im_start|>user
Target phrase: "{target_txt}"
Errors made: {"; ".join(errors[:4])}
Accuracy: {metrics['accuracy']:.0f}%<|im_end|>
<|im_start|>assistant
"""
try:
response = llm_pipe(prompt, max_new_tokens=70, max_length=None, do_sample=True, temperature=0.7)
feedback = response[0]['generated_text'].split("assistant\n")[-1].strip()
if feedback and feedback[-1] not in '.!?':
feedback = feedback.rsplit('.', 1)[0] + '.' if '.' in feedback else feedback
except Exception:
if any("Substitution" in e for _, _, e in alignment):
feedback = "Try speaking more slowly and focus on each syllable. Exaggerate mouth movements to improve clarity."
elif any("Deletion" in e for _, _, e in alignment):
feedback = "Some words were missed. Try pausing briefly between words and speaking at a steady pace."
else:
feedback = "Focus on speaking clearly and at a comfortable pace. Record yourself and compare to improve."
return score_text + f"**Tip:** {feedback}"
def get_accuracy_color(accuracy):
"""Return a color based on accuracy level."""
if accuracy >= 90:
return "#10b981"
elif accuracy >= 70:
return "#f59e0b"
elif accuracy >= 50:
return "#f97316"
else:
return "#ef4444"
def analyze(audio, selected, custom):
target_txt = (custom.strip() if selected == "Custom Phrase..." and custom else selected).strip()
if not target_txt or target_txt == "Custom Phrase...":
return (
"",
None,
"**Please select or enter a target phrase** to begin your practice session.",
None
)
if not audio:
return (
"",
None,
f"**Ready to analyze:** \"{target_txt}\"\n\nClick the microphone button above to record yourself saying this phrase.",
None
)
try:
speech = preprocess_audio(audio)
if speech.shape[1] < 1600:
return (
"",
None,
"**Recording too short**\n\nPlease record for at least 1 second. Speak the entire phrase clearly.",
None
)
inputs = processor(
speech.squeeze().numpy(),
sampling_rate=SAMPLE_RATE,
return_tensors="pt"
).input_values.to(device)
inputs.requires_grad = True
with torch.no_grad():
logits = model(inputs).logits
pred_ids = torch.argmax(logits, dim=-1)
transcription = processor.batch_decode(pred_ids)[0].lower().strip()
if not transcription:
return (
"",
None,
"**No speech detected**\n\nPlease speak louder and closer to the microphone. Make sure your microphone is working.",
None
)
alignment = get_alignment(target_txt.lower().split(), transcription.split())
metrics = calculate_accuracy(alignment)
df = pd.DataFrame(alignment, columns=["Expected", "Heard", "Status"])
inputs_grad = processor(
speech.squeeze().numpy(),
sampling_rate=SAMPLE_RATE,
return_tensors="pt"
).input_values.to(device)
inputs_grad.requires_grad = True
def forward_fn(x):
return torch.gather(model(x).logits, 2, pred_ids.unsqueeze(-1)).squeeze(-1).sum(dim=-1)
grads = Saliency(forward_fn).attribute(inputs_grad).abs().squeeze().detach().cpu().numpy()
fig = create_saliency_plot(grads, transcription)
feedback = generate_feedback(target_txt, alignment, metrics)
return transcription.upper(), fig, feedback, df
except Exception as e:
import traceback
traceback.print_exc()
return (
"",
None,
f"**Analysis Error**\n\nSomething went wrong: {str(e)}\n\nPlease try recording again.",
None
)
css = """
* { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; }
footer { visibility: hidden; }
.gradio-container {
max-width: 640px !important;
margin: 32px auto !important;
padding: 32px !important;
}
.app-header { text-align: center; margin-bottom: 32px; }
.app-title { font-size: 1.5rem; font-weight: 600; color: #111; margin: 0 0 4px; }
.app-subtitle { font-size: 0.875rem; color: #666; margin: 0; }
.section-title {
font-size: 0.7rem; font-weight: 600; color: #888;
text-transform: uppercase; letter-spacing: 0.05em;
margin: 24px 0 8px;
}
.analyze-button {
background: #111 !important; color: #fff !important;
font-weight: 500 !important; font-size: 0.875rem !important;
padding: 10px 20px !important; border: none !important;
border-radius: 6px !important; width: 100% !important;
margin-top: 16px !important;
}
.analyze-button:hover { background: #333 !important; }
.transcription-output textarea {
text-align: center !important; font-weight: 600 !important;
font-size: 1.25rem !important; padding: 16px !important;
background: #f9f9f9 !important; border: 1px solid #eee !important;
border-radius: 6px !important;
}
.feedback-output {
background: #fefce8 !important; border: 1px solid #fde047 !important;
border-radius: 6px !important; padding: 16px !important;
line-height: 1.6 !important; font-size: 0.875rem !important;
}
.dataframe-container table { font-size: 0.8rem !important; }
.dataframe-container thead th {
background: #f5f5f5 !important; font-size: 0.7rem !important;
text-transform: uppercase; padding: 8px 12px !important;
}
.dataframe-container tbody td { padding: 8px 12px !important; }
.gr-box, .gr-panel, .gr-form, .gr-group, .block, .wrap, .contain {
background: transparent !important; border: none !important; box-shadow: none !important;
}
"""
with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
gr.HTML("""
<div class="app-header">
<h1 class="app-title">Vocalis</h1>
<p class="app-subtitle">An Adaptive Explainable AI Framework For Pronunciation Training</p>
</div>
""")
target_drop = gr.Dropdown(
choices=ALL_PHRASES,
value="Hello",
label="Phrase to practice",
interactive=True
)
custom_in = gr.Textbox(
placeholder="Type your phrase...",
label="Custom phrase",
visible=False
)
audio_in = gr.Audio(
sources=["microphone", "upload"],
type="filepath",
label="Recording"
)
run_btn = gr.Button("Analyze", elem_classes="analyze-button")
gr.HTML('<div class="section-title">Results</div>')
out_t = gr.Textbox(
label="Transcription",
elem_classes="transcription-output",
interactive=False
)
out_f = gr.Markdown(elem_classes="feedback-output")
out_p = gr.Plot(label="Speech Pattern", show_label=True)
out_d = gr.Dataframe(
label="Word Comparison",
interactive=False,
elem_classes="dataframe-container"
)
target_drop.change(
lambda x: gr.update(visible=(x == "Custom Phrase...")),
target_drop, custom_in
)
run_btn.click(
fn=analyze,
inputs=[audio_in, target_drop, custom_in],
outputs=[out_t, out_p, out_f, out_d]
)
if __name__ == "__main__":
demo.launch(share=True, server_name="0.0.0.0", server_port=7860)