Spaces:
Running on Zero
Running on Zero
Lazy load models for ZeroGPU Space startup
Browse files
app.py
CHANGED
|
@@ -20,6 +20,17 @@ import html
|
|
| 20 |
import threading
|
| 21 |
import traceback
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# Global variables for controlling the flow of the program
|
| 24 |
MAX_LINES_DISPLAYED = 10
|
| 25 |
MAX_NUM_OF_SPEAKERS = 8
|
|
@@ -30,6 +41,7 @@ FILE_PROCESSING_STOPPED = threading.Event()
|
|
| 30 |
FILE_PROCESSING_STOPPED.set()
|
| 31 |
MICROPHONE_PROCESSING_ENABLED: bool = False
|
| 32 |
MICROPHONE_PROCESSING_LOCK = threading.Lock()
|
|
|
|
| 33 |
CONFIG_RENEWED_FLAG: bool = False
|
| 34 |
START_TIME_OFFSET_EXAMPLES: float = -1.6 # seconds
|
| 35 |
START_TIME_OFFSET_FILE_UPLOAD: float = -0.8 # seconds
|
|
@@ -147,13 +159,26 @@ config_file = MultitalkerTranscriptionConfig(
|
|
| 147 |
audio_pre_loading=True,
|
| 148 |
)
|
| 149 |
|
| 150 |
-
# Initialize
|
| 151 |
-
|
| 152 |
-
session_mic
|
| 153 |
session_file = StreamingSession()
|
| 154 |
-
|
|
|
|
| 155 |
MAX_NUM_OF_SPEAKERS = min(config_mic.max_num_of_spks, config_file.max_num_of_spks)
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
def render_diarization_state(diar_state, config):
|
| 158 |
"""Render Sortformer's speaker cache, FIFO, and current chunk predictions."""
|
| 159 |
num_speakers = int(config.max_num_of_spks)
|
|
@@ -200,10 +225,12 @@ def format_transcript(text):
|
|
| 200 |
if not text:
|
| 201 |
return ""
|
| 202 |
|
| 203 |
-
single_speaker_mode =
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
| 207 |
current_color = "#76B900" if single_speaker_mode else "#A8A8A8"
|
| 208 |
|
| 209 |
for raw_line in (line for line in text.strip().splitlines() if line.strip()):
|
|
@@ -356,6 +383,7 @@ def update_config_for_file(evt: gr.SelectData, state=None):
|
|
| 356 |
|
| 357 |
def file_processing(filepath, state, file_upload=False, start_time_offset=0.0):
|
| 358 |
"""Handles file uploads and processes the audio in chunks with real-time simulation."""
|
|
|
|
| 359 |
print(f"[DEBUG] file_processing() FIRST LINE: filepath: {filepath}, state[is_playing]: {state.get('is_playing', False)}, file_upload: {file_upload}")
|
| 360 |
if not filepath or state.get("should_stop", False):
|
| 361 |
yield state, state["diar_html"], format_transcript(state.get("full_transcript", ""))
|
|
@@ -499,6 +527,7 @@ async def show_progress_bar_with_sleep(filepath, num_steps=500, seconds_per_mb=0
|
|
| 499 |
|
| 500 |
async def file_processing_example_click_async(filepath, state, start_time_offset=0.0):
|
| 501 |
"""Async wrapper for file_processing with pause/resume support (doesn't update audio component)."""
|
|
|
|
| 502 |
state["should_stop"] = False
|
| 503 |
state["is_playing"] = False
|
| 504 |
state["pause_start_time"] = None
|
|
@@ -614,12 +643,14 @@ def file_processing_example_click(filepath, state, start_time_offset=0.0):
|
|
| 614 |
loop.close()
|
| 615 |
FILE_PROCESSING_STOPPED.set()
|
| 616 |
|
|
|
|
| 617 |
def file_processing_example_click_upload(filepath, state):
|
| 618 |
"""Wrapper for file upload with upload offset."""
|
| 619 |
# file_processing_example_click is a generator, so yield from it
|
| 620 |
for result in file_processing_example_click(filepath, state, start_time_offset=START_TIME_OFFSET_FILE_UPLOAD):
|
| 621 |
yield result
|
| 622 |
|
|
|
|
| 623 |
def file_processing_example_click_examples(filepath, state):
|
| 624 |
"""Wrapper for examples with examples offset."""
|
| 625 |
# file_processing_example_click is a generator, so yield from it
|
|
@@ -665,6 +696,7 @@ async def file_processing_upload_event_async(filepath, state):
|
|
| 665 |
|
| 666 |
yield s, html_diar, transcript
|
| 667 |
|
|
|
|
| 668 |
def file_processing_upload_event(filepath, state):
|
| 669 |
"""Wrapper that runs the async function synchronously for Gradio compatibility."""
|
| 670 |
print(f"[DEBUG] First line of file_processing_upload_event(): filepath: {filepath}, state[is_playing]: {state.get('is_playing', False)}")
|
|
@@ -730,6 +762,7 @@ def example_selection_stop_file_processing(state, filepath):
|
|
| 730 |
|
| 731 |
def file_upload_stop_file_processing(state):
|
| 732 |
"""Stops file processing when audio is cleared and prepares for new file."""
|
|
|
|
| 733 |
state["mode"] = "idle"
|
| 734 |
state["last_processed_file"] = None
|
| 735 |
state["is_playing"] = False
|
|
@@ -745,6 +778,7 @@ def file_upload_stop_file_processing(state):
|
|
| 745 |
|
| 746 |
def stop_file_processing_example(state):
|
| 747 |
"""Stops file processing when audio is cleared."""
|
|
|
|
| 748 |
state["mode"] = "idle"
|
| 749 |
state["last_processed_file"] = None
|
| 750 |
print(f"[DEBUG] global RUDE_LOOP_STOP is set to True")
|
|
@@ -786,8 +820,10 @@ def hide_examples_panel():
|
|
| 786 |
"""Collapse the example picker after an example is selected."""
|
| 787 |
return False, gr.update(visible=False), "Choose Example"
|
| 788 |
|
|
|
|
| 789 |
def reset_on_tab_switch(state):
|
| 790 |
"""Stop active processing and recreate both streaming instances."""
|
|
|
|
| 791 |
global MICROPHONE_PROCESSING_ENABLED, RUDE_LOOP_STOP
|
| 792 |
RUDE_LOOP_STOP = True
|
| 793 |
MICROPHONE_PROCESSING_ENABLED = False
|
|
@@ -836,8 +872,10 @@ def trigger_end_of_file_flag(state):
|
|
| 836 |
print(f"[DEBUG] END OF FILE TRIGGERED trigger_end_of_file_flag(): end_of_file set to {state['end_of_file']}")
|
| 837 |
return state
|
| 838 |
|
|
|
|
| 839 |
def start_stream_processing(state):
|
| 840 |
"""Initiates stream processing mode."""
|
|
|
|
| 841 |
global MICROPHONE_PROCESSING_ENABLED
|
| 842 |
print(f"[DEBUG] start_stream_processing() CALLED - mode before: {state.get('mode', 'None')}")
|
| 843 |
MICROPHONE_PROCESSING_ENABLED = True
|
|
@@ -853,8 +891,10 @@ def start_stream_processing(state):
|
|
| 853 |
print(f"[DEBUG] start_stream_processing() RETURNING - mode after: '{state['mode']}'")
|
| 854 |
return state, gr.Button(value="Pause", visible=True)
|
| 855 |
|
|
|
|
| 856 |
def toggle_microphone_pause(state):
|
| 857 |
"""Pause or resume microphone transcription while recording continues."""
|
|
|
|
| 858 |
if state.get("mode") != "stream":
|
| 859 |
return (
|
| 860 |
state,
|
|
@@ -883,8 +923,10 @@ def toggle_microphone_pause(state):
|
|
| 883 |
format_transcript(state.get("full_transcript", "")),
|
| 884 |
)
|
| 885 |
|
|
|
|
| 886 |
def stop_stream_processing(state):
|
| 887 |
"""Stops stream processing mode."""
|
|
|
|
| 888 |
global MICROPHONE_PROCESSING_ENABLED
|
| 889 |
print(f"[DEBUG] stop_stream_processing() CALLED - mode before: {state.get('mode', 'None')}")
|
| 890 |
MICROPHONE_PROCESSING_ENABLED = False
|
|
@@ -907,8 +949,10 @@ def stop_stream_processing(state):
|
|
| 907 |
|
| 908 |
return state, diar_html_output, transcript_textbox_html, gr.Button(value="Pause", visible=False)
|
| 909 |
|
|
|
|
| 910 |
def stream_processing(state, audio_data):
|
| 911 |
"""Handles microphone streaming."""
|
|
|
|
| 912 |
# Initialize state if it's None or doesn't have required fields
|
| 913 |
if state is None:
|
| 914 |
state = {"mode": "stream", "full_transcript": "", "diar_html_initial": ""}
|
|
@@ -1015,6 +1059,7 @@ def reload_models_with_checkbox_update(
|
|
| 1015 |
)
|
| 1016 |
return html_string, single_speaker_mode
|
| 1017 |
|
|
|
|
| 1018 |
def reload_models(
|
| 1019 |
audio_pre_loading,
|
| 1020 |
streaming_chunk_size,
|
|
@@ -1025,6 +1070,7 @@ def reload_models(
|
|
| 1025 |
sent_break_sec,
|
| 1026 |
):
|
| 1027 |
"""Reloads the streaming session with new configurations."""
|
|
|
|
| 1028 |
try:
|
| 1029 |
num_speakers = min(int(num_speakers), MAX_NUM_OF_SPEAKERS)
|
| 1030 |
if single_speaker_mode:
|
|
@@ -1106,13 +1152,22 @@ def get_text_box_html(
|
|
| 1106 |
|
| 1107 |
def load_and_reset():
|
| 1108 |
"""Resets the UI state on page load."""
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1116 |
|
| 1117 |
status_textbox_html= get_text_box_html(
|
| 1118 |
audio_pre_loading=AUDIO_PRE_LOADING,
|
|
@@ -1124,7 +1179,7 @@ def load_and_reset():
|
|
| 1124 |
text_break_silence=TEXT_BREAK_SILENCE,
|
| 1125 |
)
|
| 1126 |
diar_html_output = render_diarization_state(None, config_file)
|
| 1127 |
-
transcript_textbox = "<p style='text-align: center; font-size:20px;'><b>No transcription yet.</b></p>"
|
| 1128 |
return {"should_stop": False,
|
| 1129 |
"stream_step_num": 0,
|
| 1130 |
"microphone_paused": False,
|
|
|
|
| 20 |
import threading
|
| 21 |
import traceback
|
| 22 |
|
| 23 |
+
try:
|
| 24 |
+
import spaces
|
| 25 |
+
except ImportError:
|
| 26 |
+
class _SpacesNoop:
|
| 27 |
+
def GPU(self, *args, **kwargs):
|
| 28 |
+
def decorator(fn):
|
| 29 |
+
return fn
|
| 30 |
+
return decorator
|
| 31 |
+
|
| 32 |
+
spaces = _SpacesNoop()
|
| 33 |
+
|
| 34 |
# Global variables for controlling the flow of the program
|
| 35 |
MAX_LINES_DISPLAYED = 10
|
| 36 |
MAX_NUM_OF_SPEAKERS = 8
|
|
|
|
| 41 |
FILE_PROCESSING_STOPPED.set()
|
| 42 |
MICROPHONE_PROCESSING_ENABLED: bool = False
|
| 43 |
MICROPHONE_PROCESSING_LOCK = threading.Lock()
|
| 44 |
+
GPU_DURATION_SECONDS = int(os.environ.get("SPACES_GPU_DURATION", "120"))
|
| 45 |
CONFIG_RENEWED_FLAG: bool = False
|
| 46 |
START_TIME_OFFSET_EXAMPLES: float = -1.6 # seconds
|
| 47 |
START_TIME_OFFSET_FILE_UPLOAD: float = -0.8 # seconds
|
|
|
|
| 159 |
audio_pre_loading=True,
|
| 160 |
)
|
| 161 |
|
| 162 |
+
# Initialize streaming session containers. Models are loaded lazily so ZeroGPU
|
| 163 |
+
# Spaces can start the app before CUDA is allocated to a request.
|
| 164 |
+
session_mic = StreamingSession()
|
| 165 |
session_file = StreamingSession()
|
| 166 |
+
MODEL_INIT_LOCK = threading.Lock()
|
| 167 |
+
MODELS_INITIALIZED = False
|
| 168 |
MAX_NUM_OF_SPEAKERS = min(config_mic.max_num_of_spks, config_file.max_num_of_spks)
|
| 169 |
|
| 170 |
+
|
| 171 |
+
def ensure_sessions_initialized(reload=False):
|
| 172 |
+
"""Load or reload ASR/diarization sessions on demand."""
|
| 173 |
+
global MODELS_INITIALIZED
|
| 174 |
+
with MODEL_INIT_LOCK:
|
| 175 |
+
should_reload = reload and MODELS_INITIALIZED
|
| 176 |
+
if MODELS_INITIALIZED and not reload:
|
| 177 |
+
return
|
| 178 |
+
session_mic.setup_streaming_session(config_mic, reload=should_reload)
|
| 179 |
+
session_file.setup_streaming_session(config_file, reload=should_reload)
|
| 180 |
+
MODELS_INITIALIZED = True
|
| 181 |
+
|
| 182 |
def render_diarization_state(diar_state, config):
|
| 183 |
"""Render Sortformer's speaker cache, FIFO, and current chunk predictions."""
|
| 184 |
num_speakers = int(config.max_num_of_spks)
|
|
|
|
| 225 |
if not text:
|
| 226 |
return ""
|
| 227 |
|
| 228 |
+
single_speaker_mode = False
|
| 229 |
+
if MODELS_INITIALIZED:
|
| 230 |
+
single_speaker_mode = (
|
| 231 |
+
session_mic.multispk_asr_streamer._single_speaker_mode
|
| 232 |
+
or session_file.multispk_asr_streamer._single_speaker_mode
|
| 233 |
+
)
|
| 234 |
current_color = "#76B900" if single_speaker_mode else "#A8A8A8"
|
| 235 |
|
| 236 |
for raw_line in (line for line in text.strip().splitlines() if line.strip()):
|
|
|
|
| 383 |
|
| 384 |
def file_processing(filepath, state, file_upload=False, start_time_offset=0.0):
|
| 385 |
"""Handles file uploads and processes the audio in chunks with real-time simulation."""
|
| 386 |
+
ensure_sessions_initialized()
|
| 387 |
print(f"[DEBUG] file_processing() FIRST LINE: filepath: {filepath}, state[is_playing]: {state.get('is_playing', False)}, file_upload: {file_upload}")
|
| 388 |
if not filepath or state.get("should_stop", False):
|
| 389 |
yield state, state["diar_html"], format_transcript(state.get("full_transcript", ""))
|
|
|
|
| 527 |
|
| 528 |
async def file_processing_example_click_async(filepath, state, start_time_offset=0.0):
|
| 529 |
"""Async wrapper for file_processing with pause/resume support (doesn't update audio component)."""
|
| 530 |
+
ensure_sessions_initialized()
|
| 531 |
state["should_stop"] = False
|
| 532 |
state["is_playing"] = False
|
| 533 |
state["pause_start_time"] = None
|
|
|
|
| 643 |
loop.close()
|
| 644 |
FILE_PROCESSING_STOPPED.set()
|
| 645 |
|
| 646 |
+
@spaces.GPU(duration=GPU_DURATION_SECONDS)
|
| 647 |
def file_processing_example_click_upload(filepath, state):
|
| 648 |
"""Wrapper for file upload with upload offset."""
|
| 649 |
# file_processing_example_click is a generator, so yield from it
|
| 650 |
for result in file_processing_example_click(filepath, state, start_time_offset=START_TIME_OFFSET_FILE_UPLOAD):
|
| 651 |
yield result
|
| 652 |
|
| 653 |
+
@spaces.GPU(duration=GPU_DURATION_SECONDS)
|
| 654 |
def file_processing_example_click_examples(filepath, state):
|
| 655 |
"""Wrapper for examples with examples offset."""
|
| 656 |
# file_processing_example_click is a generator, so yield from it
|
|
|
|
| 696 |
|
| 697 |
yield s, html_diar, transcript
|
| 698 |
|
| 699 |
+
@spaces.GPU(duration=GPU_DURATION_SECONDS)
|
| 700 |
def file_processing_upload_event(filepath, state):
|
| 701 |
"""Wrapper that runs the async function synchronously for Gradio compatibility."""
|
| 702 |
print(f"[DEBUG] First line of file_processing_upload_event(): filepath: {filepath}, state[is_playing]: {state.get('is_playing', False)}")
|
|
|
|
| 762 |
|
| 763 |
def file_upload_stop_file_processing(state):
|
| 764 |
"""Stops file processing when audio is cleared and prepares for new file."""
|
| 765 |
+
ensure_sessions_initialized()
|
| 766 |
state["mode"] = "idle"
|
| 767 |
state["last_processed_file"] = None
|
| 768 |
state["is_playing"] = False
|
|
|
|
| 778 |
|
| 779 |
def stop_file_processing_example(state):
|
| 780 |
"""Stops file processing when audio is cleared."""
|
| 781 |
+
ensure_sessions_initialized()
|
| 782 |
state["mode"] = "idle"
|
| 783 |
state["last_processed_file"] = None
|
| 784 |
print(f"[DEBUG] global RUDE_LOOP_STOP is set to True")
|
|
|
|
| 820 |
"""Collapse the example picker after an example is selected."""
|
| 821 |
return False, gr.update(visible=False), "Choose Example"
|
| 822 |
|
| 823 |
+
@spaces.GPU(duration=GPU_DURATION_SECONDS)
|
| 824 |
def reset_on_tab_switch(state):
|
| 825 |
"""Stop active processing and recreate both streaming instances."""
|
| 826 |
+
ensure_sessions_initialized()
|
| 827 |
global MICROPHONE_PROCESSING_ENABLED, RUDE_LOOP_STOP
|
| 828 |
RUDE_LOOP_STOP = True
|
| 829 |
MICROPHONE_PROCESSING_ENABLED = False
|
|
|
|
| 872 |
print(f"[DEBUG] END OF FILE TRIGGERED trigger_end_of_file_flag(): end_of_file set to {state['end_of_file']}")
|
| 873 |
return state
|
| 874 |
|
| 875 |
+
@spaces.GPU(duration=GPU_DURATION_SECONDS)
|
| 876 |
def start_stream_processing(state):
|
| 877 |
"""Initiates stream processing mode."""
|
| 878 |
+
ensure_sessions_initialized()
|
| 879 |
global MICROPHONE_PROCESSING_ENABLED
|
| 880 |
print(f"[DEBUG] start_stream_processing() CALLED - mode before: {state.get('mode', 'None')}")
|
| 881 |
MICROPHONE_PROCESSING_ENABLED = True
|
|
|
|
| 891 |
print(f"[DEBUG] start_stream_processing() RETURNING - mode after: '{state['mode']}'")
|
| 892 |
return state, gr.Button(value="Pause", visible=True)
|
| 893 |
|
| 894 |
+
@spaces.GPU(duration=GPU_DURATION_SECONDS)
|
| 895 |
def toggle_microphone_pause(state):
|
| 896 |
"""Pause or resume microphone transcription while recording continues."""
|
| 897 |
+
ensure_sessions_initialized()
|
| 898 |
if state.get("mode") != "stream":
|
| 899 |
return (
|
| 900 |
state,
|
|
|
|
| 923 |
format_transcript(state.get("full_transcript", "")),
|
| 924 |
)
|
| 925 |
|
| 926 |
+
@spaces.GPU(duration=GPU_DURATION_SECONDS)
|
| 927 |
def stop_stream_processing(state):
|
| 928 |
"""Stops stream processing mode."""
|
| 929 |
+
ensure_sessions_initialized()
|
| 930 |
global MICROPHONE_PROCESSING_ENABLED
|
| 931 |
print(f"[DEBUG] stop_stream_processing() CALLED - mode before: {state.get('mode', 'None')}")
|
| 932 |
MICROPHONE_PROCESSING_ENABLED = False
|
|
|
|
| 949 |
|
| 950 |
return state, diar_html_output, transcript_textbox_html, gr.Button(value="Pause", visible=False)
|
| 951 |
|
| 952 |
+
@spaces.GPU(duration=GPU_DURATION_SECONDS)
|
| 953 |
def stream_processing(state, audio_data):
|
| 954 |
"""Handles microphone streaming."""
|
| 955 |
+
ensure_sessions_initialized()
|
| 956 |
# Initialize state if it's None or doesn't have required fields
|
| 957 |
if state is None:
|
| 958 |
state = {"mode": "stream", "full_transcript": "", "diar_html_initial": ""}
|
|
|
|
| 1059 |
)
|
| 1060 |
return html_string, single_speaker_mode
|
| 1061 |
|
| 1062 |
+
@spaces.GPU(duration=GPU_DURATION_SECONDS)
|
| 1063 |
def reload_models(
|
| 1064 |
audio_pre_loading,
|
| 1065 |
streaming_chunk_size,
|
|
|
|
| 1070 |
sent_break_sec,
|
| 1071 |
):
|
| 1072 |
"""Reloads the streaming session with new configurations."""
|
| 1073 |
+
ensure_sessions_initialized()
|
| 1074 |
try:
|
| 1075 |
num_speakers = min(int(num_speakers), MAX_NUM_OF_SPEAKERS)
|
| 1076 |
if single_speaker_mode:
|
|
|
|
| 1152 |
|
| 1153 |
def load_and_reset():
|
| 1154 |
"""Resets the UI state on page load."""
|
| 1155 |
+
if MODELS_INITIALIZED:
|
| 1156 |
+
AUDIO_PRE_LOADING = session_file._audio_pre_loading
|
| 1157 |
+
STREAMING_CHUNK_SIZE = session_file.multispk_asr_streamer._att_context_size[1]
|
| 1158 |
+
SINGLE_SPEAKER_MODE = 'ON' if session_file.multispk_asr_streamer._single_speaker_mode else 'OFF'
|
| 1159 |
+
NUMBER_OF_SPEAKERS = f"{session_file.multispk_asr_streamer._max_num_of_spks}"
|
| 1160 |
+
TEXT_BREAK_SILENCE = f"{session_file.multispk_asr_streamer._sent_break_sec} seconds"
|
| 1161 |
+
CACHE_GATING_STATUS = 'ON' if session_file.multispk_asr_streamer._cache_gating else 'OFF'
|
| 1162 |
+
BINARY_DIAR_PREDICTION_STATUS = 'ON' if session_file.multispk_asr_streamer._binary_diar_preds else 'OFF'
|
| 1163 |
+
else:
|
| 1164 |
+
AUDIO_PRE_LOADING = config_file.audio_pre_loading
|
| 1165 |
+
STREAMING_CHUNK_SIZE = config_file.att_context_size[1]
|
| 1166 |
+
SINGLE_SPEAKER_MODE = 'ON' if config_file.single_speaker_mode else 'OFF'
|
| 1167 |
+
NUMBER_OF_SPEAKERS = f"{config_file.max_num_of_spks}"
|
| 1168 |
+
TEXT_BREAK_SILENCE = f"{config_file.sent_break_sec} seconds"
|
| 1169 |
+
CACHE_GATING_STATUS = 'ON' if config_file.cache_gating else 'OFF'
|
| 1170 |
+
BINARY_DIAR_PREDICTION_STATUS = 'ON' if config_file.binary_diar_preds else 'OFF'
|
| 1171 |
|
| 1172 |
status_textbox_html= get_text_box_html(
|
| 1173 |
audio_pre_loading=AUDIO_PRE_LOADING,
|
|
|
|
| 1179 |
text_break_silence=TEXT_BREAK_SILENCE,
|
| 1180 |
)
|
| 1181 |
diar_html_output = render_diarization_state(None, config_file)
|
| 1182 |
+
transcript_textbox = "<p style='text-align: center; font-size:20px;'><b>No transcription yet. Models load on first transcription request.</b></p>"
|
| 1183 |
return {"should_stop": False,
|
| 1184 |
"stream_step_num": 0,
|
| 1185 |
"microphone_paused": False,
|