Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
59f6f54
Allow Naomi to use an LLM
aaronchantrill Dec 16, 2024
a41d7a4
Fix a unit test
aaronchantrill Dec 16, 2024
bc08b65
Fixing CodeQL issue
aaronchantrill Dec 16, 2024
f7365b6
Changed System message order
aaronchantrill Jan 20, 2025
2a42305
Add ALPACA template
aaronchantrill Jan 21, 2025
6ee080d
Additional LLM integration
aaronchantrill Jul 8, 2025
7efe4d5
Merge pull request #441 from aaronchantrill/LLM
aaronchantrill Jul 9, 2025
14479e8
Fix issue #190: check Python version and locale encoding at startup b…
Sekhar03 Aug 13, 2026
ff7853f
Fix issue #412: prevent Naomi VAD from triggering on its own beeps by…
Sekhar03 Aug 13, 2026
7080f7e
Fix issue #390: verify return codes of plugin install/update scripts …
Sekhar03 Aug 13, 2026
e04ab16
Fix issue #415: fall back to 'virtualenvwrapper' apt package if pytho…
Sekhar03 Aug 13, 2026
dd5ddb3
Fix issue #379 and #345: add libttspico-utils to apt_requirements.txt…
Sekhar03 Aug 13, 2026
682e7d5
Fix issue #303: add download_file progress bar and download_git helpe…
Sekhar03 Aug 13, 2026
a6f1167
Fix TypeError in check_python_import on Python 3.10+
Sekhar03 Aug 14, 2026
d7045ac
Fix clock formatting ValueError on Windows by using SafeDatetime wrapper
Sekhar03 Aug 14, 2026
b7ba5ea
Fix pocketsphinx import failure on Windows: support AMD64 check, use …
Sekhar03 Aug 14, 2026
a829b36
Merge origin/LLM branch, resolving conflict in naomi/mic.py
Sekhar03 Aug 14, 2026
39511ff
Create Python-native compile_translations.py script to compile po tra…
Sekhar03 Aug 14, 2026
63f14ef
Create Python-native naomi_apt_requirements.py script to parse and in…
Sekhar03 Aug 14, 2026
f52693d
Update AUTHORS.md and create Python-native update_authors.py (resolve…
Sekhar03 Aug 14, 2026
6bdcc07
Change Python 2 shebang to Python 3 in setup.py to match runtime envi…
Sekhar03 Aug 14, 2026
20c0caa
Implement RESTful API server (Issue #46) and fix msgfmt.py to always …
Sekhar03 Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,20 @@
- Drask <drask@dottywood.org>
- aaronchantrill <aaron.chantrill@dottywood.org>
- pyup-bot <github-bot@pyup.io>
- Chaz Straney <chazu@users.noreply.github.com>
- Mickaël Schoentgen <contact@tiger-222.fr>
- AaronC <aaron.chantrill@dottywood.org>
- Austin Casteel <austin.casteel@gmail.com>
- Erik Andresen <erik@vontaene.de>
- Dmytro Semenchenko <39010377+gospodima@users.noreply.github.com>
- Stephen Chavez <stephen@dicesoft.net>
- olest <ole.st@gmx.de>
- Bernhard Suttner <sbernhard@users.noreply.github.com>
- Julian Liebig <je.liebig@web.de>
- LGTM Migrator <lgtm-migrator@users.noreply.github.com>
- Aryan Yadav <141573833+aryan0931@users.noreply.github.com>
- Farookh Zaheer Siddiqui <129654632+FarukhS52@users.noreply.github.com>
- Sanketh Kumar <sankethkumarbg1995@gmail.com>
- Sekhar03 <125454858+Sekhar03@users.noreply.github.com>

#### Generated by bin/update-authors.sh.
#### Generated by update_authors.py.
21 changes: 16 additions & 5 deletions Naomi.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import naomi
import sys

if(sys.version_info.major < 3):
raise SystemError("This version of Naomi requires Python 3.5 or greater")
if (sys.version_info.major < 3):
sys.stderr.write("Error: This version of Naomi requires Python 3.5 or greater\n")
sys.exit(1)

import locale
try:
encoding = locale.getpreferredencoding()
if encoding and encoding.lower() not in ('utf-8', 'utf8'):
sys.stderr.write("WARNING: Your system locale is configured to use a non-UTF-8 character encoding ({}).\n".format(encoding))
sys.stderr.write("This may cause Naomi to crash when reading files or configuration containing international characters.\n")
sys.stderr.write("Please consider setting your system locale to UTF-8 (e.g. by setting LC_ALL=en_US.UTF-8 in Linux).\n")
except Exception:
pass

import naomi

naomi.main()

21 changes: 16 additions & 5 deletions Populate.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import naomi
import sys

if(sys.version_info.major < 3):
raise SystemError("This version of Naomi requires Python 3.5 or greater")
if (sys.version_info.major < 3):
sys.stderr.write("Error: This version of Naomi requires Python 3.5 or greater\n")
sys.exit(1)

import locale
try:
encoding = locale.getpreferredencoding()
if encoding and encoding.lower() not in ('utf-8', 'utf8'):
sys.stderr.write("WARNING: Your system locale is configured to use a non-UTF-8 character encoding ({}).\n".format(encoding))
sys.stderr.write("This may cause Naomi to crash when reading files or configuration containing international characters.\n")
sys.stderr.write("Please consider setting your system locale to UTF-8 (e.g. by setting LC_ALL=en_US.UTF-8 in Linux).\n")
except Exception:
pass

import naomi

naomi.main(args=["--repopulate"])

2 changes: 2 additions & 0 deletions apt_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ liblzma-dev
# TTS
## flite
flite
## pico2wave (for French and German TTS support)
libttspico-utils

# SpeechHandler
## MPDControl
Expand Down
30 changes: 30 additions & 0 deletions compile_translations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys

# Ensure current directory is in search path for msgfmt
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from msgfmt import Msgfmt

def main():
root_dir = os.path.dirname(os.path.abspath(__file__))
print(f"Search directory: '{root_dir}'")
compiled_count = 0
for root, dirs, files in os.walk(root_dir):
for file in files:
if file.lower().endswith(".po"):
po_file = os.path.join(root, file)
print(f"Compiling: '{po_file}'")
mo_file = po_file[:-3] + ".mo"
try:
mo_data = Msgfmt(po_file).get()
with open(mo_file, 'wb') as out_f:
out_f.write(mo_data)
compiled_count += 1
except Exception as e:
print(f"Error compiling {po_file}: {e}", file=sys.stderr)
print(f"Done. Compiled {compiled_count} translation file(s).")

if __name__ == "__main__":
main()
12 changes: 12 additions & 0 deletions installers/script.deb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,18 @@ setup_wizard() {
printf "${B_G}sourcing virtualenvwrapper.sh${B_W}${NL}"
export WORKON_HOME=$HOME/.virtualenvs
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
# Some Debian variants (e.g. Armbian Bookworm) ship python3-virtualenvwrapper
# without virtualenvwrapper.sh in /usr/share/virtualenvwrapper/. In that case
# fall back to the standalone 'virtualenvwrapper' apt package. See issue #415.
if [ ! -f /usr/share/virtualenvwrapper/virtualenvwrapper.sh ]; then
printf "${B_R}Notice:${B_W} /usr/share/virtualenvwrapper/virtualenvwrapper.sh not found.${NL}"
printf "${B_G}Trying to install standalone 'virtualenvwrapper' package...${B_W}${NL}"
sudo apt-get install -y virtualenvwrapper
if [ $? -ne 0 ] || [ ! -f /usr/share/virtualenvwrapper/virtualenvwrapper.sh ]; then
printf "${B_R}Notice:${B_W} Could not install virtualenvwrapper. Please install it manually and re-run the installer.${NL}" >&2
exit 1
fi
fi
source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
printf "${B_G}checking if Naomi virtualenv exists${B_W}${NL}"
workon Naomi > /dev/null 2>&1
Expand Down
2 changes: 1 addition & 1 deletion msgfmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def readPoData(self):

def add(self, context, id, string, fuzzy):
"Add a non-empty and non-fuzzy translation to the dictionary."
if string and not fuzzy:
if string and (not fuzzy or not id):
# The context is put before the id and separated by a EOT char.
if context:
id = context + u('\x04') + id
Expand Down
36 changes: 24 additions & 12 deletions naomi/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ def main(args=None):
'own voice.'
])
)
parser.add_argument(
'--quiet',
'-q',
action='store_true',
help=' '.join([
'Quiet mode: skip interactive prompts for missing settings and',
'use defaults instead. Useful when running Naomi as a service',
'or in a non-interactive (screen/tmux) session.'
])
)

# Plugin Repository Management
pr_man = parser.add_mutually_exclusive_group(required=False)
Expand Down Expand Up @@ -194,17 +204,17 @@ def main(args=None):
print(_("Logging to {}").format(logfile))

print(logo)
print(" ___ ___ ___ ___ ")
print(" /\__\ /\ \ /\ \ /\__\ ___ ")
print(" /::| | /::\ \ /::\ \ /::| | /\ \ ")
print(" /:|:| | /:/\:\ \ /:/\:\ \ /:|:| | \:\ \ ")
print(" /:/|:| |__ /::\~\:\ \ /:/ \:\ \ /:/|:|__|__ /::\__\ ")
print(" /:/ |:| /\__\ /:/\:\ \:\__\ /:/__/ \:\__\ /:/ |::::\__\ /:/\/__/ ")
print(" \/__|:|/:/ / \/__\:\/:/ / \:\ \ /:/ / \/__/~~/:/ / __/:/ / ")
print(" |:/:/ / \::/ / \:\ /:/ / /:/ / /\/:/ / ")
print(" |::/ / /:/ / \:\/:/ / /:/ / \::/__/ ")
print(" /:/ / /:/ / \::/ / /:/ / \:\__\ ")
print(" \/__/ \/__/ \/__/ \/__/ \/__/ ")
print(r" ___ ___ ___ ___ ")
print(r" /\__\ /\ \ /\ \ /\__\ ___ ")
print(r" /::| | /::\ \ /::\ \ /::| | /\ \ ")
print(r" /:|:| | /:/\:\ \ /:/\:\ \ /:|:| | \:\ \ ")
print(r" /:/|:| |__ /::\~\:\ \ /:/ \:\ \ /:/|:|__|__ /::\__\ ")
print(r" /:/ |:| /\__\ /:/\:\ \:\__\ /:/__/ \:\__\ /:/ |::::\__\ /:/\/__/ ")
print(r" \/__|:|/:/ / \/__\:\/:/ / \:\ \ /:/ / \/__/~~/:/ / __/:/ / ")
print(r" |:/:/ / \::/ / \:\ /:/ / /:/ / /\/:/ / ")
print(r" |::/ / /:/ / \:\/:/ / /:/ / \::/__/ ")
print(r" /:/ / /:/ / \::/ / /:/ / \:\__\ ")
print(r" \/__/ \/__/ \/__/ \/__/ \/__/ ")
print(sto)

# Select Mic
Expand All @@ -225,6 +235,7 @@ def main(args=None):
# variable while instantiating plugin objects
# in plugin.GenericPlugin.__init__()
profile.set_arg("repopulate", p_args.repopulate)
profile.set_arg("quiet", p_args.quiet)

if(p_args.listen_while_talking):
profile.set_arg("listen_while_talking", 'Yes')
Expand All @@ -249,7 +260,8 @@ def main(args=None):
save_audio=p_args.save_audio,
save_passive_audio=p_args.save_passive_audio,
save_active_audio=p_args.save_active_audio,
save_noise=p_args.save_noise
save_noise=p_args.save_noise,
quiet=p_args.quiet
)
if p_args.list_audio_devices:
app.list_audio_devices()
Expand Down
99 changes: 90 additions & 9 deletions naomi/app_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import requests
import smtplib
import sys
from dateutil import parser
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
Expand Down Expand Up @@ -380,13 +381,93 @@ def is_positive(phrase):


# Copied from https://stackoverflow.com/questions/16694907/download-large-file-in-python-with-requests/16696317#16696317
def download_file(url, local_filename=None):
if(local_filename is None):
local_filename = url.split('/')[-1]
# NOTE the stream=True parameter below
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(local_filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
# Enhanced for issue #303: shows a terminal progress bar during download.
def download_file(url, local_filename=None, show_progress=True):
"""Download a file from *url* to *local_filename* with an optional progress bar.

Arguments:
url -- the URL to download
local_filename -- destination path (defaults to the URL basename)
show_progress -- print a progress bar to stdout (default True)

Returns the local filename.
"""
if local_filename is None:
local_filename = url.split('/')[-1]

with requests.get(url, stream=True) as r:
r.raise_for_status()
total = int(r.headers.get('content-length', 0))
downloaded = 0
bar_width = 40

with open(local_filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if show_progress and total:
done = int(bar_width * downloaded / total)
pct = downloaded * 100 // total
sys.stdout.write(
"\r[{}{}] {}% {}/{}B ".format(
'#' * done,
'-' * (bar_width - done),
pct,
downloaded,
total
)
)
sys.stdout.flush()

if show_progress and total:
sys.stdout.write("\n")
sys.stdout.flush()

return local_filename


def download_git(url, destination, branch=None, show_progress=True):
"""Clone or update a git repository with a progress indicator.

If *destination* already exists and is a git repo, runs ``git pull``.
Otherwise runs ``git clone``.

Arguments:
url -- the git repository URL
destination -- local directory path
branch -- optional branch name to clone/checkout
show_progress -- print status messages to stdout (default True)

Returns True on success, False on failure.
"""
from .run_command import run_command as _run_command
import os as _os

if _os.path.isdir(_os.path.join(destination, '.git')):
if show_progress:
sys.stdout.write("Updating {} ...\n".format(destination))
sys.stdout.flush()
cmd = ['git', '-C', destination, 'pull']
if branch:
cmd = ['git', '-C', destination, 'pull', 'origin', branch]
result = _run_command(cmd)
else:
if show_progress:
sys.stdout.write("Cloning {} -> {} ...\n".format(url, destination))
sys.stdout.flush()
cmd = ['git', 'clone', url, destination]
if branch:
cmd = ['git', 'clone', '-b', branch, url, destination]
result = _run_command(cmd)

success = (result is not None and result.returncode == 0)
if show_progress:
if success:
sys.stdout.write("Done.\n")
else:
sys.stderr.write("Error: git command failed (exit code {})\n".format(
result.returncode if result else 'N/A'
))
sys.stdout.flush()
return success
44 changes: 42 additions & 2 deletions naomi/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from . import profile
from . import visualizations
from .run_command import run_command
from .rest_api import NaomiRestServer


USE_STANDARD_MIC = 0
Expand All @@ -36,7 +37,8 @@ def __init__(
save_audio=False,
save_passive_audio=False,
save_active_audio=False,
save_noise=False
save_noise=False,
quiet=False
):
global _
profile.set_arg("application", self)
Expand Down Expand Up @@ -81,7 +83,14 @@ def __init__(
# Go ahead and pull the setting
settings_complete = False
if (profile.get_arg("repopulate") or profile.get_arg("profile_missing") or not settings_complete):
populate.run()
if profile.get_arg("quiet"):
self._logger.warning(
"Quiet mode enabled: skipping interactive profile setup."
" Some features may not work until the profile is configured."
" Run without --quiet or run 'Naomi --repopulate' to configure."
)
else:
populate.run()
# keywords will always be a list
# keyword is the first item from the keywords list
keywords = profile.get_profile_var(['keyword'], ['Naomi'])
Expand Down Expand Up @@ -696,6 +705,32 @@ def settings(self):
"default": True
}
),
(
("LLM", "enabled"), {
"type": "boolean",
"title": _("Should I use a large language model?"),
"description": _("With this option enabled, I will attempt to run my output through a large language model before speaking. This can make me much more conversational, but can interfere with my functioning and make me take longer to respond."),
"default": False
}
),
(
("LLM", "completion_url"), {
"title": _("LLM endpoint URL"),
"description": _("The url used for requesting text completion from your LLM"),
"default": "http://localhost:8080/v1/completions",
"active": lambda: profile.get(['LLM', 'enabled'], False)
}
),
(
("LLM", "template"), {
"title": _("LLM Template type"),
"type": "listbox",
"description": _("The jinja2 template to use for formatting prompts for your LLM model."),
"options": ['LLAMA3', 'LLAMA2', 'CHATML'],
"default": "LLAMA3",
"active": lambda: profile.get(['LLM', 'enabled'], False)
}
),
(
("email", "address"), {
"type": "encrypted",
Expand Down Expand Up @@ -1073,6 +1108,11 @@ def list_audio_devices(self):
verbose=(self._logger.getEffectiveLevel() == logging.DEBUG))

def run(self):
try:
self.rest_server = NaomiRestServer(self)
self.rest_server.start()
except Exception as e:
self._logger.warning("Could not start REST API server: {}".format(e))
self.conversation.sayName()
self.conversation.greet()
self.conversation.handleForever()
Loading
Loading