Machine learning, LiveView powered Desktop Applications
There are multiple ways to interact with your software and one of the most common ones is Desktop applications. It’s a very tricky area but a recent project has made it a lot easier - Tauri. In this post we’re going to build a Liveview applications that uses Bumblebee and a new proof of concept library to install Tauri in your Phoenix project called ex_tauri
In the beggining, there was the Web App
All the code is available at Grammarlocal repository
First we create a new application which I will be creating Grammarlocal the usual way.
mix phx.new grammarlocal --no-gettext --no-mailer --no-dashboard --no-gettext --no-ecto
Then we build a really basic UI using Liveview
defmodule GrammarlocalWeb.InputLive do
use GrammarlocalWeb, :live_view
def mount(_params, _session, socket) do
socket =
socket
|> assign(:output, "")
|> assign(:input, "")
|> assign(:loading, false)
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<textarea
phx-blur="run"
class="rounded-xl border border-slate-200 shadow w-full h-auto p-3 resize-none mb-10"
rows="10"
><%= @input %></textarea>
<div :if={@loading} class="w-full flex justify-center">
<img src={~p"/images/loading.png"} class="animate-spin-slow" width="42" />
</div>
<div :if={!@loading} class="w-full p-3"><%= @output %></div>
</div>
"""
end
end
Time to make it smart
Now we’ll need to make it smart by using our usual suspect (Bumblebee) and using a special model from Grammarly.
We load the model and prepare the serving before using it.
defmodule Grammarlocal.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
Nx.global_default_backend(EXLA.Backend)
{:ok, model} = Bumblebee.load_model({:hf, "grammarly/coedit-large"})
{:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, "grammarly/coedit-large"})
{:ok, generation_config} = Bumblebee.load_generation_config({:hf, "grammarly/coedit-large"})
generation_config = %{generation_config | max_new_tokens: 1500}
serving =
Bumblebee.Text.generation(model, tokenizer, generation_config,
defn_options: [compiler: EXLA]
)
children = [
GrammarlocalWeb.Telemetry,
{Phoenix.PubSub, name: Grammarlocal.PubSub},
GrammarlocalWeb.Endpoint,
{Nx.Serving, serving: serving, name: Grammarlocal.Serving, batch_timeout: 100}
]
opts = [strategy: :one_for_one, name: Grammarlocal.Supervisor]
Supervisor.start_link(children, opts)
end
end
Then we go back to our GrammarlocalWeb.InputLive
module and handle the events as expected
def handle_event("run", %{"value" => value}, socket) do
Task.async(fn -> Nx.Serving.batched_run({:local, Grammarlocal.Serving}, value) end)
{:noreply, socket |> assign(:input, value) |> assign(:loading, true)}
end
def handle_info({_, %{results: [%{text: text}]}}, socket) do
{:noreply, socket |> assign(:output, text) |> assign(:loading, false)}
end
def handle_info({:DOWN, _, _, _, :normal}, socket), do: {:noreply, socket}
This will run the model in a Task, capture the result from running said model and handle the Task shutdown process.
In the end, we have a web application that is able to receive text and run it against a model that will apply ortographic corrections based on the model we just provided it!
Desktop time
Now comes the “new thing” from this post - have this as a Desktop application you could install locally and distribute.
For that effect we will use Tauri. We could pull all dependencies and go along that path no issue but I’ve created a proof of concept to install Tauri in your project easily with the library ex_tauri.
How it works?
The ex_tauri will be your friend and install within your _build
folder the tauri-cli
utility that will be able to ramp up a tauri project for you.
After that we run the tauri cli which will package your Phoenix application with Burrito into a single binary to be used by tauri as a sidecar
In the end, tauri will be able to grab our Phoenix binary, start it up, wait for it to be up and running on the specified port and open a web view with your Liveview application in all it’s glory!
Let’s do it!
Let’s add our dependency
defp deps do
[
{:ex_tauri, git: "https://github.com/filipecabaco/ex_tauri"}
]
end
Add the config for tauri in our config.exs
config :ex_tauri,
version: "1.4.0",
app_name: "Example Desktop",
host: "localhost",
port: 4000
Then we need to add a release configuration to our mix.exs
def project do
[
app: :grammarlocal,
version: "0.1.0",
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
releases: [
# We need this to be named desktop for this PoC
desktop: [
steps: [:assemble, &Burrito.wrap/1],
burrito: [
targets: [
# At the moment we still need this really specific names
"aarch64-apple-darwin": [os: :darwin, cpu: :aarch64]
]
]
]
]
]
end
Now you just need to run mix tauri.install
which will install all the dependecies in your _build
folder and also create a new folder called src-tauri
that has all the required code to start up your tauri application.
Run it!
After you have everything installed, you can run the command mix ex_tauri dev
. All the arguments from mix ex_tauri
will be forwarded to the tauri cli so you can explore other commands like mix ex_tauri info
that gives you some information regarding your tauri application.
But with mix ex_tauri dev
you will be able to see your application up and running in your Desktop and have your own local Grammarly.
Web View vs Native
Both approaches to the same problem are fine as long as you are aware of the limitations and, more importantly, have fun building stuff regardless of the approach 😁.
Acknowledgments
- Tauri App for building it and providing a ton of support to find the right way to build the PoC
- Digit / Doawoo for Burrito which enables us to build the binary to be used as a sidecar
- Kevin Pan / Feng19 for their example that heavily inspired the approach taken with their phx_new_desktop repository
- yos for a great discussion and bringing Feng19 example into the mix (no pun intended)
- Jonatan Kłosko for helping me fix some misuses of Bumblebee on my side and just being an MVP in our community when it comes to ML
- Phoenix Framework Tailwind which was a big inspiration on the approach to be taken when it came to install an outside package and use it within an Elixir project
Conclusion
- LiveView makes building reactive applications so easy that it’s worth the effort to bring it to a new environment
- Bumblebee is still an excellent tool that makes ML approachable and easy to work with
- Tauri is awesome and greatly simplifies building and distributing apps
- A new (still badly coded) library to help you get started with Liveview Desktop applications is available 🎉
- This application was used to check some content of this blog 😆