| """ |
| Verifies the app ACTUALLY LAUNCHES under the pinned gradio, and that every |
| event handler is wired with a matching signature. |
| |
| This is the check that was missing before: the previous app.py used |
| `stream_every`, which does not exist in gradio 4.44 β it would have crashed on |
| Space startup no matter how correct the rest of the code was. |
| |
| Models are stubbed, so this tests the app wiring, not the ML. |
| """ |
| import os |
| import sys |
| import types |
| import numpy as np |
|
|
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
|
|
| |
| import pipeline as _pl |
|
|
|
|
| def _fake_transcribe(self, audio, sr=16000): |
| return "duba asusuna sannan ka aika 35000 zuwa Amina" |
|
|
|
|
| def _fake_translate(self, text, src, tgt, **kw): |
| table = { |
| "duba asusuna sannan ka aika 35000 zuwa Amina": |
| "check my balance and also send 35000 to amina", |
| "2234567890": "2234567890", |
| "eh": "yes", |
| } |
| return table.get(text, text) |
|
|
|
|
| def _fake_tts(self, text): |
| return 16000, (np.sin(np.arange(8000) / 20) * 8000).astype(np.int16) |
|
|
|
|
| _pl.HausaVoiceAIPipeline.transcribe = _fake_transcribe |
| _pl.HausaVoiceAIPipeline.transcribe_partial = _fake_transcribe |
| _pl.HausaVoiceAIPipeline.translate = _fake_translate |
| _pl.HausaVoiceAIPipeline.synthesize = _fake_tts |
| _pl.HausaVoiceAIPipeline.hausa_text_to_audio = _fake_tts |
| _pl.HausaVoiceAIPipeline._load_asr = lambda self: None |
| _pl.HausaVoiceAIPipeline._load_asr_fast = lambda self: None |
| _pl.HausaVoiceAIPipeline._load_nllb = lambda self: None |
| _pl.HausaVoiceAIPipeline._load_tts = lambda self: None |
|
|
| os.environ.setdefault("SHOW_PARTIALS", "0") |
|
|
| PASS, FAIL = "\033[92mPASS\033[0m", "\033[91mFAIL\033[0m" |
| results = [] |
|
|
|
|
| def check(name, cond, detail=""): |
| results.append((name, cond)) |
| print(f" [{PASS if cond else FAIL}] {name}" + (f" β {detail}" if detail else "")) |
|
|
|
|
| print("\nββ App imports and builds under the pinned gradio ββ") |
| import gradio as gr |
| print(f" gradio {gr.__version__}") |
| import app as A |
| check("app module imported", True) |
| check("Blocks object built", isinstance(A.demo, gr.Blocks)) |
|
|
| print("\nββ Streaming API exists in this gradio version ββ") |
| import inspect |
| p = inspect.signature(gr.Audio(streaming=True).stream).parameters |
| check("Audio.stream accepts stream_every", "stream_every" in p) |
| check("gr.skip available", hasattr(gr, "skip")) |
|
|
| print("\nββ Event handlers are wired ββ") |
| fns = A.demo.fns |
| check("events registered", len(fns) > 0, f"{len(fns)} handlers") |
|
|
| print("\nββ Text handler: full compound-request conversation ββ") |
| st = None |
| audio, convo, status, st = A.on_text( |
| "duba asusuna sannan ka aika 35000 zuwa Amina", st) |
| check("turn 1 returns audio", audio is not None and audio is not gr.skip()) |
| check("turn 1 acknowledges BOTH tasks", |
| "one at a time" in convo.lower() or "balance" in convo.lower(), |
| status[:70]) |
| audio, convo, status, st = A.on_text("2234567890", st) |
| check("turn 2 delivers balance + continues transfer", |
| "balance is" in convo.lower() and "amina" in convo.lower()) |
| audio, convo, status, st = A.on_text("eh", st) |
| check("turn 3 executes the transfer", "sent to amina" in convo.lower()) |
| check("latency breakdown in status", "total" in status, status[-52:]) |
|
|
| print("\nββ Empty input does not crash ββ") |
| a, c, s, st2 = A.on_text("", None) |
| check("empty text handled", a == gr.skip() or a is None) |
| a, c, s, st2 = A.on_record(None, None) |
| check("no recording handled", a == gr.skip() or a is None) |
|
|
| print("\nββ Streaming handler with synthetic audio ββ") |
| SR = 16000 |
|
|
|
|
| def sil(ms, n=0.0005): |
| k = int(SR * ms / 1000) |
| return (np.random.randn(k) * n).astype(np.float32) |
|
|
|
|
| def sp(ms, amp=0.25): |
| k = int(SR * ms / 1000) |
| t = np.arange(k) / SR |
| s = (np.sin(2*np.pi*120*t) + 0.5*np.sin(2*np.pi*700*t) |
| + 0.3*np.sin(2*np.pi*1220*t)) |
| s = s * (0.6 + 0.4*np.sin(2*np.pi*4*t)) + np.random.randn(k)*0.01 |
| return (s / np.abs(s).max() * amp).astype(np.float32) |
|
|
|
|
| st = A.new_state() |
| stream = np.concatenate([sil(400), sp(1600), sil(1200)]) |
| step = int(SR * 0.5) |
| got_reply = False |
| n_skips = 0 |
| for i in range(0, len(stream), step): |
| out = A.on_stream((SR, stream[i:i+step]), st) |
| check_len = len(out) == 5 |
| audio_o, convo_o, status_o, partial_o, st = out |
| if audio_o == gr.skip(): |
| n_skips += 1 |
| elif audio_o is not None: |
| got_reply = True |
| check("stream handler returns 5 values", check_len) |
| check("VAD endpointed and produced a reply", got_reply) |
| check("idle ticks return gr.skip (no player restart)", n_skips > 0, |
| f"{n_skips} skipped ticks") |
|
|
| print("\nββ Reset ββ") |
| r = A.reset(st) |
| check("reset returns 5 values", len(r) == 5) |
| check("history cleared", "Press" in r[1] or "empty" in r[1]) |
|
|
| print("\nββ Launch smoke test (real server, then shut down) ββ") |
| try: |
| A.demo.queue(max_size=4) |
| _, url, _ = A.demo.launch(prevent_thread_lock=True, quiet=True, |
| server_port=7899, show_api=False) |
| check("Gradio server started", True, url or "local") |
| import urllib.request |
| code = urllib.request.urlopen("http://127.0.0.1:7899/", timeout=15).getcode() |
| check("HTTP 200 from the app root", code == 200, f"status {code}") |
| A.demo.close() |
| except Exception as e: |
| check("Gradio server started", False, str(e)[:110]) |
|
|
| passed = sum(1 for _, ok in results if ok) |
| print(f"\n{'='*62}\n {passed}/{len(results)} checks passed\n{'='*62}") |
| sys.exit(0 if passed == len(results) else 1) |
|
|