init
This commit is contained in:
parent
b13f618b03
commit
72b77a5586
1
.env.template
Normal file
1
.env.template
Normal file
@ -0,0 +1 @@
|
||||
export OPENAI_API_KEY=$(rbw get openai-api-key)
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.direnv
|
||||
.gpteng
|
||||
__pycache__
|
||||
.chroma
|
||||
.pdm-python
|
||||
.env
|
||||
options.json
|
276
flake.lock
Normal file
276
flake.lock
Normal file
@ -0,0 +1,276 @@
|
||||
{
|
||||
"nodes": {
|
||||
"clan-core": {
|
||||
"inputs": {
|
||||
"disko": "disko",
|
||||
"flake-parts": "flake-parts",
|
||||
"nixos-generators": "nixos-generators",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"sops-nix": "sops-nix",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713339954,
|
||||
"narHash": "sha256-FA2Be/Lm6IwC8/PkQunID3dZuWsBBguWaNtWVES3GeE=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "0dde75829644db3fa5320800f9019793ccbd3b71",
|
||||
"revCount": 2589,
|
||||
"type": "git",
|
||||
"url": "https://git.clan.lol/clan/clan-core"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://git.clan.lol/clan/clan-core"
|
||||
}
|
||||
},
|
||||
"disko": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"clan-core",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712356478,
|
||||
"narHash": "sha256-kTcEtrQIRnexu5lAbLsmUcfR2CrmsACF1s3ZFw1NEVA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "0a17298c0d96190ef3be729d594ba202b9c53beb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"dream2nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"purescript-overlay": "purescript-overlay",
|
||||
"pyproject-nix": "pyproject-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711432406,
|
||||
"narHash": "sha256-UaZqNasBRNU43aIvZVUL9bjwfDveQNlAxVVFiMk8cew=",
|
||||
"owner": "nix-community",
|
||||
"repo": "dream2nix",
|
||||
"rev": "6ddb9a7f5e2119327ebfc8dbcc98cc4456ab4657",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "dream2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"clan-core",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712014858,
|
||||
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixlib": {
|
||||
"locked": {
|
||||
"lastModified": 1711846064,
|
||||
"narHash": "sha256-cqfX0QJNEnge3a77VnytM0Q6QZZ0DziFXt6tSCV8ZSc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "90b1a963ff84dc532db92f678296ff2499a60a87",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixos-generators": {
|
||||
"inputs": {
|
||||
"nixlib": "nixlib",
|
||||
"nixpkgs": [
|
||||
"clan-core",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712191720,
|
||||
"narHash": "sha256-xXtSSnVHURHsxLQO30dzCKW5NJVGV/umdQPmFjPFMVA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-generators",
|
||||
"rev": "0c15e76bed5432d7775a22e8d22059511f59d23a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-generators",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1712468661,
|
||||
"narHash": "sha256-n2gVVBs+rV+HzPv/N3QQv5cdAXqSkjmaObvfeMqnw2c=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "298edc8f1e0dfffce67f50375c9f5952e04a6d02",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable-small",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1711231723,
|
||||
"narHash": "sha256-dARJQ8AJOv6U+sdRePkbcVyVbXJTi1tReCrkkOeusiA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e1d501922fd7351da4200e1275dfcf5faaad1220",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"purescript-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"dream2nix",
|
||||
"nixpkgs"
|
||||
],
|
||||
"slimlock": "slimlock"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1696022621,
|
||||
"narHash": "sha256-eMjFmsj2G1E0Q5XiibUNgFjTiSz0GxIeSSzzVdoN730=",
|
||||
"owner": "thomashoneyman",
|
||||
"repo": "purescript-overlay",
|
||||
"rev": "047c7933abd6da8aa239904422e22d190ce55ead",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "thomashoneyman",
|
||||
"repo": "purescript-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pyproject-nix": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1702448246,
|
||||
"narHash": "sha256-hFg5s/hoJFv7tDpiGvEvXP0UfFvFEDgTdyHIjDVHu1I=",
|
||||
"owner": "davhau",
|
||||
"repo": "pyproject.nix",
|
||||
"rev": "5a06a2697b228c04dd2f35659b4b659ca74f7aeb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "davhau",
|
||||
"ref": "dream2nix",
|
||||
"repo": "pyproject.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"clan-core": "clan-core",
|
||||
"dream2nix": "dream2nix",
|
||||
"nixpkgs": [
|
||||
"dream2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
}
|
||||
},
|
||||
"slimlock": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"dream2nix",
|
||||
"purescript-overlay",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1688610262,
|
||||
"narHash": "sha256-Wg0ViDotFWGWqKIQzyYCgayeH8s4U1OZcTiWTQYdAp4=",
|
||||
"owner": "thomashoneyman",
|
||||
"repo": "slimlock",
|
||||
"rev": "b5c6cdcaf636ebbebd0a1f32520929394493f1a6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "thomashoneyman",
|
||||
"repo": "slimlock",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"sops-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"clan-core",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": [
|
||||
"clan-core"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712458908,
|
||||
"narHash": "sha256-DMgBS+jNHDg8z3g9GkwqL8xTKXCRQ/0FGsAyrniVonc=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "39191e8e6265b106c9a2ba0cfd3a4dafe98a31c6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"clan-core",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711963903,
|
||||
"narHash": "sha256-N3QDhoaX+paWXHbEXZapqd1r95mdshxToGowtjtYkGI=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "49dc4a92b02b8e68798abd99184f228243b6e3ac",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
58
flake.nix
Normal file
58
flake.nix
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
inputs = {
|
||||
dream2nix.url = "github:nix-community/dream2nix";
|
||||
clan-core .url = "git+https://git.clan.lol/clan/clan-core";
|
||||
nixpkgs.follows = "dream2nix/nixpkgs";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ dream2nix, nixpkgs, clan-core, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
lib = nixpkgs.lib;
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
packageModule =
|
||||
{ dream2nix, config, ... }:
|
||||
{
|
||||
imports = [ dream2nix.modules.dream2nix.WIP-python-pdm ];
|
||||
pdm.lockfile = ./pdm.lock;
|
||||
pdm.pyproject = ./pyproject.toml;
|
||||
paths.package = ./.;
|
||||
paths.projectRoot = ./.;
|
||||
paths.projectRootFile = "flake.nix";
|
||||
overrides = {
|
||||
gpt-engineer = {
|
||||
mkDerivation.buildInputs = [ config.deps.python.pkgs.poetry-core ];
|
||||
mkDerivation.propagatedBuildInputs = [ config.deps.python.pkgs.tkinter ];
|
||||
};
|
||||
};
|
||||
};
|
||||
package = dream2nix.lib.evalModules {
|
||||
modules = [ packageModule ];
|
||||
packageSets.nixpkgs = pkgs;
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.${system}.default = package;
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
inputsFrom = [ package.devShell ];
|
||||
packages = [
|
||||
pkgs.pdm
|
||||
pkgs.imagemagickBig
|
||||
];
|
||||
buildInputs = [ pkgs.portaudio ];
|
||||
options_clan = import ./nix/options-clan-json.nix {inherit clan-core system;};
|
||||
options_nixos = import ./nix/options-nixos-json.nix {inherit pkgs;};
|
||||
shellHook = ''
|
||||
if [ -e .env ]; then
|
||||
source .env
|
||||
fi
|
||||
echo "$(which python)" > .pdm-python
|
||||
export PYTHONPATH="${pkgs.python3.pkgs.tkinter}/${pkgs.python3.sitePackages}:$PYTHONPATH"
|
||||
export PYTHONPATH="$PYTHONPATH:$(realpath ${clan-core.packages.${system}.clan-cli}/lib/*/site-packages)"
|
||||
export LD_LIBRARY_PATH="${pkgs.portaudio}/lib:${lib.getLib pkgs.libsndfile}/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export NIX_PATH=nixpkgs=${pkgs.path}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
11
nix/clean-options.jq
Normal file
11
nix/clean-options.jq
Normal file
@ -0,0 +1,11 @@
|
||||
to_entries
|
||||
| map(
|
||||
select(.value.readOnly == false)
|
||||
| .value.option = .key
|
||||
| del(.value.loc)
|
||||
| del(.value.readOnly)
|
||||
| .value.default = .value.default.text
|
||||
| .value.example = .value.example.text
|
||||
| del(..|select(. == null))
|
||||
)
|
||||
| from_entries
|
25
nix/options-clan-json.nix
Normal file
25
nix/options-clan-json.nix
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
clan-core ? builtins.getFlake (toString ../../.),
|
||||
system ? builtins.currentSystem,
|
||||
}:
|
||||
let
|
||||
pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system};
|
||||
lib = pkgs.lib;
|
||||
clanEvaled = pkgs.nixos {
|
||||
imports = [ clan-core.nixosModules.clanCore ] ++ (lib.attrValues clan-core.clanModules);
|
||||
# imports = [];
|
||||
};
|
||||
docsOut =
|
||||
(pkgs.nixosOptionsDoc {
|
||||
options = clanEvaled.options.clan;
|
||||
warningsAreErrors = false;
|
||||
}).optionsJSON;
|
||||
|
||||
docsJson = docsOut + "/share/doc/nixos/options.json";
|
||||
|
||||
docsCleaned = pkgs.runCommand "options-cleaned.json" { } ''
|
||||
cat ${docsJson} | ${pkgs.jq}/bin/jq "$(cat ${./clean-options.jq})" > $out
|
||||
'';
|
||||
in
|
||||
# docsOut
|
||||
docsCleaned
|
16
nix/options-nixos-json.nix
Normal file
16
nix/options-nixos-json.nix
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
pkgs ? import <nixpkgs> {},
|
||||
}:
|
||||
let
|
||||
docsOut = (pkgs.nixosOptionsDoc {
|
||||
options = (pkgs.nixos { }).options;
|
||||
warningsAreErrors = false;
|
||||
}).optionsJSON;
|
||||
|
||||
docsJson = docsOut + "/share/doc/nixos/options.json";
|
||||
|
||||
cleaned = pkgs.runCommand "options-cleaned.json" { } ''
|
||||
cat ${docsJson} | ${pkgs.jq}/bin/jq "$(cat ${./clean-options.jq})" > $out
|
||||
'';
|
||||
in
|
||||
cleaned
|
30
pyproject.toml
Normal file
30
pyproject.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[project]
|
||||
name = "ai-stickers"
|
||||
version = "0.0.0"
|
||||
|
||||
requires-python = "<3.13,>=3.10"
|
||||
|
||||
dependencies = [
|
||||
# library for audio transcription
|
||||
"faster-whisper",
|
||||
# cmdline tool for audio transcription based on faster-whisper
|
||||
"whisper-ctranslate2",
|
||||
# for bootstrapping this project
|
||||
"gpt-engineer",
|
||||
"numpy>=1.26.4",
|
||||
"sounddevice>=0.4.6",
|
||||
"soundfile>=0.12.1",
|
||||
"langchain>=0.1.13",
|
||||
"langchain-community>=0.0.29",
|
||||
"chromadb>=0.4.24",
|
||||
"jq>=1.7.0",
|
||||
|
||||
# This seems to pull in hundreds of MB of torch and nvidia deps
|
||||
# Maybe get rid of this
|
||||
"sentence-transformers>=2.6.0",
|
||||
]
|
||||
|
||||
[tool.pdm.options]
|
||||
add = ["--no-sync", "--no-self"]
|
||||
update = ["--no-sync", "--no-self"]
|
||||
remove = ["--no-sync", "--no-self"]
|
44
src/audio_recorder.py
Normal file
44
src/audio_recorder.py
Normal file
@ -0,0 +1,44 @@
|
||||
import queue
|
||||
|
||||
import numpy as np
|
||||
import sounddevice as sd
|
||||
import soundfile as sf
|
||||
|
||||
|
||||
class AudioRecorder:
|
||||
def __init__(self, samplerate=44100, channels=1):
|
||||
self.samplerate = samplerate
|
||||
self.channels = channels
|
||||
self.recording = False
|
||||
self.frames = queue.Queue()
|
||||
|
||||
def start_recording(self):
|
||||
if self.recording:
|
||||
return
|
||||
self.recording = True
|
||||
self.frames = queue.Queue()
|
||||
self.stream = sd.InputStream(samplerate=self.samplerate, channels=self.channels, callback=self.callback)
|
||||
self.stream.start()
|
||||
|
||||
def stop_recording(self):
|
||||
if not self.recording:
|
||||
return
|
||||
self.recording = False
|
||||
self.stream.stop()
|
||||
self.stream.close()
|
||||
audio_data = self._get_audio_data()
|
||||
return audio_data
|
||||
|
||||
def callback(self, indata, frames, time, status):
|
||||
if status:
|
||||
print(status)
|
||||
self.frames.put(indata.copy())
|
||||
|
||||
def _get_audio_data(self):
|
||||
frames = []
|
||||
while not self.frames.empty():
|
||||
frames.append(self.frames.get())
|
||||
return np.concatenate(frames, axis=0)
|
||||
|
||||
def save_recording(self, filename, audio_data):
|
||||
sf.write(filename, audio_data, self.samplerate, format='WAV')
|
53
src/clan_openai.py
Normal file
53
src/clan_openai.py
Normal file
@ -0,0 +1,53 @@
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
# The URL to which the request is sent
|
||||
url: str = "https://api.openai.com/v1/chat/completions"
|
||||
# url: str = "http://localhost:8080/v1/chat/completions"
|
||||
|
||||
def api_key():
|
||||
return os.environ["OPENAI_API_KEY"]
|
||||
|
||||
# The header includes the content type and the authorization with your API key
|
||||
def headers() -> dict[str, str]:
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key()}",
|
||||
}
|
||||
|
||||
|
||||
def complete(
|
||||
messages: list[dict[str, Any]],
|
||||
model: str = "gpt-3.5-turbo",
|
||||
temperature: float = 1.0,
|
||||
) -> str:
|
||||
# Data to be sent in the request
|
||||
data = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
}
|
||||
|
||||
# Create a request object with the URL and the headers
|
||||
req = urllib.request.Request(url, json.dumps(data).encode("utf-8"), headers())
|
||||
|
||||
# Make the request and read the response
|
||||
with urllib.request.urlopen(req) as response:
|
||||
response_body = response.read()
|
||||
resp_data = json.loads(response_body)
|
||||
return resp_data["choices"][0]["message"]["content"]
|
||||
|
||||
|
||||
def complete_prompt(
|
||||
prompt: str,
|
||||
system: str = "",
|
||||
model: str = "gpt-3.5-turbo",
|
||||
temperature: float = 1.0,
|
||||
) -> str:
|
||||
return complete(
|
||||
[{"role": "system", "content": system}, {"role": "user", "content": prompt}],
|
||||
model,
|
||||
temperature,
|
||||
)
|
205
src/clana/__init__.py
Normal file
205
src/clana/__init__.py
Normal file
@ -0,0 +1,205 @@
|
||||
# !/usr/bin/env python3
|
||||
# A subcommand that interfaces with openai to generate nixos configurations and launches VMs with them.
|
||||
# The `clan clana` command allows the user to enter a prompt with the wishes for the VM and then generates a nixos configuration and launches a VM with it.
|
||||
# for now this POC should be stateless. A configuration.nix should be generated ina temporary directory and directly launched.
|
||||
# there should be no additional arguments.
|
||||
# The prompt is read from stdin
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
import clan_openai
|
||||
from clan_cli.errors import ClanCmdError
|
||||
from clan_cli.vms.run import run_command
|
||||
|
||||
base_config = Path(__file__).parent.joinpath("base-config.nix").read_text()
|
||||
|
||||
clan_docs = Path(os.environ["options_clan"]).read_text()
|
||||
|
||||
# system_msg = f"""
|
||||
# Your name is clana, an assistant for creating NixOS configurations.
|
||||
# Your task is to generate a NixOS configuration.nix file.
|
||||
# Do not output any explanations or comments, not even when the user asks a question or provides feedback.
|
||||
# Always provide only the content of the configuration.nix file.
|
||||
# Don't use any nixos options for which you are not sure about their syntax.
|
||||
# Generate a configuration.nix which has a very high probability of just working.
|
||||
# The user who provides the prompt might have technical expertise, or none at all.
|
||||
# Even a grandmother who has no idea about computers should be able to use this.
|
||||
# Translate the users requirements to a working configuration.nix file.
|
||||
# Don't set any options under `nix.`.
|
||||
# The user should not have a password and log in automatically.
|
||||
|
||||
# Take care specifically about:
|
||||
# - specify every option only once within the same file. Otherwise it will lead to an error like this: error: attribute 'environment.systemPackages' already defined at [...]/configuration.nix:X:X
|
||||
# - don't set a password for the user. it's already set in the base config
|
||||
|
||||
|
||||
# Assume the following base config is already imported. Any option set in there is already configured and doesn't need to be specified anymore:
|
||||
|
||||
# ```nix
|
||||
# {base_config}
|
||||
# ```
|
||||
|
||||
# The base config will be imported by the system. No need to import it anymore.
|
||||
# """
|
||||
|
||||
system_msg = f"""
|
||||
## Task
|
||||
Your task is to generate a NixOS configuration.nix file.
|
||||
Do not output any explanations or comments, not even when the user asks a question or provides feedback.
|
||||
Always provide only the content of the configuration.nix file.
|
||||
Don't use any nixos options for which you are not sure about their syntax.
|
||||
Generate a configuration.nix which has a very high probability of just working.
|
||||
The user who provides the prompt might have technical expertise, or none at all.
|
||||
Even a grandmother who has no idea about computers should be able to use this.
|
||||
Translate the users requirements to a working configuration.nix file.
|
||||
Don't set any options under `nix.`.
|
||||
The user should not have a password and log in automatically.
|
||||
|
||||
## Error handling
|
||||
If your generated config results in an error, the error message will be fed back to you. You can then adjust your config and try again.
|
||||
|
||||
### Missing definitions
|
||||
Whenever you encounter an error similar to this this (think of any option instead of `foo.bar.baz`):
|
||||
```
|
||||
error: The option `foo.bar.baz' is used but not defined
|
||||
```
|
||||
... it means that the option `foo.bar.baz` doesn't have any default value and therefore needs to be defined by you.
|
||||
React by defining `foo.bar.baz` in your configuration, similar to this:
|
||||
```
|
||||
{{config, lib, pkgs, clan-core, ...}}: {{
|
||||
imports = [
|
||||
# imports go here
|
||||
];
|
||||
# foo.bar.baz = "some value";
|
||||
}}
|
||||
```
|
||||
|
||||
## Take care specifically about
|
||||
- specify every option only once within the same file. Otherwise it will lead to an error like this: error: attribute 'environment.systemPackages' already defined at [...]/configuration.nix:X:X
|
||||
- don't set a password for the user. it's already set in the base config
|
||||
|
||||
|
||||
## Template
|
||||
Use this template for the configuration you generate:
|
||||
```
|
||||
{{config, lib, pkgs, clan-core, ...}}: {{
|
||||
imports = [
|
||||
# imports go here
|
||||
];
|
||||
# configuration goes here
|
||||
}}
|
||||
```
|
||||
|
||||
## Base config
|
||||
Assume the following base config is already imported. Any option set in there is already configured and doesn't need to be specified anymore:
|
||||
|
||||
```nix
|
||||
{base_config}
|
||||
```
|
||||
|
||||
The base config will be imported by the system. No need to import it anymore.
|
||||
|
||||
## Use clan framework
|
||||
Prefer using options from the clan framework over the upstream nixos ones.
|
||||
Whenever you use any option prefixed with `clan.`, like for example `clan.something` you also need to add a module named `something` to the imports like this:
|
||||
```
|
||||
imports = [
|
||||
clan-core.clanModules.something
|
||||
]
|
||||
```
|
||||
Here the documentation of the clan options as JSON:
|
||||
{clan_docs}
|
||||
"""
|
||||
|
||||
|
||||
# takes a (sub)parser and configures it
|
||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--show", action="store_true", help="show the configuration")
|
||||
parser.set_defaults(func=clana_command)
|
||||
|
||||
|
||||
def clana_command(args: argparse.Namespace) -> None:
|
||||
print("Please enter your wishes for the new computer: ")
|
||||
prompt = input()
|
||||
# prompt = "I want to email my grandchildren and watch them on facebook"
|
||||
print("Thank you. Generating your computer...")
|
||||
# config = clan_openai.complete(messages, model="gpt-4-turbo-preview").strip()
|
||||
config = Path(".direnv/configuration.nix").read_text()
|
||||
|
||||
|
||||
def start_vm_from_prompt(prompt, show=False):
|
||||
messages = [
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
conf_dir = Path("/tmp/clana")
|
||||
conf_dir.mkdir(exist_ok=True)
|
||||
shutil.rmtree(conf_dir)
|
||||
conf_dir.mkdir()
|
||||
(conf_dir / "flake.nix").write_bytes(
|
||||
Path(__file__).parent.joinpath("flake.nix.template").read_bytes()
|
||||
)
|
||||
# create sops user key to make run.py happy
|
||||
# TODO: this should not be necessary
|
||||
key_json = conf_dir / "sops" / "users" / "user" / "key.json"
|
||||
key_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
key_json.write_text('{"publickey": "age1vphy2sr6uw4ptsua3gh9khrm2cqyt65t46tusmt44z98qa7q6ymq6prrdl", "type": "age"}')
|
||||
# write config files
|
||||
with open(conf_dir / "base-config.nix", "w") as f:
|
||||
f.write(base_config)
|
||||
with open(conf_dir / "hardware-configuration.nix", "w") as f:
|
||||
f.write("{}")
|
||||
with open(conf_dir / "configuration.nix", "w") as f:
|
||||
f.write(
|
||||
"""
|
||||
{
|
||||
imports = [
|
||||
./base-config.nix
|
||||
./ai-config.nix
|
||||
];
|
||||
}
|
||||
"""
|
||||
)
|
||||
while True:
|
||||
# write dump of messages to /tmp/clana/messages.json
|
||||
with open(conf_dir / "messages.json", "w") as f:
|
||||
json.dump(messages, f, indent=2)
|
||||
config_orig = clan_openai.complete(
|
||||
messages, model="gpt-4-turbo-preview"
|
||||
# messages, model="mistral-openorca"
|
||||
# messages, model="mistral"
|
||||
).strip()
|
||||
# remove code blocks
|
||||
lines = config_orig.split("\n")
|
||||
if lines[0].startswith("```"):
|
||||
lines = lines[1:-1]
|
||||
config = "\n".join(lines)
|
||||
if show:
|
||||
print("Configuration generated:")
|
||||
print(config)
|
||||
print("Configuration generated. Launching...")
|
||||
with open(conf_dir / "ai-config.nix", "w") as f:
|
||||
f.write(config)
|
||||
|
||||
os.environ["NIXPKGS_ALLOW_UNFREE"] = "1"
|
||||
try:
|
||||
run_command(
|
||||
machine="clana-machine", flake=conf_dir, #nix_options=["--impure"]
|
||||
)
|
||||
break
|
||||
except ClanCmdError as e:
|
||||
error = \
|
||||
"error:" + e.cmd.stderr.split("error:")[-1].strip() \
|
||||
if "error:" in e.cmd.stderr \
|
||||
else e.cmd.stderr
|
||||
messages += [
|
||||
{"role": "assistant", "content": config_orig},
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"There was a problem that needs to be fixed:\n{error}",
|
||||
},
|
||||
]
|
62
src/clana/base-config-kde.nix
Normal file
62
src/clana/base-config-kde.nix
Normal file
@ -0,0 +1,62 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# Include the results of the hardware scan.
|
||||
./hardware-configuration.nix
|
||||
];
|
||||
|
||||
# Ensure that software properties (e.g., being unfree) are respected.
|
||||
nixpkgs.config = {
|
||||
allowUnfree = true;
|
||||
};
|
||||
|
||||
# Use the systemd-boot EFI boot loader.
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
networking.hostName = "clana"; # Define your hostname.
|
||||
networking.networkmanager.enable = true;
|
||||
|
||||
# Enable the X11 windowing system.
|
||||
services.xserver.enable = true;
|
||||
services.xserver.layout = "us";
|
||||
services.xserver.xkbOptions = "eurosign:e";
|
||||
|
||||
# Enable touchpad support.
|
||||
services.xserver.libinput.enable = true;
|
||||
|
||||
# Enable the KDE Desktop Environment.
|
||||
services.xserver.displayManager.sddm.enable = true;
|
||||
services.xserver.desktopManager.plasma5.enable = true;
|
||||
|
||||
# Enable sound.
|
||||
sound.enable = true;
|
||||
hardware.pulseaudio.enable = true;
|
||||
|
||||
# Autologin settings.
|
||||
services.xserver.displayManager.autoLogin.enable = true;
|
||||
services.xserver.displayManager.autoLogin.user = "user";
|
||||
|
||||
# User settings.
|
||||
users.users.user = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "wheel" ]; # Enable sudo for the user.
|
||||
uid = 1000;
|
||||
password = "hello";
|
||||
openssh.authorizedKeys.keys = [ ];
|
||||
};
|
||||
|
||||
# Enable firewall.
|
||||
networking.firewall.enable = true;
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
443
|
||||
]; # HTTP and HTTPS
|
||||
|
||||
# Set time zone.
|
||||
time.timeZone = "UTC";
|
||||
|
||||
# System-wide settings.
|
||||
system.stateVersion = "22.05"; # Edit this to your NixOS release version.
|
||||
}
|
49
src/clana/base-config.nix
Normal file
49
src/clana/base-config.nix
Normal file
@ -0,0 +1,49 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# Include the results of the hardware scan.
|
||||
./hardware-configuration.nix
|
||||
];
|
||||
|
||||
# Ensure that software properties (e.g., being unfree) are respected.
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
# Use the systemd-boot EFI boot loader.
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
networking.hostName = "clana"; # Define your hostname.
|
||||
networking.networkmanager.enable = true;
|
||||
|
||||
services.xserver.enable = true;
|
||||
services.xserver.displayManager.autoLogin.enable = true;
|
||||
services.xserver.displayManager.autoLogin.user = "user";
|
||||
services.xserver.desktopManager.xfce.enable = true;
|
||||
services.xserver.desktopManager.xfce.enableScreensaver = false;
|
||||
services.xserver.xkb.layout = "us";
|
||||
|
||||
# Enable sound.
|
||||
sound.enable = true;
|
||||
hardware.pulseaudio.enable = true;
|
||||
|
||||
# User settings.
|
||||
users.users.user.isNormalUser = true;
|
||||
users.users.user.extraGroups = [ "wheel" ]; # Enable sudo for the user.
|
||||
users.users.user.uid = 1000;
|
||||
users.users.user.password = "hello";
|
||||
users.users.user.openssh.authorizedKeys.keys = [ ];
|
||||
|
||||
# Enable firewall.
|
||||
networking.firewall.enable = true;
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
443
|
||||
]; # HTTP and HTTPS
|
||||
|
||||
# Set time zone.
|
||||
time.timeZone = "UTC";
|
||||
|
||||
# System-wide settings.
|
||||
system.stateVersion = "22.05"; # Edit this to your NixOS release version.
|
||||
}
|
31
src/clana/flake.nix.template
Normal file
31
src/clana/flake.nix.template
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
description = "<Put your description here>";
|
||||
|
||||
inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core";
|
||||
|
||||
outputs = { self, clan-core, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system};
|
||||
lib = pkgs.lib;
|
||||
clan = clan-core.lib.buildClan {
|
||||
directory = self;
|
||||
clanName = "clana-clan";
|
||||
machines.clana-machine = {
|
||||
imports = [
|
||||
./configuration.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# all machines managed by cLAN
|
||||
inherit (clan) nixosConfigurations clanInternals;
|
||||
# add the cLAN cli tool to the dev shell
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
packages = [
|
||||
clan-core.packages.${system}.clan-cli
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
5
src/debug.py
Normal file
5
src/debug.py
Normal file
@ -0,0 +1,5 @@
|
||||
from clan_cli.vms.run import run_command
|
||||
|
||||
run_command(
|
||||
machine="clana-machine", flake="/tmp/clana", #nix_options=["--impure"]
|
||||
)
|
67
src/embeddings.py
Normal file
67
src/embeddings.py
Normal file
@ -0,0 +1,67 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from langchain.embeddings.sentence_transformer import SentenceTransformerEmbeddings
|
||||
from langchain_community.document_loaders import JSONLoader
|
||||
from langchain_community.vectorstores import Chroma
|
||||
import argparse
|
||||
|
||||
def strip_store_path(path: str):
|
||||
# strip /nix/store/xxx-source prefix
|
||||
return str(Path(*Path(path).parts[4:]))
|
||||
|
||||
def metadata_func(record: dict, metadata: dict):
|
||||
metadata["declarations"] = strip_store_path(record["declarations"][0])
|
||||
return metadata
|
||||
|
||||
loader = JSONLoader(
|
||||
file_path=os.environ["options_nixos"],
|
||||
jq_schema=".[]",
|
||||
text_content=True,
|
||||
content_key="description",
|
||||
metadata_func=metadata_func,
|
||||
)
|
||||
|
||||
def init_db(embedding_function, documents):
|
||||
if os.path.exists(".chroma"):
|
||||
shutil.rmtree(".chroma")
|
||||
print("adding documents to DB...", file=sys.stderr)
|
||||
db = Chroma.from_documents(documents, embedding_function, persist_directory=".chroma")
|
||||
print("DB initialization completed", file=sys.stderr)
|
||||
return db
|
||||
|
||||
def main():
|
||||
is_initialized = os.path.exists(".chroma")
|
||||
parser = argparse.ArgumentParser(description='Process some integers.')
|
||||
parser.add_argument('query', type=str, help='The query for the search')
|
||||
parser.add_argument('--init', action='store_true', help='Initialize the database', default=not is_initialized)
|
||||
args = parser.parse_args()
|
||||
query = args.query
|
||||
|
||||
embedding_function = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
|
||||
print("loaded embedding function", file=sys.stderr)
|
||||
|
||||
if args.init:
|
||||
documents = loader.load()
|
||||
print("loaded documents", file=sys.stderr)
|
||||
db = init_db(embedding_function, documents)
|
||||
else:
|
||||
db = Chroma(embedding_function=embedding_function, persist_directory=".chroma")
|
||||
|
||||
|
||||
# docs = db.similarity_search(query)
|
||||
docs = db.max_marginal_relevance_search(query, k=100, fetch_k=50)
|
||||
|
||||
modules = []
|
||||
for doc in docs:
|
||||
print("\n\n")
|
||||
print(doc.page_content)
|
||||
print(doc.metadata["declarations"])
|
||||
modules.append(doc.metadata["declarations"])
|
||||
print(f"modules: {modules}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
14
src/main.py
Normal file
14
src/main.py
Normal file
@ -0,0 +1,14 @@
|
||||
import logging
|
||||
|
||||
from clan_cli.custom_logger import setup_logging
|
||||
from popup_window import PopupWindow
|
||||
|
||||
setup_logging(logging.DEBUG, root_log_name=__name__.split(".")[0])
|
||||
|
||||
def main():
|
||||
# Create and run the popup window
|
||||
popup = PopupWindow()
|
||||
popup.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
60
src/popup_window.py
Normal file
60
src/popup_window.py
Normal file
@ -0,0 +1,60 @@
|
||||
import threading
|
||||
import tkinter as tk
|
||||
|
||||
from audio_recorder import AudioRecorder
|
||||
import clana
|
||||
from transcribe import transcribe
|
||||
|
||||
|
||||
class PopupWindow:
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.geometry('600x200') # Set a constant size for the popup window
|
||||
self.root.resizable(False, False) # Prevent the window from being resized
|
||||
self.root.title("Popup")
|
||||
self.label = tk.Label(
|
||||
self.root,
|
||||
text="Press SPACE and provide instructions",
|
||||
justify="left",
|
||||
wraplength=580,
|
||||
font=("Arial", 12),
|
||||
)
|
||||
self.label.pack(pady=20)
|
||||
self.audio_recorder = AudioRecorder()
|
||||
self.recording = False
|
||||
|
||||
# Bind the space key to the start_recording method and space key release to the stop_recording method
|
||||
self.root.bind('<KeyPress-space>', self.start_stop_recording)
|
||||
|
||||
def start_stop_recording(self, event):
|
||||
if self.recording:
|
||||
self.stop_recording(event)
|
||||
else:
|
||||
self.start_recording(event)
|
||||
|
||||
def start_recording(self, _):
|
||||
self.recording = True
|
||||
# Change the label text to "Listening" and start recording
|
||||
self.label.config(text="Listening")
|
||||
self.audio_recorder.start_recording()
|
||||
|
||||
def stop_recording(self, _):
|
||||
self.recording = False
|
||||
# Change the label text to "Creating your machine" and stop recording
|
||||
|
||||
audio_data = self.audio_recorder.stop_recording()
|
||||
self.audio_recorder.save_recording("/tmp/recording.wav", audio_data)
|
||||
spoken_text = transcribe("/tmp/recording.wav")
|
||||
print(spoken_text)
|
||||
self.label.config(text="Creating machine from your requirements:\n\n" + spoken_text)
|
||||
|
||||
# spoken_text = "Play games with Steam and print documents"
|
||||
# spoken_text = "Play games with Steam and print documents and use localsend for chat"
|
||||
|
||||
# Start the VM in a separate thread
|
||||
vm_thread = threading.Thread(target=clana.start_vm_from_prompt, args=(spoken_text, True))
|
||||
vm_thread.start()
|
||||
|
||||
def run(self):
|
||||
# Run the main loop of the tkinter application
|
||||
self.root.mainloop()
|
12
src/prompt
Normal file
12
src/prompt
Normal file
@ -0,0 +1,12 @@
|
||||
A simple python program which displays a small popup.
|
||||
The popup should capture the space key.
|
||||
|
||||
By default the popup should display: "Press SPACE and provide instructions"
|
||||
If the space key is pressed, the text should change to "Listening"
|
||||
If the space bar is released, the text should change to "Creating your machine"
|
||||
|
||||
Whenever te space bar is pressed, record an audio file from the user microphone until the space bar is released
|
||||
handle the sound related code in a separate file
|
||||
store the audio file in wav format using the soundfile library
|
||||
|
||||
Ensure the popup window has a constant size
|
4
src/requirements.txt
Normal file
4
src/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
# This file lists all the dependencies for the application
|
||||
sounddevice
|
||||
numpy
|
||||
soundfile
|
5
src/run.sh
Normal file
5
src/run.sh
Normal file
@ -0,0 +1,5 @@
|
||||
# Install dependencies from requirements.txt before running the application.
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run the main.py script.
|
||||
python main.py
|
22
src/transcribe.py
Normal file
22
src/transcribe.py
Normal file
@ -0,0 +1,22 @@
|
||||
import sys
|
||||
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
|
||||
def transcribe(audio_file):
|
||||
model_size = "small.en"
|
||||
|
||||
# or run on CPU with INT8
|
||||
model = WhisperModel(model_size, device="cpu", compute_type="int8")
|
||||
|
||||
segments, info = model.transcribe(audio_file, beam_size=5)
|
||||
|
||||
print("Detected language '%s' with probability %f" % (info.language, info.language_probability))
|
||||
|
||||
# for segment in segments:
|
||||
# print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))
|
||||
return "\n".join([segment.text for segment in segments])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
transcribe(sys.argv[1])
|
Loading…
Reference in New Issue
Block a user