Every AI model I use runs in a Docker container of its own, each one started by a service in my Docker module (the models are in AI models in Docker containers). The Ai module is the layer on top: one function for each task, which starts the model's container if it isn't already running, sends it the request, and hands back the result.
Making a piece of music is one call:
const Ai = require("Ai");
const fs = require("fs");
const prompt = "Drum and bass with electric guitar";
const settings = {
_is: "Ai.Audio.Audiocraft",
duration: 20.0
}
Ai.Audio.Audiocraft(
prompt,
settings,
(data) => {
const audioBuffer = Buffer.from(data.split(',')[1], 'base64');
fs.writeFileSync(`./resources/audiocraft.wav`, audioBuffer);
},
(msg) => {
console.log("Ai server failed with following message:\n")
console.log(msg)
}
);
The first time this runs, the MusicGen container is built and started, which takes a while. After that the container is already running with the model in memory, and every script on the machine that asks for MusicGen shares it.
How a call works
Every function has the same shape: what to work on, a settings object, a function for the result and a function for errors. Inside, each one does the same five things. Here's the music function:
Audiocraft: (prompt, settings, onSuccess, onError) => {
const defaultSettings = {
top_k: 250,
top_p: 0.0,
temperature: 1.0,
duration: 30.0,
cfg_coef: 3.0,
}
const serviceName = `ai-audiocraft`;
const serverPort = 8001;
const serverUri = `ws://localhost:${serverPort}`;
Security.String(prompt);
Security.Check(prompt.length > 0, `Prompt is too short, please provide more context`);
Security.Object("Ai.Audio.Audiocraft")(settings);
Security.Callback(onSuccess);
Security.Callback(onError);
for (let key in defaultSettings) {
if (!settings.hasOwnProperty(key)) {
settings[key] = defaultSettings[key]
}
}
const isStarted = Docker.Container.Exists(serviceName, true);
const connHandler = () => {
const ws = new WebSocket(serverUri);
ws.on('open', function () {
ws.send(JSON.stringify({ started: true, prompt: prompt, settings }));
});
ws.on('message', function (data) {
const {success, error, progress} = JSON.parse(data);
if (success !== undefined) {
onSuccess(success[0]);
} else if (error !== undefined) {
onError(error);
} else if (progress !== undefined) {
console.info(`Progress information received - ${progress}% done`);
}
});
ws.on('error', function (error) {
setTimeout(connHandler, 5000)
});
}
if (!isStarted) {
Docker.Service.AI.AUDIOCRAFT_LARGE(serviceName, serverPort);
setTimeout(connHandler, 10000);
}
else {
connHandler()
}
}
- Check the arguments. The Security module checks every argument before anything else happens. The settings object carries its type in
_is, so passing the settings for one function to another is caught straight away. - Fill in the defaults. Any setting that wasn't given takes the model's default, so most calls only set the one or two things they care about.
- Start the container if it isn't running. The container's name is fixed for each model, so it's only ever started once.
- Connect, and keep trying. A model takes a while to load once its container starts, so the first connection waits a few seconds, and a failed connection tries again every five until the server answers.
- Send one message and wait for the answer. The server sends progress messages while it works, then either the result or an error, and the matching callback is called.
Results that are files, like audio and images, come back as base64 data URLs, ready to be saved or put straight into a web page.
The functions
The module is organised by the kind of media each function makes or works on: Ai.Audio, Ai.Image, Ai.Text, Ai.Code and Ai.Object, with Ai.OpenAi for the functions that call OpenAI's online API instead of a local model.
Audio
| Function | What it does | Model |
|---|---|---|
Ai.Audio.Audiocraft(prompt, settings, onSuccess, onError) | Music from a text prompt, up to 30 seconds | MusicGen large |
Ai.Audio.Continuous(seedPrompt, settings, saveChunk, getPrompt) | Music that carries on for as long as you like, a chunk at a time, asking for a new prompt as it goes | MusicGen large, with beat tracking |
Ai.Audio.Fixed(prompts, settings, saveChunk) | Continuous music from a fixed list of prompts | MusicGen large, with beat tracking |
Ai.Audio.Stateless(prompts, settings, onSuccess) | Continuous music from a list of prompts, which can pick up again from a saved WAV file | MusicGen large, with beat tracking |
Ai.Audio.Music(prompt, settings, onSuccess, onError) | Music from a text prompt, with the spectrogram it was made from | Riffusion |
Ai.Audio.Demucs(audio, onSuccess, onError) | Splits a song into its bass, drums, vocals and everything else | Demucs |
Ai.Audio.Midi(audio, onSuccess, onError) | Turns a recording into MIDI notes | Basic Pitch |
Ai.Audio.Speech(text, settings, onSuccess, onError) | Text to speech | FastSpeech 2 |
Ai.Audio.Transcript(audio, onSuccess, onError) | Speech to text, with timestamps | wav2vec 2.0 |
Ai.Audio.Vosk(audio, onSuccess, onProgress) | Speech to text, with partial results as it goes | Vosk |
Ai.Audio.Stream(nextChunk, onText, settings) | Live speech to text from a stream of audio, such as a microphone | Vosk |
Ai.Audio.Translate.ToAudio(text, fromLanguage, toLanguage, onSuccess, onError) | Translates text and speaks it | WizardLM 30B, then FastSpeech 2 |
Ai.Audio.Translate.ToString(audio, fromLanguage, toLanguage, onSuccess, onError) | Transcribes speech and translates it | wav2vec 2.0, then WizardLM 30B |
Images
| Function | What it does | Model |
|---|---|---|
Ai.Image.Prompt(prompt, settings, onSuccess, onError) | An image from a text prompt, with settings.model choosing STABLE_DIFFUSION, LATENT_DIFFUSION or DALLE | Stable Diffusion v1.4, CompVis latent diffusion, min-dalle |
Ai.Image.Style(contentImage, styleImage, onSuccess, onError) | Paints one image in the style of another | Magenta's arbitrary image stylisation |
Ai.Image.Upscale(image, settings, onSuccess, onError) | Enlarges an image, with settings.model choosing X2, X2_25_JXL, X4 or MINECRAFT_X4 | Upscaling models run with ONNX Runtime |
Text
| Function | What it does | Model |
|---|---|---|
Ai.Text.Prompt(prompt, settings, onSuccess, onError) | Carries on a piece of text, or fills in masked words, with settings.model choosing GPT, GPT_MEDIUM, GPT_LARGE, GPT_XLARGE, CTRL, BERT, BERT_LARGE, ROBERTA or DISTILBERT | GPT-2 at four sizes, CTRL, BERT, RoBERTa and DistilBERT, through Hugging Face transformers |
Ai.Text.LLaMa['13B'].WizardLM(prompt, settings, onSuccess, onError) | Answers a prompt with one of the open LLaMA models, sized by its number of parameters | WizardLM 13B |
Ai.Text.LLaMa['30B'].WizardLM(…) | WizardLM 30B | |
Ai.Text.LLaMa['30B'].WizardVincunaLM(…) | Wizard-Vicuna 30B | |
Ai.Text.LLaMa['33B'].Guanaco(…) | Guanaco 33B | |
Ai.Text.LLaMa['65B'].Alpaca(…) | Alpaca 65B | |
Ai.Text.LLaMa['65B'].Guanaco(…) | Guanaco 65B | |
Ai.Text.Falcon(prompt, settings, onSuccess, onError) | Answers a prompt | Falcon-40B |
Ai.Text.Documents.Query(prompt, source, settings, onSuccess, onError) | Answers a question from your own documents, with the passages it used | PrivateGPT |
Ai.Text.Translate(message, fromLanguage, toLanguage, onSuccess, onError) | Translates text from one language to another | WizardLM 30B |
The LLaMA models run with llama.cpp, from quantised weights downloaded from Hugging Face. Ai.Text['30B'] and the other sizes are a shorthand for the same models: Ai.Text['30B'].WizardLM is Ai.Text.LLaMa['30B'].WizardLM.
Code
| Function | What it does | Model |
|---|---|---|
Ai.Code.C.Ubuntu(description, settings, onSuccess, onError) | Writes a C program for Ubuntu | WizardLM 30B |
Ai.Code.Go.Program(description, settings, onSuccess, onError) | Writes a Go program | WizardLM 30B |
Ai.Code.HTML.Page(description, settings, onSuccess, onError) | Writes an HTML page | Guanaco 65B |
Ai.Code.PHP.Page(description, settings, onSuccess, onError) | Writes a PHP page | WizardLM 30B |
Ai.Code.Python.Server.WebSocket(description, settings, onSuccess, onError) | Writes a Python WebSocket server | WizardLM 30B |
Each one is a template around a LLaMA model: the description finishes a sentence like "Write a Go program that…", so Ai.Code.Go.Program("serves static files from /public", …) asks for "Write a Go program that serves static files from /public". The settings are passed on to the model, so they can set the temperature or the length of the answer.
Program: (description, settings, onSuccess, onError) => {
Security.String(description);
Security.Object("Ai.Code.Go.Program")(settings);
Security.Callback(onSuccess);
Security.Callback(onError);
const prompt = `Write a Go program that ${description}`;
Ai.Text['30B'].WizardLM(prompt, { ...settings, _is: "Ai.Text.LLaMa" }, onSuccess, onError);
}
3D objects
| Function | What it does | Model |
|---|---|---|
Ai.Object.Point_e(prompt, settings, onSuccess, onError) | A 3D point cloud from a text prompt | Point-E |
Ai.Object.Shap_e(prompt, settings, onSuccess, onError) | A 3D object from a text prompt | Shap-E |
OpenAI
| Function | What it does | Model |
|---|---|---|
Ai.OpenAi.Gpt(prompt, settings, onSuccess, onError) | Text completion, with settings.model choosing the model | GPT-3: text-davinci-003 and the other completion models |
Ai.OpenAi.Codex(prompt, settings, onSuccess, onError) | Code completion | Codex: code-davinci-002 or code-cushman-001 |
Ai.OpenAi.DallE(prompt, settings, onSuccess, onError) | An image from a text prompt | DALL·E |
These don't start any containers. They call OpenAI's API with fetch, using your own API key, which goes in the settings as apiKey (the examples read it from the OPENAI_API_KEY environment variable), and they give back OpenAI's response as it comes. OpenAI has since withdrawn the Codex models, so Ai.OpenAi.Codex no longer has anything to call.
Building on each other
Because every function has the same shape, the bigger ones are made from the smaller ones. Translating speech is a transcript followed by a translation:
ToString: (promptEncoded, fromLanguage, toLanguage, onSuccess, onError) => {
Security.String(promptEncoded);
Security.String(fromLanguage);
Security.String(toLanguage);
Security.Callback(onSuccess);
Security.Callback(onError);
Ai.Audio.Transcript(promptEncoded, (transcript) => {
const transcriptText = transcript[0].transcription;
Ai.Text.Translate(transcriptText, fromLanguage, toLanguage, onSuccess, onError);
}, onError);
}
Each step starts its own model if it needs to, so the first time this runs it brings up two containers, one after the other. Translating text into speech is the same thing the other way round, a translation followed by Ai.Audio.Speech.
The same goes for scripts built on the module. The music in Endless music with MusicGen is made by Ai.Audio.Continuous, with a small script around it choosing the prompts.
What it needs
The module only needs two of my other modules, both of which it uses in nearly every file:
- Docker, to check whether a model's container is running and to start it with the model's service.
- Security, to check every argument and settings object.
Apart from those it uses ws for the WebSocket connections, kept in the module's own node_modules, and Node's own fs. Riffusion's server takes an HTTP request rather than a WebSocket, and that and the OpenAI functions use fetch, which is built into Node from version 18.
The models themselves aren't part of the module, and nothing needs downloading in advance. Each model's Docker service downloads its weights while it builds the image, from wherever the model is published: Hugging Face, the authors' GitHub repositories, or for the LLaMA models, the URL of the quantised weights that the Ai module passes to the service. So the first call to a model builds its image, model and all, and every call after that uses the image that's there.
The examples folder has a script for each model, about 40 in all, from GPT-2 to Stable Diffusion and Demucs.
Things to know
- The interface is asynchronous in the callback sense: each function returns straight away, and the result arrives later through
onSuccessoronError. They don't return promises. - The first call to a model is slow: its image is built if it hasn't been, the container starts, and the model loads. Later calls go straight to the running container.
- A model's server works on one request at a time, so calls to the same model from several scripts are answered one after the other.
Ai.Object.Point_eandAi.Object.Shap_euse the same port, so only one of them can be running at a time.- Most models run on the processor by default. Where a function takes a
devicesetting,"cuda"or"gpu"starts its container with the graphics card.
Credits
- Piotr Nierobisz - worked on the module, its model services and the examples
See also
- AI models in Docker containers - the containers this module starts
- A Docker module for Node.js - the module that builds and runs them
- Endless music with MusicGen - long tracks made with
Ai.Audio.Continuous - How I organise Node modules - how this module and the others fit together
- Synthesizing - a website for making long-form music, which grew out of this