Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Audio Input Widget #7363

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions panel/widgets/microphone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""The Microphone widget controls the Media Stream Recording API of the browser.
It wraps the [Media Stream Recording API](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API/Using_the_MediaStream_Recording_API).
"""

import tempfile

from os import PathLike
from pathlib import Path

import numpy as np
import param

from ..custom import JSComponent
from .button import BUTTON_TYPES


class Microphone(JSComponent):
# Alternatively AudioRecorder. Or MediaRecorder if should support both video and audio.
"""The Microphone widget can be used to record audio and create speech to "other media" applications.
recorder = Microphone(format="mp3")
pn.Column("Click the button below to start recording", recorder)
"""

value = param.Bytes(doc="""The value recorded in the format given by the format parameter""")
# Maybe I will have to transfer as data url instead of bytes.

format = param.Selector(default="webm", objects=["wav", "webm", "mp3"], doc="""The format of the value""")
# webm by default because that is the default in the Mediau Stream Recording API

# Alternatively: state = param.Selector(default="not-recoding", objects=["not-recording", "recording"])

# The below parameters are aligned with SpeechToText
state = param.Selector(default="started", objects=["paused", "started", "stopped"])

continuous = param.Boolean(default=False, doc="""If True incremental values are streamed during the recording.
If False a single value is transferred when the recording ends.""")
# Alternatively: streaming = param.Boolean(default=False)

button_hide = param.Boolean(default=False, label="Hide the Button", doc="""
If True no button is shown. If False a toggle Start/ Stop button is shown.""")

button_type = param.ObjectSelector(default="light", objects=BUTTON_TYPES+['light', 'dark'], doc="""
The button styling.""")

button_not_started = param.String(label="Button Text when not started", doc="""
The text to show on the button when the Media
service is NOT started. If '' a *muted microphone* icon is
shown.""")

button_started = param.String(label="Button Text when started", doc="""
The text to show on the button when the Media
service is started. If '' a *microphone* icon is
shown.""")

options = param.Dict(doc="""Wavesurfer Options""")
# We should figure out if we want to use Wavesurfer: https://wavesurfer.xyz/docs/

@staticmethod
def _to_data_url(self, value: bytes)->str:
raise NotImplementedError()

@param.depends("value")
def value_as_data_url(self):
return self._to_data_url(self.value)

@staticmethod
def _to_numpy(self, value: bytes)->tuple[int, np.ndarray]:
"""Returns the sample rate and numpy array"""
raise NotImplementedError()

# I want to make it easy for users to bind to the numpy array but not spend resources on
# calculating it if not needed.
@param.depends("value")
def value_as_numpy(self)->tuple[int, np.ndarray]:
"""Returns the value as a tuple of sample rate and numpy array"""
return self._to_numpy(self.value)

@param.depends("value")
def value_as_file(self)->Path:
# This is currently the best way to bind to the Audio player!
if not self.value:
return None
else:
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{self.format}") as temp_file:
temp_file.write(self.value)
temp_file_name = temp_file.name
return temp_file_name

def save(self, path: PathLike):
raise NotImplementedError()

Unchanged files with check annotations Beta

# update size
icon.size = "8em"
assert page.locator(".icon-tabler-ad-filled").bounding_box()["width"] >= 96

Check failure on line 129 in panel/tests/ui/widgets/test_icon.py

GitHub Actions / ui:test-ui:macos-latest

test_toggle_icon_update_params_dynamically TypeError: 'NoneType' object is not subscriptable

Check failure on line 129 in panel/tests/ui/widgets/test_icon.py

GitHub Actions / ui:test-ui:macos-latest

test_toggle_icon_update_params_dynamically TypeError: 'NoneType' object is not subscriptable

Check failure on line 129 in panel/tests/ui/widgets/test_icon.py

GitHub Actions / ui:test-ui:macos-latest

test_toggle_icon_update_params_dynamically TypeError: 'NoneType' object is not subscriptable
icon.size = "1em"
wait_until(lambda: page.locator(".icon-tabler-ad-filled").bounding_box()["width"] <= 24, page)

Check failure on line 131 in panel/tests/ui/widgets/test_icon.py

GitHub Actions / ui:test-ui:macos-latest

test_toggle_icon_update_params_dynamically TypeError: 'NoneType' object is not subscriptable
def test_toggle_icon_svg(page):
assert table_values == list(df2['x'].sort_values(ascending=False))
else:
return False
wait_until(x_values, page)

Check failure on line 3462 in panel/tests/ui/widgets/test_tabulator.py

GitHub Actions / ui:test-ui:macos-latest

test_tabulator_sorter_default_number TimeoutError: wait_until timed out in 5000 milliseconds
def test_tabulator_update_hidden_columns(page):