Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,6 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/

# Ghostwriter stuff
*.backup
149 changes: 149 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,151 @@
# Speechhandler_Creator
A vibe-coding project with ChatGPT - an editor for creating speechhandler plugins

The **Speechhandler_Creator** is a graphical tool for building and publishing speechhandler plugins for the [Naomi voice assistant](https://github.com/NaomiProject/Naomi).

It simplifies the entire process of:
- Defining **intents** (keywords + templates) for your plugin
- Generating a Python `SpeechHandler` plugin template
- Publishing your plugin to GitHub
- Submitting a pull request to the [Naomi Plugin Exchange](https://github.com/NaomiProject/naomi-plugins)

This tool helps developers jump straight into writing plugin logic without worrying about boilerplate setup.

---

## Installation

Clone this repository:

```bash
git clone https://github.com/NaomiProject/naomi-plugin-creator.git
cd naomi-plugin-creator
```

Install dependencies:

```bash
pip install -r requirements.txt
```

Requirements:
- Python 3.9+
- [tkinter](https://docs.python.org/3/library/tkinter.html) (usually comes with Python)
- [PyGithub](https://pygithub.readthedocs.io/en/latest/)
- A GitHub account with a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) (classic, repo scope)

---

## Running the Tool

Start the application:

```bash
python main.py
```

The Plugin Creator window will open with tabs for:
- **General Info** – name, description, license, repo URL
- **Intents** – define keywords and templates
- **Locales** – manage multiple language variants
- **Publish** – push your plugin to GitHub and the Naomi registry

---

## Creating a Plugin (Example: Weather Report)

Let’s create a plugin called `WeatherPlugin`.

### Step 1: General Info
- **Name**: `WeatherPlugin`
- **Description**: "Provides local weather forecasts."
- **License**: `MIT`
- **Repo URL**: leave blank to let the tool create a GitHub repo for you.

### Step 2: Add Intents
Think of how you might expect a user to interact with your plugin. For the weather plugin, the user might use phrases such as:
* What is the forecast?
* Will it be windy this afternoon?
* When is it going to rain in Cincinnati?

Obviously, you don't want to have to enter every possible way that a user might interact with your plugin, but you can break down the phrases into templates and keywords. An additional benefit of this is that Naomi will return the user's request to your plugin with the keywords split out already so your plugin does not have to parse the user's input directly. Keywords found in user input can be picked up from the "intent['matches']" key in the "intent" object received by the "handle" method.

An "intent" for Naomi is basically asking for a particular program to be run. Each intent can be sent to a different handler function, or you can have one handler function for your intent that performs different functions depending on the value of "intent['intent']".

To add an intent, click the "Add" button next to the "intents" area. Give your intent a name, then a brief description to help users know what your plugin does.

#### Step 2A: Add Keywords
Add keywords so Naomi can extract values from user input.

Click the "Add" button next to "Keyword Categories".
Give your keyword a descriptive name and add a comma separated list of possible values.

Example keywords output:
```json
{
"ForecastKeyword": ["forecast", "outlook"],
"WeatherConditionKeyword": ["rain", "snow", "be windy", "be sunny"],
"DayKeyword": ["today", "tomorrow", "Monday", "this"],
"TimeOfDayKeyword": ["morning", "afternoon", "evening", "night"],
"LocationKeyword": ["Cincinnati", "New York", "Chicago"]
}
```

#### Step 2B: Add Templates
Templates show how keywords fit into natural phrases.

Example templates:
- `"What is the {ForecastKeyword}?"`
- `"Will it {WeatherConditionKeyword} {DayKeyword} {TimeOfDayKeyword}?"`
- `"When is it going to {WeatherConditionKeyword} in {LocationKeyword}?"`

Use curly brackets to denote that the intent parser should expect a keyword from a keyword list in that location.

### Step 3: Generate Plugin
Click **Generate Plugin**

This creates:
```
WeatherPlugin/
├── __init__.py # plugin template
├── plugin.info
├── README.md
```

The starter plugin simply repeats back the detected intent for debugging. To test your plugin, copy the whole directory to one of the Naomi speechhandler plugins directories - either ~/Naomi/plugins/speechhander or ~/.config/naomi/plugins/speechhandler - and re-start Naomi. Watch for any error messages saying that your plugin has been skipped. Try triggering your plugin by saying some of the phrases you designed for it.

### Step 4: Implement Your Logic
Open `__init__.py` in your favorite editor and replace the handler logic with real API calls.

### Step 5: Publish
When ready:
1. Re-open the Plugin Creator
2. Go to the **Publish** tab
3. Select your plugin project
4. Enter your [GitHub token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
5. Click **Publish**

The tool will:
- Commit any changes
- Push to your GitHub repo
- Create a pull request to add your plugin to the Naomi registry

---
## Troubleshooting

One particularly vexing issue was trying to publish an update when the "master" branch of my forked copy of the naomi-plugins repository had diverged from the "master" branch of the main repository. This was causing the final pull request to fail. After trying a few things, I ended up simply deleting my forked copy and re-forking it fresh, which was okay because there really wasn't anything in there that I was worried about losing. In the years since I first forked that repository, I have learned the value of always creating a new branch for my changes so I can keep the master/main branch synchronized with upstream.

If you run into other issues with this program, please open an issue on the Github repository.

---

## Contributing

Pull requests are welcome!
See [Naomi Project](https://github.com/NaomiProject/Naomi) for full developer documentation.

---

## License

MIT License (same as the Naomi project).
107 changes: 107 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
# main.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from plugin_editor import PluginEditor
from publisher import publish_plugin_folder, _read_plugin_info
import logging


class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Naomi Plugin Tool")
self.geometry("980x720")
self._logger = logging.getLogger(__name__)

nb = ttk.Notebook(self)
nb.pack(fill="both", expand=True)

# Editor tab
self.editor = PluginEditor(nb)
nb.add(self.editor, text="Plugin Editor")

# Publish tab
self.pub_tab = ttk.Frame(nb)
nb.add(self.pub_tab, text="Publish")
self._build_publish_tab()

def _build_publish_tab(self):
f = self.pub_tab

row = 0
ttk.Label(f, text="GitHub Username").grid(row=row, column=0, sticky="w", padx=10, pady=8)
self.gh_user = ttk.Entry(f, width=40)
self.gh_user.grid(row=row, column=1, sticky="w", padx=10, pady=8)

row += 1
ttk.Label(f, text="GitHub Token (for PR & fork)").grid(row=row, column=0, sticky="w", padx=10, pady=8)
self.gh_token = ttk.Entry(f, width=60, show="*")
self.gh_token.grid(row=row, column=1, sticky="w", padx=10, pady=8)

row += 1
ttk.Label(f, text="Plugin Folder").grid(row=row, column=0, sticky="w", padx=10, pady=8)
pf_row = ttk.Frame(f); pf_row.grid(row=row, column=1, sticky="w", padx=10, pady=8)
self.plugin_folder = ttk.Entry(pf_row, width=60)
self.plugin_folder.pack(side="left")
ttk.Button(pf_row, text="Browse", command=self._pick_folder).pack(side="left", padx=6)

row += 1
sep = ttk.Separator(f, orient="horizontal")
sep.grid(row=row, column=0, columnspan=2, sticky="ew", padx=10, pady=10)

row += 1
ttk.Label(f, text="Detected from plugin.info").grid(row=row, column=0, sticky="w", padx=10, pady=4)

row += 1
ttk.Label(f, text="Plugin Name").grid(row=row, column=0, sticky="w", padx=10, pady=4)
self.name_val = ttk.Entry(f, width=50); self.name_val.grid(row=row, column=1, sticky="w", padx=10, pady=4)

row += 1
ttk.Label(f, text="Repo URL (SSH)").grid(row=row, column=0, sticky="w", padx=10, pady=4)
self.repo_val = ttk.Entry(f, width=70); self.repo_val.grid(row=row, column=1, sticky="w", padx=10, pady=4)

row += 1
ttk.Label(f, text="License").grid(row=row, column=0, sticky="w", padx=10, pady=4)
self.license_val = ttk.Entry(f, width=30); self.license_val.grid(row=row, column=1, sticky="w", padx=10, pady=4)

row += 1
ttk.Button(f, text="Publish (Push + PR)", command=self._do_publish).grid(row=row, column=0, columnspan=2, pady=18)

# stretch cols
f.grid_columnconfigure(1, weight=1)

def _pick_folder(self):
folder = filedialog.askdirectory(title="Select Plugin Folder")
if not folder:
return
self.plugin_folder.delete(0, "end")
self.plugin_folder.insert(0, folder)

try:
info = _read_plugin_info(folder)
self.name_val.delete(0, "end"); self.name_val.insert(0, info.get("name", ""))
self.repo_val.delete(0, "end"); self.repo_val.insert(0, info.get("repo_url", ""))
self.license_val.delete(0, "end"); self.license_val.insert(0, info.get("license", ""))
except Exception as e:
messagebox.showerror("Error", f"Failed to read plugin.info: {e}")
self._logger.error(f"Failed to read plugin.info: {e}", exc_info=True)

def _do_publish(self):
folder = self.plugin_folder.get().strip()
token = self.gh_token.get().strip()
user = self.gh_user.get().strip()

if not folder or not token or not user:
messagebox.showerror("Missing info", "Please provide GitHub username, token, and plugin folder.")
return

try:
pr_url = publish_plugin_folder(folder, token, user)
messagebox.showinfo("Success", f"Pull Request created:\n{pr_url}")
except Exception as e:
messagebox.showerror("Publish failed", str(e))


if __name__ == "__main__":
App().mainloop()
Loading