packages feed

cut-the-crap-2.2.0: src/Cut/Analyze.hs

module Cut.Analyze
  ( detectSoundInterval
  , Interval(..)
  , Sound
  , Silent
  , getStart
  , getEnd
  , getDuration
  , takeOnlyLines
  , detectSpeech
  )
where

import           Control.Lens
import           Control.Monad
import           Control.Monad.Catch
import           Control.Monad.IO.Unlift
import           Cut.Shell
import           Cut.Options
import           Cut.SpeechRecognition
import           Data.Coerce
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, shelly)
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

detectSoundInterval :: (MonadMask m, MonadUnliftIO m) => ListenCutOptions -> m [Interval Sound]
detectSoundInterval 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
      detector       = if opts ^. cut_noise then detectSilence else detectSound
      fancyResult    = detector 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'

detectSilence :: ListenCutOptions -> [Interval Silent] -> [Interval Sound]
detectSilence _ = coerce

detectSound :: ListenCutOptions -> [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'
  :: ListenCutOptions
  -> 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 :: ListenCutOptions -> Sh [Text]
detectShell opt' = ffmpeg (opt' ^. lc_fileio . in_file . packed)
  ["-map"
  , voice_track_map opt'
  , "-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


-- | Detect the speech on the mkv file
detectSpeech :: ListenCutOptions -> Prelude.FilePath -> Prelude.FilePath -> Sh (Either ResultCode [WordFrame])
detectSpeech options tempdir inputFile = do
  void $ ffmpeg inputFile $ (specifyTracks options) <> [
      Text.pack tmpMp3File
    ]
  void $ ffmpeg tmpMp3File [
    "-f", "s16le", "-acodec", "pcm_s16le", "-filter:a", "aresample=resampler=soxr:osr=16000", "-ac", "1", Text.pack tmpRawFile
    ]
  liftIO $ speechAnalyses tmpRawFile
  where
    tmpMp3File = tempdir <> "/" <> "speechdetect.mp3"
    tmpRawFile = tempdir <> "/" <> "speechdetect.raw"