updated files
Browse files- README.md +13 -1
- app.py +344 -0
- packages.txt +1 -0
- requirements.txt +10 -0
- runtime.txt +1 -0
- sample_utils/__init__.py +0 -0
- sample_utils/download.py +50 -0
- sample_utils/turn.py +39 -0
README.md
CHANGED
@@ -1,2 +1,14 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
Setting up a Hugging Face Spaces Demo using Streamlit
|
|
|
1 |
+
---
|
2 |
+
title: Face Emotion Recognition
|
3 |
+
emoji: 🏃
|
4 |
+
colorFrom: purple
|
5 |
+
colorTo: red
|
6 |
+
sdk: streamlit
|
7 |
+
sdk_version: 1.26.0
|
8 |
+
app_file: app.py
|
9 |
+
pinned: false
|
10 |
+
license: bsd-3-clause-clear
|
11 |
+
---
|
12 |
+
|
13 |
+
# MobileNet-SSD Object Detection Demo with Web App using Hugging Face Spaces
|
14 |
Setting up a Hugging Face Spaces Demo using Streamlit
|
app.py
ADDED
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Emotion Detection:
|
3 |
+
Model from: https://github.com/onnx/models/blob/main/vision/body_analysis/emotion_ferplus/model/emotion-ferplus-8.onnx
|
4 |
+
Model name: emotion-ferplus-8.onnx
|
5 |
+
"""
|
6 |
+
|
7 |
+
import logging
|
8 |
+
import queue
|
9 |
+
from pathlib import Path
|
10 |
+
from typing import List, NamedTuple
|
11 |
+
|
12 |
+
import cv2
|
13 |
+
import numpy as np
|
14 |
+
import time
|
15 |
+
import os
|
16 |
+
|
17 |
+
from cv2 import dnn
|
18 |
+
from math import ceil
|
19 |
+
|
20 |
+
import av
|
21 |
+
import streamlit as st
|
22 |
+
from streamlit_webrtc import WebRtcMode, webrtc_streamer
|
23 |
+
|
24 |
+
from sample_utils.download import download_file
|
25 |
+
from sample_utils.turn import get_ice_servers
|
26 |
+
|
27 |
+
HERE = Path(__file__).parent
|
28 |
+
ROOT = HERE.parent
|
29 |
+
|
30 |
+
logger = logging.getLogger(__name__)
|
31 |
+
|
32 |
+
ONNX_MODEL_URL = "https://github.com/spmallick/learnopencv/raw/master/Facial-Emotion-Recognition/emotion-ferplus-8.onnx"
|
33 |
+
ONNX_MODEL_LOCAL_PATH = ROOT / "./emotion-ferplus-8.onnx"
|
34 |
+
CAFFE_MODEL_URL = "https://github.com/spmallick/learnopencv/raw/master/Facial-Emotion-Recognition/RFB-320/RFB-320.caffemodel" # noqa: E501
|
35 |
+
CAFFE_MODEL_LOCAL_PATH = ROOT / "./RFB-320/RFB-320.caffemodel"
|
36 |
+
PROTOTXT_URL = "https://github.com/spmallick/learnopencv/raw/master/Facial-Emotion-Recognition/RFB-320/RFB-320.prototxt" # noqa: E501
|
37 |
+
PROTOTXT_LOCAL_PATH = ROOT / "./RFB-320/RFB-320.prototxt.txt"
|
38 |
+
|
39 |
+
download_file(CAFFE_MODEL_URL, CAFFE_MODEL_LOCAL_PATH) #, expected_size=23147564)
|
40 |
+
download_file(ONNX_MODEL_URL, ONNX_MODEL_LOCAL_PATH) #, expected_size=23147564)
|
41 |
+
download_file(PROTOTXT_URL, PROTOTXT_LOCAL_PATH) #, expected_size=29353)
|
42 |
+
|
43 |
+
# Session-specific caching
|
44 |
+
onnx_cache_key = "emotion_dnn"
|
45 |
+
caffe_cache_key = "face_detection_dnn"
|
46 |
+
if caffe_cache_key in st.session_state and onnx_cache_key in st.session_state:
|
47 |
+
model = st.session_state[onnx_cache_key]
|
48 |
+
net = st.session_state[caffe_cache_key]
|
49 |
+
else:
|
50 |
+
# Read ONNX model
|
51 |
+
model = 'onnx_model.onnx'
|
52 |
+
model = cv2.dnn.readNetFromONNX('emotion-ferplus-8.onnx')
|
53 |
+
st.session_state[onnx_cache_key] = model
|
54 |
+
# Read the Caffe face detector.
|
55 |
+
net = cv2.dnn.readNetFromCaffe(str(PROTOTXT_LOCAL_PATH), str(CAFFE_MODEL_LOCAL_PATH))
|
56 |
+
st.session_state[caffe_cache_key] = net
|
57 |
+
|
58 |
+
|
59 |
+
########################################
|
60 |
+
image_mean = np.array([127, 127, 127])
|
61 |
+
image_std = 128.0
|
62 |
+
iou_threshold = 0.3
|
63 |
+
center_variance = 0.1
|
64 |
+
size_variance = 0.2
|
65 |
+
min_boxes = [
|
66 |
+
[10.0, 16.0, 24.0],
|
67 |
+
[32.0, 48.0],
|
68 |
+
[64.0, 96.0],
|
69 |
+
[128.0, 192.0, 256.0]
|
70 |
+
]
|
71 |
+
strides = [8.0, 16.0, 32.0, 64.0]
|
72 |
+
threshold = 0.5
|
73 |
+
|
74 |
+
emotion_dict = {
|
75 |
+
0: 'neutral',
|
76 |
+
1: 'happiness',
|
77 |
+
2: 'surprise',
|
78 |
+
3: 'sadness',
|
79 |
+
4: 'anger',
|
80 |
+
5: 'disgust',
|
81 |
+
6: 'fear'
|
82 |
+
}
|
83 |
+
########################################
|
84 |
+
|
85 |
+
def define_img_size(image_size):
|
86 |
+
shrinkage_list = []
|
87 |
+
feature_map_w_h_list = []
|
88 |
+
for size in image_size:
|
89 |
+
feature_map = [int(ceil(size / stride)) for stride in strides]
|
90 |
+
feature_map_w_h_list.append(feature_map)
|
91 |
+
|
92 |
+
for i in range(0, len(image_size)):
|
93 |
+
shrinkage_list.append(strides)
|
94 |
+
priors = generate_priors(
|
95 |
+
feature_map_w_h_list, shrinkage_list, image_size, min_boxes
|
96 |
+
)
|
97 |
+
return priors
|
98 |
+
|
99 |
+
|
100 |
+
def generate_priors(
|
101 |
+
feature_map_list, shrinkage_list, image_size, min_boxes
|
102 |
+
):
|
103 |
+
priors = []
|
104 |
+
for index in range(0, len(feature_map_list[0])):
|
105 |
+
scale_w = image_size[0] / shrinkage_list[0][index]
|
106 |
+
scale_h = image_size[1] / shrinkage_list[1][index]
|
107 |
+
for j in range(0, feature_map_list[1][index]):
|
108 |
+
for i in range(0, feature_map_list[0][index]):
|
109 |
+
x_center = (i + 0.5) / scale_w
|
110 |
+
y_center = (j + 0.5) / scale_h
|
111 |
+
|
112 |
+
for min_box in min_boxes[index]:
|
113 |
+
w = min_box / image_size[0]
|
114 |
+
h = min_box / image_size[1]
|
115 |
+
priors.append([
|
116 |
+
x_center,
|
117 |
+
y_center,
|
118 |
+
w,
|
119 |
+
h
|
120 |
+
])
|
121 |
+
print("priors nums:{}".format(len(priors)))
|
122 |
+
return np.clip(priors, 0.0, 1.0)
|
123 |
+
|
124 |
+
|
125 |
+
def hard_nms(box_scores, iou_threshold, top_k=-1, candidate_size=200):
|
126 |
+
scores = box_scores[:, -1]
|
127 |
+
boxes = box_scores[:, :-1]
|
128 |
+
picked = []
|
129 |
+
indexes = np.argsort(scores)
|
130 |
+
indexes = indexes[-candidate_size:]
|
131 |
+
while len(indexes) > 0:
|
132 |
+
current = indexes[-1]
|
133 |
+
picked.append(current)
|
134 |
+
if 0 < top_k == len(picked) or len(indexes) == 1:
|
135 |
+
break
|
136 |
+
current_box = boxes[current, :]
|
137 |
+
indexes = indexes[:-1]
|
138 |
+
rest_boxes = boxes[indexes, :]
|
139 |
+
iou = iou_of(
|
140 |
+
rest_boxes,
|
141 |
+
np.expand_dims(current_box, axis=0),
|
142 |
+
)
|
143 |
+
indexes = indexes[iou <= iou_threshold]
|
144 |
+
return box_scores[picked, :]
|
145 |
+
|
146 |
+
|
147 |
+
def area_of(left_top, right_bottom):
|
148 |
+
hw = np.clip(right_bottom - left_top, 0.0, None)
|
149 |
+
return hw[..., 0] * hw[..., 1]
|
150 |
+
|
151 |
+
|
152 |
+
def iou_of(boxes0, boxes1, eps=1e-5):
|
153 |
+
overlap_left_top = np.maximum(boxes0[..., :2], boxes1[..., :2])
|
154 |
+
overlap_right_bottom = np.minimum(boxes0[..., 2:], boxes1[..., 2:])
|
155 |
+
|
156 |
+
overlap_area = area_of(overlap_left_top, overlap_right_bottom)
|
157 |
+
area0 = area_of(boxes0[..., :2], boxes0[..., 2:])
|
158 |
+
area1 = area_of(boxes1[..., :2], boxes1[..., 2:])
|
159 |
+
return overlap_area / (area0 + area1 - overlap_area + eps)
|
160 |
+
|
161 |
+
|
162 |
+
def predict(
|
163 |
+
width,
|
164 |
+
height,
|
165 |
+
confidences,
|
166 |
+
boxes,
|
167 |
+
prob_threshold,
|
168 |
+
iou_threshold=0.3,
|
169 |
+
top_k=-1
|
170 |
+
):
|
171 |
+
boxes = boxes[0]
|
172 |
+
confidences = confidences[0]
|
173 |
+
picked_box_probs = []
|
174 |
+
picked_labels = []
|
175 |
+
for class_index in range(1, confidences.shape[1]):
|
176 |
+
probs = confidences[:, class_index]
|
177 |
+
mask = probs > prob_threshold
|
178 |
+
probs = probs[mask]
|
179 |
+
if probs.shape[0] == 0:
|
180 |
+
continue
|
181 |
+
subset_boxes = boxes[mask, :]
|
182 |
+
box_probs = np.concatenate(
|
183 |
+
[subset_boxes, probs.reshape(-1, 1)], axis=1
|
184 |
+
)
|
185 |
+
box_probs = hard_nms(box_probs,
|
186 |
+
iou_threshold=iou_threshold,
|
187 |
+
top_k=top_k,
|
188 |
+
)
|
189 |
+
picked_box_probs.append(box_probs)
|
190 |
+
picked_labels.extend([class_index] * box_probs.shape[0])
|
191 |
+
if not picked_box_probs:
|
192 |
+
return np.array([]), np.array([]), np.array([])
|
193 |
+
picked_box_probs = np.concatenate(picked_box_probs)
|
194 |
+
picked_box_probs[:, 0] *= width
|
195 |
+
picked_box_probs[:, 1] *= height
|
196 |
+
picked_box_probs[:, 2] *= width
|
197 |
+
picked_box_probs[:, 3] *= height
|
198 |
+
return (
|
199 |
+
picked_box_probs[:, :4].astype(np.int32),
|
200 |
+
np.array(picked_labels),
|
201 |
+
picked_box_probs[:, 4]
|
202 |
+
)
|
203 |
+
|
204 |
+
|
205 |
+
def convert_locations_to_boxes(locations, priors, center_variance,
|
206 |
+
size_variance):
|
207 |
+
if len(priors.shape) + 1 == len(locations.shape):
|
208 |
+
priors = np.expand_dims(priors, 0)
|
209 |
+
return np.concatenate([
|
210 |
+
locations[..., :2] * center_variance * priors[..., 2:] + priors[..., :2],
|
211 |
+
np.exp(locations[..., 2:] * size_variance) * priors[..., 2:]
|
212 |
+
], axis=len(locations.shape) - 1)
|
213 |
+
|
214 |
+
|
215 |
+
def center_form_to_corner_form(locations):
|
216 |
+
return np.concatenate(
|
217 |
+
[locations[..., :2] - locations[..., 2:] / 2,
|
218 |
+
locations[..., :2] + locations[..., 2:] / 2],
|
219 |
+
len(locations.shape) - 1
|
220 |
+
)
|
221 |
+
|
222 |
+
|
223 |
+
# def FER_live_cam():
|
224 |
+
def video_frame_callback(frame: av.VideoFrame) -> av.VideoFrame:
|
225 |
+
frame = frame.to_ndarray(format="bgr24")
|
226 |
+
|
227 |
+
# cap = cv2.VideoCapture('video3.mp4')
|
228 |
+
# cap = cv2.VideoCapture(0)
|
229 |
+
|
230 |
+
# frame_width = int(cap.get(3))
|
231 |
+
# frame_height = int(cap.get(4))
|
232 |
+
# size = (frame_width, frame_height)
|
233 |
+
# result = cv2.VideoWriter('infer2-test.avi',
|
234 |
+
# cv2.VideoWriter_fourcc(*'MJPG'),
|
235 |
+
# 10, size)
|
236 |
+
# Read ONNX model
|
237 |
+
# model = 'onnx_model.onnx'
|
238 |
+
# model = cv2.dnn.readNetFromONNX('emotion-ferplus-8.onnx')
|
239 |
+
|
240 |
+
# # Read the Caffe face detector.
|
241 |
+
# model_path = 'RFB-320/RFB-320.caffemodel'
|
242 |
+
# proto_path = 'RFB-320/RFB-320.prototxt'
|
243 |
+
# net = dnn.readNetFromCaffe(proto_path, model_path)
|
244 |
+
|
245 |
+
input_size = [320, 240]
|
246 |
+
width = input_size[0]
|
247 |
+
height = input_size[1]
|
248 |
+
priors = define_img_size(input_size)
|
249 |
+
|
250 |
+
# while cap.isOpened():
|
251 |
+
# ret, frame = cap.read()
|
252 |
+
# if ret:
|
253 |
+
img_ori = frame
|
254 |
+
#print("frame size: ", frame.shape)
|
255 |
+
rect = cv2.resize(img_ori, (width, height))
|
256 |
+
rect = cv2.cvtColor(rect, cv2.COLOR_BGR2RGB)
|
257 |
+
net.setInput(dnn.blobFromImage(rect, 1 / image_std, (width, height), 127))
|
258 |
+
start_time = time.time()
|
259 |
+
boxes, scores = net.forward(["boxes", "scores"])
|
260 |
+
boxes = np.expand_dims(np.reshape(boxes, (-1, 4)), axis=0)
|
261 |
+
scores = np.expand_dims(np.reshape(scores, (-1, 2)), axis=0)
|
262 |
+
boxes = convert_locations_to_boxes(
|
263 |
+
boxes, priors, center_variance, size_variance
|
264 |
+
)
|
265 |
+
boxes = center_form_to_corner_form(boxes)
|
266 |
+
boxes, labels, probs = predict(
|
267 |
+
img_ori.shape[1],
|
268 |
+
img_ori.shape[0],
|
269 |
+
scores,
|
270 |
+
boxes,
|
271 |
+
threshold
|
272 |
+
)
|
273 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
274 |
+
for (x1, y1, x2, y2) in boxes:
|
275 |
+
w = x2 - x1
|
276 |
+
h = y2 - y1
|
277 |
+
cv2.rectangle(frame, (x1,y1), (x2, y2), (255,0,0), 2)
|
278 |
+
resize_frame = cv2.resize(
|
279 |
+
gray[y1:y1 + h, x1:x1 + w], (64, 64)
|
280 |
+
)
|
281 |
+
resize_frame = resize_frame.reshape(1, 1, 64, 64)
|
282 |
+
model.setInput(resize_frame)
|
283 |
+
output = model.forward()
|
284 |
+
end_time = time.time()
|
285 |
+
fps = 1 / (end_time - start_time)
|
286 |
+
print(f"FPS: {fps:.1f}")
|
287 |
+
pred = emotion_dict[list(output[0]).index(max(output[0]))]
|
288 |
+
cv2.rectangle(
|
289 |
+
img_ori,
|
290 |
+
(x1, y1),
|
291 |
+
(x2, y2),
|
292 |
+
(215, 5, 247),
|
293 |
+
2,
|
294 |
+
lineType=cv2.LINE_AA
|
295 |
+
)
|
296 |
+
cv2.putText(
|
297 |
+
frame,
|
298 |
+
pred,
|
299 |
+
(x1, y1-10),
|
300 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
301 |
+
0.8,
|
302 |
+
(215, 5, 247),
|
303 |
+
2,
|
304 |
+
lineType=cv2.LINE_AA
|
305 |
+
)
|
306 |
+
|
307 |
+
# result.write(frame)
|
308 |
+
|
309 |
+
# cv2.imshow('frame', frame)
|
310 |
+
# if cv2.waitKey(1) & 0xFF == ord('q'):
|
311 |
+
# break
|
312 |
+
# else:
|
313 |
+
# break
|
314 |
+
|
315 |
+
# cap.release()
|
316 |
+
# result.release()
|
317 |
+
# cv2.destroyAllWindows()
|
318 |
+
return av.VideoFrame.from_ndarray(frame, format="bgr24")
|
319 |
+
|
320 |
+
if __name__ == "__main__":
|
321 |
+
# FER_live_cam()
|
322 |
+
webrtc_ctx = webrtc_streamer(
|
323 |
+
key="object-detection",
|
324 |
+
mode=WebRtcMode.SENDRECV,
|
325 |
+
rtc_configuration={"iceServers": get_ice_servers()},
|
326 |
+
video_frame_callback=video_frame_callback,
|
327 |
+
media_stream_constraints={"video": True, "audio": False},
|
328 |
+
async_processing=True,
|
329 |
+
)
|
330 |
+
|
331 |
+
if st.checkbox("Show the detected labels", value=True):
|
332 |
+
if webrtc_ctx.state.playing:
|
333 |
+
labels_placeholder = st.empty()
|
334 |
+
# NOTE: The video transformation with object detection and
|
335 |
+
# this loop displaying the result labels are running
|
336 |
+
# in different threads asynchronously.
|
337 |
+
# Then the rendered video frames and the labels displayed here
|
338 |
+
# are not strictly synchronized.
|
339 |
+
|
340 |
+
st.markdown(
|
341 |
+
"This demo uses a model and code from "
|
342 |
+
"https://github.com/spmallick/learnopncv/Facial-Emotion-Recognition. "
|
343 |
+
"Many thanks to the project."
|
344 |
+
)
|
packages.txt
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
ffmpeg
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
tqdm
|
2 |
+
numpy
|
3 |
+
torch
|
4 |
+
torchvision
|
5 |
+
Pillow
|
6 |
+
opencv-python-headless==4.8.0.76
|
7 |
+
streamlit_webrtc==0.47.0
|
8 |
+
pydub==0.25.1
|
9 |
+
twilio~=8.5.0
|
10 |
+
matplotlib
|
runtime.txt
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
python-3.8.7
|
sample_utils/__init__.py
ADDED
File without changes
|
sample_utils/download.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import urllib.request
|
2 |
+
from pathlib import Path
|
3 |
+
|
4 |
+
import streamlit as st
|
5 |
+
|
6 |
+
|
7 |
+
# This code is based on https://github.com/streamlit/demo-self-driving/blob/230245391f2dda0cb464008195a470751c01770b/streamlit_app.py#L48 # noqa: E501
|
8 |
+
def download_file(url, download_to: Path, expected_size=None):
|
9 |
+
# Don't download the file twice.
|
10 |
+
# (If possible, verify the download using the file length.)
|
11 |
+
if download_to.exists():
|
12 |
+
if expected_size:
|
13 |
+
if download_to.stat().st_size == expected_size:
|
14 |
+
return
|
15 |
+
else:
|
16 |
+
st.info(f"{url} is already downloaded.")
|
17 |
+
if not st.button("Download again?"):
|
18 |
+
return
|
19 |
+
|
20 |
+
download_to.parent.mkdir(parents=True, exist_ok=True)
|
21 |
+
|
22 |
+
# These are handles to two visual elements to animate.
|
23 |
+
weights_warning, progress_bar = None, None
|
24 |
+
try:
|
25 |
+
weights_warning = st.warning("Downloading %s..." % url)
|
26 |
+
progress_bar = st.progress(0)
|
27 |
+
with open(download_to, "wb") as output_file:
|
28 |
+
with urllib.request.urlopen(url) as response:
|
29 |
+
length = int(response.info()["Content-Length"])
|
30 |
+
counter = 0.0
|
31 |
+
MEGABYTES = 2.0 ** 20.0
|
32 |
+
while True:
|
33 |
+
data = response.read(8192)
|
34 |
+
if not data:
|
35 |
+
break
|
36 |
+
counter += len(data)
|
37 |
+
output_file.write(data)
|
38 |
+
|
39 |
+
# We perform animation by overwriting the elements.
|
40 |
+
weights_warning.warning(
|
41 |
+
"Downloading %s... (%6.2f/%6.2f MB)"
|
42 |
+
% (url, counter / MEGABYTES, length / MEGABYTES)
|
43 |
+
)
|
44 |
+
progress_bar.progress(min(counter / length, 1.0))
|
45 |
+
# Finally, we remove these visual elements by calling .empty().
|
46 |
+
finally:
|
47 |
+
if weights_warning is not None:
|
48 |
+
weights_warning.empty()
|
49 |
+
if progress_bar is not None:
|
50 |
+
progress_bar.empty()
|
sample_utils/turn.py
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import os
|
3 |
+
|
4 |
+
import streamlit as st
|
5 |
+
from twilio.base.exceptions import TwilioRestException
|
6 |
+
from twilio.rest import Client
|
7 |
+
|
8 |
+
logger = logging.getLogger(__name__)
|
9 |
+
|
10 |
+
|
11 |
+
def get_ice_servers():
|
12 |
+
"""Use Twilio's TURN server because Streamlit Community Cloud has changed
|
13 |
+
its infrastructure and WebRTC connection cannot be established without TURN server now. # noqa: E501
|
14 |
+
We considered Open Relay Project (https://www.metered.ca/tools/openrelay/) too,
|
15 |
+
but it is not stable and hardly works as some people reported like https://github.com/aiortc/aiortc/issues/832#issuecomment-1482420656 # noqa: E501
|
16 |
+
See https://github.com/whitphx/streamlit-webrtc/issues/1213
|
17 |
+
"""
|
18 |
+
|
19 |
+
# Ref: https://www.twilio.com/docs/stun-turn/api
|
20 |
+
try:
|
21 |
+
account_sid = os.environ["TWILIO_ACCOUNT_SID"]
|
22 |
+
auth_token = os.environ["TWILIO_AUTH_TOKEN"]
|
23 |
+
except KeyError:
|
24 |
+
logger.warning(
|
25 |
+
"Twilio credentials are not set. Fallback to a free STUN server from Google." # noqa: E501
|
26 |
+
)
|
27 |
+
return [{"urls": ["stun:stun.l.google.com:19302"]}]
|
28 |
+
|
29 |
+
client = Client(account_sid, auth_token)
|
30 |
+
|
31 |
+
try:
|
32 |
+
token = client.tokens.create()
|
33 |
+
except TwilioRestException as e:
|
34 |
+
st.warning(
|
35 |
+
f"Error occurred while accessing Twilio API. Fallback to a free STUN server from Google. ({e})" # noqa: E501
|
36 |
+
)
|
37 |
+
return [{"urls": ["stun:stun.l.google.com:19302"]}]
|
38 |
+
|
39 |
+
return token.ice_servers
|