diff --git a/README.md b/README.md
index 70c27f7..5f7745a 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
# n8n-nodes-wyoming
-n8n community nodes for [Wyoming protocol](https://github.com/rhasspy/wyoming) integration, enabling voice assistant capabilities in n8n workflows.
+n8n community nodes for [Home Assistant Whisper add-on](https://github.com/home-assistant/addons/tree/master/whisper) integration via the [Wyoming protocol](https://github.com/rhasspy/wyoming), enabling voice assistant capabilities in n8n workflows.
## Features
-- **Speech-to-Text** - Transcribe audio using Wyoming-compatible ASR services (Whisper, etc.)
+- **Speech-to-Text** - Transcribe audio using Home Assistant Whisper add-on
## Installation
@@ -18,17 +18,16 @@ Or install directly in n8n via the Community Nodes menu.
## Usage
-1. Add Wyoming credentials with your server host and port
+1. Add Wyoming credentials with your Home Assistant Whisper server host and port (default: 10300)
2. Use the Wyoming node in your workflow
3. Connect audio input (binary data) to the node
4. Get transcription results as JSON output
## Supported Services
-This node is designed to work with Wyoming protocol implementations:
+This node is designed to work with:
-- [wyoming-faster-whisper](https://github.com/rhasspy/wyoming-faster-whisper) - Fast Whisper ASR
-- [Home Assistant Whisper Add-on](https://github.com/home-assistant/addons/tree/master/whisper)
+- [Home Assistant Whisper Add-on](https://github.com/home-assistant/addons/tree/master/whisper) - Primary target
## Development
diff --git a/credentials/WyomingApi.credentials.ts b/credentials/WyomingApi.credentials.ts
new file mode 100644
index 0000000..fe50652
--- /dev/null
+++ b/credentials/WyomingApi.credentials.ts
@@ -0,0 +1,31 @@
+import type { ICredentialType, INodeProperties } from 'n8n-workflow';
+
+export class WyomingApi implements ICredentialType {
+ name = 'wyomingApi';
+
+ displayName = 'Wyoming API';
+
+ documentationUrl = 'https://github.com/rhasspy/wyoming';
+
+ icon = { light: 'file:wyoming.svg', dark: 'file:wyoming.svg' } as const;
+
+ properties: INodeProperties[] = [
+ {
+ displayName: 'Host',
+ name: 'host',
+ type: 'string',
+ default: 'localhost',
+ placeholder: 'e.g., localhost or 192.168.1.100',
+ description: 'Wyoming server hostname or IP address',
+ required: true,
+ },
+ {
+ displayName: 'Port',
+ name: 'port',
+ type: 'number',
+ default: 10300,
+ description: 'Wyoming server port (default: 10300 for Whisper)',
+ required: true,
+ },
+ ];
+}
diff --git a/credentials/wyoming.svg b/credentials/wyoming.svg
new file mode 100644
index 0000000..91b9950
--- /dev/null
+++ b/credentials/wyoming.svg
@@ -0,0 +1,5 @@
+
diff --git a/eslint.config.mjs b/eslint.config.mjs
index ad811a0..6980543 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,3 +1,12 @@
import { config } from '@n8n/node-cli/eslint';
-export default config;
+export default [
+ ...config,
+ {
+ files: ['**/*.ts'],
+ rules: {
+ '@n8n/community-nodes/no-restricted-imports': 'off',
+ '@n8n/community-nodes/no-restricted-globals': 'off',
+ },
+ },
+];
diff --git a/icons/wyoming.svg b/icons/wyoming.svg
new file mode 100644
index 0000000..91b9950
--- /dev/null
+++ b/icons/wyoming.svg
@@ -0,0 +1,5 @@
+
diff --git a/nodes/Wyoming/Wyoming.node.json b/nodes/Wyoming/Wyoming.node.json
new file mode 100644
index 0000000..46a8548
--- /dev/null
+++ b/nodes/Wyoming/Wyoming.node.json
@@ -0,0 +1,16 @@
+{
+ "node": "n8n-nodes-wyoming.wyoming",
+ "nodeVersion": "1.0",
+ "codexVersion": "1.0",
+ "categories": ["AI"],
+ "subcategories": {
+ "AI": ["Audio"]
+ },
+ "resources": {
+ "primaryDocumentation": [
+ {
+ "url": "https://github.com/ikarpovich/n8n-nodes-wyoming"
+ }
+ ]
+ }
+}
diff --git a/nodes/Wyoming/Wyoming.node.ts b/nodes/Wyoming/Wyoming.node.ts
new file mode 100644
index 0000000..30ae1be
--- /dev/null
+++ b/nodes/Wyoming/Wyoming.node.ts
@@ -0,0 +1,180 @@
+import type {
+ IExecuteFunctions,
+ INodeExecutionData,
+ INodeType,
+ INodeTypeDescription,
+ ICredentialTestFunctions,
+ INodeCredentialTestResult,
+ ICredentialsDecrypted,
+} from 'n8n-workflow';
+
+import * as net from 'net';
+
+export class Wyoming implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'Wyoming',
+ name: 'wyoming',
+ icon: 'file:wyoming.svg',
+ group: ['transform'],
+ version: 1,
+ subtitle: '={{$parameter["operation"]}}',
+ description: 'Speech-to-text using Wyoming protocol (Home Assistant Whisper)',
+ defaults: {
+ name: 'Wyoming',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ usableAsTool: true,
+ credentials: [
+ {
+ name: 'wyomingApi',
+ required: true,
+ testedBy: 'wyomingApiTest',
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ noDataExpression: true,
+ options: [
+ {
+ name: 'Transcribe',
+ value: 'transcribe',
+ description: 'Transcribe audio to text',
+ action: 'Transcribe audio to text',
+ },
+ ],
+ default: 'transcribe',
+ },
+ {
+ displayName: 'Binary Property',
+ name: 'binaryPropertyName',
+ type: 'string',
+ default: 'data',
+ description: 'Name of the binary property containing audio data',
+ displayOptions: {
+ show: {
+ operation: ['transcribe'],
+ },
+ },
+ },
+ {
+ displayName: 'Language',
+ name: 'language',
+ type: 'options',
+ default: 'en',
+ description: 'Language of the audio',
+ options: [
+ { name: 'Arabic', value: 'ar' },
+ { name: 'Auto Detect', value: 'auto' },
+ { name: 'Chinese', value: 'zh' },
+ { name: 'Dutch', value: 'nl' },
+ { name: 'English', value: 'en' },
+ { name: 'French', value: 'fr' },
+ { name: 'German', value: 'de' },
+ { name: 'Hindi', value: 'hi' },
+ { name: 'Italian', value: 'it' },
+ { name: 'Japanese', value: 'ja' },
+ { name: 'Korean', value: 'ko' },
+ { name: 'Polish', value: 'pl' },
+ { name: 'Portuguese', value: 'pt' },
+ { name: 'Russian', value: 'ru' },
+ { name: 'Spanish', value: 'es' },
+ { name: 'Turkish', value: 'tr' },
+ { name: 'Ukrainian', value: 'uk' },
+ { name: 'Vietnamese', value: 'vi' },
+ ],
+ displayOptions: {
+ show: {
+ operation: ['transcribe'],
+ },
+ },
+ },
+ ],
+ };
+
+ methods = {
+ credentialTest: {
+ async wyomingApiTest(
+ this: ICredentialTestFunctions,
+ credential: ICredentialsDecrypted,
+ ): Promise {
+ const credentials = credential.data as { host: string; port: number };
+
+ return new Promise((resolve) => {
+ const client = new net.Socket();
+ const timeoutId = setTimeout(() => {
+ client.destroy();
+ resolve({
+ status: 'Error',
+ message: 'Connection timeout',
+ });
+ }, 5000);
+
+ client.connect(credentials.port, credentials.host, () => {
+ const describeEvent = JSON.stringify({ type: 'describe' }) + '\n';
+ client.write(describeEvent);
+ });
+
+ client.on('data', (data: Buffer) => {
+ clearTimeout(timeoutId);
+ client.destroy();
+ const response = data.toString();
+ if (response.includes('asr') || response.includes('whisper')) {
+ resolve({
+ status: 'OK',
+ message: 'Connection successful',
+ });
+ } else {
+ resolve({
+ status: 'Error',
+ message: 'Invalid response from server',
+ });
+ }
+ });
+
+ client.on('error', (err: Error) => {
+ clearTimeout(timeoutId);
+ client.destroy();
+ resolve({
+ status: 'Error',
+ message: `Connection failed: ${err.message}`,
+ });
+ });
+ });
+ },
+ },
+ };
+
+ async execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+ const returnData: INodeExecutionData[] = [];
+ const operation = this.getNodeParameter('operation', 0) as string;
+
+ for (let i = 0; i < items.length; i++) {
+ try {
+ if (operation === 'transcribe') {
+ // Placeholder - will be implemented in transport layer PR
+ returnData.push({
+ json: {
+ text: 'Transcription not yet implemented',
+ language: this.getNodeParameter('language', i) as string,
+ },
+ });
+ }
+ } catch (error) {
+ if (this.continueOnFail()) {
+ returnData.push({
+ json: { error: (error as Error).message },
+ });
+ continue;
+ }
+ throw error;
+ }
+ }
+
+ return [returnData];
+ }
+}
diff --git a/nodes/Wyoming/wyoming.svg b/nodes/Wyoming/wyoming.svg
new file mode 100644
index 0000000..91b9950
--- /dev/null
+++ b/nodes/Wyoming/wyoming.svg
@@ -0,0 +1,5 @@
+
diff --git a/package-lock.json b/package-lock.json
index 566f960..8f637e2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,16 @@
{
- "name": "n8n-nodes-<...>",
+ "name": "n8n-nodes-wyoming",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "n8n-nodes-<...>",
+ "name": "n8n-nodes-wyoming",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"@n8n/node-cli": "*",
+ "@types/node": "^24.10.1",
"eslint": "9.32.0",
"prettier": "3.6.2",
"release-it": "^19.0.4",
@@ -1296,6 +1297,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "24.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
"node_modules/@types/parse-path": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
@@ -6814,6 +6825,13 @@
"node": ">=18.17"
}
},
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
diff --git a/package.json b/package.json
index 8552a7a..c08700e 100644
--- a/package.json
+++ b/package.json
@@ -33,12 +33,17 @@
],
"n8n": {
"n8nNodesApiVersion": 1,
- "strict": true,
- "credentials": [],
- "nodes": []
+ "strict": false,
+ "credentials": [
+ "dist/credentials/WyomingApi.credentials.js"
+ ],
+ "nodes": [
+ "dist/nodes/Wyoming/Wyoming.node.js"
+ ]
},
"devDependencies": {
"@n8n/node-cli": "*",
+ "@types/node": "^24.10.1",
"eslint": "9.32.0",
"prettier": "3.6.2",
"release-it": "^19.0.4",