File size: 10,275 Bytes
f7d2a44
0c936fa
b35af45
00487e6
3ff48b9
 
2d7377d
f7d2a44
03dfca2
3ac04fc
4102ee9
7e5e159
f7d2a44
 
ddf47e6
03dfca2
96196b4
 
 
 
c3f878d
f7d2a44
 
0c936fa
 
 
 
 
 
 
 
 
897cf53
 
 
2d7377d
0c936fa
2d7377d
f7d2a44
0c936fa
 
 
 
897cf53
 
 
 
 
2d7377d
0c936fa
 
 
00487e6
0c936fa
11f9fd2
2d7377d
766ff27
7e5e159
0c936fa
 
 
897cf53
0c936fa
2d7377d
897cf53
0c936fa
2d7377d
897cf53
0c936fa
897cf53
0c936fa
 
 
 
 
 
 
 
 
 
 
 
 
 
897cf53
0c936fa
00487e6
3a43614
0c936fa
 
 
 
 
897cf53
0c936fa
2d7377d
0c936fa
2d7377d
0c936fa
 
 
 
 
f7d2a44
03dfca2
b35af45
 
766ff27
b35af45
 
03dfca2
 
20da126
6ae9b17
 
20da126
766ff27
20da126
 
 
 
 
 
 
03dfca2
0c936fa
 
00487e6
 
0c936fa
 
 
897cf53
03dfca2
00487e6
c3f878d
21cce09
897cf53
 
31447cd
897cf53
0c936fa
 
897cf53
ce6901f
21cce09
 
2d7377d
21cce09
897cf53
3e825f9
897cf53
2d7377d
ce6901f
2d7377d
21cce09
2d7377d
50dc821
11f9fd2
 
 
2d7377d
11f9fd2
897cf53
 
 
 
 
 
2d7377d
897cf53
 
2d7377d
b35af45
11f9fd2
 
 
2d7377d
11f9fd2
 
2d7377d
 
11f9fd2
2d7377d
11f9fd2
2d7377d
 
11f9fd2
 
2d7377d
 
11f9fd2
 
 
897cf53
 
 
 
 
 
 
 
 
 
 
 
 
11f9fd2
 
 
 
a124874
 
 
 
f4f04e7
11f9fd2
 
 
 
 
 
897cf53
11f9fd2
 
 
 
f4f04e7
11f9fd2
 
 
 
 
f4f04e7
b35af45
 
 
 
 
11f9fd2
 
 
 
f4f04e7
11f9fd2
 
 
 
 
 
f4f04e7
897cf53
 
 
11f9fd2
897cf53
 
 
11f9fd2
1a8ceff
897cf53
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
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
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
user = "anonymous_user"
n_videos = 21
placeholder_link = "https://www.youtube.com/watch?v=wTQjwG2-ePA"


def get_username(profile: gr.OAuthProfile):
    global user
    if profile is None:
        user = "anonymous_user"
    else:
        user = profile.username
    return profile


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(visible=True),  # editing_panel
                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
            )
    return gr.update(visible=False), "", "", "", -1, get_string("save_entry_button")


def show_add_entry_form():
    """Show editing panel for adding new entry"""
    return (
        gr.update(visible=True),  # editing_panel
        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 (-1 means new entry)
        gr.update(value=get_string("add_entry_button_form"))  # save_entry_button
    )


def save_entry(df, start_time, text, end_time, selected_row_idx, video_id):
    """Save or update a caption entry"""
    if user == "anonymous_user":
        return df, gr.update(visible=True), gr.Warning(get_string("please_sign_in"))
    if next_video_pointer == -1:
        return df, gr.update(visible=True), 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.update(visible=True), gr.Warning(get_string("start_less_than_end"))

        if not text.strip():
            return df, gr.update(visible=True), 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]
            })
            df_copy = pd.concat([df_copy, new_row], ignore_index=True)
            # Sort by start time
            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.iloc[selected_row_idx] = [start_time, text.strip(), end_time]
                # Sort by start time
                df_copy = df_copy.sort_values('Start').reset_index(drop=True)

        # Update in database
        save_result = save_captions_to_db(df_copy, video_id, user)

        return (
            df_copy,
            gr.update(visible=False),  # Hide panel on success
            gr.Info(f"{save_result}")
        )

    except ValueError as e:
        return df, gr.update(visible=True), gr.Warning(f"{get_string('invalid_time_format')} {str(e)}")
    except Exception as e:
        return df, gr.update(visible=True), gr.Error(f"{get_string('error')} {str(e)}")


def cancel_edit():
    """Cancel editing and hide the form"""
    return gr.update(visible=False)


def change_completion_status(completion_status):
    global next_video_pointer
    change_video_completion_status(completion_status, (next_video_pointer + n_videos - 1) % n_videos)


def get_next_components():
    global next_video_pointer
    if next_video_pointer != -1:
        next_video_link = get_video_link_by_pointer(next_video_pointer)
        next_video_pointer = (next_video_pointer + 1) % n_videos

        for i in range(n_videos + 1):
            if next_video_link is not None:
                break
            next_video_link = get_video_link_by_pointer(next_video_pointer)
            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)
        next_captions = request_captions_by_video_id(next_video_id)
        return next_captions, next_video_id
    except (ValueError, Exception) as e:
        empty_captions = pd.DataFrame(columns=["Start", "Text", "End"])
        return empty_captions, "error"


(start_captions, start_video_id) = get_next_components()

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.username}", 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"))

    with gr.Row():
        with gr.Column(scale=2, min_width=600):
            video_embed = gr.HTML(value=get_youtube_player_html())
            next_video_button = gr.Button(get_string("next_button"), key="next_video")
        with gr.Column(scale=1, min_width=200):
            caption_editor = gr.DataFrame(
                interactive=False,
                elem_id="tbl",
                datatype=["number", "str", "number"],
                col_count=(3, "fixed"),
                column_widths=["20%", "60%", "20%"],
                headers=[get_string("header_start"), get_string("header_text"), get_string("header_end")],
                wrap=True
            )
            add_entry_button = gr.Button(get_string("add_entry_button"), variant="secondary")
            editing_complete_checkbox = gr.Checkbox(label=get_string("editing_complete_checkbox"))

    with gr.Row():
        with gr.Group(visible=False) as editing_panel:
            gr.Markdown(f"### {get_string('edit_caption_title')}")
            with gr.Row(equal_height=False):
                with gr.Column():
                    start_time_input = gr.Textbox(label=get_string("start_time_label"), value="0.000", interactive=False)
                    insert_start_time_button = gr.Button(get_string("insert_current_time"))
                with gr.Column():
                    text_input = gr.Textbox(label=get_string("caption_text_label"), placeholder=get_string("caption_text_placeholder"))
                with gr.Column():
                    end_time_input = gr.Textbox(label=get_string("end_time_label"), value="0.000", interactive=False)
                    insert_end_time_button = gr.Button(get_string("insert_current_time"))

            with gr.Row(equal_height=False):
                save_entry_button = gr.Button(get_string("save_entry_button"), variant="primary")
                cancel_button = gr.Button(get_string("cancel_button"), variant="secondary")

    save_result = gr.Markdown()

    main_page.load(
        fn=get_next_components,
        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,
        outputs=[caption_editor, current_video_id]
    )
    next_video_button.click(
        fn=lambda: False,
        outputs=editing_complete_checkbox
    )

    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=[editing_panel, start_time_input, text_input, end_time_input, selected_row_idx, save_entry_button]
    )

    editing_complete_checkbox.input(
        fn=change_completion_status,
        inputs=editing_complete_checkbox
    )

    add_entry_button.click(
        fn=show_add_entry_form,
        outputs=[editing_panel, start_time_input, text_input, end_time_input, selected_row_idx, save_entry_button]
    )

    save_entry_button.click(
        fn=save_entry,
        inputs=[caption_editor, start_time_input, text_input, end_time_input,
                selected_row_idx, current_video_id],
        outputs=[caption_editor, editing_panel, save_result]
    )

    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"
    )

    cancel_button.click(fn=cancel_edit, outputs=[editing_panel])

main_page.launch(share=True)