cut-the-crap (empty) → 1.0.0
raw patch · 13 files changed
+855/−0 lines, 13 filesdep +basedep +cut-the-crapdep +exceptions
Dependencies added: base, cut-the-crap, exceptions, generic-lens, hspec, hspec-core, lens, optparse-applicative, optparse-generic, regex-tdfa, shelly, system-filepath, temporary, text, unliftio-core
Files
- LICENSE +21/−0
- Readme.md +139/−0
- app/exe.hs +6/−0
- cut-the-crap.cabal +110/−0
- src/Cut/Analyze.hs +173/−0
- src/Cut/CutVideo.hs +86/−0
- src/Cut/Ffmpeg.hs +22/−0
- src/Cut/Lib.hs +120/−0
- src/Cut/Options.hs +93/−0
- src/Cut/SplitVideo.hs +32/−0
- test/Spec.hs +1/−0
- test/Test/MatchLineSpec.hs +36/−0
- test/Test/TestSpec.hs +16/−0
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2019 Jappie Klooster++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ Readme.md view
@@ -0,0 +1,139 @@+++[](https://www.twitch.tv/jappiejappie)+[](https://www.Youtube.com/channel/UCQxmXSQEYyCeBC6urMWRPVw)+[](https://travis-ci.org/jappeace/cut-the-crap/builds/)+[](https://discord.gg/Hp4agqy)++> Bless This Mess++Cut the crap is an automatic video editing program for streamers.+It can cut out uninteresting parts by detecting silences.+This was inspired by [jumpcutter](https://github.com/carykh/jumpcutter),+where this program can get better quality results+by using an (optional) dedicated microphone track.+This prevents cutting of [quieter consonants](https://youtu.be/DQ8orIurGxw?t=675)+for example.+Using ffmpeg more efficiently also produces faster results and+is less error prone.++Youtube has different requirements from streams then twitch does.+We want to cut out boring parts.+Jumpcut has solved that problem partly and this program+builds on top of that idea.+At the moment we use ffmpeg for silence detection, +then we do some maths to figure out which segments are sounded,+which is combined into the output video.++In the future we will add support for a music track+which will not be chopped up.++# Install++## Nix/Nixos++ Apply [overlay](https://nixos.wiki/wiki/Overlays) found [here](https://github.com/jappeace/cut-the-crap/tree/master/overlay).++ Run `nix-env -iA cut-the-crap` or add to systemPackages.++ simply run `cut-the-crap` to display usage instructions.++## Ubuntu+Download the .deb file from the release page.+Install with:++```shell+apt install ffmpeg+dpkg -i ./cut-the-crap_1.0.0_amd64.deb +```++Execute with:+```shell+cut-the-crap+```+# Usage notes++## Noise gate+Make sure to record with a noise gate on your microphone.+This will cut out background buzzing and allow you to use a more aggressive+threshold on noise detection.++## OBS tracks++Setup OBS so that you record the microphone and the desktop audio+on [separate tracks](https://obsproject.com/forum/resources/obs-studio-high-quality-recording-and-multiple-audio-tracks.221/).+In my own setup I have track 1 for combining all audio, track 2 for just the microphone and track 3 for desktop audio.+Then I can use:++```shell+ cut-the-crap --inputFile ./recordFromObs.mkv --outputFile ./someOut.mkv --voiceTrack 2 --musicTrack 3+```++So we throw away track 1, we use track 2 for silence detection, and track 3 get's mixed in after cutting is complete.+If you don't want music being mixed back into the result,+for example for further editing,+you can also leave that argument out.+I did this for example to mix back in the music of the original file later.++# Use case+I'm using this program to record my [stream](https://www.twitch.tv/jappiejappie)+and upload it to my+[Youtube channel](https://www.Youtube.com/channel/UCQxmXSQEYyCeBC6urMWRPVw).++The concrete result is that your audience retention percentage will go up since the videos+will be shorter, and more engaging.+Sometimes on stream I have intro screens for example which completely get removed,+and other times I'm simply thinking.+Reducing videos by 30% is not uncommon in my case, which means by default+30% more retention.+You could even decide to edit after that which means you have to spend less time+on cutting out silences and more time on making it look cool.++Feel free to use or modify this program however you like.+Pull requests are appreciated.++# Features++## Track based silence detection+It is possible to specify one audio output as speech track.+This will be used to for silence detection only.+The result is very precise silence detection.++## Separate music track+Another track would be background and won't be modified at all.+In the end it just get's cut of how far it is.++This way we get good music and interesting stream.+Another idea is to remix an entirely different source of music+into the video, so we can play copyrighted music on stream+and Youtube friendly music on Youtube.++# Design+This project is mostly a wrapper around ffmpeg.+We use Haskell for shell programming.++The [shelly](http://hackage.haskell.org/package/shelly) library was chosen in support of shell programming.+Originally we used [turtle](http://hackage.haskell.org/package/turtle),+but that library is much more complicated to use because it assumes you+want to do stream programming,+creating several unexpected bugs.+So we replaced it with shelly and noticabally reduced code complexity.+Now it's truly a 'dumb' wrapper around ffmpeg.++## Why not to extend jumpcutter directly?+I wish to build out this idea more to essentially+make all streams look like human edited Youtube videos.+Although I'm familiar with python,+I (am or feel) more productive in haskell,+therefore I chose to integrate with,+and eventually replace jumpcutter.+On stream we've determined most of the functionality is basically+ffmpeg.+Haskell also opens up the ability to do direct native ffmpeg+integration,+where we use ffmpeg as a library instead of calling it as a CLI+program.++One glaring limitation I've encountered with jumpcutter is that+it can't handle larger video files (2 hour 30 minutes +).+Scipy throws an exception complaining the wav is to big.+Since this program doesn't use scipy it doesn't have that issue.++It also appears like jumpcutter is unmaintained.
+ app/exe.hs view
@@ -0,0 +1,6 @@+module Main where++import qualified Cut.Lib as Lib++main :: IO ()+main = Lib.entryPoint
+ cut-the-crap.cabal view
@@ -0,0 +1,110 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 080b14b4ca581bc972d8d8ae8b9ef84e51078211786c6c8657e2c39a77628fb6++name: cut-the-crap+version: 1.0.0+synopsis: Cuts out uninteresting parts of videos by detecting silences.+description: Cut the crap is an automatic video editing program for streamers. It can cut out uninteresting parts by detecting silences. This was inspired by [jumpcutter](https://github.com/carykh/jumpcutter), where this program can get better quality results by using an (optional) dedicated microphone track. This prevents cutting of [quieter consonants](https://youtu.be/DQ8orIurGxw?t=675) for example. Using ffmpeg more efficiently also produces faster results and is less error prone.+category: video+author: Jappie Klooster+maintainer: jappieklooster@hotmail.com+copyright: 2019 Jappie Klooster+license: MIT+license-file: LICENSE+build-type: Simple+extra-source-files:+ Readme.md+ LICENSE++library+ exposed-modules:+ Cut.Analyze+ Cut.CutVideo+ Cut.Ffmpeg+ Cut.Lib+ Cut.Options+ Cut.SplitVideo+ other-modules:+ Paths_cut_the_crap+ hs-source-dirs:+ src+ default-extensions: EmptyCase FlexibleContexts FlexibleInstances InstanceSigs MultiParamTypeClasses LambdaCase MultiWayIf NamedFieldPuns TupleSections DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies GeneralizedNewtypeDeriving StandaloneDeriving OverloadedStrings TypeApplications+ ghc-options: -Wall -Wcompat -Wincomplete-uni-patterns -Wredundant-constraints -Wincomplete-record-updates -Widentities+ build-depends:+ base >=4.7 && <5+ , exceptions+ , generic-lens+ , lens+ , optparse-applicative+ , optparse-generic+ , regex-tdfa+ , shelly+ , system-filepath+ , temporary+ , text+ , unliftio-core+ default-language: Haskell2010++executable cut-the-crap+ main-is: exe.hs+ other-modules:+ Paths_cut_the_crap+ hs-source-dirs:+ app+ default-extensions: EmptyCase FlexibleContexts FlexibleInstances InstanceSigs MultiParamTypeClasses LambdaCase MultiWayIf NamedFieldPuns TupleSections DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies GeneralizedNewtypeDeriving StandaloneDeriving OverloadedStrings TypeApplications+ ghc-options: -Wall -Wcompat -Wincomplete-uni-patterns -Wredundant-constraints -Wincomplete-record-updates -Widentities -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.7 && <5+ , cut-the-crap+ , exceptions+ , generic-lens+ , lens+ , optparse-applicative+ , optparse-generic+ , regex-tdfa+ , shelly+ , system-filepath+ , temporary+ , text+ , unliftio-core+ default-language: Haskell2010++test-suite unit+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Test.MatchLineSpec+ Test.TestSpec+ Cut.Analyze+ Cut.CutVideo+ Cut.Ffmpeg+ Cut.Lib+ Cut.Options+ Cut.SplitVideo+ Paths_cut_the_crap+ hs-source-dirs:+ test+ src+ default-extensions: EmptyCase FlexibleContexts FlexibleInstances InstanceSigs MultiParamTypeClasses LambdaCase MultiWayIf NamedFieldPuns TupleSections DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies GeneralizedNewtypeDeriving StandaloneDeriving OverloadedStrings TypeApplications+ ghc-options: -Wall -Wcompat -Wincomplete-uni-patterns -Wredundant-constraints -Wincomplete-record-updates -Widentities+ build-depends:+ base >=4.7 && <5+ , exceptions+ , generic-lens+ , hspec+ , hspec-core+ , lens+ , optparse-applicative+ , optparse-generic+ , regex-tdfa+ , shelly+ , system-filepath+ , temporary+ , text+ , unliftio-core+ default-language: Haskell2010
+ src/Cut/Analyze.hs view
@@ -0,0 +1,173 @@+module Cut.Analyze+ ( detect+ , Interval(..)+ , Sound+ , Silent+ , getStart+ , getEnd+ , getDuration+ , takeOnlyLines+ )+where++import Control.Lens+import Control.Monad+import Control.Monad.Catch+import Control.Monad.IO.Unlift+import Cut.Ffmpeg+import Cut.Options+import Data.Foldable+import Data.Maybe+import Data.Text ( Text )+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import Data.Text.Lens+import Shelly hiding ( find )+import Text.Regex.TDFA hiding ( empty )++data Silent+data Sound++data Interval e = Interval+ { interval_start :: Double+ , interval_end :: Double+ , interval_duration :: Double+ , interval_input_start :: Text+ , interval_input_end :: Text+ } deriving Show++detect :: (MonadMask m, MonadUnliftIO m) => Options -> m [Interval Sound]+detect opts = do+ lines'' <- shelly $ detectShell opts+ let linesRes = do+ line <- lines''+ if takeOnlyLines line then pure $ Right line else pure $ Left line+ lines' = linesRes ^.. traversed . _Right++ liftIO $ putStrLn "-----------------------------------------"+ liftIO $ putStrLn "-----------------actual lines-----------------"+ liftIO $ putStrLn "-----------------------------------------"+ liftIO $ Text.putStrLn $ Text.unlines lines'+ liftIO $ putStrLn "-----------------------------------------"+ liftIO $ putStrLn "-----------------filtered lines-----------------"+ liftIO $ putStrLn "-----------------------------------------"+ liftIO $ Text.putStrLn $ Text.unlines (linesRes ^.. traversed . _Left)++ let linedUp = zipped lines'+ parsed = parse <$> linedUp+ fancyResult = detectSound opts parsed+ negativeResult = find ((0 >) . interval_duration) fancyResult++ liftIO $ putStrLn "-----------------------------------------"+ liftIO $ putStrLn "-----------------lined up-----------------"+ liftIO $ putStrLn "-----------------------------------------"+ liftIO $ traverse_ print linedUp++ liftIO $ putStrLn "-----------------------------------------"+ liftIO $ putStrLn "-----------------parsed-----------------"+ liftIO $ putStrLn "-----------------------------------------"+ liftIO $ traverse_ print parsed++ if isJust negativeResult+ then do+ liftIO $ traverse_ print fancyResult+ liftIO $ print negativeResult+ error "Found negative durations"+ else pure fancyResult++takeOnlyLines :: Text -> Bool+takeOnlyLines matchWith = matches+ where+ silenceRegex :: String+ silenceRegex = ".*silencedetect.*"+ matches :: Bool+ matches = Text.unpack matchWith =~ silenceRegex++zipped :: [Text] -> [(Text, Text)]+zipped [] = mempty+zipped [_ ] = []+zipped (one : two : rem') = (one, two) : zipped rem'++detectSound :: Options -> [Interval Silent] -> [Interval Sound]+detectSound opts =+ -- -- TODO figure out why these durations get recorded as < 0+ reverse . snd . foldl' (flip (compare' opts)) (Interval 0 0 0 "" "", [])++compare'+ :: Options+ -> Interval Silent+ -> (Interval Silent, [Interval Sound])+ -> (Interval Silent, [Interval Sound])+compare' opts current prev = (current, soundedInterval : snd prev)+ where+ soundedInterval = Interval+ { interval_start = interval_end $ fst prev+ , interval_end = interval_start current - margin+ , interval_duration = (soundEnd - soundStart) + margin+ , interval_input_start =+ interval_input_start (fst prev) <> "," <> interval_input_end (fst prev)+ , interval_input_end = interval_input_start current+ <> ","+ <> interval_input_end current+ }+ soundEnd = interval_start current+ soundStart = interval_end $ fst prev++ margin = opts ^. detect_margin+++detectShell :: Options -> Sh [Text]+detectShell opt' = ffmpeg+ [ "-i"+ , opt' ^. in_file . packed+ , "-map"+ , "0:" <> opt' ^. voice_track . to show . packed+ , "-filter:a"+ -- , "silencedetect=noise=-30dB:d=0.5"+ , "silencedetect=noise="+ <> (opt' ^. silent_treshold . to floatToText)+ <> ":d="+ <> (opt' ^. silent_duration . to floatToText)+ , "-f"+ , "null"+ , "-"+ ]++parse :: (Text, Text) -> Interval Silent+parse xx = Interval { interval_start = getStart $ fst xx+ , interval_end = getEnd $ snd xx+ , interval_duration = getDuration $ snd xx+ , interval_input_start = fst xx+ , interval_input_end = snd xx+ }++getStart :: Text -> Double+getStart line = read $ takeWhile (/= '\'') $ matches ^. _3+ where+ str = Text.unpack line+ matches :: (String, String, String)+ matches = str =~ startMatch++startMatch :: String+startMatch = "(.*)?: "++pipe :: String+pipe = " \\| "++getDuration :: Text -> Double+getDuration line = read $ takeWhile (/= '\'') $ match2 ^. _1+ where+ str = Text.unpack line+ match1 :: (String, String, String)+ match1 = str =~ startMatch+ match2 :: (String, String, String)+ match2 = (match1 ^. _3) =~ pipe++getEnd :: Text -> Double+getEnd line = read $ match2 ^. _3+ where+ str = Text.unpack line+ match1 :: (String, String, String)+ match1 = str =~ pipe+ match2 :: (String, String, String)+ match2 = (match1 ^. _1) =~ startMatch
+ src/Cut/CutVideo.hs view
@@ -0,0 +1,86 @@+{-# OPTIONS_GHC -Wno-type-defaults #-}++module Cut.CutVideo+ ( extract+ , Interval(..)+ , Silent+ , Sound+ , combine+ , combineOutput+ )+where++import Control.Lens+import Control.Monad+import Control.Monad.Catch+import Cut.Analyze+import Cut.Ffmpeg+import Cut.Options+import Data.Foldable+import Data.Text ( Text )+import qualified Data.Text as Text+import Data.Text.Lens+import Shelly hiding ( FilePath )++specifyTracks :: Options -> [Text]+specifyTracks options =+ [ "-map"+ , "0:0"+ , "-map" -- then copy only the voice track+ , "0:" <> options ^. voice_track . to show . packed+ ]++toArgs :: Options -> FilePath -> Interval Sound -> (Interval Sound, [Text])+toArgs options tmp inter =+ ( inter+ , -- keep ter interval for debugging+ ["-y", "-ss", start, "-t", duration, "-i", options ^. in_file . packed]+ <> specifyTracks options+ <> [ Text.pack tmp+ <> "/"+ <> options+ ^. out_file+ . packed+ <> "-"+ <> fname+ <> ".mkv"+ ]+ )+ where+ start = floatToText $ interval_start inter+ duration = floatToText $ interval_duration inter+ fname = Text.pack $ show $ truncate $ interval_start inter * 100++extract :: Options -> FilePath -> [Interval Sound] -> IO ()+extract options tempDir intervals = do+ traverse_+ (\(inter, args) -> void $ catch (shelly $ ffmpeg args) $ \exec -> do+ liftIO+ (print ("expection during edit: ", exec :: SomeException, args, inter)+ )+ pure ["expection"]+ )+ $ toArgs options tempDir+ <$> intervals+ liftIO $ putStrLn "finish extracting"++combineOutput :: FilePath+combineOutput = "combined-output.mkv"++combine :: FilePath -> Sh ()+combine tempfiles = do+ output' <- ffmpeg args+ liftIO $ print ("output", output')+ where -- https://stackoverflow.com/questions/7333232/how-to-concatenate-two-mp4-files-using-ffmpeg+ args =+ [ "-y"+ , "-f"+ , "concat"+ , "-safe"+ , "0"+ , "-i"+ , Text.pack (tempfiles <> "/input.txt")+ , "-c"+ , "copy"+ , Text.pack $ tempfiles <> "/" <> combineOutput+ ]
+ src/Cut/Ffmpeg.hs view
@@ -0,0 +1,22 @@+module Cut.Ffmpeg+ ( ffmpeg+ , floatToText+ )+where++import Data.Text ( Text )+import qualified Data.Text as Text+import Numeric+import Shelly++-- | Wrap ffmpeg for convenience and logging+ffmpeg :: [Text] -> Sh [Text]+ffmpeg args = do+ liftIO $ putStr "Running: "+ liftIO $ print $ "ffmpeg " <> Text.unwords args+ run_ "ffmpeg" args+ Text.lines <$> lastStderr++-- | Format floats for cli+floatToText :: Double -> Text+floatToText = Text.pack . flip (showFFloat (Just 10)) ""
+ src/Cut/Lib.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}+{-# OPTIONS_GHC -w #-}++module Cut.Lib+ ( entryPoint+ , combineDir+ )+where++import Control.Lens+import Control.Monad+import Control.Monad.Catch+import Control.Monad.IO.Class+import Control.Monad.IO.Unlift+import Cut.Analyze+import Cut.CutVideo+import Cut.Ffmpeg+import Cut.Options+import Cut.SplitVideo+import Data.Bifunctor+import Data.Either+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import Data.Text.Lens+import Options.Applicative+import Options.Generic+import Shelly hiding ( FilePath )+import System.IO.Temp+import Text.Regex.TDFA hiding ( empty+ , extract+ )++entryPoint :: (MonadMask m, MonadUnliftIO m) => m ()+entryPoint = catch main+ $ \exec -> liftIO (print ("Uncaught exception: ", exec :: SomeException))++main :: (MonadMask m, MonadUnliftIO m) => m ()+main = do+ options <- liftIO readSettings+ liftIO $ putStr "started with options: "+ liftIO $ print options++ parsed <- detect options+ case options ^. work_dir of+ Nothing ->+ withTempDirectory "/tmp" "streamedit" $ liftIO . runEdit options parsed+ Just x -> liftIO $ runEdit options parsed x++runEdit :: Options -> [Interval Sound] -> FilePath -> IO ()+runEdit options parsed temp = do+ extract options temp parsed+ shelly $ combineDir options temp+ getMusic options temp++combineDir :: Options -> FilePath -> Sh ()+combineDir options temp = do+ res <- lsT $ fromText $ Text.pack temp+ let paths :: Text+ paths = Text.unlines $ flip (<>) "'" . ("file '" <>) <$> res+ writefile (fromText $ Text.pack $ temp <> "/input.txt") paths+ combine temp++readSettings :: IO Options+readSettings = customExecParser (prefs showHelpOnError) $ info+ parseRecord+ (fullDesc <> Options.Applicative.header "Cut the crap" <> progDesc+ "Automated video extracting, can cut out silences"+ )++musicFile :: FilePath+musicFile = "music.mp3"++withMusicFile :: FilePath+withMusicFile = "combined.mkv"++getMusic :: Options -> FilePath -> IO ()+getMusic opt' tempfiles = do+ res <- case opt' ^. music_track of+ Nothing -> pure $ Text.pack combinedFile+ Just x -> do+ shelly $ ffmpeg $ args x+ shelly $ combineMusic tempfiles+ pure $ Text.pack (tempfiles <> "/" <> withMusicFile)+ putStrLn "done get music"+ shelly $ cp (fromText res) (opt' ^. out_file . packed . to fromText)+ pure ()+ where -- https://stackoverflow.com/questions/7333232/how-to-concatenate-two-mp4-files-using-ffmpeg+ combinedFile = tempfiles <> "/" <> combineOutput+ args x' =+ [ "-i"+ , opt' ^. in_file . packed+ , "-map"+ , "0:" <> Text.pack (show x')+ , Text.pack (tempfiles <> "/" <> musicFile)+ ]++combineMusic :: FilePath -> Sh ()+combineMusic tempfiles = void $ ffmpeg args+ where -- https://stackoverflow.com/questions/7333232/how-to-concatenate-two-mp4-files-using-ffmpeg+ args =+ [ "-i"+ , Text.pack $ tempfiles <> "/" <> combineOutput+ , "-i"+ , Text.pack $ tempfiles <> "/" <> musicFile+ , "-filter_complex"+ , "[0:a][1:a]amerge=inputs=2[a]"+ , "-map"+ , "0:v"+ , "-map"+ , "[a]"+ , "-c:v"+ , "copy"+ , "-c:a"+ , "mp3"+ , "-ac"+ , "2"+ , "-shortest"+ , Text.pack (tempfiles <> "/" <> withMusicFile)+ ]
+ src/Cut/Options.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}++module Cut.Options+ ( Options+ , in_file+ , out_file+ , seg_size+ , silent_treshold+ , detect_margin+ , voice_track+ , music_track+ , silent_duration+ , work_dir+ , simpleOptions+ )+where++import Control.Lens+import Data.Generics.Product.Fields+import Options.Generic++simpleOptions :: Options+simpleOptions = Options { inFile = halp # "in.mkv"+ , outFile = halp # "out.mkv"+ , segmentSize = halp . _Just # def_seg_size+ , silentTreshold = halp . _Just # def_silent+ , detectMargin = halp . _Just # def_margin+ , voiceTrack = halp . _Just # 2+ , musicTrack = halp # Nothing+ , silentDuration = halp . _Just # def_duration+ , workDir = halp # Nothing+ }++data Options = Options+ { inFile :: FilePath <?> "The input video"+ , outFile :: FilePath <?> "The output name without format"+ , segmentSize :: Maybe Int <?> "The size of video segments in minutes"+ , silentTreshold :: Maybe Double <?> "The treshold for determining intersting sections, closer to zero is detects more audio (n: https://ffmpeg.org/ffmpeg-filters.html#silencedetect)"+ , silentDuration :: Maybe Double <?> "The duration before soemthing can be considered a silence (d: https://ffmpeg.org/ffmpeg-filters.html#silencedetect)"+ , detectMargin :: Maybe Double <?> "Margin seconds around detection"+ , voiceTrack :: Maybe Int <?> "The track to detect audio upon"+ , musicTrack :: Maybe Int <?> "The track to detect audio upon"+ , workDir :: Maybe FilePath <?> "If specified will use this as temporty directory to store intermeidate files in, good for debugging. Needs to be absolute"+ } deriving (Show, Generic)++halp :: Iso' (a <?> b) a+halp = iso unHelpful Helpful++in_file :: Lens' Options FilePath+in_file = field @"inFile" . halp++out_file :: Lens' Options FilePath+out_file = field @"outFile" . halp++def_seg_size :: Int+def_seg_size = 20++def_margin :: Double+def_margin = 0.05++def_silent :: Double+def_silent = 0.0001++def_duration :: Double+def_duration = 0.25++def_voice :: Int+def_voice = 1++seg_size :: Lens' Options Int+seg_size = field @"segmentSize" . halp . non def_seg_size++detect_margin :: Lens' Options Double+detect_margin = field @"detectMargin" . halp . non def_margin++silent_treshold :: Lens' Options Double+silent_treshold = field @"silentTreshold" . halp . non def_silent++silent_duration :: Lens' Options Double+silent_duration = field @"silentDuration" . halp . non def_duration++voice_track :: Lens' Options Int+voice_track = field @"voiceTrack" . halp . non def_voice++music_track :: Lens' Options (Maybe Int)+music_track = field @"musicTrack" . halp++work_dir :: Lens' Options (Maybe FilePath)+work_dir = field @"workDir" . halp++instance ParseRecord Options
+ src/Cut/SplitVideo.hs view
@@ -0,0 +1,32 @@+module Cut.SplitVideo+ ( split+ )+where++import Control.Lens+import Control.Monad+import Cut.Ffmpeg+import Cut.Options+import Data.Text.Lens+import Shelly hiding ( FilePath )++-- | Splits a video into segments+split :: FilePath -> Options -> Sh ()+split tmp opt' = do+ cp (fromText (tmp ^. packed))+ (fromText (opt' ^. out_file . packed <> "-full.mp4"))+ void $ ffmpeg+ [ "-i"+ , tmp ^. packed+ , "-c"+ , "copy"+ , "-map"+ , "0"+ , "-segment_time"+ , "00:" <> (opt' ^. seg_size . to show . packed) <> ":00"+ , "-f"+ , "segment"+ , "-reset_timestamps"+ , "1"+ , opt' ^. out_file . packed <> "%03d.mp4"+ ]
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/Test/MatchLineSpec.hs view
@@ -0,0 +1,36 @@+module Test.MatchLineSpec+ ( spec+ )+where++import Cut.Analyze+import Test.Hspec++spec :: Spec+spec = describe "Match a line" $ do+ it "should satisfy equality"+ $ getStart "[silencedetect @ 0xad44e0] silence_start: -0.012381"+ `shouldBe` (-0.012381)++ it "Should deal with rng output"+ $ getStart "[silencedetect @ 0x10b0280] silence_start: 430.41'peed= 858x"+ `shouldBe` 430.41+ it "Should match the end line as well"+ $ getEnd+ "[silencedetect @ 0x23a94e0] silence_end: 148.515 | silence_duration: 0.825079"+ `shouldBe` 148.515+ it "Should match the duration"+ $ getDuration+ "[silencedetect @ 0x23a94e0] silence_end: 148.515 | silence_duration: 0.825079"+ `shouldBe` 0.825079++ it "Should deal with quotes on end as well"+ $ getDuration+ "[silencedetect @ 0x1900280] silence_end: 1249.91 | silence_duration: 15.7841'"+ `shouldBe` 15.7841++ it "Shouldn't filter this line"+ $ takeOnlyLines+ "[silencedetect @ 0xcf3280] silence_end: 415.498 | silence_duration: 69.6312'"+ `shouldBe` True+
+ test/Test/TestSpec.hs view
@@ -0,0 +1,16 @@+module Test.TestSpec+ ( spec+ )+where++import Test.Hspec++one :: Int+one = 1++spec :: Spec+spec =+ describe "The sanity of our test setup"+ $ it "should satisfy equality"+ $ one+ `shouldBe` 1