Spaces:
Sleeping
Sleeping
Commit
·
0fea377
1
Parent(s):
b6f0124
Add chat
Browse files- .dockerignore +31 -0
- .gitignore +4 -0
- Dockerfile +90 -0
- README.md +34 -0
- assets/css/app.css +12 -0
- assets/js/app.js +4 -0
- assets/js/hooks/messages.js +15 -0
- assets/js/hooks/microphone.js +123 -0
- config/config.exs +7 -0
- lib/chai.ex +6 -0
- lib/chai/ai.ex +138 -0
- lib/chai/application.ex +6 -0
- lib/chai/utils.ex +42 -0
- lib/chai_web.ex +1 -1
- lib/chai_web/components/layouts/app.html.heex +3 -3
- lib/chai_web/endpoint.ex +4 -1
- lib/chai_web/live/chat_live.ex +282 -0
- lib/chai_web/router.ex +8 -0
- mix.exs +5 -1
- mix.lock +22 -5
- priv/static/uploads/.gitkeep +0 -0
- rel/overlays/bin/server +3 -0
- test/chai/utils_test.exs +5 -0
.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://
|
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="
|
27 |
-
<div class="
|
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 |
-
|
|
|
|
|
|
|
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 |
-
"
|
|
|
|
|
|
|
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 |
-
"
|
|
|
|
|
|
|
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.
|
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.
|
19 |
-
"plug_cowboy": {:hex, :plug_cowboy, "2.6.
|
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
|