packages feed

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 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 @@+![logo](doc/logo.png)++[![Jappiejappie](https://img.shields.io/badge/twitch.tv-jappiejappie-purple?logo=twitch)](https://www.twitch.tv/jappiejappie)+[![Jappiejappie](https://img.shields.io/badge/Youtube-jappieklooster-red?logo=Youtube)](https://www.Youtube.com/channel/UCQxmXSQEYyCeBC6urMWRPVw)+[![Build status](https://img.shields.io/travis/jappeace/cut-the-crap)](https://travis-ci.org/jappeace/cut-the-crap/builds/)+[![Jappiejappie](https://img.shields.io/badge/discord-jappiejappie-black?logo=discord)](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