packages feed

silero-vad-0.1.0.0: src/Silero/Detector.hs

module Silero.Detector (
  VoiceDetector (..),
  SpeechSegment (..),
  detectSegments,
  defaultVad,
  withVad,
) where

import Control.Applicative (Applicative (liftA2))
import Control.Exception (bracket, finally)
import Control.Monad (join)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.Int (Int32)
import Data.Vector.Storable (Storable, Vector)
import qualified Data.Vector.Storable as Vector
import Foreign (Ptr, Storable (..), free, malloc, nullPtr, peekArray, with)
import Foreign.C (CFloat (..))
import Foreign.Storable.Generic (GStorable)
import GHC.Generics (Generic)
import Silero.Model (SileroModel (..), sampleRate, withModel)
import UnliftIO (MonadUnliftIO)

data VoiceDetector = VoiceDetector
  { model :: SileroModel
  , startThreshold :: Float
  , endThreshold :: Float
  , minSpeechSamples :: Float
  , maxSpeechSamples :: Float
  , speechPadSamples :: Float
  , minSilenceSamples :: Float
  , minSilenceSamplesAtMaxSpeech :: Float
  }
  deriving (Generic, GStorable)

-- |
-- Create a **VoiceDetector**.
-- **Warning: SileroModel holds internal state and is NOT thread safe.**
defaultVad :: SileroModel -> VoiceDetector
defaultVad model =
  VoiceDetector
    { model = model
    , startThreshold = 0.5
    , endThreshold = 0.35
    , minSpeechSamples = fromIntegral sampleRate / 1000.0 * 250.0 -- 250ms.
    , maxSpeechSamples = 1.0 / 0.0 -- Infinity.
    , speechPadSamples = fromIntegral sampleRate / 1000.0 * 30.0 -- 30ms.
    , minSilenceSamples = fromIntegral sampleRate / 1000.0 * 100.0 -- 100ms.
    , minSilenceSamplesAtMaxSpeech = fromIntegral sampleRate / 1000.0 * 98.0 -- 98ms
    }

-- |
-- Create a **VoiceDetector**.
-- **Warning: SileroModel holds internal state and is NOT thread safe.**
withVad :: (MonadUnliftIO m) => (VoiceDetector -> m a) -> m a
withVad runVad = withModel (runVad . defaultVad)

data SpeechSegment = SpeechSegment
  { startIndex :: Int32
  , endIndex :: Int32
  , startTime :: CFloat
  , endTime :: CFloat
  }
  deriving (Show, Read, Eq, Ord, Generic, GStorable)

foreign import ccall "detector.h detect_segments"
  c_detect_segments ::
    Ptr VoiceDetector ->
    Int -> -- samplesLength
    Ptr Float -> -- samples
    Ptr Int -> -- outSegmentsLength
    Ptr (Ptr SpeechSegment) -> -- outSegments
    IO ()

-- |
-- Detect the segments where speech starts and ends.
-- This implicitly resets the model after it finishes.
detectSegments :: (MonadIO m) => VoiceDetector -> Vector Float -> m [SpeechSegment]
detectSegments vad samples =
  liftIO . with vad $ \vadPtr ->
    Vector.unsafeWith samples $ \samplesPtr ->
      withPtr @Int $ \segmentsLengthPtr ->
        withPtr @(Ptr SpeechSegment) $ \segmentsPtr -> do
          poke samplesPtr 0
          poke segmentsPtr nullPtr
          flip finally (free =<< peek segmentsPtr) $ do
            c_detect_segments
              vadPtr
              (Vector.length samples)
              samplesPtr
              segmentsLengthPtr
              segmentsPtr
            join $
              liftA2
                peekArray
                (peek segmentsLengthPtr)
                (peek segmentsPtr)
  where
    withPtr :: forall a b. (Storable a) => (Ptr a -> IO b) -> IO b
    withPtr = bracket (malloc @a) free