flashblast-0.0.1.0: app/Main.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
import Composite.Record
import qualified Data.Attoparsec.Text as A
import Dhall hiding (embed, string)
import Path
import Path.Dhall ()
import Path.Utils
import Polysemy
import Polysemy.Error as P
import Polysemy.Reader
import Polysemy.Video
import Data.Align
import RIO hiding (Reader, ask, asks, many,
runReader)
import RIO.List
import RIO.List.Partial
import qualified RIO.Map as Map
import qualified RIO.Text as T
import qualified Text.Subtitles.SRT as SR
import qualified Turtle as S
import FlashBlast.AnkiDB
import FlashBlast.ClozeParse
import FlashBlast.Conventions
data MultiClozeSpec = MultiClozeSpec {
phrases :: [Text]
, images :: [Path Rel File]
} deriving (Eq, Generic, Show)
instance FromDhall MultiClozeSpec
data YDLInfo = YDLInfo {
url :: Text
, out :: Path Rel File
, format :: Text
} deriving (Eq, Generic, Show)
instance FromDhall YDLInfo
data VideoSource = LocalVideo (Path Rel File) | YouTubeDL YDLInfo
deriving (Eq, Generic, Show)
instance FromDhall VideoSource
data ExcerptSpec = ExcerptSpec {
source :: VideoSource
, subs :: Text
, clipf :: Text -> Text
, audiof :: Text -> Text
, framef :: Text -> Text
} deriving Generic
instance FromDhall ExcerptSpec
data Locale = Locale Text
deriving (Eq, Show, Generic)
data ExportDirs = ExportDirs {
audio :: Path Rel Dir
, clips :: Path Rel Dir
, images :: Path Rel Dir
, notes :: Path Rel Dir
} deriving (Eq, Show, Generic)
instance FromDhall ExportDirs
fromTime :: SR.Time -> Time
fromTime (SR.Time h m s f) = Time h m s f
fromRange :: SR.Range -> Range
fromRange (SR.Range f t) = Range (fromTime f) (fromTime t)
data YouTubeDL m a where
YouTubeDL' :: Text -> Path Rel File -> Text -> YouTubeDL m ()
makeSem ''YouTubeDL
interpretYouTubeDL :: Member (Embed IO) effs => Sem (YouTubeDL ': effs) a -> Sem effs a
interpretYouTubeDL = interpret \case
YouTubeDL' x k f -> S.sh $ S.inproc "youtube-dl" [x, "-o", toFilePathText k, "-f", f] mempty
genExcerpts :: Members '[Error SomeException, YouTubeDL, ClipProcess, Reader ExportDirs] m => Path Rel Dir -> ExcerptSpec -> Sem m [RExcerptNote]
genExcerpts dir (ExcerptSpec {..}) = do
t <- case source of
YouTubeDL (YDLInfo x y f) -> do
youTubeDL' x (dir </> y) f
return (dir </> y)
LocalVideo x -> return (dir </> x)
ExportDirs{..} <- ask @ExportDirs
s' <- either (throwM . SubtitleParseException) return $ A.parseOnly SR.parseSRT subs
cs <- mapM (parseRelFile . T.unpack . clipf . T.pack . show . SR.index) s'
es <- mapM (parseRelFile . T.unpack . audiof . T.pack . show . SR.index) s'
fs <- mapM (parseRelFile . T.unpack . framef . T.pack . show . SR.index) s'
extractClips t $ zip (fromRange . SR.range <$> s') (clips </$> cs)
extractAudio t $ zip (fromRange . SR.range <$> s') (audio </$> es)
forM (zip4 s' cs es fs) $ \(l, c, e, f) -> do
extractFrames (clips </> c) $ [(Time 0 0 0 0, images </> f)]
return $ val @"front" (fst . genClozePhrase . SR.dialog $ l)
:& val @"extra" f
:& val @"back" e
:& RNil
newtype SubtitleParseException = SubtitleParseException String
deriving (Eq, Show, Generic)
instance Exception SubtitleParseException
instance Member (Error SomeException) r => MonadThrow (Sem r) where
throwM e = P.throw (toException e)
runExcerptSpecIO :: ResourceDirs -> ExportDirs -> [ExcerptSpec] -> Path Rel File -> IO ()
runExcerptSpecIO (ResourceDirs{..}) x xs out = do
zs <- sequenceA <$> forM xs \k -> do
runM . runError . runReader x . interpretYouTubeDL . interpretFFMpegCli $ genExcerpts video k
case zs of
Right a -> do
S.mktree . S.decodeString . toFilePath $ (notes x)
writeFileUtf8 (toFilePath (notes x </> out)) $ T.intercalate "\n" $ renderExcerptNote <$> join a
Left (SomeException p) -> throwIO p
genForvos :: MonadThrow m => Locale -> Text -> [Path Rel File] -> [Text] -> m RForvoNote
genForvos (Locale l) x zs as = do
ys' <- mapM (forvoConvention l) as
let ys = lpadZipWith (\a _ -> if isJust a then a else Nothing) ys' (replicate 16 ())
let k = ys !! 0 :*: ys !! 1 :*: ys !! 2 :*: ys !! 3 :*: ys !! 4 :*: ys !! 5 :*: ys !! 6 :*: ys !! 7 :*: ys !! 8 :*: ys !! 9 :*: ys !! 10 :*: ys !! 11 :*: ys !! 12 :*: ys !! 13 :*: ys !! 14 :*: ys !! 15 :*: RNil
return $ x :*: zs :*: k
runMultiClozeSpecIO :: Locale -> ResourceDirs -> ExportDirs -> [MultiClozeSpec] -> Path Rel File -> IO ()
runMultiClozeSpecIO l _ x xs out = do
zs <- forM xs \(MultiClozeSpec p f) -> do
forM p \a -> let (b, c) = genClozePhrase a
in genForvos l b f c
S.mktree . S.decodeString . toFilePath $ (notes x)
writeFileUtf8 (toFilePath (notes x </> out)) $ (T.intercalate "\n" $ renderForvoNote <$> join zs)
data ResourceDirs = ResourceDirs {
audio :: Path Rel Dir
, video :: Path Rel Dir
, images :: Path Rel Dir
} deriving (Eq, Show, Generic)
instance FromDhall ResourceDirs
data Deck = Deck {
resourceDirs :: ResourceDirs
, exportDirs :: ExportDirs
, parts :: [Part]
} deriving Generic
data Part = Part {
outfile :: Path Rel File
, spec :: Spec
} deriving Generic
instance FromDhall Deck
instance FromDhall Part
data BasicReversedSpec = BasicReversedSpec {
from :: VF
, from_extra :: VF
, to :: VF
, to_extra :: VF
} deriving (Eq, Show, Generic)
instance FromDhall BasicReversedSpec
instance FromDhall Locale
data ForvoSpec = ForvoSpec {
locale :: Locale
, spec :: [MultiClozeSpec]
} deriving (Eq, Show, Generic)
instance FromDhall ForvoSpec
data Spec =
Forvo ForvoSpec
| Excerpt [ExcerptSpec]
| BasicReversed [BasicReversedSpec]
| MinimalReversed [MinimalReversedSpec]
deriving Generic
instance FromDhall Spec
data WonkyConfig = WonkyConfig {
decks :: Map Text Deck
} deriving Generic
instance FromDhall WonkyConfig
data MinimalReversedSpec = MinimalReversedSpec {
from :: VF
, to :: VF
} deriving (Eq, Show, Generic)
instance FromDhall MinimalReversedSpec
runMinimalReversedIO :: ResourceDirs -> ExportDirs -> [MinimalReversedSpec] -> Path Rel File -> IO ()
runMinimalReversedIO _ x xs out = do
zs <- forM xs \MinimalReversedSpec{..} -> return $ val @"from" from :& val @"to" to :& RNil
S.mktree . S.decodeString . toFilePath $ notes x
writeFileUtf8 (toFilePath (notes x </> out)) $ (T.intercalate "\n" $ renderMinimalNoteVF <$> zs)
runBasicReversedIO :: ResourceDirs -> ExportDirs -> [BasicReversedSpec] -> Path Rel File -> IO ()
runBasicReversedIO ResourceDirs{..} x xs out = do
zs <- forM xs \BasicReversedSpec{..} -> return $ val @"from" from :& val @"from-extra" from_extra :& val @"to" to :& val @"to-extra" to_extra :& RNil
S.mktree . S.decodeString . toFilePath $ notes x
writeFileUtf8 (toFilePath (notes x </> out)) $ T.intercalate "\n" (renderBasicReversedNoteVF <$> zs)
runMakeDeck :: Deck -> IO ()
runMakeDeck Deck{..} = do
forM_ parts \(Part a p) -> case p of
Excerpt x -> runExcerptSpecIO resourceDirs exportDirs x a
Forvo (ForvoSpec l x) -> runMultiClozeSpecIO l resourceDirs exportDirs x a
MinimalReversed x -> runMinimalReversedIO resourceDirs exportDirs x a
BasicReversed x -> runBasicReversedIO resourceDirs exportDirs x a
main :: IO ()
main = do
x <- input auto "./index.dhall"
mapM_ runMakeDeck $ fmap snd . Map.toList . decks $ x