update v0.2.0

This commit is contained in:
杍超
2025-04-01 16:04:53 +08:00
198 changed files with 27674 additions and 2392 deletions

11
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View File

@@ -1,73 +1,73 @@
<script lang="ts">
import { playable } from "./shared/utils";
import { type FileData } from "@gradio/client";
import { playable } from "./shared/utils";
import { type FileData } from "@gradio/client";
export let type: "gallery" | "table";
export let selected = false;
export let value: { video: FileData; subtitles: FileData | null } | null;
export let loop: boolean;
let video: HTMLVideoElement;
export let type: "gallery" | "table";
export let selected = false;
export let value: { video: FileData; subtitles: FileData | null } | null;
export let loop: boolean;
let video: HTMLVideoElement;
async function init(): Promise<void> {
video.muted = true;
video.playsInline = true;
video.controls = false;
video.setAttribute("muted", "");
async function init(): Promise<void> {
video.muted = true;
video.playsInline = true;
video.controls = false;
video.setAttribute("muted", "");
await video.play();
video.pause();
}
await video.play();
video.pause();
}
</script>
{#if value}
{#if playable()}
<div
class="container"
class:table={type === "table"}
class:gallery={type === "gallery"}
class:selected
>
<video
bind:this={video}
on:loadeddata={init}
on:mouseover={video.play.bind(video)}
on:mouseout={video.pause.bind(video)}
src={value?.video.url}
/>
</div>
{:else}
<div>{value}</div>
{/if}
{#if playable()}
<div
class="container"
class:table={type === "table"}
class:gallery={type === "gallery"}
class:selected
>
<video
bind:this={video}
on:loadeddata={init}
on:mouseover={video.play.bind(video)}
on:mouseout={video.pause.bind(video)}
src={value?.video.url}
/>
</div>
{:else}
<div>{value}</div>
{/if}
{/if}
<style>
.container {
flex: none;
max-width: none;
}
.container :global(video) {
width: var(--size-full);
height: var(--size-full);
object-fit: cover;
}
.container {
flex: none;
max-width: none;
}
.container :global(video) {
width: var(--size-full);
height: var(--size-full);
object-fit: cover;
}
.container:hover,
.container.selected {
border-color: var(--border-color-accent);
}
.container.table {
margin: 0 auto;
border: 2px solid var(--border-color-primary);
border-radius: var(--radius-lg);
overflow: hidden;
width: var(--size-20);
height: var(--size-20);
object-fit: cover;
}
.container:hover,
.container.selected {
border-color: var(--border-color-accent);
}
.container.table {
margin: 0 auto;
border: 2px solid var(--border-color-primary);
border-radius: var(--radius-lg);
overflow: hidden;
width: var(--size-20);
height: var(--size-20);
object-fit: cover;
}
.container.gallery {
height: var(--size-20);
max-height: var(--size-20);
object-fit: cover;
}
.container.gallery {
height: var(--size-20);
max-height: var(--size-20);
object-fit: cover;
}
</style>

View File

@@ -1,57 +1,84 @@
<svelte:options accessors={true} />
<script lang="ts">
import { Block, UploadText } from "@gradio/atoms";
import Video from "./shared/InteractiveVideo.svelte";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
import StaticVideo from "./shared/StaticVideo.svelte";
import StaticAudio from "./shared/StaticAudio.svelte";
import InteractiveAudio from "./shared/InteractiveAudio.svelte";
import VideoChat from './shared/VideoChat.svelte'
export let elem_id = "";
export let elem_classes: string[] = [];
export let visible = true;
export let value: string = "__webrtc_value__";
export let button_labels: {start: string, stop: string, waiting: string};
import { Block, UploadText } from "@gradio/atoms";
import Video from "./shared/InteractiveVideo.svelte";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
import StaticVideo from "./shared/StaticVideo.svelte";
import StaticAudio from "./shared/StaticAudio.svelte";
import InteractiveAudio from "./shared/InteractiveAudio.svelte";
import VideoChat from './shared/VideoChat/index.svelte'
export let label: string;
export let root: string;
export let show_label: boolean;
export let loading_status: LoadingStatus;
export let height: number | undefined;
export let width: number | undefined;
export let server: {
offer: (body: any) => Promise<any>;
};
export let video_chat = false;
export let elem_id = "";
export let elem_classes: string[] = [];
export let visible = true;
export let value: string = "__webrtc_value__";
export let button_labels: { start: string; stop: string; waiting: string };
export let container = false;
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let gradio;
export let rtc_configuration: Object;
export let time_limit: number | null = null;
export let modality: "video" | "audio" | "audio-video" = "video";
export let mode: "send-receive" | "receive" | "send" = "send-receive";
export let show_local_video: string | undefined = undefined;
export let video_chat: boolean = false;
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let track_constraints: MediaTrackConstraints = {};
export let icon: string | undefined = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
export let label: string;
export let root: string;
export let show_label: boolean;
export let loading_status: LoadingStatus;
export let height: number | undefined;
export let width: number | undefined;
export let server: {
offer: (body: any) => Promise<any>;
};
const on_change_cb = (msg: "change" | "tick" | any) => {
if (msg?.type === "info" || msg?.type === "warning" || msg?.type === "error") {
console.log("dispatching info", msg.message);
gradio.dispatch(msg?.type === "error"? "error": "warning", msg.message);
}
gradio.dispatch(msg === "change" ? "state_change" : "tick");
}
export let container = false;
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let gradio;
export let rtc_configuration: Object;
export let time_limit: number | null = null;
export let modality: "video" | "audio" | "audio-video" = "video";
export let mode: "send-receive" | "receive" | "send" = "send-receive";
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let track_constraints: MediaTrackConstraints = {};
export let icon: string | undefined = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
export let icon_radius: number = 50;
let dragging = false;
const on_change_cb = (msg: "change" | "tick" | any) => {
if (
msg?.type === "info" ||
msg?.type === "warning" ||
msg?.type === "error"
) {
gradio.dispatch(msg?.type === "error" ? "error" : "warning", msg.message);
} else if (msg?.type === "fetch_output") {
gradio.dispatch("state_change");
} else if (msg?.type === "send_input") {
gradio.dispatch("tick");
} else if (msg?.type === "connection_timeout") {
gradio.dispatch(
"warning",
"Taking a while to connect. Are you on a VPN?",
);
}
if (msg.type === "state_change") {
gradio.dispatch(msg === "change" ? "state_change" : "tick");
}
};
$: console.log("value", value);
const reject_cb = (msg: object) => {
if (
msg.status === "failed" &&
msg.meta?.error === "concurrency_limit_reached"
) {
gradio.dispatch(
"error",
`Too many concurrent connections. Please try again later!`,
);
} else {
gradio.dispatch("error", "Unexpected server error");
}
};
let dragging = false;
</script>
{#if video_chat}
@@ -89,108 +116,111 @@
</Block>
{:else}<Block
{visible}
variant={"solid"}
border_mode={dragging ? "focus" : "base"}
padding={false}
{elem_id}
{elem_classes}
{height}
{width}
{container}
{scale}
{min_width}
allow_overflow={false}
>
<StatusTracker
autoscroll={gradio.autoscroll}
i18n={gradio.i18n}
{...loading_status}
on:clear_status={() => gradio.dispatch("clear_status", loading_status)}
/>
{visible}
variant={"solid"}
border_mode={dragging ? "focus" : "base"}
padding={false}
{elem_id}
{elem_classes}
{height}
{width}
{container}
{scale}
{min_width}
allow_overflow={false}
>
<StatusTracker
autoscroll={gradio.autoscroll}
i18n={gradio.i18n}
{...loading_status}
on:clear_status={() => gradio.dispatch("clear_status", loading_status)}
/>
{#if mode == "receive" && modality === "video"}
<StaticVideo
bind:value={value}
{on_change_cb}
{label}
{show_label}
{server}
{rtc_configuration}
on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)}
/>
{:else if mode == "receive" && modality === "audio"}
<StaticAudio
bind:value={value}
{on_change_cb}
{label}
{show_label}
{server}
{rtc_configuration}
{icon}
{icon_button_color}
{pulse_color}
i18n={gradio.i18n}
on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)}
/>
{:else if (mode === "send-receive" || mode == "send") && (modality === "video" || modality == "audio-video")}
<Video
bind:value={value}
{label}
{show_label}
active_source={"webcam"}
include_audio={modality === "audio-video"}
show_local_video={mode === "send-receive" && modality === "audio-video" && show_local_video}
{server}
{rtc_configuration}
{time_limit}
{mode}
{track_constraints}
{rtp_params}
{on_change_cb}
{icon}
{icon_button_color}
{pulse_color}
{button_labels}
on:clear={() => gradio.dispatch("clear")}
on:play={() => gradio.dispatch("play")}
on:pause={() => gradio.dispatch("pause")}
on:upload={() => gradio.dispatch("upload")}
on:stop={() => gradio.dispatch("stop")}
on:end={() => gradio.dispatch("end")}
on:start_recording={() => gradio.dispatch("start_recording")}
on:stop_recording={() => gradio.dispatch("stop_recording")}
on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)}
i18n={gradio.i18n}
stream_handler={(...args) => gradio.client.stream(...args)}
>
<UploadText i18n={gradio.i18n} type="video" />
</Video>
{:else if (mode === "send-receive" || mode === "send") && modality === "audio"}
<InteractiveAudio
bind:value={value}
{on_change_cb}
{label}
{show_label}
{server}
{rtc_configuration}
{time_limit}
{track_constraints}
{mode}
{rtp_params}
i18n={gradio.i18n}
{icon}
{icon_button_color}
{pulse_color}
{button_labels}
on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)}
on:warning={({ detail }) => gradio.dispatch("warning", detail)}
/>
{/if}
{#if mode == "receive" && modality === "video"}
<StaticVideo
bind:value
{on_change_cb}
{label}
{show_label}
{server}
{rtc_configuration}
on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)}
/>
{:else if mode == "receive" && modality === "audio"}
<StaticAudio
bind:value
{on_change_cb}
{label}
{show_label}
{server}
{rtc_configuration}
{icon}
{icon_button_color}
{pulse_color}
{icon_radius}
i18n={gradio.i18n}
on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)}
/>
{:else if (mode === "send-receive" || mode == "send") && (modality === "video" || modality == "audio-video")}
<Video
bind:value
{label}
{show_label}
active_source={"webcam"}
include_audio={modality === "audio-video"}
{server}
{rtc_configuration}
{time_limit}
{mode}
{track_constraints}
{rtp_params}
{on_change_cb}
{reject_cb}
{icon}
{icon_button_color}
{pulse_color}
{icon_radius}
{button_labels}
on:clear={() => gradio.dispatch("clear")}
on:play={() => gradio.dispatch("play")}
on:pause={() => gradio.dispatch("pause")}
on:upload={() => gradio.dispatch("upload")}
on:stop={() => gradio.dispatch("stop")}
on:end={() => gradio.dispatch("end")}
on:start_recording={() => gradio.dispatch("start_recording")}
on:stop_recording={() => gradio.dispatch("stop_recording")}
on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)}
i18n={gradio.i18n}
stream_handler={(...args) => gradio.client.stream(...args)}
>
<UploadText i18n={gradio.i18n} type="video" />
</Video>
{:else if (mode === "send-receive" || mode === "send") && modality === "audio"}
<InteractiveAudio
bind:value
{on_change_cb}
{label}
{show_label}
{server}
{rtc_configuration}
{time_limit}
{track_constraints}
{mode}
{rtp_params}
i18n={gradio.i18n}
{icon}
{reject_cb}
{icon_button_color}
{icon_radius}
{pulse_color}
{button_labels}
on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)}
on:warning={({ detail }) => gradio.dispatch("warning", detail)}
/>
{/if}
</Block>
{/if}

4471
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,13 +19,16 @@
"@gradio/utils": "0.7.0",
"@gradio/wasm": "0.14.2",
"hls.js": "^1.5.16",
"mrmime": "^2.0.0"
"mrmime": "^2.0.0",
"cookie": "^1.0.2",
"terser": "^5.14.2"
},
"devDependencies": {
"@gradio/preview": "0.12.0",
"less": "^4.2.2",
"prettier": "3.3.3",
"vite-plugin-commonjs": "^0.10.4"
"vite-plugin-commonjs": "^0.10.4",
"prettier-plugin-svelte": "^3.3.3"
},
"exports": {
"./package.json": "./package.json",

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import type {ComponentType} from 'svelte';
import { onDestroy } from "svelte";
import type { ComponentType } from "svelte";
import PulsingIcon from './PulsingIcon.svelte';
import PulsingIcon from "./PulsingIcon.svelte";
export let numBars = 16;
export let stream_state: "open" | "closed" | "waiting" = "closed";
@@ -16,13 +16,13 @@
let analyser: AnalyserNode;
let dataArray: Uint8Array;
let animationId: number;
let pulseScale = 1;
export let pulseScale = 1;
$: containerWidth = icon
$: containerWidth = icon
? "128px"
: `calc((var(--boxSize) + var(--gutter)) * ${numBars} + 80px)`;
$: if(stream_state === "open") setupAudioContext();
$: if (stream_state === "open") setupAudioContext();
onDestroy(() => {
if (animationId) {
@@ -34,12 +34,17 @@
});
function setupAudioContext() {
// @ts-ignore
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
const source = audioContext.createMediaStreamSource(audio_source_callback());
const streamSource = audio_source_callback()
if(!streamSource)return
const source = audioContext.createMediaStreamSource(
streamSource,
);
source.connect(analyser);
analyser.fftSize = 64;
analyser.smoothingTimeConstant = 0.8;
dataArray = new Uint8Array(analyser.frequencyBinCount);
@@ -73,21 +78,23 @@
</script>
<div class="gradio-webrtc-waveContainer">
{#if icon}
<div class="gradio-webrtc-icon-container">
<div
class="gradio-webrtc-icon"
style:transform={`scale(${pulseScale})`}
style:background={icon_button_color}
>
<PulsingIcon
{stream_state}
{pulse_color}
{icon}
{icon_button_color}
{audio_source_callback}/>
{#if icon && !pending}
<div class="gradio-webrtc-icon-container">
<div
class="gradio-webrtc-icon"
style:transform={`scale(${pulseScale})`}
style:background={icon_button_color}
>
<PulsingIcon
{stream_state}
{pulse_color}
{icon}
{icon_button_color}
{icon_radius}
{audio_source_callback}
/>
</div>
</div>
</div>
{:else}
<div class="gradio-webrtc-boxContainer" style:width={containerWidth}>
{#each Array(numBars/2) as _}
@@ -102,14 +109,14 @@
</div>
<style>
.gradio-webrtc-waveContainer {
position: relative;
display: flex;
min-height: 100px;
max-height: 128px;
justify-content: center;
align-items: center;
}
.gradio-webrtc-waveContainer {
position: relative;
display: flex;
min-height: 100px;
max-height: 128px;
justify-content: center;
align-items: center;
}
.gradio-webrtc-boxContainer {
display: flex;
@@ -130,47 +137,47 @@
transition: transform 0.05s ease;
}
.gradio-webrtc-icon-container {
position: relative;
width: 128px;
height: 128px;
display: flex;
justify-content: center;
align-items: center;
}
.gradio-webrtc-icon-container {
position: relative;
width: 128px;
height: 128px;
display: flex;
justify-content: center;
align-items: center;
}
.gradio-webrtc-icon {
position: relative;
width: 48px;
height: 48px;
border-radius: 50%;
transition: transform 0.1s ease;
display: flex;
justify-content: center;
align-items: center;
z-index: 2;
}
.gradio-webrtc-icon {
position: relative;
width: 48px;
height: 48px;
border-radius: 50%;
transition: transform 0.1s ease;
display: flex;
justify-content: center;
align-items: center;
z-index: 2;
}
.icon-image {
width: 32px;
height: 32px;
object-fit: contain;
filter: brightness(0) invert(1);
}
.icon-image {
width: 32px;
height: 32px;
object-fit: contain;
filter: brightness(0) invert(1);
}
.pulse-ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
border-radius: 50%;
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
opacity: 0.5;
}
.pulse-ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
border-radius: 50%;
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
opacity: 0.5;
}
@keyframes pulse {
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.5;
@@ -180,4 +187,39 @@
opacity: 0;
}
}
</style>
.dots {
display: flex;
gap: 8px;
align-items: center;
height: 64px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
opacity: 0.5;
animation: pulse 1.5s infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes pulse {
0%,
100% {
opacity: 0.4;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.1);
}
}
</style>

View File

@@ -1,454 +1,580 @@
<script lang="ts">
import {
BlockLabel,
} from "@gradio/atoms";
import type { I18nFormatter } from "@gradio/utils";
import { createEventDispatcher } from "svelte";
import { onMount } from "svelte";
import { fade } from "svelte/transition";
import { StreamingBar } from "@gradio/statustracker";
import {
Circle,
Square,
Spinner,
Music,
DropdownArrow,
Microphone
} from "@gradio/icons";
import { BlockLabel } from "@gradio/atoms";
import type { I18nFormatter } from "@gradio/utils";
import { createEventDispatcher } from "svelte";
import { onMount } from "svelte";
import { fade } from "svelte/transition";
import { StreamingBar } from "@gradio/statustracker";
import {
Circle,
Spinner,
Music,
DropdownArrow,
VolumeMuted,
VolumeHigh,
Microphone,
} from "@gradio/icons";
import MicrophoneMuted from "./MicrophoneMuted.svelte";
import { start, stop } from "./webrtc_utils";
import { get_devices, set_available_devices } from "./stream_utils";
import AudioWave from "./AudioWave.svelte";
import WebcamPermissions from "./WebcamPermissions.svelte";
import { start, stop } from "./webrtc_utils";
import { get_devices, set_available_devices } from "./stream_utils";
import AudioWave from "./AudioWave.svelte";
import WebcamPermissions from "./WebcamPermissions.svelte";
import PulsingIcon from "./PulsingIcon.svelte";
export let mode: "send-receive" | "send";
export let value: string | null = null;
export let label: string | undefined = undefined;
export let show_label = true;
export let rtc_configuration: Object | null = null;
export let i18n: I18nFormatter;
export let time_limit: number | null = null;
export let track_constraints: MediaTrackConstraints = {};
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let on_change_cb: (mg: "tick" | "change") => void;
export let reject_cb: (msg: object) => void;
export let icon: string | undefined = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
export let icon_radius: number = 50;
export let button_labels: { start: string; stop: string; waiting: string };
let pending = false;
export let mode: "send-receive" | "send";
export let value: string | null = null;
export let label: string | undefined = undefined;
export let show_label = true;
export let rtc_configuration: Object | null = null;
export let i18n: I18nFormatter;
export let time_limit: number | null = null;
export let track_constraints: MediaTrackConstraints = {};
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let on_change_cb: (mg: "tick" | "change") => void;
export let icon: string | undefined = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
export let button_labels: {start: string, stop: string, waiting: string};
let stopword_recognized = false;
let stopword_recognized = false;
let notification_sound;
let notification_sound;
onMount(() => {
if (value === "__webrtc_value__") {
notification_sound = new Audio(
"https://huggingface.co/datasets/freddyaboulton/bucket/resolve/main/pop-sounds.mp3",
);
}
});
onMount(() => {
if (value === "__webrtc_value__") {
notification_sound = new Audio("https://huggingface.co/datasets/freddyaboulton/bucket/resolve/main/pop-sounds.mp3");
}
let _on_change_cb = (msg: "change" | "tick" | "stopword") => {
if (msg === "stopword") {
stopword_recognized = true;
setTimeout(() => {
stopword_recognized = false;
}, 3000);
} else {
console.debug("calling on_change_cb with msg", msg);
on_change_cb(msg);
}
};
let options_open = false;
let _time_limit: number | null = null;
export let server: {
offer: (body: any) => Promise<any>;
};
let stream_state: "open" | "closed" | "waiting" = "closed";
let audio_player: HTMLAudioElement;
let pc: RTCPeerConnection;
let _webrtc_id = null;
let stream: MediaStream;
let available_audio_devices: MediaDeviceInfo[];
let selected_device: MediaDeviceInfo | null = null;
let mic_accessed = false;
let is_muted = false;
let is_mic_muted = false;
const audio_source_callback = () => {
if (mode === "send") return stream;
else return audio_player.srcObject as MediaStream;
};
const dispatch = createEventDispatcher<{
tick: undefined;
state_change: undefined;
error: string;
play: undefined;
stop: undefined;
}>();
async function access_mic(): Promise<void> {
try {
const constraints = selected_device
? {
deviceId: { exact: selected_device.deviceId },
...track_constraints,
}
: track_constraints;
const stream_ = await navigator.mediaDevices.getUserMedia({
audio: constraints,
});
stream = stream_;
} catch (err) {
if (!navigator.mediaDevices) {
dispatch("error", i18n("audio.no_device_support"));
return;
}
if (err instanceof DOMException && err.name == "NotAllowedError") {
dispatch("error", i18n("audio.allow_recording_access"));
return;
}
throw err;
}
available_audio_devices = set_available_devices(
await get_devices(),
"audioinput",
);
mic_accessed = true;
const used_devices = stream
.getTracks()
.map((track) => track.getSettings()?.deviceId)[0];
selected_device = used_devices
? available_audio_devices.find(
(device) => device.deviceId === used_devices,
) || available_audio_devices[0]
: available_audio_devices[0];
}
async function start_stream(): Promise<void> {
if (stream_state === "open") {
stop(pc);
stream_state = "closed";
_time_limit = null;
await access_mic();
return;
}
_webrtc_id = Math.random().toString(36).substring(2);
value = _webrtc_id;
pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange", async (event) => {
switch (pc.connectionState) {
case "connected":
console.info("connected");
stream_state = "open";
_time_limit = time_limit;
break;
case "disconnected":
console.info("closed");
stream_state = "closed";
_time_limit = null;
stop(pc);
break;
case "failed":
console.info("failed");
stream_state = "closed";
_time_limit = null;
dispatch("error", "Connection failed!");
stop(pc);
break;
default:
break;
}
});
stream_state = "waiting";
stream = null;
let _on_change_cb = (msg: "change" | "tick" | "stopword") => {
console.log("msg", msg);
if (msg === "stopword") {
console.log("stopword recognized");
stopword_recognized = true;
setTimeout(() => {
stopword_recognized = false;
}, 3000);
} else {
console.log("calling on_change_cb with msg", msg);
on_change_cb(msg);
}
try {
await access_mic();
} catch (err) {
if (!navigator.mediaDevices) {
dispatch("error", i18n("audio.no_device_support"));
return;
}
if (err instanceof DOMException && err.name == "NotAllowedError") {
dispatch("error", i18n("audio.allow_recording_access"));
return;
}
throw err;
}
if (stream == null) return;
const additional_message_cb = (msg: object) => {
// @ts-ignore
if (msg.type === "log" && msg.data === "pause_detected") {
pending = true;
// @ts-ignore
} else if (msg.type === "log" && msg.data === "response_starting") {
pending = false;
}
};
let options_open = false;
const timeoutId = setTimeout(() => {
// @ts-ignore
_on_change_cb({ type: "connection_timeout" });
}, 5000);
let _time_limit: number | null = null;
export let server: {
offer: (body: any) => Promise<any>;
start(
stream,
pc,
mode === "send" ? null : audio_player,
server.offer,
_webrtc_id,
"audio",
_on_change_cb,
rtp_params,
additional_message_cb,
reject_cb,
)
.then(([connection]) => {
clearTimeout(timeoutId);
pc = connection;
})
.catch(() => {
console.info("catching");
clearTimeout(timeoutId);
stream_state = "closed";
});
}
function handle_click_outside(event: MouseEvent): void {
event.preventDefault();
event.stopPropagation();
options_open = false;
}
function click_outside(node: Node, cb: any): any {
const handle_click = (event: MouseEvent): void => {
if (
node &&
!node.contains(event.target as Node) &&
!event.defaultPrevented
) {
cb(event);
}
};
let stream_state: "open" | "closed" | "waiting" = "closed";
let audio_player: HTMLAudioElement;
let pc: RTCPeerConnection;
let _webrtc_id = null;
let stream: MediaStream;
let available_audio_devices: MediaDeviceInfo[];
let selected_device: MediaDeviceInfo | null = null;
let mic_accessed = false;
document.addEventListener("click", handle_click, true);
const audio_source_callback = () => {
console.log("stream in callback", stream);
if(mode==="send") return stream;
else return audio_player.srcObject as MediaStream
return {
destroy() {
document.removeEventListener("click", handle_click, true);
},
};
}
const handle_device_change = async (event: InputEvent): Promise<void> => {
const target = event.target as HTMLInputElement;
const device_id = target.value;
stream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: device_id }, ...track_constraints },
});
selected_device =
available_audio_devices.find((device) => device.deviceId === device_id) ||
null;
options_open = false;
};
function toggleMute(): void {
if (audio_player) {
audio_player.muted = !audio_player.muted;
is_muted = audio_player.muted;
}
}
const dispatch = createEventDispatcher<{
tick: undefined;
state_change: undefined;
error: string
play: undefined;
stop: undefined;
}>();
async function access_mic(): Promise<void> {
try {
const constraints = selected_device ? { deviceId: { exact: selected_device.deviceId }, ...track_constraints } : track_constraints;
const stream_ = await navigator.mediaDevices.getUserMedia({ audio: constraints });
stream = stream_;
} catch (err) {
if (!navigator.mediaDevices) {
dispatch("error", i18n("audio.no_device_support"));
return;
}
if (err instanceof DOMException && err.name == "NotAllowedError") {
dispatch("error", i18n("audio.allow_recording_access"));
return;
}
throw err;
}
available_audio_devices = set_available_devices(await get_devices(), "audioinput");
mic_accessed = true;
const used_devices = stream
.getTracks()
.map((track) => track.getSettings()?.deviceId)[0];
selected_device = used_devices
? available_audio_devices.find((device) => device.deviceId === used_devices) ||
available_audio_devices[0]
: available_audio_devices[0];
}
async function start_stream(): Promise<void> {
if( stream_state === "open"){
stop(pc);
stream_state = "closed";
_time_limit = null;
await access_mic();
return;
}
_webrtc_id = Math.random().toString(36).substring(2);
value = _webrtc_id;
pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange",
async (event) => {
switch(pc.connectionState) {
case "connected":
console.info("connected");
stream_state = "open";
_time_limit = time_limit;
break;
case "disconnected":
console.info("closed");
stream_state = "closed";
_time_limit = null;
stop(pc);
break;
default:
break;
}
}
)
stream_state = "waiting"
stream = null
try {
await access_mic();
} catch (err) {
if (!navigator.mediaDevices) {
dispatch("error", i18n("audio.no_device_support"));
return;
}
if (err instanceof DOMException && err.name == "NotAllowedError") {
dispatch("error", i18n("audio.allow_recording_access"));
return;
}
throw err;
}
if (stream == null) return;
start(stream, pc, mode === "send" ? null: audio_player, server.offer, _webrtc_id, "audio", _on_change_cb, rtp_params).then((connection) => {
pc = connection;
}).catch(() => {
console.info("catching")
dispatch("error", "Too many concurrent users. Come back later!");
});
}
function handle_click_outside(event: MouseEvent): void {
event.preventDefault();
event.stopPropagation();
options_open = false;
}
function click_outside(node: Node, cb: any): any {
const handle_click = (event: MouseEvent): void => {
if (
node &&
!node.contains(event.target as Node) &&
!event.defaultPrevented
) {
cb(event);
}
};
document.addEventListener("click", handle_click, true);
return {
destroy() {
document.removeEventListener("click", handle_click, true);
}
};
}
const handle_device_change = async (event: InputEvent): Promise<void> => {
const target = event.target as HTMLInputElement;
const device_id = target.value;
stream = await navigator.mediaDevices.getUserMedia({ audio: {deviceId: { exact: device_id }, ...track_constraints }});
selected_device =
available_audio_devices.find(
(device) => device.deviceId === device_id
) || null;
options_open = false;
};
$: if(stopword_recognized){
notification_sound.play();
function toggleMuteMicrophone(): void {
if (stream && stream.getAudioTracks().length > 0) {
const audioTrack = stream.getAudioTracks()[0];
audioTrack.enabled = !audioTrack.enabled;
is_mic_muted = !audioTrack.enabled;
}
}
$: if (stopword_recognized) {
notification_sound.play();
}
</script>
<BlockLabel
{show_label}
Icon={Music}
float={false}
label={label || i18n("audio.audio")}
{show_label}
Icon={Music}
float={false}
label={label || i18n("audio.audio")}
/>
<div class="audio-container">
<audio
class="standard-player"
class:hidden={value === "__webrtc_value__"}
on:load
bind:this={audio_player}
on:ended={() => dispatch("stop")}
on:play={() => dispatch("play")}
<audio
class="standard-player"
class:hidden={value === "__webrtc_value__"}
on:load
bind:this={audio_player}
on:ended={() => dispatch("stop")}
on:play={() => dispatch("play")}
/>
{#if !mic_accessed}
<div
in:fade={{ delay: 100, duration: 200 }}
title="grant webcam access"
style="height: 100%"
>
<WebcamPermissions
icon={Microphone}
on:click={async () => access_mic()}
/>
</div>
{:else}
<AudioWave
{audio_source_callback}
{stream_state}
{icon}
{icon_button_color}
{pulse_color}
{pending}
{icon_radius}
/>
{#if !mic_accessed}
<div
in:fade={{ delay: 100, duration: 200 }}
title="grant webcam access"
style="height: 100%"
>
<WebcamPermissions icon={Microphone} on:click={async () => access_mic()} />
</div>
{:else}
<AudioWave {audio_source_callback} {stream_state} {icon} {icon_button_color} {pulse_color}/>
<StreamingBar time_limit={_time_limit} />
<div class="button-wrap" class:pulse={stopword_recognized}>
<button
on:click={start_stream}
aria-label={"start stream"}
>
{#if stream_state === "waiting"}
<div class="icon-with-text">
<div class="icon color-primary" title="spinner">
<Spinner />
</div>
{button_labels.waiting || i18n("audio.waiting")}
</div>
{:else if stream_state === "open"}
<div class="icon-with-text">
<div class="icon color-primary" title="stop recording">
<Square />
</div>
{button_labels.stop || i18n("audio.stop")}
</div>
{:else}
<div class="icon-with-text">
<div class="icon color-primary" title="start recording">
<Circle />
</div>
{button_labels.start || i18n("audio.record")}
</div>
{/if}
</button>
{#if stream_state === "closed"}
<button
class="icon"
on:click={() => (options_open = true)}
aria-label="select input source"
>
<DropdownArrow />
</button>
<StreamingBar time_limit={_time_limit} />
<div class="button-wrap" class:pulse={stopword_recognized}>
<button on:click={start_stream} aria-label={"start stream"}>
{#if stream_state === "waiting"}
<div class="icon-with-text">
<div class="icon color-primary" title="spinner">
<Spinner />
</div>
{button_labels.waiting || "Connecting..."}
</div>
{:else if stream_state === "open"}
<div class="icon-with-text">
{#if mode === "send-receive"}
<div
class="icon"
title="stop recording"
style={`fill: ${icon_button_color}; stroke: ${icon_button_color}; color: ${icon_button_color};`}
>
<PulsingIcon
audio_source_callback={() => stream}
stream_state={"open"}
icon={Circle}
{icon_button_color}
{pulse_color}
/>
</div>
{:else}
<div class="icon color-primary" title="start recording">
<Circle />
</div>
{/if}
{#if options_open && selected_device}
<select
class="select-wrap"
aria-label="select source"
use:click_outside={handle_click_outside}
on:change={handle_device_change}
>
<button
class="inset-icon"
on:click|stopPropagation={() => (options_open = false)}
>
<DropdownArrow />
</button>
{#if available_audio_devices.length === 0}
<option value="">{i18n("common.no_devices")}</option>
{:else}
{#each available_audio_devices as device}
<option
value={device.deviceId}
selected={selected_device.deviceId === device.deviceId}
>
{device.label}
</option>
{/each}
{/if}
</select>
{button_labels.stop || i18n("audio.stop")}
</div>
{:else}
<div class="icon-with-text">
<div class="icon color-primary" title="start recording">
<Circle />
</div>
{button_labels.start || i18n("audio.record")}
</div>
{/if}
</div>
{/if}
</button>
{#if stream_state === "closed"}
<button
class="icon"
on:click={() => (options_open = true)}
aria-label="select input source"
>
<DropdownArrow />
</button>
{/if}
{#if stream_state === "open" && mode === "send-receive"}
<button
class="mute-button"
on:click={toggleMute}
aria-label={is_muted ? "unmute audio" : "mute audio"}
>
<div
class="icon"
style={`fill: ${icon_button_color}; stroke: ${icon_button_color}; color: ${icon_button_color};`}
>
{#if is_muted}
<VolumeMuted />
{:else}
<VolumeHigh />
{/if}
</div>
</button>
{/if}
{#if stream_state === "open" && mode.includes("send")}
<button
class="mute-button"
on:click={toggleMuteMicrophone}
aria-label={is_mic_muted ? "unmute mic" : "mute mic"}
>
<div
class="icon"
style={`fill: ${icon_button_color}; stroke: ${icon_button_color}; color: ${icon_button_color};`}
>
{#if is_mic_muted}
<MicrophoneMuted />
{:else}
<Microphone />
{/if}
</div>
</button>
{/if}
{#if options_open && selected_device}
<select
class="select-wrap"
aria-label="select source"
use:click_outside={handle_click_outside}
on:change={handle_device_change}
>
<button
class="inset-icon"
on:click|stopPropagation={() => (options_open = false)}
>
<DropdownArrow />
</button>
{#if available_audio_devices.length === 0}
<option value="">{i18n("common.no_devices")}</option>
{:else}
{#each available_audio_devices as device}
<option
value={device.deviceId}
selected={selected_device.deviceId === device.deviceId}
>
{device.label}
</option>
{/each}
{/if}
</select>
{/if}
</div>
{/if}
</div>
<style>
.audio-container {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.audio-container {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
:global(::part(wrapper)) {
margin-bottom: var(--size-2);
}
.standard-player {
width: 100%;
padding: var(--size-2);
}
:global(::part(wrapper)) {
margin-bottom: var(--size-2);
.hidden {
display: none;
}
.button-wrap {
margin-top: var(--size-2);
margin-bottom: var(--size-2);
background-color: var(--block-background-fill);
border: 1px solid var(--border-color-primary);
border-radius: var(--radius-xl);
padding: var(--size-1-5);
display: flex;
bottom: var(--size-2);
box-shadow: var(--shadow-drop-lg);
border-radius: var(--radius-xl);
line-height: var(--size-3);
color: var(--button-secondary-text-color);
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(var(--primary-500-rgb), 0.7);
}
.standard-player {
width: 100%;
padding: var(--size-2);
70% {
transform: scale(1.25);
box-shadow: 0 0 0 10px rgba(var(--primary-500-rgb), 0);
}
.hidden {
display: none;
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(var(--primary-500-rgb), 0);
}
}
.pulse {
animation: pulse 1s infinite;
}
.button-wrap {
margin-top: var(--size-2);
margin-bottom: var(--size-2);
background-color: var(--block-background-fill);
border: 1px solid var(--border-color-primary);
border-radius: var(--radius-xl);
padding: var(--size-1-5);
display: flex;
bottom: var(--size-2);
box-shadow: var(--shadow-drop-lg);
border-radius: var(--radius-xl);
line-height: var(--size-3);
color: var(--button-secondary-text-color);
}
.icon-with-text {
min-width: var(--size-16);
align-items: center;
margin: 0 var(--spacing-xl);
display: flex;
justify-content: space-evenly;
gap: var(--size-2);
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(var(--primary-500-rgb), 0.7);
}
70% {
transform: scale(1.25);
box-shadow: 0 0 0 10px rgba(var(--primary-500-rgb), 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(var(--primary-500-rgb), 0);
}
@media (--screen-md) {
button {
bottom: var(--size-4);
}
}
.pulse {
animation: pulse 1s infinite;
@media (--screen-xl) {
button {
bottom: var(--size-8);
}
}
.icon-with-text {
min-width: var(--size-16);
align-items: center;
margin: 0 var(--spacing-xl);
display: flex;
justify-content: space-evenly;
gap: var(--size-2);
}
.icon {
width: 18px;
height: 18px;
display: flex;
justify-content: space-between;
align-items: center;
}
@media (--screen-md) {
button {
bottom: var(--size-4);
}
}
.color-primary {
fill: var(--primary-600);
stroke: var(--primary-600);
color: var(--primary-600);
}
@media (--screen-xl) {
button {
bottom: var(--size-8);
}
}
.select-wrap {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
color: var(--button-secondary-text-color);
background-color: transparent;
width: 95%;
font-size: var(--text-md);
position: absolute;
bottom: var(--size-2);
background-color: var(--block-background-fill);
box-shadow: var(--shadow-drop-lg);
border-radius: var(--radius-xl);
z-index: var(--layer-top);
border: 1px solid var(--border-color-primary);
text-align: left;
line-height: var(--size-4);
white-space: nowrap;
text-overflow: ellipsis;
left: 50%;
transform: translate(-50%, 0);
max-width: var(--size-52);
}
.icon {
width: 18px;
height: 18px;
display: flex;
justify-content: space-between;
align-items: center;
}
.select-wrap > option {
padding: 0.25rem 0.5rem;
border-bottom: 1px solid var(--border-color-accent);
padding-right: var(--size-8);
text-overflow: ellipsis;
overflow: hidden;
}
.color-primary {
fill: var(--primary-600);
stroke: var(--primary-600);
color: var(--primary-600);
}
.select-wrap > option:hover {
background-color: var(--color-accent);
}
.select-wrap {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
color: var(--button-secondary-text-color);
background-color: transparent;
width: 95%;
font-size: var(--text-md);
position: absolute;
bottom: var(--size-2);
background-color: var(--block-background-fill);
box-shadow: var(--shadow-drop-lg);
border-radius: var(--radius-xl);
z-index: var(--layer-top);
border: 1px solid var(--border-color-primary);
text-align: left;
line-height: var(--size-4);
white-space: nowrap;
text-overflow: ellipsis;
left: 50%;
transform: translate(-50%, 0);
max-width: var(--size-52);
}
.select-wrap > option:last-child {
border: none;
}
.select-wrap > option {
padding: 0.25rem 0.5rem;
border-bottom: 1px solid var(--border-color-accent);
padding-right: var(--size-8);
text-overflow: ellipsis;
overflow: hidden;
}
.select-wrap > option:hover {
background-color: var(--color-accent);
}
.select-wrap > option:last-child {
border: none;
}
</style>
.mute-button {
background-color: var(--block-background-fill);
padding-right: var(--size-2);
display: flex;
color: var(--button-secondary-text-color);
}
</style>

View File

@@ -1,91 +1,90 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { ComponentType } from "svelte";
import type { FileData, Client } from "@gradio/client";
import { BlockLabel } from "@gradio/atoms";
import Webcam from "./Webcam.svelte";
import { Video } from "@gradio/icons";
import { createEventDispatcher } from "svelte";
import type { ComponentType } from "svelte";
import type { FileData, Client } from "@gradio/client";
import { BlockLabel } from "@gradio/atoms";
import Webcam from "./Webcam.svelte";
import { Video } from "@gradio/icons";
import type { I18nFormatter } from "@gradio/utils";
import type { I18nFormatter } from "@gradio/utils";
export let value: string = null;
export let label: string | undefined = undefined;
export let show_label = true;
export let include_audio: boolean;
export let show_local_video: string | undefined;
export let i18n: I18nFormatter;
export let active_source: "webcam" | "upload" = "webcam";
export let handle_reset_value: () => void = () => {};
export let stream_handler: Client["stream"];
export let time_limit: number | null = null;
export let button_labels: {start: string, stop: string, waiting: string};
export let server: {
offer: (body: any) => Promise<any>;
};
export let rtc_configuration: Object;
export let track_constraints: MediaTrackConstraints = {};
export let mode: "send" | "send-receive";
export let on_change_cb: (msg: "change" | "tick") => void;
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let icon: string | undefined | ComponentType = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
export let value: string = null;
export let label: string | undefined = undefined;
export let show_label = true;
export let include_audio: boolean;
export let i18n: I18nFormatter;
export let active_source: "webcam" | "upload" = "webcam";
export let handle_reset_value: () => void = () => {};
export let stream_handler: Client["stream"];
export let time_limit: number | null = null;
export let button_labels: { start: string; stop: string; waiting: string };
export let server: {
offer: (body: any) => Promise<any>;
};
export let rtc_configuration: Object;
export let track_constraints: MediaTrackConstraints = {};
export let mode: "send" | "send-receive";
export let on_change_cb: (msg: "change" | "tick") => void;
export let reject_cb: (msg: object) => void;
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let icon: string | undefined | ComponentType = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
export let icon_radius: number = 50;
const dispatch = createEventDispatcher<{
change: FileData | null;
clear?: never;
play?: never;
pause?: never;
end?: never;
drag: boolean;
error: string;
upload: FileData;
start_recording?: never;
stop_recording?: never;
tick: never;
}>();
let dragging = false;
$: dispatch("drag", dragging);
$: console.log("value", value)
const dispatch = createEventDispatcher<{
change: FileData | null;
clear?: never;
play?: never;
pause?: never;
end?: never;
drag: boolean;
error: string;
upload: FileData;
start_recording?: never;
stop_recording?: never;
tick: never;
}>();
let dragging = false;
$: dispatch("drag", dragging);
</script>
<BlockLabel {show_label} Icon={Video} label={label || "Video"} />
<div data-testid="video" class="video-container">
<Webcam
{rtc_configuration}
{include_audio}
{show_local_video}
{time_limit}
{track_constraints}
{mode}
{rtp_params}
{on_change_cb}
{icon}
{icon_button_color}
{pulse_color}
{button_labels}
on:error
on:start_recording
on:stop_recording
on:tick
{i18n}
stream_every={0.5}
{server}
bind:webrtc_id={value}
/>
<Webcam
{rtc_configuration}
{include_audio}
{time_limit}
{track_constraints}
{mode}
{rtp_params}
{on_change_cb}
{icon}
{icon_button_color}
{pulse_color}
{icon_radius}
{button_labels}
on:error
on:start_recording
on:stop_recording
on:tick
{i18n}
stream_every={0.5}
{server}
bind:webrtc_id={value}
{reject_cb}
/>
<!-- <SelectSource {sources} bind:active_source /> -->
<!-- <SelectSource {sources} bind:active_source /> -->
</div>
<style>
.video-container {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.video-container {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,20 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-mic"
><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" /><path
d="M19 10v2a7 7 0 0 1-14 0v-2"
/><line x1="12" y1="19" x2="12" y2="23" /><line
x1="8"
y1="23"
x2="16"
y2="23"
/><line x1="1" y1="1" x2="23" y2="23" /></svg
>

After

Width:  |  Height:  |  Size: 489 B

View File

@@ -1,96 +1,102 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import type {ComponentType} from 'svelte';
export let stream_state: "open" | "closed" | "waiting" = "closed";
export let audio_source_callback: () => MediaStream;
export let icon: string | ComponentType = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
import { onDestroy } from "svelte";
import type { ComponentType } from "svelte";
let audioContext: AudioContext;
let analyser: AnalyserNode;
let dataArray: Uint8Array;
let animationId: number;
let pulseScale = 1;
let pulseIntensity = 0;
$: if(stream_state === "open") setupAudioContext();
onDestroy(() => {
if (animationId) {
cancelAnimationFrame(animationId);
}
if (audioContext) {
audioContext.close();
}
});
function setupAudioContext() {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
const source = audioContext.createMediaStreamSource(audio_source_callback());
source.connect(analyser);
analyser.fftSize = 64;
analyser.smoothingTimeConstant = 0.8;
dataArray = new Uint8Array(analyser.frequencyBinCount);
updateVisualization();
}
function updateVisualization() {
analyser.getByteFrequencyData(dataArray);
export let stream_state: "open" | "closed" | "waiting" = "closed";
export let audio_source_callback: () => MediaStream;
export let icon: string | ComponentType = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
export let icon_radius: number = 50;
// Calculate average amplitude for pulse effect
const average = Array.from(dataArray).reduce((a, b) => a + b, 0) / dataArray.length;
const normalizedAverage = average / 255;
pulseScale = 1 + (normalizedAverage * 0.15);
pulseIntensity = normalizedAverage;
animationId = requestAnimationFrame(updateVisualization);
let audioContext: AudioContext;
let analyser: AnalyserNode;
let dataArray: Uint8Array;
let animationId: number;
let pulseScale = 1;
let pulseIntensity = 0;
$: if (stream_state === "open") setupAudioContext();
onDestroy(() => {
if (animationId) {
cancelAnimationFrame(animationId);
}
$: maxPulseScale = 1 + (pulseIntensity * 10); // Scale from 1x to 3x based on intensity
</script>
if (audioContext) {
audioContext.close();
}
});
function setupAudioContext() {
// @ts-ignore
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
const source = audioContext.createMediaStreamSource(
audio_source_callback(),
);
source.connect(analyser);
analyser.fftSize = 64;
analyser.smoothingTimeConstant = 0.8;
dataArray = new Uint8Array(analyser.frequencyBinCount);
updateVisualization();
}
function updateVisualization() {
analyser.getByteFrequencyData(dataArray);
// Calculate average amplitude for pulse effect
const average =
Array.from(dataArray).reduce((a, b) => a + b, 0) / dataArray.length;
const normalizedAverage = average / 255;
pulseScale = 1 + normalizedAverage * 0.15;
pulseIntensity = normalizedAverage;
animationId = requestAnimationFrame(updateVisualization);
}
$: maxPulseScale = 1 + pulseIntensity * 10; // Scale from 1x to 3x based on intensity
</script>
<div class="gradio-webrtc-icon-wrapper">
<div class="gradio-webrtc-icon-wrapper">
<div class="gradio-webrtc-pulsing-icon-container">
{#if pulseIntensity > 0}
{#each Array(3) as _, i}
<div
class="pulse-ring"
style:background={pulse_color}
style:animation-delay={`${i * 0.4}s`}
style:--max-scale={maxPulseScale}
style:opacity={0.5 * pulseIntensity}
/>
{/each}
{/if}
<div
class="gradio-webrtc-pulsing-icon"
style:transform={`scale(${pulseScale})`}
style:background={icon_button_color}
>
{#if typeof icon === "string"}
<img
src={icon}
alt="Audio visualization icon"
class="icon-image"
/>
{:else}
<svelte:component this={icon} />
<div class="gradio-webrtc-pulsing-icon-container">
{#if pulseIntensity > 0}
{#each Array(3) as _, i}
<div
class="pulse-ring"
style:background={pulse_color}
style:animation-delay={`${i * 0.4}s`}
style:--max-scale={maxPulseScale}
style:opacity={0.5 * pulseIntensity}
/>
{/each}
{/if}
<div
class="gradio-webrtc-pulsing-icon"
style:transform={`scale(${pulseScale})`}
style:background={icon_button_color}
>
{#if typeof icon === "string"}
<img
src={icon}
alt="Audio visualization icon"
class="icon-image"
style:border-radius={`${icon_radius}%`}
/>
{:else if icon === undefined}
<div></div>
{:else}
<div>
<svelte:component this={icon} />
</div>
{/if}
</div>
</div>
</div>
</div>
<style>
.gradio-webrtc-icon-wrapper {
position: relative;
display: flex;
@@ -98,7 +104,6 @@
justify-content: center;
align-items: center;
}
}
.gradio-webrtc-pulsing-icon-container {
position: relative;
width: 100%;
@@ -107,7 +112,6 @@
justify-content: center;
align-items: center;
}
}
.gradio-webrtc-pulsing-icon {
position: relative;
width: 100%;
@@ -119,14 +123,12 @@
align-items: center;
z-index: 2;
}
}
.icon-image {
width: 100%;
height: 100%;
object-fit: contain;
object-fit: contain;
}
}
.pulse-ring {
position: absolute;
top: 50%;
@@ -137,16 +139,18 @@
border-radius: 50%;
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
opacity: 0.5;
min-width: 18px;
min-height: 18px;
}
}
@keyframes pulse {
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.5;
}
100% {
transform: translate(-50%, -50%) scale(var(--max-scale, 3));
opacity: 0;
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.5;
}
}
100% {
transform: translate(-50%, -50%) scale(var(--max-scale, 3));
opacity: 0;
}
}
</style>

View File

@@ -1,135 +1,146 @@
<script lang="ts">
import { Empty } from "@gradio/atoms";
import {
BlockLabel,
} from "@gradio/atoms";
import { Music } from "@gradio/icons";
import type { I18nFormatter } from "@gradio/utils";
import { createEventDispatcher } from "svelte";
import { onMount } from "svelte";
import { Empty } from "@gradio/atoms";
import { BlockLabel } from "@gradio/atoms";
import { Music } from "@gradio/icons";
import type { I18nFormatter } from "@gradio/utils";
import { createEventDispatcher } from "svelte";
import { onMount } from "svelte";
import { start, stop } from "./webrtc_utils";
import AudioWave from "./AudioWave.svelte";
import { start, stop } from "./webrtc_utils";
import AudioWave from "./AudioWave.svelte";
export let value: string | null = null;
export let label: string | undefined = undefined;
export let show_label = true;
export let rtc_configuration: Object | null = null;
export let i18n: I18nFormatter;
export let on_change_cb: (msg: "change" | "tick") => void;
export let icon: string | undefined = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
export let icon_radius: number = 50;
export let value: string | null = null;
export let label: string | undefined = undefined;
export let show_label = true;
export let rtc_configuration: Object | null = null;
export let i18n: I18nFormatter;
export let on_change_cb: (msg: "change" | "tick") => void;
export let icon: string | undefined = undefined;
export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)";
export let server: {
offer: (body: any) => Promise<any>;
};
export let server: {
offer: (body: any) => Promise<any>;
};
let stream_state: "open" | "closed" | "waiting" = "closed";
let audio_player: HTMLAudioElement;
let pc: RTCPeerConnection;
let _webrtc_id = Math.random().toString(36).substring(2);
let stream_state: "open" | "closed" | "waiting" = "closed";
let audio_player: HTMLAudioElement;
let pc: RTCPeerConnection;
let _webrtc_id = Math.random().toString(36).substring(2);
const dispatch = createEventDispatcher<{
tick: undefined;
error: string;
play: undefined;
stop: undefined;
}>();
const dispatch = createEventDispatcher<{
tick: undefined;
error: string
play: undefined;
stop: undefined;
}>();
onMount(() => {
window.setInterval(() => {
if (stream_state == "open") {
dispatch("tick");
}
}, 1000);
async function start_stream(value: string): Promise<string> {
if (value === "start_webrtc_stream") {
stream_state = "waiting";
_webrtc_id = Math.random().toString(36).substring(2);
value = _webrtc_id;
pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange", async (event) => {
switch (pc.connectionState) {
case "connected":
console.info("connected");
stream_state = "open";
dispatch("tick");
break;
case "disconnected":
console.info("closed");
stop(pc);
break;
case "failed":
stream_state = "closed";
dispatch("error", "Connection failed!");
stop(pc);
break;
default:
break;
}
)
});
let stream = null;
const timeoutId = setTimeout(() => {
// @ts-ignore
on_change_cb({ type: "connection_timeout" });
}, 5000);
async function start_stream(value: string): Promise<string> {
if( value === "start_webrtc_stream") {
stream_state = "waiting";
_webrtc_id = Math.random().toString(36).substring(2)
value = _webrtc_id;
console.log("set value to ", value);
pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange",
async (event) => {
switch(pc.connectionState) {
case "connected":
console.info("connected");
stream_state = "open";
break;
case "disconnected":
console.info("closed");
stop(pc);
break;
default:
break;
}
}
)
let stream = null;
start(stream, pc, audio_player, server.offer, _webrtc_id, "audio", on_change_cb).then((connection) => {
pc = connection;
}).catch(() => {
console.info("catching")
dispatch("error", "Too many concurrent users. Come back later!");
});
}
return value;
start(
stream,
pc,
audio_player,
server.offer,
_webrtc_id,
"audio",
on_change_cb,
)
.then(([connection]) => {
clearTimeout(timeoutId);
pc = connection;
})
.catch(() => {
clearTimeout(timeoutId);
console.info("catching");
dispatch("error", "Too many concurrent users. Come back later!");
});
}
return value;
}
$: start_stream(value).then((val) => {
value = val;
});
$: start_stream(value).then((val) => {
value = val;
});
</script>
<BlockLabel
{show_label}
Icon={Music}
float={false}
label={label || i18n("audio.audio")}
{show_label}
Icon={Music}
float={false}
label={label || i18n("audio.audio")}
/>
<audio
class="standard-player"
class:hidden={true}
on:load
bind:this={audio_player}
on:ended={() => dispatch("stop")}
on:play={() => dispatch("play")}
class="standard-player"
class:hidden={true}
on:load
bind:this={audio_player}
on:ended={() => dispatch("stop")}
on:play={() => dispatch("play")}
/>
{#if value !== "__webrtc_value__"}
<div class="audio-container">
<AudioWave audio_source_callback={() => audio_player.srcObject} {stream_state} {icon} {icon_button_color} {pulse_color}/>
</div>
<div class="audio-container">
<AudioWave
audio_source_callback={() => audio_player.srcObject}
{stream_state}
{icon}
{icon_button_color}
{pulse_color}
{icon_radius}
/>
</div>
{/if}
{#if value === "__webrtc_value__"}
<Empty size="small">
<Music />
</Empty>
<Empty size="small">
<Music />
</Empty>
{/if}
<style>
.audio-container {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.audio-container {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.standard-player {
width: 100%;
}
.standard-player {
width: 100%;
}
.hidden {
display: none;
}
</style>
.hidden {
display: none;
}
</style>

View File

@@ -1,119 +1,121 @@
<script lang="ts">
import { createEventDispatcher, onMount} from "svelte";
import {
BlockLabel,
Empty
} from "@gradio/atoms";
import { Video } from "@gradio/icons";
import { createEventDispatcher, onMount } from "svelte";
import { BlockLabel, Empty } from "@gradio/atoms";
import { Video } from "@gradio/icons";
import { start, stop } from "./webrtc_utils";
import { start, stop } from "./webrtc_utils";
export let value: string | null = null;
export let label: string | undefined = undefined;
export let show_label = true;
export let rtc_configuration: Object | null = null;
export let on_change_cb: (msg: "change" | "tick") => void;
export let server: {
offer: (body: any) => Promise<any>;
};
export let value: string | null = null;
export let label: string | undefined = undefined;
export let show_label = true;
export let rtc_configuration: Object | null = null;
export let on_change_cb: (msg: "change" | "tick") => void;
export let server: {
offer: (body: any) => Promise<any>;
};
let video_element: HTMLVideoElement;
let video_element: HTMLVideoElement;
let _webrtc_id = Math.random().toString(36).substring(2);
let _webrtc_id = Math.random().toString(36).substring(2);
let pc: RTCPeerConnection;
let pc: RTCPeerConnection;
const dispatch = createEventDispatcher<{
error: string;
tick: undefined;
}>();
let stream_state = "closed";
const dispatch = createEventDispatcher<{
error: string;
tick: undefined;
}>();
onMount(() => {
window.setInterval(() => {
if (stream_state == "open") {
dispatch("tick");
}
}, 1000);
}
)
let stream_state = "closed";
$: if( value === "start_webrtc_stream") {
_webrtc_id = Math.random().toString(36).substring(2);
value = _webrtc_id;
pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange",
async (event) => {
switch(pc.connectionState) {
case "connected":
console.log("connected");
stream_state = "open";
break;
case "disconnected":
console.log("closed");
stop(pc);
break;
default:
break;
}
}
)
start(null, pc, video_element, server.offer, _webrtc_id, "video", on_change_cb).then((connection) => {
pc = connection;
}).catch(() => {
console.log("catching")
dispatch("error", "Too many concurrent users. Come back later!");
});
}
$: if (value === "start_webrtc_stream") {
_webrtc_id = Math.random().toString(36).substring(2);
value = _webrtc_id;
pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange", async (event) => {
switch (pc.connectionState) {
case "connected":
stream_state = "open";
dispatch("tick");
break;
case "disconnected":
stop(pc);
break;
case "failed":
stream_state = "closed";
dispatch("error", "Connection failed!");
stop(pc);
break;
default:
break;
}
});
const timeoutId = setTimeout(() => {
// @ts-ignore
on_change_cb({ type: "connection_timeout" });
}, 5000);
start(
null,
pc,
video_element,
server.offer,
_webrtc_id,
"video",
on_change_cb,
)
.then(([connection]) => {
clearTimeout(timeoutId);
pc = connection;
})
.catch(() => {
clearTimeout(timeoutId);
dispatch("error", "Too many concurrent users. Come back later!");
});
}
</script>
<BlockLabel {show_label} Icon={Video} label={label || "Video"} />
{#if value === "__webrtc_value__"}
<Empty unpadded_box={true} size="large"><Video /></Empty>
<Empty unpadded_box={true} size="large"><Video /></Empty>
{/if}
<div class="wrap">
<video
class:hidden={value === "__webrtc_value__"}
bind:this={video_element}
autoplay={true}
on:loadeddata={dispatch.bind(null, "loadeddata")}
on:click={dispatch.bind(null, "click")}
on:play={dispatch.bind(null, "play")}
on:pause={dispatch.bind(null, "pause")}
on:ended={dispatch.bind(null, "ended")}
on:mouseover={dispatch.bind(null, "mouseover")}
on:mouseout={dispatch.bind(null, "mouseout")}
on:focus={dispatch.bind(null, "focus")}
on:blur={dispatch.bind(null, "blur")}
on:load
data-testid={$$props["data-testid"]}
crossorigin="anonymous"
>
<track kind="captions" />
</video>
<video
class:hidden={value === "__webrtc_value__"}
bind:this={video_element}
autoplay={true}
on:loadeddata={dispatch.bind(null, "loadeddata")}
on:click={dispatch.bind(null, "click")}
on:play={dispatch.bind(null, "play")}
on:pause={dispatch.bind(null, "pause")}
on:ended={dispatch.bind(null, "ended")}
on:mouseover={dispatch.bind(null, "mouseover")}
on:mouseout={dispatch.bind(null, "mouseout")}
on:focus={dispatch.bind(null, "focus")}
on:blur={dispatch.bind(null, "blur")}
on:load
data-testid={$$props["data-testid"]}
crossorigin="anonymous"
>
<track kind="captions" />
</video>
</div>
<style>
.hidden {
display: none;
}
.hidden {
display: none;
}
.wrap {
position: relative;
background-color: var(--background-fill-secondary);
height: var(--size-full);
width: var(--size-full);
border-radius: var(--radius-xl);
}
.wrap :global(video) {
height: var(--size-full);
width: var(--size-full);
}
.wrap {
position: relative;
background-color: var(--background-fill-secondary);
height: var(--size-full);
width: var(--size-full);
border-radius: var(--radius-xl);
}
.wrap :global(video) {
height: var(--size-full);
width: var(--size-full);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
<script lang="ts">
import { Spinner } from "@gradio/icons";
import AudioWave from "../../AudioWave.svelte";
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let stream_state;
export let onStartChat
export let audio_source_callback
export let wave_color
</script>
<div class="player-controls">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="chat-btn"
class:start-chat={stream_state === "closed"}
class:stop-chat={stream_state === "open"}
on:click={onStartChat}
>
{#if stream_state === "closed"}
<span>点击开始对话</span>
{:else if stream_state === "waiting"}
<div class="waiting-icon-text">
<div class="icon" title="spinner">
<Spinner />
</div>
<span>等待中</span>
</div>
{:else}
<div class="stop-chat-inner"></div>
{/if}
</div>
{#if stream_state === "open"}
<div class="input-audio-wave">
<AudioWave {audio_source_callback} {stream_state} {wave_color} />
</div>
{/if}
</div>
<style lang="less">
.player-controls {
height: 15%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
min-height: 84px;
.chat-btn {
height: 64px;
width: 296px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 999px;
opacity: 1;
background: linear-gradient(180deg, #7873f6 0%, #524de1 100%);
transition: all 0.3s;
z-index: 2;
cursor: pointer;
}
.start-chat {
font-size: 16px;
font-weight: 500;
text-align: center;
color: #ffffff;
}
.waiting-icon-text {
width: 80px;
align-items: center;
font-size: 16px;
font-weight: 500;
color: #ffffff;
margin: 0 var(--spacing-sm);
display: flex;
justify-content: space-evenly;
gap: var(--size-1);
.icon {
width: 25px;
height: 25px;
fill: #ffffff;
stroke: #ffffff;
color: #ffffff;
}
}
.stop-chat {
width: 64px;
.stop-chat-inner {
width: 25px;
height: 25px;
border-radius: 6.25px;
background: #fafafa;
}
}
.input-audio-wave {
position: absolute;
}
}
</style>

View File

@@ -0,0 +1,172 @@
<script lang="ts">
import { IconFont, Send, Stop } from "../icons";
import { insertStringAt } from "../utils";
export let replying;
export let onSend;
export let onStop;
export let onInterrupt;
let inputHeight = 24;
let rowsDivRef: HTMLDivElement;
let chatInputRef: HTMLTextAreaElement;
let inputValue = "";
function on_chat_input_keydown(event: KeyboardEvent) {
if (event.key === "Enter") {
if (event.altKey) {
chatInputRef.value = insertStringAt(
chatInputRef.value,
"\n",
chatInputRef.selectionStart,
);
chatInputRef.dispatchEvent(new InputEvent("input"));
} else {
event.preventDefault();
on_send();
}
}
}
async function on_send() {
await onSend(chatInputRef.value);
chatInputRef.value = "";
}
function on_chat_input(event: InputEvent) {
if (rowsDivRef) {
rowsDivRef.textContent = (event.target as any).value.replace(
/\n$/,
"\n\n",
);
inputHeight = rowsDivRef.offsetHeight;
}
}
</script>
<div class="chat-input-container">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="stop-chat-btn" on:click={onStop}></div>
<div class="chat-input-inner">
<div class="chat-input-wrapper">
<textarea
class="chat-input"
bind:this={chatInputRef}
on:keydown={on_chat_input_keydown}
on:input={on_chat_input}
style={`height:${inputHeight}px`}
/>
<div class="rowsDiv" bind:this={rowsDivRef}>{inputValue}</div>
</div>
{#if replying}
<button class="interrupt-btn" on:click={onInterrupt}></button>
{:else}
<button class="send-btn" on:click={on_send}>
<IconFont icon={Send} color={"#fff"} ></IconFont>
</button>
{/if}
</div>
</div>
<style lang="less">
.chat-input-container {
height: 15%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
min-height: 84px;
// padding: 0 12px;
.chat-input-inner {
padding: 0 12px;
background-color: #fff;
height: 64px;
flex: 1;
display: flex;
align-items: center;
border: 1px solid #e8eaf2;
border-radius: 12px;
border-radius: 20px;
box-shadow:
0 12px 24px -16px rgba(54, 54, 73, 0.04),
0 12px 40px 0 rgba(51, 51, 71, 0.08),
0 0 1px 0 rgba(44, 44, 54, 0.02);
.chat-input-wrapper {
flex: 1;
position: relative;
display: flex;
align-items: center;
.chat-input {
width: 100%;
border: none;
outline: none;
color: #26244c;
font-size: 16px;
font-weight: 400;
resize: none;
padding: 0;
margin: 8px 0;
line-height: 24px;
max-height: 48px;
min-height: 24px;
}
.rowsDiv {
position: absolute;
left: 0;
right: 0;
z-index: -1;
visibility: hidden;
font-size: 16px;
font-weight: 400;
line-height: 24px;
white-space: pre-wrap;
word-wrap: break-word;
}
}
.send-btn,.interrupt-btn {
flex: 0 0 auto;
background: #615ced;
border-radius: 20px;
height: 28px;
width: 28px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 16px;
cursor: pointer;
}
.interrupt-btn{
&::after {
content: " ";
width: 12px;
height: 12px;
border-radius: 2px;
background: #fafafa;
}
}
}
.stop-chat-btn {
cursor: pointer;
margin-right: 12px;
height: 28px;
width: 28px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 999px;
opacity: 1;
background: linear-gradient(180deg, #7873f6 0%, #524de1 100%);
&::after {
content: " ";
width: 12px;
height: 12px;
border-radius: 2px;
background: #fafafa;
}
}
}
</style>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
export let message;
export let style = '';
</script>
<div
class="answer-message-container"
{style}
>
<div class="answer-message-text">
{message}
</div>
</div>
<style lang="less">
.answer-message-container {
position: absolute;
right: 12px;
z-index: 101;
padding: 6px 12px;
border-radius: 12px;
width: 200px;
background: rgba(255, 255, 255, 0.8);
bottom: 166px;
}
</style>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,54 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
fill="none"
version="1.1"
width="20"
height="20"
viewBox="0 0 20 20"
><defs
><clipPath id="master_svg0_13_279"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
><clipPath id="master_svg1_13_279/13_007"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
></defs
><g clip-path="url(#master_svg0_13_279)"
><g clip-path="url(#master_svg1_13_279/13_007)"
><g
><rect
x="0"
y="0"
width="20"
height="20"
rx="0"
fill="#FFFFFF"
fill-opacity="0.009999999776482582"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M0.83317090625,15.8333259765625L0.83317090625,4.1666259765625Q0.83317090625,4.0845497765625,0.84918290625,4.0040509765625Q0.86519490625,3.9235519765625,0.89660490625,3.8477229765625Q0.92801390625,3.7718949765625,0.97361290625,3.7036509765625Q1.01921190625,3.6354069765625,1.07724790625,3.5773699765625Q1.13528490625,3.5193339765625,1.2035289062499999,3.4737349765625Q1.27177290625,3.4281359765625,1.34760090625,3.3967269765625Q1.42342990625,3.3653169765625,1.50392890625,3.3493049765625003Q1.58442770625,3.3332929765625,1.66650390625,3.3332929765625L14.99980390625,3.3332929765625Q15.08190390625,3.3332929765625,15.16240390625,3.3493049765625003Q15.24290390625,3.3653169765625,15.31870390625,3.3967269765625Q15.39460390625,3.4281359765625,15.46280390625,3.4737349765625Q15.53100390625,3.5193339765625,15.58910390625,3.5773699765625Q15.64710390625,3.6354069765625,15.69270390625,3.7036509765625Q15.73830390625,3.7718949765625,15.76970390625,3.8477229765625Q15.80110390625,3.9235519765625,15.81720390625,4.0040509765625Q15.83320390625,4.0845497765625,15.83320390625,4.1666259765625L15.83320390625,15.8333259765625Q15.83320390625,15.9153259765625,15.81720390625,15.9958259765625Q15.80110390625,16.0763259765625,15.76970390625,16.152225976562498Q15.73830390625,16.2280259765625,15.69270390625,16.2962259765625Q15.64710390625,16.3645259765625,15.58910390625,16.4225259765625Q15.53100390625,16.4806259765625,15.46280390625,16.5262259765625Q15.39460390625,16.5718259765625,15.31870390625,16.6032259765625Q15.24290390625,16.6346259765625,15.16240390625,16.6506259765625Q15.08190390625,16.6666259765625,14.99980390625,16.6666259765625L1.66650390625,16.6666259765625Q1.58442770625,16.6666259765625,1.50392890625,16.6506259765625Q1.42342990625,16.6346259765625,1.34760090625,16.6032259765625Q1.27177290625,16.5718259765625,1.2035289062499999,16.5262259765625Q1.13528490625,16.4806259765625,1.07724790625,16.4225259765625Q1.01921190625,16.3645259765625,0.97361290625,16.2962259765625Q0.92801390625,16.2280259765625,0.89660490625,16.152225976562498Q0.86519490625,16.0763259765625,0.84918290625,15.9958259765625Q0.83317090625,15.9153259765625,0.83317090625,15.8333259765625ZM2.49983690625,4.9999589765625L2.49983690625,14.9999259765625L14.16650390625,14.9999259765625L14.16650390625,4.9999589765625L2.49983690625,4.9999589765625Z"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M18.97024,14.7041040234375Q19.06538,14.5913440234375,19.11602,14.4527940234375Q19.16667,14.3142340234375,19.16667,14.1667040234375L19.16667,5.8333740234375Q19.16667,5.7512978234375,19.15065,5.6707990234375Q19.13464,5.5903000234375,19.10323,5.5144710234375Q19.07182,5.4386430234375,19.026220000000002,5.3703990234375Q18.98063,5.3021550234375,18.92259,5.2441180234375Q18.86455,5.1860820234375,18.79631,5.1404830234375Q18.72806,5.0948840234375,18.65224,5.0634750234375Q18.57641,5.0320650234375,18.49591,5.0160530234375Q18.41541,5.0000410234375,18.33333,5.0000410234375Q18.18581,5.0000410234375,18.04725,5.0506860234375Q17.90869,5.1013300234375,17.79594,5.1964640234375L14.462608,8.0089640234375Q14.393074,8.067634023437499,14.337838,8.1399240234375Q14.282601,8.2122140234375,14.244265,8.2947240234375Q14.205928,8.377224023437499,14.186297,8.4660640234375Q14.166667,8.5548940234375,14.166667,8.6458740234375L14.166667,11.3542040234375Q14.166667,11.4451840234375,14.186297,11.5340240234375Q14.205928,11.622854023437501,14.244265,11.7053640234375Q14.282601,11.7878640234375,14.337838,11.860154023437499Q14.393074,11.932444023437501,14.462608,11.9911140234375L17.79594,14.8036140234375Q17.922629999999998,14.9105140234375,18.08058,14.9607840234375Q18.23853,15.0110640234375,18.4037,14.9970640234375Q18.56887,14.9830640234375,18.71611,14.9069240234375Q18.86335,14.8307940234375,18.97024,14.7041040234375ZM17.5,12.3732440234375L17.5,7.6268340234375L15.833333,9.0330840234375L15.833333,10.9669940234375L17.5,12.3732440234375Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M7.65749209375,7.3101989765625L10.11698609375,9.3597759765625Q10.17518609375,9.4082759765625,10.22367609375,9.4664759765625Q10.32979609375,9.5938159765625,10.37910609375,9.7520659765625Q10.42841609375,9.9103259765625,10.41340609375,10.0754059765625Q10.39839609375,10.2404859765625,10.321366093750001,10.3872559765625Q10.24432609375,10.5340259765625,10.11698609375,10.6401459765625L7.65748809375,12.6897259765625Q7.54118509375,12.7998059765625,7.39241009375,12.8590459765625Q7.24363409375,12.9182959765625,7.08349609375,12.9182959765625Q7.00125579375,12.9182959765625,6.92059609375,12.9022459765625Q6.83993609375,12.8862059765625,6.76395509375,12.8547359765625Q6.6879750937499995,12.8232559765625,6.61959509375,12.7775659765625Q6.55121509375,12.731875976562499,6.49306209375,12.673725976562501Q6.43490909375,12.6155759765625,6.38921909375,12.547195976562499Q6.34352909375,12.478815976562501,6.31205709375,12.4028359765625Q6.28058509375,12.3268559765625,6.26454009375,12.2461959765625Q6.24849609375,12.1655359765625,6.24849609375,12.0832959765625Q6.24849609375,11.9848059765625,6.27141009375,11.8890159765625Q6.29432409375,11.7932359765625,6.33889509375,11.7054159765625Q6.38346609375,11.6175859765625,6.44724609375,11.5425459765625Q6.51102709375,11.4674959765625,6.59051809375,11.4093459765625L8.28178609375,9.9999559765625L6.59051809375,8.5905679765625Q6.51102709375,8.5324209765625,6.44724609375,8.4573769765625Q6.38346509375,8.3823319765625,6.33889509375,8.2945069765625Q6.29432409375,8.2066819765625,6.27141009375,8.1108979765625Q6.24849609375,8.0151131765625,6.24849609375,7.9166259765625Q6.24849609375,7.8343856765625,6.26454009375,7.7537259765625Q6.28058509375,7.6730659765625,6.31205709375,7.5970849765625Q6.34352909375,7.5211049765624995,6.38921909375,7.4527249765625Q6.43490909375,7.3843449765625,6.49306209375,7.3261919765625Q6.55121509375,7.2680389765625,6.61959509375,7.2223489765625Q6.6879750937499995,7.1766589765625,6.76395509375,7.1451869765625Q6.83993609375,7.1137149765625,6.92059609375,7.0976699765625Q7.00125579375,7.0816259765625,7.08349609375,7.0816259765625Q7.24363509375,7.0816259765625,7.39241209375,7.1408709765625Q7.54118909375,7.2001159765625005,7.65749209375,7.3101989765625Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
></g
></g
></svg
>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
fill="none"
version="1.1"
width="14.000000357627869"
height="10.000000357627869"
viewBox="0 0 14.000000357627869 10.000000357627869"
><g
><path
d="M13.802466686534881,1.1380186865348816Q13.89646668653488,1.0444176865348815,13.947366686534881,0.9218876865348815Q13.998366686534881,0.7993576865348816,13.998366686534881,0.6666666865348816Q13.998366686534881,0.6011698865348816,13.98556668653488,0.5369316865348817Q13.972766686534882,0.4726936865348816,13.947666686534882,0.4121826865348816Q13.922666686534882,0.3516706865348816,13.886266686534881,0.2972126865348816Q13.849866686534881,0.2427536865348816,13.803566686534882,0.19644068653488161Q13.757266686534882,0.15012768653488162,13.702766686534881,0.11373968653488165Q13.648366686534882,0.07735168653488156,13.587866686534882,0.052286686534881555Q13.527266686534881,0.02722268653488158,13.463066686534882,0.014444686534881623Q13.398866686534882,0.0016666865348815563,13.333366686534882,0.0016666865348815563Q13.201466686534882,0.0016666865348815563,13.079566686534882,0.051981686534881555Q12.957666686534882,0.10229768653488158,12.864266686534881,0.1953146865348816L12.863066686534882,0.19413268653488158L4.624996686534882,8.392776686534882L1.1369396865348815,4.921396686534882L1.1357636865348817,4.922586686534881Q1.0422996865348817,4.829566686534881,0.9204146865348816,4.779246686534882Q0.7985286865348816,4.728936686534881,0.6666666865348816,4.728936686534881Q0.6011698865348816,4.728936686534881,0.5369316865348817,4.741706686534882Q0.4726936865348816,4.754486686534881,0.4121826865348816,4.779556686534882Q0.3516706865348816,4.804616686534882,0.2972126865348816,4.8410066865348815Q0.2427536865348816,4.8773966865348815,0.19644068653488161,4.9237066865348815Q0.15012768653488162,4.970016686534882,0.11373968653488165,5.024476686534881Q0.07735168653488156,5.078936686534882,0.052286686534881555,5.139446686534882Q0.02722268653488158,5.199956686534882,0.014444686534881623,5.2641966865348815Q0.0016666865348815563,5.328436686534881,0.0016666865348815563,5.3939366865348815Q0.0016666865348815563,5.526626686534882,0.05259268653488158,5.649156686534882Q0.10351768653488158,5.771686686534881,0.1975696865348816,5.865286686534882L0.1963936865348816,5.866466686534881L4.1547266865348815,9.805866686534882Q4.201126686534882,9.852046686534882,4.255616686534882,9.888306686534882Q4.310106686534882,9.924576686534882,4.3706166865348814,9.949556686534882Q4.431126686534881,9.974536686534881,4.495326686534882,9.987266686534882Q4.559536686534882,9.999996686534882,4.624996686534882,9.999996686534882Q4.690456686534882,9.999996686534882,4.754666686534882,9.987266686534882Q4.818876686534882,9.974536686534881,4.879386686534882,9.949556686534882Q4.939886686534882,9.924576686534882,4.994386686534882,9.888306686534882Q5.048876686534881,9.852046686534882,5.0952766865348815,9.805866686534882L13.803566686534882,1.1392006865348816L13.802466686534881,1.1380186865348816Z"
fill-rule="evenodd"
fill="#E0E0FC"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
></svg
>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,13 @@
<script lang="ts">
export let color;
export let fontSize = "16px";
export let icon: any = undefined;
</script>
<span style={`color: ${color}; font-size: ${fontSize}`}>
{#if icon}
<svelte:component this={icon} />
{:else}
<slot></slot>
{/if}
</span>

View File

@@ -0,0 +1,60 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
fill="none"
version="1.1"
width="20"
height="20"
viewBox="0 0 20 20"
><defs
><clipPath id="master_svg0_13_287/13_278"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
><clipPath id="master_svg1_13_287/13_278/13_040"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
></defs
><g clip-path="url(#master_svg0_13_287/13_278)"
><g clip-path="url(#master_svg1_13_287/13_278/13_040)"
><g
><path
d="M7.34851109375,12.6516259765625Q8.44685609375,13.7499259765625,10.00016609375,13.7499259765625Q11.55346609375,13.7499259765625,12.65181609375,12.6516259765625Q13.75016609375,11.5532659765625,13.75016609375,9.9999559765625L13.75016609375,4.5832959765625Q13.75016609375,3.0299959765624997,12.65181609375,1.9316429765625Q11.55346609375,0.8332929765625,10.00016609375,0.8332919765625Q8.44685609375,0.8332929765625,7.34851109375,1.9316429765625Q6.25016309375,3.0299959765624997,6.25016309375,4.5832959765625L6.25016309375,9.9999559765625Q6.25016309375,11.5532659765625,7.34851109375,12.6516259765625ZM11.47330609375,11.4730959765625Q10.86310609375,12.0833259765625,10.00016609375,12.0833259765625Q9.13721609375,12.0833259765625,8.527026093749999,11.4730959765625Q7.91682909375,10.8629059765625,7.91682909375,9.9999559765625L7.91683009375,4.5832959765625Q7.91683009375,3.7203459765625,8.527026093749999,3.1101559765625Q9.13721609375,2.4999589765625,10.00016609375,2.4999589765625Q10.86310609375,2.4999589765625,11.47330609375,3.1101559765625Q12.08349609375,3.7203459765625,12.08349609375,4.5832959765625L12.08349609375,9.9999559765625Q12.08349609375,10.8629059765625,11.47330609375,11.4730959765625Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M17.08315046875,9.6393233234375Q17.08502046875,9.6113801234375,17.08502046875,9.5833740234375Q17.08502046875,9.5011337234375,17.06898046875,9.4204740234375Q17.05293046875,9.3398140234375,17.02146046875,9.2638330234375Q16.98999046875,9.1878530234375,16.94430046875,9.1194730234375Q16.89861046875,9.0510930234375,16.84046046875,8.9929400234375Q16.78230046875,8.9347870234375,16.71392346875,8.8890970234375Q16.64554246875,8.8434070234375,16.56956246875,8.8119350234375Q16.49358246875,8.7804630234375,16.41292246875,8.7644180234375Q16.33226246875,8.7483740234375,16.25002246875,8.7483740234375Q16.16778146875,8.7483740234375,16.08712146875,8.7644180234375Q16.00646146875,8.7804630234375,15.93048146875,8.8119350234375Q15.85450146875,8.8434070234375,15.78612106875,8.8890970234375Q15.71774076875,8.9347870234375,15.65958806875,8.9929400234375Q15.60143546875,9.0510930234375,15.55574546875,9.1194730234375Q15.51005446875,9.1878530234375,15.47858246875,9.2638330234375Q15.44711046875,9.3398140234375,15.43106646875,9.4204740234375Q15.41502246875,9.5011337234375,15.41502246875,9.5833740234375Q15.41502246875,9.6080265234375,15.41647646875,9.6326360234375Q15.40712446875,10.7164940234375,14.98582546875,11.7046140234375Q14.89498046875,11.8831040234375,14.89498046875,12.0833740234375Q14.89498046875,12.1656140234375,14.91102446875,12.2462740234375Q14.92706946875,12.3269340234375,14.95854146875,12.4029140234375Q14.99001346875,12.4788940234375,15.03570346875,12.5472740234375Q15.08139346875,12.6156540234375,15.13954646875,12.6738040234375Q15.19769946875,12.7319640234375,15.26607946875,12.7776540234375Q15.33445946875,12.8233440234375,15.41043946875,12.8548140234375Q15.48642046875,12.8862840234375,15.56708046875,12.9023340234375Q15.64774016875,12.9183740234375,15.72998046875,12.9183740234375Q15.79409136875,12.9183740234375,15.85745046875,12.9085840234375Q15.92081046875,12.8988040234375,15.98193346875,12.8794540234375Q16.04305546875,12.8601140234375,16.10050846875,12.8316640234375Q16.15796246875,12.8032140234375,16.21039846875,12.7663240234375Q16.26283546875,12.7294440234375,16.309026468749998,12.6849840234375Q16.35521746875,12.6405240234375,16.39408046875,12.5895340234375Q16.43294246875,12.538544023437499,16.46356646875,12.4822240234375Q16.49418946875,12.4258940234375,16.51585446875,12.3655540234375Q17.07244046875,11.0643540234375,17.08315046875,9.6393233234375Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M4.583527,9.6329521234375Q4.585,9.6081849234375,4.585,9.5833740234375Q4.585,9.5011337234375,4.568956,9.4204740234375Q4.552911,9.3398140234375,4.521439,9.2638330234375Q4.489967,9.1878530234375,4.444277,9.1194730234375Q4.398587,9.0510930234375,4.340434,8.9929400234375Q4.282281,8.9347870234375,4.213901,8.8890970234375Q4.1455210000000005,8.8434070234375,4.069541,8.8119350234375Q3.99356,8.7804630234375,3.9129,8.7644180234375Q3.8322403,8.7483740234375,3.75,8.7483740234375Q3.6677597,8.7483740234375,3.5871,8.7644180234375Q3.50644,8.7804630234375,3.430459,8.8119350234375Q3.354479,8.8434070234375,3.286099,8.8890970234375Q3.2177189999999998,8.9347870234375,3.159566,8.9929400234375Q3.101413,9.0510930234375,3.055723,9.1194730234375Q3.010033,9.1878530234375,2.978561,9.2638330234375Q2.947089,9.3398140234375,2.931044,9.4204740234375Q2.915,9.5011337234375,2.915,9.5833740234375Q2.915,9.6112012234375,2.916853,9.6389666234375Q2.9363479999999997,12.5370740234375,4.99132,14.5920540234375Q7.06598,16.6667040234375,10,16.6667040234375Q11.1917,16.6667040234375,12.30806,16.2819440234375Q12.37346,16.2636640234375,12.43505,16.235064023437502Q12.49663,16.2064640234375,12.55279,16.1682840234375Q12.60894,16.1301040234375,12.65819,16.0833540234375Q12.70744,16.036604023437498,12.74849,15.9825140234375Q12.78954,15.9284240234375,12.82131,15.868404023437499Q12.85308,15.8083940234375,12.87473,15.7440340234375Q12.89639,15.6796740234375,12.90736,15.6126640234375Q12.91833,15.5456540234375,12.91833,15.4777440234375Q12.91833,15.3955040234375,12.90229,15.3148440234375Q12.88624,15.2341840234375,12.85477,15.1582040234375Q12.8233,15.082224023437501,12.77761,15.0138440234375Q12.73192,14.9454640234375,12.67377,14.8873140234375Q12.61561,14.8291640234375,12.54723,14.783474023437499Q12.47885,14.7377840234375,12.40287,14.7063140234375Q12.32689,14.6748340234375,12.24623,14.658794023437501Q12.16557,14.6427540234375,12.08333,14.642744023437501Q11.91469,14.642744023437501,11.75926,14.7082040234375Q10.9093,15.0000440234375,10,15.0000440234375Q7.75633,15.0000440234375,6.16983,13.413544023437499Q4.6008890000000005,11.8445940234375,4.583527,9.6329521234375Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M10.833333,15.8861049234375Q10.835,15.8597658234375,10.835,15.8333740234375Q10.835,15.7511337234375,10.818956,15.6704740234375Q10.802911,15.5898140234375,10.771439,15.5138330234375Q10.739967,15.4378530234375,10.694277,15.3694730234375Q10.648587,15.3010930234375,10.590434,15.2429400234375Q10.532281,15.1847870234375,10.463901,15.1390970234375Q10.395521,15.0934070234375,10.319541,15.0619350234375Q10.24356,15.0304630234375,10.1629,15.0144180234375Q10.0822403,14.9983740234375,10,14.9983740234375Q9.9177597,14.9983740234375,9.8371,15.0144180234375Q9.75644,15.0304630234375,9.680459,15.0619350234375Q9.604479,15.0934070234375,9.536099,15.1390970234375Q9.467719,15.1847870234375,9.409566,15.2429400234375Q9.351413,15.3010930234375,9.305723,15.3694730234375Q9.260033,15.4378530234375,9.228561,15.5138330234375Q9.197089,15.5898140234375,9.181044,15.6704740234375Q9.165,15.7511337234375,9.165,15.8333740234375Q9.165,15.8597658234375,9.166667,15.8861049234375L9.166667,18.2806440234375Q9.165,18.3069840234375,9.165,18.3333740234375Q9.165,18.4156140234375,9.181044,18.4962740234375Q9.197089,18.5769340234375,9.228561,18.6529140234375Q9.260033,18.7288940234375,9.305723,18.7972740234375Q9.351413,18.8656540234375,9.409566,18.9238040234375Q9.467719,18.9819640234375,9.536099,19.0276540234375Q9.604479,19.0733440234375,9.680459,19.1048140234375Q9.75644,19.1362840234375,9.8371,19.1523340234375Q9.9177597,19.1683740234375,10,19.1683740234375Q10.0822403,19.1683740234375,10.1629,19.1523340234375Q10.24356,19.1362840234375,10.319541,19.1048140234375Q10.395521,19.0733440234375,10.463901,19.0276540234375Q10.532281,18.9819640234375,10.590434,18.9238040234375Q10.648587,18.8656540234375,10.694277,18.7972740234375Q10.739967,18.7288940234375,10.771439,18.6529140234375Q10.802911,18.5769340234375,10.818956,18.4962740234375Q10.835,18.4156140234375,10.835,18.3333740234375Q10.835,18.3069840234375,10.833333,18.2806440234375L10.833333,15.8861049234375Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M1.9480309999999998,3.126542Q1.813081,3.007654,1.7390400000000001,2.843752Q1.665,2.67985,1.665,2.5Q1.665,2.4177597,1.681044,2.3371Q1.697089,2.25644,1.728561,2.180459Q1.760033,2.104479,1.805723,2.036099Q1.851413,1.967719,1.9095659999999999,1.9095659999999999Q1.967719,1.851413,2.036099,1.805723Q2.104479,1.760033,2.180459,1.728561Q2.25644,1.697089,2.3371,1.681044Q2.4177597,1.665,2.5,1.665Q2.67985,1.665,2.843752,1.7390400000000001Q3.007654,1.813081,3.126542,1.9480309999999998L18.052,16.8735Q18.1869,16.9923,18.261,17.1562Q18.335,17.3202,18.335,17.5Q18.335,17.5822,18.319000000000003,17.6629Q18.3029,17.7436,18.2714,17.819499999999998Q18.240000000000002,17.8955,18.1943,17.963900000000002Q18.148600000000002,18.0323,18.090400000000002,18.090400000000002Q18.0323,18.148600000000002,17.963900000000002,18.1943Q17.8955,18.240000000000002,17.819499999999998,18.2714Q17.7436,18.3029,17.6629,18.319000000000003Q17.5822,18.335,17.5,18.335Q17.3202,18.335,17.1562,18.261Q16.9923,18.1869,16.8735,18.052L1.9480309999999998,3.126542Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
></g
></g
></svg
>

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -0,0 +1,54 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
fill="none"
version="1.1"
width="20"
height="20"
viewBox="0 0 20 20"
><defs
><clipPath id="master_svg0_13_278"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
><clipPath id="master_svg1_13_278/13_029"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
></defs
><g clip-path="url(#master_svg0_13_278)"
><g clip-path="url(#master_svg1_13_278/13_029)"
><g
><rect
x="0"
y="0"
width="20"
height="20"
rx="0"
fill="#FFFFFF"
fill-opacity="0.009999999776482582"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M6.249918953125,9.9999559765625L6.249918953125,4.5832959765625Q6.249918953125,3.0299959765624997,7.348267953125,1.9316419765625Q8.446621953125,0.8332929765625,9.999921953125,0.8332929765625Q11.553221953125,0.8332929765625,12.651571953125,1.9316419765625Q13.749921953125,3.0299959765624997,13.749921953125,4.5832959765625L13.749921953125,9.9999559765625Q13.749921953125,11.5532559765625,12.651571953125,12.6516259765625Q11.553221953125,13.7499259765625,9.999921953125,13.7499259765625Q8.446621953125,13.7499259765625,7.348267953125,12.6516259765625Q6.249918953125,11.5532559765625,6.249918953125,9.9999559765625ZM7.916584953125,9.9999559765625Q7.916584953125,10.8629059765625,8.526781953124999,11.4730959765625Q9.136971953125,12.0833259765625,9.999921953125,12.0833259765625Q10.862861953125,12.0833259765625,11.473061953125,11.4730959765625Q12.083251953125,10.8629059765625,12.083251953125,9.9999559765625L12.083251953125,4.5832959765625Q12.083251953125,3.7203459765625,11.473061953125,3.1101559765625Q10.862861953125,2.4999589765625,9.999921953125,2.4999589765625Q9.136971953125,2.4999589765625,8.526781953124999,3.1101559765625Q7.916584953125,3.7203459765625,7.916584953125,4.5832959765625L7.916584953125,9.9999559765625Z"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M4.583527,9.6329521234375Q4.585,9.6081849234375,4.585,9.5833740234375Q4.585,9.5011337234375,4.568956,9.4204740234375Q4.552911,9.3398140234375,4.521439,9.2638330234375Q4.489967,9.1878530234375,4.444277,9.1194730234375Q4.398587,9.0510930234375,4.340434,8.9929400234375Q4.282281,8.9347870234375,4.213901,8.8890970234375Q4.1455210000000005,8.8434070234375,4.069541,8.8119350234375Q3.99356,8.7804630234375,3.9129,8.7644180234375Q3.8322403,8.7483740234375,3.75,8.7483740234375Q3.6677597,8.7483740234375,3.5871,8.7644180234375Q3.50644,8.7804630234375,3.430459,8.8119350234375Q3.354479,8.8434070234375,3.286099,8.8890970234375Q3.2177189999999998,8.9347870234375,3.159566,8.9929400234375Q3.101413,9.0510930234375,3.055723,9.1194730234375Q3.010033,9.1878530234375,2.978561,9.2638330234375Q2.947089,9.3398140234375,2.931044,9.4204740234375Q2.915,9.5011337234375,2.915,9.5833740234375Q2.915,9.6112012234375,2.916853,9.6389666234375Q2.9363479999999997,12.5370740234375,4.99132,14.5920540234375Q7.06598,16.6667040234375,10,16.6667040234375Q12.93402,16.6667040234375,15.0087,14.5920540234375Q17.0636,12.5370940234375,17.0831,9.6390003234375Q17.085,9.6112181234375,17.085,9.5833740234375Q17.085,9.5011337234375,17.069000000000003,9.4204740234375Q17.0529,9.3398140234375,17.0214,9.2638330234375Q16.990000000000002,9.1878530234375,16.9443,9.1194730234375Q16.898600000000002,9.0510930234375,16.840400000000002,8.9929400234375Q16.7823,8.9347870234375,16.713900000000002,8.8890970234375Q16.6455,8.8434070234375,16.569499999999998,8.8119350234375Q16.4936,8.7804630234375,16.4129,8.7644180234375Q16.3322,8.7483740234375,16.25,8.7483740234375Q16.1678,8.7483740234375,16.0871,8.7644180234375Q16.0064,8.7804630234375,15.9305,8.8119350234375Q15.8545,8.8434070234375,15.7861,8.8890970234375Q15.7177,8.9347870234375,15.6596,8.9929400234375Q15.6014,9.0510930234375,15.5557,9.1194730234375Q15.51,9.1878530234375,15.4786,9.2638330234375Q15.4471,9.3398140234375,15.431,9.4204740234375Q15.415,9.5011337234375,15.415,9.5833740234375Q15.415,9.6081817234375,15.4165,9.6329456234375Q15.3991,11.8445940234375,13.8302,13.413544023437499Q12.24366,15.0000440234375,10,15.0000440234375Q7.75633,15.0000440234375,6.16983,13.413544023437499Q4.6008890000000005,11.8445940234375,4.583527,9.6329521234375Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M10.833333,15.8861049234375Q10.835,15.8597658234375,10.835,15.8333740234375Q10.835,15.7511337234375,10.818956,15.6704740234375Q10.802911,15.5898140234375,10.771439,15.5138330234375Q10.739967,15.4378530234375,10.694277,15.3694730234375Q10.648587,15.3010930234375,10.590434,15.2429400234375Q10.532281,15.1847870234375,10.463901,15.1390970234375Q10.395521,15.0934070234375,10.319541,15.0619350234375Q10.24356,15.0304630234375,10.1629,15.0144180234375Q10.0822403,14.9983740234375,10,14.9983740234375Q9.9177597,14.9983740234375,9.8371,15.0144180234375Q9.75644,15.0304630234375,9.680459,15.0619350234375Q9.604479,15.0934070234375,9.536099,15.1390970234375Q9.467719,15.1847870234375,9.409566,15.2429400234375Q9.351413,15.3010930234375,9.305723,15.3694730234375Q9.260033,15.4378530234375,9.228561,15.5138330234375Q9.197089,15.5898140234375,9.181044,15.6704740234375Q9.165,15.7511337234375,9.165,15.8333740234375Q9.165,15.8597658234375,9.166667,15.8861049234375L9.166667,18.2806440234375Q9.165,18.3069840234375,9.165,18.3333740234375Q9.165,18.4156140234375,9.181044,18.4962740234375Q9.197089,18.5769340234375,9.228561,18.6529140234375Q9.260033,18.7288940234375,9.305723,18.7972740234375Q9.351413,18.8656540234375,9.409566,18.9238040234375Q9.467719,18.9819640234375,9.536099,19.0276540234375Q9.604479,19.0733440234375,9.680459,19.1048140234375Q9.75644,19.1362840234375,9.8371,19.1523340234375Q9.9177597,19.1683740234375,10,19.1683740234375Q10.0822403,19.1683740234375,10.1629,19.1523340234375Q10.24356,19.1362840234375,10.319541,19.1048140234375Q10.395521,19.0733440234375,10.463901,19.0276540234375Q10.532281,18.9819640234375,10.590434,18.9238040234375Q10.648587,18.8656540234375,10.694277,18.7972740234375Q10.739967,18.7288940234375,10.771439,18.6529140234375Q10.802911,18.5769340234375,10.818956,18.4962740234375Q10.835,18.4156140234375,10.835,18.3333740234375Q10.835,18.3069840234375,10.833333,18.2806440234375L10.833333,15.8861049234375Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
></g
></g
></svg
>

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,9 @@
<svg
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
><path
d="M899.925333 172.080762a48.761905 48.761905 0 0 1 0 28.525714l-207.969523 679.448381a48.761905 48.761905 0 0 1-81.115429 20.187429l-150.552381-150.552381-96.304762 96.329143a24.380952 24.380952 0 0 1-41.593905-17.237334v-214.966857l275.821715-243.370667-355.57181 161.596953-103.253333-103.228953a48.761905 48.761905 0 0 1 20.23619-81.091047L838.997333 139.702857a48.761905 48.761905 0 0 1 60.903619 32.353524z"
></path></svg
>

After

Width:  |  Height:  |  Size: 544 B

View File

@@ -0,0 +1,37 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
fill="none"
version="1.1"
width="20"
height="20"
viewBox="0 0 20 20"
><defs
><clipPath id="master_svg0_13_533/13_323"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
></defs
><g clip-path="url(#master_svg0_13_533/13_323)"
><g
><path
d="M7.5,5.0016259765625Q7.58224,5.0016259765625,7.6629,4.9855819765625Q7.74356,4.9695369765625,7.81954,4.9380649765625Q7.89552,4.9065929765625,7.9639,4.8609029765625Q8.03228,4.8152129765625,8.09043,4.7570599765625Q8.14859,4.6989069765625,8.19428,4.6305269765625Q8.23997,4.5621469765625005,8.27144,4.4861669765625Q8.30291,4.4101859765625,8.318950000000001,4.3295259765625Q8.335,4.2488662765625,8.335,4.1666259765625Q8.335,4.0843856765625,8.31896,4.0037259765625Q8.30291,3.9230659765625,8.27144,3.8470849765625Q8.23997,3.7711049765625,8.19428,3.7027249765625Q8.14859,3.6343449765624998,8.09043,3.5761919765625Q8.03228,3.5180389765625,7.9639,3.4723489765625Q7.89552,3.4266589765625,7.81954,3.3951869765625Q7.74356,3.3637149765625,7.6629,3.3476699765625Q7.58224,3.3316259765625,7.5,3.3316259765625Q7.47361,3.3316259765625,7.44727,3.3332929765625L4.16667,3.3332929765625Q3.131133,3.3332929765625,2.3989,4.0655259765625Q1.666667,4.7977589765625,1.666667,5.8332959765625L1.666667,14.1666259765625Q1.666667,15.2021259765625,2.3989,15.9344259765625Q3.131133,16.6666259765625,4.16667,16.6666259765625L7.44728,16.6666259765625Q7.47361,16.6683259765625,7.5,16.6683259765625Q7.58224,16.6683259765625,7.6629,16.652225976562498Q7.74356,16.6362259765625,7.81954,16.6047259765625Q7.89552,16.573225976562497,7.9639,16.5275259765625Q8.03228,16.4819259765625,8.09043,16.4237259765625Q8.14859,16.365525976562502,8.19428,16.2972259765625Q8.23997,16.2288259765625,8.27144,16.1528259765625Q8.30291,16.0768259765625,8.318950000000001,15.9962259765625Q8.335,15.9155259765625,8.335,15.8333259765625Q8.335,15.7510259765625,8.31896,15.6704259765625Q8.30291,15.5897259765625,8.27144,15.5137259765625Q8.23997,15.4377259765625,8.19428,15.3694259765625Q8.14859,15.3010259765625,8.09043,15.2428259765625Q8.03228,15.1847259765625,7.9639,15.1390259765625Q7.89552,15.0933259765625,7.81954,15.0618259765625Q7.74356,15.0304259765625,7.6629,15.0143259765625Q7.58224,14.9983259765625,7.5,14.9983259765625Q7.47361,14.9983259765625,7.44728,14.9999259765625L4.16667,14.9999259765625Q3.82149,14.9999259765625,3.57741,14.7559259765625Q3.333333,14.5118259765625,3.333333,14.1666259765625L3.333333,5.8332959765625Q3.333333,5.4881159765625,3.57741,5.2440359765625Q3.82149,4.9999589765625,4.16667,4.9999589765625L7.44727,4.9999589765625Q7.47361,5.0016259765625,7.5,5.0016259765625Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
/></g
><g
><path
d="M12.55273,4.9999589765625Q12.5263913,5.0016259765625,12.5,5.0016259765625Q12.4177597,5.0016259765625,12.3371,4.9855819765625Q12.25644,4.9695369765625,12.180459,4.9380649765625Q12.104479,4.9065929765625,12.036099,4.8609029765625Q11.967719,4.8152129765625,11.909566,4.7570599765625Q11.851413,4.6989069765625,11.805723,4.6305269765625Q11.760033,4.5621469765625005,11.728561,4.4861669765625Q11.697089,4.4101859765625,11.681044,4.3295259765625Q11.665,4.2488662765625,11.665,4.1666259765625Q11.665,4.0843856765625,11.681044,4.0037259765625Q11.697089,3.9230659765625,11.728561,3.8470849765625Q11.760033,3.7711049765625,11.805723,3.7027249765625Q11.851413,3.6343449765624998,11.909566,3.5761919765625Q11.967719,3.5180389765625,12.036099,3.4723489765625Q12.104479,3.4266589765625,12.180459,3.3951869765625Q12.25644,3.3637149765625,12.3371,3.3476699765625Q12.4177597,3.3316259765625,12.5,3.3316259765625Q12.5263913,3.3316259765625,12.55273,3.3332929765625L15.83333,3.3332929765625Q16.86887,3.3332929765625,17.6011,4.0655259765625Q18.33333,4.7977589765625,18.33333,5.8332959765625L18.33333,14.1666259765625Q18.33333,15.2021259765625,17.6011,15.9344259765625Q16.86887,16.6666259765625,15.83333,16.6666259765625L12.5527215,16.6666259765625Q12.5263871,16.6683259765625,12.5,16.6683259765625Q12.4177597,16.6683259765625,12.3371,16.652225976562498Q12.25644,16.6362259765625,12.180459,16.6047259765625Q12.104479,16.573225976562497,12.036099,16.5275259765625Q11.967719,16.4819259765625,11.909566,16.4237259765625Q11.851413,16.365525976562502,11.805723,16.2972259765625Q11.760033,16.2288259765625,11.728561,16.1528259765625Q11.697089,16.0768259765625,11.681044,15.9962259765625Q11.665,15.9155259765625,11.665,15.8333259765625Q11.665,15.7510259765625,11.681044,15.6704259765625Q11.697089,15.5897259765625,11.728561,15.5137259765625Q11.760033,15.4377259765625,11.805723,15.3694259765625Q11.851413,15.3010259765625,11.909566,15.2428259765625Q11.967719,15.1847259765625,12.036099,15.1390259765625Q12.104479,15.0933259765625,12.180459,15.0618259765625Q12.25644,15.0304259765625,12.3371,15.0143259765625Q12.4177597,14.9983259765625,12.5,14.9983259765625Q12.5263871,14.9983259765625,12.5527215,14.9999259765625L15.83333,14.9999259765625Q16.17851,14.9999259765625,16.42259,14.7559259765625Q16.66667,14.5118259765625,16.66667,14.1666259765625L16.66667,5.8332959765625Q16.66667,5.4881159765625,16.42259,5.2440359765625Q16.17851,4.9999589765625,15.83333,4.9999589765625L12.55273,4.9999589765625Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
/></g
><g
><path
d="M10.833333,2.5527319Q10.835,2.5263923,10.835,2.5Q10.835,2.4177597,10.818956,2.3371Q10.802911,2.25644,10.771439,2.180459Q10.739967,2.104479,10.694277,2.036099Q10.648587,1.967719,10.590434,1.9095659999999999Q10.532281,1.851413,10.463901,1.805723Q10.395521,1.760033,10.319541,1.728561Q10.24356,1.697089,10.1629,1.681044Q10.0822403,1.665,10,1.665Q9.9177597,1.665,9.8371,1.681044Q9.75644,1.697089,9.680459,1.728561Q9.604479,1.760033,9.536099,1.805723Q9.467719,1.851413,9.409566,1.9095659999999999Q9.351413,1.967719,9.305723,2.036099Q9.260033,2.104479,9.228561,2.180459Q9.197089,2.25644,9.181044,2.3371Q9.165,2.4177597,9.165,2.5Q9.165,2.5263923,9.166667,2.5527319L9.166667,17.4473Q9.165,17.473599999999998,9.165,17.5Q9.165,17.5822,9.181044,17.6629Q9.197089,17.7436,9.228561,17.819499999999998Q9.260033,17.8955,9.305723,17.963900000000002Q9.351413,18.0323,9.409566,18.090400000000002Q9.467719,18.148600000000002,9.536099,18.1943Q9.604479,18.240000000000002,9.680459,18.2714Q9.75644,18.3029,9.8371,18.319000000000003Q9.9177597,18.335,10,18.335Q10.0822403,18.335,10.1629,18.319000000000003Q10.24356,18.3029,10.319541,18.2714Q10.395521,18.240000000000002,10.463901,18.1943Q10.532281,18.148600000000002,10.590434,18.090400000000002Q10.648587,18.0323,10.694277,17.963900000000002Q10.739967,17.8955,10.771439,17.819499999999998Q10.802911,17.7436,10.818956,17.6629Q10.835,17.5822,10.835,17.5Q10.835,17.473599999999998,10.833333,17.4473L10.833333,2.5527319Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
/></g
></g
></svg
>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,13 @@
<svg
t="1742449891206"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2067"
xmlns:xlink="http://www.w3.org/1999/xlink"
><path
d="M950.857143 109.714286l0 804.571429q0 14.857143-10.857143 25.714286t-25.714286 10.857143l-804.571429 0q-14.857143 0-25.714286-10.857143t-10.857143-25.714286l0-804.571429q0-14.857143 10.857143-25.714286t25.714286-10.857143l804.571429 0q14.857143 0 25.714286 10.857143t10.857143 25.714286z"
p-id="2068"
></path></svg
>

After

Width:  |  Height:  |  Size: 517 B

View File

@@ -0,0 +1,40 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
fill="none"
version="1.1"
width="20"
height="20"
viewBox="0 0 20 20"
><defs
><clipPath id="master_svg0_20_113"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
></defs
><g clip-path="url(#master_svg0_20_113)"
><g
><path
d="M17.52452171875,9.078936578124999Q17.659471718749998,8.960048578125,17.73351171875,8.796145578125Q17.80755171875,8.632242578125,17.80755171875,8.452392578125Q17.80755171875,8.370152278125,17.79151171875,8.289492578125Q17.77546171875,8.208832578125,17.74399171875,8.132851578125Q17.71252171875,8.056871578125,17.66683171875,7.988491578125Q17.62114171875,7.920111578125,17.56299171875,7.861958578125Q17.50483171875,7.803805578125,17.43645171875,7.758115578125Q17.36807171875,7.712425578125,17.29209171875,7.680953578125Q17.21611171875,7.649481578125,17.13545171875,7.633436578125Q17.05479171875,7.617392578125,16.97255171875,7.617392578125Q16.79270171875,7.617392578125,16.62880171875,7.691433578125Q16.46490171875,7.765474578125,16.34601171875,7.900425578125L12.88504271875,11.361392578124999Q12.75009271875,11.480282578125,12.67605171875,11.644182578125001Q12.60201171875,11.808082578125,12.60201171875,11.987932578125001Q12.60201171875,12.070172578125,12.61805571875,12.150832578125Q12.63410071875,12.231492578125,12.66557271875,12.307472578125001Q12.69704471875,12.383452578125,12.74273471875,12.451832578125Q12.78842471875,12.520212578125001,12.84657771875,12.578362578124999Q12.90473071875,12.636522578125,12.97311071875,12.682212578125Q13.04149071875,12.727902578125,13.11747071875,12.759372578125Q13.19345171875,12.790842578125,13.27411171875,12.806892578125Q13.35477141875,12.822932578125,13.43701171875,12.822932578125Q13.61685971875,12.822932578125,13.78076071875,12.748892578125Q13.94466171875,12.674852578125,14.06354971875,12.539902578125L17.52452171875,9.078936578124999Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M12.88553,9.078933578125Q12.75058,8.960045578125,12.67654,8.796143578125Q12.6025,8.632241578125,12.6025,8.452392578125Q12.6025,8.370152278125,12.618544,8.289492578125Q12.634589,8.208832578125,12.666061,8.132851578125Q12.697533,8.056871578125,12.743223,7.988491578125Q12.788913,7.920111578125,12.847066,7.861958578125Q12.905219,7.803805578125,12.973599,7.758115578125Q13.041979,7.712425578125,13.117959,7.680953578125Q13.19394,7.649481578125,13.2746,7.633436578125Q13.3552597,7.617392578125,13.4375,7.617392578125Q13.617349,7.617392578125,13.781251,7.691432578125Q13.945153,7.765472578125,14.064041,7.900422578125L17.52501,11.361392578124999Q17.659959999999998,11.480282578125,17.734,11.644182578125001Q17.80804,11.808082578125,17.80804,11.987932578125001Q17.80804,12.070172578125,17.792,12.150832578125Q17.77595,12.231492578125,17.74448,12.307472578125001Q17.71301,12.383452578125,17.66732,12.451832578125Q17.62163,12.520212578125001,17.56347,12.578362578124999Q17.50532,12.636522578125,17.43694,12.682212578125Q17.36856,12.727902578125,17.29258,12.759372578125Q17.2166,12.790842578125,17.13594,12.806892578125Q17.05528,12.822932578125,16.97304,12.822932578125Q16.79319,12.822932578125,16.62929,12.748892578125Q16.46539,12.674852578125,16.3465,12.539902578125L12.88553,9.078933578125Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M4.44364390625,5.42117L2.49983690625,5.42117Q1.80948090625,5.42117,1.32132890625,5.90931Q0.83317090625,6.39747,0.83317090625,7.08783L0.83317090625,12.8496Q0.83317090625,13.54,1.32132990625,14.0281Q1.80948090625,14.5163,2.49983690625,14.5163L4.43961390625,14.5163Q6.77175390625,18.3333,9.99983390625,18.3333Q10.08191390625,18.3333,10.16241390625,18.3173Q10.24291390625,18.301299999999998,10.31874390625,18.2699Q10.39456390625,18.238500000000002,10.46281390625,18.1929Q10.53105390625,18.1473,10.58909390625,18.0893Q10.64713390625,18.0312,10.69272390625,17.963Q10.73832390625,17.8947,10.76973390625,17.8189Q10.80114390625,17.7431,10.81715390625,17.662599999999998Q10.83317390625,17.5821,10.83317390625,17.5L10.83317390625,2.5Q10.83317390625,2.4179238,10.81715390625,2.337425Q10.80114390625,2.256926,10.76973390625,2.181097Q10.73832390625,2.105269,10.69272390625,2.037025Q10.64712390625,1.968781,10.58909390625,1.910744Q10.53105390625,1.852708,10.46281390625,1.807109Q10.39456390625,1.76151,10.31874390625,1.7301009999999999Q10.24291390625,1.698691,10.16241390625,1.682679Q10.08191390625,1.666667,9.99983390625,1.666667Q6.77619390625,1.666667,4.44364390625,5.42117ZM4.91587390625,7.08783Q5.02559390625,7.08783,5.13157390625,7.05943Q5.23755390625,7.03103,5.3325739062499995,6.97617Q5.42758390625,6.92131,5.50516390625,6.84372Q5.58274390625,6.76614,5.63759390625,6.67111Q7.22859390625,3.91495,9.16650390625,3.434681L9.16650390625,16.563299999999998Q7.23188390625,16.074199999999998,5.6405439062500005,13.2715Q5.58600390625,13.1754,5.50830390625,13.0969Q5.4306139062500005,13.0184,5.33514390625,12.9628Q5.23966390625,12.9072,5.1330139062499995,12.8784Q5.02635390625,12.8496,4.91587390625,12.8496L2.49983690625,12.8496L2.49983790625,7.08783L4.91587390625,7.08783Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
></g
></svg
>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,55 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
fill="none"
version="1.1"
width="20"
height="20"
viewBox="0 0 20 20"
><defs
><clipPath id="master_svg0_13_280"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
><clipPath id="master_svg1_13_280/13_053"
><rect x="0" y="0" width="20" height="20" rx="0" /></clipPath
></defs
><g clip-path="url(#master_svg0_13_280)"
><g clip-path="url(#master_svg1_13_280/13_053)"
><g
><rect
x="0"
y="0"
width="20"
height="20"
rx="0"
fill="#FFFFFF"
fill-opacity="0.009999999776482582"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M4.443888046875,5.42117L2.500081046875,5.42117Q1.809725046875,5.42117,1.321573046875,5.90931Q0.833415046875,6.39747,0.833415046875,7.08783L0.833415046875,12.8496Q0.833415046875,13.54,1.321574046875,14.0281Q1.809725046875,14.5163,2.500081046875,14.5163L4.439858046875,14.5163Q6.771998046875,18.3333,10.000078046875,18.3333Q10.082158046875,18.3333,10.162658046875,18.3173Q10.243158046875,18.301299999999998,10.318988046875,18.2699Q10.394808046875,18.238500000000002,10.463058046875,18.1929Q10.531298046875,18.1473,10.589338046875,18.0893Q10.647378046875,18.0312,10.692968046875,17.963Q10.738568046875,17.8947,10.769978046875,17.8189Q10.801388046875,17.7431,10.817398046875,17.662599999999998Q10.833418046875,17.5821,10.833418046875,17.5L10.833418046875,2.5Q10.833418046875,2.4179238,10.817398046875,2.337425Q10.801388046875,2.256926,10.769978046875,2.181097Q10.738568046875,2.105269,10.692968046875,2.037025Q10.647368046875,1.968781,10.589338046875,1.910744Q10.531298046875,1.852708,10.463058046875,1.807109Q10.394808046875,1.76151,10.318988046875,1.7301009999999999Q10.243158046875,1.698691,10.162658046875,1.682679Q10.082158046875,1.666667,10.000078046875,1.666667Q6.776438046875,1.666667,4.443888046875,5.42117ZM4.916118046875,7.08783Q5.025838046875,7.08783,5.131818046875,7.05943Q5.237798046875,7.03103,5.3328180468749995,6.97617Q5.427828046875,6.92131,5.505408046875,6.84372Q5.582988046875,6.76614,5.637838046875,6.67111Q7.228838046875,3.91495,9.166748046875,3.434681L9.166748046875,16.563299999999998Q7.232128046875,16.074199999999998,5.6407880468750005,13.2715Q5.586248046875,13.1754,5.508548046875,13.0969Q5.4308580468750005,13.0184,5.335388046875,12.9628Q5.239908046875,12.9072,5.1332580468749995,12.8784Q5.026598046875,12.8496,4.916118046875,12.8496L2.500081046875,12.8496L2.500082046875,7.08783L4.916118046875,7.08783Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M12.813896953124999,6.903831Q12.740067953125,6.845187,12.681175953125,6.771557Q12.622282953125,6.697926,12.581291953125,6.613017Q12.540300953125,6.528109,12.519276953125,6.436197Q12.498251953125,6.3442856,12.498251953125,6.25Q12.498251953125,6.1677597,12.514295953125,6.0871Q12.530340953125,6.00644,12.561812953125,5.930459Q12.593284953125,5.8544789999999995,12.638974953125,5.786099Q12.684664953125,5.717719,12.742817953125,5.659566Q12.800970953125,5.601413,12.869350953125,5.555723Q12.937730953125,5.510033,13.013710953125,5.478561Q13.089691953125,5.447089,13.170351953125,5.431044Q13.251011653125,5.415,13.333251953125,5.415Q13.501904953125,5.415,13.657335953125,5.4804580000000005Q13.812766953125,5.545916,13.930607953125,5.66657Q14.362131953125001,6.059567,14.707911953125,6.532997Q15.248111953125001,7.2726299999999995,15.535961953125,8.14354Q15.833251953125,9.04304,15.833251953125,10Q15.833251953125,10.94869,15.540941953125,11.84127Q15.257921953125,12.70551,14.726221953125,13.4418Q14.373671953125,13.92992,13.930609953125,14.33343Q13.812768953125,14.45408,13.657336953125,14.51954Q13.501904953125,14.585,13.333251953125,14.585Q13.251011653125,14.585,13.170351953125,14.56895Q13.089691953125,14.55291,13.013710953125,14.52144Q12.937730953125,14.48997,12.869350953125,14.44428Q12.800970953125,14.39859,12.742817953125,14.34043Q12.684664953125,14.28228,12.638974953125,14.213899999999999Q12.593284953125,14.145520000000001,12.561812953125,14.06954Q12.530340953125,13.99356,12.514295953125,13.9129Q12.498251953125,13.832239999999999,12.498251953125,13.75Q12.498251953125,13.655719999999999,12.519276953125,13.5638Q12.540300953125,13.47189,12.581291953125,13.386980000000001Q12.622282953125,13.30207,12.681174953125,13.228439999999999Q12.740067953125,13.154810000000001,12.813895953125,13.09617Q13.125969953125,12.8109,13.375114153125,12.46595Q14.166584953125,11.36993,14.166584953125,10Q14.166584953125,8.61762,13.362005753125,7.516Q13.117749953125,7.181583,12.813896953124999,6.903831Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
><g
><path
d="M14.863105578125,2.228405984375Q16.823672578125,3.456592984375,17.969842578125,5.468508984375Q19.166602578125,7.569218984375,19.166602578125,10.000018984375Q19.166602578125,12.469708984375,17.933552578125,14.594658984375Q16.751772578125,16.631258984375002,14.739248578125,17.847858984375Q14.525103578125,17.995758984375,14.264892578125,17.995758984375Q14.182652278125,17.995758984375,14.101992578125,17.979658984375Q14.021332578125,17.963658984375,13.945351578125,17.932158984375Q13.869371578125,17.900658984375,13.800991578125,17.854958984375Q13.732611578125,17.809358984375002,13.674458578125,17.751158984375Q13.616305578125,17.692958984375,13.570615578125,17.624658984375Q13.524925578125,17.556258984375,13.493453578125,17.480258984375Q13.461981578125,17.404258984374998,13.445936578125,17.323658984375Q13.429892578125,17.242958984375,13.429892578125,17.160758984375Q13.429892578125,17.045958984374998,13.460862578124999,16.935458984375Q13.491831578125,16.824858984375,13.551473578125,16.726858984375Q13.611115578125,16.628758984375,13.695005578125,16.550458984375Q13.778896578125,16.472058984375,13.880811578125,16.419258984375Q15.525652578125,15.423558984375,16.492012578125,13.758158984375Q17.499932578125,12.021168984375,17.499932578125,10.000018984375Q17.499932578125,8.010658984374999,16.521692578125,6.293508984375Q15.584492578125,4.648418984375,13.982075578125,3.643182984375Q13.882499578125,3.589574984375,13.800770578125,3.511412984375Q13.719041578125,3.433249984375,13.661047578125,3.336162984375Q13.603053578125,3.239076984375,13.572972578125,3.130062984375Q13.542891578125,3.021047984375,13.542891578125,2.907958984375Q13.542891578125,2.825718684375,13.558936578125,2.745058984375Q13.574980578125,2.664398984375,13.606452578125,2.588417984375Q13.637924578125,2.512437984375,13.683614578125,2.444057984375Q13.729305578125,2.3756779843749998,13.787457578125,2.317524984375Q13.845610578125,2.259371984375,13.913990578125,2.213681984375Q13.982370578125,2.167991984375,14.058351578125,2.136519984375Q14.134331578125,2.105047984375,14.214991478125,2.089002984375Q14.295651478125,2.072958984375,14.377891578125,2.072958984375Q14.508378578125,2.072958984375,14.632644578125,2.112769984375Q14.756910578125,2.152579984375,14.863105578125,2.228405984375Z"
fill-rule="evenodd"
fill="#FFFFFF"
fill-opacity="1"
style="mix-blend-mode:passthrough"
/></g
></g
></g
></svg
>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,28 @@
import './style.css'
import IconFont from "./IconFont.svelte";
import Send from "./Send.svelte";
import Stop from "./Stop.svelte";
import CameraOff from "./CameraOff.svelte";
import CameraOn from "./CameraOn.svelte";
import VolumeOff from "./VolumeOff.svelte";
import VolumeOn from "./VolumeOn.svelte";
import MicOff from "./MicOff.svelte";
import MicOn from "./MicOn.svelte";
import Check from "./Check.svelte";
import PictureInPicture from "./PictureInPicture.svelte";
import SideBySide from "./SideBySide.svelte";
export {
IconFont,
CameraOff,
CameraOn,
VolumeOff,
VolumeOn,
MicOff,
MicOn,
Send,
Check,
PictureInPicture,
SideBySide,
Stop
}

View File

@@ -0,0 +1,9 @@
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
color: inherit;
font-size: inherit;
}

View File

@@ -0,0 +1,881 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
import type { ComponentType } from "svelte";
import {
CameraOff,
CameraOn,
Check,
PictureInPicture,
Send,
SideBySide,
VolumeOff,
VolumeOn,
MicOff,
MicOn,
} from "./icons";
import type { I18nFormatter } from "@gradio/utils";
import { Spinner } from "@gradio/icons";
import WebcamPermissions from "../WebcamPermissions.svelte";
import AudioWave from "../AudioWave.svelte";
import { fade } from "svelte/transition";
import {
createSimulatedAudioTrack,
createSimulatedVideoTrack,
get_devices,
get_stream,
set_available_devices,
set_local_stream,
} from "../VideoChat/stream_utils";
import { start, stop } from "../webrtc_utils";
import { derived } from "svelte/store";
import ChatInput from "./components/ChatInput.svelte";
import ChatBtn from "./components/ChatBtn.svelte";
import ChatMessage from "./components/ChatMessage.svelte";
import { click_outside } from "./utils";
let available_video_devices: MediaDeviceInfo[] = [];
let available_audio_devices: MediaDeviceInfo[] = [];
let selected_video_device: MediaDeviceInfo | null = null;
let selected_audio_device: MediaDeviceInfo | null = null;
let stream_state: "open" | "waiting" | "closed" = "closed";
export let on_change_cb: (msg: "tick" | "change") => void;
const _webrtc_id = Math.random().toString(36).substring(2);
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let button_labels: { start: string; stop: string; waiting: string };
export let height: number | undefined;
export const modify_stream: (state: "open" | "closed" | "waiting") => void = (
state: "open" | "closed" | "waiting",
) => {
if (state === "closed") {
stream_state = "closed";
} else if (state === "waiting") {
stream_state = "waiting";
} else {
stream_state = "open";
}
};
export let track_constraints: MediaTrackConstraints | null = null;
export let rtc_configuration: Object;
export let stream_every = 1;
export let server: {
offer: (body: any) => Promise<any>;
};
export let i18n: I18nFormatter;
let volumeMuted = false;
let micMuted = false;
let cameraOff = false;
const handle_volume_mute = () => {
volumeMuted = !volumeMuted;
};
const handle_mic_mute = () => {
micMuted = !micMuted;
stream.getTracks().forEach((track) => {
if (track.kind.includes("audio")) track.enabled = !micMuted;
});
};
const handle_camera_off = () => {
cameraOff = !cameraOff;
stream.getTracks().forEach((track) => {
if (track.kind.includes("video")) track.enabled = !cameraOff;
});
};
const dispatch = createEventDispatcher<{
tick: undefined;
error: string;
start_recording: undefined;
stop_recording: undefined;
close_stream: undefined;
}>();
const handle_device_change = async (deviceId: string): Promise<void> => {
const device_id = deviceId;
console.log(deviceId, selected_audio_device, selected_video_device);
let videoDeviceId = selected_video_device
? selected_video_device.deviceId
: "";
let audioDeviceId = selected_audio_device
? selected_audio_device.deviceId
: "";
if (
available_audio_devices.find(
(audio_device) => audio_device.deviceId === device_id,
)
) {
audioDeviceId = device_id;
micListShow = false;
micMuted = false;
} else if (
available_video_devices.find(
(video_device) => video_device.deviceId === device_id,
)
) {
videoDeviceId = device_id;
cameraListShow = false;
cameraOff = false;
}
const node = localVideoRef;
await get_stream(
audioDeviceId
? {
deviceId: { exact: audioDeviceId },
}
: hasMic,
videoDeviceId ? { deviceId: { exact: videoDeviceId } } : hasCamera,
node,
track_constraints,
).then(async (local_stream) => {
stream = local_stream;
local_stream = local_stream;
set_local_stream(local_stream, node);
selected_video_device =
available_video_devices.find(
(device) => device.deviceId === videoDeviceId,
) || null;
selected_audio_device =
available_audio_devices.find(
(device) => device.deviceId === audioDeviceId,
) || null;
});
};
let hasCamera = true;
let hasMic = true;
async function access_webcam(): Promise<void> {
try {
const node = localVideoRef;
micMuted = false;
cameraOff = false;
volumeMuted = false;
const devices = await get_devices();
available_video_devices = set_available_devices(devices, "videoinput");
available_audio_devices = set_available_devices(devices, "audioinput");
console.log(available_video_devices);
console.log(available_audio_devices);
await get_stream(
devices.some((device) => device.kind === "audioinput"),
devices.some((device) => device.kind === "videoinput"),
node,
track_constraints,
)
.then(async (local_stream) => {
stream = local_stream;
})
.then(() => {
const used_devices = stream
.getTracks()
.map((track) => track.getSettings()?.deviceId);
used_devices.forEach((device_id) => {
const used_device = devices.find(
(device) => device.deviceId === device_id,
);
if (used_device && used_device?.kind.includes("video")) {
selected_video_device = used_device;
} else if (used_device && used_device?.kind.includes("audio")) {
selected_audio_device = used_device;
}
});
!selected_video_device &&
(selected_video_device = available_video_devices[0]);
})
.catch(() => {
alert(i18n("image.no_webcam_support"));
})
.finally(() => {
if (!stream) {
stream = new MediaStream();
}
if (!stream.getTracks().find((item) => item.kind === "audio")) {
stream.addTrack(createSimulatedAudioTrack());
hasMic = false;
}
if (!stream.getTracks().find((item) => item.kind === "video")) {
stream.addTrack(createSimulatedVideoTrack());
hasCamera = false;
}
webcam_accessed = true;
local_stream = stream;
set_local_stream(local_stream, node);
});
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
dispatch("error", i18n("image.no_webcam_support"));
alert(i18n("image.no_webcam_support"));
}
} catch (err) {
if (err instanceof DOMException && err.name == "NotAllowedError") {
dispatch("error", i18n("image.allow_webcam_access"));
} else {
throw err;
}
}
}
let recording = false;
let stream: MediaStream;
let local_stream: MediaStream;
let webcam_accessed = false;
let webcam_received = false;
let pc: RTCPeerConnection;
export let webrtc_id;
export let wave_color: string = "#7873F6";
const audio_source_callback = () => {
if (local_stream) return local_stream;
else return localVideoRef.srcObject as MediaStream;
};
let replying = false;
let chat_data_channel;
function on_interrupt() {
if (chat_data_channel) {
chat_data_channel.send(JSON.stringify({type: 'stop_chat'}));
}
}
function on_send(message: string) {
chat_data_channel.send(JSON.stringify({ type: "chat", data: message }));
replying = true;
}
let answerId = "";
let answerMessage = "";
function on_channel_message(event: any) {
const data = JSON.parse(event.data);
if (data.type === "chat") {
if (answerId !== data.id) {
answerMessage = "";
answerId = data.id;
}
answerMessage += data.message;
} else if (data.type === "avatar_end") {
replying = false;
}
}
async function start_webrtc(): Promise<void> {
if (stream_state === "closed") {
pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange", async (event) => {
switch (pc.connectionState) {
case "connected":
stream_state = "open";
break;
case "disconnected":
stream_state = "closed";
stop(pc);
await access_webcam();
break;
default:
break;
}
});
stream_state = "waiting";
webrtc_id = Math.random().toString(36).substring(2);
start(
stream,
pc,
remoteVideoRef,
server.offer,
webrtc_id,
"video",
on_change_cb,
rtp_params,
)
.then(([connection, datachannel]) => {
pc = connection;
webcam_received = true;
computeRemotePosition();
chat_data_channel = datachannel;
chat_data_channel.addEventListener("message", on_channel_message);
})
.catch(() => {
console.info("catching");
stream_state = "closed";
webcam_received = false;
dispatch("error", "Too many concurrent users. Come back later!");
});
} else if (stream_state === "waiting") {
// waiting 中不允许操作
return;
} else {
replying = false
remoteVideoPosition.init = false;
computeLocalPosition();
stop(pc);
stream_state = "closed";
webcam_received = false;
await access_webcam();
}
}
let wrapperRef: HTMLDivElement;
const wrapperRect = {
width: 0,
height: 0,
};
let videoShowType: "side-by-side" | "picture-in-picture" =
"picture-in-picture";
$: isSideBySide = videoShowType === "side-by-side";
$: isPictureInPicture = videoShowType === "picture-in-picture";
let localVideoRef: HTMLVideoElement;
let localVideoContainerRef: HTMLDivElement;
let localVideoPosition = {
left: 10,
top: 0,
width: 1,
height: 1,
init: false,
};
let remoteVideoRef: HTMLVideoElement;
let remoteVideoContainerRef: HTMLDivElement;
let remoteVideoPosition = {
left: 0,
top: 0,
width: 1,
height: 1,
init: false,
};
let chatInputPosition = {
left: 0,
top: 0,
width: 1,
height: 1,
};
let actionsPosition = {
left: 0,
bottom: 0,
init: false,
isOverflow: false,
};
// remoteVideoPosition
// const computeVideoPosition = () => {
// }
// deal with dom events
onMount(() => {
wrapperRef.getBoundingClientRect();
wrapperRect.width = wrapperRef.clientWidth;
wrapperRect.height = wrapperRef.clientHeight;
isLandScape = wrapperRect.width * 1.5 > wrapperRect.height;
console.log(wrapperRect);
});
function changeVideoShowType() {
if (videoShowType === "picture-in-picture") {
videoShowType = "side-by-side";
} else if (videoShowType === "side-by-side") {
videoShowType = "picture-in-picture";
}
}
function computeChatInputPosition() {
const newPosition = remoteVideoPosition.init
? remoteVideoPosition
: localVideoPosition;
chatInputPosition.left = newPosition.left;
chatInputPosition.width = newPosition.width;
}
function computeLocalPosition() {
if (!localVideoRef || !localVideoContainerRef || !localVideoRef.videoHeight)
return;
if (remoteVideoPosition.init) {
// 存在远端视频则计算画中画
let height = remoteVideoPosition.height / 4;
let width =
(height / localVideoRef.videoHeight) * localVideoRef.videoWidth;
localVideoPosition.left = remoteVideoPosition.left + 14;
localVideoPosition.top =
remoteVideoPosition.top + remoteVideoPosition.height - height - 14;
localVideoPosition.width = width;
localVideoPosition.height = height;
actionsPosition.left =
remoteVideoPosition.left + remoteVideoPosition.width + 10;
actionsPosition.left =
actionsPosition.left > wrapperRect.width
? actionsPosition.left - 60
: actionsPosition.left;
actionsPosition.bottom =
wrapperRect.height -
remoteVideoPosition.top -
remoteVideoPosition.height +
5;
actionsPosition.isOverflow =
actionsPosition.left + 300 > wrapperRect.width;
} else {
// 否则则占用全屏
let height = wrapperRect.height - 24;
let width =
(height / localVideoRef.videoHeight) * localVideoRef.videoWidth;
width > wrapperRect.width && (width = wrapperRect.width);
localVideoPosition.left = (wrapperRect.width - width) / 2;
localVideoPosition.top = wrapperRect.height - height;
localVideoPosition.width = width;
localVideoPosition.height = height;
actionsPosition.left =
localVideoPosition.left + localVideoPosition.width + 10;
actionsPosition.left =
actionsPosition.left > wrapperRect.width
? actionsPosition.left - 60
: actionsPosition.left;
actionsPosition.bottom =
wrapperRect.height -
localVideoPosition.top -
localVideoPosition.height +
5;
actionsPosition.isOverflow =
actionsPosition.left + 300 > wrapperRect.width;
}
computeChatInputPosition();
}
function computeRemotePosition() {
if (!remoteVideoRef.srcObject || !remoteVideoRef.videoHeight) return;
console.log(
remoteVideoRef.videoHeight,
remoteVideoRef.videoWidth,
"---------------------",
);
let height = wrapperRect.height - 24;
let width =
(height / remoteVideoRef.videoHeight) * remoteVideoRef.videoWidth;
width > wrapperRect.width && (width = wrapperRect.width);
remoteVideoPosition.left = (wrapperRect.width - width) / 2;
remoteVideoPosition.top = wrapperRect.height - height;
remoteVideoPosition.width = width;
remoteVideoPosition.height = height;
remoteVideoPosition.init = true;
computeLocalPosition();
}
let micListShow = false;
let cameraListShow = false;
function open_mic_list(e) {
micListShow = true;
e.preventDefault();
e.stopPropagation();
}
function open_camera_list(e) {
cameraListShow = true;
e.preventDefault();
e.stopPropagation();
}
export let isLandScape = true;
window.addEventListener("resize", () => {
wrapperRef.getBoundingClientRect();
wrapperRect.width = wrapperRef.clientWidth;
wrapperRect.height = wrapperRef.clientHeight;
isLandScape = wrapperRect.width * 1.5 > wrapperRect.height;
computeLocalPosition();
computeRemotePosition();
});
</script>
<div class="wrap" style:height={height > 100 ? "100%" : "90vh"}>
<!-- svelte-ignore a11y-missing-attribute -->
{#if !webcam_accessed}
<div in:fade={{ delay: 100, duration: 200 }} style="height: 100%">
<WebcamPermissions on:click={async () => access_webcam()} />
</div>
{/if}
<div
class="video-container"
bind:this={wrapperRef}
class:vertical={!isLandScape}
class:picture-in-picture={isPictureInPicture}
class:side-by-side={isSideBySide}
class:no-local-video={!hasCamera || cameraOff}
style:visibility={webcam_accessed ? "visible" : "hidden"}
>
<div
class="local-video-container"
style:display={(!hasCamera && stream_state==='open') || cameraOff
? "none"
: "block"}
bind:this={localVideoContainerRef}
style:left={localVideoPosition.width < 10
? "50%"
: localVideoPosition.left + "px"}
style:top={localVideoPosition.height < 10
? "50%"
: localVideoPosition.top + "px"}
style:width={isPictureInPicture
? localVideoPosition.width + "px"
: ""}
style:height={isPictureInPicture
? localVideoPosition.height + "px"
: ""}
>
<video
style:display={(!hasCamera ) || cameraOff
? "none"
: "block"}
class="local-video"
on:playing={computeLocalPosition}
bind:this={localVideoRef}
autoplay
muted
playsinline
style:visibility={isPictureInPicture && cameraOff
? "hidden"
: "visible"}
/>
</div>
<div
class="remote-video-container"
bind:this={remoteVideoContainerRef}
style:left={remoteVideoPosition.width < 10
? "50%"
: remoteVideoPosition.left + "px"}
style:top={remoteVideoPosition.height < 10
? "50%"
: remoteVideoPosition.top + "px"}
style:width={isPictureInPicture
? remoteVideoPosition.width + "px"
: ""}
style:height={isPictureInPicture
? remoteVideoPosition.height + "px"
: ""}
>
<video
class="remote-video"
on:playing={computeRemotePosition}
bind:this={remoteVideoRef}
autoplay
playsinline
muted={volumeMuted}
/>
{#if stream_state === "open"}
{#if answerMessage}
<ChatMessage message={answerMessage}></ChatMessage>
{/if}
{/if}
</div>
<div
class="actions"
style:left={isPictureInPicture
? actionsPosition.left + "px"
: ""}
style:bottom={isPictureInPicture
? actionsPosition.bottom + "px"
: ""}
>
<div class="action-group">
<!-- svelte-ignore a11y-missing-attribute -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if hasCamera}
<div
class="action"
on:click={handle_camera_off}
use:click_outside={() => (cameraListShow = false)}
>
{#if cameraOff}
<CameraOff></CameraOff>
{:else}
<CameraOn></CameraOn>
{/if}
{#if stream_state === "closed"}<div
class="corner"
on:click={open_camera_list}
>
<div class="corner-inner"></div>
</div>{/if}
<div
class={`selectors ${actionsPosition.isOverflow || isSideBySide ? "left" : ""}`}
style:display={cameraListShow && stream_state === "closed"
? "block"
: "none"}
>
{#each available_video_devices as device, i}
<div
class="selector"
on:click|stopPropagation={(e) =>
handle_device_change(device.deviceId)}
>
{device.label}
{#if selected_video_device && device.deviceId === selected_video_device.deviceId}<div
class="active-icon"
>
<Check></Check>
</div>{/if}
</div>
{/each}
</div>
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if hasMic}
<div
class="action"
on:click={handle_mic_mute}
use:click_outside={() => (micListShow = false)}
>
{#if micMuted}
<MicOff></MicOff>
{:else}
<MicOn></MicOn>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if stream_state === "closed"}<div
class="corner"
on:click={open_mic_list}
>
<div class="corner-inner"></div>
</div>{/if}
<div
class={`selectors ${actionsPosition.isOverflow || isSideBySide ? "left" : ""}`}
style:display={micListShow && stream_state === "closed"
? "block"
: "none"}
>
{#each available_audio_devices as device, i}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="selector"
on:click|stopPropagation={(e) =>
handle_device_change(device.deviceId)}
>
{device.label}
{#if selected_audio_device && device.deviceId === selected_audio_device.deviceId}
<div class="active-icon">
<Check></Check>
</div>
{/if}
</div>
{/each}
</div>
</div>
{/if}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="action" on:click={handle_volume_mute}>
{#if volumeMuted}
<VolumeOff></VolumeOff>
{:else}
<VolumeOn></VolumeOn>
{/if}
</div>
</div>
{#if hasCamera}
<div class="action-group">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="action" on:click={changeVideoShowType}>
{#if isPictureInPicture}
<PictureInPicture></PictureInPicture>
{:else}
<SideBySide></SideBySide>
{/if}
</div>
</div>
{/if}
</div>
</div>
{#if (!hasMic || micMuted) && stream_state === "open"}
<div
class="chat-input-wrapper"
class:side-by-side={isSideBySide}
style={isPictureInPicture? `left: ${chatInputPosition.left}px;width: ${chatInputPosition.width}px`:''}
>
<ChatInput {replying} onInterrupt={on_interrupt} onSend={on_send} onStop={start_webrtc}></ChatInput>
</div>
{:else}
<ChatBtn
onStartChat={start_webrtc}
{audio_source_callback}
{stream_state}
{wave_color}
></ChatBtn>
{/if}
</div>
<style lang="less">
.wrap {
background-image: url(../background.png);
height: calc(max(80vh, 100%));
position: relative;
.chat-input-wrapper {
position: absolute;
transition: width 0.1s ease;
&.side-by-side{
left: 12px;
right: 12px;
}
}
.video-container {
position: relative;
height: 85%;
padding-top: 24px;
&.picture-in-picture {
.local-video-container,
.remote-video-container {
position: absolute;
top: 50%;
left: 50%;
width: 10px;
height: 10px;
border-radius: 32px;
overflow: hidden;
transition: all 0.3s linear;
}
.local-video-container {
z-index: 1;
background:#fff;
}
.local-video,
.remote-video {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&.side-by-side {
display: flex;
justify-content: space-between;
align-items: center;
&.no-local-video{
justify-content: center;
}
.local-video-container,
.remote-video-container {
position: static;
width: 49%;
height: 100%;
flex-shrink: 0;
flex-grow: 0;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
border-radius: 32px;
transition: all 0.3s linear;
overflow: hidden;
}
&.vertical {
flex-direction: column-reverse;
.local-video-container,
.remote-video-container {
width: 100%;
height: 49%;
}
}
.local-video,
.remote-video {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.actions {
position: absolute;
z-index: 2;
left: calc(100% - 60px);
.action-group {
border-radius: 12px;
background: rgba(88, 87, 87, 0.5);
padding: 2px;
backdrop-filter: blur(8px);
.action {
cursor: pointer;
width: 42px;
height: 42px;
border-radius: 8px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.corner {
position: absolute;
right: 0px;
bottom: 0px;
padding: 3px;
.corner-inner {
width: 6px;
height: 6px;
border-top: 3px transparent solid;
border-left: 3px transparent solid;
border-bottom: 3px #fff solid;
border-right: 3px #fff solid;
}
}
// &:hover {
// .selectors {
// display: block !important;
// }
// }
.selectors {
position: absolute;
top: 0;
left: calc(100%);
margin-left: 3px;
&.left {
left: 0;
margin-left: -3px;
transform: translateX(-100%);
}
border-radius: 12px;
width: max-content;
overflow: hidden;
background: rgba(90, 90, 90, 0.5);
backdrop-filter: blur(8px);
.selector {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
cursor: pointer;
height: 42px;
line-height: 42px;
color: #fff;
font-size: 14px;
&:hover {
background: #67666a;
}
padding-left: 15px;
padding-right: 50px;
.active-icon {
position: absolute;
right: 10px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
top: 0;
}
}
}
}
.action:hover {
background: #67666a;
}
}
.action-group + .action-group {
margin-top: 10px;
}
}
}
}
</style>

View File

@@ -0,0 +1,127 @@
export function get_devices(): Promise<MediaDeviceInfo[]> {
return navigator.mediaDevices.enumerateDevices();
}
export function handle_error(error: string): void {
throw new Error(error);
}
export function set_local_stream(
local_stream: MediaStream | null,
video_source: HTMLVideoElement,
): void {
video_source.srcObject = local_stream;
video_source.muted = true;
video_source.play();
}
export async function get_stream(
audio: boolean | { deviceId: { exact: string } },
video: boolean | { deviceId: { exact: string } },
video_source: HTMLVideoElement,
track_constraints?:
| MediaTrackConstraints
| { video: MediaTrackConstraints; audio: MediaTrackConstraints },
): Promise<MediaStream> {
const video_fallback_constraints = (track_constraints as any)?.video ||
track_constraints || {
width: { ideal: 500 },
height: { ideal: 500 },
};
const audio_fallback_constraints = (track_constraints as any)?.audio ||
track_constraints || {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
};
const constraints = {
video:
typeof video === "object"
? { ...video, ...video_fallback_constraints }
: video,
audio:
typeof audio === "object"
? { ...audio, ...audio_fallback_constraints }
: audio,
};
return navigator.mediaDevices
.getUserMedia(constraints)
.then((local_stream: MediaStream) => {
return local_stream;
});
}
export function set_available_devices(
devices: MediaDeviceInfo[],
kind: "videoinput" | "audioinput" = "videoinput",
): MediaDeviceInfo[] {
const cameras = devices.filter(
(device: MediaDeviceInfo) => device.kind === kind,
);
return cameras;
}
let video_track: MediaStreamTrack | null = null;
let audio_track: MediaStreamTrack | null = null;
export function createSimulatedVideoTrack(width = 1, height = 1) {
// if (video_track) return video_track
// 创建一个 canvas 元素
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.width = width || 500;
canvas.height = height || 500;
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
ctx.fillStyle = `hsl(0,0, 0, 1)`; // 动态颜色
ctx.fillRect(0, 0, canvas.width, canvas.height);
let time = 0;
// 在 canvas 上绘制动画内容
function drawFrame() {
// ctx.fillStyle = `rgb(0, ${(Date.now() / 10) % 360}, 1)`; // 动态颜色
ctx.fillStyle = `rgb(255, 255, 255)`; // 动态颜色
ctx.fillRect(0, 0, canvas.width, canvas.height);
// ctx.font = 'bold 50px Arial';
// ctx.fillStyle = `rgb(0, 0, 0)`;
// ctx.fillText(String(time++), 100, 100)
requestAnimationFrame(drawFrame);
}
drawFrame();
// 捕获 canvas 的视频流
const stream = canvas.captureStream(30); // 30 FPS
video_track = stream.getVideoTracks()[0]; // 返回视频轨道
video_track.stop = () => {
canvas.remove();
};
video_track.onended = () => {
video_track?.stop();
};
return video_track;
}
export function createSimulatedAudioTrack() {
if (audio_track) return audio_track;
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
oscillator.frequency.setValueAtTime(0, audioContext.currentTime);
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
const destination = audioContext.createMediaStreamDestination();
oscillator.connect(gainNode);
gainNode.connect(destination);
oscillator.start();
audio_track = destination.stream.getAudioTracks()[0];
audio_track.stop = () => {
audioContext.close();
};
audio_track.onended = () => {
audio_track?.stop();
};
return audio_track;
}

View File

@@ -0,0 +1,28 @@
export function click_outside(node: Node, cb: any): any {
const handle_click = (event: MouseEvent): void => {
if (
node &&
!node.contains(event.target as Node) &&
!event.defaultPrevented
) {
cb(event);
}
};
document.addEventListener("click", handle_click, true);
return {
destroy() {
document.removeEventListener("click", handle_click, true);
},
};
}
export function insertStringAt(rawStr: string, insertString: string, index: number) {
if (index < 0 || index > rawStr.length) {
console.error("索引超出范围");
return rawStr;
}
return rawStr.substring(0, index) + insertString + rawStr.substring(index);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +1,50 @@
<script lang="ts">
import { Webcam } from "@gradio/icons";
import { createEventDispatcher } from "svelte";
import { Webcam } from "@gradio/icons";
import { createEventDispatcher } from "svelte";
export let icon = Webcam;
$: text = icon === Webcam ? "Click to Access Webcam" : "Click to Access Microphone";
export let icon = Webcam;
$: text =
icon === Webcam ? "Click to Access Webcam" : "Click to Access Microphone";
const dispatch = createEventDispatcher<{
click: undefined;
}>();
const dispatch = createEventDispatcher<{
click: undefined;
}>();
</script>
<button style:height="100%" on:click={() => dispatch("click")}>
<div class="wrap">
<span class="icon-wrap">
<svelte:component this={icon} />
</span>
{text}
</div>
<div class="wrap">
<span class="icon-wrap">
<svelte:component this={icon} />
</span>
{text}
</div>
</button>
<style>
button {
cursor: pointer;
width: var(--size-full);
}
button {
cursor: pointer;
width: var(--size-full);
}
.wrap {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: var(--size-60);
color: var(--block-label-text-color);
height: 100%;
padding-top: var(--size-3);
}
.wrap {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: var(--size-60);
color: var(--block-label-text-color);
height: 100%;
padding-top: var(--size-3);
}
.icon-wrap {
width: 30px;
margin-bottom: var(--spacing-lg);
}
.icon-wrap {
width: 30px;
margin-bottom: var(--spacing-lg);
}
@media (--screen-md) {
.wrap {
font-size: var(--text-lg);
}
}
</style>
@media (--screen-md) {
.wrap {
font-size: var(--text-lg);
}
}
</style>

View File

@@ -8,7 +8,7 @@ export function handle_error(error: string): void {
export function set_local_stream(
local_stream: MediaStream | null,
video_source: HTMLVideoElement
video_source: HTMLVideoElement,
): void {
video_source.srcObject = local_stream;
video_source.muted = true;
@@ -21,7 +21,7 @@ export async function get_video_stream(
device_id?: string,
track_constraints?:
| MediaTrackConstraints
| { video: MediaTrackConstraints; audio: MediaTrackConstraints }
| { video: MediaTrackConstraints; audio: MediaTrackConstraints },
): Promise<MediaStream> {
console.log(track_constraints);
const video_fallback_constraints = (track_constraints as any)?.video ||
@@ -48,6 +48,8 @@ export async function get_video_stream(
return navigator.mediaDevices
.getUserMedia(constraints)
.then((local_stream: MediaStream) => {
// local_stream.removeTrack(local_stream.getVideoTracks()[0])
// local_stream.addTrack(createSimulatedVideoTrack())
set_local_stream(local_stream, video_source);
return local_stream;
});
@@ -55,10 +57,10 @@ export async function get_video_stream(
export function set_available_devices(
devices: MediaDeviceInfo[],
kind: "videoinput" | "audioinput" = "videoinput"
kind: "videoinput" | "audioinput" = "videoinput",
): MediaDeviceInfo[] {
const cameras = devices.filter(
(device: MediaDeviceInfo) => device.kind === kind
(device: MediaDeviceInfo) => device.kind === kind,
);
return cameras;

View File

@@ -53,6 +53,8 @@ export async function start(
modality: "video" | "audio" = "video",
on_change_cb: (msg: "change" | "tick") => void = () => {},
rtp_params = {},
additional_message_cb: (msg: object) => void = () => {},
reject_cb: (msg: object) => void = () => {},
) {
pc = createPeerConnection(pc, node);
const data_channel = pc.createDataChannel("text");
@@ -70,17 +72,19 @@ export async function start(
} catch (e) {
console.debug("Error parsing JSON");
}
console.log("event_json", event_json);
if (
event.data === "change" ||
event.data === "tick" ||
event.data === "stopword" ||
event_json?.type === "warning" ||
event_json?.type === "error"
event_json?.type === "error" ||
event_json?.type === "send_input" ||
event_json?.type === "fetch_output" ||
event_json?.type === "stopword"
) {
console.debug(`${event.data} event received`);
on_change_cb(event_json ?? event.data);
}
additional_message_cb(event_json ?? event.data);
};
if (stream) {
@@ -97,15 +101,20 @@ export async function start(
pc.addTransceiver(modality, { direction: "recvonly" });
}
await negotiate(pc, server_fn, webrtc_id);
return pc;
await negotiate(pc, server_fn, webrtc_id, reject_cb);
return [pc, data_channel] as const;
}
function make_offer(server_fn: any, body): Promise<object> {
function make_offer(
server_fn: any,
body,
reject_cb: (msg: object) => void = () => {},
): Promise<object> {
return new Promise((resolve, reject) => {
server_fn(body).then((data) => {
console.debug("data", data);
if (data?.status === "failed") {
reject_cb(data);
console.debug("rejecting");
reject("error");
}
@@ -118,6 +127,7 @@ async function negotiate(
pc: RTCPeerConnection,
server_fn: any,
webrtc_id: string,
reject_cb: (msg: object) => void = () => {},
): Promise<void> {
return pc
.createOffer()
@@ -138,17 +148,40 @@ async function negotiate(
resolve();
}
};
pc.addEventListener("icecandidate", () => {
console.debug("ice candidate", pc.iceGatheringState);
if (pc.iceGatheringState === "complete") {
pc.removeEventListener("icegatheringstatechange", checkState);
resolve();
}
});
pc.addEventListener("icegatheringstatechange", checkState);
pc.addEventListener("icecandidate", () => {
console.debug("ice candidate", pc.iceGatheringState);
if (pc.iceGatheringState === "complete") {
pc.removeEventListener("icegatheringstatechange", checkState);
resolve();
}
});
if (navigator.userAgent.includes("Safari")) {
setTimeout(() => {
resolve();
}, 3000);
}
}
});
})
.then(() => {
var offer = pc.localDescription;
return make_offer(server_fn, {
sdp: offer.sdp,
type: offer.type,
webrtc_id: webrtc_id,
});
return make_offer(
server_fn,
{
sdp: offer.sdp,
type: offer.type,
webrtc_id: webrtc_id,
},
reject_cb,
);
})
.then((response) => {
return response;