defmodule Whispex do
@moduledoc """
Create and execute whisper CLI commands.
Note that adding options is backwards from using
the whisper CLI; when using whisper CLI, you specify the options
before each file.
But with Whispex (this library), you add the file first, then
add the relevant options afterward.
## Example
import Whispex
use Whispex.Options
command =
Whispex.new_command
|> add_input_file("/path/to/input.avi")
|> add_file_option(option_language("English"))
|> add_file_option(option_output_dir("other_dir"))
|> add_file_option(option_output_format("vtt"))
|> add_file_option(option_model("tiny"))
:ok = execute(command)
"""
alias Whispex.Command
alias Whispex.File
@doc """
Begin a new blank (no options) whisper command.
"""
def new_command, do: %Command{}
@doc """
Add an input file to the command.
"""
def add_input_file(%Command{files: files} = command, %File{} = file) do
file = %File{file | type: :input}
%Command{command | files: [file | files]}
end
def add_input_file(%Command{files: files} = command, file_path) when is_binary(file_path) do
file = %File{type: :input, path: file_path}
%Command{command | files: [file | files]}
end
@doc """
Add an output file to the command.
"""
def add_output_file(%Command{files: files} = command, %File{} = file) do
file = %File{file | type: :output}
%Command{command | files: [file | files]}
end
def add_output_file(%Command{files: files} = command, file_path) when is_binary(file_path) do
file = %File{type: :output, path: file_path}
%Command{command | files: [file | files]}
end
@doc """
Add a per-file option to the command.
Applies to the most recently added file.
"""
def add_file_option(
%Command{files: [file | files]} = command,
option
) do
file = %File{file | options: [option | file.options]}
%Command{command | files: [file | files]}
end
@doc """
Execute the command using whisper CLI.
Returns `{:ok, output}` on success, or `{:error, {cmd_output, exit_status}}` on error.
"""
@spec execute(command :: Command.t()) ::
{:ok, binary()} | {:error, {Collectable.t(), exit_status :: non_neg_integer}}
def execute(%Command{} = command) do
{executable, args} = prepare(command)
Rambo.run(executable, args, log: false)
|> format_output()
end
@doc """
Prepares the command to be executed, by converting the `%Command{}` into
proper parameters to be feeded to `System.cmd/3` or `Port.open/2`.
Under normal circumstances `Whispex.execute/1` should be used, use `prepare`
only when converted args are needed to be feeded in a custom execution method.
"""
@spec prepare(command :: Command.t()) :: {binary() | nil, list(binary)}
def prepare(%Command{files: files}) do
cmd_args = List.flatten([options_list(files)])
{whisper_path(), cmd_args}
end
defp options_list(files) do
input_files = Enum.filter(files, fn %File{type: type} -> type == :input end)
output_files = Enum.filter(files, fn %File{type: type} -> type == :output end)
options_list(input_files, output_files)
end
defp options_list(input_files, output_files, acc \\ [])
defp options_list([], [], acc), do: List.flatten(acc)
defp options_list(input_files, [output_file | output_files], acc) do
acc = [File.command_arguments(output_file), output_file.path | acc]
options_list(input_files, output_files, acc)
end
defp options_list([input_file | input_files], [], acc) do
acc = [File.command_arguments(input_file), input_file.path | acc]
options_list(input_files, [], acc)
end
defp format_output({:ok, %{out: stdout}}), do: {:ok, stdout}
defp format_output({:error, reason}) when is_binary(reason), do: {:error, {reason, 1}}
defp format_output({:error, %{err: stderr, status: exit_status}}),
do: {:error, {stderr, exit_status}}
# Read whisper path from config. If unspecified, assume `whisper` is in env $PATH.
defp whisper_path do
case Application.get_env(:whisper, :whisper_path, nil) do
nil -> System.find_executable("whisper")
path -> path
end
end
end