packages feed

reanimate-0.3.2.1: examples/voice_transcript.hs

#!/usr/bin/env stack
-- stack --resolver lts-15.04 runghc --package reanimate
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE RecordWildCards #-}
module Main where

import           Codec.Picture.Types
import           Control.Monad
import           Data.Hashable
import           Data.Aeson
import           Data.Char
import           System.IO.Unsafe
import           Data.Function
import           Data.List
import           Data.Maybe
import           Data.Ratio
import qualified Data.Text                     as T
import           Data.Tuple
import qualified Data.Vector                   as V
import           Debug.Trace
import           Reanimate
import           Reanimate.Animation
import           Reanimate.Interpolate
import           Reanimate.Svg
import           Graphics.SvgTree                         ( Texture(..)
                                                          , ElementRef(..)
                                                          )

data Transcript = Transcript
  { transcriptText  :: T.Text
  , transcriptWords :: [TWord]
  } deriving (Show)

instance FromJSON Transcript where
  parseJSON =
    withObject "transcript" $ \o -> Transcript <$> o .: "transcript" <*> o .: "words"

data TWord = TWord
  { wordAligned     :: T.Text
  , wordCase        :: T.Text
  , wordStart       :: Double
  , wordStartOffset :: Int
  , wordEnd         :: Double
  , wordEndOffset   :: Int
  , wordPhones      :: [Phone]
  , wordReference   :: T.Text
  } deriving (Show)

instance FromJSON TWord where
  parseJSON = withObject "word" $ \o ->
    TWord
      <$> o
      .:? "alignedWord"
      .!= T.empty
      <*> o
      .:  "case"
      <*> o
      .:? "start"
      .!= 0
      <*> o
      .:  "startOffset"
      <*> o
      .:? "end"
      .!= 0
      <*> o
      .:  "endOffset"
      <*> o
      .:? "phones"
      .!= []
      <*> o
      .:  "word"

data Phone = Phone
  { phoneDuration :: Double
  , phoneType     :: T.Text
  } deriving (Show)

instance FromJSON Phone where
  parseJSON = withObject "phone" $ \o -> Phone <$> o .: "duration" <*> o .: "phone"

-- transcript :: Transcript
-- transcript = case unsafePerformIO (decodeFileStrict "voice_transcript.json") of
--   Nothing -> error "bad json"
--   Just t  -> t

transcript :: Transcript
transcript = fakeTranscript
  "This is a fake transcript.\n\n\n\
  \No audio has been recorded\n\n\
  \and the timings are guessed."

data Token = TokenWord Int Int T.Text | TokenComma | TokenPeriod | TokenParagraph
  deriving (Show)

lexText :: T.Text -> [Token]
lexText = worker 0
 where
  worker offset txt = case T.uncons txt of
    Nothing -> []
    Just (c, cs)
      | isSpace c
      -> let (w, rest) = T.span (== '\n') txt
         in  if T.length w >= 3
               then TokenParagraph : worker (offset + T.length w) rest
               else worker (offset + 1) cs
      | c == '.'
      -> TokenPeriod : worker (offset + 1) cs
      | c == ','
      -> TokenComma : worker (offset + 1) cs
      | isAlphaNum c
      -> let (w, rest) = T.span isAlphaNum txt
             newOffset = offset + T.length w
         in  TokenWord offset newOffset w : worker newOffset rest
      | otherwise
      -> worker (offset + 1) cs

fakeTranscript :: T.Text -> Transcript
fakeTranscript input = Transcript { transcriptText  = input
                                  , transcriptWords = worker 0 (lexText input)
                                  }
 where
  worker now []             = []
  worker now (token : rest) = case token of
    TokenWord start end w ->
      let duration = realToFrac (end-start) * 0.1
      in  TWord { wordAligned     = T.toLower w
                , wordCase        = "success"
                , wordStart       = now
                , wordStartOffset = start
                , wordEnd         = now + duration
                , wordEndOffset   = end
                , wordPhones      = []
                , wordReference   = w
                }
            : worker (now + duration) rest
    TokenComma     -> worker (now + commaPause) rest
    TokenPeriod    -> worker (now + periodPause) rest
    TokenParagraph -> worker (now + paragraphPause) rest
  wpm            = 130
  paragraphPause = 0.5
  commaPause     = 0.1
  periodPause    = 0.2

-- tweenVar :: Var s a -> Duration -> (a -> Time -> a) -> Scene s ()
-- interpolateRGBA8 :: ColorComponents -> PixelRGBA8 -> PixelRGBA8 -> (Double -> PixelRGBA8)
toColor :: String -> PixelRGBA8
toColor c = case mkColor c of
  ColorRef pixel -> pixel

main :: IO ()
main = reanimate $ sceneAnimation $ do
  newSpriteSVG_ $ mkBackground "black"
  waitOn $ forM_ (transcriptGlyphs transcript) $ \(svg, tword) -> do
    highlighted <- newVar 0
    s <- newSprite $ do
      v <- unVar highlighted
      pure $ translate (-2) 2 $ scale 0.5 $ mkGroup
        [ maskedIn v svg (withFillColor "white" $ mkRect (svgWidth svg) screenHeight)
        , maskedOut v svg (withFillColor "grey" $ mkRect (svgWidth svg) screenHeight)
        ]
    fork $ do
      wait (wordStart tword)
      let dur = wordEnd tword - wordStart tword
      tweenVar highlighted dur $ \v -> fromToS v 1
  wait 2

maskedIn :: Double -> SVG -> SVG -> SVG
maskedIn t maskSVG targetSVG = mkGroup
  [ mkClipPath label $ removeGroups maskSVG
  , withClipPathRef (Ref label) $ translate (x-w/2 + w * t) y targetSVG
  ]
  where
    label = "word-mask-" ++ show (hash $ renderTree maskSVG)
    (x, y, w, _h) = boundingBox maskSVG

maskedOut :: Double -> SVG -> SVG -> SVG
maskedOut t maskSVG targetSVG = mkGroup
  [ mkClipPath label $ removeGroups maskSVG
  , withClipPathRef (Ref label) $ translate (x+w/2 + w * t) y targetSVG
  ]
  where
    label = "word-mask-" ++ show (hash (renderTree maskSVG, renderTree targetSVG))
    (x, y, w, _h) = boundingBox maskSVG

-- svgGlyphs :: Tree -> [(Tree -> Tree, DrawAttributes, Tree)]
transcriptGlyphs :: Transcript -> [(SVG, TWord)]
transcriptGlyphs Transcript {..}
  | T.length textSymbols /= length gls
  = error "Bad size"
  | otherwise
  = [ ( mkGroup $ take (wordEndOffset - wordStartOffset) $ drop
        (wordStartOffset - spaces)
        gls
      , tword
      )
    | tword@TWord {..} <- transcriptWords
    , let spaces = nSpaces wordStartOffset
    ]
 where
  nSpaces limit = T.length (T.filter isSpace (T.take limit transcriptText))
  textSymbols = T.filter (not . isSpace) transcriptText
  total       = center $ simplify $ latex transcriptText
  gls         = [ ctx g | (ctx, _attr, g) <- svgGlyphs total ]

forceLayout txt =
  fst $ splitGlyphs [0, 1, 2, 3] (latex $ "\\fbox{\\phantom{TyhILW}}" <> txt)

alignText :: SVG -> SVG
alignText txt = translate 0 (svgHeight ref / 2) $ centerX txt
  where ref = latex "\\fbox{Thy}"

-- abc, width=14.92 height=7.02
-- Thy, width=32.73 height=8.95