2pift commited on
Commit
bceb0a7
·
1 Parent(s): b3ef023

update dockerfile and appliaction file:

Browse files
Dockerfile CHANGED
@@ -13,6 +13,7 @@ RUN mkdir -p /app/.streamlit \
13
 
14
  RUN apt-get update && apt-get install -y \
15
  build-essential \
 
16
  curl \
17
  git \
18
  && rm -rf /var/lib/apt/lists/*
 
13
 
14
  RUN apt-get update && apt-get install -y \
15
  build-essential \
16
+ ffmpeg \
17
  curl \
18
  git \
19
  && rm -rf /var/lib/apt/lists/*
src/streamlit_app.py CHANGED
@@ -1,40 +1,229 @@
1
- import altair as alt
 
 
 
2
  import numpy as np
3
  import pandas as pd
4
  import streamlit as st
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import ffmpeg
3
+ import keras
4
+ import matplotlib.pyplot as plt
5
  import numpy as np
6
  import pandas as pd
7
  import streamlit as st
8
+ import tensorflow as tf
9
+ from huggingface_hub import hf_hub_download
10
 
11
+ # ========= App title =========
12
+ st.title("Speaker Verification - Demo")
13
+
14
+ # ========= Session state =========
15
+ if "load_model_button" not in st.session_state:
16
+ st.session_state.load_model_button = False
17
+ # if "verify_speaker_button" not in st.session_state:
18
+ # st.session_state.verify_speaker_button = False
19
+ if "audio_left" not in st.session_state:
20
+ st.session_state.audio_left = None
21
+ if "audio_right" not in st.session_state:
22
+ st.session_state.audio_right = None
23
+
24
+ # ========= UI: choose model =========
25
+ model_df = pd.DataFrame({"first column": ["verification_model_resnet34_512dim"]})
26
+ option = st.selectbox("Choose model to test out:", model_df["first column"])
27
+ st.button("Load the model", on_click=lambda: st.session_state.update(load_model_button=True))
28
+
29
+ # ========= Helpers =========
30
+ FS = 16000 # target sample rate
31
+ WT = 48560 # window length in samples
32
+
33
+ EXT2FMT = {
34
+ "wav": "wav",
35
+ "mp3": "mp3",
36
+ "ogg": "ogg",
37
+ "aac": "aac",
38
+ "m4a": "mp4"
39
+ }
40
+
41
+ def infer_input_format(name: str) -> str | None:
42
+ if name and "." in name:
43
+ ext = name.rsplit(".", 1)[-1].lower()
44
+ return EXT2FMT.get(ext)
45
+ return None
46
+
47
+ @st.cache_data(show_spinner=False)
48
+ def bytes_to_pcm16k_mono(data: bytes, in_format: str | None) -> np.ndarray:
49
+ """
50
+ Konwertuje wejściowe audio (dowolny wspierany kontener) do surowego PCM 16kHz mono 16-bit LE
51
+ i zwraca jako float32 w zakresie [-1, 1].
52
+ Cache'owane po (bytes, format).
53
+ """
54
+ stream = (
55
+ ffmpeg
56
+ .input("pipe:0", **({"format": in_format} if in_format else {}))
57
+ .output("pipe:1", format="s16le", acodec="pcm_s16le", ar=str(FS), ac=1)
58
+ .global_args("-hide_banner")
59
+ )
60
+ out, err = ffmpeg.run(stream, capture_stdout=True, capture_stderr=True, input=data)
61
+ audio = np.frombuffer(out, dtype="<i2").astype(np.float32) / 32768.0
62
+ if audio.size < WT:
63
+ # padding do WT
64
+ audio = np.pad(audio, (int((WT - audio.size) / 2) + 1, int((WT - audio.size) / 2) + 1), mode="constant")
65
+ return audio
66
+
67
+ def plot_waveform(audio_np: np.ndarray, fs: int = FS, title: str = "Waveform"):
68
+ t = np.arange(audio_np.size) / fs if audio_np.size else np.array([0, 1e-6])
69
+ fig, ax = plt.subplots()
70
+ ax.plot(t, audio_np)
71
+ ax.set_title(title)
72
+ ax.set_xlabel("Time [s]")
73
+ ax.set_ylabel("Amplitude")
74
+ ax.margins(x=0, y=0)
75
+ if audio_np.size:
76
+ ax.set_xlim(t[0], t[-1])
77
+ return fig
78
+
79
+ @st.cache_resource(show_spinner=True)
80
+ def load_model_from_hub(repo_id: str, filename: str, revision: str):
81
+ """Pobiera i ładuje model Keras (cache resource – trzymamy w pamięci)."""
82
+ model_path = hf_hub_download(
83
+ repo_id=repo_id,
84
+ filename=filename,
85
+ repo_type="model",
86
+ revision=revision,
87
+ )
88
+ # Import modułu z customami, żeby rejestratory Keras się wykonały
89
+ import custom_models, custom_losses # noqa: F401
90
+ model = keras.models.load_model(model_path)
91
+ if hasattr(model, "return_embedding"):
92
+ model.return_embedding = True
93
+ with open(model_path, "rb") as f:
94
+ model_bytes = f.read() # do download_button (bez trzymania otwartego pliku)
95
+ return model, model_path, model_bytes
96
+
97
+ def handle_record(label: str) -> np.ndarray | None:
98
+ rec = st.audio_input(label)
99
+ if not rec:
100
+ return None
101
+ try:
102
+ audio_np = bytes_to_pcm16k_mono(rec.getvalue(), in_format="wav")
103
+ return audio_np
104
+ except ffmpeg.Error as e:
105
+ st.error("FFmpeg failed while processing recording.")
106
+ st.code(e.stderr.decode("utf-8", "ignore"))
107
+ return None
108
+
109
+ def handle_upload(label: str, key: str) -> np.ndarray | None:
110
+ file = st.file_uploader(
111
+ label,
112
+ type=["wav", "m4a", "aac", "mp3", "ogg", "webm", "flac"],
113
+ key=key,
114
+ )
115
+ if not file:
116
+ return None
117
+ in_fmt = infer_input_format(file.name)
118
+ try:
119
+ audio_np = bytes_to_pcm16k_mono(file.getvalue(), in_fmt)
120
+ return audio_np
121
+ except ffmpeg.Error as e:
122
+ st.error("FFmpeg failed while converting uploaded file.")
123
+ st.code(e.stderr.decode("utf-8", "ignore"))
124
+ return None
125
+
126
+ def delta(x):
127
+ """Computes first-order difference along time axis."""
128
+ return x[:, 1:] - x[:, :-1]
129
+
130
+ def array_to_spectrogram(audio_np: np.ndarray,
131
+ audio_in_samples: int = 48560,
132
+ window_length: int = 400,
133
+ step_length: int = 160,
134
+ fft_length: int = 1023
135
+ ) -> tf.Tensor:
136
+
137
+ audio = tf.convert_to_tensor(audio_np, dtype=tf.float32)
138
+ audio_length = audio_np.size
139
+
140
+ random_int = tf.random.uniform(shape=(), minval=0, maxval=(audio_length-audio_in_samples), dtype=tf.int32)
141
+ stft = tf.signal.stft(audio[random_int:(random_int+audio_in_samples)],
142
+ frame_length=window_length,
143
+ frame_step=step_length,
144
+ fft_length=fft_length)
145
+
146
+ spectrogram = tf.abs(stft)
147
+ spectrogram = tf.transpose(spectrogram) # shape: (freq, time)
148
+ spectrogram = tf.math.log1p(spectrogram)
149
+
150
+ spectrogram_delta = delta(spectrogram)
151
+ spectrogram_delta2 = delta(spectrogram_delta)
152
+
153
+ return tf.stack([spectrogram[:, :-2],
154
+ spectrogram_delta[:, :-1],
155
+ spectrogram_delta2],
156
+ axis=-1) # shape: (freq, time, 3)
157
+
158
+ @st.cache_data(show_spinner=True)
159
+ def verify_speakers(model, audio_left, audio_right, margin):
160
+
161
+ spec_left = array_to_spectrogram(audio_left)[tf.newaxis, ...]
162
+ spec_right = array_to_spectrogram(audio_right)[tf.newaxis, ...]
163
+
164
+ emb_left = model.predict(spec_left, verbose=0)
165
+ emb_right = model.predict(spec_right, verbose=0)
166
+
167
+ cosine_similarity = tf.linalg.matmul(emb_left, emb_right, transpose_b=True)
168
+ cosine_similarity = float(cosine_similarity.numpy().squeeze())
169
+
170
+ if cosine_similarity >= margin:
171
+ st.success("Both voice recordings belong to the same person.")
172
+ else:
173
+ st.warning("The voice recordings belong to different people.")
174
+ st.caption(f"Cosine similarity: {cosine_similarity:.4f}, margin: {margin:.4f}")
175
+
176
+ # ========= Load model =========
177
+ if st.session_state.load_model_button:
178
+ try:
179
+ model, model_path, model_bytes = load_model_from_hub(
180
+ repo_id="2pift/sv-resnet34-keras",
181
+ filename="best_model.keras",
182
+ revision="v1.0.0",
183
+ )
184
+ st.success("Model loaded. You can now upload/record audio files.")
185
+ st.download_button(
186
+ "Download the model",
187
+ data=model_bytes,
188
+ file_name="verification_model_resnet34_512dim.keras",
189
+ )
190
+ except Exception as e:
191
+ st.error(f"Error loading model: {e}")
192
+
193
+ # ========= Two columns (symetryczne) =========
194
+ left_column, right_column = st.columns(2)
195
+
196
+ with left_column:
197
+ st.subheader("Left input")
198
+ record_left = st.checkbox("Record left input")
199
+ if record_left:
200
+ audio_left = handle_record("Record (left)")
201
+ else:
202
+ audio_left = handle_upload("Upload left audio", key="file_left")
203
+ if audio_left is not None:
204
+ st.session_state.audio_left = audio_left
205
+ fig = plot_waveform(audio_left, FS, "Left audio waveform")
206
+ st.pyplot(fig, use_container_width=True)
207
+ st.caption(f"Samples: {audio_left.size} • Duration: {audio_left.size/FS:.2f}s")
208
+
209
+ with right_column:
210
+ st.subheader("Right input")
211
+ record_right = st.checkbox("Record right input")
212
+ if record_right:
213
+ audio_right = handle_record("Record (right)")
214
+ else:
215
+ audio_right = handle_upload("Upload right audio", key="file_right")
216
+ if audio_right is not None:
217
+ st.session_state.audio_right = audio_right
218
+ fig = plot_waveform(audio_right, FS, "Right audio waveform")
219
+ st.pyplot(fig, use_container_width=True)
220
+ st.caption(f"Samples: {audio_right.size} • Duration: {audio_right.size/FS:.2f}s")
221
+
222
+ if audio_left is not None and audio_right is not None:
223
+ margin = st.slider('Selected margin:', -1.0, 1.0, 0.26, 0.01)
224
+ verify_button = st.button("Verify speaker!")
225
+ if verify_button:
226
+ try:
227
+ verify_speakers(model, audio_left, audio_right, margin)
228
+ except Exception as e:
229
+ st.error(f"Error during verification: {e}")
src/streamlit_app_old ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import altair as alt
2
+ import numpy as np
3
+ import pandas as pd
4
+ import streamlit as st
5
+
6
+ """
7
+ # Welcome to Streamlit!
8
+
9
+ Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
+ If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
+ forums](https://discuss.streamlit.io).
12
+
13
+ In the meantime, below is an example of what you can do with just a few lines of code:
14
+ """
15
+
16
+ num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
+ num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
+
19
+ indices = np.linspace(0, 1, num_points)
20
+ theta = 2 * np.pi * num_turns * indices
21
+ radius = indices
22
+
23
+ x = radius * np.cos(theta)
24
+ y = radius * np.sin(theta)
25
+
26
+ df = pd.DataFrame({
27
+ "x": x,
28
+ "y": y,
29
+ "idx": indices,
30
+ "rand": np.random.randn(num_points),
31
+ })
32
+
33
+ st.altair_chart(alt.Chart(df, height=700, width=700)
34
+ .mark_point(filled=True)
35
+ .encode(
36
+ x=alt.X("x", axis=None),
37
+ y=alt.Y("y", axis=None),
38
+ color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
+ size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
+ ))
src/streamlit_app_old_old DELETED
@@ -1,229 +0,0 @@
1
- from pathlib import Path
2
- import ffmpeg
3
- import keras
4
- import matplotlib.pyplot as plt
5
- import numpy as np
6
- import pandas as pd
7
- import streamlit as st
8
- import tensorflow as tf
9
- from huggingface_hub import hf_hub_download
10
-
11
- # ========= App title =========
12
- st.title("Speaker Verification - Demo")
13
-
14
- # ========= Session state =========
15
- if "load_model_button" not in st.session_state:
16
- st.session_state.load_model_button = False
17
- # if "verify_speaker_button" not in st.session_state:
18
- # st.session_state.verify_speaker_button = False
19
- if "audio_left" not in st.session_state:
20
- st.session_state.audio_left = None
21
- if "audio_right" not in st.session_state:
22
- st.session_state.audio_right = None
23
-
24
- # ========= UI: choose model =========
25
- model_df = pd.DataFrame({"first column": ["verification_model_resnet34_512dim"]})
26
- option = st.selectbox("Choose model to test out:", model_df["first column"])
27
- st.button("Load the model", on_click=lambda: st.session_state.update(load_model_button=True))
28
-
29
- # ========= Helpers =========
30
- FS = 16000 # target sample rate
31
- WT = 48560 # window length in samples
32
-
33
- EXT2FMT = {
34
- "wav": "wav",
35
- "mp3": "mp3",
36
- "ogg": "ogg",
37
- "aac": "aac",
38
- "m4a": "mp4"
39
- }
40
-
41
- def infer_input_format(name: str) -> str | None:
42
- if name and "." in name:
43
- ext = name.rsplit(".", 1)[-1].lower()
44
- return EXT2FMT.get(ext)
45
- return None
46
-
47
- @st.cache_data(show_spinner=False)
48
- def bytes_to_pcm16k_mono(data: bytes, in_format: str | None) -> np.ndarray:
49
- """
50
- Konwertuje wejściowe audio (dowolny wspierany kontener) do surowego PCM 16kHz mono 16-bit LE
51
- i zwraca jako float32 w zakresie [-1, 1].
52
- Cache'owane po (bytes, format).
53
- """
54
- stream = (
55
- ffmpeg
56
- .input("pipe:0", **({"format": in_format} if in_format else {}))
57
- .output("pipe:1", format="s16le", acodec="pcm_s16le", ar=str(FS), ac=1)
58
- .global_args("-hide_banner")
59
- )
60
- out, err = ffmpeg.run(stream, capture_stdout=True, capture_stderr=True, input=data)
61
- audio = np.frombuffer(out, dtype="<i2").astype(np.float32) / 32768.0
62
- if audio.size < WT:
63
- # padding do WT
64
- audio = np.pad(audio, (int((WT - audio.size) / 2) + 1, int((WT - audio.size) / 2) + 1), mode="constant")
65
- return audio
66
-
67
- def plot_waveform(audio_np: np.ndarray, fs: int = FS, title: str = "Waveform"):
68
- t = np.arange(audio_np.size) / fs if audio_np.size else np.array([0, 1e-6])
69
- fig, ax = plt.subplots()
70
- ax.plot(t, audio_np)
71
- ax.set_title(title)
72
- ax.set_xlabel("Time [s]")
73
- ax.set_ylabel("Amplitude")
74
- ax.margins(x=0, y=0)
75
- if audio_np.size:
76
- ax.set_xlim(t[0], t[-1])
77
- return fig
78
-
79
- @st.cache_resource(show_spinner=True)
80
- def load_model_from_hub(repo_id: str, filename: str, revision: str):
81
- """Pobiera i ładuje model Keras (cache resource – trzymamy w pamięci)."""
82
- model_path = hf_hub_download(
83
- repo_id=repo_id,
84
- filename=filename,
85
- repo_type="model",
86
- revision=revision,
87
- )
88
- # Import modułu z customami, żeby rejestratory Keras się wykonały
89
- import custom_models, custom_losses # noqa: F401
90
- model = keras.models.load_model(model_path)
91
- if hasattr(model, "return_embedding"):
92
- model.return_embedding = True
93
- with open(model_path, "rb") as f:
94
- model_bytes = f.read() # do download_button (bez trzymania otwartego pliku)
95
- return model, model_path, model_bytes
96
-
97
- def handle_record(label: str) -> np.ndarray | None:
98
- rec = st.audio_input(label)
99
- if not rec:
100
- return None
101
- try:
102
- audio_np = bytes_to_pcm16k_mono(rec.getvalue(), in_format="wav")
103
- return audio_np
104
- except ffmpeg.Error as e:
105
- st.error("FFmpeg failed while processing recording.")
106
- st.code(e.stderr.decode("utf-8", "ignore"))
107
- return None
108
-
109
- def handle_upload(label: str, key: str) -> np.ndarray | None:
110
- file = st.file_uploader(
111
- label,
112
- type=["wav", "m4a", "aac", "mp3", "ogg", "webm", "flac"],
113
- key=key,
114
- )
115
- if not file:
116
- return None
117
- in_fmt = infer_input_format(file.name)
118
- try:
119
- audio_np = bytes_to_pcm16k_mono(file.getvalue(), in_fmt)
120
- return audio_np
121
- except ffmpeg.Error as e:
122
- st.error("FFmpeg failed while converting uploaded file.")
123
- st.code(e.stderr.decode("utf-8", "ignore"))
124
- return None
125
-
126
- def delta(x):
127
- """Computes first-order difference along time axis."""
128
- return x[:, 1:] - x[:, :-1]
129
-
130
- def array_to_spectrogram(audio_np: np.ndarray,
131
- audio_in_samples: int = 48560,
132
- window_length: int = 400,
133
- step_length: int = 160,
134
- fft_length: int = 1023
135
- ) -> tf.Tensor:
136
-
137
- audio = tf.convert_to_tensor(audio_np, dtype=tf.float32)
138
- audio_length = audio_np.size
139
-
140
- random_int = tf.random.uniform(shape=(), minval=0, maxval=(audio_length-audio_in_samples), dtype=tf.int32)
141
- stft = tf.signal.stft(audio[random_int:(random_int+audio_in_samples)],
142
- frame_length=window_length,
143
- frame_step=step_length,
144
- fft_length=fft_length)
145
-
146
- spectrogram = tf.abs(stft)
147
- spectrogram = tf.transpose(spectrogram) # shape: (freq, time)
148
- spectrogram = tf.math.log1p(spectrogram)
149
-
150
- spectrogram_delta = delta(spectrogram)
151
- spectrogram_delta2 = delta(spectrogram_delta)
152
-
153
- return tf.stack([spectrogram[:, :-2],
154
- spectrogram_delta[:, :-1],
155
- spectrogram_delta2],
156
- axis=-1) # shape: (freq, time, 3)
157
-
158
- @st.cache_data(show_spinner=True)
159
- def verify_speakers(model, audio_left, audio_right, margin):
160
-
161
- spec_left = array_to_spectrogram(audio_left)[tf.newaxis, ...]
162
- spec_right = array_to_spectrogram(audio_right)[tf.newaxis, ...]
163
-
164
- emb_left = model.predict(spec_left, verbose=0)
165
- emb_right = model.predict(spec_right, verbose=0)
166
-
167
- cosine_similarity = tf.linalg.matmul(emb_left, emb_right, transpose_b=True)
168
- cosine_similarity = float(cosine_similarity.numpy().squeeze())
169
-
170
- if cosine_similarity >= margin:
171
- st.success("Both voice recordings belong to the same person.")
172
- else:
173
- st.warning("The voice recordings belong to different people.")
174
- st.caption(f"Cosine similarity: {cosine_similarity:.4f}, margin: {margin:.4f}")
175
-
176
- # ========= Load model =========
177
- if st.session_state.load_model_button:
178
- try:
179
- model, model_path, model_bytes = load_model_from_hub(
180
- repo_id="2pift/sv-resnet34-keras",
181
- filename="best_model.keras",
182
- revision="v1.0.0",
183
- )
184
- st.success("Model loaded. You can now upload/record audio files.")
185
- st.download_button(
186
- "Download the model",
187
- data=model_bytes,
188
- file_name="verification_model_resnet34_512dim.keras",
189
- )
190
- except Exception as e:
191
- st.error(f"Error loading model: {e}")
192
-
193
- # ========= Two columns (symetryczne) =========
194
- left_column, right_column = st.columns(2)
195
-
196
- with left_column:
197
- st.subheader("Left input")
198
- record_left = st.checkbox("Record left input")
199
- if record_left:
200
- audio_left = handle_record("Record (left)")
201
- else:
202
- audio_left = handle_upload("Upload left audio", key="file_left")
203
- if audio_left is not None:
204
- st.session_state.audio_left = audio_left
205
- fig = plot_waveform(audio_left, FS, "Left audio waveform")
206
- st.pyplot(fig, use_container_width=True)
207
- st.caption(f"Samples: {audio_left.size} • Duration: {audio_left.size/FS:.2f}s")
208
-
209
- with right_column:
210
- st.subheader("Right input")
211
- record_right = st.checkbox("Record right input")
212
- if record_right:
213
- audio_right = handle_record("Record (right)")
214
- else:
215
- audio_right = handle_upload("Upload right audio", key="file_right")
216
- if audio_right is not None:
217
- st.session_state.audio_right = audio_right
218
- fig = plot_waveform(audio_right, FS, "Right audio waveform")
219
- st.pyplot(fig, use_container_width=True)
220
- st.caption(f"Samples: {audio_right.size} • Duration: {audio_right.size/FS:.2f}s")
221
-
222
- if audio_left is not None and audio_right is not None:
223
- margin = st.slider('Selected margin:', -1.0, 1.0, 0.26, 0.01)
224
- verify_button = st.button("Verify speaker!")
225
- if verify_button:
226
- try:
227
- verify_speakers(model, audio_left, audio_right, margin)
228
- except Exception as e:
229
- st.error(f"Error during verification: {e}")