Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changes/018-video-extract-audio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
group: platform
component: Integrations
impact: compatible
category: Added
---

## Summary

You can now pull the audio out of an uploaded video with the new
`Integration.Video.ExtractAudio` integration. Give it the video's file
reference and it returns the audio as a mono 16 kHz WAV (`audio/wav`), ready
to pass to `Integration.Audio.Transcribe` with `mimeType = "audio/wav"`. By
default it allows 180 seconds and stops at 25 MB; both limits are in the
config. It needs `ffmpeg` on the machine that runs your app (for example
`brew install ffmpeg`). If `ffmpeg` is missing, the action fails with a
message that tells you to install it. If the video itself cannot be
converted, your `onError` command receives the reason.

Raise the dispatcher's `eventProcessingTimeoutMs` above the extraction
timeout, for example to `Just 200000`. Its 30-second default would otherwise
stop a slow extraction before your `onSuccess` or `onError` command is sent.
To keep the audio in a command, store it as Base64 text
(`result.audio |> Bytes.toBase64 |> Text.fromBytes`). The new
`Bytes.fromBase64` turns it back into the exact audio bytes
(`audioBase64 |> Text.toBytes |> Bytes.fromBase64`) and returns an error
instead of different bytes if the text is not valid Base64.

Existing applications do not need any changes. To verify locally, run
`./dev test 'Integration.Video' nhintegrations-test` and confirm the
extraction examples pass. They use a stand-in for `ffmpeg`, so you do not
need it installed to run them.
4 changes: 3 additions & 1 deletion .hlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@
within: [Command, Config, Console, Environment, Log, Main, Service.Integration.Selection],
message: "Use Core `Environment`. New exceptions need a justification + belongs-in note here."}
# Directory/Path
# Integration.Video.ExtractAudio.Internal: needs the OS temp directory for
# the video/WAV temp files; belongs-in: Core `Directory` (getTemporary).
- {name: [System.Directory],
within: [Directory, Environment, File, Integration.Pdf.ExtractText.Internal, Subprocess],
within: [Directory, Environment, File, Integration.Pdf.ExtractText.Internal, Integration.Video.ExtractAudio.Internal, Subprocess],
message: "Use Core `Directory`/`Path`. New exceptions need a justification + belongs-in note here."}
# Json
- {name: [Data.Aeson, Data.Aeson.Key, Data.Aeson.KeyMap, Data.Aeson.Types, Data.Aeson.Encoding],
Expand Down
102 changes: 51 additions & 51 deletions codemap/api-hot.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

## Task

- `yield :: value -> Task w value` <!-- 596 call sites -->
- `ignoreError :: Task err Unit -> Task w Unit` <!-- 240 call sites -->
- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 224 call sites -->
- `throw :: err -> Task err w` <!-- 211 call sites -->
- `fromIO :: IO value -> Task w value` <!-- 130 call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 88 call sites -->
- `yield :: value -> Task w value` <!-- 602 call sites -->
- `ignoreError :: Task err Unit -> Task w Unit` <!-- 242 call sites -->
- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 229 call sites -->
- `throw :: err -> Task err w` <!-- 216 call sites -->
- `fromIO :: IO value -> Task w value` <!-- 131 call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 89 call sites -->
- `when :: Bool -> Task err Unit -> Task err Unit` <!-- 31 call sites -->
- `forEach :: (element -> Task err Unit) -> Array element -> Task err Unit` <!-- 30 call sites -->
- `fromFailableIO :: Exception exception => IO result -> Task exception result` <!-- 24 call sites -->
Expand All @@ -25,7 +25,7 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

- `map :: (a -> b) -> Array a -> Array b` <!-- 101 call sites -->
- `>>> map sqrt (fromLinkedList [1,4,9] :: Array Float)` → `Array [1.0,2.0,3.0]`
- `fromLinkedList :: LinkedList a -> Array a` <!-- 92 call sites -->
- `fromLinkedList :: LinkedList a -> Array a` <!-- 93 call sites -->
- `empty :: Array a` <!-- 65 call sites -->
- `>>> empty :: Array Int` → `Array []`
- `toLinkedList :: Array a -> LinkedList a` <!-- 59 call sites -->
Expand All @@ -45,12 +45,12 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

## Text

- `fromLinkedList :: LinkedList Char -> Text` <!-- 90 call sites -->
- `fromLinkedList :: LinkedList Char -> Text` <!-- 91 call sites -->
- `toLinkedList :: Text -> LinkedList Char` <!-- 78 call sites -->
- `toBytes :: Text -> Bytes` <!-- 66 call sites -->
- `replace :: Text -> Text -> Text -> Text` <!-- 33 call sites -->
- `trim :: Text -> Text` <!-- 28 call sites -->
- `fromBytes :: Bytes -> Text` <!-- 27 call sites -->
- `trim :: Text -> Text` <!-- 27 call sites -->
- `joinWith :: Text -> Array Text -> Text` <!-- 26 call sites -->
- `isEmpty :: Text -> Bool` <!-- 25 call sites -->
- `append :: Text -> Text -> Text` <!-- 22 call sites -->
Expand Down Expand Up @@ -130,16 +130,16 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

## Maybe

- `withDefault :: a -> Maybe a -> a` <!-- 56 call sites -->
- `withDefault :: a -> Maybe a -> a` <!-- 57 call sites -->
- `map :: (a -> b) -> Maybe a -> Maybe b` <!-- 14 call sites -->
- `getOrDie :: HasCallStack => Maybe a -> a` <!-- 9 call sites -->
- `andThen :: (a -> Maybe b) -> Maybe a -> Maybe b` <!-- 4 call sites -->

## Integration

- `action :: (ActionContext -> Task IntegrationError (Maybe CommandPayload)) -> Action` <!-- 26 call sites -->
- `action :: (ActionContext -> Task IntegrationError (Maybe CommandPayload)) -> Action` <!-- 27 call sites -->
- `outbound :: ToAction config => config -> Action` <!-- 17 call sites -->
- `emitCommand :: forall command (name :: Symbol). (ToJSON command, name ~ NameOf command, KnownSymbol name) => command -> Task IntegrationError (Maybe CommandPayload)` <!-- 10 call sites -->
- `emitCommand :: forall command (name :: Symbol). (ToJSON command, name ~ NameOf command, KnownSymbol name) => command -> Task IntegrationError (Maybe CommandPayload)` <!-- 12 call sites -->
- `toAction :: ToAction config => config -> Action` <!-- 6 call sites -->
- `fromMap :: Map Text ValidatedOAuth2ProviderConfig -> ImmutableProviderRegistry` <!-- 4 call sites -->
- `runAction :: ActionContext -> Action -> Task IntegrationError (Maybe CommandPayload)` <!-- 4 call sites -->
Expand Down Expand Up @@ -167,6 +167,17 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
- `load :: HasParser config => Task Text config` <!-- 1 call sites -->
- `secret :: FieldDef -> FieldDef` <!-- 1 call sites -->

## Uuid

- `generate :: Task w Uuid` <!-- 22 call sites -->
- `toText :: Uuid -> Text` <!-- 10 call sites -->
- `nil :: Uuid` <!-- 6 call sites -->
- `toLegacy :: Uuid -> UUID` <!-- 5 call sites -->
- `fromText :: Text -> Maybe Uuid` <!-- 3 call sites -->
- `generateV5 :: Uuid -> Text -> Uuid` <!-- 3 call sites -->
- `>>> Uuid.fromText "6ba7b810-9dad-11d1-80b4-00c04fd430c8" |> Maybe.getOrDie |> (\ns -> Uuid.generateV5 ns "python.org") |> Uuid.toText` → `"886313e1-3b8a-5372-9b90-0c9aee199e5d"`
- `fromLegacy :: UUID -> Uuid` <!-- 2 call sites -->

## LinkedList

- `map :: (a -> b) -> LinkedList a -> LinkedList b` <!-- 15 call sites -->
Expand All @@ -180,17 +191,6 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
- `all :: (a -> Bool) -> LinkedList a -> Bool` <!-- 1 call sites -->
- `concat :: LinkedList (LinkedList a) -> LinkedList a` <!-- 1 call sites -->

## Uuid

- `generate :: Task w Uuid` <!-- 21 call sites -->
- `toText :: Uuid -> Text` <!-- 9 call sites -->
- `nil :: Uuid` <!-- 6 call sites -->
- `toLegacy :: Uuid -> UUID` <!-- 5 call sites -->
- `fromText :: Text -> Maybe Uuid` <!-- 3 call sites -->
- `generateV5 :: Uuid -> Text -> Uuid` <!-- 3 call sites -->
- `>>> Uuid.fromText "6ba7b810-9dad-11d1-80b4-00c04fd430c8" |> Maybe.getOrDie |> (\ns -> Uuid.generateV5 ns "python.org") |> Uuid.toText` → `"886313e1-3b8a-5372-9b90-0c9aee199e5d"`
- `fromLegacy :: UUID -> Uuid` <!-- 2 call sites -->

## AsyncTask

- `sleep :: Int -> Task w Unit` <!-- 18 call sites -->
Expand All @@ -204,11 +204,11 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
## Path

- `toLinkedList :: Path -> LinkedList Char` <!-- 18 call sites -->
- `fromText :: Text -> Maybe Path` <!-- 9 call sites -->
- `toText :: Path -> Text` <!-- 7 call sites -->
- `append :: Path -> Path -> Path` <!-- 3 call sites -->
- `fromText :: Text -> Maybe Path` <!-- 10 call sites -->
- `toText :: Path -> Text` <!-- 9 call sites -->
- `append :: Path -> Path -> Path` <!-- 4 call sites -->
- `fromLinkedList :: LinkedList Char -> Maybe Path` <!-- 3 call sites -->
- `joinPaths :: Array Path -> Path` <!-- 3 call sites -->
- `fromLinkedList :: LinkedList Char -> Maybe Path` <!-- 2 call sites -->
- `endsWith :: Text -> Path -> Bool` <!-- 1 call sites -->

## ConcurrentMap
Expand Down Expand Up @@ -283,7 +283,7 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
- `fmap :: Functor f => (a -> b) -> f a -> f b` <!-- 17 call sites -->
- `map :: Mappable mappable => (typeA -> typeB) -> mappable typeA -> mappable typeB` <!-- 16 call sites -->

*cut: 103 more modules (AtomicVar (26), Integration.Http (26), Decider (21), Result (21), Lock (20), …) — full surface: codemap/signatures/*
*cut: 103 more modules (AtomicVar (26), Integration.Http (26), Result (24), Decider (21), Lock (20), …) — full surface: codemap/signatures/*

---

Expand All @@ -294,35 +294,35 @@ when WRITING TESTS; they are not feature-code frequency signal.

## Task

- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 824 test call sites -->
- `yield :: value -> Task w value` <!-- 466 test call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 283 test call sites -->
- `mapArray :: (element -> Task err output) -> Array element -> Task err (Array output)` <!-- 126 test call sites -->
- `throw :: err -> Task err w` <!-- 118 test call sites -->
- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 828 test call sites -->
- `yield :: value -> Task w value` <!-- 483 test call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 286 test call sites -->
- `mapArray :: (element -> Task err output) -> Array element -> Task err (Array output)` <!-- 127 test call sites -->
- `throw :: err -> Task err w` <!-- 124 test call sites -->
- `andThen :: (input -> Task err output) -> Task err input -> Task err output` <!-- 92 test call sites -->
- `fromIO :: IO value -> Task w value` <!-- 90 test call sites -->
- `map :: (input -> output) -> Task err input -> Task err output` <!-- 83 test call sites -->
- `map :: (input -> output) -> Task err input -> Task err output` <!-- 84 test call sites -->

## Array

- `length :: Array a -> Int` <!-- 265 test call sites -->
- `empty :: Array a` <!-- 221 test call sites -->
- `fromLinkedList :: LinkedList a -> Array a` <!-- 220 test call sites -->
- `map :: (a -> b) -> Array a -> Array b` <!-- 136 test call sites -->
- `get :: Int -> Array a -> Maybe a` <!-- 81 test call sites -->
- `wrap :: a -> Array a` <!-- 56 test call sites -->
- `length :: Array a -> Int` <!-- 276 test call sites -->
- `empty :: Array a` <!-- 231 test call sites -->
- `fromLinkedList :: LinkedList a -> Array a` <!-- 221 test call sites -->
- `map :: (a -> b) -> Array a -> Array b` <!-- 138 test call sites -->
- `get :: Int -> Array a -> Maybe a` <!-- 89 test call sites -->
- `wrap :: a -> Array a` <!-- 60 test call sites -->
- `initialize :: Int -> (Int -> a) -> Array a` <!-- 47 test call sites -->
- `contains :: Eq value => value -> Array value -> Bool` <!-- 41 test call sites -->

## Text

- `contains :: Text -> Text -> Bool` <!-- 436 test call sites -->
- `toBytes :: Text -> Bytes` <!-- 89 test call sites -->
- `contains :: Text -> Text -> Bool` <!-- 450 test call sites -->
- `toBytes :: Text -> Bytes` <!-- 98 test call sites -->
- `fromLinkedList :: LinkedList Char -> Text` <!-- 58 test call sites -->
- `length :: Text -> Int` <!-- 41 test call sites -->
- `repeat :: Int -> Text -> Text` <!-- 35 test call sites -->
- `toLinkedList :: Text -> LinkedList Char` <!-- 29 test call sites -->
- `startsWith :: Text -> Text -> Bool` <!-- 18 test call sites -->
- `startsWith :: Text -> Text -> Bool` <!-- 19 test call sites -->
- `all :: (Char -> Bool) -> Text -> Bool` <!-- 10 test call sites -->

## Parser
Expand All @@ -340,11 +340,11 @@ when WRITING TESTS; they are not feature-code frequency signal.

- `toJSON :: ToJSON a => a -> Value` <!-- 146 test call sites -->
- `decodeText :: FromJSON value => Text -> Result Text value` <!-- 137 test call sites -->
- `encodeText :: ToJSON value => value -> Text` <!-- 117 test call sites -->
- `encodeText :: ToJSON value => value -> Text` <!-- 120 test call sites -->
- `object :: [(Text, Value)] -> Value` <!-- 105 test call sites -->
- `null :: Value` <!-- 58 test call sites -->
- `encode :: ToJSON value => value -> Value` <!-- 55 test call sites -->
- `decode :: FromJSON value => Value -> Result Text value` <!-- 29 test call sites -->
- `encode :: ToJSON value => value -> Value` <!-- 56 test call sites -->
- `decode :: FromJSON value => Value -> Result Text value` <!-- 30 test call sites -->
- `yield :: value -> Parser value` <!-- 4 test call sites -->

## Layout
Expand All @@ -360,9 +360,9 @@ when WRITING TESTS; they are not feature-code frequency signal.

## ConcurrentVar

- `containing :: value -> Task w (ConcurrentVar value)` <!-- 125 test call sites -->
- `modify :: (value -> value) -> ConcurrentVar value -> Task w Unit` <!-- 120 test call sites -->
- `peek :: ConcurrentVar value -> Task w value` <!-- 102 test call sites -->
- `containing :: value -> Task w (ConcurrentVar value)` <!-- 126 test call sites -->
- `modify :: (value -> value) -> ConcurrentVar value -> Task w Unit` <!-- 121 test call sites -->
- `peek :: ConcurrentVar value -> Task w value` <!-- 104 test call sites -->
- `get :: ConcurrentVar value -> Task w value` <!-- 39 test call sites -->
- `set :: value -> ConcurrentVar value -> Task w ()` <!-- 15 test call sites -->
- `new :: forall value w. Task w (ConcurrentVar value)` <!-- 14 test call sites -->
Expand All @@ -372,9 +372,9 @@ when WRITING TESTS; they are not feature-code frequency signal.
## Uuid

- `generate :: Task w Uuid` <!-- 182 test call sites -->
- `nil :: Uuid` <!-- 107 test call sites -->
- `nil :: Uuid` <!-- 108 test call sites -->
- `toText :: Uuid -> Text` <!-- 45 test call sites -->
- `generateV5 :: Uuid -> Text -> Uuid` <!-- 19 test call sites -->
- `fromText :: Text -> Maybe Uuid` <!-- 3 test call sites -->

*cut: 138 more modules (Map (257), Service.Application (250), Set (233), AsyncTask (223), Stream (212), …) — full surface: codemap/signatures/*
*cut: 140 more modules (Map (260), Service.Application (250), Set (233), AsyncTask (225), Stream (212), …) — full surface: codemap/signatures/*
35 changes: 34 additions & 1 deletion codemap/phrasebook.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ Array [0]
[]
```

```haskell
>>> "aGVsbG8=" |> Text.toBytes |> Bytes.fromBase64 |> Result.map Text.fromBytes
Ok "hello"
```

```haskell
>>> "aGVsbG8" |> Text.toBytes |> Bytes.fromBase64 |> Result.isErr
True
```

### Float

```haskell
Expand Down Expand Up @@ -194,6 +204,29 @@ Set (fromList [1,2,3])
"Cart"
```

## outbound-integrations

### Integration.Video.ExtractAudio

```haskell
>>> defaultConfig.timeoutSeconds
180
```

```haskell
>>> defaultConfig.sampleRateHz
16000
```

### Integration.Video.ExtractAudio.Internal

```haskell
>>> import Array qualified
>>> import Integration.Video.ExtractAudio (defaultConfig)
>>> Array.toLinkedList (buildFfmpegArgs defaultConfig "in.mp4" "out.wav")
["-nostdin","-v","error","-i","in.mp4","-vn","-ac","1","-ar","16000","-fs","25000000","out.wav"]
```

## postgres-infra

### Service.Infra.Postgres.SslMode
Expand Down Expand Up @@ -237,4 +270,4 @@ True

---

*32 example sessions · modules with ZERO doctests: 315 (the documentation backlog — see codemap/.doc-ratchet)*
*37 example sessions · modules with ZERO doctests: 315 (the documentation backlog — see codemap/.doc-ratchet)*
1 change: 1 addition & 0 deletions codemap/signatures/nhcore-core.txt
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ drop :: Int -> Bytes -> Bytes
dropEnd :: Int -> Bytes -> Bytes
empty :: Bytes
findSubstring :: Bytes -> Bytes -> Maybe Int
fromBase64 :: Bytes -> Result Text Bytes
fromLazyLegacy :: LazyByteString -> Bytes
fromLegacy :: ByteString -> Bytes
getRandom :: Int -> Task w Bytes
Expand Down
39 changes: 39 additions & 0 deletions codemap/signatures/nhintegrations.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1881,3 +1881,42 @@ buildPdftotextArgs :: Config -> Path -> Array Text
extractPageCount :: Text -> Int
instance (Data.Aeson.Types.ToJSON.ToJSON command, GHC.TypeLits.KnownSymbol (Service.Command.Core.NameOf command)) => Integration.ToAction (Integration.Pdf.ExtractText.Request command)
parsePdfInfo :: Text -> PdfMetadata

module Integration.Video.ExtractAudio
Config :: Int -> Int -> Int -> Int -> Maybe Text -> Config
ExtractionResult :: Bytes -> Text -> ExtractionResult
Request :: FileRef -> Config -> (ExtractionResult -> command) -> (Text -> command) -> Request command
[audio] :: ExtractionResult -> Bytes
[channels] :: Config -> Int
[config] :: Request command -> Config
[ffmpegPath] :: Config -> Maybe Text
[fileRef] :: Request command -> FileRef
[maxOutputBytes] :: Config -> Int
[mimeType] :: ExtractionResult -> Text
[onError] :: Request command -> Text -> command
[onSuccess] :: Request command -> ExtractionResult -> command
[sampleRateHz] :: Config -> Int
[timeoutSeconds] :: Config -> Int
data Config
data ExtractionResult
data Request command
defaultConfig :: Config
instance GHC.Classes.Eq Integration.Video.ExtractAudio.Config
instance GHC.Classes.Eq Integration.Video.ExtractAudio.ExtractionResult
instance GHC.Generics.Generic (Integration.Video.ExtractAudio.Request command)
instance GHC.Generics.Generic Integration.Video.ExtractAudio.Config
instance GHC.Generics.Generic Integration.Video.ExtractAudio.ExtractionResult
instance GHC.Show.Show Integration.Video.ExtractAudio.Config
instance GHC.Show.Show Integration.Video.ExtractAudio.ExtractionResult

module Integration.Video.ExtractAudio.Internal
Runner :: (Text -> Task Error (Maybe Text)) -> (Int -> Text -> Array Text -> Path -> Task Error Completion) -> Runner
[run] :: Runner -> Int -> Text -> Array Text -> Path -> Task Error Completion
[which] :: Runner -> Text -> Task Error (Maybe Text)
buildFfmpegArgs :: Config -> Path -> Path -> Array Text
data Runner
executeExtraction :: (ToJSON command, KnownSymbol (NameOf command)) => Runner -> ActionContext -> Request command -> Task IntegrationError (Maybe CommandPayload)
extractAudio :: Runner -> Text -> FileAccessContext -> FileRef -> Config -> Task IntegrationError ExtractionResult
instance (Data.Aeson.Types.ToJSON.ToJSON command, GHC.TypeLits.KnownSymbol (Service.Command.Core.NameOf command)) => Integration.ToAction (Integration.Video.ExtractAudio.Request command)
realRunner :: Runner
resolveFfmpeg :: Runner -> Config -> Task IntegrationError Text
Loading
Loading