Download controllers/app.py from seba3y/SpeakLab: direct link, hf CLI and curl.
- Browser
- Download file 18 kB
-
https://huggingface.co/spaces/seba3y/SpeakLab/resolve/main/controllers/app.py
- Command line
-
hf download hf://spaces/seba3y/SpeakLab/controllers/app.py
-
curl -L -o app.py https://huggingface.co/spaces/seba3y/SpeakLab/resolve/main/controllers/app.py
18 kB
| """ | |
| Speak Lab β Gradio Edition (MVC refactor) | |
| ========================================== | |
| app.py is the entry-point View/Router only. | |
| Business logic lives in controllers/, data shapes in models/, HTML in views/. | |
| Missing-from-Gradio features now implemented vs original web app: | |
| β No-repeat shuffler (cycles full pool before repeating, state in BrowserState) | |
| β PRES method tips card (Impromptu) | |
| β STAR method tips card (Interview) | |
| β JAM habit tip card | |
| β Tongue Twister articulation tip card | |
| β Free tab: module category selector (JAM / TT / Impromptu / Interview) | |
| β Free tab: prompt badge showing selected category | |
| β Countdown bar for JAM (CSS animation, 60s) | |
| β Min-session guard (5 s) β toast warning | |
| β Streak only increments on "Save / Download" intent, not on Stop | |
| β Toast notifications | |
| β Analysis panel stub wired to all tabs | |
| """ | |
| import gradio as gr | |
| from controllers import DataController, StreakController, AnalysisController | |
| from controllers.ui_handlers import UIHandlers | |
| from models.session_models import FreeModuleChoice, AnalysisResult | |
| from views import render_streak_html, render_analysis_html, CSS | |
| from views.html_helpers import badge_html, prompt_html | |
| # ββ Bootstrap ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| data_ctrl = DataController() | |
| streak_ctrl = StreakController() | |
| analysis_ctrl = AnalysisController() | |
| handlers = UIHandlers(data_ctrl, streak_ctrl, analysis_ctrl) | |
| EMPTY_STREAK_HTML = render_streak_html(None) | |
| EMPTY_ANALYSIS_HTML = render_analysis_html(None) | |
| MIN_SESSION_SEC = 5 | |
| # ββ Shuffler pool keys (mirror JS makeShuffler keys) βββββββββββββββββββββββββ | |
| IMP_CAT_NAMES = ["All"] + data_ctrl.imp_category_names | |
| IV_CAT_NAMES = ["All"] + data_ctrl.iv_category_names | |
| FREE_MODULE_CHOICES = [e.value for e in FreeModuleChoice] | |
| # ββ Toast JS helper injected via gr.HTML ββββββββββββββββββββββββββββββββββββββ | |
| TOAST_HTML = '<div id="sl-toast"></div>' | |
| JAM_COUNTDOWN_HTML = '<div id="jam-bar-wrap"><div id="jam-bar"></div></div>' | |
| # JS injected via Blocks(head=) so scripts actually execute | |
| HEAD_JS = """ | |
| <style> | |
| @keyframes jam-shrink { from { width: 100%; } to { width: 0%; } } | |
| #jam-bar.live { animation: jam-shrink 60s linear forwards; background: linear-gradient(90deg, #6366f1, #a855f7); } | |
| #jam-bar { height: 6px; border-radius: 3px; background: #1e2130; transition: none; } | |
| #jam-bar-wrap { width:100%; background:#1e2130; border-radius:3px; overflow:hidden; margin-top:8px; } | |
| </style> | |
| <script> | |
| function slToast(msg, ms) { | |
| ms = ms || 2200; | |
| var el = document.getElementById('sl-toast'); | |
| if (!el) { el = document.createElement('div'); el.id='sl-toast'; document.body.appendChild(el); } | |
| el.textContent = msg; | |
| el.classList.add('show'); | |
| clearTimeout(window._slToastT); | |
| window._slToastT = setTimeout(function(){ el.classList.remove('show'); }, ms); | |
| } | |
| function jamStartCountdown() { | |
| var bar = document.getElementById('jam-bar'); | |
| if (!bar) return; | |
| bar.classList.remove('live'); | |
| void bar.offsetWidth; | |
| bar.classList.add('live'); | |
| } | |
| function jamStopCountdown() { | |
| var bar = document.getElementById('jam-bar'); | |
| if (bar) bar.classList.remove('live'); | |
| } | |
| </script> | |
| """ | |
| # ββ Build UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(title="Speak Lab ποΈ") as demo: | |
| # ββ Persistent state in browser localStorage ββββββββββββββββββββββββββββββ | |
| streak_store = gr.BrowserState({}) # StreakStore dict | |
| shuffler_state = gr.BrowserState({}) # {pool_key: {seen:[...]}} | |
| # ββ Toast + global helpers ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.HTML(TOAST_HTML) | |
| # ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.Markdown( | |
| "# ποΈ Speak Lab\n" | |
| "A serverless, account-free app to practice English speaking and build a daily habit.\n" | |
| "Record, review, and track your streak." | |
| ) | |
| with gr.Tabs(): | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # JAM β Just A Minute | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("β‘ JAM"): | |
| gr.HTML('<div class="desc-text">Speak for <strong>60 seconds</strong>. No hesitations, no pauses, and no filler words.</div>') | |
| jam_prompt = gr.HTML(prompt_html((data_ctrl.jam_prompts or [""])[0])) | |
| jam_shuffle = gr.Button("π² New Question", variant="primary") | |
| # Countdown bar (CSS animation, reset by JS) | |
| gr.HTML(JAM_COUNTDOWN_HTML) | |
| jam_audio = gr.Audio(label="ποΈ Record", sources=["microphone"], type="filepath") | |
| jam_duration = gr.Number(value=0, visible=False) # seconds, set by JS | |
| with gr.Row(): | |
| jam_save_btn = gr.Button("πΎ Count this session", variant="primary", interactive=False) | |
| jam_analyse = gr.Button("π¬ Analyse Recording", variant="secondary", interactive=False) | |
| jam_analysis = gr.HTML(EMPTY_ANALYSIS_HTML) | |
| # Framework tip | |
| gr.HTML(""" | |
| <div class="framework-tips"> | |
| <strong>π‘ Habit Tip:</strong> Try to express your thoughts without pausing | |
| or using filler sounds like "um" or "ah". Focus on fluency and flow. | |
| </div>""") | |
| jam_shuffle.click(handlers.jam_next, | |
| inputs=[shuffler_state], | |
| outputs=[jam_prompt, shuffler_state]) | |
| jam_audio.change(handlers.audio_ready, inputs=jam_audio, | |
| outputs=[jam_save_btn, jam_analyse]) | |
| toast_out = gr.Markdown(visible=False) | |
| jam_save_btn.click(handlers.jam_count, | |
| inputs=[jam_audio, streak_store], | |
| outputs=[streak_store, toast_out]) | |
| jam_analyse.click(handlers.jam_analyse, inputs=jam_audio, outputs=jam_analysis) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tongue Twisters | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Tongue Twisters"): | |
| gr.HTML('<div class="desc-text">Train articulation, muscle memory, and clarity. Practice 3 times slowly, then 3 times fast.</div>') | |
| with gr.Row(): | |
| tt_level = gr.Radio(["easy", "medium", "hard"], value="easy", label="Difficulty", interactive=True) | |
| tt_shuffle = gr.Button("π² New", variant="primary") | |
| tt_items = data_ctrl.tt_items("easy") | |
| first_tt = tt_items[0] if tt_items else None | |
| tt_focus = gr.HTML(badge_html(f"Focus: {first_tt.focus if first_tt else 'Practice'}")) | |
| tt_prompt = gr.HTML(prompt_html(first_tt.text if first_tt else "")) | |
| tt_audio = gr.Audio(label="ποΈ Record", sources=["microphone"], type="filepath") | |
| with gr.Row(): | |
| tt_save_btn = gr.Button("πΎ Count this session", variant="primary", interactive=False) | |
| tt_analyse = gr.Button("π¬ Analyse Recording", variant="secondary", interactive=False) | |
| tt_analysis = gr.HTML(EMPTY_ANALYSIS_HTML) | |
| gr.HTML(""" | |
| <div class="framework-tips"> | |
| <strong>π‘ Articulation Tip:</strong> Focus on pronouncing every single | |
| consonant clearly on your slow runs before ramping up the speed. | |
| </div>""") | |
| tt_shuffle.click(handlers.tt_next, | |
| inputs=[tt_level, shuffler_state], | |
| outputs=[tt_focus, tt_prompt, shuffler_state]) | |
| tt_level.change(handlers.tt_next, | |
| inputs=[tt_level, shuffler_state], | |
| outputs=[tt_focus, tt_prompt, shuffler_state]) | |
| tt_audio.change(handlers.audio_ready, inputs=tt_audio, | |
| outputs=[tt_save_btn, tt_analyse]) | |
| tt_save_btn.click(handlers.tt_count, inputs=[tt_audio, streak_store], outputs=streak_store) | |
| tt_analyse.click(handlers.tt_analyse, inputs=tt_audio, outputs=tt_analysis) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Impromptu Speech | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π€ Impromptu"): | |
| gr.HTML('<div class="desc-text">Spontaneous speaking. Structure your answer in 5 seconds and deliver a 2-minute speech.</div>') | |
| with gr.Row(): | |
| imp_cat = gr.Dropdown(IMP_CAT_NAMES, value="All", label="Category", interactive=True) | |
| imp_shuffle = gr.Button("π² New", variant="primary") | |
| imp_prompt = gr.HTML(prompt_html((data_ctrl.imp_questions("All") or [""])[0])) | |
| imp_audio = gr.Audio(label="ποΈ Record", sources=["microphone"], type="filepath") | |
| with gr.Row(): | |
| imp_save_btn = gr.Button("πΎ Count this session", variant="primary", interactive=False) | |
| imp_analyse = gr.Button("π¬ Analyse Recording", variant="secondary", interactive=False) | |
| imp_analysis = gr.HTML(EMPTY_ANALYSIS_HTML) | |
| gr.HTML(""" | |
| <div class="framework-tips"> | |
| <strong>π‘ Structure using the PRES Method:</strong> | |
| <div class="framework-steps"> | |
| <div class="framework-step active"><strong>P</strong>oint (State your position)</div> | |
| <div class="framework-step"><strong>R</strong>eason (Support it)</div> | |
| <div class="framework-step"><strong>E</strong>xample (Show a story)</div> | |
| <div class="framework-step"><strong>S</strong>ummary (So what?)</div> | |
| </div> | |
| </div>""") | |
| imp_shuffle.click(handlers.imp_next, | |
| inputs=[imp_cat, shuffler_state], | |
| outputs=[imp_prompt, shuffler_state]) | |
| imp_cat.change(handlers.imp_next, | |
| inputs=[imp_cat, shuffler_state], | |
| outputs=[imp_prompt, shuffler_state]) | |
| imp_audio.change(handlers.audio_ready, inputs=imp_audio, | |
| outputs=[imp_save_btn, imp_analyse]) | |
| imp_save_btn.click(handlers.imp_count, inputs=[imp_audio, streak_store], outputs=streak_store) | |
| imp_analyse.click(handlers.imp_analyse, inputs=imp_audio, outputs=imp_analysis) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Interview Practice | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("πΌ Interview"): | |
| gr.HTML('<div class="desc-text">Simulate professional and behavioral questions. Aim for 60β90 seconds per answer.</div>') | |
| with gr.Row(): | |
| iv_cat = gr.Dropdown(IV_CAT_NAMES, value="All", label="Category", interactive=True) | |
| iv_shuffle = gr.Button("π² New", variant="primary") | |
| iv_prompt = gr.HTML(prompt_html((data_ctrl.iv_questions("All") or [""])[0])) | |
| iv_audio = gr.Audio(label="ποΈ Record", sources=["microphone"], type="filepath") | |
| with gr.Row(): | |
| iv_save_btn = gr.Button("πΎ Count this session", variant="primary", interactive=False) | |
| iv_analyse = gr.Button("π¬ Analyse Recording", variant="secondary", interactive=False) | |
| iv_analysis = gr.HTML(EMPTY_ANALYSIS_HTML) | |
| gr.HTML(""" | |
| <div class="framework-tips"> | |
| <strong>π‘ Structure using the STAR Method:</strong> | |
| <div class="framework-steps"> | |
| <div class="framework-step active"><strong>S</strong>ituation</div> | |
| <div class="framework-step"><strong>T</strong>ask</div> | |
| <div class="framework-step"><strong>A</strong>ction</div> | |
| <div class="framework-step"><strong>R</strong>esult</div> | |
| </div> | |
| </div>""") | |
| iv_shuffle.click(handlers.iv_next, | |
| inputs=[iv_cat, shuffler_state], | |
| outputs=[iv_prompt, shuffler_state]) | |
| iv_cat.change(handlers.iv_next, | |
| inputs=[iv_cat, shuffler_state], | |
| outputs=[iv_prompt, shuffler_state]) | |
| iv_audio.change(handlers.audio_ready, inputs=iv_audio, | |
| outputs=[iv_save_btn, iv_analyse]) | |
| iv_save_btn.click(handlers.iv_count, inputs=[iv_audio, streak_store], outputs=streak_store) | |
| iv_analyse.click(handlers.iv_analyse, inputs=iv_audio, outputs=iv_analysis) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Free Talk | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("βοΈ Free Talk"): | |
| gr.HTML('<div class="desc-text">Input a custom text, select which category it counts as, and record your session.</div>') | |
| free_text = gr.Textbox( | |
| placeholder="Enter a custom prompt, an IELTS question, or any topicβ¦\n(Ctrl+Enter to apply)", | |
| label="Your prompt", | |
| lines=4, | |
| ) | |
| with gr.Row(): | |
| free_module = gr.Dropdown( | |
| FREE_MODULE_CHOICES, value="JAM", | |
| label="Save as category", interactive=True | |
| ) | |
| free_apply = gr.Button("β Use prompt", variant="primary") | |
| free_badge = gr.HTML(visible=False) | |
| free_prompt = gr.HTML(visible=False) | |
| free_audio = gr.Audio(label="ποΈ Record", sources=["microphone"], type="filepath") | |
| with gr.Row(): | |
| free_save_btn = gr.Button("πΎ Count this session", variant="primary", interactive=False) | |
| free_analyse = gr.Button("π¬ Analyse Recording", variant="secondary", interactive=False) | |
| free_analysis = gr.HTML(EMPTY_ANALYSIS_HTML) | |
| free_apply.click(handlers.free_apply, | |
| inputs=[free_text, free_module], | |
| outputs=[free_badge, free_prompt]) | |
| free_text.submit(handlers.free_apply, | |
| inputs=[free_text, free_module], | |
| outputs=[free_badge, free_prompt]) | |
| # Re-render badge if module changes while text exists | |
| free_module.change(handlers.free_apply, | |
| inputs=[free_text, free_module], | |
| outputs=[free_badge, free_prompt]) | |
| free_audio.change(handlers.audio_ready, inputs=free_audio, | |
| outputs=[free_save_btn, free_analyse]) | |
| free_save_btn.click(handlers.free_count, | |
| inputs=[free_audio, free_module, streak_store], | |
| outputs=streak_store) | |
| free_analyse.click(handlers.free_analyse, inputs=free_audio, outputs=free_analysis) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Progress β Streak Heatmap | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Progress"): | |
| streak_refresh = gr.Button("π Refresh", variant="secondary") | |
| streak_out = gr.HTML(EMPTY_STREAK_HTML) | |
| streak_refresh.click(handlers.refresh_streak, inputs=streak_store, outputs=streak_out) | |
| demo.load(handlers.refresh_streak, inputs=streak_store, outputs=streak_out) | |
| # ββ Footer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.HTML(""" | |
| <div style="text-align:center;color:#475569;font-size:.8rem;padding:16px 0 4px;border-top:1px solid #1e2130;margin-top:12px"> | |
| <a href="https://github.com/hosamation/Speak-Lab" style="color:#6366f1;text-decoration:none"> | |
| Speak Lab | |
| </a> β MIT License Β· Built with Gradio | |
| </div>""") | |
| if __name__ == "__main__": | |
| demo.launch(css=CSS, theme=gr.themes.Default(), head=HEAD_JS) | |