Audio events (#6848)
* Initial audio classification model implementation * fix mypy * Keep audio labelmap local * Cleanup * Start adding config for audio * Add the detector * Add audio detection process keypoints * Build out base config * Load labelmap correctly * Fix config bugs * Start audio process * Fix startup issues * Try to cleanup restarting * Add ffmpeg input args * Get audio detection working * Save event to db * End events if not heard for 30 seconds * Use not heard config * Stop ffmpeg when shutting down * Fixes * End events correctly * Use api instead of event queue to save audio events * Get events working * Close threads when stop event is sent * remove unused * Only start audio process if at least one camera is enabled * Add const for float * Cleanup labelmap * Add audio icon in frontend * Add ability to toggle audio with mqtt * Set initial audio value * Fix audio enabling * Close logpipe * Isort * Formatting * Fix web tests * Fix web tests * Handle cases where args are a string * Remove log * Cleanup process close * Use correct field * Simplify if statement * Use var for localhost * Add audio detectors docs * Add restream docs to mention audio detection * Add full config docs * Fix links to other docs --------- Co-authored-by: Jason Hunter <hunterjm@gmail.com>pull/6978/head
parent
f1dc3a639c
commit
c3b313a70d
28 changed files with 1090 additions and 69 deletions
@ -0,0 +1,521 @@ |
||||
speech |
||||
speech |
||||
speech |
||||
speech |
||||
babbling |
||||
speech |
||||
yell |
||||
bellow |
||||
whoop |
||||
yell |
||||
yell |
||||
yell |
||||
whispering |
||||
laughter |
||||
laughter |
||||
laughter |
||||
snicker |
||||
laughter |
||||
laughter |
||||
crying |
||||
crying |
||||
crying |
||||
yell |
||||
sigh |
||||
singing |
||||
choir |
||||
sodeling |
||||
chant |
||||
mantra |
||||
child_singing |
||||
synthetic_singing |
||||
rapping |
||||
humming |
||||
groan |
||||
grunt |
||||
whistling |
||||
breathing |
||||
wheeze |
||||
snoring |
||||
gasp |
||||
pant |
||||
snort |
||||
cough |
||||
throat_clearing |
||||
sneeze |
||||
sniff |
||||
run |
||||
shuffle |
||||
footsteps |
||||
chewing |
||||
biting |
||||
gargling |
||||
stomach_rumble |
||||
burping |
||||
hiccup |
||||
fart |
||||
hands |
||||
finger_snapping |
||||
clapping |
||||
heartbeat |
||||
heart_murmur |
||||
cheering |
||||
applause |
||||
chatter |
||||
crowd |
||||
speech |
||||
children_playing |
||||
animal |
||||
pets |
||||
dog |
||||
bark |
||||
yip |
||||
howl |
||||
bow-wow |
||||
growling |
||||
whimper_dog |
||||
cat |
||||
purr |
||||
meow |
||||
hiss |
||||
caterwaul |
||||
livestock |
||||
horse |
||||
clip-clop |
||||
neigh |
||||
cattle |
||||
moo |
||||
cowbell |
||||
pig |
||||
oink |
||||
goat |
||||
bleat |
||||
sheep |
||||
fowl |
||||
chicken |
||||
cluck |
||||
cock-a-doodle-doo |
||||
turkey |
||||
gobble |
||||
duck |
||||
quack |
||||
goose |
||||
honk |
||||
wild_animals |
||||
roaring_cats |
||||
roar |
||||
bird |
||||
chird |
||||
chirp |
||||
squawk |
||||
pigeon |
||||
coo |
||||
crow |
||||
caw |
||||
owl |
||||
hoot |
||||
flapping_wings |
||||
dogs |
||||
rats |
||||
mouse |
||||
patter |
||||
insect |
||||
cricket |
||||
mosquito |
||||
fly |
||||
buzz |
||||
buzz |
||||
frog |
||||
croak |
||||
snake |
||||
rattle |
||||
whale_vocalization |
||||
music |
||||
musical_instrument |
||||
plucked_string_instrument |
||||
guitar |
||||
electric_guitar |
||||
bass_guitar |
||||
acoustic_guitar |
||||
steel_guitar |
||||
tapping |
||||
strum |
||||
banjo |
||||
sitar |
||||
mandolin |
||||
zither |
||||
ukulele |
||||
keyboard |
||||
piano |
||||
electric_piano |
||||
organ |
||||
electronic_organ |
||||
hammond_organ |
||||
synthesizer |
||||
sampler |
||||
harpsichord |
||||
percussion |
||||
drum_kit |
||||
drum_machine |
||||
drum |
||||
snare_drum |
||||
rimshot |
||||
drum_roll |
||||
bass_drum |
||||
timpani |
||||
tabla |
||||
cymbal |
||||
hi-hat |
||||
wood_block |
||||
tambourine |
||||
rattle |
||||
maraca |
||||
gong |
||||
tubular_bells |
||||
mallet_percussion |
||||
marimba |
||||
glockenspiel |
||||
vibraphone |
||||
steelpan |
||||
orchestra |
||||
brass_instrument |
||||
french_horn |
||||
trumpet |
||||
trombone |
||||
bowed_string_instrument |
||||
string_section |
||||
violin |
||||
pizzicato |
||||
cello |
||||
double_bass |
||||
wind_instrument |
||||
flute |
||||
saxophone |
||||
clarinet |
||||
harp |
||||
bell |
||||
church_bell |
||||
jingle_bell |
||||
bicycle_bell |
||||
tuning_fork |
||||
chime |
||||
wind_chime |
||||
change_ringing |
||||
harmonica |
||||
accordion |
||||
bagpipes |
||||
didgeridoo |
||||
shofar |
||||
theremin |
||||
singing_bowl |
||||
scratching |
||||
pop_music |
||||
hip_hop_music |
||||
beatboxing |
||||
rock_music |
||||
heavy_metal |
||||
punk_rock |
||||
grunge |
||||
progressive_rock |
||||
rock_and_roll |
||||
psychedelic_rock |
||||
rhythm_and_blues |
||||
soul_music |
||||
reggae |
||||
country |
||||
swing_music |
||||
bluegrass |
||||
funk |
||||
folk_music |
||||
middle_eastern_music |
||||
jazz |
||||
disco |
||||
classical_music |
||||
opera |
||||
electronic_music |
||||
house_music |
||||
techno |
||||
dubstep |
||||
drum_and_bass |
||||
electronica |
||||
electronic_dance_music |
||||
ambient_music |
||||
trance_music |
||||
music_of_latin_america |
||||
salsa_music |
||||
flamenco |
||||
blues |
||||
music_for_children |
||||
new-age_music |
||||
vocal_music |
||||
a_capella |
||||
music_of_africa |
||||
afrobeat |
||||
christian_music |
||||
gospel_music |
||||
music_of_asia |
||||
carnatic_music |
||||
music_of_bollywood |
||||
ska |
||||
traditional_music |
||||
independent_music |
||||
song |
||||
background_music |
||||
theme_music |
||||
jingle |
||||
soundtrack_music |
||||
lullaby |
||||
video_game_music |
||||
christmas_music |
||||
dance_music |
||||
wedding_music |
||||
happy_music |
||||
sad_music |
||||
tender_music |
||||
exciting_music |
||||
angry_music |
||||
scary_music |
||||
wind |
||||
rustling_leaves |
||||
wind_noise |
||||
thunderstorm |
||||
thunder |
||||
water |
||||
rain |
||||
raindrop |
||||
rain_on_surface |
||||
stream |
||||
waterfall |
||||
ocean |
||||
waves |
||||
steam |
||||
gurgling |
||||
fire |
||||
crackle |
||||
vehicle |
||||
boat |
||||
sailboat |
||||
rowboat |
||||
motorboat |
||||
ship |
||||
motor_vehicle |
||||
car |
||||
honk |
||||
toot |
||||
car_alarm |
||||
power_windows |
||||
skidding |
||||
tire_squeal |
||||
car_passing_by |
||||
race_car |
||||
truck |
||||
air_brake |
||||
air_horn |
||||
reversing_beeps |
||||
ice_cream_truck |
||||
bus |
||||
emergency_vehicle |
||||
police_car |
||||
ambulance |
||||
fire_engine |
||||
motorcycle |
||||
traffic_noise |
||||
rail_transport |
||||
train |
||||
train_whistle |
||||
train_horn |
||||
railroad_car |
||||
train_wheels_squealing |
||||
subway |
||||
aircraft |
||||
aircraft_engine |
||||
jet_engine |
||||
propeller |
||||
helicopter |
||||
fixed-wing_aircraft |
||||
bicycle |
||||
skateboard |
||||
engine |
||||
light_engine |
||||
dental_drill's_drill |
||||
lawn_mower |
||||
chainsaw |
||||
medium_engine |
||||
heavy_engine |
||||
engine_knocking |
||||
engine_starting |
||||
idling |
||||
accelerating |
||||
door |
||||
doorbell |
||||
ding-dong |
||||
sliding_door |
||||
slam |
||||
knock |
||||
tap |
||||
squeak |
||||
cupboard_open_or_close |
||||
drawer_open_or_close |
||||
dishes |
||||
cutlery |
||||
chopping |
||||
frying |
||||
microwave_oven |
||||
blender |
||||
water_tap |
||||
sink |
||||
bathtub |
||||
hair_dryer |
||||
toilet_flush |
||||
toothbrush |
||||
electric_toothbrush |
||||
vacuum_cleaner |
||||
zipper |
||||
keys_jangling |
||||
coin |
||||
scissors |
||||
electric_shaver |
||||
shuffling_cards |
||||
typing |
||||
typewriter |
||||
computer_keyboard |
||||
writing |
||||
alarm |
||||
telephone |
||||
telephone_bell_ringing |
||||
ringtone |
||||
telephone_dialing |
||||
dial_tone |
||||
busy_signal |
||||
alarm_clock |
||||
siren |
||||
civil_defense_siren |
||||
buzzer |
||||
smoke_detector |
||||
fire_alarm |
||||
foghorn |
||||
whistle |
||||
steam_whistle |
||||
mechanisms |
||||
ratchet |
||||
clock |
||||
tick |
||||
tick-tock |
||||
gears |
||||
pulleys |
||||
sewing_machine |
||||
mechanical_fan |
||||
air_conditioning |
||||
cash_register |
||||
printer |
||||
camera |
||||
single-lens_reflex_camera |
||||
tools |
||||
hammer |
||||
jackhammer |
||||
sawing |
||||
filing |
||||
sanding |
||||
power_tool |
||||
drill |
||||
explosion |
||||
gunshot |
||||
machine_gun |
||||
fusillade |
||||
artillery_fire |
||||
cap_gun |
||||
fireworks |
||||
firecracker |
||||
burst |
||||
eruption |
||||
boom |
||||
wood |
||||
chop |
||||
splinter |
||||
crack |
||||
glass |
||||
chink |
||||
shatter |
||||
liquid |
||||
splash |
||||
slosh |
||||
squish |
||||
drip |
||||
pour |
||||
trickle |
||||
gush |
||||
fill |
||||
spray |
||||
pump |
||||
stir |
||||
boiling |
||||
sonar |
||||
arrow |
||||
whoosh |
||||
thump |
||||
thunk |
||||
electronic_tuner |
||||
effects_unit |
||||
chorus_effect |
||||
basketball_bounce |
||||
bang |
||||
slap |
||||
whack |
||||
smash |
||||
breaking |
||||
bouncing |
||||
whip |
||||
flap |
||||
scratch |
||||
scrape |
||||
rub |
||||
roll |
||||
crushing |
||||
crumpling |
||||
tearing |
||||
beep |
||||
ping |
||||
ding |
||||
clang |
||||
squeal |
||||
creak |
||||
rustle |
||||
whir |
||||
clatter |
||||
sizzle |
||||
clicking |
||||
clickety-clack |
||||
rumble |
||||
plop |
||||
jingle |
||||
hum |
||||
zing |
||||
boing |
||||
crunch |
||||
silence |
||||
sine_wave |
||||
harmonic |
||||
chirp_tone |
||||
sound_effect |
||||
pulse |
||||
inside |
||||
inside |
||||
inside |
||||
outside |
||||
outside |
||||
reverberation |
||||
echo |
||||
noise |
||||
environmental_noise |
||||
static |
||||
mains_hum |
||||
distortion |
||||
sidetone |
||||
cacophony |
||||
white_noise |
||||
pink_noise |
||||
throbbing |
||||
vibration |
||||
television |
||||
radio |
||||
field_recording |
@ -0,0 +1,63 @@ |
||||
--- |
||||
id: audio_detectors |
||||
title: Audio Detectors |
||||
--- |
||||
|
||||
Frigate provides a builtin audio detector which runs on the CPU. Compared to object detection in images, audio detection is a relatively lightweight operation so the only option is to run the detection on a CPU. |
||||
|
||||
## Configuration |
||||
|
||||
Audio events work by detecting a type of audio and creating an event, the event will end once the type of audio has not been heard for the configured amount of time. Audio events save a snapshot at the beginning of the event as well as recordings throughout the event. The recordings are retained using the configured recording retention. |
||||
|
||||
### Enabling Audio Events |
||||
|
||||
Audio events can be enabled for all cameras or only for specific cameras. |
||||
|
||||
```yaml |
||||
|
||||
audio: # <- enable audio events for all camera |
||||
enabled: True |
||||
|
||||
cameras: |
||||
front_camera: |
||||
ffmpeg: |
||||
... |
||||
audio: |
||||
enabled: True # <- enable audio events for the front_camera |
||||
``` |
||||
|
||||
If you are using multiple streams then you must set the `audio` role on the stream that is going to be used for audio detection, this can be any stream but the stream must have audio included. |
||||
|
||||
:::note |
||||
|
||||
The ffmpeg process for capturing audio will be a separate connection to the camera along with the other roles assigned to the camera, for this reason it is recommended that the go2rtc restream is used for this purpose. See [the restream docs](/configuration/restream.md) for more information. |
||||
|
||||
::: |
||||
|
||||
```yaml |
||||
cameras: |
||||
front_camera: |
||||
ffmpeg: |
||||
inputs: |
||||
- path: rtsp://.../main_stream |
||||
roles: |
||||
- record |
||||
- path: rtsp://.../sub_stream # <- this stream must have audio enabled |
||||
roles: |
||||
- audio |
||||
- detect |
||||
``` |
||||
|
||||
### Configuring Audio Events |
||||
|
||||
The included audio model has over 500 different types of audio that can be detected, many of which are not practical. By default `bark`, `speech`, `yell`, and `scream` are enabled but these can be customized. |
||||
|
||||
```yaml |
||||
audio: |
||||
enabled: True |
||||
listen: |
||||
- bark |
||||
- scream |
||||
- speech |
||||
- yell |
||||
``` |
@ -0,0 +1,247 @@ |
||||
"""Handle creating audio events.""" |
||||
|
||||
import datetime |
||||
import logging |
||||
import multiprocessing as mp |
||||
import os |
||||
import signal |
||||
import threading |
||||
from types import FrameType |
||||
from typing import Optional |
||||
|
||||
import numpy as np |
||||
import requests |
||||
from setproctitle import setproctitle |
||||
|
||||
from frigate.config import CameraConfig, FrigateConfig |
||||
from frigate.const import ( |
||||
AUDIO_DURATION, |
||||
AUDIO_FORMAT, |
||||
AUDIO_MAX_BIT_RANGE, |
||||
AUDIO_SAMPLE_RATE, |
||||
CACHE_DIR, |
||||
FRIGATE_LOCALHOST, |
||||
) |
||||
from frigate.ffmpeg_presets import parse_preset_input |
||||
from frigate.log import LogPipe |
||||
from frigate.object_detection import load_labels |
||||
from frigate.types import FeatureMetricsTypes |
||||
from frigate.util import get_ffmpeg_arg_list, listen |
||||
from frigate.video import start_or_restart_ffmpeg, stop_ffmpeg |
||||
|
||||
try: |
||||
from tflite_runtime.interpreter import Interpreter |
||||
except ModuleNotFoundError: |
||||
from tensorflow.lite.python.interpreter import Interpreter |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
def get_ffmpeg_command(input_args: list[str], input_path: str, pipe: str) -> list[str]: |
||||
return get_ffmpeg_arg_list( |
||||
f"ffmpeg {{}} -i {{}} -f {AUDIO_FORMAT} -ar {AUDIO_SAMPLE_RATE} -ac 1 -y {{}}".format( |
||||
" ".join(input_args), |
||||
input_path, |
||||
pipe, |
||||
) |
||||
) |
||||
|
||||
|
||||
def listen_to_audio( |
||||
config: FrigateConfig, |
||||
process_info: dict[str, FeatureMetricsTypes], |
||||
) -> None: |
||||
stop_event = mp.Event() |
||||
audio_threads: list[threading.Thread] = [] |
||||
|
||||
def exit_process() -> None: |
||||
for thread in audio_threads: |
||||
thread.join() |
||||
|
||||
logger.info("Exiting audio detector...") |
||||
|
||||
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: |
||||
stop_event.set() |
||||
exit_process() |
||||
|
||||
signal.signal(signal.SIGTERM, receiveSignal) |
||||
signal.signal(signal.SIGINT, receiveSignal) |
||||
|
||||
threading.current_thread().name = "process:audio_manager" |
||||
setproctitle("frigate.audio_manager") |
||||
listen() |
||||
|
||||
for camera in config.cameras.values(): |
||||
if camera.enabled and camera.audio.enabled_in_config: |
||||
audio = AudioEventMaintainer(camera, process_info, stop_event) |
||||
audio_threads.append(audio) |
||||
audio.start() |
||||
|
||||
|
||||
class AudioTfl: |
||||
def __init__(self, stop_event: mp.Event): |
||||
self.stop_event = stop_event |
||||
self.labels = load_labels("/audio-labelmap.txt") |
||||
self.interpreter = Interpreter( |
||||
model_path="/cpu_audio_model.tflite", |
||||
num_threads=2, |
||||
) |
||||
|
||||
self.interpreter.allocate_tensors() |
||||
|
||||
self.tensor_input_details = self.interpreter.get_input_details() |
||||
self.tensor_output_details = self.interpreter.get_output_details() |
||||
|
||||
def _detect_raw(self, tensor_input): |
||||
self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input) |
||||
self.interpreter.invoke() |
||||
detections = np.zeros((20, 6), np.float32) |
||||
|
||||
res = self.interpreter.get_tensor(self.tensor_output_details[0]["index"])[0] |
||||
non_zero_indices = res > 0 |
||||
class_ids = np.argpartition(-res, 20)[:20] |
||||
class_ids = class_ids[np.argsort(-res[class_ids])] |
||||
class_ids = class_ids[non_zero_indices[class_ids]] |
||||
scores = res[class_ids] |
||||
boxes = np.full((scores.shape[0], 4), -1, np.float32) |
||||
count = len(scores) |
||||
|
||||
for i in range(count): |
||||
if scores[i] < 0.4 or i == 20: |
||||
break |
||||
detections[i] = [ |
||||
class_ids[i], |
||||
float(scores[i]), |
||||
boxes[i][0], |
||||
boxes[i][1], |
||||
boxes[i][2], |
||||
boxes[i][3], |
||||
] |
||||
|
||||
return detections |
||||
|
||||
def detect(self, tensor_input, threshold=0.8): |
||||
detections = [] |
||||
|
||||
if self.stop_event.is_set(): |
||||
return detections |
||||
|
||||
raw_detections = self._detect_raw(tensor_input) |
||||
|
||||
for d in raw_detections: |
||||
if d[1] < threshold: |
||||
break |
||||
detections.append( |
||||
(self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5])) |
||||
) |
||||
return detections |
||||
|
||||
|
||||
class AudioEventMaintainer(threading.Thread): |
||||
def __init__( |
||||
self, |
||||
camera: CameraConfig, |
||||
feature_metrics: dict[str, FeatureMetricsTypes], |
||||
stop_event: mp.Event, |
||||
) -> None: |
||||
threading.Thread.__init__(self) |
||||
self.name = f"{camera.name}_audio_event_processor" |
||||
self.config = camera |
||||
self.feature_metrics = feature_metrics |
||||
self.detections: dict[dict[str, any]] = feature_metrics |
||||
self.stop_event = stop_event |
||||
self.detector = AudioTfl(stop_event) |
||||
self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),) |
||||
self.chunk_size = int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE * 2)) |
||||
self.pipe = f"{CACHE_DIR}/{self.config.name}-audio" |
||||
self.ffmpeg_cmd = get_ffmpeg_command( |
||||
get_ffmpeg_arg_list(self.config.ffmpeg.global_args) |
||||
+ parse_preset_input("preset-rtsp-audio-only", 1), |
||||
[i.path for i in self.config.ffmpeg.inputs if "audio" in i.roles][0], |
||||
self.pipe, |
||||
) |
||||
self.pipe_file = None |
||||
self.logpipe = LogPipe(f"ffmpeg.{self.config.name}.audio") |
||||
self.audio_listener = None |
||||
|
||||
def detect_audio(self, audio) -> None: |
||||
if not self.feature_metrics[self.config.name]["audio_enabled"].value: |
||||
return |
||||
|
||||
waveform = (audio / AUDIO_MAX_BIT_RANGE).astype(np.float32) |
||||
model_detections = self.detector.detect(waveform) |
||||
|
||||
for label, score, _ in model_detections: |
||||
if label not in self.config.audio.listen: |
||||
continue |
||||
|
||||
self.handle_detection(label, score) |
||||
|
||||
self.expire_detections() |
||||
|
||||
def handle_detection(self, label: str, score: float) -> None: |
||||
if self.detections.get(label): |
||||
self.detections[label][ |
||||
"last_detection" |
||||
] = datetime.datetime.now().timestamp() |
||||
else: |
||||
resp = requests.post( |
||||
f"{FRIGATE_LOCALHOST}/api/events/{self.config.name}/{label}/create", |
||||
json={"duration": None}, |
||||
) |
||||
|
||||
if resp.status_code == 200: |
||||
event_id = resp.json()[0]["event_id"] |
||||
self.detections[label] = { |
||||
"id": event_id, |
||||
"label": label, |
||||
"last_detection": datetime.datetime.now().timestamp(), |
||||
} |
||||
|
||||
def expire_detections(self) -> None: |
||||
now = datetime.datetime.now().timestamp() |
||||
|
||||
for detection in self.detections.values(): |
||||
if ( |
||||
now - detection.get("last_detection", now) |
||||
> self.config.audio.max_not_heard |
||||
): |
||||
self.detections[detection["label"]] = None |
||||
requests.put( |
||||
f"{FRIGATE_LOCALHOST}/api/events/{detection['id']}/end", |
||||
json={ |
||||
"end_time": detection["last_detection"] |
||||
+ self.config.record.events.post_capture |
||||
}, |
||||
) |
||||
|
||||
def restart_audio_pipe(self) -> None: |
||||
try: |
||||
os.mkfifo(self.pipe) |
||||
except FileExistsError: |
||||
pass |
||||
|
||||
self.audio_listener = start_or_restart_ffmpeg( |
||||
self.ffmpeg_cmd, logger, self.logpipe, None, self.audio_listener |
||||
) |
||||
|
||||
def read_audio(self) -> None: |
||||
if self.pipe_file is None: |
||||
self.pipe_file = open(self.pipe, "rb") |
||||
|
||||
try: |
||||
audio = np.frombuffer(self.pipe_file.read(self.chunk_size), dtype=np.int16) |
||||
self.detect_audio(audio) |
||||
except BrokenPipeError: |
||||
self.logpipe.dump() |
||||
self.restart_audio_pipe() |
||||
|
||||
def run(self) -> None: |
||||
self.restart_audio_pipe() |
||||
|
||||
while not self.stop_event.is_set(): |
||||
self.read_audio() |
||||
|
||||
self.pipe_file.close() |
||||
stop_ffmpeg(self.audio_listener, logger) |
||||
self.logpipe.close() |
@ -0,0 +1,36 @@ |
||||
import { h } from 'preact'; |
||||
import { memo } from 'preact/compat'; |
||||
|
||||
export function Snapshot({ className = 'h-6 w-6', stroke = 'currentColor', onClick = () => {} }) { |
||||
return ( |
||||
<svg |
||||
xmlns="http://www.w3.org/2000/svg" |
||||
className={className} |
||||
fill="none" |
||||
viewBox="0 0 32 32" |
||||
stroke={stroke} |
||||
onClick={onClick} |
||||
> |
||||
<path |
||||
stroke-linecap="round" |
||||
stroke-linejoin="round" |
||||
stroke-width="2" |
||||
d="M18 30v-2a10.011 10.011 0 0010-10h2a12.013 12.013 0 01-12 12z" |
||||
/> |
||||
<path |
||||
stroke-linecap="round" |
||||
stroke-linejoin="round" |
||||
stroke-width="2" |
||||
d="M18 22v-2a2.002 2.002 0 002-2h2a4.004 4.004 0 01-4 4zM10 2a9.01 9.01 0 00-9 9h2a7 7 0 0114 0 7.09 7.09 0 01-3.501 6.135l-.499.288v3.073a2.935 2.935 0 01-.9 2.151 4.182 4.182 0 01-4.633 1.03A4.092 4.092 0 015 20H3a6.116 6.116 0 003.67 5.512 5.782 5.782 0 002.314.486 6.585 6.585 0 004.478-1.888A4.94 4.94 0 0015 20.496v-1.942A9.108 9.108 0 0019 11a9.01 9.01 0 00-9-9z" |
||||
/> |
||||
<path |
||||
stroke-linecap="round" |
||||
stroke-linejoin="round" |
||||
stroke-width="2" |
||||
d="M9.28 8.082A3.006 3.006 0 0113 11h2a4.979 4.979 0 00-1.884-3.911 5.041 5.041 0 00-4.281-.957 4.95 4.95 0 00-3.703 3.703 5.032 5.032 0 002.304 5.458A3.078 3.078 0 019 17.924V20h2v-2.077a5.06 5.06 0 00-2.537-4.346 3.002 3.002 0 01.817-5.494z" |
||||
/> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default memo(Snapshot); |
Loading…
Reference in new issue