jonatanklosko commited on
Commit
0fea377
·
1 Parent(s): b6f0124
.dockerignore ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .dockerignore
2
+
3
+ # Ignore git, but keep git HEAD and refs to access current commit hash if needed:
4
+ #
5
+ # $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat
6
+ # d0b8727759e1e0e7aa3d41707d12376e373d5ecc
7
+ .git
8
+ !.git/HEAD
9
+ !.git/refs
10
+
11
+ # Common development/test artifacts
12
+ /cover/
13
+ /doc/
14
+ /test/
15
+ /tmp/
16
+ .elixir_ls
17
+
18
+ # Mix artifacts
19
+ /_build/
20
+ /deps/
21
+ *.ez
22
+
23
+ # Generated on crash by the VM
24
+ erl_crash.dump
25
+
26
+ # Static artifacts - These should be fetched and built inside the Docker image
27
+ /assets/node_modules/
28
+ /priv/static/assets/
29
+ /priv/static/uploads/*
30
+ !/priv/static/uploads/.gitkeep
31
+ /priv/static/cache_manifest.json
.gitignore CHANGED
@@ -28,6 +28,10 @@ chai-*.tar
28
  # Ignore assets that are produced by build tools.
29
  /priv/static/assets/
30
 
 
 
 
 
31
  # Ignore digested assets cache.
32
  /priv/static/cache_manifest.json
33
 
 
28
  # Ignore assets that are produced by build tools.
29
  /priv/static/assets/
30
 
31
+ # Ignore uploaded files.
32
+ /priv/static/uploads/*
33
+ !/priv/static/uploads/.gitkeep
34
+
35
  # Ignore digested assets cache.
36
  /priv/static/cache_manifest.json
37
 
Dockerfile ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ARG ELIXIR_VERSION=1.14.2
2
+ ARG OTP_VERSION=25.3
3
+ ARG CUDA_VERSION=11.8.0
4
+
5
+ ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-ubuntu-jammy-20230126"
6
+ ARG RUNNER_IMAGE="nvidia/cuda:${CUDA_VERSION}-cudnn8-devel-ubuntu22.04"
7
+
8
+ # Stage 1: build
9
+ FROM ${BUILDER_IMAGE} as builder
10
+
11
+ # Install build dependencies
12
+ RUN apt-get update -y && apt-get install -y build-essential git curl \
13
+ && apt-get clean && rm -f /var/lib/apt/lists/*_*
14
+
15
+ WORKDIR /app
16
+
17
+ # Install hex and rebar
18
+ RUN mix local.hex --force && \
19
+ mix local.rebar --force
20
+
21
+ # Set build ENV
22
+ ENV MIX_ENV=prod
23
+ ENV XLA_TARGET=cuda118
24
+ ENV BUMBLEBEE_CACHE_DIR=/app/.bumblebee
25
+
26
+ # Install mix dependencies
27
+ COPY mix.exs mix.lock ./
28
+ RUN mix deps.get --only $MIX_ENV
29
+ RUN mkdir config
30
+
31
+ # Copy compile-time config files before we compile dependencies to
32
+ # ensure any relevant config change will trigger the dependencies to
33
+ # be re-compiled
34
+ COPY config/config.exs config/${MIX_ENV}.exs config/
35
+ RUN mix deps.compile
36
+
37
+ COPY priv priv
38
+
39
+ COPY lib lib
40
+
41
+ COPY assets assets
42
+
43
+ # Compile assets
44
+ RUN mix assets.deploy
45
+
46
+ # Compile the release
47
+ RUN mix compile
48
+
49
+ # Changes to config/runtime.exs don't require recompiling the code
50
+ COPY config/runtime.exs config/
51
+
52
+ # Start the app to download models into the cache
53
+ RUN SECRET_KEY_BASE="$(mix phx.gen.secret)" mix app.start
54
+
55
+ COPY rel rel
56
+ RUN mix release
57
+
58
+ # Stage 2: release image
59
+ FROM ${RUNNER_IMAGE}
60
+
61
+ # Install runtime dependencies
62
+ RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \
63
+ && apt-get clean && rm -f /var/lib/apt/lists/*_*
64
+
65
+ # Set the locale
66
+ RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
67
+
68
+ ENV LANG en_US.UTF-8
69
+ ENV LANGUAGE en_US:en
70
+ ENV LC_ALL en_US.UTF-8
71
+
72
+ WORKDIR /app
73
+
74
+ # See https://huggingface.co/docs/hub/spaces-sdks-docker#permissions
75
+ RUN useradd -m -u 1000 user
76
+ RUN chown user /app
77
+
78
+ # Set runner ENV
79
+ ENV MIX_ENV=prod
80
+ ENV XLA_TARGET=cuda118
81
+ ENV BUMBLEBEE_CACHE_DIR=/app/.bumblebee
82
+ ENV BUMBLEBEE_OFFLINE=true
83
+
84
+ # Only copy the final release from the build stage
85
+ COPY --from=builder --chown=user:root /app/.bumblebee/ ./.bumblebee
86
+ COPY --from=builder --chown=user:root /app/_build/${MIX_ENV}/rel/chai ./
87
+
88
+ USER user
89
+
90
+ CMD ["/app/bin/server"]
README.md CHANGED
@@ -1 +1,35 @@
 
 
 
 
 
 
 
 
 
1
  # Chai 🍵
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Chai
3
+ emoji: 🍵
4
+ colorFrom: pink
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
  # Chai 🍵
11
+
12
+ Chai is a demo web application built with [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view)
13
+ and showcasing multiple Neural Network models from the [Bumblebee](https://github.com/elixir-nx/bumblebee) package.
14
+
15
+ The app uses a combination of pre-trained models for the following features:
16
+
17
+ * **Conversation** using [Blenderbot](https://huggingface.co/facebook/blenderbot-400M-distill)
18
+ * **Speech transcription** using [Whisper](https://huggingface.co/openai/whisper-tiny)
19
+ * **Image captioning** using [BLIP](https://huggingface.co/Salesforce/blip-image-captioning-base)
20
+ * **Emotion recognition** using [RoBERTa](https://huggingface.co/j-hartmann/emotion-english-distilroberta-base)
21
+ * **Named entity recognition (NER)** using [BERT](https://huggingface.co/dslim/bert-base-NER)
22
+
23
+ ## Development
24
+
25
+ You need Erlang and Elixir installed, then:
26
+
27
+ ```shell
28
+ git clone https://huggingface.co/spaces/jonatanklosko/chai
29
+ cd chai
30
+
31
+ mix setup
32
+ mix phx.server
33
+ ```
34
+
35
+ Now you can visit [`localhost:4040`](http://localhost:4040) from your browser.
assets/css/app.css CHANGED
@@ -3,3 +3,15 @@
3
  @import "tailwindcss/utilities";
4
 
5
  /* This file is for your main application CSS */
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  @import "tailwindcss/utilities";
4
 
5
  /* This file is for your main application CSS */
6
+
7
+ @keyframes loading-fade {
8
+ 0% {
9
+ opacity: 0;
10
+ }
11
+ 50% {
12
+ opacity: 0.8;
13
+ }
14
+ 100% {
15
+ opacity: 0;
16
+ }
17
+ }
assets/js/app.js CHANGED
@@ -3,12 +3,16 @@ import { Socket } from "phoenix";
3
  import { LiveSocket } from "phoenix_live_view";
4
  import topbar from "../vendor/topbar";
5
 
 
 
 
6
  let csrfToken = document
7
  .querySelector("meta[name='csrf-token']")
8
  .getAttribute("content");
9
 
10
  let liveSocket = new LiveSocket("/live", Socket, {
11
  params: { _csrf_token: csrfToken },
 
12
  });
13
 
14
  // Show progress bar on live navigation and form submits
 
3
  import { LiveSocket } from "phoenix_live_view";
4
  import topbar from "../vendor/topbar";
5
 
6
+ import Messages from "./hooks/messages";
7
+ import Microphone from "./hooks/microphone";
8
+
9
  let csrfToken = document
10
  .querySelector("meta[name='csrf-token']")
11
  .getAttribute("content");
12
 
13
  let liveSocket = new LiveSocket("/live", Socket, {
14
  params: { _csrf_token: csrfToken },
15
+ hooks: { Messages, Microphone },
16
  });
17
 
18
  // Show progress bar on live navigation and form submits
assets/js/hooks/messages.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const Messages = {
2
+ mounted() {
3
+ this.scroll();
4
+ },
5
+
6
+ updated() {
7
+ this.scroll();
8
+ },
9
+
10
+ scroll() {
11
+ this.el.scrollTop = this.el.scrollHeight;
12
+ },
13
+ };
14
+
15
+ export default Messages;
assets/js/hooks/microphone.js ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const SAMPLING_RATE = 16_000;
2
+
3
+ const Microphone = {
4
+ mounted() {
5
+ this.mediaRecorder = null;
6
+
7
+ this.el.addEventListener("mousedown", (event) => {
8
+ this.startRecording();
9
+ });
10
+
11
+ this.el.addEventListener("mouseup", (event) => {
12
+ this.stopRecording();
13
+ });
14
+ },
15
+
16
+ startRecording() {
17
+ this.audioChunks = [];
18
+
19
+ navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
20
+ this.mediaRecorder = new MediaRecorder(stream);
21
+
22
+ this.mediaRecorder.addEventListener("dataavailable", (event) => {
23
+ if (event.data.size > 0) {
24
+ this.audioChunks.push(event.data);
25
+ }
26
+ });
27
+
28
+ this.mediaRecorder.start();
29
+ });
30
+ },
31
+
32
+ stopRecording() {
33
+ this.mediaRecorder.addEventListener("stop", (event) => {
34
+ if (this.audioChunks.length === 0) return;
35
+
36
+ const audioBlob = new Blob(this.audioChunks);
37
+
38
+ audioBlob.arrayBuffer().then((buffer) => {
39
+ const context = new AudioContext({ sampleRate: SAMPLING_RATE });
40
+
41
+ context.decodeAudioData(buffer, (audioBuffer) => {
42
+ const pcmBuffer = audioBufferToPcm(audioBuffer);
43
+ const buffer = convertEndianness32(
44
+ pcmBuffer,
45
+ getEndianness(),
46
+ this.el.dataset.endianness
47
+ );
48
+ this.upload("audio", [new Blob([buffer])]);
49
+ });
50
+ });
51
+ });
52
+
53
+ this.mediaRecorder.stop();
54
+ },
55
+
56
+ isRecording() {
57
+ return this.mediaRecorder && this.mediaRecorder.state === "recording";
58
+ },
59
+ };
60
+
61
+ function audioBufferToPcm(audioBuffer) {
62
+ const numChannels = audioBuffer.numberOfChannels;
63
+ const length = audioBuffer.length;
64
+
65
+ const size = Float32Array.BYTES_PER_ELEMENT * length;
66
+ const buffer = new ArrayBuffer(size);
67
+
68
+ const pcmArray = new Float32Array(buffer);
69
+
70
+ const channelDataBuffers = Array.from({ length: numChannels }, (x, channel) =>
71
+ audioBuffer.getChannelData(channel)
72
+ );
73
+
74
+ // Average all channels upfront, so the PCM is always mono
75
+
76
+ for (let i = 0; i < pcmArray.length; i++) {
77
+ let sum = 0;
78
+
79
+ for (let channel = 0; channel < numChannels; channel++) {
80
+ sum += channelDataBuffers[channel][i];
81
+ }
82
+
83
+ pcmArray[i] = sum / numChannels;
84
+ }
85
+
86
+ return buffer;
87
+ }
88
+
89
+ function convertEndianness32(buffer, from, to) {
90
+ if (from === to) {
91
+ return buffer;
92
+ }
93
+
94
+ // If the endianness differs, we swap bytes accordingly
95
+ for (let i = 0; i < buffer.byteLength / 4; i++) {
96
+ const b1 = buffer[i];
97
+ const b2 = buffer[i + 1];
98
+ const b3 = buffer[i + 2];
99
+ const b4 = buffer[i + 3];
100
+ buffer[i] = b4;
101
+ buffer[i + 1] = b3;
102
+ buffer[i + 2] = b2;
103
+ buffer[i + 3] = b1;
104
+ }
105
+
106
+ return buffer;
107
+ }
108
+
109
+ function getEndianness() {
110
+ const buffer = new ArrayBuffer(2);
111
+ const int16Array = new Uint16Array(buffer);
112
+ const int8Array = new Uint8Array(buffer);
113
+
114
+ int16Array[0] = 1;
115
+
116
+ if (int8Array[0] === 1) {
117
+ return "little";
118
+ } else {
119
+ return "big";
120
+ }
121
+ }
122
+
123
+ export default Microphone;
config/config.exs CHANGED
@@ -40,6 +40,13 @@ config :logger, :console,
40
  # Use Jason for JSON parsing in Phoenix
41
  config :phoenix, :json_library, Jason
42
 
 
 
 
 
 
 
 
43
  # Import environment specific config. This must remain at the bottom
44
  # of this file so it overrides the configuration defined above.
45
  import_config "#{config_env()}.exs"
 
40
  # Use Jason for JSON parsing in Phoenix
41
  config :phoenix, :json_library, Jason
42
 
43
+ # EXLA allows only a single computation per device to run at the same
44
+ # time, so we want to run only expensive computations on the GPU, and
45
+ # those computations we generally defn-compile. Individual operations
46
+ # that are executed by the tensor backend should use the CPU (:host)
47
+ # instead.
48
+ config :nx, :default_backend, {EXLA.Backend, client: :host}
49
+
50
  # Import environment specific config. This must remain at the bottom
51
  # of this file so it overrides the configuration defined above.
52
  import_config "#{config_env()}.exs"
lib/chai.ex CHANGED
@@ -1,2 +1,8 @@
1
  defmodule Chai do
 
 
 
 
 
 
2
  end
 
1
  defmodule Chai do
2
+ @doc """
3
+ Returns path to uploaded file with the given filename.
4
+ """
5
+ def upload_path(filename) do
6
+ Path.join([:code.priv_dir(:chai), "static", "uploads", filename])
7
+ end
8
  end
lib/chai/ai.ex ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule Chai.AI do
2
+ def generate_reply(text, history) do
3
+ %{text: text, history: history} =
4
+ Nx.Serving.batched_run(Chai.ConversationServing, %{text: text, history: history})
5
+
6
+ {text, history}
7
+ end
8
+
9
+ def transcribe(audio) do
10
+ %{results: [%{text: text}]} = Nx.Serving.batched_run(Chai.SpeechToTextServing, audio)
11
+ text
12
+ end
13
+
14
+ def describe_image(image) do
15
+ %{results: [%{text: text}]} = Nx.Serving.batched_run(Chai.ImageToTextServing, image)
16
+ text
17
+ end
18
+
19
+ def get_reaction(text) do
20
+ %{predictions: [%{label: label, score: score}]} =
21
+ Nx.Serving.batched_run(Chai.EmotionServing, text)
22
+
23
+ if score >= 0.7 do
24
+ case label do
25
+ "sadness" -> "😢"
26
+ "surprise" -> "😮"
27
+ "joy" -> "😀"
28
+ "neutral" -> nil
29
+ "anger" -> "😡"
30
+ "fear" -> "😱"
31
+ "disgust" -> "🤮"
32
+ end
33
+ end
34
+ end
35
+
36
+ def get_entities(text) do
37
+ %{entities: entities} = Nx.Serving.batched_run(Chai.NERServing, text)
38
+ Enum.filter(entities, &(&1.score >= 0.8))
39
+ end
40
+
41
+ # Servings
42
+
43
+ def conversation_serving() do
44
+ {:ok, model_info} =
45
+ Bumblebee.load_model({:hf, "facebook/blenderbot-400M-distill"}, backend: params_backend())
46
+
47
+ {:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, "facebook/blenderbot-400M-distill"})
48
+
49
+ {:ok, generation_config} =
50
+ Bumblebee.load_generation_config({:hf, "facebook/blenderbot-400M-distill"})
51
+
52
+ generation_config =
53
+ Bumblebee.configure(generation_config, min_new_tokens: 5, max_new_tokens: 50)
54
+
55
+ Bumblebee.Text.conversation(model_info, tokenizer, generation_config,
56
+ compile: compile_opts(batch_size: 8, sequence_length: 128),
57
+ defn_options: [compiler: EXLA]
58
+ )
59
+ end
60
+
61
+ def speech_to_text_serving() do
62
+ {:ok, model_info} =
63
+ Bumblebee.load_model({:hf, "openai/whisper-tiny"}, backend: params_backend())
64
+
65
+ {:ok, featurizer} = Bumblebee.load_featurizer({:hf, "openai/whisper-tiny"})
66
+ {:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, "openai/whisper-tiny"})
67
+ {:ok, generation_config} = Bumblebee.load_generation_config({:hf, "openai/whisper-tiny"})
68
+
69
+ Bumblebee.Audio.speech_to_text(model_info, featurizer, tokenizer, generation_config,
70
+ compile: compile_opts(batch_size: 4),
71
+ defn_options: [compiler: EXLA]
72
+ )
73
+ end
74
+
75
+ def image_to_text_serving() do
76
+ {:ok, model_info} =
77
+ Bumblebee.load_model({:hf, "Salesforce/blip-image-captioning-base"},
78
+ backend: params_backend()
79
+ )
80
+
81
+ {:ok, featurizer} = Bumblebee.load_featurizer({:hf, "Salesforce/blip-image-captioning-base"})
82
+ {:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, "Salesforce/blip-image-captioning-base"})
83
+
84
+ {:ok, generation_config} =
85
+ Bumblebee.load_generation_config({:hf, "Salesforce/blip-image-captioning-base"})
86
+
87
+ generation_config = Bumblebee.configure(generation_config, max_new_tokens: 50)
88
+
89
+ Bumblebee.Vision.image_to_text(model_info, featurizer, tokenizer, generation_config,
90
+ compile: compile_opts(batch_size: 2),
91
+ defn_options: [compiler: EXLA]
92
+ )
93
+ end
94
+
95
+ def emotion_serving() do
96
+ {:ok, model_info} =
97
+ Bumblebee.load_model({:hf, "j-hartmann/emotion-english-distilroberta-base"},
98
+ backend: params_backend()
99
+ )
100
+
101
+ {:ok, tokenizer} =
102
+ Bumblebee.load_tokenizer({:hf, "j-hartmann/emotion-english-distilroberta-base"})
103
+
104
+ Bumblebee.Text.text_classification(model_info, tokenizer,
105
+ top_k: 1,
106
+ compile: compile_opts(batch_size: 8, sequence_length: 100),
107
+ defn_options: [compiler: EXLA]
108
+ )
109
+ end
110
+
111
+ def ner_serving() do
112
+ {:ok, model_info} =
113
+ Bumblebee.load_model({:hf, "dslim/bert-base-NER"}, backend: params_backend())
114
+
115
+ {:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, "bert-base-cased"})
116
+
117
+ Bumblebee.Text.token_classification(model_info, tokenizer,
118
+ aggregation: :word_average,
119
+ compile: compile_opts(batch_size: 8, sequence_length: 100),
120
+ defn_options: [compiler: EXLA]
121
+ )
122
+ end
123
+
124
+ # We compile computations upfront only in prod. In dev/test we JIT
125
+ # to reduce boot time
126
+ if Mix.env() == :prod do
127
+ defp compile_opts(opts), do: opts
128
+ else
129
+ defp compile_opts(_opts), do: nil
130
+ end
131
+
132
+ # In config/config.exs we set the default backend to always use :host
133
+ # client, whereas defn-compiled computations will use the preferred
134
+ # client (like :cuda). When loading parameters, we want to place them
135
+ # on the preferred client, so they don't need to be transferred for
136
+ # every defn invocation.
137
+ defp params_backend(), do: EXLA.Backend
138
+ end
lib/chai/application.ex CHANGED
@@ -10,6 +10,12 @@ defmodule Chai.Application do
10
  ChaiWeb.Telemetry,
11
  # Start the PubSub system
12
  {Phoenix.PubSub, name: Chai.PubSub},
 
 
 
 
 
 
13
  # Start the Endpoint (http/https)
14
  ChaiWeb.Endpoint
15
  ]
 
10
  ChaiWeb.Telemetry,
11
  # Start the PubSub system
12
  {Phoenix.PubSub, name: Chai.PubSub},
13
+ # Start servings
14
+ {Nx.Serving, name: Chai.ConversationServing, serving: Chai.AI.conversation_serving()},
15
+ {Nx.Serving, name: Chai.SpeechToTextServing, serving: Chai.AI.speech_to_text_serving()},
16
+ {Nx.Serving, name: Chai.ImageToTextServing, serving: Chai.AI.image_to_text_serving()},
17
+ {Nx.Serving, name: Chai.EmotionServing, serving: Chai.AI.emotion_serving()},
18
+ {Nx.Serving, name: Chai.NERServing, serving: Chai.AI.ner_serving()},
19
  # Start the Endpoint (http/https)
20
  ChaiWeb.Endpoint
21
  ]
lib/chai/utils.ex ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule Chai.Utils do
2
+ @moduledoc false
3
+
4
+ @doc """
5
+ Splits text into chunks based on the given labels.
6
+
7
+ ## Examples
8
+
9
+ iex> Chai.Utils.labelled_chunks("My friend Arnold lives in Italy.", [
10
+ ...> %{start: 10, end: 16, label: "PER"},
11
+ ...> %{start: 26, end: 31, label: "LOC"}
12
+ ...> ])
13
+ [
14
+ {"My friend ", nil},
15
+ {"Arnold", "PER"},
16
+ {" lives in ", nil},
17
+ {"Italy", "LOC"},
18
+ {".", nil}
19
+ ]
20
+
21
+ """
22
+ def labelled_chunks(text, entities), do: labelled_chunks(text, entities, 0)
23
+
24
+ defp labelled_chunks(text, [%{start: offset} = entity | entities], offset) do
25
+ length = entity.end - entity.start
26
+ chunk = binary_slice(text, offset, length)
27
+ [{chunk, entity.label} | labelled_chunks(text, entities, offset + length)]
28
+ end
29
+
30
+ defp labelled_chunks(text, [entity | entities], offset) do
31
+ length = entity.start - offset
32
+ chunk = binary_slice(text, offset, length)
33
+ [{chunk, nil} | labelled_chunks(text, [entity | entities], offset + length)]
34
+ end
35
+
36
+ defp labelled_chunks(text, [], offset) when offset < byte_size(text) do
37
+ chunk = binary_slice(text, offset..-1//1)
38
+ [{chunk, nil}]
39
+ end
40
+
41
+ defp labelled_chunks(_text, [], _offset), do: []
42
+ end
lib/chai_web.ex CHANGED
@@ -17,7 +17,7 @@ defmodule ChaiWeb do
17
  those modules here.
18
  """
19
 
20
- def static_paths, do: ~w(assets fonts images favicon.svg logo.svg robots.txt)
21
 
22
  def router do
23
  quote do
 
17
  those modules here.
18
  """
19
 
20
+ def static_paths, do: ~w(uploads assets fonts images favicon.svg logo.svg robots.txt)
21
 
22
  def router do
23
  quote do
lib/chai_web/components/layouts/app.html.heex CHANGED
@@ -14,7 +14,7 @@
14
  Source
15
  </a>
16
  <a
17
- href="https://github.com/elixir-nx/bumblebee"
18
  target="_blank"
19
  class="rounded-lg bg-zinc-100 px-2 py-1 text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:bg-zinc-200/80 active:text-zinc-900/70"
20
  >
@@ -23,8 +23,8 @@
23
  </div>
24
  </div>
25
  </header>
26
- <main class="px-4 py-20 sm:px-6 lg:px-8">
27
- <div class="mx-auto max-w-2xl">
28
  <.flash_group flash={@flash} />
29
  <%= @inner_content %>
30
  </div>
 
14
  Source
15
  </a>
16
  <a
17
+ href="https://huggingface.co/spaces/jonatanklosko/chai/blob/main/README.md"
18
  target="_blank"
19
  class="rounded-lg bg-zinc-100 px-2 py-1 text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:bg-zinc-200/80 active:text-zinc-900/70"
20
  >
 
23
  </div>
24
  </div>
25
  </header>
26
+ <main class="py-4 px-4 sm:px-6 lg:px-8">
27
+ <div class="m-auto max-w-2xl">
28
  <.flash_group flash={@flash} />
29
  <%= @inner_content %>
30
  </div>
lib/chai_web/endpoint.ex CHANGED
@@ -8,7 +8,10 @@ defmodule ChaiWeb.Endpoint do
8
  store: :cookie,
9
  key: "_chai_key",
10
  signing_salt: "HnJ/u6tl",
11
- same_site: "Lax"
 
 
 
12
  ]
13
 
14
  socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
 
8
  store: :cookie,
9
  key: "_chai_key",
10
  signing_salt: "HnJ/u6tl",
11
+ # Disable SameSite restriction to allow the app to run inside
12
+ # an iframe (on HF Spaces)
13
+ same_site: "None",
14
+ secure: true
15
  ]
16
 
17
  socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
lib/chai_web/live/chat_live.ex ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule ChaiWeb.ChatLive do
2
+ use ChaiWeb, :live_view
3
+
4
+ @impl true
5
+ def mount(_params, _session, socket) do
6
+ {:ok,
7
+ socket
8
+ |> assign(
9
+ messages: [],
10
+ message: "",
11
+ history: nil,
12
+ reply_task: nil,
13
+ transcribe_task: nil,
14
+ caption_task: nil
15
+ )
16
+ |> allow_upload(:audio, accept: :any, progress: &handle_progress/3, auto_upload: true)
17
+ |> allow_upload(:image,
18
+ accept: ~w(.jpg .jpeg .png),
19
+ progress: &handle_progress/3,
20
+ auto_upload: true
21
+ )}
22
+ end
23
+
24
+ @impl true
25
+ def render(assigns) do
26
+ ~H"""
27
+ <div class="mt-20 h-[512px] flex flex-col justify-end border border-zinc-100 rounded-lg">
28
+ <div id="messages" phx-hook="Messages" class="flex flex-col gap-2 p-3 overflow-y-auto">
29
+ <div class="flex flex-col-reverse gap-2">
30
+ <div
31
+ :for={message <- @messages}
32
+ class={["relative max-w-[80%]", if(message.user?, do: "self-end", else: "self-start")]}
33
+ >
34
+ <.message_content message={message} />
35
+ <div
36
+ :if={message.transcribed?}
37
+ class="flex absolute top-1/2 -left-5 transform -translate-y-1/2 text-zinc-500"
38
+ >
39
+ <.icon name="hero-microphone-solid" class="w-4 h-4" />
40
+ </div>
41
+ <div
42
+ :if={message.reaction}
43
+ class="flex absolute bottom-0 right-0 transform translate-y-1/2 bg-white shadow-md rounded-full p-0.5 leading-none"
44
+ >
45
+ <%= message.reaction %>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <div :if={@reply_task} class="self-start px-4 py-1.5 rounded-3xl text-zinc-900 bg-zinc-100">
50
+ <.typing />
51
+ </div>
52
+ </div>
53
+
54
+ <form phx-submit="send_message" class="px-3 py-2 flex gap-2 items-center">
55
+ <button
56
+ type="button"
57
+ id="microphone"
58
+ phx-hook="Microphone"
59
+ data-endianness={System.endianness()}
60
+ class="flex p-2.5 rounded-full text-white bg-zinc-900 hover:bg-zinc-700 active:bg-red-400 group"
61
+ >
62
+ <.icon name="hero-microphone-solid" class="w-5 h-5 group-active:animate-pulse" />
63
+ </button>
64
+ <button
65
+ type="button"
66
+ phx-click={JS.dispatch("click", to: "##{@uploads.image.ref}")}
67
+ class="flex p-2.5 rounded-full text-white bg-zinc-900 hover:bg-zinc-700"
68
+ >
69
+ <.icon name="hero-photo-solid" class="w-5 h-5" />
70
+ </button>
71
+ <input
72
+ type="text"
73
+ id="message"
74
+ name="message"
75
+ value={@message}
76
+ autofocus
77
+ autocomplete="off"
78
+ class="w-full rounded-xl bg-zinc-100 border-none focus:ring-0"
79
+ />
80
+ <button type="submit" class="flex text-zinc-900 hover:text-zinc-700 active:text-zinc-500">
81
+ <.icon name="hero-paper-airplane-solid" class="w-6 h-6" />
82
+ </button>
83
+ </form>
84
+
85
+ <form phx-change="noop" phx-submit="noop" class="hidden">
86
+ <.live_file_input upload={@uploads.audio} />
87
+ </form>
88
+
89
+ <form phx-change="noop" phx-submit="noop" class="hidden">
90
+ <.live_file_input upload={@uploads.image} />
91
+ </form>
92
+ </div>
93
+ """
94
+ end
95
+
96
+ defp typing(assigns) do
97
+ ~H"""
98
+ <div class="relative h-6 flex gap-1 items-center">
99
+ <div class="h-2 w-2 bg-current rounded-full opacity-0 animate-[loading-fade_1s_infinite] " />
100
+ <div class="h-2 w-2 bg-current rounded-full opacity-0 animate-[loading-fade_1s_infinite_0.2s] " />
101
+ <div class="h-2 w-2 bg-current rounded-full opacity-0 animate-[loading-fade_1s_infinite_0.4s] " />
102
+ </div>
103
+ """
104
+ end
105
+
106
+ defp message_content(assigns) when assigns.message.image != nil do
107
+ ~H"""
108
+ <img src={~p"/uploads/#{@message.image}"} class="max-w-[300px] max-h-[200px] rounded-xl" />
109
+ """
110
+ end
111
+
112
+ defp message_content(assigns) do
113
+ ~H"""
114
+ <div class={[
115
+ "px-4 py-1.5 rounded-3xl",
116
+ if(@message.user?, do: "text-white bg-blue-500", else: "text-zinc-900 bg-zinc-100")
117
+ ]}>
118
+ <span :for={{text, label} <- Chai.Utils.labelled_chunks(@message.text, @message.entities)}>
119
+ <%= if label do %>
120
+ <span class="inline-flex items-center gap-0.5">
121
+ <span class="font-bold"><%= text %></span>
122
+ <.icon :if={icon = label_to_icon(label)} name={icon} class="w-4 h-4" />
123
+ </span>
124
+ <% else %>
125
+ <%= text %>
126
+ <% end %>
127
+ </span>
128
+ </div>
129
+ """
130
+ end
131
+
132
+ defp label_to_icon("LOC"), do: "hero-map-pin-solid"
133
+ defp label_to_icon("PER"), do: "hero-user-solid"
134
+ defp label_to_icon("ORG"), do: "hero-building-office-solid"
135
+ defp label_to_icon("MISC"), do: nil
136
+
137
+ defp handle_progress(:audio, entry, socket) when entry.done? do
138
+ binary =
139
+ consume_uploaded_entry(socket, entry, fn %{path: path} ->
140
+ {:ok, File.read!(path)}
141
+ end)
142
+
143
+ # We always pre-process audio on the client into a single channel
144
+ audio = Nx.from_binary(binary, :f32)
145
+
146
+ {:noreply, request_transcription(socket, audio)}
147
+ end
148
+
149
+ defp handle_progress(:image, entry, socket) when entry.done? do
150
+ filename =
151
+ consume_uploaded_entry(socket, entry, fn %{path: path} ->
152
+ filename = Path.basename(path)
153
+ File.cp!(path, Chai.upload_path(filename))
154
+ {:ok, filename}
155
+ end)
156
+
157
+ {:noreply, request_caption(socket, filename)}
158
+ end
159
+
160
+ defp handle_progress(_name, _entry, socket), do: {:noreply, socket}
161
+
162
+ @impl true
163
+ def handle_event("send_message", %{"message" => text}, socket) do
164
+ {:noreply,
165
+ socket
166
+ |> insert_message(text, user?: true)
167
+ |> request_reply(text)
168
+ |> assign(message: "")}
169
+ end
170
+
171
+ def handle_event("noop", %{}, socket) do
172
+ # We need phx-change and phx-submit on the form for live uploads,
173
+ # but we process the upload immediately in handle_progress/3
174
+ {:noreply, socket}
175
+ end
176
+
177
+ @impl true
178
+ def handle_info({ref, {:reply, {text, history}}}, socket)
179
+ when socket.assigns.reply_task.ref == ref do
180
+ {:noreply,
181
+ socket
182
+ |> insert_message(text)
183
+ |> assign(history: history, reply_task: nil)}
184
+ end
185
+
186
+ def handle_info({ref, {:transcription, text}}, socket)
187
+ when socket.assigns.transcribe_task.ref == ref do
188
+ {:noreply,
189
+ socket
190
+ |> insert_message(text, user?: true, transcribed?: true)
191
+ |> request_reply(text)
192
+ |> assign(transcribe_task: nil)}
193
+ end
194
+
195
+ def handle_info({ref, {:caption, filename, text}}, socket)
196
+ when socket.assigns.caption_task.ref == ref do
197
+ text = "look, an image of " <> text
198
+
199
+ {:noreply,
200
+ socket
201
+ |> insert_message(text, user?: true, image: filename)
202
+ |> request_reply(text)
203
+ |> assign(caption_task: nil)}
204
+ end
205
+
206
+ def handle_info({_ref, {:reaction, message_id, reaction}}, socket) when reaction != nil do
207
+ {:noreply, update_message(socket, message_id, &%{&1 | reaction: reaction})}
208
+ end
209
+
210
+ def handle_info({_ref, {:entities, message_id, entities}}, socket) when entities != [] do
211
+ {:noreply, update_message(socket, message_id, &%{&1 | entities: entities})}
212
+ end
213
+
214
+ def handle_info(_message, socket), do: {:noreply, socket}
215
+
216
+ defp insert_message(socket, text, opts \\ []) do
217
+ message = %{
218
+ id: System.unique_integer(),
219
+ text: text,
220
+ user?: Keyword.get(opts, :user?, false),
221
+ transcribed?: Keyword.get(opts, :transcribed?, false),
222
+ image: Keyword.get(opts, :image),
223
+ reaction: nil,
224
+ entities: []
225
+ }
226
+
227
+ socket = update(socket, :messages, &[message | &1])
228
+
229
+ socket =
230
+ if message.image do
231
+ socket
232
+ else
233
+ request_entities(socket, message.text, message.id)
234
+ end
235
+
236
+ if message.user? do
237
+ request_reaction(socket, message.text, message.id)
238
+ else
239
+ socket
240
+ end
241
+ end
242
+
243
+ defp update_message(socket, message_id, fun) do
244
+ update(socket, :messages, fn messages ->
245
+ Enum.map(messages, fn
246
+ %{id: ^message_id} = message -> fun.(message)
247
+ message -> message
248
+ end)
249
+ end)
250
+ end
251
+
252
+ defp request_reply(socket, text) do
253
+ history = socket.assigns.history
254
+ task = Task.async(fn -> {:reply, Chai.AI.generate_reply(text, history)} end)
255
+ assign(socket, reply_task: task)
256
+ end
257
+
258
+ defp request_transcription(socket, audio) do
259
+ task = Task.async(fn -> {:transcription, Chai.AI.transcribe(audio)} end)
260
+ assign(socket, transcribe_task: task)
261
+ end
262
+
263
+ defp request_caption(socket, filename) do
264
+ task =
265
+ Task.async(fn ->
266
+ image = filename |> Chai.upload_path() |> StbImage.read_file!()
267
+ {:caption, filename, Chai.AI.describe_image(image)}
268
+ end)
269
+
270
+ assign(socket, caption_task: task)
271
+ end
272
+
273
+ defp request_reaction(socket, text, message_id) do
274
+ Task.async(fn -> {:reaction, message_id, Chai.AI.get_reaction(text)} end)
275
+ socket
276
+ end
277
+
278
+ defp request_entities(socket, text, message_id) do
279
+ Task.async(fn -> {:entities, message_id, Chai.AI.get_entities(text)} end)
280
+ socket
281
+ end
282
+ end
lib/chai_web/router.ex CHANGED
@@ -8,6 +8,7 @@ defmodule ChaiWeb.Router do
8
  plug :put_root_layout, {ChaiWeb.Layouts, :root}
9
  plug :protect_from_forgery
10
  plug :put_secure_browser_headers
 
11
  end
12
 
13
  pipeline :api do
@@ -16,6 +17,8 @@ defmodule ChaiWeb.Router do
16
 
17
  scope "/", ChaiWeb do
18
  pipe_through :browser
 
 
19
  end
20
 
21
  # Enable LiveDashboard in development
@@ -28,4 +31,9 @@ defmodule ChaiWeb.Router do
28
  live_dashboard "/dashboard", metrics: ChaiWeb.Telemetry
29
  end
30
  end
 
 
 
 
 
31
  end
 
8
  plug :put_root_layout, {ChaiWeb.Layouts, :root}
9
  plug :protect_from_forgery
10
  plug :put_secure_browser_headers
11
+ plug :allow_within_iframe
12
  end
13
 
14
  pipeline :api do
 
17
 
18
  scope "/", ChaiWeb do
19
  pipe_through :browser
20
+
21
+ live "/", ChatLive, :page
22
  end
23
 
24
  # Enable LiveDashboard in development
 
31
  live_dashboard "/dashboard", metrics: ChaiWeb.Telemetry
32
  end
33
  end
34
+
35
+ # Allow the app to run inside iframe (on HF Spaces)
36
+ defp allow_within_iframe(conn, _opts) do
37
+ delete_resp_header(conn, "x-frame-options")
38
+ end
39
  end
mix.exs CHANGED
@@ -36,7 +36,11 @@ defmodule Chai.MixProject do
36
  {:telemetry_metrics, "~> 0.6"},
37
  {:telemetry_poller, "~> 1.0"},
38
  {:jason, "~> 1.2"},
39
- {:plug_cowboy, "~> 2.5"}
 
 
 
 
40
  ]
41
  end
42
 
 
36
  {:telemetry_metrics, "~> 0.6"},
37
  {:telemetry_poller, "~> 1.0"},
38
  {:jason, "~> 1.2"},
39
+ {:plug_cowboy, "~> 2.5"},
40
+ {:bumblebee, "~> 0.3.0"},
41
+ {:exla, "~> 0.5.1"},
42
+ {:nx, "~> 0.5.1"},
43
+ {:stb_image, "~> 0.6.1"}
44
  ]
45
  end
46
 
mix.lock CHANGED
@@ -1,28 +1,45 @@
1
  %{
 
 
2
  "castore": {:hex, :castore, "1.0.1", "240b9edb4e9e94f8f56ab39d8d2d0a57f49e46c56aced8f873892df8ff64ff5a", [:mix], [], "hexpm", "b4951de93c224d44fac71614beabd88b71932d0b1dea80d2f80fb9044e01bbb3"},
 
 
3
  "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
4
  "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
5
  "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
6
- "esbuild": {:hex, :esbuild, "0.7.0", "ce3afb13cd2c5fd63e13c0e2d0e0831487a97a7696cfa563707342bb825d122a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4ae9f4f237c5ebcb001390b8ada65a12fb2bb04f3fe3d1f1692b7a06fbfe8752"},
 
 
 
7
  "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
8
  "floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"},
9
  "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
10
  "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
11
- "phoenix": {:hex, :phoenix, "1.7.2", "c375ffb482beb4e3d20894f84dd7920442884f5f5b70b9f4528cbe0cedefec63", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1ebca94b32b4d0e097ab2444a9742ed8ff3361acad17365e4e6b2e79b4792159"},
 
 
 
12
  "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
13
  "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
14
  "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
15
- "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
16
  "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
17
  "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
18
- "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
19
- "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
20
  "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
 
21
  "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
 
 
22
  "tailwind": {:hex, :tailwind, "0.1.10", "21ed80ae1f411f747ee513470578acaaa1d0eb40170005350c5b0b6d07e2d624", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e0fc474dfa8ed7a4573851ac69c5fd3ca70fbb0a5bada574d1d657ebc6f2f1f1"},
23
  "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
24
  "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
25
  "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
 
 
 
26
  "websock": {:hex, :websock, "0.5.0", "f6bbce90226121d62a0715bca7c986c5e43de0ccc9475d79c55381d1796368cc", [:mix], [], "hexpm", "b51ac706df8a7a48a2c622ee02d09d68be8c40418698ffa909d73ae207eb5fb8"},
27
  "websock_adapter": {:hex, :websock_adapter, "0.5.0", "cea35d8bbf1a6964e32d4b02ceb561dfb769c04f16d60d743885587e7d2ca55b", [:mix], [{:bandit, "~> 0.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "16318b124effab8209b1eb7906c636374f623dc9511a8278ad09c083cea5bb83"},
 
28
  }
 
1
  %{
2
+ "axon": {:hex, :axon, "0.5.1", "1ae3a2193df45e51fca912158320b2ca87cb7fba4df242bd3ebe245504d0ea1a", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:kino_vega_lite, "~> 0.1.7", [hex: :kino_vega_lite, repo: "hexpm", optional: true]}, {:nx, "~> 0.5.0", [hex: :nx, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "d36f2a11c34c6c2b458f54df5c71ffdb7ed91c6a9ccd908faba909c84cc6a38e"},
3
+ "bumblebee": {:hex, :bumblebee, "0.3.0", "ad6294b39b8fb2212620e9ed9fbebc936c574ae146d3f49b3e855e1254f7c981", [:mix], [{:axon, "~> 0.5.0", [hex: :axon, repo: "hexpm", optional: false]}, {:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.5.0", [hex: :nx, repo: "hexpm", optional: false]}, {:nx_image, "~> 0.1.0", [hex: :nx_image, repo: "hexpm", optional: false]}, {:nx_signal, "~> 0.1.0", [hex: :nx_signal, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0", [hex: :progress_bar, repo: "hexpm", optional: false]}, {:tokenizers, "~> 0.3.1", [hex: :tokenizers, repo: "hexpm", optional: false]}, {:unpickler, "~> 0.1.0", [hex: :unpickler, repo: "hexpm", optional: false]}, {:unzip, "0.8.0", [hex: :unzip, repo: "hexpm", optional: false]}], "hexpm", "477ed5e15d4a5b18343086bed83e2990ca2ba67e0dc9e2d57518bda4cca4c95e"},
4
  "castore": {:hex, :castore, "1.0.1", "240b9edb4e9e94f8f56ab39d8d2d0a57f49e46c56aced8f873892df8ff64ff5a", [:mix], [], "hexpm", "b4951de93c224d44fac71614beabd88b71932d0b1dea80d2f80fb9044e01bbb3"},
5
+ "cc_precompiler": {:hex, :cc_precompiler, "0.1.7", "77de20ac77f0e53f20ca82c563520af0237c301a1ec3ab3bc598e8a96c7ee5d9", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2768b28bf3c2b4f788c995576b39b8cb5d47eb788526d93bd52206c1d8bf4b75"},
6
+ "complex": {:hex, :complex, "0.5.0", "af2d2331ff6170b61bb738695e481b27a66780e18763e066ee2cd863d0b1dd92", [:mix], [], "hexpm", "2683bd3c184466cfb94fad74cbfddfaa94b860e27ad4ca1bffe3bff169d91ef1"},
7
  "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
8
  "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
9
  "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
10
+ "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
11
+ "elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"},
12
+ "esbuild": {:hex, :esbuild, "0.6.1", "a774bfa7b4512a1211bf15880b462be12a4c48ed753a170c68c63b2c95888150", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "569f7409fb5a932211573fc20e2a930a0d5cf3377c5b4f6506c651b1783a1678"},
13
+ "exla": {:hex, :exla, "0.5.1", "8832aa299fe06ed9b772e004760b7c97e9d8dcbe40e9a4bfcbbe10b320b9c342", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.5.1", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.4.4", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "48a990dbaf02bf5f288aa1360b5237c2f55db8bf52d4f63072f2b6a15d4e8375"},
14
  "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
15
  "floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"},
16
  "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
17
  "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
18
+ "nx": {:hex, :nx, "0.5.1", "118134b8c97c2a8f86c87aa8434994c1cbbe139a306b89cca04e08dd46228067", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ceb8fbbe19b3c4252a7188d8b0e059fac9da0f4a4f3bb770fc665fdd0b29f0c5"},
19
+ "nx_image": {:hex, :nx_image, "0.1.0", "ae10fa41fa95126f934d6160ef4320f7db583535fb868415f2562fe19969d245", [:mix], [{:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "60a2928164cdca540b4c180ff25579b97a5f2a650fc890d40db3e1a7798c93ad"},
20
+ "nx_signal": {:hex, :nx_signal, "0.1.0", "403ac73140e2f368e827e0aca1a3035abaf6d890b00376742b359a6838e00d7f", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "1c68f2f0d186700819287f37ee6154a11e06bf5dbb30b73fcc92776293309a05"},
21
+ "phoenix": {:hex, :phoenix, "1.7.1", "a029bde19d9c3b559e5c3d06c78b76e81396bedd456a6acedb42f9c7b2e535a9", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ea9d4a85c3592e37efa07d0dc013254fda445885facaefddcbf646375c116457"},
22
  "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
23
  "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
24
  "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
25
+ "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.17", "74938b02f3c531bed3f87fe1ea39af6b5b2d26ab1405e77e76b8ef5df9ffa8a1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4b5710e19a29b8dc93b7af4bab4739c067a3cb759af01ffc3057165453dce38"},
26
  "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
27
  "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
28
+ "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
29
+ "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
30
  "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
31
+ "progress_bar": {:hex, :progress_bar, "2.0.1", "7b40200112ae533d5adceb80ff75fbe66dc753bca5f6c55c073bfc122d71896d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "2519eb58a2f149a3a094e729378256d8cb6d96a259ec94841bd69fdc71f18f87"},
32
  "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
33
+ "rustler_precompiled": {:hex, :rustler_precompiled, "0.6.1", "160b545bce8bf9a3f1b436b2c10f53574036a0db628e40f393328cbbe593602f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "0dd269fa261c4e3df290b12031c575fff07a542749f7b0e8b744d72d66c43600"},
34
+ "stb_image": {:hex, :stb_image, "0.6.1", "0749a2ca9a2f1f722e31f6c8ec32062684939a260924cafe66a92331c4862dd8", [:make, :mix], [{:cc_precompiler, "~> 0.1.0", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: true]}], "hexpm", "7aa2035e314272aa165c5d09dae2f6b09e648abbc1c68ad20a2e029494858f4b"},
35
  "tailwind": {:hex, :tailwind, "0.1.10", "21ed80ae1f411f747ee513470578acaaa1d0eb40170005350c5b0b6d07e2d624", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e0fc474dfa8ed7a4573851ac69c5fd3ca70fbb0a5bada574d1d657ebc6f2f1f1"},
36
  "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
37
  "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
38
  "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
39
+ "tokenizers": {:hex, :tokenizers, "0.3.1", "31b445abf498d0edfd594651a1abe359f10ceeb841b2ba964d51473dcc1f1100", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "7f25d28291a05ea71797750000a92ba913f7f65cc7a80fdb08a1c2a2b2b90def"},
40
+ "unpickler": {:hex, :unpickler, "0.1.0", "c2262c0819e6985b761e7107546cef96a485f401816be5304a65fdd200d5bd6a", [:mix], [], "hexpm", "e2b3f61e62406187ac52afead8a63bfb4e49394028993f3c4c42712743cab79e"},
41
+ "unzip": {:hex, :unzip, "0.8.0", "ee21d87c21b01567317387dab4228ac570ca15b41cfc221a067354cbf8e68c4d", [:mix], [], "hexpm", "ffa67a483efcedcb5876971a50947222e104d5f8fea2c4a0441e6f7967854827"},
42
  "websock": {:hex, :websock, "0.5.0", "f6bbce90226121d62a0715bca7c986c5e43de0ccc9475d79c55381d1796368cc", [:mix], [], "hexpm", "b51ac706df8a7a48a2c622ee02d09d68be8c40418698ffa909d73ae207eb5fb8"},
43
  "websock_adapter": {:hex, :websock_adapter, "0.5.0", "cea35d8bbf1a6964e32d4b02ceb561dfb769c04f16d60d743885587e7d2ca55b", [:mix], [{:bandit, "~> 0.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "16318b124effab8209b1eb7906c636374f623dc9511a8278ad09c083cea5bb83"},
44
+ "xla": {:hex, :xla, "0.4.4", "c3a8ed1f579bda949df505e49ff65415c8281d991fbd6ae1d8f3c5d0fd155f54", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "484f3f9011db3c9f1ff1e98eecefd382f3882a07ada540fd58803db1d2dab671"},
45
  }
priv/static/uploads/.gitkeep ADDED
File without changes
rel/overlays/bin/server ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/sh
2
+ cd -P -- "$(dirname -- "$0")"
3
+ PHX_SERVER=true exec ./chai start
test/chai/utils_test.exs ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ defmodule Chai.UtilsTest do
2
+ use ExUnit.Case, async: true
3
+
4
+ doctest Chai.Utils
5
+ end