Skip to content

Instantly share code, notes, and snippets.

@PhosCity
Last active September 20, 2024 05:15
Show Gist options
  • Select an option

  • Save PhosCity/a79fbc6d5d8bd8625eb52c48b1a1ff6c to your computer and use it in GitHub Desktop.

Select an option

Save PhosCity/a79fbc6d5d8bd8625eb52c48b1a1ff6c to your computer and use it in GitHub Desktop.
Modification of original ASSWipe to fix some issues
export script_name = "ASSWipe"
export script_description = "Performs script cleanup, removes unnecessary tags and lines."
export script_version = "0.5.0"
export script_author = "line0"
export script_namespace = "l0.ASSWipe"
DependencyControl = require "l0.DependencyControl"
version = DependencyControl{
feed: "https://raw.githubusercontent.com/TypesettingTools/line0-Aegisub-Scripts/master/DependencyControl.json",
{
{"a-mo.LineCollection", version: "1.3.0", url: "https://github.com/TypesettingTools/Aegisub-Motion",
feed: "https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json"},
{"a-mo.ConfigHandler", version: "1.1.4", url: "https://github.com/TypesettingTools/Aegisub-Motion",
feed: "https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json"},
{"l0.ASSFoundation", version: "0.5.0", url: "https://github.com/TypesettingTools/ASSFoundation",
feed: "https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json"},
{"l0.Functional", version: "0.5.0", url: "https://github.com/TypesettingTools/Functional",
feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"},
{"SubInspector.Inspector", version: "0.7.2", url: "https://github.com/TypesettingTools/SubInspector",
feed: "https://raw.githubusercontent.com/TypesettingTools/SubInspector/master/DependencyControl.json"},
"json"
}
}
LineCollection, ConfigHandler, ASS, Functional, SubInspector, json = version\requireModules!
ffms, min, concat, sort = aegisub.frame_from_ms, math.min, table.concat, table.sort
{:list, :math, :string, :table, :unicode, :util, :re } = Functional
logger = version\getLogger!
reportMsg = [[
Done. Processed %d lines in %d seconds.
— Cleaned %d lines (%d%%)
— Removed %d invisible lines (%d%%)
— Combined %d consecutive identical lines (%d%%)
— Filtered %d clips and %d occurences of junk data
— Purged %d invisible contours (%d in drawings, %d in clips)
— Failed to purge %d invisible contours due to rendering inconsistencies
— Converted %d drawings/clips to floating-point
— Filtered %d records of extra data
— Total space saved: %.2f KB
]]
hints = {
cleanLevel: [[
0: no cleaning
1: remove empty tag sections
2: deduplicate tags inside sections
3: deduplicate tags globally,
4: remove tags matching the style defaults and otherwise ineffective tags]]
tagsToKeep: "Don't remove these tags even if they match the style defaults for the line."
filterClips: "Removes clips that don't affect the rendered output."
removeInvisible: "Deletes lines that don't generate any visible output."
combineLines: "Merges non-animated lines that render to an identical result and have consecutive times (without overlaps or gaps)."
removeJunk: "Removes any 'in-line comments' and things not starting with a \\ from tag sections."
removeUnknown: "Removes any unknown tags from the line"
scale2float: "Converts drawings and clips with a scale parameter to a floating-point representation."
tagSortOrder: "Determines the order cleaned tags will be ordered inside a tag section. Resets always go first, transforms last."
fixDrawings: "Removes extraneous ordinates from broken drawings to make them parseable. May or may not changed the rendered output."
purgeContoursDraw: "Removes all contours of a drawing that are not visible on the canvas."
purgeContoursClip: "Removes all contours of a clip that do not affect the appearance of the line."
stripComments: "Removes any comments encapsulated in {curly brackets}."
purgeContoursIgnoreHashMismatch: "Removes invisible contours even if it causes a SubInspector hash mismatch when comparing the result to the original line. This may or may not visually affect your drawing, so never use this option unsupervised!"
}
defaultSortOrder = [[
\an, \pos, \move, \org, \fscx, \fscy, \frz, \fry, \frx, \fax, \fay, \fn, \fs, \fsp, \b, \i, \u, \s, \bord, \xbord, \ybord,
\shad, \xshad, \yshad, \1c, \2c, \3c, \4c, \alpha, \1a, \2a, \3a, \4a, \blur, \be, \fad, \fade, clip_rect, iclip_rect,
clip_vect, iclip_vect, \q, \p, \k, \kf, \K, \ko, junk, unknown
]]
karaokeTags = table.concat table.pluck(table.filter(ASS.tagMap, (tag) -> tag.props.karaoke), "overrideName"), ", "
-- to be moved into ASSFoundation.Functional
sortWithKeys = (tbl, comparator) ->
-- shellsort written by Rici Lake
-- c/p from http://lua-users.org/wiki/LuaSorting, with index argument added to comparator
incs = { 1391376, 463792, 198768, 86961, 33936, 13776, 4592, 1968, 861, 336, 112, 48, 21, 7, 3, 1 }
n = #tbl
for h in *incs
for i = h+1, n
a = tbl[i]
for j = i-h, 1, -h
b = tbl[j]
break unless comparator a, b, i, j
tbl[i] = b
i = j
tbl[i] = a
return tbl
-- returns if we can merge line b into a while still maintain b's layer order
isMergeable = (a, b, linesByFrame) ->
for i = b.firstFrame, b.lastFrame
group = linesByFrame[i]
local pos
unless group.sorted
-- ensure line group is sorted by layer blending order
sortWithKeys group, (x, y, i, j) ->
pos or= i if x == b
return x.layer < y.layer or x.layer == y.layer and i < j
group.sorted = true
-- get line position in blending order
pos or= i for i, v in ipairs group when v == b
-- as b is merged into a, it gets a's layer number
-- so we can only merge if the new layer number does not change the blending order in any of the frames b is visible in
lower = group[pos-1]
return false unless (not lower or a.layer > lower.layer or a.layer == lower.layer and a.number > lower.number)
higher = group[pos+1]
return false unless (not higher or a.layer < higher.layer or a.layer == higher.layer and a.number < higher.number)
return true
mergeLines = (lines, start, cmbCnt, bytes, linesByFrame) ->
-- queue merged lines for deletion and collect statistics
if lines[start].merged
return lines[start], cmbCnt+1, bytes + #lines[start].raw + 1
-- merge applicable lines into first mergeable by extending its end time
-- then mark all merged lines
merged = lines[start]
for i=start+1,lines.n
line = lines[i]
break if line.merged or line.start_time != merged.end_time or not isMergeable merged, line, linesByFrame
lines[i].merged = true
merged.end_time = lines[i].end_time
-- update lines by frame index
for f = line.firstFrame, line.lastFrame
group = linesByFrame[f]
pos = i for i, v in ipairs group when v == line
group[pos] = merged
return nil, cmbCnt, bytes
removeInvisibleContoursOptCollectBounds = (contour, _, sectionContourIndex, sliceContourIndex, _, sliceRawContours, sliceSize, linePre, linePost, isAnimated, boundsBatch) ->
prevContours = concat sliceRawContours, " ", 1, sliceContourIndex-1
nextContours = sliceContourIndex <= sliceSize and concat(sliceRawContours, " ", sliceContourIndex+1) or ""
text = "#{linePre}#{prevContours}#{(#prevContours == 0 or #nextContours == 0) and "" or " "}#{nextContours}#{linePost}"
boundsBatch\add contour.parent.parent, text, sectionContourIndex, isAnimated
rawContour = sliceRawContours[sliceContourIndex]
removeInvisibleContoursOptPurge = (contour, contours, i, _, _, allBounds, orgBounds) ->
bounds, allBounds[i] = allBounds[i]
return false if orgBounds\equal bounds
removeInvisibleContoursOpt = (section, orgBounds) ->
cutOff, ass, contourCnt = false, section.parent, #section.contours
sliceSize = math.ceil 10e4 / contourCnt
if contourCnt > 100
logger\hint "Cleaning complex drawing with %d contours (slice size: %s)...", contourCnt, sliceSize
selectSurroundingSections = (sect, _, _, _, toTheLeft) ->
if toTheLeft
cutOff = true if section == sect
else
if section == sect
cutOff = false
return false
return not cutOff
lineStringPre, drwState = ass\getString nil, nil, selectSurroundingSections, false, true
lineStringPost = ass\getString nil, drwState, selectSurroundingSections, false, false
allBounds, sliceContours, sliceStartIndex = {}
isAnimated = ass\isAnimated!
for sliceStartIndex = 1, contourCnt, sliceSize
sliceEndIndex = min sliceStartIndex+sliceSize-1, contourCnt
sliceContours = [cnt\getTagParams! for cnt in *section.contours[sliceStartIndex, sliceEndIndex]]
boundsBatch = ASS.LineBoundsBatch!
section\callback removeInvisibleContoursOptCollectBounds, sliceStartIndex, sliceEndIndex, nil, nil,
sliceContours, sliceSize, lineStringPre, lineStringPost, isAnimated, boundsBatch
boundsBatch\run true, allBounds
lineStringPre ..= concat sliceContours, " "
boundsBatch = nil
collectgarbage!
_, purgeCnt = section\callback removeInvisibleContoursOptPurge, nil, nil, nil, nil, allBounds, orgBounds
return purgeCnt
stripComments = () -> false
process = (sub, sel, res) ->
ASS.config.fixDrawings = res.fixDrawings
lines = LineCollection sub, sel
linesToDelete, delCnt, linesToCombine, cmbCnt, lineCnt, debugError = {}, 0, {}, 0, #lines.lines, false
tagNames = res.filterClips and util.copy(ASS.tagNames.clips) or {}
tagNames[#tagNames+1] = res.removeJunk and "junk"
tagNames[#tagNames+1] = res.removeUnknown and "unknown"
stats = { bytes: 0, junk: 0, clips: 0, start: os.time!, cleaned: 0,
scale2float: 0, contoursDraw: 0, contoursDrawSkipped: 0, contoursClip: 0, extra: 0 }
linesByFrame = {}
-- create proper tag name lists from user input which may be override tag names or mixed
res.tagsToKeep = ASS\getTagNames string.split res.tagsToKeep, ",%s", nil, false
res.tagSortOrder = ASS\getTagNames string.split res.tagSortOrder, ",%s", nil, false
res.mergeConsecutiveExcept = ASS\getTagNames string.split res.mergeConsecutiveExcept, ",%s", nil, false
res.extraDataFilter = string.split res.extraDataFilter, ",%s", nil, false
callback = (lines, line, i) ->
aegisub.cancel! if aegisub.progress.is_cancelled!
aegisub.progress.task "Cleaning %d of %d lines..."\format i, lineCnt if i%10==0
aegisub.progress.set 100*i/lineCnt
unless line.styleRef
logger\warn "WARNING: Line #%d is using undefined style '%s', skipping...\n— %s", i, line.style, line.text
return
-- filter extra data
if line.extra and res.extraDataMode != "Keep all"
removed, r = switch res.extraDataMode
when "Remove all"
removed = line.extra
line.extra = nil
removed, table.length removed
when "Remove all except"
table.removeKeysExcept line.extra, res.extraDataFilter
when "Keep all except"
table.removeKeys line.extra, res.extraDataFilter
stats.extra += r
success, data = pcall ASS\parse, line
unless success
logger\warn "Couldn't parse line #%d: %s", i, data
return
-- it is essential to run SubInspector on a ASSFoundation-built line (rather than the original)
-- because ASSFoundation rounds tag values to a sane precision, which is not visible but
-- will produce a hash mismatch compared to the original line. However we must avoid that to
-- not trigger the ASSWipe bug detection
orgText, oldBounds = line.text, data\getLineBounds false, true
orgTextParsed = oldBounds.rawText
orgTagTypes = ["#{tag.__tag.name}(#{table.concat({tag\getTagParams!}, ",")})" for tag in *data\getTags!]
removeInvisibleContours = (contour) ->
contour.disabled = true
if oldBounds\equal data\getLineBounds!
if contour.parent.class == ASS.Section.Drawing
stats.contoursDraw += 1
else stats.contoursClip += 1
return false
contour.disabled = false
-- remove invisible lines
if res.removeInvisible and oldBounds.w == 0
stats.bytes += #line.raw + 1
delCnt += 1
linesToDelete[delCnt], line.ASS = line
return
purgedContourCount = 0
if res.purgeContoursDraw or res.scale2float
cb = (section) ->
-- remove invisible contours from drawings
if res.purgeContoursDraw
purgedContourCount = removeInvisibleContoursOpt section, oldBounds
-- un-scale drawings
if res.scale2float and section.scale > 1
section.scale\set 1
stats.scale2float += 1
data\callback cb, ASS.Section.Drawing
if purgedContourCount > 0
if res.purgeContoursDraw and not res.purgeContoursIgnoreHashMismatch and not data\getLineBounds!\equal oldBounds
line.text = orgText
data = ASS\parse line
stats.contoursDrawSkipped += purgedContourCount
else
stats.contoursDraw += purgedContourCount
oldBounds = data\getLineBounds! if res.purgeContoursIgnoreHashMismatch
-- pogressively build a table of visible lines by frame
-- which is required to check mergeability of consecutive identical lines
if res.combineLines
line.firstFrame = ffms line.start_time
line.lastFrame = -1 + ffms line.end_time
for i = line.firstFrame, line.lastFrame
lbf = linesByFrame[i]
if lbf
lbf[lbf.n+1] = line
lbf.n += 1
else linesByFrame[i] = {line, n: 1}
-- collect lines to combine
if oldBounds.firstHash and not oldBounds.animated
ltc = linesToCombine[oldBounds.firstHash]
if ltc
ltc[ltc.n+1] = line
ltc.n += 1
else linesToCombine[oldBounds.firstHash] = {line, n: 1}
mergeConsecutiveTagSections = if not res.mergeConsecutive
false
elseif #res.mergeConsecutiveExcept == 0
true
else
exceptions = list.makeSet res.mergeConsecutiveExcept
(sourceSection, targetSection) ->
predicate = (tag) -> exceptions[tag.__tag.name]
not table.find(sourceSection.tags, predicate) and not table.find targetSection.tags, predicate
-- clean tags
data\cleanTags res.cleanLevel, mergeConsecutiveTagSections, res.tagsToKeep, res.tagSortOrder
newBounds = data\getLineBounds!
if res.stripComments
data\stripComments!
--data\callback stripComments, ASS.Section.Comment
if res.filterClips or res.removeJunk
data\modTags tagNames, (tag) ->
-- remove junk
if tag.class == ASS.Tag.Unknown
stats.junk += 1
return false
if tag.class == ASS.Tag.ClipVect
-- un-scale clips
if res.scale2float and tag.scale>1
tag.scale\set 1
stats.scale2float += 1
-- purge ineffective contours from clips
if res.purgeContoursClip
tag\callback removeInvisibleContours
-- filter clips
tag.disabled = true
if data\getLineBounds!\equal newBounds
stats.clips += 1
return false
tag.disabled = false
data\commit nil, res.cleanLevel == 0
-- reclaim some memory
line.ASS, line.undoText = nil
if orgText != line.text
if not newBounds\equal oldBounds
debugError = true
logger\warn "Cleaning affected output on line #%d, rolling back...", line.humanizedNumber
logger\warn "—— Before: %s\n—— Parsed: %s\n—— After: %s\n—— Style: %s\n—— Tags: %s", orgText, orgTextParsed, line.text, line.styleRef.name, table.concat orgTagTypes, "; "
logger\warn "—— Hash Before: %s (%s); Hash After: %s (%s)\n",
oldBounds.firstHash, oldBounds.animated and "animated" or "static",
newBounds.firstHash, newBounds.animated and "animated" or "static"
line.text = orgText
elseif #line.text < #orgText
stats.cleaned += 1
stats.bytes += #orgText - #line.text
aegisub.cancel! if aegisub.progress.is_cancelled!
lines\runCallback callback, true
-- sort lines which are to be combined by time
sortFunc = (a, b) ->
return true if a.start_time < b.start_time
return false if a.start_time > b.start_time
return true if a.layer < b.layer
return false if a.layer > b.layer
return true if a.number < b.number
return false
linesToCombineSorted, l = {}, 1
for _, group in pairs linesToCombine
continue if group.n < 2
sort group, sortFunc
linesToCombineSorted[l] = group
l += 1
sort linesToCombineSorted, (a, b) -> sortFunc a[1], b[1]
-- combine lines
for group in *linesToCombineSorted
for j=1, group.n
linesToDelete[delCnt+cmbCnt+1], cmbCnt, stats.bytes = mergeLines group, j, cmbCnt, stats.bytes, linesByFrame
lines\replaceLines!
lines\deleteLines linesToDelete
logger\warn json.encode {Styles: lines.styles, Configuration: res} if debugError
logger\warn reportMsg, lineCnt, os.time!-stats.start, stats.cleaned, 100*stats.cleaned/lineCnt,
delCnt, 100*delCnt/lineCnt, cmbCnt, 100*cmbCnt/lineCnt, stats.clips, stats.junk,
stats.contoursClip+stats.contoursDraw, stats.contoursDraw, stats.contoursClip, stats.contoursDrawSkipped,
stats.scale2float, stats.extra, stats.bytes/1000
if debugError
logger\warn [[However, ASSWipe possibly encountered bugs while cleaning.
Affected lines have been rolled back to their previous state, so your script is most likely fine.
Please copy the whole log window contents and send them to line0.]]
return lines\getSelection!
showDialog = (sub, sel, res) ->
dlg = {
main: {
removeInvisible: class: "checkbox", x: 0, y: 0, width: 2, height: 1, value: true, config: true, label: "Remove invisible lines", hint: hints.removeInvisible
combineLines: class: "checkbox", x: 0, y: 1, width: 2, height: 1, value: true, config: true, label: "Combine consecutive identical lines", hint: hints.combineLines
mergeConsecutive: class: "checkbox", x: 2, y: 0, width: 12, height: 1, value: true, config: true, label: "Merge consecutive tag sections unless it contains any of:", hint: hints.mergeConsecutive
mergeConsecutiveExcept: class: "textbox", x: 2, y: 1, width: 12, height: 2, value: karaokeTags, config: true, hint: hints.mergeConsecutiveExcept
cleanLevelLabel: class: "label", x: 0, y: 4, width: 1, height: 1, label: "Tag cleanup level: "
cleanLevel: class: "intedit", x: 1, y: 4, width: 1, height: 1, min: 0, max: 4, value: 4, config: true, hint: hints.cleanLevel
tagsToKeepLabel: class: "label", x: 4, y: 4, width: 1, height: 1, label: "Keep default tags: "
tagsToKeep: class: "textbox", x: 4, y: 5, width: 10, height: 2, value: "\\pos", config:true, hint: hints.tagsToKeep
tagSortOrderLabel: class: "label", x: 4, y: 7, width: 1, height: 1, label: "Tag sort order: "
stripComments: class: "checkbox", x: 0, y: 5, width: 2, height: 1, value: true, config: true, label: "Strip comments", hint: hints.stripComments
removeJunk: class: "checkbox", x: 0, y: 6, width: 2, height: 1, value: true, config: true, label: "Remove junk from tag sections", hint: hints.removeJunk
removeUnknown: class: "checkbox", x: 0, y: 7, width: 2, height: 1, value: true, config: true, label: "Remove unknown tags from tag sections", hint: hints.removeJunk
tagSortOrder: class: "textbox", x: 4, y: 8, width: 10, height: 3, value: defaultSortOrder, config: true, hint: hints.tagSortOrder
filterClips: class: "checkbox", x: 0, y: 11, width: 2, height: 1, value: true, config: true, label: "Filter clips", hint: hints.filterClips
scale2float: class: "checkbox", x: 0, y: 12, width: 2, height: 1, value: true, config: true, label: "Un-scale drawings and clips", hint: hints.scale2float
fixDrawings: class: "checkbox", x: 0, y: 13, width: 2, height: 1, value: false, config: true, label: "Try to fix broken drawings", hint: hints.fixDrawings
purgeContoursLabel: class: "label", x: 0, y: 14, width: 2, height: 1, label: "Purge invisible contours: "
purgeContoursDraw: class: "checkbox", x: 4, y: 14, width: 3, height: 1, value: false, config: true, label: "from drawings", hint: hints.purgeContoursDraw
purgeContoursClip: class: "checkbox", x: 7, y: 14, width: 6, height: 1, value: false, config: true, label: "from clips", hint: hints.purgeContoursClip
purgeContoursIgnoreHashMismatch: class: "checkbox", x: 4, y: 15, width: 9, height: 1, value: false, config: true, label: "ignore rendering inconsistencies", hint: hints.purgeContoursIgnoreHashMismatch
extraDataLabel: class: "label", x: 0, y: 17, width: 1, height: 1, label: "Filter extra data: "
extraDataMode: class: "dropdown", x: 1, y: 17, width: 1, height: 1, value: "Keep All", config: true, items: {"Keep all", "Remove all", "Keep all except", "Remove all except"}, hint: hints.extraData
extraDataFilter: class: "textbox", x: 4, y: 17, width: 10, height: 3, value: "", config: true, hint: hints.extraData
quirksModeLabel: class: "label", x: 0, y: 21, width: 2, height: 1, label: "Quirks mode: "
quirksMode: class: "dropdown", x: 4, y: 21, width: 2, height: 1, value: "VSFilter", config: true, items: [k for k, v in pairs ASS.Quirks], hint: hints.quirksMode
}
}
options = ConfigHandler dlg, version.configFile, false, script_version, version.configDir
options\read!
options\updateInterface "main"
btn, res = aegisub.dialog.display dlg.main
if btn
options\updateConfiguration res, "main"
options\write!
ASS.config.quirks = ASS.Quirks[res.quirksMode]
process sub, sel, res
version\registerMacro showDialog, ->
if aegisub.project_properties!.video_file == ""
return false, "A video must be loaded to run #{script_name}."
else return true, script_description
@PhosCity
Copy link
Author

ASSWipe is a behemoth and there is a very high chance that when I try to fix one thing, I break another. When you wipe make sure that it does not wipe things you don't intend to. Always check your signs after wiping and if it does something untended, unwipe and report it.

Fix 1: Add an option to remove Unknown Tags. Asswipe already allows you to remove Junk Tags, so I'm not sure why it doesn't remove Unknown Tags.
Fix 2: When a line is invisible and remove invisible line is ticked off, Asswipe errors out. It's driving Light up the fucking wall.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment