Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -90,6 +90,66 @@ _LEGACY_DIST_MAP = {
|
|
| 90 |
"h1_h2_db": "h1_h2_corrected",
|
| 91 |
}
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
DEFAULT_SR = 22050
|
| 94 |
FRAME_LEN = 0.040
|
| 95 |
HOP_LEN = 0.010
|
|
@@ -506,7 +566,9 @@ class Metrics:
|
|
| 506 |
f2_minus_f1_hz: float
|
| 507 |
formant_dispersion_hz: float # (F3 - F1) / 2
|
| 508 |
vtl_cm: float # estimated vocal tract length
|
|
|
|
| 509 |
resonance_combo: float # z-scored composite of F2-F1 + Df
|
|
|
|
| 510 |
# Voice quality / weight
|
| 511 |
h1_h2_db: float # uncorrected H1-H2
|
| 512 |
h1_h2_corrected_db: float # H1*-H2* with Iseli-Alwan formant correction
|
|
@@ -525,7 +587,7 @@ def empty_metrics(warnings=None):
|
|
| 525 |
nan, nan, nan, nan, # pitch
|
| 526 |
nan, nan, nan, # prosody
|
| 527 |
nan, nan, nan, # volume
|
| 528 |
-
nan, nan, nan, nan, nan, nan, nan, # formants/resonance
|
| 529 |
nan, nan, nan, nan, nan, nan, # voice quality
|
| 530 |
0.0, warnings or [], # meta
|
| 531 |
)
|
|
@@ -822,6 +884,52 @@ def extract_distributions(y, sr, pitch_floor_hz, pitch_ceil_hz, max_formant_hz):
|
|
| 822 |
if z_parts:
|
| 823 |
resonance_combo = float(np.mean(z_parts))
|
| 824 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 825 |
# ββ 9. H1*-H2* with Iseli-Alwan formant correction ββ
|
| 826 |
h1h2_raw_vals = []
|
| 827 |
h1h2_corrected_vals = []
|
|
@@ -961,7 +1069,9 @@ def extract_distributions(y, sr, pitch_floor_hz, pitch_ceil_hz, max_formant_hz):
|
|
| 961 |
f2_minus_f1_hz=f2_minus_f1_med,
|
| 962 |
formant_dispersion_hz=formant_disp_med,
|
| 963 |
vtl_cm=vtl_cm,
|
|
|
|
| 964 |
resonance_combo=resonance_combo,
|
|
|
|
| 965 |
h1_h2_db=h1_h2_med,
|
| 966 |
h1_h2_corrected_db=h1_h2_corrected_med,
|
| 967 |
cpp_db=cpp_db_val,
|
|
@@ -1025,13 +1135,26 @@ def select_dists(mapping_mode, baseline_state):
|
|
| 1025 |
return GLOBAL_DISTS
|
| 1026 |
|
| 1027 |
|
| 1028 |
-
def metrics_to_sliders(m, dists):
|
| 1029 |
pitch_p = dist_to_percentile(m.f0_med_hz, dists["f0_med_hz"])
|
| 1030 |
prosody_p = dist_to_percentile(m.prosody_composite, dists["f0_sd_st"])
|
| 1031 |
volume_p = dist_to_percentile(m.rms_dbfs, dists["rms_dbfs"])
|
| 1032 |
-
resonance_p = dist_to_percentile(m.resonance_combo, dists["resonance_combo"])
|
| 1033 |
weight_p = dist_to_percentile(m.h1_h2_corrected_db, dists["h1_h2_corrected"])
|
| 1034 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
# Volume: higher RMS -> louder -> 100 should be Soft -> invert.
|
| 1036 |
if np.isfinite(volume_p):
|
| 1037 |
volume_p = 1.0 - volume_p
|
|
@@ -1074,8 +1197,10 @@ def format_report(m):
|
|
| 1074 |
" F2 (median): {}".format(_fmt(m.f2_hz, '.0f', ' Hz')),
|
| 1075 |
" F3 (median): {}".format(_fmt(m.f3_hz, '.0f', ' Hz')),
|
| 1076 |
" F2 β F1: {}".format(_fmt(m.f2_minus_f1_hz, '.0f', ' Hz')),
|
|
|
|
| 1077 |
" Formant dispersion: {}".format(_fmt(m.formant_dispersion_hz, '.0f', ' Hz')),
|
| 1078 |
" Est. VTL: {}".format(_fmt(m.vtl_cm, '.1f', ' cm')),
|
|
|
|
| 1079 |
" Resonance composite:{}".format(_fmt(m.resonance_combo, '.2f', ' z')),
|
| 1080 |
"",
|
| 1081 |
"ββ Voice Quality / Weight ββ",
|
|
@@ -1176,8 +1301,10 @@ def history_row(label, m, sliders):
|
|
| 1176 |
"clipping_ratio": m.clipping_ratio,
|
| 1177 |
"f1_hz": m.f1_hz, "f2_hz": m.f2_hz, "f3_hz": m.f3_hz,
|
| 1178 |
"f2_minus_f1_hz": m.f2_minus_f1_hz,
|
|
|
|
| 1179 |
"formant_dispersion_hz": m.formant_dispersion_hz,
|
| 1180 |
"vtl_cm": m.vtl_cm,
|
|
|
|
| 1181 |
"resonance_combo": m.resonance_combo,
|
| 1182 |
"h1_h2_db": m.h1_h2_db,
|
| 1183 |
"h1_h2_corrected_db": m.h1_h2_corrected_db,
|
|
@@ -1210,13 +1337,14 @@ ANALYSIS_CACHE = {}
|
|
| 1210 |
_CACHE_LOCK = threading.Lock()
|
| 1211 |
|
| 1212 |
|
| 1213 |
-
def cached_analyze(y, sr, pf, pc, mf, cache_key, dists):
|
| 1214 |
-
|
|
|
|
| 1215 |
with _CACHE_LOCK:
|
| 1216 |
if k in ANALYSIS_CACHE:
|
| 1217 |
return ANALYSIS_CACHE[k]
|
| 1218 |
m = extract_metrics(y, sr, pf, pc, mf)
|
| 1219 |
-
sliders = metrics_to_sliders(m, dists)
|
| 1220 |
with _CACHE_LOCK:
|
| 1221 |
ANALYSIS_CACHE[k] = (m, sliders)
|
| 1222 |
if len(ANALYSIS_CACHE) > 128:
|
|
@@ -1306,6 +1434,99 @@ def _prep_audio(audio_like, window_choice, pitch_floor, pitch_ceil,
|
|
| 1306 |
return y, sr, pf, pc, mf
|
| 1307 |
|
| 1308 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1309 |
# =============================================================================
|
| 1310 |
# Coaching tips
|
| 1311 |
# =============================================================================
|
|
@@ -1848,21 +2069,25 @@ def clear_target(history_state):
|
|
| 1848 |
return None, history_state, "", "", z, z, z, z, z, render_slider_bars(None, None, context="target")
|
| 1849 |
|
| 1850 |
def set_target(audio_like, window_choice, pitch_floor, pitch_ceil, max_formant,
|
| 1851 |
-
use_preemph, use_loudnorm, mapping_mode,
|
|
|
|
| 1852 |
z = gr.update(value=0)
|
| 1853 |
empty_bars = render_slider_bars(None, None, context="target")
|
| 1854 |
try:
|
| 1855 |
y, sr, pf, pc, mf = _prep_audio(audio_like, window_choice, pitch_floor,
|
| 1856 |
pitch_ceil, max_formant, use_preemph, use_loudnorm)
|
| 1857 |
except ValueError as e:
|
| 1858 |
-
return None, history_state, str(e), "", z, z, z, z, z, empty_bars
|
| 1859 |
|
| 1860 |
dists = select_dists(mapping_mode, baseline_state)
|
|
|
|
| 1861 |
cache_key = "target-{}-{}-{}-{}".format(window_choice, use_preemph, use_loudnorm, mapping_mode)
|
| 1862 |
-
m, sliders = cached_analyze(y, sr, pf, pc, mf, cache_key=cache_key, dists=dists
|
|
|
|
| 1863 |
target_state = {"metrics": asdict(m), "sliders": sliders}
|
| 1864 |
history_state = cap_history((history_state or []) + [history_row("Target", m, sliders)])
|
| 1865 |
bar_html = render_slider_bars(sliders, None, context="target")
|
|
|
|
| 1866 |
return (
|
| 1867 |
target_state, history_state,
|
| 1868 |
format_report(m), format_sliders(sliders),
|
|
@@ -1872,10 +2097,12 @@ def set_target(audio_like, window_choice, pitch_floor, pitch_ceil, max_formant,
|
|
| 1872 |
gr.update(value=safe(sliders["Prosody"])),
|
| 1873 |
gr.update(value=safe(sliders["Volume"])),
|
| 1874 |
bar_html,
|
|
|
|
| 1875 |
)
|
| 1876 |
|
| 1877 |
def analyze_user(audio_like, label, window_choice, pitch_floor, pitch_ceil,
|
| 1878 |
max_formant, use_preemph, use_loudnorm, mapping_mode,
|
|
|
|
| 1879 |
baseline_state, target_state, history_state):
|
| 1880 |
z = gr.update(value=0)
|
| 1881 |
z0 = gr.update(value=0.0)
|
|
@@ -1888,7 +2115,7 @@ def analyze_user(audio_like, label, window_choice, pitch_floor, pitch_ceil,
|
|
| 1888 |
None, None, None,
|
| 1889 |
[],
|
| 1890 |
render_slider_bars((target_state or {}).get("sliders"), None, context="user"),
|
| 1891 |
-
"", "", "",
|
| 1892 |
)
|
| 1893 |
try:
|
| 1894 |
y, sr, pf, pc, mf = _prep_audio(audio_like, window_choice, pitch_floor,
|
|
@@ -1896,10 +2123,12 @@ def analyze_user(audio_like, label, window_choice, pitch_floor, pitch_ceil,
|
|
| 1896 |
except ValueError:
|
| 1897 |
return empty_out
|
| 1898 |
|
|
|
|
| 1899 |
dists = select_dists(mapping_mode, baseline_state)
|
| 1900 |
cache_key = "user-{}-{}-{}-{}-{}".format(label, window_choice, use_preemph,
|
| 1901 |
use_loudnorm, mapping_mode)
|
| 1902 |
-
m, sliders = cached_analyze(y, sr, pf, pc, mf, cache_key=cache_key, dists=dists
|
|
|
|
| 1903 |
|
| 1904 |
warnings = list(m.warnings)
|
| 1905 |
append_gain_warning(target_state, sliders, warnings)
|
|
@@ -1920,6 +2149,7 @@ def analyze_user(audio_like, label, window_choice, pitch_floor, pitch_ceil,
|
|
| 1920 |
radar_html = render_radar_chart(t_sliders, sliders)
|
| 1921 |
trend_html = render_trend_plot(history_state)
|
| 1922 |
coaching = generate_coaching(delta, m)
|
|
|
|
| 1923 |
|
| 1924 |
return (
|
| 1925 |
history_state,
|
|
@@ -1943,10 +2173,12 @@ def analyze_user(audio_like, label, window_choice, pitch_floor, pitch_ceil,
|
|
| 1943 |
radar_html,
|
| 1944 |
trend_html,
|
| 1945 |
coaching,
|
|
|
|
| 1946 |
)
|
| 1947 |
|
| 1948 |
def live_analyze(stream_like, buffer_state, pitch_floor, pitch_ceil, max_formant,
|
| 1949 |
-
use_preemph, use_loudnorm, mapping_mode,
|
|
|
|
| 1950 |
no_change = (buffer_state, *(gr.update() for _ in range(16)))
|
| 1951 |
|
| 1952 |
normalized = normalize_audio_tuple(stream_like)
|
|
@@ -1988,9 +2220,10 @@ def live_analyze(stream_like, buffer_state, pitch_floor, pitch_ceil, max_formant
|
|
| 1988 |
pf = min(pf, pc - 1)
|
| 1989 |
mf = int(np.clip(int(round(max_formant)), 4500, 6500))
|
| 1990 |
|
|
|
|
| 1991 |
m = extract_metrics(seg, sr, pf, pc, mf)
|
| 1992 |
dists = select_dists(mapping_mode, baseline_state)
|
| 1993 |
-
sliders = metrics_to_sliders(m, dists)
|
| 1994 |
|
| 1995 |
warnings = list(m.warnings)
|
| 1996 |
append_gain_warning(target_state, sliders, warnings)
|
|
@@ -2122,6 +2355,11 @@ with gr.Blocks(**_blocks_kw) as demo:
|
|
| 2122 |
mapping_mode = gr.Dropdown(
|
| 2123 |
["Global", "Anchored (literature)", "Baseline (personal)"],
|
| 2124 |
value="Global", label="Mapping mode")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2125 |
with gr.Row():
|
| 2126 |
pitch_floor = gr.Slider(40, 150, value=PITCH_FLOOR_DEFAULT, step=1, label="Pitch floor (Hz)")
|
| 2127 |
pitch_ceil = gr.Slider(250, 800, value=PITCH_CEIL_DEFAULT, step=1, label="Pitch ceiling (Hz)")
|
|
@@ -2174,6 +2412,11 @@ with gr.Blocks(**_blocks_kw) as demo:
|
|
| 2174 |
elem_classes=["report-box"])
|
| 2175 |
target_slider_text = gr.Textbox(label="Slider values", lines=4,
|
| 2176 |
elem_classes=["report-box"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2177 |
|
| 2178 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2179 |
# TAB 2: YOUR VOICE
|
|
@@ -2210,6 +2453,12 @@ with gr.Blocks(**_blocks_kw) as demo:
|
|
| 2210 |
radar_html = gr.HTML(value="", label="Radar comparison")
|
| 2211 |
trend_html = gr.HTML(value="", label="Session trend")
|
| 2212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2213 |
with gr.Accordion("Live mode", open=False):
|
| 2214 |
gr.Markdown("Live mode streams from your microphone with EMA smoothing.")
|
| 2215 |
live_audio = gr.Audio(sources=["microphone"], type="numpy",
|
|
@@ -2275,9 +2524,11 @@ with gr.Blocks(**_blocks_kw) as demo:
|
|
| 2275 |
btn_clear_baseline.click(clear_baseline, [], baseline_outputs)
|
| 2276 |
|
| 2277 |
target_inputs = [target_audio, analysis_window, pitch_floor, pitch_ceil, max_formant,
|
| 2278 |
-
use_preemph, use_loudnorm, mapping_mode,
|
|
|
|
| 2279 |
target_outputs = [target_state, history_state, target_report, target_slider_text,
|
| 2280 |
-
t_pitch, t_weight, t_res, t_pros, t_vol, target_bars
|
|
|
|
| 2281 |
btn_set_target.click(set_target, target_inputs, target_outputs)
|
| 2282 |
btn_clear_target.click(clear_target, [history_state],
|
| 2283 |
[target_state, history_state, target_report, target_slider_text,
|
|
@@ -2290,9 +2541,11 @@ with gr.Blocks(**_blocks_kw) as demo:
|
|
| 2290 |
user_report, user_slider_text, delta_text,
|
| 2291 |
export_json, export_csv, export_baseline,
|
| 2292 |
history_table,
|
| 2293 |
-
user_bars, radar_html, trend_html, coaching_md
|
|
|
|
| 2294 |
user_common_inputs = [analysis_window, pitch_floor, pitch_ceil, max_formant,
|
| 2295 |
-
use_preemph, use_loudnorm, mapping_mode,
|
|
|
|
| 2296 |
target_state, history_state]
|
| 2297 |
btn_analyze_mic.click(analyze_user, [user_audio_mic, label_recorded] + user_common_inputs, user_outputs)
|
| 2298 |
btn_analyze_upload.click(analyze_user, [user_audio_up, label_uploaded] + user_common_inputs, user_outputs)
|
|
@@ -2305,8 +2558,9 @@ with gr.Blocks(**_blocks_kw) as demo:
|
|
| 2305 |
user_bars, coaching_md]
|
| 2306 |
live_audio.stream(live_analyze,
|
| 2307 |
[live_audio, live_buffer_state, pitch_floor, pitch_ceil, max_formant,
|
| 2308 |
-
use_preemph, use_loudnorm, mapping_mode,
|
|
|
|
| 2309 |
live_outputs)
|
| 2310 |
btn_reset_live.click(reset_live, [], [live_buffer_state])
|
| 2311 |
|
| 2312 |
-
demo.queue(max_size=32).launch(**_launch_kw)
|
|
|
|
| 90 |
"h1_h2_db": "h1_h2_corrected",
|
| 91 |
}
|
| 92 |
|
| 93 |
+
# ββ Resonance method definitions ββ
|
| 94 |
+
# Each method maps a Metrics field to a distribution and polarity.
|
| 95 |
+
# "field": attribute name on the Metrics dataclass
|
| 96 |
+
# "dist": distribution dict (mu/sigma/lo/hi) for percentile mapping
|
| 97 |
+
# "invert": if True, lower raw value β higher (brighter) slider position
|
| 98 |
+
# "unit": display unit for the report
|
| 99 |
+
# "source": brief literature reference
|
| 100 |
+
RESONANCE_METHODS = {
|
| 101 |
+
"F2 β F1 (Hz)": {
|
| 102 |
+
"field": "f2_minus_f1_hz",
|
| 103 |
+
"dist": {"mu": 900.0, "sigma": 200.0, "lo": 300.0, "hi": 1600.0},
|
| 104 |
+
"invert": False, # higher = brighter
|
| 105 |
+
"unit": "Hz",
|
| 106 |
+
"source": "Hillenbrand 1995 β cis-M β 780, cis-F β 1020",
|
| 107 |
+
},
|
| 108 |
+
"Formant Dispersion Df": {
|
| 109 |
+
"field": "formant_dispersion_hz",
|
| 110 |
+
"dist": {"mu": 1100.0, "sigma": 150.0, "lo": 700.0, "hi": 1500.0},
|
| 111 |
+
"invert": False, # higher spacing = shorter VTL = brighter
|
| 112 |
+
"unit": "Hz",
|
| 113 |
+
"source": "Fitch 1997 β cis-M β 1050, cis-F β 1150",
|
| 114 |
+
},
|
| 115 |
+
"Est. VTL (cm)": {
|
| 116 |
+
"field": "vtl_cm",
|
| 117 |
+
"dist": {"mu": 16.0, "sigma": 1.8, "lo": 10.0, "hi": 22.0},
|
| 118 |
+
"invert": True, # shorter VTL = brighter β invert
|
| 119 |
+
"unit": "cm",
|
| 120 |
+
"source": "Fitch 1997; VTL = c / (4 Γ Df)",
|
| 121 |
+
},
|
| 122 |
+
"F2/F1 Ratio": {
|
| 123 |
+
"field": "f2_f1_ratio",
|
| 124 |
+
"dist": {"mu": 2.0, "sigma": 0.5, "lo": 1.0, "hi": 4.0},
|
| 125 |
+
"invert": False, # higher ratio = brighter
|
| 126 |
+
"unit": "",
|
| 127 |
+
"source": "Derived; normalizes vowel-height confound",
|
| 128 |
+
},
|
| 129 |
+
"Composite Z (default)": {
|
| 130 |
+
"field": "resonance_combo",
|
| 131 |
+
"dist": {"mu": 0.0, "sigma": 1.0, "lo": -3.0, "hi": 3.0},
|
| 132 |
+
"invert": False, # z-score: positive = brighter
|
| 133 |
+
"unit": "z",
|
| 134 |
+
"source": "Z-scored mean of F2βF1 + Df",
|
| 135 |
+
},
|
| 136 |
+
"Spectral Mean (Hz)": {
|
| 137 |
+
"field": "spectral_mean_hz",
|
| 138 |
+
"dist": {"mu": 1400.0, "sigma": 350.0, "lo": 600.0, "hi": 3000.0},
|
| 139 |
+
"invert": False, # higher CoG = brighter
|
| 140 |
+
"unit": "Hz",
|
| 141 |
+
"source": "Power-spectrum center of gravity above F0; formant-free",
|
| 142 |
+
},
|
| 143 |
+
"F3 (Hz)": {
|
| 144 |
+
"field": "f3_hz",
|
| 145 |
+
"dist": {"mu": 2800.0, "sigma": 250.0, "lo": 2000.0, "hi": 3800.0},
|
| 146 |
+
"invert": False, # higher F3 = shorter pharynx = brighter
|
| 147 |
+
"unit": "Hz",
|
| 148 |
+
"source": "Direct pharyngeal-space cue; Vorperian & Kent 2007",
|
| 149 |
+
},
|
| 150 |
+
}
|
| 151 |
+
RESONANCE_METHOD_DEFAULT = "Composite Z (default)"
|
| 152 |
+
|
| 153 |
DEFAULT_SR = 22050
|
| 154 |
FRAME_LEN = 0.040
|
| 155 |
HOP_LEN = 0.010
|
|
|
|
| 566 |
f2_minus_f1_hz: float
|
| 567 |
formant_dispersion_hz: float # (F3 - F1) / 2
|
| 568 |
vtl_cm: float # estimated vocal tract length
|
| 569 |
+
f2_f1_ratio: float # F2/F1, dimensionless resonance ratio
|
| 570 |
resonance_combo: float # z-scored composite of F2-F1 + Df
|
| 571 |
+
spectral_mean_hz: float # center of gravity of power spectrum above F0
|
| 572 |
# Voice quality / weight
|
| 573 |
h1_h2_db: float # uncorrected H1-H2
|
| 574 |
h1_h2_corrected_db: float # H1*-H2* with Iseli-Alwan formant correction
|
|
|
|
| 587 |
nan, nan, nan, nan, # pitch
|
| 588 |
nan, nan, nan, # prosody
|
| 589 |
nan, nan, nan, # volume
|
| 590 |
+
nan, nan, nan, nan, nan, nan, nan, nan, nan, # formants/resonance (9)
|
| 591 |
nan, nan, nan, nan, nan, nan, # voice quality
|
| 592 |
0.0, warnings or [], # meta
|
| 593 |
)
|
|
|
|
| 884 |
if z_parts:
|
| 885 |
resonance_combo = float(np.mean(z_parts))
|
| 886 |
|
| 887 |
+
# ββ F2/F1 ratio (dimensionless resonance) ββ
|
| 888 |
+
f2_f1_ratio = float("nan")
|
| 889 |
+
if f1_vals and f2_vals:
|
| 890 |
+
f1_arr_r = np.array(f1_vals, dtype=np.float32)
|
| 891 |
+
f2_arr_r = np.array(f2_vals, dtype=np.float32)
|
| 892 |
+
# Avoid division by zero; exclude extreme F1 values
|
| 893 |
+
valid = f1_arr_r > 200
|
| 894 |
+
if valid.sum() > 3:
|
| 895 |
+
ratios = f2_arr_r[valid] / f1_arr_r[valid]
|
| 896 |
+
ratios = robust_clip(ratios)
|
| 897 |
+
f2_f1_ratio = float(np.median(ratios))
|
| 898 |
+
|
| 899 |
+
# ββ Spectral mean / center of gravity above F0 (formant-free resonance) ββ
|
| 900 |
+
spectral_mean_hz = float("nan")
|
| 901 |
+
if np.isfinite(f0_med) and y.size >= frame:
|
| 902 |
+
# Compute average power spectrum across voiced frames, then find CoG
|
| 903 |
+
n_fft_cog = 4096
|
| 904 |
+
freqs_cog = np.fft.rfftfreq(n_fft_cog, d=1.0 / sr)
|
| 905 |
+
window_cog = np.hanning(frame).astype(np.float32)
|
| 906 |
+
sum_power = np.zeros(n_fft_cog // 2 + 1, dtype=np.float64)
|
| 907 |
+
n_frames_cog = 0
|
| 908 |
+
centers_cog = (times * sr).astype(int)
|
| 909 |
+
half_cog = frame // 2
|
| 910 |
+
for i, (c, v) in enumerate(zip(centers_cog, voiced)):
|
| 911 |
+
if not v:
|
| 912 |
+
continue
|
| 913 |
+
s0 = max(0, c - half_cog)
|
| 914 |
+
s1 = s0 + frame
|
| 915 |
+
if s1 > y.size:
|
| 916 |
+
continue
|
| 917 |
+
seg = y[s0:s1] * window_cog
|
| 918 |
+
power = np.abs(np.fft.rfft(seg, n=n_fft_cog)) ** 2
|
| 919 |
+
sum_power += power
|
| 920 |
+
n_frames_cog += 1
|
| 921 |
+
if n_frames_cog > 5:
|
| 922 |
+
avg_power = sum_power / n_frames_cog
|
| 923 |
+
# Mask below 1.5 Γ F0 to exclude fundamental and first harmonic
|
| 924 |
+
f_cutoff = 1.5 * f0_med
|
| 925 |
+
mask_above_f0 = freqs_cog >= f_cutoff
|
| 926 |
+
if mask_above_f0.sum() > 10:
|
| 927 |
+
p_above = avg_power[mask_above_f0]
|
| 928 |
+
f_above = freqs_cog[mask_above_f0]
|
| 929 |
+
total = p_above.sum()
|
| 930 |
+
if total > 0:
|
| 931 |
+
spectral_mean_hz = float(np.sum(f_above * p_above) / total)
|
| 932 |
+
|
| 933 |
# ββ 9. H1*-H2* with Iseli-Alwan formant correction ββ
|
| 934 |
h1h2_raw_vals = []
|
| 935 |
h1h2_corrected_vals = []
|
|
|
|
| 1069 |
f2_minus_f1_hz=f2_minus_f1_med,
|
| 1070 |
formant_dispersion_hz=formant_disp_med,
|
| 1071 |
vtl_cm=vtl_cm,
|
| 1072 |
+
f2_f1_ratio=f2_f1_ratio,
|
| 1073 |
resonance_combo=resonance_combo,
|
| 1074 |
+
spectral_mean_hz=spectral_mean_hz,
|
| 1075 |
h1_h2_db=h1_h2_med,
|
| 1076 |
h1_h2_corrected_db=h1_h2_corrected_med,
|
| 1077 |
cpp_db=cpp_db_val,
|
|
|
|
| 1135 |
return GLOBAL_DISTS
|
| 1136 |
|
| 1137 |
|
| 1138 |
+
def metrics_to_sliders(m, dists, resonance_method=None):
|
| 1139 |
pitch_p = dist_to_percentile(m.f0_med_hz, dists["f0_med_hz"])
|
| 1140 |
prosody_p = dist_to_percentile(m.prosody_composite, dists["f0_sd_st"])
|
| 1141 |
volume_p = dist_to_percentile(m.rms_dbfs, dists["rms_dbfs"])
|
|
|
|
| 1142 |
weight_p = dist_to_percentile(m.h1_h2_corrected_db, dists["h1_h2_corrected"])
|
| 1143 |
+
|
| 1144 |
+
# Resonance: use selected method's field and distribution
|
| 1145 |
+
method_name = resonance_method or RESONANCE_METHOD_DEFAULT
|
| 1146 |
+
method_cfg = RESONANCE_METHODS.get(method_name, RESONANCE_METHODS[RESONANCE_METHOD_DEFAULT])
|
| 1147 |
+
res_field = method_cfg["field"]
|
| 1148 |
+
res_val = getattr(m, res_field, float("nan"))
|
| 1149 |
+
# Use baseline dist for resonance_combo key if available, else method-specific dist
|
| 1150 |
+
res_dist = dists.get("resonance_combo", method_cfg["dist"])
|
| 1151 |
+
# If mapping mode is Global/Anchored, always use the method-specific dist
|
| 1152 |
+
if res_dist is dists.get("resonance_combo") and method_name != RESONANCE_METHOD_DEFAULT:
|
| 1153 |
+
res_dist = method_cfg["dist"]
|
| 1154 |
+
resonance_p = dist_to_percentile(res_val, res_dist)
|
| 1155 |
+
if method_cfg.get("invert") and np.isfinite(resonance_p):
|
| 1156 |
+
resonance_p = 1.0 - resonance_p
|
| 1157 |
+
|
| 1158 |
# Volume: higher RMS -> louder -> 100 should be Soft -> invert.
|
| 1159 |
if np.isfinite(volume_p):
|
| 1160 |
volume_p = 1.0 - volume_p
|
|
|
|
| 1197 |
" F2 (median): {}".format(_fmt(m.f2_hz, '.0f', ' Hz')),
|
| 1198 |
" F3 (median): {}".format(_fmt(m.f3_hz, '.0f', ' Hz')),
|
| 1199 |
" F2 β F1: {}".format(_fmt(m.f2_minus_f1_hz, '.0f', ' Hz')),
|
| 1200 |
+
" F2/F1 ratio: {}".format(_fmt(m.f2_f1_ratio, '.2f')),
|
| 1201 |
" Formant dispersion: {}".format(_fmt(m.formant_dispersion_hz, '.0f', ' Hz')),
|
| 1202 |
" Est. VTL: {}".format(_fmt(m.vtl_cm, '.1f', ' cm')),
|
| 1203 |
+
" Spectral mean: {}".format(_fmt(m.spectral_mean_hz, '.0f', ' Hz')),
|
| 1204 |
" Resonance composite:{}".format(_fmt(m.resonance_combo, '.2f', ' z')),
|
| 1205 |
"",
|
| 1206 |
"ββ Voice Quality / Weight ββ",
|
|
|
|
| 1301 |
"clipping_ratio": m.clipping_ratio,
|
| 1302 |
"f1_hz": m.f1_hz, "f2_hz": m.f2_hz, "f3_hz": m.f3_hz,
|
| 1303 |
"f2_minus_f1_hz": m.f2_minus_f1_hz,
|
| 1304 |
+
"f2_f1_ratio": m.f2_f1_ratio,
|
| 1305 |
"formant_dispersion_hz": m.formant_dispersion_hz,
|
| 1306 |
"vtl_cm": m.vtl_cm,
|
| 1307 |
+
"spectral_mean_hz": m.spectral_mean_hz,
|
| 1308 |
"resonance_combo": m.resonance_combo,
|
| 1309 |
"h1_h2_db": m.h1_h2_db,
|
| 1310 |
"h1_h2_corrected_db": m.h1_h2_corrected_db,
|
|
|
|
| 1337 |
_CACHE_LOCK = threading.Lock()
|
| 1338 |
|
| 1339 |
|
| 1340 |
+
def cached_analyze(y, sr, pf, pc, mf, cache_key, dists, resonance_method=None):
|
| 1341 |
+
rm = resonance_method or RESONANCE_METHOD_DEFAULT
|
| 1342 |
+
k = audio_hash(y, sr, extra="{}-{}-{}-{}-rm:{}".format(pf, pc, mf, cache_key, rm))
|
| 1343 |
with _CACHE_LOCK:
|
| 1344 |
if k in ANALYSIS_CACHE:
|
| 1345 |
return ANALYSIS_CACHE[k]
|
| 1346 |
m = extract_metrics(y, sr, pf, pc, mf)
|
| 1347 |
+
sliders = metrics_to_sliders(m, dists, resonance_method=rm)
|
| 1348 |
with _CACHE_LOCK:
|
| 1349 |
ANALYSIS_CACHE[k] = (m, sliders)
|
| 1350 |
if len(ANALYSIS_CACHE) > 128:
|
|
|
|
| 1434 |
return y, sr, pf, pc, mf
|
| 1435 |
|
| 1436 |
|
| 1437 |
+
# =============================================================================
|
| 1438 |
+
# Resonance method comparison
|
| 1439 |
+
# =============================================================================
|
| 1440 |
+
|
| 1441 |
+
def resonance_comparison_table(m, dists, active_method=None):
|
| 1442 |
+
"""Generate an HTML comparison table showing all 7 resonance methods side-by-side.
|
| 1443 |
+
|
| 1444 |
+
For each method, shows: raw value, unit, slider position (0-100), and
|
| 1445 |
+
whether it's the currently active method.
|
| 1446 |
+
"""
|
| 1447 |
+
active = active_method or RESONANCE_METHOD_DEFAULT
|
| 1448 |
+
rows = []
|
| 1449 |
+
for name, cfg in RESONANCE_METHODS.items():
|
| 1450 |
+
field = cfg["field"]
|
| 1451 |
+
raw_val = getattr(m, field, float("nan"))
|
| 1452 |
+
unit = cfg["unit"]
|
| 1453 |
+
is_active = (name == active)
|
| 1454 |
+
|
| 1455 |
+
# Compute slider position using method-specific dist
|
| 1456 |
+
d = cfg["dist"]
|
| 1457 |
+
pct = dist_to_percentile(raw_val, d)
|
| 1458 |
+
if cfg.get("invert") and np.isfinite(pct):
|
| 1459 |
+
pct = 1.0 - pct
|
| 1460 |
+
slider_val = pct_to_slider(pct) if np.isfinite(pct) else float("nan")
|
| 1461 |
+
|
| 1462 |
+
# Format values
|
| 1463 |
+
if np.isfinite(raw_val):
|
| 1464 |
+
if unit == "cm":
|
| 1465 |
+
raw_str = "{:.1f}".format(raw_val)
|
| 1466 |
+
elif unit == "z":
|
| 1467 |
+
raw_str = "{:+.2f}".format(raw_val)
|
| 1468 |
+
elif unit == "":
|
| 1469 |
+
raw_str = "{:.2f}".format(raw_val)
|
| 1470 |
+
else:
|
| 1471 |
+
raw_str = "{:.0f}".format(raw_val)
|
| 1472 |
+
else:
|
| 1473 |
+
raw_str = "β"
|
| 1474 |
+
|
| 1475 |
+
slider_str = "{:.0f}".format(slider_val) if np.isfinite(slider_val) else "β"
|
| 1476 |
+
|
| 1477 |
+
# Color-code slider value: gradient from dark (0) to bright (100)
|
| 1478 |
+
if np.isfinite(slider_val):
|
| 1479 |
+
hue = 25 + (slider_val / 100.0) * 10 # orange range 25-35
|
| 1480 |
+
sat = 70
|
| 1481 |
+
light = 30 + (slider_val / 100.0) * 35 # 30% to 65%
|
| 1482 |
+
bar_pct = max(3, slider_val)
|
| 1483 |
+
bar_color = "hsl({}, {}%, {}%)".format(hue, sat, light)
|
| 1484 |
+
else:
|
| 1485 |
+
bar_pct = 0
|
| 1486 |
+
bar_color = "#555"
|
| 1487 |
+
|
| 1488 |
+
active_marker = " β¦" if is_active else ""
|
| 1489 |
+
row_class = "res-row-active" if is_active else "res-row"
|
| 1490 |
+
|
| 1491 |
+
rows.append(
|
| 1492 |
+
'<tr class="{cls}">'
|
| 1493 |
+
'<td style="font-weight:{fw}; white-space:nowrap">{name}{mark}</td>'
|
| 1494 |
+
'<td style="text-align:right; font-variant-numeric:tabular-nums">{raw} {u}</td>'
|
| 1495 |
+
'<td style="width:45%">'
|
| 1496 |
+
' <div style="background:#333;border-radius:4px;height:18px;position:relative;overflow:hidden">'
|
| 1497 |
+
' <div style="background:{bc};height:100%;width:{bp}%;border-radius:4px;'
|
| 1498 |
+
' transition:width 0.4s ease"></div>'
|
| 1499 |
+
' <span style="position:absolute;right:6px;top:0;line-height:18px;'
|
| 1500 |
+
' font-size:11px;color:#eee">{sv}</span>'
|
| 1501 |
+
' </div>'
|
| 1502 |
+
'</td>'
|
| 1503 |
+
'<td style="text-align:center;font-size:11px;color:#999">{src}</td>'
|
| 1504 |
+
'</tr>'.format(
|
| 1505 |
+
cls=row_class,
|
| 1506 |
+
fw="700" if is_active else "400",
|
| 1507 |
+
name=name, mark=active_marker,
|
| 1508 |
+
raw=raw_str, u=unit,
|
| 1509 |
+
bc=bar_color, bp=bar_pct, sv=slider_str,
|
| 1510 |
+
src=cfg["source"].split("β")[0].strip() if "β" in cfg["source"] else cfg["source"][:25],
|
| 1511 |
+
)
|
| 1512 |
+
)
|
| 1513 |
+
|
| 1514 |
+
html = (
|
| 1515 |
+
'<div style="margin:8px 0">'
|
| 1516 |
+
'<table style="width:100%;border-collapse:collapse;font-size:13px;color:#ddd">'
|
| 1517 |
+
'<thead><tr style="border-bottom:1px solid #555">'
|
| 1518 |
+
'<th style="text-align:left;padding:4px 6px;color:#e8956e">Method</th>'
|
| 1519 |
+
'<th style="text-align:right;padding:4px 6px;color:#e8956e">Value</th>'
|
| 1520 |
+
'<th style="text-align:left;padding:4px 6px;color:#e8956e">Slider (0β100)</th>'
|
| 1521 |
+
'<th style="text-align:center;padding:4px 6px;color:#e8956e">Source</th>'
|
| 1522 |
+
'</tr></thead><tbody>'
|
| 1523 |
+
)
|
| 1524 |
+
for r in rows:
|
| 1525 |
+
html += r
|
| 1526 |
+
html += '</tbody></table></div>'
|
| 1527 |
+
return html
|
| 1528 |
+
|
| 1529 |
+
|
| 1530 |
# =============================================================================
|
| 1531 |
# Coaching tips
|
| 1532 |
# =============================================================================
|
|
|
|
| 2069 |
return None, history_state, "", "", z, z, z, z, z, render_slider_bars(None, None, context="target")
|
| 2070 |
|
| 2071 |
def set_target(audio_like, window_choice, pitch_floor, pitch_ceil, max_formant,
|
| 2072 |
+
use_preemph, use_loudnorm, mapping_mode, resonance_method,
|
| 2073 |
+
baseline_state, history_state):
|
| 2074 |
z = gr.update(value=0)
|
| 2075 |
empty_bars = render_slider_bars(None, None, context="target")
|
| 2076 |
try:
|
| 2077 |
y, sr, pf, pc, mf = _prep_audio(audio_like, window_choice, pitch_floor,
|
| 2078 |
pitch_ceil, max_formant, use_preemph, use_loudnorm)
|
| 2079 |
except ValueError as e:
|
| 2080 |
+
return None, history_state, str(e), "", z, z, z, z, z, empty_bars, ""
|
| 2081 |
|
| 2082 |
dists = select_dists(mapping_mode, baseline_state)
|
| 2083 |
+
rm = resonance_method or RESONANCE_METHOD_DEFAULT
|
| 2084 |
cache_key = "target-{}-{}-{}-{}".format(window_choice, use_preemph, use_loudnorm, mapping_mode)
|
| 2085 |
+
m, sliders = cached_analyze(y, sr, pf, pc, mf, cache_key=cache_key, dists=dists,
|
| 2086 |
+
resonance_method=rm)
|
| 2087 |
target_state = {"metrics": asdict(m), "sliders": sliders}
|
| 2088 |
history_state = cap_history((history_state or []) + [history_row("Target", m, sliders)])
|
| 2089 |
bar_html = render_slider_bars(sliders, None, context="target")
|
| 2090 |
+
res_compare = resonance_comparison_table(m, dists, active_method=rm)
|
| 2091 |
return (
|
| 2092 |
target_state, history_state,
|
| 2093 |
format_report(m), format_sliders(sliders),
|
|
|
|
| 2097 |
gr.update(value=safe(sliders["Prosody"])),
|
| 2098 |
gr.update(value=safe(sliders["Volume"])),
|
| 2099 |
bar_html,
|
| 2100 |
+
res_compare,
|
| 2101 |
)
|
| 2102 |
|
| 2103 |
def analyze_user(audio_like, label, window_choice, pitch_floor, pitch_ceil,
|
| 2104 |
max_formant, use_preemph, use_loudnorm, mapping_mode,
|
| 2105 |
+
resonance_method,
|
| 2106 |
baseline_state, target_state, history_state):
|
| 2107 |
z = gr.update(value=0)
|
| 2108 |
z0 = gr.update(value=0.0)
|
|
|
|
| 2115 |
None, None, None,
|
| 2116 |
[],
|
| 2117 |
render_slider_bars((target_state or {}).get("sliders"), None, context="user"),
|
| 2118 |
+
"", "", "", "",
|
| 2119 |
)
|
| 2120 |
try:
|
| 2121 |
y, sr, pf, pc, mf = _prep_audio(audio_like, window_choice, pitch_floor,
|
|
|
|
| 2123 |
except ValueError:
|
| 2124 |
return empty_out
|
| 2125 |
|
| 2126 |
+
rm = resonance_method or RESONANCE_METHOD_DEFAULT
|
| 2127 |
dists = select_dists(mapping_mode, baseline_state)
|
| 2128 |
cache_key = "user-{}-{}-{}-{}-{}".format(label, window_choice, use_preemph,
|
| 2129 |
use_loudnorm, mapping_mode)
|
| 2130 |
+
m, sliders = cached_analyze(y, sr, pf, pc, mf, cache_key=cache_key, dists=dists,
|
| 2131 |
+
resonance_method=rm)
|
| 2132 |
|
| 2133 |
warnings = list(m.warnings)
|
| 2134 |
append_gain_warning(target_state, sliders, warnings)
|
|
|
|
| 2149 |
radar_html = render_radar_chart(t_sliders, sliders)
|
| 2150 |
trend_html = render_trend_plot(history_state)
|
| 2151 |
coaching = generate_coaching(delta, m)
|
| 2152 |
+
res_compare = resonance_comparison_table(m, dists, active_method=rm)
|
| 2153 |
|
| 2154 |
return (
|
| 2155 |
history_state,
|
|
|
|
| 2173 |
radar_html,
|
| 2174 |
trend_html,
|
| 2175 |
coaching,
|
| 2176 |
+
res_compare,
|
| 2177 |
)
|
| 2178 |
|
| 2179 |
def live_analyze(stream_like, buffer_state, pitch_floor, pitch_ceil, max_formant,
|
| 2180 |
+
use_preemph, use_loudnorm, mapping_mode, resonance_method,
|
| 2181 |
+
baseline_state, target_state):
|
| 2182 |
no_change = (buffer_state, *(gr.update() for _ in range(16)))
|
| 2183 |
|
| 2184 |
normalized = normalize_audio_tuple(stream_like)
|
|
|
|
| 2220 |
pf = min(pf, pc - 1)
|
| 2221 |
mf = int(np.clip(int(round(max_formant)), 4500, 6500))
|
| 2222 |
|
| 2223 |
+
rm = resonance_method or RESONANCE_METHOD_DEFAULT
|
| 2224 |
m = extract_metrics(seg, sr, pf, pc, mf)
|
| 2225 |
dists = select_dists(mapping_mode, baseline_state)
|
| 2226 |
+
sliders = metrics_to_sliders(m, dists, resonance_method=rm)
|
| 2227 |
|
| 2228 |
warnings = list(m.warnings)
|
| 2229 |
append_gain_warning(target_state, sliders, warnings)
|
|
|
|
| 2355 |
mapping_mode = gr.Dropdown(
|
| 2356 |
["Global", "Anchored (literature)", "Baseline (personal)"],
|
| 2357 |
value="Global", label="Mapping mode")
|
| 2358 |
+
resonance_method = gr.Dropdown(
|
| 2359 |
+
list(RESONANCE_METHODS.keys()),
|
| 2360 |
+
value=RESONANCE_METHOD_DEFAULT,
|
| 2361 |
+
label="Resonance method",
|
| 2362 |
+
info="Which acoustic measure drives the Resonance slider")
|
| 2363 |
with gr.Row():
|
| 2364 |
pitch_floor = gr.Slider(40, 150, value=PITCH_FLOOR_DEFAULT, step=1, label="Pitch floor (Hz)")
|
| 2365 |
pitch_ceil = gr.Slider(250, 800, value=PITCH_CEIL_DEFAULT, step=1, label="Pitch ceiling (Hz)")
|
|
|
|
| 2412 |
elem_classes=["report-box"])
|
| 2413 |
target_slider_text = gr.Textbox(label="Slider values", lines=4,
|
| 2414 |
elem_classes=["report-box"])
|
| 2415 |
+
with gr.Accordion("Resonance Methods Comparison", open=False):
|
| 2416 |
+
gr.Markdown("All 7 resonance measurement methods computed for this audio. "
|
| 2417 |
+
"The **active method** (β¦) drives the Resonance slider above. "
|
| 2418 |
+
"Change the method in Analysis Settings to compare.")
|
| 2419 |
+
target_res_compare = gr.HTML(value="")
|
| 2420 |
|
| 2421 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2422 |
# TAB 2: YOUR VOICE
|
|
|
|
| 2453 |
radar_html = gr.HTML(value="", label="Radar comparison")
|
| 2454 |
trend_html = gr.HTML(value="", label="Session trend")
|
| 2455 |
|
| 2456 |
+
with gr.Accordion("Resonance Methods Comparison", open=True):
|
| 2457 |
+
gr.Markdown("All 7 resonance metrics computed simultaneously. "
|
| 2458 |
+
"The **active method** (β¦) drives the slider. "
|
| 2459 |
+
"Compare to find which tracks your perceived resonance best.")
|
| 2460 |
+
user_res_compare = gr.HTML(value="")
|
| 2461 |
+
|
| 2462 |
with gr.Accordion("Live mode", open=False):
|
| 2463 |
gr.Markdown("Live mode streams from your microphone with EMA smoothing.")
|
| 2464 |
live_audio = gr.Audio(sources=["microphone"], type="numpy",
|
|
|
|
| 2524 |
btn_clear_baseline.click(clear_baseline, [], baseline_outputs)
|
| 2525 |
|
| 2526 |
target_inputs = [target_audio, analysis_window, pitch_floor, pitch_ceil, max_formant,
|
| 2527 |
+
use_preemph, use_loudnorm, mapping_mode, resonance_method,
|
| 2528 |
+
baseline_state, history_state]
|
| 2529 |
target_outputs = [target_state, history_state, target_report, target_slider_text,
|
| 2530 |
+
t_pitch, t_weight, t_res, t_pros, t_vol, target_bars,
|
| 2531 |
+
target_res_compare]
|
| 2532 |
btn_set_target.click(set_target, target_inputs, target_outputs)
|
| 2533 |
btn_clear_target.click(clear_target, [history_state],
|
| 2534 |
[target_state, history_state, target_report, target_slider_text,
|
|
|
|
| 2541 |
user_report, user_slider_text, delta_text,
|
| 2542 |
export_json, export_csv, export_baseline,
|
| 2543 |
history_table,
|
| 2544 |
+
user_bars, radar_html, trend_html, coaching_md,
|
| 2545 |
+
user_res_compare]
|
| 2546 |
user_common_inputs = [analysis_window, pitch_floor, pitch_ceil, max_formant,
|
| 2547 |
+
use_preemph, use_loudnorm, mapping_mode, resonance_method,
|
| 2548 |
+
baseline_state,
|
| 2549 |
target_state, history_state]
|
| 2550 |
btn_analyze_mic.click(analyze_user, [user_audio_mic, label_recorded] + user_common_inputs, user_outputs)
|
| 2551 |
btn_analyze_upload.click(analyze_user, [user_audio_up, label_uploaded] + user_common_inputs, user_outputs)
|
|
|
|
| 2558 |
user_bars, coaching_md]
|
| 2559 |
live_audio.stream(live_analyze,
|
| 2560 |
[live_audio, live_buffer_state, pitch_floor, pitch_ceil, max_formant,
|
| 2561 |
+
use_preemph, use_loudnorm, mapping_mode, resonance_method,
|
| 2562 |
+
baseline_state, target_state],
|
| 2563 |
live_outputs)
|
| 2564 |
btn_reset_live.click(reset_live, [], [live_buffer_state])
|
| 2565 |
|
| 2566 |
+
demo.queue(max_size=32).launch(**_launch_kw)
|