This commit is contained in:
DavHau 2024-04-17 16:00:40 +07:00
parent b13f618b03
commit 72b77a5586
25 changed files with 4475 additions and 0 deletions

1
.env.template Normal file
View File

@ -0,0 +1 @@
export OPENAI_API_KEY=$(rbw get openai-api-key)

2
.envrc Normal file
View File

@ -0,0 +1,2 @@
watch_file pdm.lock
use flake

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.direnv
.gpteng
__pycache__
.chroma
.pdm-python
.env
options.json

276
flake.lock Normal file
View 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
View 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
View 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
View 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

View 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

3408
pdm.lock Normal file

File diff suppressed because it is too large Load Diff

30
pyproject.toml Normal file
View 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
View 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
View 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
View 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}",
},
]

View 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
View 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.
}

View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
# This file lists all the dependencies for the application
sounddevice
numpy
soundfile

5
src/run.sh Normal file
View 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
View 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])

8
src/ui.py Normal file
View File

@ -0,0 +1,8 @@
import gradio as gr
def greet(name):
return "Hello " + name + "!"
demo = gr.Interface(fn=greet, inputs="text", outputs="text")
demo.launch()