ameforge commited on
Commit
bbd1ded
·
verified ·
1 Parent(s): 2bee5ac

Complete redesign: voice-first shell, pure black, numbered steps, underline input, no cards; logic unchanged

Browse files
Files changed (1) hide show
  1. app.py +149 -123
app.py CHANGED
@@ -1,4 +1,4 @@
1
- import os, json, sys, numpy as np, gradio as gr, torch
2
  from huggingface_hub import snapshot_download
3
 
4
  PRIVATE = os.environ.get("MODEL_REPO", "")
@@ -8,161 +8,187 @@ import keep_audio_core as K
8
  cfg = json.load(open(os.path.join(d, "config.json"))) if os.path.exists(os.path.join(d, "config.json")) else {}
9
  R = K.Recognizer(os.path.join(d, "audio2b_KEEP_graine0.pt"), os.path.join(d, "audio2b_normalisation.pt"), gain=cfg.get("gain", "peak"))
10
 
 
 
11
  CSS = """
12
- @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600;700&family=Space+Grotesk:wght@400;500;600&display=swap');
13
- body { background: #0A0E14 !important; }
14
  .gradio-container {
15
- --body-background-fill: #0A0E14;
16
- --background-fill-primary: #0A0E14;
17
- --background-fill-secondary: #121826;
18
- --block-background-fill: #141A26;
19
- --block-border-color: rgba(255,255,255,.09);
20
- --block-label-background-fill: #141A26;
21
- --block-label-text-color: #E6EAF2;
22
- --block-title-text-color: #E6EAF2;
23
- --body-text-color: #E6EAF2;
24
- --body-text-color-subdued: #94A3B8;
25
- --button-primary-background-fill: #F2F5F9;
26
  --button-primary-background-fill-hover: #FFFFFF;
27
- --button-primary-text-color: #0B0F14;
28
- --button-secondary-background-fill: #1B2334;
29
- --button-secondary-background-fill-hover: #232E45;
30
- --button-secondary-text-color: #E6EAF2;
31
- --button-secondary-border-color: rgba(255,255,255,.14);
32
- --input-background-fill: #0F1522;
33
- --input-border-color: rgba(255,255,255,.12);
34
- --input-text-color: #E6EAF2;
35
- --input-placeholder-color: #7C8AA0;
36
- max-width: 920px !important; margin: auto !important; background: #0A0E14 !important; color: #E6EAF2 !important; font-family: 'Space Grotesk', system-ui, sans-serif !important;
 
 
37
  }
38
- .hero { background: linear-gradient(135deg, #16233C 0%, #0B1220 100%); border: 1px solid rgba(255,255,255,.09); border-radius: 22px; padding: 34px 32px 26px; color: #fff; margin-bottom: 22px; box-shadow: 0 12px 40px rgba(0,0,0,.45); }
39
- .hero .hero-top { display: flex; gap: 18px; align-items: flex-start; }
40
- .hero .hero-icon { flex: 0 0 auto; width: 56px; height: 56px; border-radius: 16px; background: #C9A227 !important; color: #0D1B2A !important; display: flex; align-items: center; justify-content: center; }
41
- .hero h1 { font-family: 'Cormorant Garamond', Georgia, serif; font-size: 46px; margin: 0 0 6px; font-weight: 700; letter-spacing: -.5px; line-height: 1.05; color: #FFFFFF !important; }
42
- .hero .tagline { font-size: 16.5px; color: rgba(230,234,242,.85) !important; margin: 0 0 16px; line-height: 1.5; }
43
- .hero .tagline b, .hero .tagline strong { color: #E5C15D !important; }
44
- .hero .pills { display: flex; flex-wrap: wrap; gap: 8px; }
45
- .hero .pill { background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.12); border-radius: 999px; padding: 6px 14px; font-size: 13.5px; color: #FFFFFF !important; }
46
- .hero .pill b { color: #E5C15D; }
47
- .gradio-container .block,
48
- .gradio-container .form,
49
- .gradio-container .wrap,
50
- .gradio-container .panel,
51
- .gradio-container .component,
52
- .gradio-container .audio,
53
- .gradio-container .plot,
54
- .gradio-container .input,
55
- .gradio-container .output {
56
- border-radius: 18px !important;
57
- overflow: hidden !important;
58
- border: 1px solid rgba(255,255,255,.09) !important;
59
- background: #141A26 !important;
60
- box-shadow: 0 4px 24px rgba(0,0,0,.35) !important;
61
  }
62
- .gradio-container .block:has(.hero),
63
- .gradio-container .block:has(.card),
64
- .gradio-container .block.footnote {
65
- background: transparent !important;
66
- border: none !important;
67
- box-shadow: none !important;
68
  }
69
- .gradio-container button { border-radius: 12px !important; }
70
- .gradio-container button.primary { background: #F2F5F9 !important; color: #0B0F14 !important; border: none !important; }
71
- .gradio-container button.primary:hover { background: #FFFFFF !important; }
72
- .gradio-container input[type="text"],
73
- .gradio-container textarea,
74
- .gradio-container input[type="number"] { border-radius: 12px !important; background: #0F1522 !important; color: #E6EAF2 !important; }
75
- .gradio-container .tab-nav { background: #141A26 !important; border: 1px solid rgba(255,255,255,.09) !important; border-radius: 14px !important; padding: 4px !important; gap: 4px !important; }
76
- .gradio-container .tab-nav button { border-radius: 10px !important; border: none !important; color: #C7CEDD !important; background: transparent !important; }
77
- .gradio-container .tab-nav button.selected { background: #F2F5F9 !important; color: #0B0F14 !important; }
78
- .gradio-container .radio label,
79
- .gradio-container [class*="radio"] label { border-radius: 999px !important; border: 1px solid rgba(255,255,255,.12) !important; padding: 6px 14px !important; background: #1B2334 !important; color: #E6EAF2 !important; }
80
- .gradio-container .radio label:has(input:checked) { border-color: #C9A227 !important; }
81
- .gradio-container label span { font-weight: 600 !important; color: #E6EAF2 !important; }
82
- .gradio-container audio { border-radius: 12px !important; }
83
- .gradio-container .prose { color: #C7CEDD !important; }
84
- .gradio-container .prose strong { color: #E6EAF2 !important; }
85
- .footnote { color: #7C8AA0; font-size: 13.5px; line-height: 1.55; margin-top: 18px; }
86
-
87
- /* Gradio 6 radio options are labels with data-testid like "peak-radio-label" (no .radio class) */
88
- .gradio-container label[data-testid$="-radio-label"] {
89
- border-radius: 999px !important;
90
- border: 1px solid rgba(255,255,255,.12) !important;
91
- padding: 6px 14px !important;
92
- background: #1B2334 !important;
93
- color: #E6EAF2 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
- .gradio-container label[data-testid$="-radio-label"].selected {
96
- border-color: #C9A227 !important;
97
- background: #232E45 !important;
 
 
 
 
 
 
98
  }
99
- .gradio-container label[data-testid$="-radio-label"] span {
100
- color: #E6EAF2 !important;
101
- background: transparent !important;
 
102
  }
 
 
 
103
  """
104
 
105
- # Simple, professional mark: a plus sign for "learning something new", drawn as SVG, not an emoji.
106
- PLUS_SVG = """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- HERO = f"""<div class="hero"><div class="hero-top"><div class="hero-icon">{PLUS_SVG}</div><div>
109
- <h1>KEEP Learn</h1>
110
- <p class="tagline">Teach KEEP words it has <b>never heard</b>, live, with no retraining: it keeps the fingerprint of your examples, then recognizes by resemblance.</p>
111
- </div></div><div class="pills">
112
- <span class="pill"><b>2–5</b> words per session</span>
113
- <span class="pill"><b>1–3</b> examples per word</span>
114
- <span class="pill">Session only — forgotten on close</span>
115
- </div></div>"""
116
 
117
- def summary(mem):
118
- if not mem: return "_No word learned yet._"
119
- return "**Learned words:** " + " · ".join(f"{w} ({len(v)} example{'s' if len(v) > 1 else ''})" for w, v in mem.items())
 
 
 
120
 
121
  def add_example(name, audio, gain_choice, mem):
122
  mem = dict(mem or {}); name = (name or "").strip().lower()
123
- if not name: return mem, summary(mem), "Type the word name first.", gr.update()
124
- if audio is None: return mem, summary(mem), "Record the word first.", gr.update()
 
 
125
  R.gain = gain_choice; sr, wav = audio
126
  mem[name] = list(mem.get(name, [])) + [R.embedding(wav, sr).tolist()]
127
  n = len(mem[name]); tip = " Add 1 or 2 more examples to make it more sure." if n < 3 else ""
128
- return mem, summary(mem), f"Example no. {n} of “{name}” memorized.{tip}", None
129
 
130
- def forget_all(): return {}, summary({}), "Everything forgotten.", None
 
131
 
132
  def recognize(audio, gain_choice, mem):
133
- if not mem or len(mem) < 2: return None, "Teach it at least **2 words** first (field above)."
134
- if audio is None: return None, "Say one of the learned words."
 
 
135
  R.gain = gain_choice; sr, wav = audio; q = R.embedding(wav, sr)
136
  names = list(mem); P = torch.nn.functional.normalize(torch.stack([torch.tensor(mem[w]).mean(0) for w in names]), dim=-1)
137
  sim = (P @ q).numpy(); p = np.exp(20 * (sim - sim.max())); p = p / p.sum()
138
  i = int(sim.argmax()); j = np.argsort(-sim)[1] if len(sim) > 1 else i
139
  gap = sim[i] - sim[j]
140
  msg = (f"I heard: **{names[i]}**." if gap > 0.05 else f"I'm hesitating… rather **{names[i]}**, but it also sounds like “{names[j]}”. Add examples.")
141
- return {w: float(x) for w, x in zip(names, p)}, msg
142
 
143
  with gr.Blocks(title="KEEP Learn", css=CSS) as demo:
144
- gr.HTML(HERO)
145
- gr.Markdown("**1.** Type a name, record the word (1 to 3 times) and click “Memorize”. Do this for 2 to 5 words.\n"
146
- "**2.** Below, say one of the learned words again: KEEP tells you which one it recognizes.")
147
- gain_radio = gr.Radio(["peak", "none"], value=cfg.get("gain", "peak"), label="Volume setting (peak = automatic, none = raw)")
148
  mem = gr.State({})
 
 
 
 
 
149
  with gr.Row():
150
- name = gr.Textbox(label="Word name", placeholder="e.g.: bonjour")
151
- ex_audio = gr.Audio(sources=["microphone", "upload"], type="numpy", label="Say this word")
152
- with gr.Row():
153
- b_add = gr.Button("Memorize this example", variant="primary"); b_forget = gr.Button("Forget everything")
154
- info = gr.Markdown(); learned = gr.Markdown(summary({}))
155
- b_add.click(add_example, [name, ex_audio, gain_radio, mem], [mem, learned, info, ex_audio])
156
- b_forget.click(forget_all, None, [mem, learned, info, ex_audio])
157
- gr.Markdown("---")
158
- with gr.Row():
159
- q_audio = gr.Audio(sources=["microphone", "upload"], type="numpy", label="Say one of the learned words again")
160
- q_lab = gr.Label(label="Resemblance to each learned word")
161
- q_msg = gr.Markdown()
162
- gr.Button("What did I say?", variant="primary").click(recognize, [q_audio, gain_radio, mem], [q_lab, q_msg])
 
163
  gr.Markdown("_Experimental: what it learns here is forgotten when you close the page. "
164
  "It works best with the same voice as the examples, and 2 to 3 examples per word._",
165
- elem_classes=["footnote"])
166
  gr.Markdown("**Limits.** It recognizes word sounds, it does not understand speech. A noisy microphone or words that sound very alike can lower accuracy.",
167
- elem_classes=["footnote"])
168
  demo.launch()
 
1
+ import os, json, sys, html, numpy as np, gradio as gr, torch
2
  from huggingface_hub import snapshot_download
3
 
4
  PRIVATE = os.environ.get("MODEL_REPO", "")
 
8
  cfg = json.load(open(os.path.join(d, "config.json"))) if os.path.exists(os.path.join(d, "config.json")) else {}
9
  R = K.Recognizer(os.path.join(d, "audio2b_KEEP_graine0.pt"), os.path.join(d, "audio2b_normalisation.pt"), gain=cfg.get("gain", "peak"))
10
 
11
+ # Voice-first shell, same language as KEEP Listen: pure black, one narrow column,
12
+ # numbered steps instead of cards, huge type, pill actions.
13
  CSS = """
14
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap');
15
+ html, body { background: #000000 !important; }
16
  .gradio-container {
17
+ --body-background-fill: #000000;
18
+ --background-fill-primary: #000000;
19
+ --background-fill-secondary: #000000;
20
+ --block-background-fill: transparent;
21
+ --block-border-color: transparent;
22
+ --block-label-background-fill: transparent;
23
+ --block-label-text-color: #8A8A93;
24
+ --block-title-text-color: #F5F5F7;
25
+ --body-text-color: #F5F5F7;
26
+ --body-text-color-subdued: #8A8A93;
27
+ --button-primary-background-fill: #F5F5F7;
28
  --button-primary-background-fill-hover: #FFFFFF;
29
+ --button-primary-text-color: #000000;
30
+ --button-secondary-background-fill: transparent;
31
+ --button-secondary-background-fill-hover: #1A1A1E;
32
+ --button-secondary-text-color: #F5F5F7;
33
+ --button-secondary-border-color: rgba(255,255,255,.22);
34
+ --input-background-fill: transparent;
35
+ --input-border-color: rgba(255,255,255,.22);
36
+ --input-text-color: #F5F5F7;
37
+ --input-placeholder-color: #6E6E73;
38
+ max-width: 560px !important; margin: 0 auto !important; padding: 26px 22px 44px !important;
39
+ background: #000000 !important; color: #F5F5F7 !important;
40
+ font-family: 'Space Grotesk', system-ui, sans-serif !important;
41
  }
42
+ .gradio-container .block, .gradio-container .form, .gradio-container .wrap,
43
+ .gradio-container .panel, .gradio-container .component, .gradio-container .audio,
44
+ .gradio-container .plot, .gradio-container .input, .gradio-container .output {
45
+ background: transparent !important; border: none !important;
46
+ box-shadow: none !important; border-radius: 0 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
+ .gradio-container label span {
49
+ color: #8A8A93 !important; font-size: 11px !important; font-weight: 500 !important;
50
+ letter-spacing: 2.5px !important; text-transform: uppercase !important;
 
 
 
51
  }
52
+ .gradio-container .prose { color: #C7C7CC !important; }
53
+ .gradio-container .prose strong { color: #F5F5F7 !important; }
54
+ .topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
55
+ .brand { font-weight: 700; font-size: 19px; letter-spacing: -.5px; }
56
+ .brand i { color: #E5C15D; font-style: normal; }
57
+ .ondevice { font-size: 12px; color: #8A8A93; border: 1px solid rgba(255,255,255,.18); border-radius: 999px; padding: 5px 12px; }
58
+ .intro h1 { font-size: 46px; font-weight: 700; letter-spacing: -2px; line-height: 1.04; margin: 30px 0 14px; }
59
+ .intro .sub { color: #8A8A93; font-size: 16px; line-height: 1.6; margin: 0; }
60
+ .intro .sub b { color: #C7C7CC; }
61
+ .step { display: flex; align-items: baseline; gap: 14px; margin: 36px 0 4px; }
62
+ .step .num { color: #E5C15D; font-weight: 700; font-size: 14px; letter-spacing: 1px; }
63
+ .step h2 { font-size: 27px; font-weight: 700; letter-spacing: -.5px; margin: 0; }
64
+ .hairline { height: 1px; background: rgba(255,255,255,.10); margin: 32px 0; }
65
+ .chips { display: flex; flex-wrap: wrap; gap: 8px; }
66
+ .chip { font-size: 14px; color: #F5F5F7; background: #141416; border-radius: 999px; padding: 9px 15px; }
67
+ .chip span { color: #8A8A93; }
68
+ .dots { display: flex; gap: 9px; align-items: center; margin: 14px 0 4px; }
69
+ .dots i { width: 11px; height: 11px; border-radius: 50%; background: #26262A; }
70
+ .dots i.on { background: #E5C15D; }
71
+ .dots span { color: #8A8A93; font-size: 13px; margin-left: 8px; }
72
+ .qstage { text-align: center; padding: 10px 0 4px; }
73
+ .qstage .eyebrow { font-size: 11px; letter-spacing: 3px; text-transform: uppercase; color: #8A8A93; margin-bottom: 12px; }
74
+ .qstage .word { font-size: 64px; font-weight: 700; letter-spacing: -2.5px; line-height: 1; margin: 0; }
75
+ .qstage .word.dim { color: #333338; }
76
+ .qstage .bars { margin-top: 20px; display: flex; flex-direction: column; gap: 8px; }
77
+ .qstage .bars .row { display: flex; align-items: center; gap: 12px; font-size: 14px; color: #C7C7CC; }
78
+ .qstage .bars .row b { width: 110px; text-align: right; font-weight: 500; color: #F5F5F7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
79
+ .qstage .bars .bar { flex: 1; height: 5px; background: #1C1C1F; border-radius: 99px; overflow: hidden; }
80
+ .qstage .bars .bar i { display: block; height: 100%; background: linear-gradient(90deg, #C9A227, #F5D98B); border-radius: 99px; }
81
+ .qstage .bars .pct { width: 46px; text-align: left; color: #8A8A93; }
82
+ .quiet { color: #8A8A93 !important; font-size: 13.5px !important; line-height: 1.65 !important; }
83
+ .quiet strong { color: #C7C7CC !important; }
84
+ .gradio-container button.primary { border-radius: 999px !important; height: 56px !important; font-size: 17px !important; font-weight: 700 !important; }
85
+ .gradio-container button:not(.primary) { border-radius: 999px !important; }
86
+ .gradio-container .audio button { border-radius: 999px !important; background: #141416 !important; border: 1px solid rgba(255,255,255,.14) !important; }
87
+ .gradio-container input[type="text"] {
88
+ background: transparent !important; border: none !important;
89
+ border-bottom: 2px solid rgba(255,255,255,.25) !important; border-radius: 0 !important;
90
+ font-size: 30px !important; font-weight: 700 !important; padding: 10px 0 !important; letter-spacing: -.5px !important;
91
  }
92
+ .gradio-container input[type="text"]:focus { border-bottom-color: #E5C15D !important; box-shadow: none !important; outline: none !important; }
93
+ /* Gradio 6 renders Textbox as a textarea inside label.container.show_textbox_border */
94
+ .gradio-container label.container.show_textbox_border { border: none !important; background: transparent !important; box-shadow: none !important; }
95
+ .gradio-container .input-container { background: transparent !important; border: none !important; }
96
+ .gradio-container textarea {
97
+ background: transparent !important; border: none !important;
98
+ border-bottom: 2px solid rgba(255,255,255,.25) !important; border-radius: 0 !important;
99
+ font-size: 30px !important; font-weight: 700 !important; padding: 10px 0 !important; letter-spacing: -.5px !important;
100
+ color: #F5F5F7 !important;
101
  }
102
+ .gradio-container textarea:focus { border-bottom-color: #E5C15D !important; box-shadow: none !important; outline: none !important; }
103
+ .gradio-container label[data-testid$="-radio-label"] {
104
+ border-radius: 999px !important; border: 1px solid rgba(255,255,255,.14) !important;
105
+ padding: 7px 15px !important; background: transparent !important; color: #8A8A93 !important;
106
  }
107
+ .gradio-container label[data-testid$="-radio-label"].selected { border-color: #E5C15D !important; color: #F5F5F7 !important; }
108
+ .gradio-container label[data-testid$="-radio-label"] span { color: inherit !important; background: transparent !important; letter-spacing: 0 !important; text-transform: none !important; font-size: 14px !important; }
109
+ .footer { opacity: .45; }
110
  """
111
 
112
+ TOPBAR = """<div class="topbar"><div class="brand">KEEP <i>•</i> Learn</div><div class="ondevice">On-device · CPU</div></div>"""
113
+ INTRO = """<div class="intro"><h1>Teach KEEP<br>a new word.</h1><p class="sub">No retraining. It keeps the <b>fingerprint</b> of your examples, then recognizes by resemblance. What you teach here lives only for this session.</p></div>"""
114
+
115
+ def learned_html(mem):
116
+ if not mem:
117
+ return '<p class="quiet" style="margin:8px 0 0">No word learned yet.</p>'
118
+ chips = "".join(
119
+ f'<span class="chip">{html.escape(w)} <span>×{len(v)}</span></span>' for w, v in mem.items())
120
+ return f'<div class="chips">{chips}</div>'
121
+
122
+ def dots_html(mem, name):
123
+ n = len((mem or {}).get((name or "").strip().lower(), []))
124
+ dots = "".join('<i class="on"></i>' if i < min(n, 3) else '<i></i>' for i in range(3))
125
+ label = "example" if n == 1 else "examples"
126
+ return f'<div class="dots">{dots}<span>{n} {label} for this word</span></div>'
127
 
128
+ def qstage_empty():
129
+ return """<div class="qstage"><div class="eyebrow">Test it</div><p class="word dim">Say it back</p></div>"""
 
 
 
 
 
 
130
 
131
+ def qstage_result(names, probs):
132
+ rows = "".join(
133
+ f'<div class="row"><b>{html.escape(w)}</b><span class="bar"><i style="width:{p * 100:.0f}%"></i></span><span class="pct">{p:.0%}</span></div>'
134
+ for w, p in zip(names, probs))
135
+ return (f"""<div class="qstage"><div class="eyebrow">KEEP thinks you said</div><p class="word">{html.escape(names[int(np.argmax(probs))])}</p>"""
136
+ f"""<div class="bars">{rows}</div></div>""")
137
 
138
  def add_example(name, audio, gain_choice, mem):
139
  mem = dict(mem or {}); name = (name or "").strip().lower()
140
+ if not name:
141
+ return mem, learned_html(mem), dots_html(mem, name), "Type the word name first.", gr.update()
142
+ if audio is None:
143
+ return mem, learned_html(mem), dots_html(mem, name), "Record the word first.", gr.update()
144
  R.gain = gain_choice; sr, wav = audio
145
  mem[name] = list(mem.get(name, [])) + [R.embedding(wav, sr).tolist()]
146
  n = len(mem[name]); tip = " Add 1 or 2 more examples to make it more sure." if n < 3 else ""
147
+ return mem, learned_html(mem), dots_html(mem, name), f"Example no. {n} of “{name}” memorized.{tip}", None
148
 
149
+ def forget_all():
150
+ return {}, learned_html({}), dots_html({}, ""), "Everything forgotten.", None
151
 
152
  def recognize(audio, gain_choice, mem):
153
+ if not mem or len(mem) < 2:
154
+ return qstage_empty(), "Teach it at least **2 words** first (steps above)."
155
+ if audio is None:
156
+ return qstage_empty(), "Say one of the learned words."
157
  R.gain = gain_choice; sr, wav = audio; q = R.embedding(wav, sr)
158
  names = list(mem); P = torch.nn.functional.normalize(torch.stack([torch.tensor(mem[w]).mean(0) for w in names]), dim=-1)
159
  sim = (P @ q).numpy(); p = np.exp(20 * (sim - sim.max())); p = p / p.sum()
160
  i = int(sim.argmax()); j = np.argsort(-sim)[1] if len(sim) > 1 else i
161
  gap = sim[i] - sim[j]
162
  msg = (f"I heard: **{names[i]}**." if gap > 0.05 else f"I'm hesitating… rather **{names[i]}**, but it also sounds like “{names[j]}”. Add examples.")
163
+ return qstage_result(names, p), msg
164
 
165
  with gr.Blocks(title="KEEP Learn", css=CSS) as demo:
166
+ gr.HTML(TOPBAR)
167
+ gr.HTML(INTRO)
 
 
168
  mem = gr.State({})
169
+ gr.HTML('<div class="step"><span class="num">01</span><h2>Name it</h2></div>')
170
+ name = gr.Textbox(label="Word name", placeholder="e.g. bonjour", show_label=False)
171
+ gr.HTML('<div class="step"><span class="num">02</span><h2>Say it</h2></div>')
172
+ ex_audio = gr.Audio(sources=["microphone", "upload"], type="numpy", label="Say this word, 1 to 3 times")
173
+ dots = gr.HTML(dots_html({}, ""))
174
  with gr.Row():
175
+ b_add = gr.Button("Memorize", variant="primary"); b_forget = gr.Button("Forget everything")
176
+ info = gr.Markdown(elem_classes=["quiet"])
177
+ learned = gr.HTML(learned_html({}))
178
+ gr.HTML('<div class="hairline"></div>')
179
+ gr.HTML('<div class="step"><span class="num">03</span><h2>Test it</h2></div>')
180
+ q_audio = gr.Audio(sources=["microphone", "upload"], type="numpy", label="Say one of the learned words again")
181
+ q_stage = gr.HTML(qstage_empty())
182
+ q_msg = gr.Markdown(elem_classes=["quiet"])
183
+ q_btn = gr.Button("What did I say?", variant="primary")
184
+ gain_radio = gr.Radio(["peak", "none"], value=cfg.get("gain", "peak"), label="Volume setting (peak = automatic, none = raw)")
185
+ b_add.click(add_example, [name, ex_audio, gain_radio, mem], [mem, learned, dots, info, ex_audio])
186
+ b_forget.click(forget_all, None, [mem, learned, dots, info, ex_audio])
187
+ q_btn.click(recognize, [q_audio, gain_radio, mem], [q_stage, q_msg])
188
+ gr.HTML('<div class="hairline"></div>')
189
  gr.Markdown("_Experimental: what it learns here is forgotten when you close the page. "
190
  "It works best with the same voice as the examples, and 2 to 3 examples per word._",
191
+ elem_classes=["quiet"])
192
  gr.Markdown("**Limits.** It recognizes word sounds, it does not understand speech. A noisy microphone or words that sound very alike can lower accuracy.",
193
+ elem_classes=["quiet"])
194
  demo.launch()