From f43a71e09c8f240af63c5ead310eb2c9b456dd94 Mon Sep 17 00:00:00 2001 From: j <971121+smrl@users.noreply.github.com> Date: Fri, 19 Jul 2024 02:58:06 -0500 Subject: [PATCH 1/4] added CONTRIBUTING.md --- docs/CONTRIBUTING.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/CONTRIBUTING.md diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..d3a10b48 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,23 @@ + +# Tech Audio Open Source Project Guidelines + +Welcome to Tech Audio's open-source community! We're glad you're interested in our work and want to provide some guidelines to help you engage with the project effectively. + +If you encounter issues or have questions, please take the time to review existing resources. Familiarize yourself with our tools' features and limitations by reading the documentation contained in our GitHub repositories. Searching the GitHub issue tracker or our Discord server may reveal information about your problem. Be sure you're using the latest version of our software, as your issue may have already been resolved in a recent update. Google is a great resource as well ;) + +## Asking Questions +When you need to ask a question, choose the appropriate forum. Use the appropriate channel in our Discord server for general inquiries or discussions. If you think you've encountered a bug or have a specific software issue, create a GitHub issue. If you're a programmer, examine the source code for insights. +When posting, use a meaningful, specific subject header and provide clear details about the symptoms of your problem. Use the provisions for code formatting where appropriate to improve legibility. Include information about the specific tool you're using, your hardware & operating system and software versions, a description of what you're trying to achieve, and what isn't working as expected. It really helps if you can provide steps to reproduce the issue. Showing that you've attempted to reproduce the issue and have taken some steps to consider the problem on your own goes a long way, and detailed information makes it easier for us or community members to help. Beyond that, please be patient and respectful. + +## Interpreting Answers +When you receive a response, approach it with an open mind. Sometimes the answer might point you to resources you overlooked or suggest a completely different approach. Remember, direct responses aren't rudeness, but a sign of an efficient, focused community. If you need clarification, show that you've made an effort to understand the initial response before asking for more details. It deepens your understanding of our tools, which makes the community stronger, and is a small ask for the effort we've put into building and releasing this work. And if you solve your problem, please share the solution! + +Please don't take it personally if you don't receive an answer. Review your question to see if you can improve it, and consider if there are other forums where your question might be more appropriate. We provide support for our tools on a best-effort basis, but we are a small team and our resources are limited. + +## Project Contributions +As an open-source project, we welcome you to fork and experiment with our code. We're still determining our approach to external code or documentation contributions, so keep an eye out for updates on this. If you've encountered a problem and see someone struggling with it, please chime in but be clear, constructive, and patient. Point to relevant documentation when possible. This all makes conversations useful beyond the initial exchange and becomes a valuable contribution in itself. Another way to contribute is by sharing your successes and interesting use cases with our tools. + +## Feature Requests and Project Direction +It's important to understand that our tools are a work in progress. We're generally welcome to discussions about the project, and Discord is the right place for that. It's particularly helpful to provide feedback about your specific workflows/techniques that work well or don't work with our software. And while we value every suggestion, we can't pursue every idea and feature requests are just that: suggestions rather than commitments. If you have specific needs that our current tools don't meet, we're open to discussions about custom software development -- in which case, please reach out to explore potential arrangements. + +Thank you for your interest in what we do -- our decision to open-source is an experiment to make our work broadly available and develop a community surrounding it. Your thoughtful participation, constructive feedback, and respect for the guidelines we've outlined will determine whether this becomes a positive, ongoing initiative or not. We're optimistic! \ No newline at end of file From 495979cecab52772267fd9ff44a995b98610cc87 Mon Sep 17 00:00:00 2001 From: j <971121+smrl@users.noreply.github.com> Date: Fri, 6 Dec 2024 07:04:58 -0600 Subject: [PATCH 2/4] add persistence and tooltips to export dialogs --- .../ReaSpeech/source/ReaSpeechWidgets.lua | 696 +++++++++--------- .../ReaSpeech/source/TranscriptExporter.lua | 574 +++++++++------ reascripts/ReaSpeech/source/VTTWriter.lua | 84 +++ 3 files changed, 793 insertions(+), 561 deletions(-) create mode 100644 reascripts/ReaSpeech/source/VTTWriter.lua diff --git a/reascripts/ReaSpeech/source/ReaSpeechWidgets.lua b/reascripts/ReaSpeech/source/ReaSpeechWidgets.lua index e1c7a14e..f1c989b0 100644 --- a/reascripts/ReaSpeech/source/ReaSpeechWidgets.lua +++ b/reascripts/ReaSpeech/source/ReaSpeechWidgets.lua @@ -2,486 +2,490 @@ ReaSpeechWidgets.lua - collection of common widgets that ReaSpeech uses -]]-- - +]] -- ReaSpeechWidget = Polo { - HELP_ICON_SIZE = 15, + HELP_ICON_SIZE = 15 } function ReaSpeechWidget:init() - if not self.state then - assert(self.default ~= nil, "default value not provided") - self.state = Storage.memory(self.default) - end - assert(self.renderer, "renderer not provided") - self.ctx = self.ctx or ctx - self.widget_id = self.widget_id or reaper.genGuid() - self.on_set = nil + if not self.state then + assert(self.default ~= nil, "default value not provided") + self.state = Storage.memory(self.default) + end + assert(self.renderer, "renderer not provided") + self.ctx = self.ctx or ctx + self.widget_id = self.widget_id or reaper.genGuid() + self.on_set = nil end function ReaSpeechWidget:render(...) - ImGui.PushID(self.ctx, self.widget_id) - local args = ... - Trap(function() - self.renderer(self, args) - end) - ImGui.PopID(self.ctx) + ImGui.PushID(self.ctx, self.widget_id) + local args = ... + Trap(function() + self.renderer(self, args) + end) + ImGui.PopID(self.ctx) end function ReaSpeechWidget:render_help_icon() - local options = self.options - local size = self.HELP_ICON_SIZE - Widgets.icon(Icons.info, '##help-text', size, size, options.help_text, 0xffffffa0, 0xffffffff) + local options = self.options + local size = self.HELP_ICON_SIZE + Widgets.icon(Icons.info, '##help-text', size, size, options.help_text, 0xffffffa0, 0xffffffff) end function ReaSpeechWidget:render_label(label) - local options = self.options - label = label or options.label + local options = self.options + label = label or options.label - ImGui.Text(self.ctx, label) + ImGui.Text(self.ctx, label) - if label ~= '' and options.help_text then - ImGui.SameLine(self.ctx) - self:render_help_icon() - end + if label ~= '' and options.help_text then + ImGui.SameLine(self.ctx) + self:render_help_icon() + end - ImGui.Dummy(self.ctx, 0, 0) + ImGui.Dummy(self.ctx, 0, 0) end function ReaSpeechWidget:value() - return self.state:get() + return self.state:get() end function ReaSpeechWidget:set(value) - self.state:set(value) - if self.on_set then self:on_set() end + self.state:set(value) + if self.on_set then + self:on_set() + end end -- Widget Implementations ReaSpeechCheckbox = {} -ReaSpeechCheckbox.new = function (options) - options = options or { - label_long = nil, - label_short = nil, - width_threshold = nil, - } - options.default = options.default or false - - options.changed_handler = options.changed_handler or function(_) end - - local o = ReaSpeechWidget.new({ - state = options.state, - default = options.default, - widget_id = options.widget_id, - renderer = ReaSpeechCheckbox.renderer, - options = options, - }) - - options.changed_handler(o:value()) - - return o +ReaSpeechCheckbox.new = function(options) + options = options or { + label_long = nil, + label_short = nil, + width_threshold = nil + } + options.default = options.default or false + + options.changed_handler = options.changed_handler or function(_) + end + + local o = ReaSpeechWidget.new({ + state = options.state, + default = options.default, + widget_id = options.widget_id, + renderer = ReaSpeechCheckbox.renderer, + options = options + }) + + options.changed_handler(o:value()) + + return o end ReaSpeechCheckbox.simple = function(default_value, label, changed_handler) - return ReaSpeechCheckbox.new { - default = default_value, - label_long = label, - label_short = label, - width_threshold = 0, - changed_handler = changed_handler or function() end, - } + return ReaSpeechCheckbox.new { + default = default_value, + label_long = label, + label_short = label, + width_threshold = 0, + changed_handler = changed_handler or function() + end + } end -ReaSpeechCheckbox.renderer = function (self, column) - local options = self.options - local label = options.label_long +ReaSpeechCheckbox.renderer = function(self, column) + local options = self.options + local label = options.label_long - if column and column.width < options.width_threshold then - label = options.label_short - end + if column and column.width < options.width_threshold then + label = options.label_short + end - local rv, value = ImGui.Checkbox(self.ctx, label, self:value()) + local rv, value = ImGui.Checkbox(self.ctx, label, self:value()) - if options.help_text then - ImGui.SameLine(self.ctx) - ImGui.SetCursorPosY(self.ctx, ImGui.GetCursorPosY(self.ctx) + 7) - self:render_help_icon() - end - - if rv then - self:set(value) - options.changed_handler(value) - end + if options.help_text then + ImGui.SameLine(self.ctx) + ImGui.SetCursorPosY(self.ctx, ImGui.GetCursorPosY(self.ctx) + 7) + self:render_help_icon() + end + + if rv then + self:set(value) + options.changed_handler(value) + end end ReaSpeechTextInput = {} -ReaSpeechTextInput.new = function (options) - options = options or { - label = nil, - } - options.default = options.default or '' - - local o = ReaSpeechWidget.new({ - state = options.state, - default = options.default, - widget_id = options.widget_id, - renderer = ReaSpeechTextInput.renderer, - options = options, - }) - - return o +ReaSpeechTextInput.new = function(options) + options = options or { + label = nil + } + options.default = options.default or '' + + local o = ReaSpeechWidget.new({ + state = options.state, + default = options.default, + widget_id = options.widget_id, + renderer = ReaSpeechTextInput.renderer, + options = options + }) + + return o end ReaSpeechTextInput.simple = function(default_value, label) - return ReaSpeechTextInput.new { - default = default_value, - label = label - } + return ReaSpeechTextInput.new { + default = default_value, + label = label + } end -ReaSpeechTextInput.renderer = function (self) - local options = self.options +ReaSpeechTextInput.renderer = function(self) + local options = self.options - self:render_label() + self:render_label() - local imgui_label = ("##%s"):format(options.label) + local imgui_label = ("##%s"):format(options.label) - local rv, value = ImGui.InputText(self.ctx, imgui_label, self:value()) + local rv, value = ImGui.InputText(self.ctx, imgui_label, self:value()) - if rv then - self:set(value) - end + if rv then + self:set(value) + end end ReaSpeechCombo = {} -ReaSpeechCombo.new = function (options) - options = options or {} +ReaSpeechCombo.new = function(options) + options = options or {} - -- nothing is selected by default - options.default = options.default or nil + -- nothing is selected by default + options.default = options.default or nil - -- nil label won't render anything that takes space - options.label = options.label or "" + -- nil label won't render anything that takes space + options.label = options.label or "" - options.items = options.items or {} - options.item_labels = options.item_labels or {} + options.items = options.items or {} + options.item_labels = options.item_labels or {} - local o = ReaSpeechWidget.new({ - state = options.state, - default = options.default, - widget_id = options.widget_id, - renderer = ReaSpeechCombo.renderer, - options = options, - }) + local o = ReaSpeechWidget.new({ + state = options.state, + default = options.default, + widget_id = options.widget_id, + renderer = ReaSpeechCombo.renderer, + options = options + }) - return o + return o end -ReaSpeechCombo.renderer = function (self) - local options = self.options +ReaSpeechCombo.renderer = function(self) + local options = self.options - self:render_label() + self:render_label() - local imgui_label = ("##%s"):format(options.label) - local item_label = options.item_labels[self:value()] or "" - local combo_flags = ImGui.ComboFlags_HeightLarge() + local imgui_label = ("##%s"):format(options.label) + local item_label = options.item_labels[self:value()] or self:value() + local combo_flags = ImGui.ComboFlags_HeightLarge() - if ImGui.BeginCombo(self.ctx, imgui_label, item_label, combo_flags) then - Trap(function() - for _, item in pairs(options.items) do - local is_selected = (item == self:value()) - if ImGui.Selectable(self.ctx, options.item_labels[item], is_selected) then - self:set(item) - end - end - end) - ImGui.EndCombo(self.ctx) - end + if ImGui.BeginCombo(self.ctx, imgui_label, item_label, combo_flags) then + Trap(function() + for _, item in pairs(options.items) do + local is_selected = (item == self:value()) + local display_label = options.item_labels[item] or item + if ImGui.Selectable(self.ctx, display_label, is_selected) then + self:set(item) + end + end + end) + ImGui.EndCombo(self.ctx) + end end ReaSpeechTabBar = {} -ReaSpeechTabBar.new = function (options) - options = options or {} +ReaSpeechTabBar.new = function(options) + options = options or {} - -- nothing is selected by default - options.default = options.default or nil + -- nothing is selected by default + options.default = options.default or nil - options.tabs = options.tabs or {} + options.tabs = options.tabs or {} - local o = ReaSpeechWidget.new({ - default = options.default, - widget_id = options.widget_id, - renderer = ReaSpeechTabBar.renderer, - options = options, - }) + local o = ReaSpeechWidget.new({ + default = options.default, + widget_id = options.widget_id, + renderer = ReaSpeechTabBar.renderer, + options = options + }) - return o + return o end -ReaSpeechTabBar.renderer = function (self) - if ImGui.BeginTabBar(self.ctx, 'TabBar') then - for _, tab in pairs(self.options.tabs) do - if ImGui.BeginTabItem(self.ctx, tab.label) then - Trap(function() - self:set(tab.key) - end) - ImGui.EndTabItem(self.ctx) - end +ReaSpeechTabBar.renderer = function(self) + if ImGui.BeginTabBar(self.ctx, 'TabBar') then + for _, tab in pairs(self.options.tabs) do + if ImGui.BeginTabItem(self.ctx, tab.label) then + Trap(function() + self:set(tab.key) + end) + ImGui.EndTabItem(self.ctx) + end + end + ImGui.EndTabBar(self.ctx) end - ImGui.EndTabBar(self.ctx) - end end ReaSpeechTabBar.tab = function(key, label) - return { - key = key, - label = label - } + return { + key = key, + label = label + } end ReaSpeechButtonBar = {} -ReaSpeechButtonBar.new = function (options) - options = options or {} - - -- nothing is selected by default - options.default = options.default or nil - - -- nil label won't render anything that takes space - options.label = options.label or "" - - options.buttons = options.buttons or {} - options.styles = options.styles or {} - - local o = ReaSpeechWidget.new({ - state = options.state, - default = options.default, - widget_id = options.widget_id, - renderer = ReaSpeechButtonBar.renderer, - options = options, - }) - - local with_button_color = function (selected, f) - if selected then - ImGui.PushStyleColor(o.ctx, ImGui.Col_Button(), Theme.colors.dark_gray_translucent) - Trap(f) - ImGui.PopStyleColor(o.ctx) - else - f() - end - end - - o.layout = ColumnLayout.new { - column_padding = options.column_padding or 0, - margin_bottom = options.margin_bottom or 0, - margin_left = options.margin_left or 0, - margin_right = options.margin_right or 0, - width = options.width or 0, - num_columns = #options.buttons, - - render_column = function (column) - local bar_label = column.num == 1 and options.label or "" - o:render_label(bar_label) - - local button_label, model_name = table.unpack(options.buttons[column.num]) - with_button_color(o:value() == model_name, function () - if ImGui.Button(o.ctx, button_label, column.width) then - o:set(model_name) +ReaSpeechButtonBar.new = function(options) + options = options or {} + + -- nothing is selected by default + options.default = options.default or nil + + -- nil label won't render anything that takes space + options.label = options.label or "" + + options.buttons = options.buttons or {} + options.styles = options.styles or {} + + local o = ReaSpeechWidget.new({ + state = options.state, + default = options.default, + widget_id = options.widget_id, + renderer = ReaSpeechButtonBar.renderer, + options = options + }) + + local with_button_color = function(selected, f) + if selected then + ImGui.PushStyleColor(o.ctx, ImGui.Col_Button(), Theme.colors.dark_gray_translucent) + Trap(f) + ImGui.PopStyleColor(o.ctx) + else + f() end - end) end - } - return o + + o.layout = ColumnLayout.new { + column_padding = options.column_padding or 0, + margin_bottom = options.margin_bottom or 0, + margin_left = options.margin_left or 0, + margin_right = options.margin_right or 0, + width = options.width or 0, + num_columns = #options.buttons, + + render_column = function(column) + local bar_label = column.num == 1 and options.label or "" + o:render_label(bar_label) + + local button_label, model_name = table.unpack(options.buttons[column.num]) + with_button_color(o:value() == model_name, function() + if ImGui.Button(o.ctx, button_label, column.width) then + o:set(model_name) + end + end) + end + } + return o end -ReaSpeechButtonBar.renderer = function (self) - self.layout:render() +ReaSpeechButtonBar.renderer = function(self) + self.layout:render() end ReaSpeechButton = {} ReaSpeechButton.new = function(options) - options = options or {} + options = options or {} - -- nil label won't render anything that takes space - options.label = options.label or "" + -- nil label won't render anything that takes space + options.label = options.label or "" - options.disabled = options.disabled or false + options.disabled = options.disabled or false - if not options.disabled then - assert(options.on_click, "on_click handler not provided") - end + if not options.disabled then + assert(options.on_click, "on_click handler not provided") + end - local o = ReaSpeechWidget.new({ - default = true, - renderer = ReaSpeechButton.renderer, - options = options, - }) + local o = ReaSpeechWidget.new({ + default = true, + renderer = ReaSpeechButton.renderer, + options = options + }) - return o + return o end ReaSpeechButton.renderer = function(self) - local disable_if = ReaUtil.disabler(self.ctx) - local options = self.options + local disable_if = ReaUtil.disabler(self.ctx) + local options = self.options - disable_if(options.disabled, function() - if ImGui.Button(self.ctx, options.label, options.width) then - Trap(options.on_click) - end - end) + disable_if(options.disabled, function() + if ImGui.Button(self.ctx, options.label, options.width) then + Trap(options.on_click) + end + end) end ReaSpeechFileSelector = { - JSREASCRIPT_URL = 'https://forum.cockos.com/showthread.php?t=212174', - has_js_ReaScriptAPI = function() - return reaper.JS_Dialog_BrowseForSaveFile - end + JSREASCRIPT_URL = 'https://forum.cockos.com/showthread.php?t=212174', + has_js_ReaScriptAPI = function() + return reaper.JS_Dialog_BrowseForSaveFile + end } ReaSpeechFileSelector.new = function(options) - options = options or {} - options.default = options.default or '' - local title = options.title or 'Save file' - local folder = options.folder or '' - local file = options.file or '' - local ext = options.ext or '' - local save = options.save or false - local multi = options.multi or false - - local dialog_function - if save then - dialog_function = function() - return reaper.JS_Dialog_BrowseForSaveFile(title, folder, file, ext) - end - else - dialog_function = function() - return reaper.JS_Dialog_BrowseForOpenFiles(title, folder, file, ext, multi) - end - end - - local o = ReaSpeechWidget.new({ - default = options.default, - renderer = ReaSpeechFileSelector.renderer, - options = options, - }) - - options.button = ReaSpeechButton.new({ - label = options.button_label or 'Choose File', - disabled = not ReaSpeechFileSelector.has_js_ReaScriptAPI(), - width = options.button_width, - on_click = function() - local rv, selected_file = dialog_function() - if rv == 1 then - o:set(selected_file) - end + options = options or {} + options.default = options.default or '' + local title = options.title or 'Save file' + local folder = options.folder or '' + local file = options.file or '' + local ext = options.ext or '' + local save = options.save or false + local multi = options.multi or false + + local dialog_function + if save then + dialog_function = function() + return reaper.JS_Dialog_BrowseForSaveFile(title, folder, file, ext) + end + else + dialog_function = function() + return reaper.JS_Dialog_BrowseForOpenFiles(title, folder, file, ext, multi) + end end - }) - return o + local o = ReaSpeechWidget.new({ + default = options.default, + renderer = ReaSpeechFileSelector.renderer, + options = options + }) + + options.button = ReaSpeechButton.new({ + label = options.button_label or 'Choose File', + disabled = not ReaSpeechFileSelector.has_js_ReaScriptAPI(), + width = options.button_width, + on_click = function() + local rv, selected_file = dialog_function() + if rv == 1 then + o:set(selected_file) + end + end + }) + + return o end -- Display a text input for the output filename, with a Browse button if -- the js_ReaScriptAPI extension is available. ReaSpeechFileSelector.renderer = function(self) - local options = self.options + local options = self.options - ImGui.Text(self.ctx, options.label) + ImGui.Text(self.ctx, options.label) - ReaSpeechFileSelector.render_jsapi_notice(self) + ReaSpeechFileSelector.render_jsapi_notice(self) - options.button:render() - ImGui.SameLine(self.ctx) + options.button:render() + ImGui.SameLine(self.ctx) - local w, _ - if not options.input_width then - w, _ = ImGui.GetContentRegionAvail(self.ctx) - else - w = options.input_width - end + local w, _ + if not options.input_width then + w, _ = ImGui.GetContentRegionAvail(self.ctx) + else + w = options.input_width + end - ImGui.SetNextItemWidth(self.ctx, w) - local file_changed, file = ImGui.InputText(self.ctx, '##file', self:value()) - if file_changed then - self:set(file) - end + ImGui.SetNextItemWidth(self.ctx, w) + local file_changed, file = ImGui.InputText(self.ctx, '##file', self:value()) + if file_changed then + self:set(file) + end end ReaSpeechFileSelector.render_jsapi_notice = function(self) - if ReaSpeechFileSelector.has_js_ReaScriptAPI() then - return - end - - local _, spacing_v = ImGui.GetStyleVar(self.ctx, ImGui.StyleVar_ItemSpacing()) - ImGui.PushStyleVar(self.ctx, ImGui.StyleVar_ItemSpacing(), 0, spacing_v) - ImGui.Text(self.ctx, "To enable file selector, ") - ImGui.SameLine(self.ctx) - Widgets.link('install js_ReaScriptAPI', ReaUtil.url_opener(ReaSpeechFileSelector.JSREASCRIPT_URL)) - ImGui.SameLine(self.ctx) - ImGui.Text(self.ctx, ".") - ImGui.PopStyleVar(self.ctx) + if ReaSpeechFileSelector.has_js_ReaScriptAPI() then + return + end + + local _, spacing_v = ImGui.GetStyleVar(self.ctx, ImGui.StyleVar_ItemSpacing()) + ImGui.PushStyleVar(self.ctx, ImGui.StyleVar_ItemSpacing(), 0, spacing_v) + ImGui.Text(self.ctx, "To enable file selector, ") + ImGui.SameLine(self.ctx) + Widgets.link('install js_ReaScriptAPI', ReaUtil.url_opener(ReaSpeechFileSelector.JSREASCRIPT_URL)) + ImGui.SameLine(self.ctx) + ImGui.Text(self.ctx, ".") + ImGui.PopStyleVar(self.ctx) end ReaSpeechListBox = {} ReaSpeechListBox.new = function(options) - options = options or {} + options = options or {} - options = options or {} + options = options or {} - -- nothing is selected by default - options.default = options.default or nil + -- nothing is selected by default + options.default = options.default or nil - options.items = options.items or {} - options.item_labels = options.item_labels or {} + options.items = options.items or {} + options.item_labels = options.item_labels or {} - local o = ReaSpeechWidget.new({ - state = options.state, - default = options.default, - widget_id = options.widget_id, - renderer = ReaSpeechListBox.renderer, - options = options, - }) + local o = ReaSpeechWidget.new({ + state = options.state, + default = options.default, + widget_id = options.widget_id, + renderer = ReaSpeechListBox.renderer, + options = options + }) - Logging.init(o, 'ReaSpeechListBox') + Logging.init(o, 'ReaSpeechListBox') - return o + return o end ReaSpeechListBox.renderer = function(self) - local options = self.options + local options = self.options - self:render_label() + self:render_label() - local imgui_label = ("##%s"):format(options.label) + local imgui_label = ("##%s"):format(options.label) - local needs_update = false - if ImGui.BeginListBox(self.ctx, imgui_label) then - Trap(function() - local current = self:value() - local new_value = {} - for i, item in ipairs(options.items) do - new_value[item] = current[item] or false - local is_selected = current[item] - local label = options.item_labels[item] - ImGui.PushID(self.ctx, 'item' .. i) + local needs_update = false + if ImGui.BeginListBox(self.ctx, imgui_label) then Trap(function() - local result, now_selected = ImGui.Selectable(self.ctx, label, is_selected) - - if result and is_selected ~= now_selected then - needs_update = true - new_value[item] = now_selected - end + local current = self:value() + local new_value = {} + for i, item in ipairs(options.items) do + new_value[item] = current[item] or false + local is_selected = current[item] + local label = options.item_labels[item] + ImGui.PushID(self.ctx, 'item' .. i) + Trap(function() + local result, now_selected = ImGui.Selectable(self.ctx, label, is_selected) + + if result and is_selected ~= now_selected then + needs_update = true + new_value[item] = now_selected + end + end) + ImGui.PopID(self.ctx) + end + + if needs_update then + self:set(new_value) + end end) - ImGui.PopID(self.ctx) - end - - if needs_update then - self:set(new_value) - end - end) - ImGui.EndListBox(self.ctx) - end + ImGui.EndListBox(self.ctx) + end end diff --git a/reascripts/ReaSpeech/source/TranscriptExporter.lua b/reascripts/ReaSpeech/source/TranscriptExporter.lua index d27d951b..1a20e9a7 100644 --- a/reascripts/ReaSpeech/source/TranscriptExporter.lua +++ b/reascripts/ReaSpeech/source/TranscriptExporter.lua @@ -2,321 +2,465 @@ TranscriptExporter.lua - Transcript export UI -]]-- - +]] -- TranscriptExporter = Polo { - TITLE = 'Export', - WIDTH = 650, - HEIGHT = 200, - BUTTON_WIDTH = 120, - INPUT_WIDTH = 120, - FILE_WIDTH = 500, + TITLE = 'Export', + WIDTH = 650, + HEIGHT = 200, + BUTTON_WIDTH = 120, + INPUT_WIDTH = 120, + FILE_WIDTH = 500 } function TranscriptExporter:init() - assert(self.transcript, 'missing transcript') - - Logging.init(self, 'TranscriptExporter') - - ToolWindow.init(self, { - title = self.TITLE, - width = self.WIDTH, - height = self.HEIGHT, - window_flags = 0 - | ImGui.WindowFlags_AlwaysAutoResize() - | ImGui.WindowFlags_NoCollapse() - | ImGui.WindowFlags_NoDocking() - | ImGui.WindowFlags_TopMost(), - }) - - self.export_formats = TranscriptExporterFormats.new { - TranscriptExportFormat.exporter_json(), - TranscriptExportFormat.exporter_srt(), - TranscriptExportFormat.exporter_csv(), - } - self.export_options = {} - self.file_selector = ReaSpeechFileSelector.new({ - label = 'File', - save = true, - button_width = self.BUTTON_WIDTH, - input_width = self.FILE_WIDTH - }) - - self.alert_popup = AlertPopup.new {} + assert(self.transcript, 'missing transcript') + + Logging.init(self, 'TranscriptExporter') + + ToolWindow.init(self, { + title = self.TITLE, + width = self.WIDTH, + height = self.HEIGHT, + window_flags = 0 | ImGui.WindowFlags_AlwaysAutoResize() | ImGui.WindowFlags_NoCollapse() | + ImGui.WindowFlags_NoDocking() | ImGui.WindowFlags_TopMost() + }) + + self.export_formats = TranscriptExporterFormats.new {TranscriptExportFormat.exporter_json(), + TranscriptExportFormat.exporter_srt(), + TranscriptExportFormat.exporter_csv(), + TranscriptExportFormat.exporter_vtt()} + + self.export_options = {} + + self.file_selector = ReaSpeechFileSelector.new({ + label = 'File', + save = true, + button_width = self.BUTTON_WIDTH, + input_width = self.FILE_WIDTH + }) + + self.alert_popup = AlertPopup.new {} + end function TranscriptExporter:show_success() - self.alert_popup.onclose = function () - self.alert_popup.onclose = nil - self:close() - end - self.alert_popup:show('Export Successful', 'Exported ' .. self.export_formats:selected_key() .. ' to: ' .. self.file_selector:value()) + self.alert_popup.onclose = function() + self.alert_popup.onclose = nil + self:close() + end + self.alert_popup:show('Export Successful', + 'Exported ' .. self.export_formats:selected_key() .. ' to: ' .. self.file_selector:value()) end function TranscriptExporter:show_error(msg) - self.alert_popup:show('Export Failed', msg) + self.alert_popup:show('Export Failed', msg) end function TranscriptExporter:render_content() - self.alert_popup:render() + self.alert_popup:render() - self.export_formats:render_combo(self.INPUT_WIDTH) + self.export_formats:render_combo() - ImGui.Spacing(ctx) + ImGui.Spacing(ctx) - self.export_formats:render_format_options(self.export_options) + self.export_formats:render_format_options(self.export_options) - ImGui.Spacing(ctx) + ImGui.Spacing(ctx) - self:render_file_selector() + self:render_file_selector() - self:render_separator() + self:render_separator() - self:render_buttons() + self:render_buttons() end function TranscriptExporter:render_file_selector() - self.file_selector:render() + self.file_selector:render() end function TranscriptExporter:render_buttons() - ReaUtil.disabler(ctx)(self.file_selector:value() == '', function() - if ImGui.Button(ctx, 'Export', self.BUTTON_WIDTH, 0) then - if self:handle_export() then - self:show_success() - end - end - end) + ReaUtil.disabler(ctx)(self.file_selector:value() == '', function() + if ImGui.Button(ctx, 'Export', self.BUTTON_WIDTH, 0) then + if self:handle_export() then + self:show_success() + end + end + end) - ImGui.SameLine(ctx) - if ImGui.Button(ctx, 'Cancel', self.BUTTON_WIDTH, 0) then - self:close() - end + ImGui.SameLine(ctx) + if ImGui.Button(ctx, 'Cancel', self.BUTTON_WIDTH, 0) then + self:close() + end end function TranscriptExporter:handle_export() - if self.file_selector:value() == '' then - self:show_error('Please specify a file name.') - return false - end - local file = io.open(self.file_selector:value(), 'w') - if not file then - self:show_error('Could not open file: ' .. self.file_selector:value()) - return false - end - self.export_formats:write(self.transcript, file, self.export_options) - file:close() - return true + if self.file_selector:value() == '' then + self:show_error('Please specify a file name.') + return false + end + local file = io.open(self.file_selector:value(), 'w') + if not file then + self:show_error('Could not open file: ' .. self.file_selector:value()) + return false + end + self.export_formats:write(self.transcript, file, self.export_options) + file:close() + return true end TranscriptExporterFormats = Polo { - new = function(formatters) - local format_map = {} + new = function(formatters) + local format_map = {} - for i, formatter in ipairs(formatters) do - format_map[formatter.key] = i - end + for i, formatter in ipairs(formatters) do + format_map[formatter.key] = i + end - return { - formatters = formatters, - format_map = format_map, - } - end, + return { + formatters = formatters, + format_map = format_map + } + end } -function TranscriptExporterFormats:render_combo(width) - ImGui.Text(ctx, 'Format') - ImGui.SetNextItemWidth(ctx, width) - if ImGui.BeginCombo(ctx, "##format", self.selected_format_key) then - Trap(function() - for _, format in pairs(self.formatters) do - local is_selected = self.selected_format_key == format.key - if ImGui.Selectable(ctx, format.key, is_selected) then - self.selected_format_key = format.key - end - if is_selected then - ImGui.SetItemDefaultFocus(ctx) +function TranscriptExporterFormats:init() + if not self.format_widget then + local storage = Storage.ExtState.make { + section = 'ReaSpeech.Export.Format', + persist = true + } + + local format_items = {} + for _, format in ipairs(self.formatters) do + table.insert(format_items, format.key) end - end - end) - ImGui.EndCombo(ctx) - end + + self.format_widget = ReaSpeechCombo.new { + state = storage:string('format', self.formatters[1].key), + label = 'Format', + items = format_items + } + end +end + +function TranscriptExporterFormats:render_combo() + self:init() + self.format_widget:render() end function TranscriptExporterFormats:selected_key() - return self:selected_format().key + return self.format_widget:value() end function TranscriptExporterFormats:file_selector_spec() - return self:selected_format():file_selector_spec() + return self:selected_format():file_selector_spec() end function TranscriptExporterFormats:write(transcript, output_file, options) - return self:selected_format().writer(transcript, output_file, options) + return self:selected_format().writer(transcript, output_file, options) end function TranscriptExporterFormats:selected_format() - if not self.selected_format_key then - if not self.formatters or #self.formatters < 1 then - self:debug('no formats to set for default') - return - end - - self.selected_format_key = self.formatters[1].key - end - - local index = self.format_map[self.selected_format_key] - - return self.formatters[index] + local index = self.format_map[self:selected_key()] + return self.formatters[index] end function TranscriptExporterFormats:render_format_options(options) - Trap(function() - local format = self:selected_format() + Trap(function() + local format = self:selected_format() - if format then - format.option_renderer(options) - end - end) + if format then + format.option_renderer(options) + end + end) end TranscriptExportFormat = Polo { - OPTIONS_NOOP = function(_options) end, - - new = function (key, extension, option_renderer, writer_f) - return { - key = key, - extension = extension, - option_renderer = option_renderer, - writer = writer_f, - } - end, + OPTIONS_NOOP = function(_options) + end, + + new = function(key, extension, option_renderer, writer_f) + return { + key = key, + extension = extension, + option_renderer = option_renderer, + writer = writer_f + } + end } function TranscriptExportFormat:file_selector_spec() - local selector_spec = '%s files (*.%s)\0*.%s\0All files (*.*)\0*.*\0\0' - return selector_spec:format(self.key, self.extension, self.extension) + local selector_spec = '%s files (*.%s)\0*.%s\0All files (*.*)\0*.*\0\0' + return selector_spec:format(self.key, self.extension, self.extension) end function TranscriptExportFormat.exporter_json() - return TranscriptExportFormat.new( - 'JSON', 'json', - TranscriptExportFormat.options_json, - TranscriptExportFormat.writer_json - ) + return TranscriptExportFormat.new('JSON', 'json', TranscriptExportFormat.options_json, + TranscriptExportFormat.writer_json) end function TranscriptExportFormat.options_json(options) - local rv, value = ImGui.Checkbox(ctx, 'One Object per Transcript Segment', options.object_per_segment) - if rv then - options.object_per_segment = value - end + if not options.json_widgets then + local storage = Storage.ExtState.make { + section = 'ReaSpeech.Export.JSON', + persist = true + } + + options.json_widgets = { + object_per_segment = ReaSpeechCheckbox.new { + state = storage:boolean('object_per_segment', false), + label_long = 'One Object per Transcript Segment', -- Changed from label to label_long + help_text = [[Each transcript segment is exported a separate JSON object.]] + } + } + end + + Trap(function() + options.json_widgets.object_per_segment:render() + options.object_per_segment = options.json_widgets.object_per_segment:value() + end) end function TranscriptExportFormat.writer_json(transcript, output_file, options) - if options.object_per_segment then - for _, segment in pairs(transcript:get_segments()) do - output_file:write(segment:to_json()) - output_file:write('\n') + if options.object_per_segment then + for _, segment in pairs(transcript:get_segments()) do + output_file:write(segment:to_json()) + output_file:write('\n') + end + else + output_file:write(transcript:to_json()) end - else - output_file:write(transcript:to_json()) - end end function TranscriptExportFormat.exporter_srt() - return TranscriptExportFormat.new( - 'SRT', 'srt', - TranscriptExportFormat.options_srt, - TranscriptExportFormat.writer_srt - ) + return TranscriptExportFormat.new('SRT', 'srt', TranscriptExportFormat.options_srt, + TranscriptExportFormat.writer_srt) end function TranscriptExportFormat.strip_non_numeric(value) - return value:gsub("[^0-9]", ""):gsub("^0+", "") + return value:gsub("[^0-9]", ""):gsub("^0+", "") end function TranscriptExportFormat.options_srt(options) - local rv, value - - rv, value = ImGui.InputText(ctx, 'X1', options.coords_x1, ImGui.InputTextFlags_CharsDecimal()) - if rv then - options.coords_x1 = TranscriptExportFormat.strip_non_numeric(value) - end - - ImGui.SameLine(ctx) + if not options.srt_widgets then + local storage = Storage.ExtState.make { + section = 'ReaSpeech.Export.SRT', + persist = true + } + + options.srt_widgets = { + coords_x1 = ReaSpeechTextInput.new { + state = storage:string('coords_x1', ''), + label = 'X1', + help_text = [[Left coordinate of the subtitle box.]], + flags = ImGui.InputTextFlags_CharsDecimal() + }, + coords_y1 = ReaSpeechTextInput.new { + state = storage:string('coords_y1', ''), + label = 'Y1', + help_text = [[Top coordinate of the subtitle box.]], + flags = ImGui.InputTextFlags_CharsDecimal() + }, + coords_x2 = ReaSpeechTextInput.new { + state = storage:string('coords_x2', ''), + label = 'X2', + help_text = [[Right coordinate of the subtitle box.]], + flags = ImGui.InputTextFlags_CharsDecimal() + }, + coords_y2 = ReaSpeechTextInput.new { + state = storage:string('coords_y2', ''), + label = 'Y2', + help_text = [[Bottom coordinate of the subtitle box.]], + flags = ImGui.InputTextFlags_CharsDecimal() + } + } + end - rv, value = ImGui.InputText(ctx, 'Y1', options.coords_y1, ImGui.InputTextFlags_CharsDecimal()) - if rv then - options.coords_y1 = TranscriptExportFormat.strip_non_numeric(value) - end + Trap(function() + ImGui.Text(ctx, "Subtitle Coordinates") - rv, value = ImGui.InputText(ctx, 'X2', options.coords_x2, ImGui.InputTextFlags_CharsDecimal()) - if rv then - options.coords_x2 = TranscriptExportFormat.strip_non_numeric(value) - end + options.srt_widgets.coords_x1:render() + ImGui.SameLine(ctx) + options.srt_widgets.coords_y1:render() - ImGui.SameLine(ctx) + options.srt_widgets.coords_x2:render() + ImGui.SameLine(ctx) + options.srt_widgets.coords_y2:render() - rv, value = ImGui.InputText(ctx, 'Y2', options.coords_y2, ImGui.InputTextFlags_CharsDecimal()) - if rv then - options.coords_y2 = TranscriptExportFormat.strip_non_numeric(value) - end + -- Update options with widget values + options.coords_x1 = TranscriptExportFormat.strip_non_numeric(options.srt_widgets.coords_x1:value()) + options.coords_y1 = TranscriptExportFormat.strip_non_numeric(options.srt_widgets.coords_y1:value()) + options.coords_x2 = TranscriptExportFormat.strip_non_numeric(options.srt_widgets.coords_x2:value()) + options.coords_y2 = TranscriptExportFormat.strip_non_numeric(options.srt_widgets.coords_y2:value()) + end) end function TranscriptExportFormat.writer_srt(transcript, output_file, options) - local writer = SRTWriter.new { file = output_file, options = options } - writer:write(transcript) + local writer = SRTWriter.new { + file = output_file, + options = options + } + writer:write(transcript) end function TranscriptExportFormat.exporter_csv() - return TranscriptExportFormat.new( - 'CSV', 'csv', - TranscriptExportFormat.options_csv, - TranscriptExportFormat.writer_csv - ) + return TranscriptExportFormat.new('CSV', 'csv', TranscriptExportFormat.options_csv, + TranscriptExportFormat.writer_csv) end function TranscriptExportFormat.options_csv(options) - local delimiters = CSVWriter.DELIMITERS - - local selected_delimiter = delimiters[1] + if not options.csv_widgets then + local storage = Storage.ExtState.make { + section = 'ReaSpeech.Export.CSV', + persist = true + } + + local delimiter_items = {} + for _, delimiter in ipairs(CSVWriter.DELIMITERS) do + table.insert(delimiter_items, delimiter.name) + end - for _, delimiter in ipairs(delimiters) do - if delimiter.char == options.delimiter then - selected_delimiter = delimiter - break + options.csv_widgets = { + delimiter = ReaSpeechCombo.new { + state = storage:string('delimiter', CSVWriter.DELIMITERS[1].name), + label = 'Delimiter', + help_text = [[Choose the character that separates fields: +Comma - Standard CSV format +Tab - Tab-separated values (TSV) +Semicolon (;) - Alternative for locales using comma as decimal]], + items = delimiter_items + }, + + include_header = ReaSpeechCheckbox.new { + state = storage:boolean('include_header', true), + label_long = 'Include Header Row', + help_text = [[Adds a header row with column names. +Useful for importing into spreadsheets.]] + } + } end - end - if ImGui.BeginCombo(ctx, 'Delimiter', selected_delimiter.name) then Trap(function() - for _, delimiter in ipairs(delimiters) do - local is_selected = options.delimiter == delimiter.char - if ImGui.Selectable(ctx, delimiter.name, is_selected) then - options.delimiter = delimiter.char - end - if is_selected then - ImGui.SetItemDefaultFocus(ctx) + options.csv_widgets.delimiter:render() + ImGui.Spacing(ctx) + options.csv_widgets.include_header:render() + + -- Update options with widget values + local selected_delimiter = CSVWriter.DELIMITERS[1] + for _, delimiter in ipairs(CSVWriter.DELIMITERS) do + if delimiter.name == options.csv_widgets.delimiter:value() then + selected_delimiter = delimiter + break + end end - end + options.delimiter = selected_delimiter.char + options.include_header_row = options.csv_widgets.include_header:value() end) - ImGui.EndCombo(ctx) - end +end - ImGui.Spacing(ctx) +function TranscriptExportFormat.writer_csv(transcript, output_file, options) + local writer = CSVWriter.new { + file = output_file, + delimiter = options.delimiter, + include_header_row = options.include_header_row + } + writer:write(transcript) +end - local rv, value = ImGui.Checkbox(ctx, 'Include Header Row', options.include_header_row) - if rv then - options.include_header_row = value - end +function TranscriptExportFormat.exporter_vtt() + return TranscriptExportFormat.new('VTT', 'vtt', TranscriptExportFormat.options_vtt, + TranscriptExportFormat.writer_vtt) end -function TranscriptExportFormat.writer_csv(transcript, output_file, options) - local writer = CSVWriter.new { - file = output_file, - delimiter = options.delimiter, - include_header_row = options.include_header_row - } - writer:write(transcript) +function TranscriptExportFormat.options_vtt(options) + if not options.vtt_widgets then + local storage = Storage.ExtState.make { + section = 'ReaSpeech.Export.VTT', + persist = true + } + + options.vtt_widgets = { + text_direction = ReaSpeechCombo.new { + state = storage:string('text_direction', 'Horizontal'), + label = 'Text Direction', + -- widget_id = 'vtt_text_direction', + help_text = [[Horizontal: default text, for languages like Japanese that can be written vertically: + • Right to Left - Vertical text written right-to-left + • Left to Right - Vertical text written left-to-right]], + items = {'Horizontal', 'Right to Left', 'Left to Right'} + }, + + line = ReaSpeechTextInput.new { + state = storage:string('line', ''), + label = 'Line Position', + -- widget_id = 'vtt_line', + help_text = [[line position of captions: + • Numbers: Place on specific line + - Positive (1,2,3...) counts from top + - Negative (-1,-2,-3...) counts from bottom + • Percentages: Position relative to video height + - 0% = top + - 100% = bottom]] + }, + + position = ReaSpeechTextInput.new { + state = storage:string('position', ''), + label = 'Position %', + -- widget_id = 'vtt_position', + help_text = [[Sets horizontal position: 0% = left edge, 50% = center, 100% = right edge]] + }, + + size = ReaSpeechTextInput.new { + state = storage:string('size', ''), + label = 'Size %', + -- widget_id = 'vtt_size', + help_text = [[Sets caption box width (0-100% of video width), Default auto-sizes to fit text]] + }, + + align = ReaSpeechCombo.new { + state = storage:string('align', 'Default'), + label = 'Alignment', + -- widget_id = 'vtt_align', + help_text = [[• Start - Aligns to start of text direction + (left for horizontal, top for vertical) +• Middle - Centers text (default) +• End - Aligns to end of text direction + (right for horizontal, bottom for vertical)]], + items = {'Default', 'Start', 'Middle', 'End'} + } + } + end + + Trap(function() + options.vtt_widgets.text_direction:render() + ImGui.Spacing(ctx) + + options.vtt_widgets.line:render() + ImGui.Spacing(ctx) + + options.vtt_widgets.position:render() + ImGui.Spacing(ctx) + + options.vtt_widgets.size:render() + ImGui.Spacing(ctx) + + options.vtt_widgets.align:render() + ImGui.Spacing(ctx) + + options.vertical = options.vtt_widgets.text_direction:value() == 'Horizontal' and nil or + options.vtt_widgets.text_direction:value() == 'Right to Left' and 'rl' or 'lr' + options.line = options.vtt_widgets.line:value() + options.position = options.vtt_widgets.position:value() + options.size = options.vtt_widgets.size:value() + options.align = options.vtt_widgets.align:value() ~= 'Default' and + string.lower(options.vtt_widgets.align:value()) or nil + end) +end + +function TranscriptExportFormat.writer_vtt(transcript, output_file, options) + local writer = VTTWriter.new { + file = output_file, + options = options + } + writer:write(transcript) end diff --git a/reascripts/ReaSpeech/source/VTTWriter.lua b/reascripts/ReaSpeech/source/VTTWriter.lua new file mode 100644 index 00000000..a26e9c40 --- /dev/null +++ b/reascripts/ReaSpeech/source/VTTWriter.lua @@ -0,0 +1,84 @@ +--[[ + VTTWriter.lua - WebVTT file writer +]] -- +VTTWriter = Polo { + TIME_FORMAT = '%02d:%02d:%02d.%03d' +} + +function VTTWriter:init() + assert(self.file, 'missing file') + self.options = self.options or {} + + -- Map options directly from passed in options + self.vertical = self.options.vertical + self.line = self.options.line + self.position = self.options.position + self.size = self.options.size + self.align = self.options.align +end + +VTTWriter.format_time = function(time) + local milliseconds = math.floor(time * 1000) % 1000 + local seconds = math.floor(time) % 60 + local minutes = math.floor(time / 60) % 60 + local hours = math.floor(time / 3600) + return string.format(VTTWriter.TIME_FORMAT, hours, minutes, seconds, milliseconds) +end + +function VTTWriter:write(transcript) + -- Write the required WEBVTT header + self.file:write('WEBVTT\n\n') + + -- Write each segment + for _, segment in pairs(transcript:get_segments()) do + self:write_segment(segment) + end +end + +function VTTWriter:write_segment(segment) + local start = segment:get('start') + local end_ = segment:get('end') + local text = segment:get('text') + self:write_cue(text, start, end_) +end + +function VTTWriter:write_cue(text, start, end_) + local start_str = VTTWriter.format_time(start) + local end_str = VTTWriter.format_time(end_) + + -- Write timestamp line with positioning if specified + self.file:write(start_str) + self.file:write(' --> ') + self.file:write(end_str) + self.file:write(self:settings()) + self.file:write('\n') + + -- Write cue text + self.file:write(text) + self.file:write('\n\n') +end + +function VTTWriter:settings() + local settings = {} + + if self.vertical then + table.insert(settings, 'vertical:' .. self.vertical) + end + if self.line then + table.insert(settings, 'line:' .. self.line) + end + if self.position then + table.insert(settings, 'position:' .. self.position .. '%') + end + if self.size then + table.insert(settings, 'size:' .. self.size .. '%') + end + if self.align then + table.insert(settings, 'align:' .. self.align) + end + + if #settings == 0 then + return '' + end + return ' ' .. table.concat(settings, ' ') +end \ No newline at end of file From e8e5978f8ccd65142012cd83f9ed996852fc8644 Mon Sep 17 00:00:00 2001 From: j <971121+smrl@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:02:26 -0600 Subject: [PATCH 3/4] fixed text direction option so horizontal mode adds no tag --- .../ReaSpeech/source/TranscriptExporter.lua | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/reascripts/ReaSpeech/source/TranscriptExporter.lua b/reascripts/ReaSpeech/source/TranscriptExporter.lua index ebecec61..dd21aaf8 100644 --- a/reascripts/ReaSpeech/source/TranscriptExporter.lua +++ b/reascripts/ReaSpeech/source/TranscriptExporter.lua @@ -17,16 +17,13 @@ function TranscriptExporter:init() Logging.init(self, 'TranscriptExporter') - ToolWindow.init(self, { - title = self.TITLE, - width = self.WIDTH, - height = self.HEIGHT, - window_flags = 0 - | ImGui.WindowFlags_AlwaysAutoResize() - | ImGui.WindowFlags_NoCollapse() - | ImGui.WindowFlags_NoDocking() - }) - + ToolWindow.init(self, { + title = self.TITLE, + width = self.WIDTH, + height = self.HEIGHT, + window_flags = 0 | ImGui.WindowFlags_AlwaysAutoResize() | ImGui.WindowFlags_NoCollapse() | + ImGui.WindowFlags_NoDocking() + }) self.export_formats = TranscriptExporterFormats.new {TranscriptExportFormat.exporter_json(), TranscriptExportFormat.exporter_srt(), @@ -212,7 +209,7 @@ function TranscriptExportFormat.options_json(options) options.json_widgets = { object_per_segment = ReaSpeechCheckbox.new { state = storage:boolean('object_per_segment', false), - label_long = 'One Object per Transcript Segment', -- Changed from label to label_long + label_long = 'One Object per Transcript Segment', -- Changed from label to label_long help_text = [[Each transcript segment is exported a separate JSON object.]] } } @@ -386,22 +383,20 @@ function TranscriptExportFormat.options_vtt(options) text_direction = ReaSpeechCombo.new { state = storage:string('text_direction', 'Horizontal'), label = 'Text Direction', - -- widget_id = 'vtt_text_direction', help_text = [[Horizontal: default text, for languages like Japanese that can be written vertically: - • Right to Left - Vertical text written right-to-left - • Left to Right - Vertical text written left-to-right]], + Right to Left - Vertical text written right-to-left + Left to Right - Vertical text written left-to-right]], items = {'Horizontal', 'Right to Left', 'Left to Right'} }, line = ReaSpeechTextInput.new { state = storage:string('line', ''), label = 'Line Position', - -- widget_id = 'vtt_line', help_text = [[line position of captions: - • Numbers: Place on specific line + Numbers: Place on specific line - Positive (1,2,3...) counts from top - Negative (-1,-2,-3...) counts from bottom - • Percentages: Position relative to video height + Percentages: Position relative to video height - 0% = top - 100% = bottom]] }, @@ -409,27 +404,19 @@ function TranscriptExportFormat.options_vtt(options) position = ReaSpeechTextInput.new { state = storage:string('position', ''), label = 'Position %', - -- widget_id = 'vtt_position', help_text = [[Sets horizontal position: 0% = left edge, 50% = center, 100% = right edge]] }, size = ReaSpeechTextInput.new { state = storage:string('size', ''), label = 'Size %', - -- widget_id = 'vtt_size', help_text = [[Sets caption box width (0-100% of video width), Default auto-sizes to fit text]] }, align = ReaSpeechCombo.new { state = storage:string('align', 'Default'), label = 'Alignment', - -- widget_id = 'vtt_align', - help_text = [[• Start - Aligns to start of text direction - (left for horizontal, top for vertical) -• Middle - Centers text (default) -• End - Aligns to end of text direction - (right for horizontal, bottom for vertical)]], - items = {'Default', 'Start', 'Middle', 'End'} + items = {'Start', 'Center', 'End', 'Left', 'Right'} } } end @@ -450,8 +437,9 @@ function TranscriptExportFormat.options_vtt(options) options.vtt_widgets.align:render() ImGui.Spacing(ctx) - options.vertical = options.vtt_widgets.text_direction:value() == 'Horizontal' and nil or - options.vtt_widgets.text_direction:value() == 'Right to Left' and 'rl' or 'lr' + local direction = options.vtt_widgets.text_direction:value() + options.vertical = direction == 'Right to Left' and 'rl' or direction == 'Left to Right' and 'lr' or nil -- Horizontal case + options.line = options.vtt_widgets.line:value() options.position = options.vtt_widgets.position:value() options.size = options.vtt_widgets.size:value() From ae6c7c22a7d9f95cc2c40321097eb11c86f0fa2d Mon Sep 17 00:00:00 2001 From: j <971121+smrl@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:20:30 -0600 Subject: [PATCH 4/4] added sanitization for some VTT input fields --- .../ReaSpeech/source/TranscriptExporter.lua | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/reascripts/ReaSpeech/source/TranscriptExporter.lua b/reascripts/ReaSpeech/source/TranscriptExporter.lua index dd21aaf8..37658203 100644 --- a/reascripts/ReaSpeech/source/TranscriptExporter.lua +++ b/reascripts/ReaSpeech/source/TranscriptExporter.lua @@ -241,6 +241,16 @@ function TranscriptExportFormat.strip_non_numeric(value) return value:gsub("[^0-9]", ""):gsub("^0+", "") end +function TranscriptExportFormat.strip_non_numeric_percent(value) + -- Remove any % sign from the end + value = value:gsub("%%$", "") + -- Keep only numbers, decimal points + value = value:gsub("[^0-9%.]", "") + -- Remove leading zeros (but keep single zero) + value = value:gsub("^0+(%d)", "%1") + return value +end + function TranscriptExportFormat.options_srt(options) if not options.srt_widgets then local storage = Storage.ExtState.make { @@ -383,7 +393,8 @@ function TranscriptExportFormat.options_vtt(options) text_direction = ReaSpeechCombo.new { state = storage:string('text_direction', 'Horizontal'), label = 'Text Direction', - help_text = [[Horizontal: default text, for languages like Japanese that can be written vertically: + help_text = [[Horizontal: default text +for languages like Japanese that can be written vertically: Right to Left - Vertical text written right-to-left Left to Right - Vertical text written left-to-right]], items = {'Horizontal', 'Right to Left', 'Left to Right'} @@ -403,19 +414,19 @@ function TranscriptExportFormat.options_vtt(options) position = ReaSpeechTextInput.new { state = storage:string('position', ''), - label = 'Position %', + label = 'Horizontal Position %', help_text = [[Sets horizontal position: 0% = left edge, 50% = center, 100% = right edge]] }, size = ReaSpeechTextInput.new { state = storage:string('size', ''), - label = 'Size %', + label = 'Caption Box Size %', help_text = [[Sets caption box width (0-100% of video width), Default auto-sizes to fit text]] }, align = ReaSpeechCombo.new { state = storage:string('align', 'Default'), - label = 'Alignment', + label = 'Text Alignment', items = {'Start', 'Center', 'End', 'Left', 'Right'} } } @@ -441,8 +452,8 @@ function TranscriptExportFormat.options_vtt(options) options.vertical = direction == 'Right to Left' and 'rl' or direction == 'Left to Right' and 'lr' or nil -- Horizontal case options.line = options.vtt_widgets.line:value() - options.position = options.vtt_widgets.position:value() - options.size = options.vtt_widgets.size:value() + options.position = TranscriptExportFormat.strip_non_numeric_percent(options.vtt_widgets.position:value()) + options.size = TranscriptExportFormat.strip_non_numeric_percent(options.vtt_widgets.size:value()) options.align = options.vtt_widgets.align:value() ~= 'Default' and string.lower(options.vtt_widgets.align:value()) or nil end)