adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
432 lines
11 KiB
Svelte
432 lines
11 KiB
Svelte
<script lang="ts">
|
|
import { onDestroy } from "svelte";
|
|
import { Upload, ModifyUpload } from "@gradio/upload";
|
|
import { prepare_files, type FileData, type Client } from "@gradio/client";
|
|
import { BlockLabel, ShareButton, CustomButton } from "@gradio/atoms";
|
|
import { Music } from "@gradio/icons";
|
|
import { uploadToHuggingFace } from "@gradio/utils";
|
|
import { StreamingBar } from "@gradio/statustracker";
|
|
import AudioPlayer from "../player/AudioPlayer.svelte";
|
|
|
|
import type { IBlobEvent, IMediaRecorder } from "extendable-media-recorder";
|
|
import type { I18nFormatter } from "js/core/src/gradio_helper";
|
|
import AudioRecorder from "../recorder/AudioRecorder.svelte";
|
|
import StreamAudio from "../streaming/StreamAudio.svelte";
|
|
import { init_media_recorder } from "../streaming/media_recorder";
|
|
import type { IMediaRecorderConstructor } from "extendable-media-recorder";
|
|
import { SelectSource } from "@gradio/atoms";
|
|
import type { WaveformOptions, SubtitleData } from "../shared/types";
|
|
import type { CustomButton as CustomButtonType } from "@gradio/utils";
|
|
|
|
let {
|
|
value = $bindable(null),
|
|
subtitles = null,
|
|
label,
|
|
root,
|
|
loop,
|
|
show_label = true,
|
|
buttons = ["download", "share"],
|
|
on_custom_button_click = null,
|
|
sources = ["microphone", "upload"],
|
|
pending = false,
|
|
streaming = false,
|
|
i18n,
|
|
waveform_settings,
|
|
trim_region_settings = {},
|
|
waveform_options = {},
|
|
dragging = $bindable(false),
|
|
active_source = $bindable<"microphone" | "upload">("microphone"),
|
|
handle_reset_value = () => {},
|
|
editable = true,
|
|
max_file_size = null,
|
|
upload,
|
|
stream_handler,
|
|
stream_every = 0.1,
|
|
uploading = $bindable(false),
|
|
recording = $bindable(false),
|
|
class_name = "",
|
|
upload_promise = $bindable(),
|
|
initial_value = $bindable(),
|
|
playback_position = $bindable(),
|
|
time_limit = null,
|
|
stream_state = "closed",
|
|
onchange,
|
|
onstream,
|
|
onedit,
|
|
onplay,
|
|
onpause,
|
|
onstop,
|
|
ondrag,
|
|
onerror,
|
|
onupload,
|
|
onclear,
|
|
onstart_recording,
|
|
onpause_recording,
|
|
onstop_recording,
|
|
onclose_stream,
|
|
children
|
|
}: {
|
|
value?: null | FileData;
|
|
subtitles?: null | FileData | SubtitleData[];
|
|
label: string;
|
|
root: string;
|
|
loop?: boolean;
|
|
show_label?: boolean;
|
|
buttons?: (string | CustomButtonType)[];
|
|
on_custom_button_click?: ((id: number) => void) | null;
|
|
sources?:
|
|
| ["microphone"]
|
|
| ["upload"]
|
|
| ["microphone", "upload"]
|
|
| ["upload", "microphone"];
|
|
pending?: boolean;
|
|
streaming?: boolean;
|
|
i18n: I18nFormatter;
|
|
waveform_settings: Record<string, any>;
|
|
trim_region_settings?: Record<string, any>;
|
|
waveform_options?: WaveformOptions;
|
|
dragging?: boolean;
|
|
active_source?: "microphone" | "upload";
|
|
handle_reset_value?: () => void;
|
|
editable?: boolean;
|
|
max_file_size?: number | null;
|
|
upload: Client["upload"];
|
|
stream_handler: Client["stream"];
|
|
stream_every?: number;
|
|
uploading?: boolean;
|
|
recording?: boolean;
|
|
class_name?: string;
|
|
upload_promise?: Promise<any> | null;
|
|
initial_value?: FileData | null;
|
|
playback_position?: number;
|
|
time_limit?: number | null;
|
|
stream_state?: "open" | "waiting" | "closed";
|
|
onchange?: (value: FileData | null) => void;
|
|
onstream?: (value: FileData) => void;
|
|
onedit?: () => void;
|
|
onplay?: () => void;
|
|
onpause?: () => void;
|
|
onstop?: () => void;
|
|
ondrag?: (dragging: boolean) => void;
|
|
onerror?: (error: string) => void;
|
|
onupload?: (value: FileData) => void;
|
|
onclear?: () => void;
|
|
onstart_recording?: () => void;
|
|
onpause_recording?: () => void;
|
|
onstop_recording?: () => void;
|
|
onclose_stream?: () => void;
|
|
children?: import("svelte").Snippet;
|
|
} = $props();
|
|
|
|
$effect(() => {
|
|
ondrag?.(dragging);
|
|
});
|
|
|
|
// TODO: make use of this
|
|
// export let type: "normal" | "numpy" = "normal";
|
|
let recorder: IMediaRecorder;
|
|
let mode = $state("");
|
|
let header: Uint8Array | undefined = undefined;
|
|
let pending_stream: Uint8Array[] = [];
|
|
let submit_pending_stream_on_pending_end = false;
|
|
let inited = false;
|
|
let streaming_media_recorder: IMediaRecorderConstructor;
|
|
|
|
const NUM_HEADER_BYTES = 44;
|
|
let audio_chunks: Blob[] = [];
|
|
const is_browser = typeof window !== "undefined";
|
|
if (is_browser && streaming) {
|
|
init_media_recorder().then((a) => {
|
|
streaming_media_recorder = a;
|
|
});
|
|
}
|
|
|
|
const to_blob_parts = (parts: Uint8Array[] | Blob[]): BlobPart[] =>
|
|
parts.map((part) => {
|
|
if (part instanceof Blob) return part;
|
|
return part.slice();
|
|
});
|
|
|
|
const dispatch_blob = async (
|
|
blobs: Uint8Array[] | Blob[],
|
|
event: "stream" | "change" | "stop_recording"
|
|
): Promise<void> => {
|
|
let _audio_blob = new File(to_blob_parts(blobs), "audio.wav", {
|
|
type: "audio/wav"
|
|
});
|
|
if (_audio_blob.size === 0) {
|
|
return;
|
|
}
|
|
const val = await prepare_files([_audio_blob], event === "stream");
|
|
initial_value = value;
|
|
value = (
|
|
(await upload(val, root, undefined, max_file_size || undefined))?.filter(
|
|
Boolean
|
|
) as FileData[]
|
|
)[0];
|
|
if (event === "stream") {
|
|
onstream?.(value);
|
|
} else if (event === "change") {
|
|
onchange?.(value);
|
|
} else if (event === "stop_recording") {
|
|
onstop_recording?.();
|
|
}
|
|
};
|
|
|
|
onDestroy(() => {
|
|
if (streaming && recorder && recorder.state !== "inactive") {
|
|
recorder.stop();
|
|
}
|
|
});
|
|
|
|
async function prepare_audio(): Promise<void> {
|
|
let stream: MediaStream | null;
|
|
|
|
try {
|
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
} catch (err) {
|
|
if (!navigator.mediaDevices) {
|
|
onerror?.(i18n("audio.no_device_support"));
|
|
return;
|
|
}
|
|
if (err instanceof DOMException && err.name == "NotAllowedError") {
|
|
onerror?.(i18n("audio.allow_recording_access"));
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
if (stream == null) return;
|
|
if (streaming) {
|
|
recorder = new streaming_media_recorder(stream, {
|
|
mimeType: "audio/wav"
|
|
});
|
|
|
|
recorder.addEventListener("dataavailable", handle_chunk);
|
|
} else {
|
|
recorder = new MediaRecorder(stream);
|
|
recorder.addEventListener("dataavailable", (event) => {
|
|
audio_chunks.push(event.data);
|
|
});
|
|
}
|
|
recorder.addEventListener("stop", async () => {
|
|
recording = false;
|
|
recorder.stop();
|
|
await dispatch_blob(audio_chunks, "change");
|
|
await dispatch_blob(audio_chunks, "stop_recording");
|
|
audio_chunks = [];
|
|
});
|
|
inited = true;
|
|
}
|
|
|
|
async function handle_chunk(event: IBlobEvent): Promise<void> {
|
|
let buffer = await event.data.arrayBuffer();
|
|
let payload = new Uint8Array(buffer);
|
|
if (!header) {
|
|
header = new Uint8Array(buffer.slice(0, NUM_HEADER_BYTES));
|
|
payload = new Uint8Array(buffer.slice(NUM_HEADER_BYTES));
|
|
}
|
|
if (pending) {
|
|
pending_stream.push(payload);
|
|
} else {
|
|
let blobParts = [header].concat(pending_stream, [payload]);
|
|
if (!recording || stream_state === "waiting") return;
|
|
dispatch_blob(blobParts, "stream");
|
|
pending_stream = [];
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (submit_pending_stream_on_pending_end && pending === false) {
|
|
submit_pending_stream_on_pending_end = false;
|
|
if (header && pending_stream) {
|
|
let blobParts: Uint8Array[] = [header].concat(pending_stream);
|
|
pending_stream = [];
|
|
dispatch_blob(blobParts, "stream");
|
|
}
|
|
}
|
|
});
|
|
|
|
async function record(): Promise<void> {
|
|
recording = true;
|
|
onstart_recording?.();
|
|
if (!inited) await prepare_audio();
|
|
|
|
header = undefined;
|
|
if (streaming && recorder.state != "recording") {
|
|
recorder.start(stream_every * 1000);
|
|
}
|
|
}
|
|
|
|
function clear(): void {
|
|
onchange?.(null);
|
|
onclear?.();
|
|
mode = "";
|
|
value = null;
|
|
}
|
|
|
|
function handle_load(detail: FileData): void {
|
|
value = detail;
|
|
onchange?.(detail);
|
|
onupload?.(detail);
|
|
}
|
|
|
|
async function stop(): Promise<void> {
|
|
recording = false;
|
|
|
|
if (streaming) {
|
|
onclose_stream?.();
|
|
onstop_recording?.();
|
|
recorder.stop();
|
|
|
|
if (pending) {
|
|
submit_pending_stream_on_pending_end = true;
|
|
}
|
|
await dispatch_blob(audio_chunks, "stop_recording");
|
|
onclear?.();
|
|
mode = "";
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (!recording && recorder && mode !== "") stop();
|
|
});
|
|
|
|
$effect(() => {
|
|
if (recording && recorder) record();
|
|
});
|
|
</script>
|
|
|
|
<BlockLabel
|
|
{show_label}
|
|
Icon={Music}
|
|
float={active_source === "upload" && value === null}
|
|
label={label || i18n("audio.audio")}
|
|
/>
|
|
<div
|
|
class="audio-container {class_name}"
|
|
data-testid={label ? "waveform-" + label : "unlabelled-audio"}
|
|
>
|
|
<StreamingBar {time_limit} />
|
|
{#if value == null || streaming}
|
|
{#if active_source === "microphone"}
|
|
<ModifyUpload {i18n} onclear={clear} />
|
|
{#if streaming}
|
|
<StreamAudio
|
|
{record}
|
|
{recording}
|
|
{stop}
|
|
{i18n}
|
|
{waveform_settings}
|
|
{waveform_options}
|
|
waiting={stream_state === "waiting"}
|
|
/>
|
|
{:else}
|
|
<AudioRecorder
|
|
bind:mode
|
|
{i18n}
|
|
{editable}
|
|
{recording}
|
|
{dispatch_blob}
|
|
{waveform_settings}
|
|
{waveform_options}
|
|
{handle_reset_value}
|
|
onstartrecording={() => onstart_recording?.()}
|
|
onpauserecording={() => onpause_recording?.()}
|
|
onstoprecording={() => onstop_recording?.()}
|
|
/>
|
|
{/if}
|
|
{:else if active_source === "upload"}
|
|
<!-- explicitly listed out audio mimetypes due to iOS bug not recognizing audio/* -->
|
|
<Upload
|
|
bind:upload_promise
|
|
filetype="audio/aac,audio/midi,audio/mpeg,audio/ogg,audio/wav,audio/x-wav,audio/opus,audio/webm,audio/flac,audio/vnd.rn-realaudio,audio/x-ms-wma,audio/x-aiff,audio/amr,audio/*"
|
|
onload={handle_load}
|
|
bind:dragging
|
|
bind:uploading
|
|
onerror={(detail: string) => onerror?.(detail)}
|
|
{root}
|
|
{max_file_size}
|
|
{upload}
|
|
{stream_handler}
|
|
aria_label={i18n("audio.drop_to_upload")}
|
|
>
|
|
{#if children}{@render children()}{/if}
|
|
</Upload>
|
|
{/if}
|
|
{:else}
|
|
<ModifyUpload
|
|
{i18n}
|
|
onclear={clear}
|
|
onedit={() => {
|
|
mode = "edit";
|
|
onedit?.();
|
|
}}
|
|
download={buttons === null
|
|
? value.url
|
|
: buttons.some((btn) => typeof btn === "string" && btn === "download")
|
|
? value.url
|
|
: null}
|
|
>
|
|
{#if value !== null && buttons}
|
|
{#each buttons as btn}
|
|
{#if typeof btn === "string"}
|
|
{#if btn === "share"}
|
|
<ShareButton
|
|
{i18n}
|
|
{onerror}
|
|
onshare={() => {}}
|
|
formatter={async (fileData: FileData) => {
|
|
if (!fileData || !fileData.url) return "";
|
|
let url = await uploadToHuggingFace(fileData.url, "url");
|
|
return `<audio controls src="${url}"></audio>`;
|
|
}}
|
|
{value}
|
|
/>
|
|
{/if}
|
|
{:else}
|
|
<CustomButton
|
|
button={btn}
|
|
on_click={(id: number) => {
|
|
if (on_custom_button_click) {
|
|
on_custom_button_click(id);
|
|
}
|
|
}}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
</ModifyUpload>
|
|
|
|
<AudioPlayer
|
|
bind:mode
|
|
{value}
|
|
subtitles={Array.isArray(subtitles) ? subtitles : subtitles?.url}
|
|
{label}
|
|
{i18n}
|
|
{dispatch_blob}
|
|
{waveform_settings}
|
|
{waveform_options}
|
|
{trim_region_settings}
|
|
{handle_reset_value}
|
|
{editable}
|
|
{loop}
|
|
bind:playback_position
|
|
interactive
|
|
{onstop}
|
|
{onplay}
|
|
{onpause}
|
|
{onedit}
|
|
/>
|
|
{/if}
|
|
<SelectSource {sources} bind:active_source handle_clear={clear} />
|
|
</div>
|
|
|
|
<style>
|
|
.audio-container {
|
|
height: calc(var(--size-full) - var(--size-6));
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
</style>
|