-- See LICENSE for more info about your rights as a person to be -- rightfully persecuted export script_name = "Aegisub-Motion" export script_description = "A set of tools for simplifying the process of creating and applying motion tracking data with Aegisub." export script_author = "torque" export script_version = "1.0.9" export script_namespace = "a-mo.Aegisub-Motion" local interface, setProgress, setTask local versionRecord, clipboard, json, ConfigHandler, DataWrapper local LineCollection, log, Math, MotionHandler, Statistics, TrimHandler, Tags haveDepCtrl, DependencyControl = pcall require, "l0.DependencyControl" if haveDepCtrl versionRecord = DependencyControl { url: 'https://github.com/TypesettingTools/Aegisub-Motion' feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json' { 'aegisub.clipboard' 'json' { 'a-mo.ConfigHandler', version: '1.1.4' } { 'a-mo.DataWrapper', version: '1.0.2' } { 'a-mo.LineCollection', version: '1.3.0' } { 'a-mo.Log' , version: '1.0.0' } { 'a-mo.Math' , version: '1.0.0' } { 'a-mo.MotionHandler', version: '1.1.8' } { 'a-mo.Statistics' , version: '0.1.3' } { 'a-mo.TrimHandler', version: '1.0.5' } { 'a-mo.Tags', version: '1.3.4' } } } clipboard, json, ConfigHandler, DataWrapper, LineCollection, log, Math, MotionHandler, Statistics, TrimHandler, Tags = versionRecord\requireModules! else clipboard = require 'aegisub.clipboard' json = require 'json' ConfigHandler = require 'a-mo.ConfigHandler' DataWrapper = require 'a-mo.DataWrapper' LineCollection = require 'a-mo.LineCollection' log = require 'a-mo.Log' Math = require 'a-mo.Math' MotionHandler = require 'a-mo.MotionHandler' Statistics = require 'a-mo.Statistics' TrimHandler = require 'a-mo.TrimHandler' Tags = require 'a-mo.Tags' statsTemplate = { apply: { longestTrack: 0 lines: { largestInput: 0, largestOutput: 0, totalOutput: 0 } bytes: { largestInput: 0, largestOutput: 0, totalOutput: 0 } runCount: 0 } trim: { runCount: 0 clipsCreated: 0 } trimEach: { runCount: 0 } revert: { lines: { total: 0 } bytes: { total: 0 } runCount: 0 } uuid: 0 } initStats = -> stats = Statistics statsTemplate, "aegisub-motion.stats.json" if 0 == stats\getValue "uuid" stats\setValue "uuid", Math.uuid! stats\write! return stats initializeInterface = -> -- Set up interface tables. interface = { main: { -- mnemonics: xyOCSBuRWen + G\A + Wl\A dataLabel: { class: "label", x: 0, y: 0, width: 10, height: 1, label: " Paste data or enter a filepath." } data: { class: "textbox", x: 0, y: 1, width: 10, height: 4, hint: "Paste data or the path to a file containing it. No quotes or escapes." } -- optLabel: { class: "label", x: 0, y: 5, width: 5, height: 1, label: "Data to be applied:" } xPosition: { class: "checkbox", x: 0, y: 5, width: 1, height: 1, config: true, label: "&x", value: true, hint: "Apply x position data to the selected lines." } yPosition: { class: "checkbox", x: 1, y: 5, width: 1, height: 1, config: true, label: "&y", value: true, hint: "Apply y position data to the selected lines." } origin: { class: "checkbox", x: 2, y: 5, width: 2, height: 1, config: true, label: "&Origin", value: false, hint: "Move the origin along with the position." } absPos: { class: "checkbox", x: 4, y: 5, width: 2, height: 1, config: true, label: "Absolut&e", value: false, hint: "Set position to exactly that of the tracking data with no processing." } xScale: { class: "checkbox", x: 0, y: 6, width: 2, height: 1, config: true, label: "&Scale", value: true, hint: "Apply scaling data to the selected lines." } border: { class: "checkbox", x: 2, y: 6, width: 2, height: 1, config: true, label: "&Border", value: true, hint: "Scale border with the line (only if Scale is also selected)." } shadow: { class: "checkbox", x: 4, y: 6, width: 2, height: 1, config: true, label: "&Shadow", value: true, hint: "Scale shadow with the line (only if Scale is also selected)." } blur: { class: "checkbox", x: 4, y: 7, width: 2, height: 1, config: true, label: "Bl&ur", value: true, hint: "Scale blur with the line (only if Scale is also selected, does not scale \\be)." } blurScale: { class:"floatedit", x: 7, y: 7, width: 2, height: 1, config: true, step: 0.01, value: 1, hint: "Factor to attenuate (or amplify) blur values by." } zRotation: { class: "checkbox", x: 0, y: 7, width: 3, height: 1, config: true, label: "&Rotation", value: false, hint: "Apply rotation data to the selected lines." } writeConf: { class: "checkbox", x: 0, y: 10, width: 4, height: 1, config: true, label: "&Write config", value: true, hint: "Write current settings to the configuration file." } relative: { class: "checkbox", x: 4, y: 10, width: 3, height: 1, config: true, label: "Relat&ive", value: true, hint: "Start frame should be relative to the beginning of the selection rather than the beginning of the video." } startFrame:{ class: "intedit", x: 7, y: 10, width: 2, height: 1, config: true, value: 1, hint: "Frame used as the starting point for the tracking data. \"-1\" corresponds to the last frame." } linear: { class: "checkbox", x: 4, y: 11, width: 2, height: 1, config: true, label: "Li&near", value: false, hint: "Use transforms and \\move to create a linear transition, instead of frame-by-frame." } clipOnly: { class: "checkbox", x: 0, y: 11, width: 3, height: 1, config: true, label: "&Clip Only", value: false, hint: "Only apply the main data to \\clips present in the line." } rectClip: { class: "checkbox", x: 0, y: 8, width: 3, height: 1, config: true, label: "Rect C&lip", value: true, hint: "Apply tracking data to the rectangular clip contained in the line." } vectClip: { class: "checkbox", x: 3, y: 8, width: 3, height: 1, config: true, label: "&Vect Clip", value: true, hint: "Apply tracking data to the vector clip contained in the line." } rcToVc: { class: "checkbox", x: 6, y: 8, width: 4, height: 1, config: true, label: "Rect -> Vect", value: false, hint: "Convert rectangular clips contained in the line to vector clips." } killTrans: { class: "checkbox", x: 0, y: 9, width: 10, height: 1, config: true, label: "Interpolate &transforms", value: true, hint: "Attempt to interpolate transform value instead of just shifting transform times." } } clip: { -- mnemonics: xySRe + GCA dataLabel: { class: "label", x: 0, y: 0, width: 10, height: 1, label: " This stuff is for clips." } data: { class: "textbox", x: 0, y: 1, width: 10, height: 4, name: "data", hint: "Paste data or the path to a file containing it. No quotes or escapes." } optLabel: { class: "label", x: 0, y: 5, width: 5, height: 1, label: "Data to be applied:" } xPosition: { class: "checkbox", x: 0, y: 6, width: 1, height: 1, config: true, label: "&x", value: true, hint: "Apply x position data to the selected clips in the selected lines." } yPosition: { class: "checkbox", x: 1, y: 6, width: 1, height: 1, config: true, label: "&y", value: true, hint: "Apply y position data to the selected clips in the selected lines." } xScale: { class: "checkbox", x: 0, y: 7, width: 2, height: 1, config: true, label: "&Scale", value: true, hint: "Apply scaling data to the selected clips in the selected lines." } zRotation: { class: "checkbox", x: 0, y: 8, width: 3, height: 1, config: true, label: "&Rotation", value: false, hint: "Apply rotation data to the selected clips in the selected lines." } rectClip: { class: "checkbox", x: 0, y: 10, width: 3, height: 1, config: true, label: "Rect C&lip", value: true, hint: "Apply tracking data to the rectangular clip contained in the line." } vectClip: { class: "checkbox", x: 3, y: 10, width: 3, height: 1, config: true, label: "&Vect Clip", value: true, hint: "Apply tracking data to the vector clip contained in the line." } rcToVc: { class: "checkbox", x: 6, y: 10, width: 4, height: 1, config: true, label: "Rect -> Vect", value: false, hint: "Convert rectangular clips contained in the line to vector clips." } startLabel:{ class: "label", x: 7, y: 5, width: 3, height: 1, label: "Start Frame:" } startFrame:{ class: "intedit", x: 7, y: 6, width: 3, height: 1, config: true, value: 1, hint: "Frame used as the starting point for the tracking data. \"-1\" corresponds to the last frame." } } trim: { pLabel: { class: "label", x: 0, y: 0, width: 10, height: 1, label: [[ Prefix the encoded video is written. Useful values are ?video for the directory of the currently loaded video and ?script for the directory of the currently open script. Can be a hardcoded path, too.]] } psLabel: { class: "label", x: 0, y: 3, width: 10, height: 1, label: [[ Encoding preset. Different presets may have different output.]] } eLabel: { class: "label", x: 0, y: 5, width: 10, height: 1, label: [[ The currently selected encoding binary. Use the "Encoder..." button below to set this. Manual edits will not be saved.]] } cLabel: { class: "label", x: 0, y: 7, width: 10, height: 1, label: [[ If you want to use a custom encoding command, write it here. Leave this blank to use the built-in presets (probably what you want).]] } prefix: { config: true, value: "?video", class: "textbox", x: 0, y: 1, width: 10, height: 1, hint: "Prefix the encoded video is written. Useful values are ?video for the directory of the currently loaded video and ?script for the directory of the currently open script." } makePfix: { config: true, value: true, class: "checkbox", x: 0, y: 2, width: 10, height: 1, label: "Try to create prefix directory.", hint: "Try to create prefix directory." } preset: { config: true, value: "x264", class: "dropdown", x: 0, y: 4, width: 10, height: 1, label: "Sort lines by", items: TrimHandler.existingPresets, hint: "Choose an existing preset by name." } encBin: { config: true, value: "", class: "textbox", x: 0, y: 6, width: 10, height: 1, hint: "The full path to your encoding binary (x264.exe if you're using the default preset)" } command: { config: true, value: "", class: "textbox", x: 0, y: 8, width: 10, height: 4, hint: "If you want to use a custom encoding command, write it here." } } } if TrimHandler.windows interface.trim.writeLog = { config: true, value: true, class: "checkbox", x: 0, y: 12, width: 10, height: 1, label: "Write encode log.", hint: "Write encode log. Allows aegisub-motion to print information if an encode fails. Only matters on Windows." } interface fetchDataFromClipboard = -> -- According to vague reports, Aegisub clipboard usage crashes it on -- Linux. Disable any clipboard use until I find a better way to -- handle this. dataString = (jit.os != "Linux") and clipboard.get! -- If there's nothing on the clipboard, clipboard.get returns nil. return dataString or "" prepareConfig = ( config, mainData, clipData, lineCollection ) -> rectClipData, vectClipData = nil, nil totalFrames = lineCollection.totalFrames for field, data in pairs { main: mainData, clip: clipData } configField = config[field] if '' == configField.data or nil == configField.data for option in pairs configField -- Nuke everything because it doesn't matter at this point. configField[option] = false else -- Be extremely lazy and just re-parse data from scratch. unless data\bestEffortParsingAttempt configField.data, lineCollection.meta.PlayResX, lineCollection.meta.PlayResY log.windowError "You put something in the #{field} data box\nbut it is wrong in ways I can't imagine." unless data.dataObject\checkLength totalFrames log.windowError "The length of your #{field} data (#{data.dataObject.length} frames) doesn't match\nthe length of your lines (#{totalFrames} frames) and I quit." if configField.rcToVc configField.rectClip = true configField.vectClip = true if configField.rectClip rectClipData = data if configField.vectClip vectClipData = data unless config.main.data or config.clip.data log.windowError "As far as I can tell, you've forgotten to give me any motion data." -- Disable options that depend on scale. unless config.main.xScale config.main.border = false config.main.shadow = false config.main.blur = false -- Nudge the start frames. if config.main.relative for context in *{ 'main', 'clip' } -- Have to check that the field exists (if the clip dialog wasn't -- opened it will be nil) to avoid comparison with nil errors. if config[context].startFrame if config[context].startFrame == 0 config[context].startFrame = 1 elseif config[context].startFrame < 0 config[context].startFrame = totalFrames + config[context].startFrame + 1 else for context in *{ 'main', 'clip' } if config[context].startFrame config[context].startFrame = config[context].startFrame - lineCollection.startFrame + 1 if config[context].startFrame <= 0 log.windowError "You have specified an out-of-range absolute\nstart frame and you have been judged." return rectClipData, vectClipData -- Used in getMissingTags, for fade handling when killTrans is used getMissingAlphas = ( block, properties ) -> -- makes sure every necessary alpha tag exists in the first block alpha = Tags.allTags.alpha alpha1, alpha2, alpha3, alpha4 = Tags.allTags.alpha1, Tags.allTags.alpha2, Tags.allTags.alpha3, Tags.allTags.alpha4 outline, shadow = Tags.allTags.border, Tags.allTags.shadow if block\find alpha.pattern return '' if ( properties[alpha1] == 0 and properties[alpha2] == 0 and properties[alpha3] == 0 and properties[alpha4] == 0 ) return alpha\format 0 alphas = { } if not block\find alpha1.pattern table.insert alphas, alpha1\format properties[alpha1] if ( not block\find alpha2.pattern ) and ( block\find Tags.allTags.karaoke.pattern ) table.insert alphas, alpha2\format properties[alpha2] if ( not block\find alpha3.pattern ) and ( ( block\find "\\[xy]?bord([%d%.]+)" ) or ( properties[outline] > 0 ) ) table.insert alphas, alpha3\format properties[alpha3] if ( not block\find alpha4.pattern ) and ( ( block\find "\\[xy]?shad([%d%.]+)" ) or ( properties[shadow] > 0 ) ) table.insert alphas, alpha4\format properties[alpha4] return table.concat alphas, '' -- This table is used to verify that style defaults are inserted at -- the beginning the selected line(s) if the corresponding options are -- selected. The structure is: tag = { opt:"opt", skip:val } where -- "opt" is the option that must be enabled and skip specifies -- not to write the tag if the style default is that value. importantTags = { xscale: { opt: "xScale", skip: 0 } yscale: { opt: "xScale", skip: 0 } border: { opt: "border", skip: 0 } shadow: { opt: "shadow", skip: 0 } zrot: { opt: "zRotation" } } -- A style table is passed to this function so that it can cope with -- \r. getMissingTags = ( block, options, properties ) -> result = { } for key, tab in pairs importantTags tag = Tags.allTags[key] if options[tab.opt] if not block\match tag.pattern if properties[tag] != tab.skip table.insert result, tag\format properties[tag] if options.killTrans table.insert result, getMissingAlphas block, properties return table.concat result, '' rectClipToVectClip = ( clip ) -> if clip\match "[%-%d%.]+, *[%-%d%.]+" clip = clip\gsub "([%-%d%.]+), *([%-%d%.]+), *([%-%d%.]+), *([%-%d%.]+)", ( l, t, r, b ) -> return table.concat ({ "m %g %g "\format l, t "l %g %g "\format r, t "%g %g "\format r, b "%g %g"\format l, b }) return clip convertClipToFP = ( clip ) -> -- only muck around with vector clips (convert scaling factor into floating point coordinates). unless clip\match "[%-%d%.]+, *[%-%d%.]+" -- Convert clip with scale into floating point coordinates. clip = clip\gsub "%((%d*),?(.-)%)", ( scaleFactor, points ) -> if scaleFactor ~= "" scaleFactor = tonumber scaleFactor points = points\gsub "([%.%d%-]+) ([%.%d%-]+)", ( x, y ) -> x = Math.round tonumber( x )/(2^(scaleFactor - 1)), 2 y = Math.round tonumber( y )/(2^(scaleFactor - 1)), 2 ("%g %g")\format x, y return '(' .. points .. ')' return clip prepareLines = ( lineCollection ) -> setProgress 0 totalLines = #lineCollection.lines options = lineCollection.options -- remove the lines while ensuring new lines will be inserted in the -- correct place. lineCollection\deleteLines! -- Perform all of the manipulation that used to be performed in -- Line.moon but are actually fairly Aegisub-Motion specific. lineCollection\runCallback ( line, index ) => log.checkCancellation! -- If a line already contains a-mo extradata, it is probably being -- tracked again after being tracked. There are some reasons to do -- this, but it results in a huge amount of extradata garbage if -- unchecked. Presumably 99% of tracking that isn't this case will -- be on lines without extradata. This isn't a perfect solution, but -- it is probably better than before. This doesn't change -- extradata's lack of resilience toward copying lines between -- scripts. if line\getExtraData 'a-mo' line.retrack = true else -- Add our signature extradata. line\setExtraData 'a-mo', { originalText: line.text, uuid: Math.uuid! } -- Get default style properties (will be used later if transform -- interpolation is enabled) line\getPropertiesFromStyle! -- need to brutalize fades before tokenizing transforms so that the -- produced transforms will be tokenized as well. Alternately (this -- may be more clean but also less efficient), tokenize transforms, -- dedup tags, detokenize transforms, brutalize fades, and then -- retokenize transforms. line\tokenizeTransforms! line\runCallbackOnOverrides ( tagBlock ) => return tagBlock\gsub "\\fade?%((%d+),(%d+)%)", ( start, finish ) -> return "\\fade(255,0,255,0,%d,%d,%d)"\format start, @duration - finish, @duration -- retokenize the transforms to simplify later processing. line\dontTouchTransforms! line\tokenizeTransforms! -- Deduplicate all override tags. line\deduplicateTags! -- Collect alignment and position info for each line. styles = @styles lineStyle = line.styleRef unless line\extraMetrics lineStyle -- unless line\moveToPosition math.floor(0.5*(aegisub.ms_from_frame(lineCollection.startFrame + options.main.startFrame - 1) + aegisub.ms_from_frame(lineCollection.startFrame + options.main.startFrame))) - line.start_time line\ensureLeadingOverrideBlockExists! -- Note that we are repeatedly shadowing @, so in this function it -- refers to the line. This is interestingly the opposite of how -- fat arrow functions work in coffeescript. line\runCallbackOnFirstOverride ( tagBlock ) => return tagBlock\gsub "{", ("{\\pos(%g,%g)")\format @xPosition, @yPosition -- Add any tags we need that are missing from the line. line\runCallbackOnFirstOverride ( tagBlock ) => tags = getMissingTags tagBlock, options.main, line.properties return '{' .. tags .. tagBlock\sub( 2 ) line\runCallbackOnOverrides ( tagBlock ) => tagBlock\gsub "\\org%([%.%d%-]+,[%.%d%-]+%)", -> @hasOrg = true return nil savestyle = line.styleRef reset = false tagBlock = tagBlock\gsub "\\r([^\\}]*)([^}]*)", ( resetStyle, remainder ) -> if styles[resetStyle] line.styleRef = styles[resetStyle] line\getPropertiesFromStyle! reset = true tags = getMissingTags remainder, options.main, line.properties return '\\r' .. resetStyle .. tags .. remainder if reset line.styleRef = savestyle line\getPropertiesFromStyle! if options.main.rectClip or options.main.vectClip or options.clip.rectClip or options.clip.vectClip tagBlock = tagBlock\gsub "(\\i?clip%b())", ( clip ) -> @hasClip = true clip = convertClipToFP clip if options.main.rcToVc or options.clip.rcToVc clip = rectClipToVectClip clip return clip return tagBlock unless line.hasClip line\runCallbackOnFirstOverride ( tagBlock ) => "{\\clip()" .. tagBlock\sub 2 setProgress index/totalLines*100 postprocLines = ( lineCollection ) -> setProgress 0 totalLines = #lineCollection.lines lineCollection\runCallback ( line, index ) => if line.wasLinear line\dontTouchTransforms! else line\deduplicateTags! line\shiftKaraoke! line.text = line.text\gsub "{}", "" setProgress index/totalLines*100 log.checkCancellation! -- No progress for this. lineCollection\combineIdenticalLines! applyProcessor = ( subtitles, selectedLines ) -> setTask = aegisub.progress.task setProgress = aegisub.progress.set setTask "Loading Interface" initializeInterface! math.randomseed tonumber tostring( os.time! )\reverse!\sub( 1, 8 ) -- Initialize the configuration options = ConfigHandler interface, "aegisub-motion.json", true, script_version options\read! options\updateInterface { "main", "clip" } stats = initStats! lineCollection = LineCollection subtitles, selectedLines currentVideoFrame = aegisub.project_properties!.video_position rawInputData = fetchDataFromClipboard! -- Instantiate both of these so they can be passed by reference later. setTask "Checking Clipboard for Data" mainData = DataWrapper! clipData = DataWrapper! if mainData\bestEffortParsingAttempt rawInputData, lineCollection.meta.PlayResX, lineCollection.meta.PlayResY if mainData.dataObject\checkLength lineCollection.totalFrames interface.main.data.value = rawInputData interface.main.dataLabel.label = " Data is the correct length." else interface.main.dataLabel.label = "Data had the wrong framecount. Expected: #{lineCollection.totalFrames}. Got: #{mainData.dataObject.length}" if options.configuration.main.relative relativeFrame = currentVideoFrame - lineCollection.startFrame + 1 if relativeFrame > 0 and relativeFrame <= lineCollection.totalFrames interface.main.startFrame.value = relativeFrame interface.clip.startFrame.value = relativeFrame else interface.main.startFrame.value = currentVideoFrame interface.clip.startFrame.value = currentVideoFrame setTask "Launching Interface" -- cancel:Abort in the main dialog tells Esc key to abort the entire macro -- cancel:Back in \clip dialog tells Esc key to close it and go back to the main dialog buttons = { main: { list: { "&Go" "Track &\\clip separately" "&Quit" } namedList: { ok: "&Go" clip: "Track &\\clip separately" cancel: "&Quit" } } clip: { list: { "&Go" "&Back" "&Quit" } namedList: { ok: "&Go" close: "&Back" abort: "&Quit" } } } currentDialog = "main" config = { clip: { }, main: { } } while true button, config[currentDialog] = aegisub.dialog.display interface[currentDialog], buttons[currentDialog].list, buttons[currentDialog].namedList for k, v in pairs config[currentDialog] interface[currentDialog][k].value = v switch button when buttons.main.namedList.clip currentDialog = "clip" when false, buttons.clip.namedList.abort setTask "ABORT" aegisub.cancel! when buttons.clip.namedList.close currentDialog = "main" else break -- Update the persistent configuration before it gets (potentially) -- horribly mutilated in prepareConfig. Ensures that what the user saw -- last is what will be presented to them next time. if config.main.writeConf setTask "Updating Configuration" options\updateConfiguration config, { "main", "clip" } if config.main.writeConf or (options.configuration.main.writeConf != config.main.writeConf) options.configuration.main.writeConf = config.main.writeConf options\write! setTask "Preparing Configuration and Data" rectClipData, vectClipData = prepareConfig config, mainData, clipData, lineCollection lineCollection.options = config setTask "Preprocessing Lines" prepareLines lineCollection stats\setMax "apply.longestTrack", lineCollection.totalFrames stats\setMax "apply.lines.largestInput", #lineCollection.lines for line in *lineCollection.lines stats\setMax "apply.bytes.largestInput", #line.text if mainData.type and 'SRS' != mainData.type mainData.dataObject\addReferenceFrame config.main.startFrame mainData.dataObject\stripFields config.main if clipData.type and 'SRS' != clipData.type clipData.dataObject\addReferenceFrame config.clip.startFrame clipData.dataObject\stripFields config.clip setTask "Applying Data" motionHandler = MotionHandler lineCollection, mainData, rectClipData, vectClipData newLines = motionHandler\applyMotion! -- Postproc lines: detokenize transforms and combine identical lines. setTask "Postprocessing Lines" postprocLines newLines newLines\replaceLines! stats\setMax "apply.lines.largestOutput", #newLines.lines stats\incrementValue "apply.lines.totalOutput", #newLines.lines for line in *newLines.lines stats\setMax "apply.bytes.largestOutput", #line.text stats\incrementValue "apply.bytes.totalOutput", #line.text stats\incrementValue "apply.runCount" stats\write! trimConfigDialog = ( options ) -> buttons = { { "&Save", "&Encoder...", "&Cancel" }, { ok: "&Save", enc: "&Encoder...", cancel: "&Cancel" } } while true options\updateInterface "trim" button, config = aegisub.dialog.display interface.trim, buttons[1], buttons[2] if button == buttons[2].ok or button == buttons[2].enc -- only update encBin when the open dialog is shown. config.encBin = nil options\updateConfiguration config, "trim" if button == buttons[2].ok options\write! break else encoder = aegisub.dialog.open "Choose an Encoding Binary", "", "", "", false, true if encoder options\updateConfiguration { encBin: encoder }, "trim" else aegisub.cancel! trimConfigurator = -> initializeInterface! options = ConfigHandler interface, "aegisub-motion.json", true, script_version options\read! trimConfigDialog options trimProcessor = ( subtitles, selectedLines, activeLine, eachFlag ) -> setTask = aegisub.progress.task setProgress = aegisub.progress.set initializeInterface! options = ConfigHandler interface, "aegisub-motion.json", true, script_version stats = initStats! options\read! -- Check if encBin has been set. if options.configuration.trim.encBin == "" interface.trim.pLabel.label = [[ You must specify the path to your encoding binary. ]] .. interface.trim.pLabel.label trimConfigDialog options trim = TrimHandler options.configuration.trim if eachFlag seenRanges = { } for lineIndex in *selectedLines lineCollection = LineCollection subtitles, { lineIndex } collectionRange = "#{lineCollection.startFrame}-#{lineCollection.endFrame}" unless seenRanges[collectionRange] seenRanges[collectionRange] = true trim\calculateTrimLength lineCollection trim\performTrim! stats\incrementValue "trim.clipsCreated" stats\incrementValue "trimEach.runCount" else lineCollection = LineCollection subtitles, selectedLines trim\calculateTrimLength lineCollection trim\performTrim! stats\incrementValue "trim.clipsCreated" stats\incrementValue "trim.runCount" stats\write! return selectedLines trimProcessorEach = ( subtitles, selectedLines ) -> trimProcessor subtitles, selectedLines, nil, true revertProcessor = ( subtitles, selectedLines ) -> setTask = aegisub.progress.task setProgress = aegisub.progress.set stats = initStats! setTask "Collecting UUIDs" -- A table of all UUIDs found in the selected lines uuids = { } -- Indices of lines that need to be removed later (are part of a -- tracked line collection, but are not the first line in that -- collection) for index in *selectedLines with line = subtitles[index] if .extra['a-mo'] data = json.decode .extra['a-mo'] unless uuids[data.uuid] .text = data.originalText .number = index .extra = {} uuids[data.uuid] = line indicesToNuke = { } setTask "Gathering Matching Lines" totalLines = #subtitles for index = 1, totalLines with line = subtitles[index] if line.extra -- Catch lines containing our signature extradata. if .extra['a-mo'] -- Decode our data, which is stored as json. data = json.decode .extra['a-mo'] if uuids[data.uuid] oldLine = uuids[data.uuid] -- Check if we should change the start time. if .start_time < oldLine.start_time oldLine.start_time = .start_time -- Check if we should change the end time. if .end_time > oldLine.end_time oldLine.end_time = .end_time -- Mark the line for deletion. if index < oldLine.number oldLine.number = index elseif index > oldLine.number stats\incrementValue "revert.bytes.total", #line.text indicesToNuke[#indicesToNuke+1] = index setProgress index/totalLines*100 setTask "Replacing Lines" -- Replace the lines. for _, line in pairs uuids subtitles[line.number] = line -- Delete the remainders. subtitles.delete indicesToNuke stats\incrementValue "revert.lines.total", #indicesToNuke stats\incrementValue "revert.runCount" stats\write! return nil canRun = ( sub, selectedLines ) -> if not aegisub.frame_from_ms 0 return false, "You must have a video loaded to run this macro." elseif 0 == #selectedLines return false, "You must have lines selected to use this macro." true if haveDepCtrl versionRecord\registerMacros { { "Apply", "Applies properly formatted motion tracking data to selected subtitles.", applyProcessor, canRun } { "Revert", "Removes properly formatted motion tracking data from selected subtitles.", revertProcessor } { "Trim", "Cuts and encodes the current scene for use with motion tracking software.", trimProcessor, canRun } { "Trim Each", "Cuts and encodes selected scenes for use with motion tracking software.", trimProcessorEach, canRun } { "Trim Settings", "Opens a gui to configure the trim tool.", trimConfigurator } } else aegisub.register_macro "#{script_name}/Apply", "Applies properly formatted motion tracking data to selected subtitles.", applyProcessor, canRun aegisub.register_macro "#{script_name}/Revert", "Removes properly formatted motion tracking data from selected subtitles.", revertProcessor aegisub.register_macro "#{script_name}/Trim", "Cuts and encodes the current scene for use with motion tracking software.", trimProcessor, canRun aegisub.register_macro "#{script_name}/Trim Each", "Cuts and encodes selected scenes for use with motion tracking software.", trimProcessorEach, canRun aegisub.register_macro "#{script_name}/Trim Settings", "Opens a gui to configure the trim tool.", trimConfigurator