--[[ TranscriptUI.lua - @Transcript table & actions UI ]] TranscriptUI = Polo { TITLE = 'Transcript', FLOAT_FORMAT = '%.4f', COLUMN_WIDTH = 55, LARGE_COLUMN_WIDTH = 300, ACTIONS_MARGIN = 8, ACTIONS_PADDING = 8, SCORE_COLORS = { bright_green = 0xa3ff00a6, dark_green = 0x2cba00a6, orange = 0xffa700a6, red = 0xff2c2cff } } TranscriptUI.table_flags = function (sortable) local sort_flags = 0 if sortable then sort_flags = ImGui.TableFlags_Sortable() | ImGui.TableFlags_SortTristate() end return ( sort_flags | ImGui.TableFlags_Borders() | ImGui.TableFlags_Hideable() | ImGui.TableFlags_Resizable() | ImGui.TableFlags_Reorderable() | ImGui.TableFlags_RowBg() | ImGui.TableFlags_ScrollX() | ImGui.TableFlags_ScrollY() | ImGui.TableFlags_SizingFixedFit() ) end function TranscriptUI:init() assert(self.transcript, 'missing transcript') self.words = false self.colorize_words = false self.autoplay = true self.transcript_editor = TranscriptEditor.new { transcript = self.transcript } self.transcript_exporter = TranscriptExporter.new { transcript = self.transcript } self:init_layouts() end function TranscriptUI:init_layouts() local renderers = { self.render_create_regions, self.render_create_markers, self.render_create_notes, self.render_word_options, self.render_result_actions, self.render_auto_play, self.render_search } self.actions_layout = ColumnLayout.new { column_padding = self.ACTIONS_PADDING, num_columns = #renderers, render_column = function (column) renderers[column.num](self, column) end } end function TranscriptUI:render() if self.transcript:has_segments() then ImGui.SeparatorText(ctx, "Transcript") self.actions_layout:render() self:render_table() end self.transcript_editor:render() self.transcript_exporter:render() end function TranscriptUI:render_create_regions(column) if ImGui.Button(ctx, "Create Regions", column.width) then self:handle_create_markers(true) end end function TranscriptUI:render_create_markers(column) if ImGui.Button(ctx, "Create Markers", column.width) then self:handle_create_markers(false) end end function TranscriptUI:render_create_notes(column) if ImGui.Button(ctx, "Create Notes", column.width) then self:handle_create_notes() end end function TranscriptUI:render_word_options() local rv, value = ImGui.Checkbox(ctx, "Words", self.words) if rv then self.words = value end if self.words then ImGui.SameLine(ctx) rv, value = ImGui.Checkbox(ctx, "Colorize", self.colorize_words) if rv then self.colorize_words = value end end end function TranscriptUI:render_result_actions() self:render_export() ImGui.SameLine(ctx) self:render_clear() end function TranscriptUI:render_export() if ImGui.Button(ctx, "Export") then self:handle_export() end end function TranscriptUI:render_clear() if ImGui.Button(ctx, "Clear") then self:handle_transcript_clear() end end function TranscriptUI:render_auto_play() local rv, value = ImGui.Checkbox(ctx, "Auto Play", self.autoplay) if rv then self.autoplay = value end end function TranscriptUI:render_search(column) ImGui.SetCursorPosX(ctx, ImGui.GetWindowWidth(ctx) - column.width - self.ACTIONS_MARGIN) ImGui.PushItemWidth(ctx, column.width) app:trap(function() local search_changed, search = ImGui.InputTextWithHint(ctx, '##search', 'Search', self.transcript.search) if search_changed then self:handle_search(search) end end) ImGui.PopItemWidth(ctx) end function TranscriptUI:handle_create_markers(regions) reaper.PreventUIRefresh(1) reaper.Undo_BeginBlock() self.transcript:create_markers(0, regions, self.words) reaper.Undo_EndBlock( ("Create %s from speech"):format(regions and 'regions' or 'markers'), -1) reaper.PreventUIRefresh(-1) end function TranscriptUI:handle_create_notes() reaper.PreventUIRefresh(1) reaper.Undo_BeginBlock() self.transcript:create_notes_track(self.words) reaper.Undo_EndBlock("Create notes from speech", -1) reaper.PreventUIRefresh(-1) end function TranscriptUI:handle_export() self.transcript_exporter:open() end function TranscriptUI:handle_transcript_clear() self.transcript:clear() end function TranscriptUI:handle_search(search) self.transcript.search = search self.transcript:update() end -- formerly ReaSpeechUI:render_table() function TranscriptUI:render_table() local columns = self.transcript:get_columns() local num_columns = #columns + 1 local ok = ImGui.BeginTable(ctx, "results", num_columns, self.table_flags(true)) if ok then app:trap(function () ImGui.TableSetupColumn(ctx, "##actions", ImGui.TableColumnFlags_NoSort(), 20) for _, column in pairs(columns) do local column_flags = 0 if TranscriptSegment.default_hide(column) then -- reaper.ShowConsoleMsg(string.format('column %s: %s\n', column, TranscriptSegment.default_hide(column))) column_flags = column_flags | ImGui.TableColumnFlags_DefaultHide() end local init_width = self.COLUMN_WIDTH if column == "text" or column == "file" then init_width = self.LARGE_COLUMN_WIDTH end -- reaper.ShowConsoleMsg(string.format('column %s: %s / flags: %s\n', column, TranscriptSegment.default_hide(column), column_flags)) ImGui.TableSetupColumn(ctx, column, column_flags, init_width) end ImGui.TableSetupScrollFreeze(ctx, 0, 1) ImGui.TableHeadersRow(ctx) self:sort_table() for index, segment in pairs(self.transcript:get_segments()) do ImGui.TableNextRow(ctx) ImGui.TableNextColumn(ctx) self:render_segment_actions(segment, index) for _, column in pairs(columns) do ImGui.TableNextColumn(ctx) self:render_table_cell(segment, column) end end end) ImGui.EndTable(ctx) end end function TranscriptUI:render_segment_actions(segment, index) ImGui.PushFont(ctx, Fonts.icons) app:trap(function() ImGui.Text(ctx, Fonts.ICON.pencil) end) ImGui.PopFont(ctx) if ImGui.IsItemHovered(ctx) then ImGui.SetMouseCursor(ctx, ImGui.MouseCursor_Hand()) end if ImGui.IsItemClicked(ctx) then self.transcript_editor:edit_segment(segment, index) end app:tooltip("Edit") end function TranscriptUI:render_table_cell(segment, column) if column == "text" or column == "word" then self:render_text(segment, column) elseif column == "score" then self:render_score(segment:get(column, 0.0)) elseif column == "start" or column == "end" then ImGui.Text(ctx, reaper.format_timestr(segment:get(column, 0.0), '')) else local value = segment:get(column) if type(value) == 'table' then value = table.concat(value, ', ') elseif math.type(value) == 'float' then value = self.FLOAT_FORMAT:format(value) end ImGui.Text(ctx, tostring(value)) end end function TranscriptUI:render_link(text, onclick, text_color, underline_color) text_color = text_color or 0xffffffff underline_color = underline_color or 0xffffffa0 ImGui.TextColored(ctx, text_color, text) if ImGui.IsItemHovered(ctx) then local rect_min_x, rect_min_y = ImGui.GetItemRectMin(ctx) local rect_max_x, _ = ImGui.GetItemRectMax(ctx) local _, rect_size_y = ImGui.GetItemRectSize(ctx) local line_y = rect_min_y + rect_size_y - 1 ImGui.DrawList_AddLine( ImGui.GetWindowDrawList(ctx), rect_min_x, line_y, rect_max_x, line_y, underline_color, 1.0) ImGui.SetMouseCursor(ctx, ImGui.MouseCursor_Hand()) end if ImGui.IsItemClicked(ctx) then onclick() end end function TranscriptUI:render_text(segment, column) if self.words then self:render_text_words(segment, column) else self:render_text_simple(segment, column) end end function TranscriptUI:render_text_simple(segment, column) self:render_link(segment:get(column, ""), function () segment:navigate(nil,self.autoplay) end) end function TranscriptUI:render_text_words(segment, _) if segment.words then for i, word in pairs(segment.words) do if i > 1 then ImGui.SameLine(ctx, 0, 0) ImGui.Text(ctx, ' ') ImGui.SameLine(ctx, 0, 0) end local color = nil if self.colorize_words then color = self.score_color(word:score()) end self:render_link(word.word, function () segment:navigate(i, self.autoplay) end, color) end end end function TranscriptUI:render_score(value) local w, h = 50 * value, 3 local color = self.score_color(value) if color then local draw_list = ImGui.GetWindowDrawList(ctx) local x, y = ImGui.GetCursorScreenPos(ctx) y = y + 7 ImGui.DrawList_AddRectFilled(draw_list, x, y, x + w, y + h, color) end ImGui.Dummy(ctx, w, h) end function TranscriptUI.score_color(value) local colors = TranscriptUI.SCORE_COLORS if value > 0.9 then return colors.bright_green elseif value > 0.8 then return colors.dark_green elseif value > 0.7 then return colors.orange elseif value > 0.0 then return colors.red else return nil end end function TranscriptUI:sort_table() local specs_dirty, has_specs = ImGui.TableNeedSort(ctx) if has_specs and specs_dirty then local columns = self.transcript:get_columns() local column = nil local ascending = true for next_id = 0, math.huge do local ok, _, col_idx, _, sort_direction = ImGui.TableGetColumnSortSpecs(ctx, next_id) if not ok then break end column = columns[col_idx] ascending = (sort_direction == ImGui.SortDirection_Ascending()) end if column then self.transcript:sort(column, ascending) else self.transcript:update() end end end