Spaces:
Sleeping
Sleeping
| --[[ | |
| TranscriptEditor.lua - Transcript editor UI | |
| ]]-- | |
| TranscriptEditor = Polo { | |
| TITLE = 'Edit Segment', | |
| WIDTH = 500, | |
| HEIGHT = 500, | |
| MIN_CONTENT_WIDTH = 375, | |
| BUTTON_WIDTH = 120, | |
| WORDS_PER_LINE = 5, | |
| ZOOM_LEVEL = { | |
| NONE = { value = "none", description = "None" }, | |
| WORD = { value = "word", description = "Word" }, | |
| SEGMENT = { value = "segment", description = "Segment" }, | |
| }, | |
| } | |
| function TranscriptEditor:init() | |
| assert(self.transcript, 'missing transcript') | |
| self.editing = nil | |
| self.is_open = false | |
| self.sync_time_selection = false | |
| self.zoom_level = self.ZOOM_LEVEL.NONE.value | |
| self.key_bindings = self:make_key_bindings() | |
| end | |
| function TranscriptEditor:make_key_bindings() | |
| return KeyMap.new({ | |
| [ImGui.Key_LeftArrow()] = function () | |
| self:edit_word(self.editing.word_index - 1) | |
| end, | |
| [ImGui.Key_RightArrow()] = function () | |
| self:edit_word(self.editing.word_index + 1) | |
| end, | |
| }) | |
| end | |
| function TranscriptEditor:edit_segment(segment, index) | |
| self.editing = { | |
| segment = segment, | |
| words = {}, | |
| index = index, | |
| text = segment:get('text'), | |
| } | |
| for i, word in pairs(segment.words) do | |
| self.editing.words[i] = word:copy() | |
| end | |
| self:edit_word(1) | |
| end | |
| function TranscriptEditor:edit_word(index) | |
| if index < 1 or index > #self.editing.words then | |
| return | |
| end | |
| local word = self.editing.words[index] | |
| self.editing.word = word | |
| self.editing.word_index = index | |
| if self.sync_time_selection then | |
| self:update_time_selection() | |
| self:zoom(self.zoom_level) | |
| end | |
| end | |
| function TranscriptEditor:render() | |
| if not self.editing then | |
| return | |
| end | |
| local opening = not self.is_open | |
| if opening then | |
| self:_open() | |
| end | |
| local center = {ImGui.Viewport_GetCenter(ImGui.GetWindowViewport(ctx))} | |
| ImGui.SetNextWindowPos(ctx, center[1], center[2], ImGui.Cond_Appearing(), 0.5, 0.5) | |
| ImGui.SetNextWindowSize(ctx, self.WIDTH, self.HEIGHT, ImGui.Cond_FirstUseEver()) | |
| if ImGui.BeginPopupModal(ctx, self.TITLE, true, ImGui.WindowFlags_AlwaysAutoResize()) then | |
| app:trap(function () | |
| self.key_bindings:react() | |
| self:render_content() | |
| end) | |
| ImGui.EndPopup(ctx) | |
| else | |
| self:_close() | |
| end | |
| end | |
| function TranscriptEditor:render_content() | |
| if self.editing.word then | |
| self:render_word_navigation() | |
| self:render_separator() | |
| end | |
| local edit_requested = self:render_words() | |
| if self.editing.word then | |
| self:render_separator() | |
| self:render_word_actions() | |
| self:render_word_inputs() | |
| end | |
| if edit_requested then | |
| self:edit_word(edit_requested) | |
| end | |
| self:render_separator() | |
| if ImGui.Button(ctx, 'Save', self.BUTTON_WIDTH, 0) then | |
| self:handle_save() | |
| self:_close() | |
| end | |
| ImGui.SameLine(ctx) | |
| if ImGui.Button(ctx, 'Cancel', self.BUTTON_WIDTH, 0) then | |
| self:_close() | |
| end | |
| end | |
| function TranscriptEditor:render_word_navigation() | |
| local words = self.editing.words | |
| local word_index = self.editing.word_index | |
| local num_words = #words | |
| local spacing = ImGui.GetStyleVar(ctx, ImGui.StyleVar_ItemInnerSpacing()) | |
| local disable_if = ReaUtil.disabler(ctx, app.onerror) | |
| ImGui.PushButtonRepeat(ctx, true) | |
| app:trap(function () | |
| if ImGui.ArrowButton(ctx, '##left', ImGui.Dir_Left()) then | |
| self:edit_word(self.editing.word_index - 1) | |
| end | |
| ImGui.SameLine(ctx, 0, spacing) | |
| if ImGui.ArrowButton(ctx, '##right', ImGui.Dir_Right()) then | |
| self:edit_word(self.editing.word_index + 1) | |
| end | |
| end) | |
| ImGui.PopButtonRepeat(ctx) | |
| ImGui.SameLine(ctx) | |
| ImGui.AlignTextToFramePadding(ctx) | |
| ImGui.Text(ctx, 'Word ' .. word_index .. ' / ' .. num_words) | |
| ImGui.SameLine(ctx) | |
| if ImGui.Button(ctx, 'Add') then | |
| self:handle_word_add() | |
| end | |
| app:tooltip('Add word after current word') | |
| ImGui.SameLine(ctx, 0, spacing) | |
| disable_if(num_words <= 1, function() | |
| if ImGui.Button(ctx, 'Delete') then | |
| self:handle_word_delete() | |
| end | |
| end) | |
| app:tooltip('Delete current word') | |
| ImGui.SameLine(ctx, 0, spacing) | |
| if ImGui.Button(ctx, 'Split') then | |
| self:handle_word_split() | |
| end | |
| app:tooltip('Split current word into two words') | |
| ImGui.SameLine(ctx, 0, spacing) | |
| disable_if(word_index >= num_words, function() | |
| if ImGui.Button(ctx, 'Merge') then | |
| self:handle_word_merge() | |
| end | |
| end) | |
| app:tooltip('Merge current word with next word') | |
| end | |
| function TranscriptEditor:render_words() | |
| local words = self.editing.words | |
| local num_words = #words | |
| local spacing = ImGui.GetStyleVar(ctx, ImGui.StyleVar_ItemInnerSpacing()) | |
| local edit_requested = nil | |
| for i, word in pairs(words) do | |
| if self.editing.word_index ~= i then | |
| ImGui.PushStyleColor(ctx, ImGui.Col_Button(), 0xffffff33) | |
| end | |
| app:trap(function() | |
| if ImGui.Button(ctx, word.word .. '##' .. i) then | |
| edit_requested = i | |
| end | |
| end) | |
| if self.editing.word_index ~= i then | |
| ImGui.PopStyleColor(ctx) | |
| end | |
| if i < num_words and i % self.WORDS_PER_LINE ~= 0 then | |
| ImGui.SameLine(ctx, 0, spacing) | |
| end | |
| end | |
| return edit_requested | |
| end | |
| function TranscriptEditor:render_word_inputs() | |
| self:render_word_input() | |
| if self.sync_time_selection then | |
| local sel_start, sel_end = reaper.GetSet_LoopTimeRange(false, false, 0, 0, false) | |
| local offset = Transcript.calculate_offset(self.editing.segment.item, self.editing.segment.take) | |
| self.editing.word.start = sel_start - offset | |
| self.editing.word.end_ = sel_end - offset | |
| end | |
| self:render_time_input('start', self.editing.word.start, function (time) | |
| self.editing.word.start = time | |
| end) | |
| self:render_time_input('end', self.editing.word.end_, function (time) | |
| self.editing.word.end_ = time | |
| end) | |
| self:render_score_input() | |
| end | |
| function TranscriptEditor:render_word_input() | |
| local rv, value = ImGui.InputText(ctx, 'word', self.editing.word.word) | |
| if rv then | |
| value = value:gsub('^%s*(.-)%s*$', '%1') | |
| if #value > 0 then | |
| self.editing.word.word = value | |
| end | |
| end | |
| end | |
| function TranscriptEditor:render_time_input(label, value, callback) | |
| local value_str = reaper.format_timestr(value, '') | |
| local rv, new_value = ImGui.InputText(ctx, label, value_str) | |
| if rv then | |
| callback(reaper.parse_timestr(new_value)) | |
| end | |
| end | |
| function TranscriptEditor:render_score_input() | |
| local color = TranscriptUI.score_color(self.editing.word:score()) | |
| if color then | |
| ImGui.PushStyleColor(ctx, ImGui.Col_SliderGrab(), color) | |
| ImGui.PushStyleColor(ctx, ImGui.Col_SliderGrabActive(), color) | |
| end | |
| app:trap(function () | |
| local rv, value = ImGui.SliderDouble(ctx, 'score', self.editing.word.probability, 0, 1) | |
| if rv then | |
| self.editing.word.probability = value | |
| end | |
| end) | |
| if color then | |
| ImGui.PopStyleColor(ctx, 2) | |
| end | |
| end | |
| function TranscriptEditor:render_icon_button(icon, callback) | |
| ImGui.PushFont(ctx, Fonts.icons) | |
| app:trap(function () | |
| if ImGui.Button(ctx, Fonts.ICON[icon]) then | |
| callback() | |
| end | |
| end) | |
| ImGui.PopFont(ctx) | |
| end | |
| function TranscriptEditor:render_word_actions() | |
| self:render_icon_button('play', function () | |
| self:update_time_selection() | |
| reaper.Main_OnCommand(1016, 0) -- Transport: Stop | |
| reaper.Main_OnCommand(40630, 0) -- Go to start of time selection | |
| reaper.Main_OnCommand(40044, 0) -- Transport: Play/stop | |
| end) | |
| app:tooltip('Play word') | |
| local spacing = ImGui.GetStyleVar(ctx, ImGui.StyleVar_ItemInnerSpacing()) | |
| ImGui.SameLine(ctx, 0, spacing) | |
| self:render_icon_button('stop', function () | |
| reaper.Main_OnCommand(1016, 0) -- Transport: Stop | |
| end) | |
| app:tooltip('Stop') | |
| ImGui.SameLine(ctx) | |
| local rv, value = ImGui.Checkbox(ctx, 'sync time selection', self.sync_time_selection) | |
| if rv then | |
| self.sync_time_selection = value | |
| if value then | |
| self:update_time_selection() | |
| end | |
| end | |
| ImGui.SameLine(ctx) | |
| ImGui.Text(ctx, 'and') | |
| ImGui.SameLine(ctx) | |
| self:render_zoom_combo() | |
| end | |
| function TranscriptEditor:render_zoom_combo() | |
| local disable_if = ReaUtil.disabler(ctx, app.onerror) | |
| ImGui.SameLine(ctx) | |
| ImGui.Text(ctx, "zoom to") | |
| ImGui.SameLine(ctx) | |
| ImGui.PushItemWidth(ctx, self.BUTTON_WIDTH) | |
| app:trap(function() | |
| disable_if(not self.sync_time_selection, function() | |
| if ImGui.BeginCombo(ctx, "##zoom_level", self.zoom_level) then | |
| app:trap(function() | |
| for _, zoom in pairs(self.ZOOM_LEVEL) do | |
| if ImGui.Selectable(ctx, zoom.description, self.zoom_level == zoom.value) then | |
| self.zoom_level = zoom.value | |
| self:handle_zoom_change() | |
| end | |
| end | |
| end) | |
| ImGui.EndCombo(ctx) | |
| end | |
| end) | |
| end) | |
| ImGui.PopItemWidth(ctx) | |
| end | |
| function TranscriptEditor:offset() | |
| return Transcript.calculate_offset(self.editing.segment.item, self.editing.segment.take) | |
| end | |
| function TranscriptEditor:zoom(zoom_level) | |
| -- save current selection | |
| local start, end_ = reaper.GetSet_LoopTimeRange(false, true, 0, 0, false) | |
| if zoom_level == self.ZOOM_LEVEL.WORD.value then | |
| self.editing.word:select_in_timeline(self:offset()) | |
| elseif zoom_level == self.ZOOM_LEVEL.SEGMENT.value then | |
| self.editing.segment:select_in_timeline(self:offset()) | |
| else | |
| return | |
| end | |
| -- View: Zoom time selection | |
| reaper.Main_OnCommandEx(40031, 1) | |
| -- restore selection | |
| reaper.GetSet_LoopTimeRange(true, true, start, end_, false) | |
| end | |
| function TranscriptEditor:render_separator() | |
| ImGui.Dummy(ctx, self.MIN_CONTENT_WIDTH, 0) | |
| ImGui.Separator(ctx) | |
| ImGui.Dummy(ctx, 0, 0) | |
| end | |
| function TranscriptEditor:update_time_selection() | |
| if self.editing then | |
| self.editing.word:select_in_timeline(self:offset()) | |
| end | |
| end | |
| function TranscriptEditor:handle_save() | |
| if self.editing then | |
| local segment = self.editing.segment | |
| segment:set_words(self.editing.words) | |
| self.transcript:update() | |
| end | |
| end | |
| function TranscriptEditor:handle_word_add() | |
| local words = self.editing.words | |
| local word_index = self.editing.word_index | |
| table.insert(words, word_index + 1, TranscriptWord.new { | |
| word = '...', | |
| start = words[word_index].end_, | |
| end_ = words[word_index].end_, | |
| probability = 1.0 | |
| }) | |
| self:edit_word(word_index + 1) | |
| end | |
| function TranscriptEditor:handle_word_delete() | |
| local words = self.editing.words | |
| local word_index = self.editing.word_index | |
| table.remove(words, word_index) | |
| local num_words = #words | |
| if word_index > num_words then | |
| word_index = num_words | |
| end | |
| self:edit_word(word_index) | |
| end | |
| function TranscriptEditor:handle_word_split() | |
| local words = self.editing.words | |
| local word_index = self.editing.word_index | |
| TranscriptSegment.split_word(words, word_index) | |
| self:edit_word(word_index) | |
| end | |
| function TranscriptEditor:handle_word_merge() | |
| local words = self.editing.words | |
| local word_index = self.editing.word_index | |
| local num_words = #words | |
| if word_index < num_words then | |
| TranscriptSegment.merge_words(words, word_index, word_index + 1) | |
| self:edit_word(word_index) | |
| end | |
| end | |
| function TranscriptEditor:handle_zoom_change() | |
| if self.sync_time_selection then | |
| self:zoom(self.zoom_level) | |
| end | |
| end | |
| function TranscriptEditor:_open() | |
| ImGui.OpenPopup(ctx, self.TITLE) | |
| self.is_open = true | |
| end | |
| function TranscriptEditor:_close() | |
| ImGui.CloseCurrentPopup(ctx) | |
| self.editing = nil | |
| self.is_open = false | |
| end | |