wogiflow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Voice Input
|
|
5
|
+
*
|
|
6
|
+
* Voice-to-transcript support with multiple provider options:
|
|
7
|
+
* - Local: Whisper.cpp (no API key required)
|
|
8
|
+
* - Cloud: OpenAI Whisper API
|
|
9
|
+
* - Cloud: Groq (free tier available)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* flow voice-input # Record and transcribe
|
|
13
|
+
* flow voice-input --duration 30 # Record for 30 seconds
|
|
14
|
+
* flow voice-input --provider openai # Use specific provider
|
|
15
|
+
* flow voice-input --to-story # Create story from transcript
|
|
16
|
+
* flow voice-input setup # Interactive setup
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { spawn, execSync } = require('child_process');
|
|
22
|
+
const { HttpClient } = require('./flow-http-client');
|
|
23
|
+
const readline = require('readline');
|
|
24
|
+
const { getConfig, getProjectRoot, colors: c } = require('./flow-utils');
|
|
25
|
+
|
|
26
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
27
|
+
const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
|
|
28
|
+
const CONFIG_PATH = path.join(WORKFLOW_DIR, 'config.json');
|
|
29
|
+
|
|
30
|
+
// ============================================================
|
|
31
|
+
// Voice Provider Types
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
const VOICE_PROVIDERS = {
|
|
35
|
+
LOCAL: 'local',
|
|
36
|
+
OPENAI: 'openai',
|
|
37
|
+
GROQ: 'groq'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const PROVIDER_INFO = {
|
|
41
|
+
local: {
|
|
42
|
+
name: 'Local (Whisper.cpp)',
|
|
43
|
+
description: 'Run transcription locally - no API key required',
|
|
44
|
+
requiresKey: false,
|
|
45
|
+
binaryName: 'whisper'
|
|
46
|
+
},
|
|
47
|
+
openai: {
|
|
48
|
+
name: 'OpenAI Whisper',
|
|
49
|
+
description: 'Cloud transcription via OpenAI API',
|
|
50
|
+
requiresKey: true,
|
|
51
|
+
endpoint: 'https://api.openai.com/v1/audio/transcriptions',
|
|
52
|
+
model: 'whisper-1'
|
|
53
|
+
},
|
|
54
|
+
groq: {
|
|
55
|
+
name: 'Groq',
|
|
56
|
+
description: 'Fast cloud transcription - free tier available',
|
|
57
|
+
requiresKey: true,
|
|
58
|
+
endpoint: 'https://api.groq.com/openai/v1/audio/transcriptions',
|
|
59
|
+
model: 'whisper-large-v3'
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// Configuration
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get voice configuration from config.json
|
|
69
|
+
*/
|
|
70
|
+
function getVoiceConfig() {
|
|
71
|
+
const config = getConfig();
|
|
72
|
+
return config.voice || {
|
|
73
|
+
enabled: false,
|
|
74
|
+
provider: null,
|
|
75
|
+
openaiApiKey: null,
|
|
76
|
+
groqApiKey: null,
|
|
77
|
+
localModelPath: null,
|
|
78
|
+
defaultDuration: 30,
|
|
79
|
+
sampleRate: 16000,
|
|
80
|
+
channels: 1
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if voice input is enabled
|
|
86
|
+
*/
|
|
87
|
+
function isVoiceEnabled() {
|
|
88
|
+
const config = getVoiceConfig();
|
|
89
|
+
return config.enabled === true && config.provider !== null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the active provider
|
|
94
|
+
*/
|
|
95
|
+
function getActiveProvider() {
|
|
96
|
+
const config = getVoiceConfig();
|
|
97
|
+
return config.provider;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// Audio Recording
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if recording dependencies are available
|
|
106
|
+
*/
|
|
107
|
+
function checkRecordingDependencies() {
|
|
108
|
+
const issues = [];
|
|
109
|
+
|
|
110
|
+
// Check for sox (required for most recorders)
|
|
111
|
+
try {
|
|
112
|
+
execSync('which sox', { stdio: 'pipe' });
|
|
113
|
+
} catch {
|
|
114
|
+
issues.push('sox not found - install with: brew install sox');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for rec (part of sox)
|
|
118
|
+
try {
|
|
119
|
+
execSync('which rec', { stdio: 'pipe' });
|
|
120
|
+
} catch {
|
|
121
|
+
issues.push('rec not found - install with: brew install sox');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return issues;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Record audio from microphone
|
|
129
|
+
* Returns path to recorded WAV file
|
|
130
|
+
*/
|
|
131
|
+
async function recordAudio(durationSeconds = 30, options = {}) {
|
|
132
|
+
const {
|
|
133
|
+
sampleRate = 16000,
|
|
134
|
+
channels = 1,
|
|
135
|
+
showProgress = true
|
|
136
|
+
} = options;
|
|
137
|
+
|
|
138
|
+
const tempFile = path.join('/tmp', `wogi-voice-${Date.now()}.wav`);
|
|
139
|
+
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
if (showProgress) {
|
|
142
|
+
console.log(`${c.cyan}Recording for ${durationSeconds} seconds...${c.reset}`);
|
|
143
|
+
console.log(`${c.dim}Press Ctrl+C to stop early${c.reset}\n`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Use sox's rec command for cross-platform recording
|
|
147
|
+
const rec = spawn('rec', [
|
|
148
|
+
'-r', String(sampleRate),
|
|
149
|
+
'-c', String(channels),
|
|
150
|
+
'-b', '16',
|
|
151
|
+
tempFile,
|
|
152
|
+
'trim', '0', String(durationSeconds)
|
|
153
|
+
], {
|
|
154
|
+
stdio: showProgress ? ['inherit', 'inherit', 'inherit'] : 'pipe'
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
rec.on('close', (code) => {
|
|
158
|
+
if (code === 0 && fs.existsSync(tempFile)) {
|
|
159
|
+
resolve(tempFile);
|
|
160
|
+
} else {
|
|
161
|
+
reject(new Error(`Recording failed with code ${code}`));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
rec.on('error', (err) => {
|
|
166
|
+
reject(new Error(`Recording error: ${err.message}`));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================
|
|
172
|
+
// Transcription Providers
|
|
173
|
+
// ============================================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Transcribe with local Whisper.cpp
|
|
177
|
+
*/
|
|
178
|
+
async function transcribeLocal(audioPath, options = {}) {
|
|
179
|
+
const config = getVoiceConfig();
|
|
180
|
+
const modelPath = config.localModelPath || 'base.en';
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// Check if whisper is available
|
|
184
|
+
execSync('which whisper', { stdio: 'pipe' });
|
|
185
|
+
} catch {
|
|
186
|
+
throw new Error(
|
|
187
|
+
'Local Whisper not found. Install with:\n' +
|
|
188
|
+
' brew install openai-whisper\n' +
|
|
189
|
+
'Or download whisper.cpp from: https://github.com/ggerganov/whisper.cpp'
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const whisper = spawn('whisper', [
|
|
195
|
+
audioPath,
|
|
196
|
+
'--model', modelPath,
|
|
197
|
+
'--output_format', 'txt',
|
|
198
|
+
'--output_dir', '/tmp'
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
let stderr = '';
|
|
202
|
+
whisper.stderr.on('data', (data) => {
|
|
203
|
+
stderr += data.toString();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
whisper.on('close', (code) => {
|
|
207
|
+
if (code === 0) {
|
|
208
|
+
// Read the output file
|
|
209
|
+
const outputPath = audioPath.replace('.wav', '.txt');
|
|
210
|
+
if (fs.existsSync(outputPath)) {
|
|
211
|
+
const text = fs.readFileSync(outputPath, 'utf-8').trim();
|
|
212
|
+
fs.unlinkSync(outputPath); // Cleanup
|
|
213
|
+
resolve({ text, provider: 'local', model: modelPath });
|
|
214
|
+
} else {
|
|
215
|
+
reject(new Error('Whisper output file not found'));
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
reject(new Error(`Whisper failed: ${stderr}`));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Transcribe with OpenAI Whisper API using shared HttpClient
|
|
226
|
+
*/
|
|
227
|
+
async function transcribeOpenAI(audioPath, options = {}) {
|
|
228
|
+
const config = getVoiceConfig();
|
|
229
|
+
const apiKey = config.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
230
|
+
|
|
231
|
+
if (!apiKey) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
'OpenAI API key not found. Set it in config.json:\n' +
|
|
234
|
+
' "voice": { "openaiApiKey": "sk-..." }\n' +
|
|
235
|
+
'Or set OPENAI_API_KEY environment variable'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const audioData = fs.readFileSync(audioPath);
|
|
240
|
+
const client = new HttpClient('https://api.openai.com', {
|
|
241
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
242
|
+
timeout: 60000
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const response = await client.postMultipart('/v1/audio/transcriptions', [
|
|
246
|
+
{ name: 'file', value: audioData, filename: 'audio.wav', contentType: 'audio/wav' },
|
|
247
|
+
{ name: 'model', value: 'whisper-1' }
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
if (response.data?.error) {
|
|
251
|
+
throw new Error(response.data.error.message);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { text: response.data.text, provider: 'openai', model: 'whisper-1' };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Transcribe with Groq API using shared HttpClient
|
|
259
|
+
*/
|
|
260
|
+
async function transcribeGroq(audioPath, options = {}) {
|
|
261
|
+
const config = getVoiceConfig();
|
|
262
|
+
const apiKey = config.groqApiKey || process.env.GROQ_API_KEY;
|
|
263
|
+
|
|
264
|
+
if (!apiKey) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
'Groq API key not found. Set it in config.json:\n' +
|
|
267
|
+
' "voice": { "groqApiKey": "gsk_..." }\n' +
|
|
268
|
+
'Or set GROQ_API_KEY environment variable\n\n' +
|
|
269
|
+
'Get a free key at: https://console.groq.com'
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const audioData = fs.readFileSync(audioPath);
|
|
274
|
+
const client = new HttpClient('https://api.groq.com', {
|
|
275
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
276
|
+
timeout: 60000
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const response = await client.postMultipart('/openai/v1/audio/transcriptions', [
|
|
280
|
+
{ name: 'file', value: audioData, filename: 'audio.wav', contentType: 'audio/wav' },
|
|
281
|
+
{ name: 'model', value: 'whisper-large-v3' }
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
if (response.data?.error) {
|
|
285
|
+
throw new Error(response.data.error.message);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { text: response.data.text, provider: 'groq', model: 'whisper-large-v3' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Main transcription function - routes to appropriate provider
|
|
293
|
+
*/
|
|
294
|
+
async function transcribe(audioPath, providerOverride = null) {
|
|
295
|
+
const config = getVoiceConfig();
|
|
296
|
+
const provider = providerOverride || config.provider || 'openai';
|
|
297
|
+
|
|
298
|
+
switch (provider) {
|
|
299
|
+
case 'local':
|
|
300
|
+
return transcribeLocal(audioPath);
|
|
301
|
+
case 'openai':
|
|
302
|
+
return transcribeOpenAI(audioPath);
|
|
303
|
+
case 'groq':
|
|
304
|
+
return transcribeGroq(audioPath);
|
|
305
|
+
default:
|
|
306
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ============================================================
|
|
311
|
+
// Interactive Setup
|
|
312
|
+
// ============================================================
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Interactive setup wizard for voice input
|
|
316
|
+
*/
|
|
317
|
+
async function runSetup() {
|
|
318
|
+
const rl = readline.createInterface({
|
|
319
|
+
input: process.stdin,
|
|
320
|
+
output: process.stdout
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const question = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
324
|
+
|
|
325
|
+
console.log(`\n${c.cyan}=== Wogi Flow Voice Input Setup ===${c.reset}\n`);
|
|
326
|
+
|
|
327
|
+
// Check recording dependencies
|
|
328
|
+
const issues = checkRecordingDependencies();
|
|
329
|
+
if (issues.length > 0) {
|
|
330
|
+
console.log(`${c.yellow}Recording dependencies missing:${c.reset}`);
|
|
331
|
+
issues.forEach(i => console.log(` - ${i}`));
|
|
332
|
+
console.log('');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Enable voice input
|
|
336
|
+
const enable = await question(`Enable voice input? (y/n): `);
|
|
337
|
+
if (enable.toLowerCase() !== 'y') {
|
|
338
|
+
console.log(`${c.dim}Voice input disabled.${c.reset}`);
|
|
339
|
+
rl.close();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Choose provider
|
|
344
|
+
console.log(`\n${c.cyan}Choose transcription provider:${c.reset}`);
|
|
345
|
+
console.log(' 1. Local (Whisper.cpp) - No API key, works offline');
|
|
346
|
+
console.log(' 2. OpenAI - Best accuracy, requires API key');
|
|
347
|
+
console.log(' 3. Groq - Fast, free tier available');
|
|
348
|
+
|
|
349
|
+
const providerChoice = await question(`\nSelect provider (1-3): `);
|
|
350
|
+
let provider, apiKey = null;
|
|
351
|
+
|
|
352
|
+
switch (providerChoice) {
|
|
353
|
+
case '1':
|
|
354
|
+
provider = 'local';
|
|
355
|
+
console.log(`\n${c.green}Local provider selected.${c.reset}`);
|
|
356
|
+
console.log(`${c.dim}Ensure whisper is installed: brew install openai-whisper${c.reset}`);
|
|
357
|
+
break;
|
|
358
|
+
case '2':
|
|
359
|
+
provider = 'openai';
|
|
360
|
+
apiKey = await question(`\nEnter OpenAI API key (sk-...): `);
|
|
361
|
+
if (!apiKey.startsWith('sk-')) {
|
|
362
|
+
console.log(`${c.yellow}Warning: Key doesn't look like an OpenAI key${c.reset}`);
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
case '3':
|
|
366
|
+
provider = 'groq';
|
|
367
|
+
apiKey = await question(`\nEnter Groq API key (gsk_...): `);
|
|
368
|
+
if (!apiKey.startsWith('gsk_')) {
|
|
369
|
+
console.log(`${c.yellow}Warning: Key doesn't look like a Groq key${c.reset}`);
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
default:
|
|
373
|
+
console.log(`${c.red}Invalid choice${c.reset}`);
|
|
374
|
+
rl.close();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Save configuration
|
|
379
|
+
const config = getConfig();
|
|
380
|
+
config.voice = {
|
|
381
|
+
enabled: true,
|
|
382
|
+
provider,
|
|
383
|
+
openaiApiKey: provider === 'openai' ? apiKey : (config.voice?.openaiApiKey || null),
|
|
384
|
+
groqApiKey: provider === 'groq' ? apiKey : (config.voice?.groqApiKey || null),
|
|
385
|
+
localModelPath: config.voice?.localModelPath || 'base.en',
|
|
386
|
+
defaultDuration: 30,
|
|
387
|
+
sampleRate: 16000,
|
|
388
|
+
channels: 1
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
392
|
+
|
|
393
|
+
console.log(`\n${c.green}Voice input configured!${c.reset}`);
|
|
394
|
+
console.log(`Provider: ${PROVIDER_INFO[provider].name}`);
|
|
395
|
+
console.log(`\nTest with: ${c.cyan}./scripts/flow voice-input${c.reset}`);
|
|
396
|
+
|
|
397
|
+
rl.close();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ============================================================
|
|
401
|
+
// Main CLI Handler
|
|
402
|
+
// ============================================================
|
|
403
|
+
|
|
404
|
+
async function main() {
|
|
405
|
+
const args = process.argv.slice(2);
|
|
406
|
+
const command = args[0];
|
|
407
|
+
|
|
408
|
+
// Parse options
|
|
409
|
+
const options = {
|
|
410
|
+
duration: 30,
|
|
411
|
+
provider: null,
|
|
412
|
+
toStory: false,
|
|
413
|
+
output: null
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
for (let i = 0; i < args.length; i++) {
|
|
417
|
+
switch (args[i]) {
|
|
418
|
+
case '--duration':
|
|
419
|
+
case '-d':
|
|
420
|
+
options.duration = parseInt(args[++i]) || 30;
|
|
421
|
+
break;
|
|
422
|
+
case '--provider':
|
|
423
|
+
case '-p':
|
|
424
|
+
options.provider = args[++i];
|
|
425
|
+
break;
|
|
426
|
+
case '--to-story':
|
|
427
|
+
options.toStory = true;
|
|
428
|
+
break;
|
|
429
|
+
case '--output':
|
|
430
|
+
case '-o':
|
|
431
|
+
options.output = args[++i];
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Handle commands
|
|
437
|
+
switch (command) {
|
|
438
|
+
case 'setup':
|
|
439
|
+
await runSetup();
|
|
440
|
+
break;
|
|
441
|
+
|
|
442
|
+
case 'status':
|
|
443
|
+
showStatus();
|
|
444
|
+
break;
|
|
445
|
+
|
|
446
|
+
case 'test':
|
|
447
|
+
await testVoiceInput();
|
|
448
|
+
break;
|
|
449
|
+
|
|
450
|
+
case undefined:
|
|
451
|
+
case 'record':
|
|
452
|
+
await recordAndTranscribe(options);
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
default:
|
|
456
|
+
showHelp();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Show voice input status
|
|
462
|
+
*/
|
|
463
|
+
function showStatus() {
|
|
464
|
+
const config = getVoiceConfig();
|
|
465
|
+
|
|
466
|
+
console.log(`\n${c.cyan}Voice Input Status${c.reset}\n`);
|
|
467
|
+
console.log(`Enabled: ${config.enabled ? c.green + 'Yes' : c.red + 'No'}${c.reset}`);
|
|
468
|
+
|
|
469
|
+
if (config.enabled) {
|
|
470
|
+
const providerInfo = PROVIDER_INFO[config.provider] || {};
|
|
471
|
+
console.log(`Provider: ${providerInfo.name || config.provider}`);
|
|
472
|
+
console.log(`Default duration: ${config.defaultDuration}s`);
|
|
473
|
+
|
|
474
|
+
if (config.provider === 'openai') {
|
|
475
|
+
console.log(`API Key: ${config.openaiApiKey ? c.green + 'Configured' : c.red + 'Missing'}${c.reset}`);
|
|
476
|
+
} else if (config.provider === 'groq') {
|
|
477
|
+
console.log(`API Key: ${config.groqApiKey ? c.green + 'Configured' : c.red + 'Missing'}${c.reset}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Check dependencies
|
|
482
|
+
const issues = checkRecordingDependencies();
|
|
483
|
+
if (issues.length > 0) {
|
|
484
|
+
console.log(`\n${c.yellow}Missing dependencies:${c.reset}`);
|
|
485
|
+
issues.forEach(i => console.log(` - ${i}`));
|
|
486
|
+
} else {
|
|
487
|
+
console.log(`\n${c.green}Recording dependencies OK${c.reset}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Test voice input with a short recording
|
|
493
|
+
*/
|
|
494
|
+
async function testVoiceInput() {
|
|
495
|
+
const config = getVoiceConfig();
|
|
496
|
+
|
|
497
|
+
if (!config.enabled) {
|
|
498
|
+
console.log(`${c.yellow}Voice input not enabled. Run: ./scripts/flow voice-input setup${c.reset}`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
console.log(`${c.cyan}Testing voice input...${c.reset}`);
|
|
503
|
+
console.log(`Provider: ${PROVIDER_INFO[config.provider]?.name || config.provider}\n`);
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
console.log('Recording 5 seconds of audio...\n');
|
|
507
|
+
const audioPath = await recordAudio(5);
|
|
508
|
+
|
|
509
|
+
console.log(`\n${c.cyan}Transcribing...${c.reset}`);
|
|
510
|
+
const result = await transcribe(audioPath);
|
|
511
|
+
|
|
512
|
+
console.log(`\n${c.green}Success!${c.reset}`);
|
|
513
|
+
console.log(`Transcript: "${result.text}"`);
|
|
514
|
+
console.log(`Provider: ${result.provider}, Model: ${result.model}`);
|
|
515
|
+
|
|
516
|
+
// Cleanup
|
|
517
|
+
if (fs.existsSync(audioPath)) {
|
|
518
|
+
fs.unlinkSync(audioPath);
|
|
519
|
+
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
console.error(`${c.red}Test failed: ${error.message}${c.reset}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Main record and transcribe flow
|
|
527
|
+
*/
|
|
528
|
+
async function recordAndTranscribe(options) {
|
|
529
|
+
const config = getVoiceConfig();
|
|
530
|
+
|
|
531
|
+
if (!config.enabled) {
|
|
532
|
+
console.log(`${c.yellow}Voice input not enabled.${c.reset}`);
|
|
533
|
+
console.log(`Run: ${c.cyan}./scripts/flow voice-input setup${c.reset}`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
// Record
|
|
539
|
+
const duration = options.duration || config.defaultDuration || 30;
|
|
540
|
+
const audioPath = await recordAudio(duration);
|
|
541
|
+
|
|
542
|
+
// Transcribe
|
|
543
|
+
console.log(`\n${c.cyan}Transcribing...${c.reset}`);
|
|
544
|
+
const result = await transcribe(audioPath, options.provider);
|
|
545
|
+
|
|
546
|
+
console.log(`\n${c.green}Transcript:${c.reset}`);
|
|
547
|
+
console.log(result.text);
|
|
548
|
+
|
|
549
|
+
// Output to file if requested
|
|
550
|
+
if (options.output) {
|
|
551
|
+
fs.writeFileSync(options.output, result.text);
|
|
552
|
+
console.log(`\n${c.dim}Saved to: ${options.output}${c.reset}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Create story if requested
|
|
556
|
+
if (options.toStory) {
|
|
557
|
+
console.log(`\n${c.cyan}Creating story from transcript...${c.reset}`);
|
|
558
|
+
// This would integrate with flow-story.js
|
|
559
|
+
console.log(`${c.dim}[Integration with story creation would go here]${c.reset}`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Cleanup
|
|
563
|
+
if (fs.existsSync(audioPath)) {
|
|
564
|
+
fs.unlinkSync(audioPath);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Return result for programmatic use
|
|
568
|
+
return result;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.error(`${c.red}Error: ${error.message}${c.reset}`);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Show help
|
|
577
|
+
*/
|
|
578
|
+
function showHelp() {
|
|
579
|
+
console.log(`
|
|
580
|
+
${c.cyan}Wogi Flow - Voice Input${c.reset}
|
|
581
|
+
|
|
582
|
+
${c.bold}Usage:${c.reset}
|
|
583
|
+
flow voice-input [command] [options]
|
|
584
|
+
|
|
585
|
+
${c.bold}Commands:${c.reset}
|
|
586
|
+
setup Interactive setup wizard
|
|
587
|
+
status Show voice input configuration
|
|
588
|
+
test Test with a 5-second recording
|
|
589
|
+
record Record and transcribe (default)
|
|
590
|
+
|
|
591
|
+
${c.bold}Options:${c.reset}
|
|
592
|
+
--duration, -d Recording duration in seconds (default: 30)
|
|
593
|
+
--provider, -p Override provider (local, openai, groq)
|
|
594
|
+
--to-story Create a story from the transcript
|
|
595
|
+
--output, -o Save transcript to file
|
|
596
|
+
|
|
597
|
+
${c.bold}Examples:${c.reset}
|
|
598
|
+
flow voice-input setup # Configure voice input
|
|
599
|
+
flow voice-input # Record and transcribe
|
|
600
|
+
flow voice-input -d 60 # Record for 60 seconds
|
|
601
|
+
flow voice-input -p groq # Use Groq provider
|
|
602
|
+
flow voice-input --to-story # Create story from voice
|
|
603
|
+
|
|
604
|
+
${c.bold}Providers:${c.reset}
|
|
605
|
+
local - Whisper.cpp (no API key, works offline)
|
|
606
|
+
openai - OpenAI Whisper API (best accuracy)
|
|
607
|
+
groq - Groq API (fast, free tier available)
|
|
608
|
+
`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ============================================================
|
|
612
|
+
// Exports
|
|
613
|
+
// ============================================================
|
|
614
|
+
|
|
615
|
+
module.exports = {
|
|
616
|
+
getVoiceConfig,
|
|
617
|
+
isVoiceEnabled,
|
|
618
|
+
getActiveProvider,
|
|
619
|
+
recordAudio,
|
|
620
|
+
transcribe,
|
|
621
|
+
transcribeLocal,
|
|
622
|
+
transcribeOpenAI,
|
|
623
|
+
transcribeGroq,
|
|
624
|
+
checkRecordingDependencies,
|
|
625
|
+
VOICE_PROVIDERS,
|
|
626
|
+
PROVIDER_INFO
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// ============================================================
|
|
630
|
+
// Run CLI
|
|
631
|
+
// ============================================================
|
|
632
|
+
|
|
633
|
+
if (require.main === module) {
|
|
634
|
+
main().catch(err => {
|
|
635
|
+
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
|
636
|
+
process.exit(1);
|
|
637
|
+
});
|
|
638
|
+
}
|