USL-Editor / app.py
lex-sobieski's picture
add ability to show all videos
8840fd7
import gradio as gr
import pandas as pd
from Functions.video_player_functions import (youtube_link_to_id, get_video_link_by_pointer, get_youtube_player_html,
change_video_completion_status, get_number_of_videos)
from Functions.caption_editor_functions import request_captions_by_video_id, save_captions_to_db
from Resources.css import css
from Resources.js import yt_init_js
from Resources.localization import get_string
next_video_pointer = 0
n_videos = get_number_of_videos()
placeholder_link = "https://www.youtube.com/watch?v=d37lwXaSjs4"
def get_username(profile: gr.OAuthProfile):
if profile is None:
return "anonymous_user"
return profile.username
def on_row_select(df, evt: gr.SelectData):
"""Handle row selection in DataFrame"""
if evt.index is not None and len(evt.index) > 0:
row_idx = evt.index[0]
if row_idx < len(df):
row = df.iloc[row_idx]
return (
gr.update(value=float(row['Start'])), # start_time
gr.update(value=str(row['Text'])), # text_input
gr.update(value=float(row['End'])), # end_time
gr.update(value=row_idx), # selected_row_idx
gr.update(value=get_string("update_entry_button")), # save_entry_button
gr.update(value=bool(row['Aligned'])) # aligned_checkbox
)
return gr.update(value=0.0), gr.update(value=""), gr.update(value=0.0), -1, get_string("save_entry_button"), gr.update(value=True)
# ADC-IMPLEMENTS: <gc-feature-aligned-ui-01>
def save_entry(df, start_time, text, end_time, selected_row_idx, video_id, aligned, current_user):
"""Save or update a caption entry with per-entry alignment tracking.
Works directly with the 4-column DataFrame (Start, Text, End, Aligned).
Sets aligned=True ONLY for the specific row being added or updated.
All other rows retain their existing Aligned values from the DataFrame.
The current video pointer is computed from global state and passed to
save_captions_to_db for auto-assignment.
"""
if current_user == "anonymous_user":
return df, gr.Warning(get_string("please_sign_in"))
if next_video_pointer == -1:
return df, gr.Warning(get_string("all_videos_transcribed"))
try:
start_time = float(start_time)
end_time = float(end_time)
if start_time >= end_time:
return df, gr.Warning(get_string("start_less_than_end"))
if not text.strip():
return df, gr.Warning(get_string("text_cannot_be_empty"))
df_copy = df.copy()
if selected_row_idx == -1: # Adding new entry
new_row = pd.DataFrame({
'Start': [start_time],
'Text': [text.strip()],
'End': [end_time],
'Aligned': [bool(aligned)]
})
df_copy = pd.concat([df_copy, new_row], ignore_index=True)
df_copy = df_copy.sort_values('Start').reset_index(drop=True)
else: # Updating existing entry
if 0 <= selected_row_idx < len(df_copy):
df_copy.at[selected_row_idx, 'Start'] = start_time
df_copy.at[selected_row_idx, 'Text'] = text.strip()
df_copy.at[selected_row_idx, 'End'] = end_time
df_copy.at[selected_row_idx, 'Aligned'] = bool(aligned)
df_copy = df_copy.sort_values('Start').reset_index(drop=True)
# Compute the current video's pointer index from global state.
# This is the same formula used by change_completion_status.
current_pointer = (next_video_pointer + n_videos - 1) % n_videos
# save_captions_to_db receives the 4-col DF directly
save_result = save_captions_to_db(df_copy, video_id, current_user, current_pointer)
return (
df_copy,
gr.Info(f"{save_result}")
)
except ValueError as e:
return df, gr.Warning(f"{get_string('invalid_time_format')} {str(e)}")
except Exception as e:
return df, gr.Error(f"{get_string('error')} {str(e)}")
def clear_form():
"""Clear the editing form"""
return (
gr.update(value=0.0), # start_time
gr.update(value=""), # text_input
gr.update(value=0.0), # end_time
gr.update(value=-1), # selected_row_idx
gr.update(value=get_string("save_entry_button")), # save_entry_button
gr.update(value=True) # aligned_checkbox
)
def validate_preview(start_time, end_time):
"""Validate times for preview playback"""
try:
start = float(start_time)
end = float(end_time)
if start == end:
return None, gr.Warning(get_string("preview_times_equal"))
if start >= end:
return None, gr.Warning(get_string("preview_invalid_times"))
return (start, end), None
except ValueError:
return None, gr.Warning(get_string("invalid_time_format"))
def update_speed_buttons(selected_speed):
"""Return updates for all speed buttons, highlighting the selected one"""
speeds = [0.25, 0.5, 0.75, 1, 2]
return [
gr.update(variant="primary" if speed == selected_speed else "secondary")
for speed in speeds
]
def change_completion_status(completion_status, current_user):
if current_user == "anonymous_user":
return gr.Warning(get_string("please_sign_in"))
global next_video_pointer
try:
change_video_completion_status(completion_status, (next_video_pointer + n_videos - 1) % n_videos)
return gr.Info(get_string("change_video_completion_status_success"))
except Exception as e:
return gr.Error(f"{get_string('error')} {str(e)}")
# ADC-IMPLEMENTS: <gc-feature-assignment-01>
def get_next_components(show_incomplete_only, current_user="anonymous_user", show_all=False):
"""Get the next video and its captions as a 4-column DataFrame.
Returns:
captions: 4-column DataFrame [Start, Text, End, Aligned] for the UI
next_video_id: YouTube video ID string
Args:
show_incomplete_only: If True, skip videos marked as complete
current_user: Per-session username (or OAuthProfile) used to filter
out videos assigned to other users.
show_all: If True, bypass the assignment filter.
"""
global next_video_pointer
if next_video_pointer == -1:
next_video_pointer = 0
next_video_link = get_video_link_by_pointer(next_video_pointer, show_incomplete_only, current_user, show_all)
next_video_pointer = (next_video_pointer + 1) % n_videos
for _ in range(n_videos + 1):
if next_video_link is not None:
break
next_video_link = get_video_link_by_pointer(next_video_pointer, show_incomplete_only, current_user, show_all)
next_video_pointer = (next_video_pointer + 1) % n_videos
if next_video_link is None:
next_video_link = placeholder_link
next_video_pointer = -1
try:
next_video_id = youtube_link_to_id(next_video_link)
captions = request_captions_by_video_id(next_video_id)
return captions, next_video_id
except (ValueError, Exception):
empty_captions = pd.DataFrame(columns=["Start", "Text", "End", "Aligned"])
return empty_captions, "error"
with gr.Blocks(css=css, head=yt_init_js, fill_width=True) as main_page:
user_state = gr.State("anonymous_user")
pointer_state = gr.State(0)
current_video_id = gr.Textbox(value="", visible=False, interactive=False)
selected_row_idx = gr.Number(value=-1, visible=False)
main_page.load(get_username, outputs=user_state)
with gr.Row(variant="panel"):
with gr.Column(scale=4):
gr.Markdown(f"## {get_string('app_title')}")
with gr.Column(scale=4):
@gr.render(inputs=user_state)
def check_login(logged_in_user):
if logged_in_user == "anonymous_user":
gr.Markdown(get_string("please_log_in"), rtl=True)
else:
gr.Markdown(f"{get_string('logged_in_as')} {logged_in_user}", rtl=True)
with gr.Column(scale=1, min_width=50):
gr.LoginButton(value=get_string("log_in_button"), logout_value=get_string("log_in_button"))
# Top row: Video player (left) + Controls & Editing panel (right)
with gr.Row():
with gr.Column(scale=2, min_width=600):
video_embed = gr.HTML(value=get_youtube_player_html())
with gr.Column(scale=1, min_width=500):
with gr.Group():
gr.Markdown(f"### {get_string('playback_controls_title')}")
with gr.Row():
seek_back_1s_btn = gr.Button(get_string("seek_back_1s"), size="sm", min_width=60)
seek_back_100ms_btn = gr.Button(get_string("seek_back_100ms"), size="sm", min_width=80)
play_pause_btn = gr.Button(get_string("play_button"), size="sm", min_width=80, variant="primary")
seek_forward_100ms_btn = gr.Button(get_string("seek_forward_100ms"), size="sm", min_width=80)
seek_forward_1s_btn = gr.Button(get_string("seek_forward_1s"), size="sm", min_width=60)
gr.Markdown(f"**{get_string('speed_label')}**")
with gr.Row():
speed_025_btn = gr.Button("0.25x", size="sm", min_width=60)
speed_05_btn = gr.Button("0.5x", size="sm", min_width=60)
speed_075_btn = gr.Button("0.75x", size="sm", min_width=60)
speed_1_btn = gr.Button("1x", size="sm", min_width=60, variant="primary")
speed_2_btn = gr.Button("2x", size="sm", min_width=60)
with gr.Group():
gr.Markdown(f"### {get_string('edit_caption_title')}")
gr.Markdown(f"**{get_string('start_time_label')}**")
with gr.Row():
start_time_input = gr.Textbox(show_label=False, value="0.000", interactive=False, scale=0, min_width=100)
insert_start_time_button = gr.Button(get_string("insert_time_button"), scale=1)
goto_start_time_button = gr.Button(get_string("goto_time_button"), scale=1)
gr.Markdown(f"**{get_string('caption_text_label')}**")
text_input = gr.Textbox(show_label=False, placeholder=get_string("caption_text_placeholder"))
gr.Markdown(f"**{get_string('end_time_label')}**")
with gr.Row():
end_time_input = gr.Textbox(show_label=False, value="0.000", interactive=False, scale=0, min_width=100)
insert_end_time_button = gr.Button(get_string("insert_time_button"), scale=1)
goto_end_time_button = gr.Button(get_string("goto_time_button"), scale=1)
aligned_checkbox = gr.Checkbox(label=get_string("aligned_label"), value=True)
with gr.Row():
save_entry_button = gr.Button(get_string("save_entry_button"), variant="primary")
preview_button = gr.Button(get_string("preview_button"), variant="secondary")
clear_button = gr.Button(get_string("cancel_button"), variant="secondary")
# Bottom row: Caption table (left) + Navigation & checkboxes (right)
with gr.Row():
with gr.Column(scale=2):
caption_editor = gr.DataFrame(
interactive=False,
elem_id="tbl",
datatype=["number", "str", "number", "bool"],
col_count=(4, "fixed"),
column_widths=["12%", "60%", "12%", "16%"],
headers=[get_string("header_start"), get_string("header_text"), get_string("header_end"), get_string("header_aligned")],
wrap=True
)
with gr.Column(scale=1, min_width=200):
next_video_button = gr.Button(get_string("next_button"), key="next_video", variant="primary")
show_incomplete_only_checkbox = gr.Checkbox(label=get_string("show_incomplete_only_checkbox"), value=True)
show_all_checkbox = gr.Checkbox(label=get_string("show_all_videos_checkbox"), value=False)
editing_complete_checkbox = gr.Checkbox(label=get_string("editing_complete_checkbox"))
info_window = gr.Markdown()
main_page.load(
fn=get_next_components,
inputs=[show_incomplete_only_checkbox, user_state, show_all_checkbox],
outputs=[caption_editor, current_video_id],
js="""() => {
const checkPlayer = setInterval(() => {
if (window.ytPlayer && window.ytPlayer.cueVideoById) {
clearInterval(checkPlayer);
// cue video dynamically once captions arrive
}
}, 100);
}"""
)
next_video_button.click(
fn=get_next_components,
inputs=[show_incomplete_only_checkbox, user_state, show_all_checkbox],
outputs=[caption_editor, current_video_id]
)
next_video_button.click(
fn=lambda: False,
outputs=editing_complete_checkbox
)
next_video_button.click(
fn=clear_form,
outputs=[start_time_input, text_input, end_time_input, selected_row_idx, save_entry_button, aligned_checkbox]
)
show_incomplete_only_checkbox.input(
fn=lambda: gr.Info(get_string("show_incomplete_only_change")),
outputs=info_window
)
show_all_checkbox.input(
fn=lambda: gr.Info(get_string("show_incomplete_only_change")),
outputs=info_window
)
current_video_id.change(
fn=None,
inputs=current_video_id,
outputs=None,
js="""(videoId) => {
if (window.ytPlayer && window.ytPlayer.cueVideoById) {
console.log('[Video Load] cueVideoById', videoId);
window.ytPlayer.cueVideoById(videoId);
}
}"""
)
caption_editor.select(
fn=on_row_select,
inputs=[caption_editor],
outputs=[start_time_input, text_input, end_time_input, selected_row_idx, save_entry_button, aligned_checkbox]
)
editing_complete_checkbox.input(
fn=change_completion_status,
inputs=[editing_complete_checkbox, user_state],
outputs=info_window
)
save_entry_button.click(
fn=save_entry,
inputs=[caption_editor, start_time_input, text_input, end_time_input,
selected_row_idx, current_video_id, aligned_checkbox, user_state],
outputs=[caption_editor, info_window]
)
insert_start_time_button.click(
fn=None, inputs=None, outputs=start_time_input,
js="() => window.ytPlayer ? +window.ytPlayer.getCurrentTime().toFixed(3) : 0"
)
insert_end_time_button.click(
fn=None, inputs=None, outputs=end_time_input,
js="() => window.ytPlayer ? +window.ytPlayer.getCurrentTime().toFixed(3) : 0"
)
goto_start_time_button.click(
fn=None, inputs=[start_time_input], outputs=None,
js="(time) => { if (window.ytPlayer) { window.ytPlayer.seekTo(parseFloat(time), true); window.ytPlayer.pauseVideo(); } }"
)
goto_end_time_button.click(
fn=None, inputs=[end_time_input], outputs=None,
js="(time) => { if (window.ytPlayer) { window.ytPlayer.seekTo(parseFloat(time), true); window.ytPlayer.pauseVideo(); } }"
)
clear_button.click(
fn=clear_form,
outputs=[start_time_input, text_input, end_time_input, selected_row_idx, save_entry_button, aligned_checkbox]
)
preview_button.click(
fn=validate_preview,
inputs=[start_time_input, end_time_input],
outputs=[gr.State(), info_window]
).success(
fn=None,
inputs=[start_time_input, end_time_input],
js="""(startTime, endTime) => {
if (window.ytPlayer) {
const start = parseFloat(startTime);
const end = parseFloat(endTime);
window.ytPlayer.seekTo(start, true);
window.ytPlayer.playVideo();
if (window.previewInterval) clearInterval(window.previewInterval);
window.previewInterval = setInterval(() => {
if (window.ytPlayer.getCurrentTime() >= end) {
window.ytPlayer.pauseVideo();
clearInterval(window.previewInterval);
}
}, 20);
}
}"""
)
# Playback control handlers
seek_back_1s_btn.click(
fn=None, inputs=None, outputs=None,
js="() => { if (window.ytPlayer) { window.ytPlayer.seekTo(Math.max(0, window.ytPlayer.getCurrentTime() - 1), true); } }"
)
seek_back_100ms_btn.click(
fn=None, inputs=None, outputs=None,
js="() => { if (window.ytPlayer) { window.ytPlayer.seekTo(Math.max(0, window.ytPlayer.getCurrentTime() - 0.1), true); } }"
)
seek_forward_100ms_btn.click(
fn=None, inputs=None, outputs=None,
js="() => { if (window.ytPlayer) { window.ytPlayer.seekTo(window.ytPlayer.getCurrentTime() + 0.1, true); } }"
)
seek_forward_1s_btn.click(
fn=None, inputs=None, outputs=None,
js="() => { if (window.ytPlayer) { window.ytPlayer.seekTo(window.ytPlayer.getCurrentTime() + 1, true); } }"
)
play_pause_btn.click(
fn=None, inputs=None, outputs=play_pause_btn,
js=f"""() => {{
if (window.ytPlayer) {{
const state = window.ytPlayer.getPlayerState();
if (state === 1) {{
window.ytPlayer.pauseVideo();
return "{get_string('play_button')}";
}} else {{
window.ytPlayer.playVideo();
return "{get_string('pause_button')}";
}}
}}
return "{get_string('play_button')}";
}}"""
)
# Speed control handlers
speed_outputs = [speed_025_btn, speed_05_btn, speed_075_btn, speed_1_btn, speed_2_btn]
speed_025_btn.click(
fn=lambda: update_speed_buttons(0.25), outputs=speed_outputs,
js="() => { if (window.ytPlayer) { window.ytPlayer.setPlaybackRate(0.25); } }"
)
speed_05_btn.click(
fn=lambda: update_speed_buttons(0.5), outputs=speed_outputs,
js="() => { if (window.ytPlayer) { window.ytPlayer.setPlaybackRate(0.5); } }"
)
speed_075_btn.click(
fn=lambda: update_speed_buttons(0.75), outputs=speed_outputs,
js="() => { if (window.ytPlayer) { window.ytPlayer.setPlaybackRate(0.75); } }"
)
speed_1_btn.click(
fn=lambda: update_speed_buttons(1), outputs=speed_outputs,
js="() => { if (window.ytPlayer) { window.ytPlayer.setPlaybackRate(1); } }"
)
speed_2_btn.click(
fn=lambda: update_speed_buttons(2), outputs=speed_outputs,
js="() => { if (window.ytPlayer) { window.ytPlayer.setPlaybackRate(2); } }"
)
main_page.launch(share=True)