Spaces:
Running
Running
import datetime as dt | |
import gradio as gr | |
import json | |
import numpy as np | |
import os | |
from datetime import timedelta, date, datetime | |
from gradio_vistimeline import VisTimeline, VisTimelineData | |
# --- Region: Handlers for demo tab 1 --- | |
def pull_from_timeline(timeline): | |
"""Convert timeline data to JSON string for display""" | |
if hasattr(timeline, "model_dump"): | |
data = timeline.model_dump(exclude_none=True) | |
else: | |
data = timeline | |
return json.dumps(data, indent=2) | |
def push_to_timeline(json_str): | |
"""Convert JSON string to timeline data""" | |
try: | |
return VisTimelineData.model_validate_json(json_str) | |
except Exception as e: | |
print(f"Error parsing JSON: {e}") | |
return VisTimelineData(groups=[], items=[]) | |
def on_timeline_change(): | |
return f"Most recent value change event:\n{get_now()}" | |
def on_timeline_input(event_data: gr.EventData): | |
return f"Most recent input event:\nAction: '{event_data._data}' at {get_now()}" | |
def on_timeline_select(): | |
return f"Most recent timeline selected event::\n{get_now()}" | |
def on_item_select(timeline, event_data: gr.EventData): | |
selected_ids = event_data._data # A collection of selected item IDs that can be str or int: ["example", 0, "example2"] | |
items = timeline.items | |
if selected_ids: | |
first_id = selected_ids[0] | |
for item in items: | |
if item.id == first_id: | |
content = getattr(item, 'content', 'Unknown') | |
return f"Currently selected item:\nContent: \"{content}\"\nID: \"{first_id}\"" | |
return "Currently selected item:\nNone" | |
# --- Region: Handlers for demo tab 1 --- | |
def update_table(timeline): | |
if hasattr(timeline, "model_dump"): | |
data = timeline.model_dump(exclude_none=True) | |
else: | |
data = timeline | |
items = data["items"] | |
track_length_ms = get_grouped_item_end_in_ms(items, "track-length") | |
rows = [] | |
for item in items: | |
if item["content"] != "": | |
duration = calculate_and_format_duration(item["start"], item.get("end"), track_length_ms) | |
rows.append([ | |
item["content"], | |
format_date_to_milliseconds(item["start"]), | |
duration | |
]) | |
return gr.DataFrame( | |
value=rows, | |
headers=["Item Name", "Start Time", "Duration"] | |
) | |
# --- Region: Handlers for demo tab 2 --- | |
def update_audio(timeline): | |
""" | |
Handler function for generating audio from timeline data. | |
Returns audio data in format expected by Gradio's Audio component. | |
""" | |
audio_data, sample_rate = generate_audio_from_timeline(timeline) | |
# Convert to correct shape and data type for Gradio Audio | |
# Gradio expects a 2D array with shape (samples, channels) | |
audio_data = audio_data.reshape(-1, 1) # Make it 2D with 1 channel | |
return (sample_rate, audio_data) | |
def generate_audio_from_timeline(timeline_data, sample_rate=44100): | |
""" | |
Generate audio from timeline items containing frequency information. | |
Args: | |
timeline_data: Timeline data containing items with start/end times in milliseconds | |
sample_rate: Audio sample rate in Hz (default 44100) | |
Returns: | |
Tuple of (audio_data: np.ndarray, sample_rate: int) | |
""" | |
# Get all items from the timeline | |
if hasattr(timeline_data, "model_dump"): | |
data = timeline_data.model_dump(exclude_none=True) | |
else: | |
data = timeline_data | |
items = data["items"] | |
# Find the track length from the background item | |
track_length_ms = get_grouped_item_end_in_ms(items, "track-length") | |
# Convert milliseconds to samples | |
total_samples = int((track_length_ms / 1000) * sample_rate) | |
# Initialize empty audio buffer | |
audio_buffer = np.zeros(total_samples) | |
# Frequency mapping | |
freq_map = { | |
1: 440.0, | |
2: 554.37, | |
3: 659.26 | |
} | |
# Generate sine waves for each item | |
for item in items: | |
id = item.get("id", 0) | |
start_time = parse_date_to_milliseconds(item["start"]) | |
end_time = parse_date_to_milliseconds(item["end"]) | |
# Skip items that are completely outside the valid range | |
if end_time <= 0 or start_time >= track_length_ms or start_time >= end_time: | |
continue | |
# Clamp times to valid range | |
start_time = max(0, min(start_time, track_length_ms)) | |
end_time = max(0, min(end_time, track_length_ms)) | |
if id in freq_map: | |
freq = freq_map[id] | |
# Convert millisecond timestamps to sample indices | |
start_sample = int((start_time / 1000) * sample_rate) | |
end_sample = int((end_time / 1000) * sample_rate) | |
# Generate time array for this segment | |
t = np.arange(start_sample, end_sample) | |
# Generate sine wave | |
duration = end_sample - start_sample | |
envelope = np.ones(duration) | |
fade_samples = min(int(0.10 * sample_rate), duration // 2) # 100ms fade or half duration | |
envelope[:fade_samples] = np.linspace(0, 1, fade_samples) | |
envelope[-fade_samples:] = np.linspace(1, 0, fade_samples) | |
wave = 0.2 * envelope * np.sin(2 * np.pi * freq * t / sample_rate) | |
# Add to buffer | |
audio_buffer[start_sample:end_sample] += wave | |
# Normalize to prevent clipping | |
max_val = np.max(np.abs(audio_buffer)) | |
if max_val > 0: | |
audio_buffer = audio_buffer / max_val | |
return (audio_buffer, sample_rate) | |
# Helper function to get hard-coded track-length from timeline value | |
def get_grouped_item_end_in_ms(items, group_id): | |
default_length = 6000 | |
for item in items: | |
if item.get("group") == group_id: | |
return parse_date_to_milliseconds(item.get("end", default_length)) | |
return default_length | |
# --- Region: Demo specific datetime helper functions --- | |
def calculate_and_format_duration(start_date, end_date, max_range): | |
"""Calculate the seconds between two datetime inputs and format the result with up to 2 decimals.""" | |
if not end_date: | |
return "0s" | |
# Convert dates to milliseconds | |
start_ms = max(0, parse_date_to_milliseconds(start_date)) | |
end_ms = min(max_range, parse_date_to_milliseconds(end_date)) | |
if end_ms < start_ms: | |
return "0s" | |
# Calculate duration in seconds | |
duration = (end_ms - start_ms) / 1000 | |
# Format to remove trailing zeroes after rounding to 2 decimal places | |
formatted_duration = f"{duration:.2f}".rstrip("0").rstrip(".") | |
return f"{formatted_duration}s" | |
def format_date_to_milliseconds(date): | |
"""Format input (ISO8601 string or milliseconds) to mm:ss.SSS.""" | |
date_in_milliseconds = max(0, parse_date_to_milliseconds(date)) | |
time = timedelta(milliseconds=date_in_milliseconds) | |
# Format timedelta into mm:ss.SSS | |
minutes, seconds = divmod(time.seconds, 60) | |
milliseconds_part = time.microseconds // 1000 | |
return f"{minutes:02}:{seconds:02}.{milliseconds_part:03}" | |
def parse_date_to_milliseconds(date): | |
"""Convert input (ISO8601 string or milliseconds) milliseconds""" | |
if isinstance(date, int): # Input is already in milliseconds (Unix timestamp) | |
return date | |
elif isinstance(date, str): # Input is ISO8601 datetime string | |
dt = datetime.fromisoformat(date.replace("Z", "+00:00")) | |
epoch = datetime(1970, 1, 1, tzinfo=dt.tzinfo) # Calculate difference from Unix epoch | |
return int((dt - epoch).total_seconds() * 1000) | |
else: | |
return 0 # Fallback for unsupported types | |
def get_now(): | |
"""Returns current time in HH:MM:SS format""" | |
return datetime.now().strftime("%H:%M:%S") | |
TIMELINE_ID = "dateless_timeline" | |
AUDIO_ID = "timeline-audio" | |
# Example for how to access the timeline through JavaScript | |
# In this case, to bind the custom time bar of the timeline to be in sync with the audio component | |
# Read the JavaScript file | |
js_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'custom_time_control.js') | |
with open(js_path, 'r') as f: | |
js_content = f.read() | |
script = f"""<script>{js_content}</script>""" | |
style = f"""<style>.vis-custom-time.{TIMELINE_ID} {{pointer-events: none !important;}}</style>""" | |
head = script + style | |
# --- Region: Gradio --- | |
with gr.Blocks(head=head) as demo: | |
today = date.today() | |
day_offset = lambda days: (today + dt.timedelta(days=days)).isoformat() | |
gr.Markdown("# Vis.js Timeline Component Demo") | |
with gr.Tabs(): | |
# --- Tab 1: Basic Timeline with Events --- | |
with gr.Tab("Basic Timeline Demo"): | |
# Timeline component | |
basic_timeline = VisTimeline( | |
value={ | |
"groups": [{"id": 0, "content": ""}], | |
"items": [] | |
}, | |
options= { | |
"start": day_offset(-1), | |
"end": day_offset(20), | |
"editable": True, | |
"format": { # You don't need to define this as these are the default values, but for this demo it is necessary because of two timelines with different formats on one page | |
"minorLabels": { | |
"millisecond": "SSS", | |
"second": "ss", | |
"minute": "HH:mm", | |
"hour": "HH:mm", | |
} | |
} | |
}, | |
label="Interactive Timeline", | |
interactive=True | |
) | |
gr.Markdown("### Events") | |
# Event listener outputs | |
with gr.Row(): | |
change_textbox = gr.Textbox(value="Most recent value change event:", label="Change:", lines=3, interactive=False) | |
input_textbox = gr.Textbox(value="Most recent user input event:", label="Input:", lines=3, interactive=False) | |
select_textbox = gr.Textbox(value="Most recent timeline selected event:", label="Select:", lines=3, interactive=False) | |
item_select_textbox = gr.Textbox(value="Currently selected item:\nNone", label="Currently selected item:", lines=3, interactive=False) | |
# Examples and JSON area in two columns | |
with gr.Row(): | |
# Left column: Examples | |
with gr.Column(): | |
gr.Markdown("### Timeline Examples") | |
gr.Examples( | |
examples=[ | |
{ | |
"groups": [{"id": 0, "content": ""}], | |
"items": [ | |
{"content": "Working", "group": 0, "start": day_offset(1), "end": day_offset(5)}, | |
{"content": "Resting", "group": 0, "start": day_offset(5), "end": day_offset(7)}, | |
{"content": "Working", "group": 0, "start": day_offset(7), "end": day_offset(11)}, | |
{"content": "Resting", "group": 0, "start": day_offset(11), "end": day_offset(13)}, | |
{"content": "Working", "group": 0, "start": day_offset(13), "end": day_offset(17)}, | |
{"content": "Resting", "group": 0, "start": day_offset(17), "end": day_offset(19)}, | |
{"content": "Working", "group": 0, "start": day_offset(19), "end": day_offset(23)}, | |
], | |
"description": "DateTime ranges" | |
}, | |
{ | |
"groups": [{"id": 0, "content": "Group"}], | |
"items": [ | |
{"id": 0, "content": "Simple item", "group": 0, "start": day_offset(9)} | |
] | |
}, | |
{ | |
"groups": [{"id": 0, "content": "Worker 1"}, {"id": 1, "content": "Worker 2"}], | |
"items": [ | |
{"content": "Working", "group": 0, "start": day_offset(1), "end": day_offset(5)}, | |
{"content": "Resting", "group": 0, "start": day_offset(5), "end": day_offset(7)}, | |
{"content": "Working", "group": 0, "start": day_offset(7), "end": day_offset(11)}, | |
{"content": "Resting", "group": 0, "start": day_offset(11), "end": day_offset(13)}, | |
{"content": "Working", "group": 0, "start": day_offset(13), "end": day_offset(17)}, | |
{"content": "Resting", "group": 0, "start": day_offset(17), "end": day_offset(19)}, | |
{"content": "Working", "group": 0, "start": day_offset(19), "end": day_offset(23)}, | |
{"content": "Working", "group": 1, "start": day_offset(-3), "end": day_offset(2)}, | |
{"content": "Resting", "group": 1, "start": day_offset(2), "end": day_offset(4)}, | |
{"content": "Working", "group": 1, "start": day_offset(4), "end": day_offset(8)}, | |
{"content": "Resting", "group": 1, "start": day_offset(8), "end": day_offset(10)}, | |
{"content": "Working", "group": 1, "start": day_offset(10), "end": day_offset(14)}, | |
{"content": "Resting", "group": 1, "start": day_offset(14), "end": day_offset(16)}, | |
{"content": "Working", "group": 1, "start": day_offset(16), "end": day_offset(20)} | |
], | |
"description": "DateTime ranges in groups" | |
}, | |
{ | |
"groups": [{"id": 1, "content": "Group 1"}, {"id": 2, "content": "Group 2"}], | |
"items": [ | |
{"id": "A", "content": "Period A", "start": day_offset(1), "end": day_offset(7), "type": "background", "group": 1 }, | |
{"id": "B", "content": "Period B", "start": day_offset(8), "end": day_offset(11), "type": "background", "group": 2 }, | |
{"id": "C", "content": "Period C", "start": day_offset(12), "end": day_offset(17), "type": "background" }, | |
{"content": "Range inside period A", "start": day_offset(2), "end": day_offset(6), "group": 1 }, | |
{"content": "Item inside period C", "group": 2, "start": day_offset(14) } | |
], | |
"description": "Background type example" | |
}, | |
{ | |
"groups": [{"id": 1, "content": "Group 1"}, {"id": 2, "content": "Group 2"}], | |
"items": [ | |
{"content": "Range item", "group": 1, "start": day_offset(7), "end": day_offset(14) }, | |
{"content": "Point item", "group": 2, "start": day_offset(7), "type": "point" }, | |
{"content": "Point item with a longer name", "group": 2, "start": day_offset(7), "type": "point" }, | |
], | |
"description": "Point type example" | |
}, | |
{ | |
"groups": [{"id": 1, "content": "Group 1", "subgroupStack": {"A": True, "B": True}}, {"id": 2, "content": "Group 2" }], | |
"items": [ | |
{"content": "Subgroup 2 Background", "start": day_offset(0), "end": day_offset(4), "type": "background", "group": 1, "subgroup": "A", "subgroupOrder": 0}, | |
{"content": "Subgroup 2 Item", "start": day_offset(5), "end": day_offset(7), "group": 1, "subgroup": "A", "subgroupOrder": 0}, | |
{"content": "Subgroup 1 Background", "start": day_offset(0), "end": day_offset(4), "type": "background", "group": 1, "subgroup": "B", "subgroupOrder": 1}, | |
{"content": "Subgroup 1 Item", "start": day_offset(8), "end": day_offset(10), "group": 1, "subgroup": "B", "subgroupOrder": 1}, | |
{"content": "Full group background", "start": day_offset(5), "end": day_offset(9), "type": "background", "group": 2}, | |
{"content": "No subgroup item 1", "start": day_offset(10), "end": day_offset(12), "group": 2}, | |
{"content": "No subgroup item 2", "start": day_offset(13), "end": day_offset(15), "group": 2} | |
], | |
"description": "Subgroups with backgrounds and items" | |
}, | |
{ | |
"groups": [{"id": 1, "content": "Group 1", "subgroupStack": {"A": True, "B": True}}, {"id": 2, "content": "Group 2" }], | |
"items": [ | |
{"content": "Subgroup 2 background", "start": day_offset(0), "end": day_offset(4), "type": "background", "group": 1, "subgroup": "A", "subgroupOrder": 0}, | |
{"content": "Subgroup 2 range", "start": day_offset(5), "end": day_offset(7), "group": 1, "subgroup": "A", "subgroupOrder": 0}, | |
{"content": "Subgroup 2 item", "start": day_offset(10), "group": 1, "subgroup": "A" }, | |
{"content": "Subgroup 1 background", "start": day_offset(0), "end": day_offset(4), "type": "background", "group": 1, "subgroup": "B", "subgroupOrder": 1}, | |
{"content": "Subgroup 1 range", "start": day_offset(8), "end": day_offset(10), "group": 1, "subgroup": "B", "subgroupOrder": 1}, | |
{"content": "Subgroup 1 item", "start": day_offset(14), "group": 1, "subgroup": "B" }, | |
{"content": "No subgroup item", "start": day_offset(12), "group": 1}, | |
{"content": "Full group background", "start": day_offset(5), "end": day_offset(9), "type": "background", "group": 2}, | |
{"content": "No subgroup range 1", "start": day_offset(11), "end": day_offset(13), "group": 2}, | |
{"content": "No subgroup range 2", "start": day_offset(15), "end": day_offset(17), "group": 2}, | |
{"content": "No subgroup point", "start": day_offset(1), "group": 2, "type": "point" } | |
], | |
"description": "Combination of item and group types" | |
} | |
], | |
inputs=basic_timeline | |
) | |
# Right column: JSON staging area | |
with gr.Column(): | |
gr.Markdown("### Serialized Timeline Value") | |
json_textbox = gr.Textbox(label="JSON", lines=4) | |
with gr.Row(): | |
pull_button = gr.Button("Pull Timeline into JSON") | |
push_button = gr.Button("Push JSON onto Timeline", variant="primary") | |
# Event handlers | |
basic_timeline.change(fn=on_timeline_change, outputs=[change_textbox]) # Triggered when the value of the timeline changes by any means | |
basic_timeline.input(fn=on_timeline_input, outputs=[input_textbox]) # Triggered when the value of the timeline changes, caused directly by a user input on the component (dragging, adding & removing items) | |
basic_timeline.select(fn=on_timeline_select, outputs=[select_textbox]) # Triggered when the timeline is clicked | |
basic_timeline.item_select(fn=on_item_select, inputs=[basic_timeline], outputs=[item_select_textbox]) # Triggered when items are selected or unselected | |
pull_button.click(fn=pull_from_timeline, inputs=[basic_timeline], outputs=[json_textbox]) # Example of using the timeline as an input | |
push_button.click(fn=push_to_timeline, inputs=[json_textbox], outputs=[basic_timeline]) # Example of using the timeline as an output | |
# --- Tab 2: Timeline without date --- | |
with gr.Tab("Timeline Without Date"): | |
audio_output = gr.Audio(label="Generated Audio", type="numpy", elem_id=AUDIO_ID) | |
dateless_timeline = VisTimeline( | |
value={ | |
"groups": [{"id": "track-length", "content": ""}, {"id": 1, "content": ""}, {"id": 2, "content": ""}, {"id": 3, "content": ""}], | |
"items": [ | |
{"content": "", "group": "track-length", "selectable": False, "type": "background", "start": 0, "end": 6000, "className": "color-primary-600"}, | |
{"id": 1, "content": "440.00Hz", "group": 1, "selectable": False, "start": 0, "end": 1500}, | |
{"id": 2, "content": "554.37Hz", "group": 2, "selectable": False, "start": 2000, "end": 3500}, | |
{"id": 3, "content": "659.26Hz", "group": 3, "selectable": False, "start": 4000, "end": 5500} | |
]}, | |
options={ | |
"moment": "+00:00", # Force the timeline into a certain UTC offset timezone | |
"showCurrentTime": False, | |
"editable": { | |
"add": False, | |
"remove": False, | |
"updateGroup": False, | |
"updateTime": True | |
}, | |
"itemsAlwaysDraggable": { # So dragging does not require selection first | |
"item": True, | |
"range": True | |
}, | |
"showMajorLabels": False, # This hides the month & year labels | |
"format": { | |
"minorLabels": { # Force the minor labels into a format that does not include weekdays or months | |
"millisecond": "mm:ss.SSS", | |
"second": "mm:ss", | |
"minute": "mm:ss", | |
"hour": "HH:mm:ss" | |
} | |
}, | |
"start": 0, # Timeline will start at unix epoch | |
"end": 6000, # Initial timeline range will end at 1 minute (unix timestamp in milliseconds) | |
"min": 0, # Restrict timeline navigation, timeline can not be scrolled further to the left than 0 seconds | |
"max": 7000, # Restrict timeline navigation, timeline can not be scrolled further to the right than 70 seconds | |
"zoomMin": 1000, # Allow zoom in up until the entire timeline spans 1000 milliseconds | |
}, | |
label="Timeline without date labels, with restrictions on navigation and zoom. You can drag and resize items without having to select them first.", | |
elem_id=TIMELINE_ID # This will also make the timeline instance accessible in JavaScript via 'window.visTimelineInstances["your elem_id"]' | |
) | |
table = gr.DataFrame( | |
headers=["Item Name", "Start Time", "Duration"], | |
label="Timeline Items", | |
interactive=False | |
) | |
generate_audio_button = gr.Button("Generate Audio") | |
# Event handlers | |
dateless_timeline.change(fn=update_table, inputs=[dateless_timeline], outputs=[table]) | |
dateless_timeline.load(fn=update_table, inputs=[dateless_timeline], outputs=[table]) | |
generate_audio_button.click(fn=update_audio, inputs=[dateless_timeline], outputs=[audio_output]) | |
generate_audio_button.click( | |
fn=update_audio, | |
inputs=[dateless_timeline], | |
outputs=[audio_output], | |
).then( | |
fn=None, | |
inputs=None, | |
outputs=None, | |
js=f'() => initAudioSync("{TIMELINE_ID}", "{AUDIO_ID}", 6000)' | |
) | |
# --- Tab 3: Links to documentation and examples --- | |
with gr.Tab("Documentation & More Examples"): | |
gr.Markdown(""" | |
## Vis.js Timeline Examples | |
A collection of HTML/CSS/JavaScript snippets displaying various properties and use-cases: | |
[https://visjs.github.io/vis-timeline/examples/timeline/](https://visjs.github.io/vis-timeline/examples/timeline/) | |
<br><br> | |
## Vis.js Timeline Documentation | |
The official documentation of the timeline: | |
[https://visjs.github.io/vis-timeline/docs/timeline/](https://visjs.github.io/vis-timeline/docs/timeline/) | |
<br><br> | |
## Vis.js DataSet Documentation | |
The official documentation of the DataSet model: | |
[https://visjs.github.io/vis-data/data/dataset.html](https://visjs.github.io/vis-data/data/dataset.html) | |
""") | |
if __name__ == "__main__": | |
demo.launch(show_api=False) | |