Skip to content

Commit baa1cd6

Browse files
hannahblairgradio-pr-botfreddyaboulton
authored
Improve audio player UI in gr.Chatbot (#12102)
* update wavesurfer * Add minimal audio player to audio component * Add minimal audio player to audio component * add changeset * update button panel * tweak * format * Merge remote-tracking branch 'origin/6.0-dev' into minimal-audio-player * spacing * fix button position on other components in the chatbot * add changeset * fix tests * fix test * fix test * fix test * fix liked test * format * test tweak * add await * test id fix * tweak * label fix * test fix * test tweak --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Freddy Boulton <41651716+freddyaboulton@users.noreply.github.com>
1 parent c61d741 commit baa1cd6

18 files changed

+398
-99
lines changed

.changeset/some-memes-jam.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@gradio/atoms": minor
3+
"@gradio/audio": minor
4+
"@gradio/chatbot": minor
5+
"@gradio/image": minor
6+
"gradio": minor
7+
---
8+
9+
feat:Improve audio player UI in gr.Chatbot

js/atoms/src/IconButtonWrapper.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script>
22
export let top_panel = true;
33
export let display_top_corner = false;
4+
export let show_background = true;
45
</script>
56

67
<div
7-
class={`icon-button-wrapper ${top_panel ? "top-panel" : ""} ${display_top_corner ? "display-top-corner" : "hide-top-corner"}`}
8+
class={`icon-button-wrapper ${top_panel ? "top-panel" : ""} ${display_top_corner ? "display-top-corner" : "hide-top-corner"} ${!show_background ? "no-background" : ""}`}
89
>
910
<slot></slot>
1011
</div>
@@ -74,4 +75,12 @@
7475
.icon-button-wrapper :global(> *) {
7576
height: 100%;
7677
}
78+
79+
.icon-button-wrapper.no-background {
80+
box-shadow: none;
81+
border: none;
82+
background: none;
83+
padding: 0;
84+
z-index: var(--layer-1);
85+
}
7786
</style>

js/audio/Index.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
3333
const gradio = new AudioGradio(props);
3434
let label = $derived(gradio.shared.label || gradio.i18n("audio.audio"));
35+
let minimal = $derived(
36+
(props as any).minimal ?? (gradio.props as any).minimal ?? false
37+
);
3538
3639
// let uploading = $state(false);
3740
let active_source = $derived.by(() =>
@@ -143,6 +146,7 @@
143146
{waveform_settings}
144147
waveform_options={gradio.props.waveform_options}
145148
editable={gradio.props.editable}
149+
{minimal}
146150
on:share={(e) => gradio.dispatch("share", e.detail)}
147151
on:error={(e) => gradio.dispatch("error", e.detail)}
148152
on:play={() => gradio.dispatch("play")}

js/audio/interactive/InteractiveAudio.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,10 @@
238238
float={active_source === "upload" && value === null}
239239
label={label || i18n("audio.audio")}
240240
/>
241-
<div class="audio-container {class_name}">
241+
<div
242+
class="audio-container {class_name}"
243+
data-testid={label ? "waveform-" + label : "unlabelled-audio"}
244+
>
242245
<StreamingBar {time_limit} />
243246
{#if value === null || streaming}
244247
{#if active_source === "microphone"}

js/audio/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"hls.js": "1.5.13",
2121
"resize-observer-polyfill": "1.5.1",
2222
"svelte-range-slider-pips": "4.1.0",
23-
"wavesurfer.js": "7.4.2"
23+
"wavesurfer.js": "7.11.0"
2424
},
2525
"devDependencies": {
2626
"@gradio/preview": "workspace:^"

js/audio/player/AudioPlayer.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@
9191
timeRef && (timeRef.textContent = format_time(currentTime))
9292
);
9393
94+
waveform?.on("interaction", () => {
95+
const currentTime = waveform?.getCurrentTime() || 0;
96+
timeRef && (timeRef.textContent = format_time(currentTime));
97+
});
98+
9499
waveform?.on("ready", () => {
95100
if (!waveform_settings.autoplay) {
96101
waveform?.stop();
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<script lang="ts">
2+
import { onMount, onDestroy } from "svelte";
3+
import WaveSurfer from "wavesurfer.js";
4+
import type { FileData } from "@gradio/client";
5+
import { format_time } from "@gradio/utils";
6+
7+
export let value: FileData;
8+
export let label: string;
9+
export let loop = false;
10+
11+
let container: HTMLDivElement;
12+
let waveform: WaveSurfer | undefined;
13+
let playing = false;
14+
let duration = 0;
15+
let currentTime = 0;
16+
let waveform_ready = false;
17+
18+
$: resolved_src = value.url;
19+
20+
const create_waveform = async (): Promise<void> => {
21+
if (!container || !resolved_src || waveform_ready) return;
22+
23+
if (waveform) {
24+
waveform.destroy();
25+
}
26+
27+
const accentColor =
28+
getComputedStyle(document.documentElement).getPropertyValue(
29+
"--color-accent"
30+
) || "#ff7c00";
31+
32+
waveform = WaveSurfer.create({
33+
container,
34+
height: 32,
35+
waveColor: "rgba(128, 128, 128, 0.5)",
36+
progressColor: accentColor,
37+
cursorColor: "transparent",
38+
barWidth: 2,
39+
barGap: 2,
40+
barRadius: 2,
41+
normalize: true,
42+
interact: true,
43+
dragToSeek: true,
44+
hideScrollbar: true
45+
});
46+
47+
waveform.on("play", () => (playing = true));
48+
waveform.on("pause", () => (playing = false));
49+
waveform.on("ready", () => {
50+
duration = waveform?.getDuration() || 0;
51+
waveform_ready = true;
52+
});
53+
waveform.on("audioprocess", () => {
54+
currentTime = waveform?.getCurrentTime() || 0;
55+
});
56+
waveform.on("interaction", () => {
57+
currentTime = waveform?.getCurrentTime() || 0;
58+
});
59+
waveform.on("finish", () => {
60+
playing = false;
61+
if (loop) {
62+
waveform?.play();
63+
}
64+
});
65+
66+
await waveform.load(resolved_src);
67+
};
68+
69+
onMount(async () => {
70+
await create_waveform();
71+
});
72+
73+
onDestroy(() => {
74+
if (waveform) {
75+
waveform.destroy();
76+
}
77+
});
78+
79+
const togglePlay = (): void => {
80+
if (waveform) {
81+
waveform.playPause();
82+
}
83+
};
84+
</script>
85+
86+
<div
87+
class="minimal-audio-player"
88+
aria-label={label || "Audio"}
89+
data-testid={label && typeof label === "string" && label.trim()
90+
? "waveform-" + label
91+
: "unlabelled-audio"}
92+
>
93+
<button
94+
class="play-btn"
95+
on:click={togglePlay}
96+
aria-label={playing ? "Pause" : "Play"}
97+
>
98+
{#if playing}
99+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
100+
<rect x="6" y="5" width="4" height="14" rx="1" fill="currentColor" />
101+
<rect x="14" y="5" width="4" height="14" rx="1" fill="currentColor" />
102+
</svg>
103+
{:else}
104+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
105+
<path
106+
d="M8 5.74537C8 5.06444 8.77346 4.64713 9.35139 5.02248L18.0227 10.2771C18.5518 10.6219 18.5518 11.3781 18.0227 11.7229L9.35139 16.9775C8.77346 17.3529 8 16.9356 8 16.2546V5.74537Z"
107+
fill="currentColor"
108+
/>
109+
</svg>
110+
{/if}
111+
</button>
112+
113+
<div class="waveform-wrapper" bind:this={container}></div>
114+
115+
<div class="timestamp">{format_time(playing ? currentTime : duration)}</div>
116+
</div>
117+
118+
<style>
119+
.minimal-audio-player {
120+
display: flex;
121+
align-items: center;
122+
gap: var(--spacing-sm);
123+
border-radius: var(--radius-sm);
124+
width: var(--size-52);
125+
padding: var(--spacing-sm);
126+
}
127+
128+
.play-btn {
129+
display: inline-flex;
130+
align-items: center;
131+
justify-content: center;
132+
padding: 0;
133+
border: none;
134+
background: none;
135+
color: var(--body-text-color);
136+
opacity: 0.7;
137+
cursor: pointer;
138+
border-radius: 50%;
139+
transition: all 0.2s ease;
140+
flex-shrink: 0;
141+
}
142+
143+
.play-btn:hover {
144+
color: var(--color-accent);
145+
opacity: 1;
146+
}
147+
148+
.play-btn:active {
149+
transform: scale(0.95);
150+
}
151+
152+
.play-btn svg {
153+
width: var(--size-5);
154+
height: var(--size-5);
155+
display: block;
156+
}
157+
158+
.waveform-wrapper {
159+
flex: 1 1 auto;
160+
cursor: pointer;
161+
width: auto;
162+
}
163+
164+
.waveform-wrapper :global(::part(wrapper)) {
165+
margin-bottom: 0;
166+
}
167+
168+
.timestamp {
169+
font-size: 13px;
170+
font-weight: 500;
171+
color: var(--body-text-color);
172+
opacity: 0.7;
173+
font-variant-numeric: tabular-nums;
174+
flex-shrink: 0;
175+
min-width: 40px;
176+
text-align: center;
177+
}
178+
179+
@media (prefers-reduced-motion: reduce) {
180+
.play-btn {
181+
transition: none;
182+
}
183+
}
184+
</style>

js/audio/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { default as Audio } from "./Audio.svelte";
2+
export { default as MinimalAudioPlayer } from "./MinimalAudioPlayer.svelte";

js/audio/shared/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface AudioProps {
3535
streaming: boolean;
3636
stream_every: number;
3737
input_ready: boolean;
38+
minimal?: boolean;
3839
}
3940

4041
export interface AudioEvents {

js/audio/static/StaticAudio.svelte

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import { Download, Music } from "@gradio/icons";
1212
import type { I18nFormatter } from "@gradio/utils";
1313
import AudioPlayer from "../player/AudioPlayer.svelte";
14+
import MinimalAudioPlayer from "../shared/MinimalAudioPlayer.svelte";
1415
import { createEventDispatcher } from "svelte";
1516
import type { FileData } from "@gradio/client";
1617
import type { WaveformOptions, SubtitleData } from "../shared/types";
@@ -28,6 +29,7 @@
2829
export let editable = true;
2930
export let loop: boolean;
3031
export let display_icon_button_wrapper_top_corner = false;
32+
export let minimal = false;
3133
3234
const dispatch = createEventDispatcher<{
3335
change: FileData;
@@ -48,48 +50,52 @@
4850
/>
4951

5052
{#if value !== null}
51-
<IconButtonWrapper
52-
display_top_corner={display_icon_button_wrapper_top_corner}
53-
>
54-
{#if buttons === null ? true : buttons.includes("download")}
55-
<DownloadLink
56-
href={value.is_stream
57-
? value.url?.replace("playlist.m3u8", "playlist-file")
58-
: value.url}
59-
download={value.orig_name || value.path}
60-
>
61-
<IconButton Icon={Download} label={i18n("common.download")} />
62-
</DownloadLink>
63-
{/if}
64-
{#if buttons === null ? true : buttons.includes("share")}
65-
<ShareButton
66-
{i18n}
67-
on:error
68-
on:share
69-
formatter={async (value) => {
70-
if (!value) return "";
71-
let url = await uploadToHuggingFace(value.url, "url");
72-
return `<audio controls src="${url}"></audio>`;
73-
}}
74-
{value}
75-
/>
76-
{/if}
77-
</IconButtonWrapper>
53+
{#if minimal}
54+
<MinimalAudioPlayer {value} label={label || i18n("audio.audio")} {loop} />
55+
{:else}
56+
<IconButtonWrapper
57+
display_top_corner={display_icon_button_wrapper_top_corner}
58+
>
59+
{#if buttons === null ? true : buttons.includes("download")}
60+
<DownloadLink
61+
href={value.is_stream
62+
? value.url?.replace("playlist.m3u8", "playlist-file")
63+
: value.url}
64+
download={value.orig_name || value.path}
65+
>
66+
<IconButton Icon={Download} label={i18n("common.download")} />
67+
</DownloadLink>
68+
{/if}
69+
{#if buttons === null ? true : buttons.includes("share")}
70+
<ShareButton
71+
{i18n}
72+
on:error
73+
on:share
74+
formatter={async (value) => {
75+
if (!value) return "";
76+
let url = await uploadToHuggingFace(value.url, "url");
77+
return `<audio controls src="${url}"></audio>`;
78+
}}
79+
{value}
80+
/>
81+
{/if}
82+
</IconButtonWrapper>
7883

79-
<AudioPlayer
80-
{value}
81-
subtitles={Array.isArray(subtitles) ? subtitles : subtitles?.url}
82-
{label}
83-
{i18n}
84-
{waveform_settings}
85-
{waveform_options}
86-
{editable}
87-
{loop}
88-
on:pause
89-
on:play
90-
on:stop
91-
on:load
92-
/>
84+
<AudioPlayer
85+
{value}
86+
subtitles={Array.isArray(subtitles) ? subtitles : subtitles?.url}
87+
{label}
88+
{i18n}
89+
{waveform_settings}
90+
{waveform_options}
91+
{editable}
92+
{loop}
93+
on:pause
94+
on:play
95+
on:stop
96+
on:load
97+
/>
98+
{/if}
9399
{:else}
94100
<Empty size="small">
95101
<Music />

0 commit comments

Comments
 (0)