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

feat: Audio annotator. #1706

Merged
merged 46 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8d55bfb
chore: Rebase against master. #1601
mturoci Mar 29, 2023
4959657
feat: Render proper track in zoomed area. #1601
mturoci Mar 29, 2023
b06d7ee
fix: Render zoomed annotations properly. #1601
mturoci Mar 29, 2023
4a85e18
feat: Support background change during zoom + prevent time jumping. #…
mturoci Apr 3, 2023
5e2786c
fix: Get proper zoomed annotations. #1601
mturoci Apr 4, 2023
f8faf47
test: Fix tests. #1601
mturoci Apr 4, 2023
841be6f
fix: Make annotation calculation correct + unit tests. Needs optimiza…
mturoci Apr 5, 2023
91c30e2
fix: Do not allow tooltip intersection when dragging too fast. #1601
mturoci Apr 6, 2023
5288cd5
fix: Make sure drawing is fluent. #1601
mturoci Apr 6, 2023
5bfa109
feat: Improve loading UX a bit. #1601
mturoci Apr 11, 2023
c0ca68e
feat: Small color improvements. #1601
mturoci Apr 11, 2023
36e0ac1
fix: Sync slider thumb with canvas. #1601
mturoci Apr 11, 2023
43867a0
chore: Small final pre-feedback improvements. #1601
mturoci Apr 11, 2023
d781e5f
fix: Sort annotations before move/resize to avoid funky bugs. #1601
mturoci Aug 1, 2023
9471842
fix: Sync UI render with state - change tag of focused annotation #1601
mturoci Aug 1, 2023
b2de945
feat: Do not show zoom for < 120s audio files. #1601
mturoci Aug 2, 2023
7ba3788
chore: Fix typing. #1601
mturoci Aug 2, 2023
d76f96e
feat: Use regular waveform instead of bar plot. #1601
mturoci Aug 2, 2023
34a3526
feat: Make audio annotator waveform responsive. #1601
mturoci Aug 2, 2023
e02deed
chore: Minor styling updates. #1601
mturoci Aug 2, 2023
eb39fb1
fix: Zoom annotations properly. #1601
mturoci Aug 3, 2023
785c3d5
test: Mock properly. #1601
mturoci Aug 3, 2023
3d05be8
fix: Render move resize during audio playing within zoom. #1601
mturoci Aug 3, 2023
009bdf3
fix: Do not mvoe zoom in red. #1601
mturoci Aug 3, 2023
c206ba8
feat: Add err handling. #1601
mturoci Aug 4, 2023
e3f2a68
feat: Draw as many waveform samples as possible. #1601
mturoci Aug 4, 2023
d6e17af
perf: Rerender waveform only when necessary. #1601
mturoci Aug 4, 2023
a6cb85f
feat: Real zoom. #1601
mturoci Aug 7, 2023
0e987dc
chore: Remove premature optimization - generics. #1601
mturoci Aug 7, 2023
e4550eb
fix: Sync canvas and slider tracks. #1601
mturoci Aug 7, 2023
8305ce9
chore: Put 120s zoom threshold back. #1601
mturoci Aug 7, 2023
35d56c7
chore: Add audio annotator to Tour. #1601
mturoci Aug 8, 2023
7a4dceb
fix: Render waveform for non-zoom sound as well. #1601
mturoci Aug 8, 2023
a295102
chore: Put zoom threshold back. #1601
mturoci Aug 8, 2023
8a9f263
chore: Add docs example screenshot. #1601
mturoci Aug 8, 2023
bd80a0d
chore: Rename API. #1601
mturoci Aug 8, 2023
1abf829
chore: Get rid of dead code. #1601
mturoci Aug 8, 2023
d34ea6b
chore: Revert Microbars changes. #1601
mturoci Aug 8, 2023
1bef657
fix: Make the sound play on Safari as well. #1601
mturoci Aug 8, 2023
4c9d3c3
feat: Render annotations on items change. #1601
mturoci Aug 9, 2023
a9e2d3f
fix: Handle more edge cases when drawing annotations. #1601
mturoci Aug 9, 2023
26bc2f1
fix: Attempt to make Safari skip to time smoother. #1601
mturoci Aug 9, 2023
c9b3712
Revert "fix: Attempt to make Safari skip to time smoother. #1601"
mturoci Aug 10, 2023
54b28e2
fix: Do not add multiple zooms when duration changes. #1601
mturoci Aug 10, 2023
09f29ef
docs: Add audio annotator to widgets. #1601
mturoci Aug 10, 2023
71ed46b
fix: Attempt to sync Safari track. #1601
mturoci Aug 10, 2023
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
Binary file added assets/examples/sample-audio.mp3
Binary file not shown.
35 changes: 35 additions & 0 deletions py/examples/audio_annotator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Form / Audio Annotator
# Use when you need to annotate audio.
# #form #annotator #audio
# ---
from h2o_wave import main, app, Q, ui
import os


@app('/demo')
async def serve(q: Q):
# Upload the audio file to Wave server first.
if not q.app.initialized:
example_dir = os.path.dirname(os.path.realpath(__file__))
q.app.uploaded_mp3, = await q.site.upload([os.path.join(example_dir, 'audio_annotator_sample.mp3')])
q.app.initialized = True

if q.args.annotator is not None:
q.page['example'].items = [
ui.text(f'annotator={q.args.annotator}'),
ui.button(name='back', label='Back', primary=True),
]
else:
q.page['example'] = ui.form_card(box='1 1 7 -1', items=[
ui.audio_annotator(
name='annotator',
title='Drag to annotate',
path=q.app.uploaded_mp3,
tags=[
ui.audio_annotator_tag(name='f', label='Flute', color='$blue'),
ui.audio_annotator_tag(name='d', label='Drum', color='$brown'),
],
),
ui.button(name='submit', label='Submit', primary=True)
])
await q.page.save()
Binary file added py/examples/audio_annotator_sample.mp3
Binary file not shown.
1 change: 1 addition & 0 deletions py/examples/tour.conf
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ image_popup.py
image_annotator.py
image_annotator_events_click.py
image_annotator_events_tool_change.py
audio_annotator.py
inline.py
file_stream.py
frame.py
Expand Down
191 changes: 190 additions & 1 deletion py/h2o_lightwave/h2o_lightwave/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6736,7 +6736,7 @@ def __init__(
self.allowed_shapes = allowed_shapes
"""List of allowed shapes. Available values are 'rect' and 'polygon'. If not set, all shapes are available by default."""
self.events = events
"""The events to capture on this image annotator. One of `click` or `tool_change`."""
"""The events to capture on this image annotator. One of `click` | `tool_change`."""

def dump(self) -> Dict:
"""Returns the contents of this object as a dict."""
Expand Down Expand Up @@ -6804,6 +6804,185 @@ def load(__d: Dict) -> 'ImageAnnotator':
)


class AudioAnnotatorTag:
"""Create a unique tag type for use in an audio annotator.
"""
def __init__(
self,
name: str,
label: str,
color: str,
):
_guard_scalar('AudioAnnotatorTag.name', name, (str,), True, False, False)
_guard_scalar('AudioAnnotatorTag.label', label, (str,), False, False, False)
_guard_scalar('AudioAnnotatorTag.color', color, (str,), False, False, False)
self.name = name
"""An identifying name for this tag."""
self.label = label
"""Text to be displayed for the annotation."""
self.color = color
"""Hex or RGB color string to be used as the background color."""

def dump(self) -> Dict:
"""Returns the contents of this object as a dict."""
_guard_scalar('AudioAnnotatorTag.name', self.name, (str,), True, False, False)
_guard_scalar('AudioAnnotatorTag.label', self.label, (str,), False, False, False)
_guard_scalar('AudioAnnotatorTag.color', self.color, (str,), False, False, False)
return _dump(
name=self.name,
label=self.label,
color=self.color,
)

@staticmethod
def load(__d: Dict) -> 'AudioAnnotatorTag':
"""Creates an instance of this class using the contents of a dict."""
__d_name: Any = __d.get('name')
_guard_scalar('AudioAnnotatorTag.name', __d_name, (str,), True, False, False)
__d_label: Any = __d.get('label')
_guard_scalar('AudioAnnotatorTag.label', __d_label, (str,), False, False, False)
__d_color: Any = __d.get('color')
_guard_scalar('AudioAnnotatorTag.color', __d_color, (str,), False, False, False)
name: str = __d_name
label: str = __d_label
color: str = __d_color
return AudioAnnotatorTag(
name,
label,
color,
)


class AudioAnnotatorItem:
"""Create an annotator item with initial selected tags or no tags.
"""
def __init__(
self,
start: float,
end: float,
tag: str,
):
_guard_scalar('AudioAnnotatorItem.start', start, (float, int,), False, False, False)
_guard_scalar('AudioAnnotatorItem.end', end, (float, int,), False, False, False)
_guard_scalar('AudioAnnotatorItem.tag', tag, (str,), False, False, False)
self.start = start
"""The start of the audio annotation in seconds."""
self.end = end
"""The end of the audio annotation in seconds."""
self.tag = tag
"""The `name` of the audio annotator tag to refer to for the `label` and `color` of this item."""

def dump(self) -> Dict:
"""Returns the contents of this object as a dict."""
_guard_scalar('AudioAnnotatorItem.start', self.start, (float, int,), False, False, False)
_guard_scalar('AudioAnnotatorItem.end', self.end, (float, int,), False, False, False)
_guard_scalar('AudioAnnotatorItem.tag', self.tag, (str,), False, False, False)
return _dump(
start=self.start,
end=self.end,
tag=self.tag,
)

@staticmethod
def load(__d: Dict) -> 'AudioAnnotatorItem':
"""Creates an instance of this class using the contents of a dict."""
__d_start: Any = __d.get('start')
_guard_scalar('AudioAnnotatorItem.start', __d_start, (float, int,), False, False, False)
__d_end: Any = __d.get('end')
_guard_scalar('AudioAnnotatorItem.end', __d_end, (float, int,), False, False, False)
__d_tag: Any = __d.get('tag')
_guard_scalar('AudioAnnotatorItem.tag', __d_tag, (str,), False, False, False)
start: float = __d_start
end: float = __d_end
tag: str = __d_tag
return AudioAnnotatorItem(
start,
end,
tag,
)


class AudioAnnotator:
"""Create an audio annotator component.

This component allows annotating and labeling parts of audio file.
"""
def __init__(
self,
name: str,
title: str,
path: str,
tags: List[AudioAnnotatorTag],
items: Optional[List[AudioAnnotatorItem]] = None,
trigger: Optional[bool] = None,
):
_guard_scalar('AudioAnnotator.name', name, (str,), True, False, False)
_guard_scalar('AudioAnnotator.title', title, (str,), False, False, False)
_guard_scalar('AudioAnnotator.path', path, (str,), False, False, False)
_guard_vector('AudioAnnotator.tags', tags, (AudioAnnotatorTag,), False, False, False)
_guard_vector('AudioAnnotator.items', items, (AudioAnnotatorItem,), False, True, False)
_guard_scalar('AudioAnnotator.trigger', trigger, (bool,), False, True, False)
self.name = name
"""An identifying name for this component."""
self.title = title
"""The audio annotator's title."""
self.path = path
"""The path to the audio file. Use mp3 or wav formats to achieve the best cross-browser support. See https://caniuse.com/?search=audio%20format for other formats."""
self.tags = tags
"""The master list of tags that can be used for annotations."""
self.items = items
"""Annotations to display on the image, if any."""
self.trigger = trigger
"""True if the form should be submitted as soon as an annotation is made."""

def dump(self) -> Dict:
"""Returns the contents of this object as a dict."""
_guard_scalar('AudioAnnotator.name', self.name, (str,), True, False, False)
_guard_scalar('AudioAnnotator.title', self.title, (str,), False, False, False)
_guard_scalar('AudioAnnotator.path', self.path, (str,), False, False, False)
_guard_vector('AudioAnnotator.tags', self.tags, (AudioAnnotatorTag,), False, False, False)
_guard_vector('AudioAnnotator.items', self.items, (AudioAnnotatorItem,), False, True, False)
_guard_scalar('AudioAnnotator.trigger', self.trigger, (bool,), False, True, False)
return _dump(
name=self.name,
title=self.title,
path=self.path,
tags=[__e.dump() for __e in self.tags],
items=None if self.items is None else [__e.dump() for __e in self.items],
trigger=self.trigger,
)

@staticmethod
def load(__d: Dict) -> 'AudioAnnotator':
"""Creates an instance of this class using the contents of a dict."""
__d_name: Any = __d.get('name')
_guard_scalar('AudioAnnotator.name', __d_name, (str,), True, False, False)
__d_title: Any = __d.get('title')
_guard_scalar('AudioAnnotator.title', __d_title, (str,), False, False, False)
__d_path: Any = __d.get('path')
_guard_scalar('AudioAnnotator.path', __d_path, (str,), False, False, False)
__d_tags: Any = __d.get('tags')
_guard_vector('AudioAnnotator.tags', __d_tags, (dict,), False, False, False)
__d_items: Any = __d.get('items')
_guard_vector('AudioAnnotator.items', __d_items, (dict,), False, True, False)
__d_trigger: Any = __d.get('trigger')
_guard_scalar('AudioAnnotator.trigger', __d_trigger, (bool,), False, True, False)
name: str = __d_name
title: str = __d_title
path: str = __d_path
tags: List[AudioAnnotatorTag] = [AudioAnnotatorTag.load(__e) for __e in __d_tags]
items: Optional[List[AudioAnnotatorItem]] = None if __d_items is None else [AudioAnnotatorItem.load(__e) for __e in __d_items]
trigger: Optional[bool] = __d_trigger
return AudioAnnotator(
name,
title,
path,
tags,
items,
trigger,
)


class Facepile:
"""A face pile displays a list of personas. Each circle represents a person and contains their image or initials.
Often this control is used when sharing who has access to a specific view or file.
Expand Down Expand Up @@ -7233,6 +7412,7 @@ def __init__(
persona: Optional[Persona] = None,
text_annotator: Optional[TextAnnotator] = None,
image_annotator: Optional[ImageAnnotator] = None,
audio_annotator: Optional[AudioAnnotator] = None,
facepile: Optional[Facepile] = None,
copyable_text: Optional[CopyableText] = None,
menu: Optional[Menu] = None,
Expand Down Expand Up @@ -7284,6 +7464,7 @@ def __init__(
_guard_scalar('Component.persona', persona, (Persona,), False, True, False)
_guard_scalar('Component.text_annotator', text_annotator, (TextAnnotator,), False, True, False)
_guard_scalar('Component.image_annotator', image_annotator, (ImageAnnotator,), False, True, False)
_guard_scalar('Component.audio_annotator', audio_annotator, (AudioAnnotator,), False, True, False)
_guard_scalar('Component.facepile', facepile, (Facepile,), False, True, False)
_guard_scalar('Component.copyable_text', copyable_text, (CopyableText,), False, True, False)
_guard_scalar('Component.menu', menu, (Menu,), False, True, False)
Expand Down Expand Up @@ -7379,6 +7560,8 @@ def __init__(
"""Text annotator."""
self.image_annotator = image_annotator
"""Image annotator."""
self.audio_annotator = audio_annotator
"""Audio annotator."""
self.facepile = facepile
"""Facepile."""
self.copyable_text = copyable_text
Expand Down Expand Up @@ -7437,6 +7620,7 @@ def dump(self) -> Dict:
_guard_scalar('Component.persona', self.persona, (Persona,), False, True, False)
_guard_scalar('Component.text_annotator', self.text_annotator, (TextAnnotator,), False, True, False)
_guard_scalar('Component.image_annotator', self.image_annotator, (ImageAnnotator,), False, True, False)
_guard_scalar('Component.audio_annotator', self.audio_annotator, (AudioAnnotator,), False, True, False)
_guard_scalar('Component.facepile', self.facepile, (Facepile,), False, True, False)
_guard_scalar('Component.copyable_text', self.copyable_text, (CopyableText,), False, True, False)
_guard_scalar('Component.menu', self.menu, (Menu,), False, True, False)
Expand Down Expand Up @@ -7488,6 +7672,7 @@ def dump(self) -> Dict:
persona=None if self.persona is None else self.persona.dump(),
text_annotator=None if self.text_annotator is None else self.text_annotator.dump(),
image_annotator=None if self.image_annotator is None else self.image_annotator.dump(),
audio_annotator=None if self.audio_annotator is None else self.audio_annotator.dump(),
facepile=None if self.facepile is None else self.facepile.dump(),
copyable_text=None if self.copyable_text is None else self.copyable_text.dump(),
menu=None if self.menu is None else self.menu.dump(),
Expand Down Expand Up @@ -7588,6 +7773,8 @@ def load(__d: Dict) -> 'Component':
_guard_scalar('Component.text_annotator', __d_text_annotator, (dict,), False, True, False)
__d_image_annotator: Any = __d.get('image_annotator')
_guard_scalar('Component.image_annotator', __d_image_annotator, (dict,), False, True, False)
__d_audio_annotator: Any = __d.get('audio_annotator')
_guard_scalar('Component.audio_annotator', __d_audio_annotator, (dict,), False, True, False)
__d_facepile: Any = __d.get('facepile')
_guard_scalar('Component.facepile', __d_facepile, (dict,), False, True, False)
__d_copyable_text: Any = __d.get('copyable_text')
Expand Down Expand Up @@ -7643,6 +7830,7 @@ def load(__d: Dict) -> 'Component':
persona: Optional[Persona] = None if __d_persona is None else Persona.load(__d_persona)
text_annotator: Optional[TextAnnotator] = None if __d_text_annotator is None else TextAnnotator.load(__d_text_annotator)
image_annotator: Optional[ImageAnnotator] = None if __d_image_annotator is None else ImageAnnotator.load(__d_image_annotator)
audio_annotator: Optional[AudioAnnotator] = None if __d_audio_annotator is None else AudioAnnotator.load(__d_audio_annotator)
facepile: Optional[Facepile] = None if __d_facepile is None else Facepile.load(__d_facepile)
copyable_text: Optional[CopyableText] = None if __d_copyable_text is None else CopyableText.load(__d_copyable_text)
menu: Optional[Menu] = None if __d_menu is None else Menu.load(__d_menu)
Expand Down Expand Up @@ -7694,6 +7882,7 @@ def load(__d: Dict) -> 'Component':
persona,
text_annotator,
image_annotator,
audio_annotator,
facepile,
copyable_text,
menu,
Expand Down
76 changes: 75 additions & 1 deletion py/h2o_lightwave/h2o_lightwave/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2497,7 +2497,7 @@ def image_annotator(
trigger: True if the form should be submitted as soon as an annotation is drawn.
image_height: The card’s image height. The actual image size is used by default.
allowed_shapes: List of allowed shapes. Available values are 'rect' and 'polygon'. If not set, all shapes are available by default.
events: The events to capture on this image annotator. One of `click` or `tool_change`.
events: The events to capture on this image annotator. One of `click` | `tool_change`.
Returns:
A `h2o_wave.types.ImageAnnotator` instance.
"""
Expand All @@ -2514,6 +2514,80 @@ def image_annotator(
))


def audio_annotator_tag(
name: str,
label: str,
color: str,
) -> AudioAnnotatorTag:
"""Create a unique tag type for use in an audio annotator.

Args:
name: An identifying name for this tag.
label: Text to be displayed for the annotation.
color: Hex or RGB color string to be used as the background color.
Returns:
A `h2o_wave.types.AudioAnnotatorTag` instance.
"""
return AudioAnnotatorTag(
name,
label,
color,
)


def audio_annotator_item(
start: float,
end: float,
tag: str,
) -> AudioAnnotatorItem:
"""Create an annotator item with initial selected tags or no tags.

Args:
start: The start of the audio annotation in seconds.
end: The end of the audio annotation in seconds.
tag: The `name` of the audio annotator tag to refer to for the `label` and `color` of this item.
Returns:
A `h2o_wave.types.AudioAnnotatorItem` instance.
"""
return AudioAnnotatorItem(
start,
end,
tag,
)


def audio_annotator(
name: str,
title: str,
path: str,
tags: List[AudioAnnotatorTag],
items: Optional[List[AudioAnnotatorItem]] = None,
trigger: Optional[bool] = None,
) -> Component:
"""Create an audio annotator component.

This component allows annotating and labeling parts of audio file.

Args:
name: An identifying name for this component.
title: The audio annotator's title.
path: The path to the audio file. Use mp3 or wav formats to achieve the best cross-browser support. See https://caniuse.com/?search=audio%20format for other formats.
tags: The master list of tags that can be used for annotations.
items: Annotations to display on the image, if any.
trigger: True if the form should be submitted as soon as an annotation is made.
Returns:
A `h2o_wave.types.AudioAnnotator` instance.
"""
return Component(audio_annotator=AudioAnnotator(
name,
title,
path,
tags,
items,
trigger,
))


def facepile(
items: List[Component],
name: Optional[str] = None,
Expand Down
Loading