cutitout.lua 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. -- cutitout: automatically cut silence from videos
  2. -- Copyright (C) 2020 Wolf Clement
  3. -- This program is free software: you can redistribute it and/or modify
  4. -- it under the terms of the GNU Affero General Public License as published
  5. -- by the Free Software Foundation, either version 3 of the License, or
  6. -- (at your option) any later version.
  7. -- This program is distributed in the hope that it will be useful,
  8. -- but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. -- GNU Affero General Public License for more details.
  11. -- You should have received a copy of the GNU Affero General Public License
  12. -- along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. -- Margin before and after a clip (in seconds)
  14. local clip_margin = 0.4
  15. assert(clip_margin >= 0.0)
  16. -- How loud should noise be to be considered a sound?
  17. local audio_treshold = 0.01
  18. assert(audio_treshold > 0.0 and audio_treshold <= 1.0)
  19. -- Minimum clip length (in seconds)
  20. -- Sounds shorter than that will be considered noise and cut.
  21. local min_clip_length = 0.2
  22. assert(min_clip_length > 0.0)
  23. -- Minimum silence length to skip (in seconds)
  24. local min_skip_length = 2.0
  25. assert(min_skip_length > 2 * clip_margin)
  26. local enabled = false
  27. local minutes = {}
  28. local skips = {}
  29. function fix_boundaries()
  30. -- Merge skips across minutes (required for next step)
  31. for ia, a in pairs(skips) do
  32. for ib, b in pairs(skips) do
  33. if b[1] > a[1] and (b[1] - a[2] < min_clip_length) then
  34. table.insert(skips, {a[1], b[2]})
  35. if ia > ib then
  36. table.remove(skips, ia)
  37. table.remove(skips, ib)
  38. else
  39. table.remove(skips, ib)
  40. table.remove(skips, ia)
  41. end
  42. return fix_boundaries()
  43. end
  44. end
  45. end
  46. -- Add margin when someone stops talking right as the minute rolls over
  47. for _, a in pairs(skips) do
  48. if a[1] > 0.0 and a[1] % 60.0 == 0.0 then
  49. a[1] = a[1] + clip_margin
  50. end
  51. end
  52. end
  53. function load_minute(minute)
  54. if minutes[minute] == "loading" then return end
  55. local utils = require("mp.utils")
  56. local scripts_dir = mp.find_config_file("scripts")
  57. local cutitoutpy = utils.join_path(scripts_dir, "cutitout_shared/cutitout.py")
  58. local video_path = mp.get_property("path")
  59. minutes[minute] = "loading"
  60. mp.command_native_async({
  61. name = "subprocess",
  62. capture_stdout = true,
  63. playback_only = false,
  64. args = {
  65. "python3", cutitoutpy, video_path, tostring(minute - 1),
  66. tostring(clip_margin), tostring(audio_treshold),
  67. tostring(min_clip_length), tostring(min_skip_length),
  68. }
  69. }, function(res, val, err)
  70. new_skips = loadstring(val.stdout)()
  71. local seconds_skippable = 0.0
  72. for _, skip in pairs(new_skips) do
  73. table.insert(skips, skip)
  74. seconds_skippable = seconds_skippable + (skip[2] - skip[1])
  75. end
  76. fix_boundaries()
  77. minutes[minute] = "loaded"
  78. print(tostring(#new_skips) .. " skips loaded for minute " .. tostring(minute - 1))
  79. print(tostring(math.floor(seconds_skippable)) .. " seconds skippable in minute " .. tostring(minute - 1))
  80. -- If majority of the minute is skippable, keep looking for skips
  81. -- (so silent videos portions lasting more than 2 minutes get skipped in one go)
  82. if seconds_skippable > 30 then
  83. load_minute(minute + 1)
  84. end
  85. end)
  86. end
  87. -- Whenever time updates...
  88. mp.observe_property("time-pos", "native", function (_, pos)
  89. if pos == nil then return end
  90. if not enabled then return end
  91. -- Check if we should load skips for the current minute
  92. local current_minute = math.floor(pos / 60.0) + 1
  93. if minutes[current_minute] == nil then
  94. load_minute(current_minute)
  95. end
  96. -- Check if we should load skips for the next minute
  97. local next_minute = math.ceil((pos + 1.0) / 60.0) + 1
  98. if minutes[current_minute] == "loaded" and minutes[next_minute] == nil then
  99. load_minute(next_minute)
  100. end
  101. -- Check if we should skip
  102. for _, t in pairs(skips) do
  103. -- t[1] == start time of the skip
  104. -- t[2] == end time of the skip
  105. if t[1] <= pos and t[2] > pos then
  106. print("Skipping to " .. t[2])
  107. mp.set_property("time-pos", t[2])
  108. return
  109. end
  110. end
  111. end)
  112. -- F12 toggles silence skipping (off by default)
  113. mp.add_key_binding("F12", "toggle_silence_skip", function ()
  114. enabled = not enabled
  115. if enabled then
  116. mp.osd_message("Silence skipping enabled.")
  117. else
  118. mp.osd_message("Silence skipping disabled.")
  119. end
  120. end)
  121. -- Whenever we load another file, we reset skips
  122. mp.register_event("file-loaded", function ()
  123. minutes = {}
  124. skips = {}
  125. end)