docs(stt): Add implementation plan of speech-to-text support in neuro… - #789
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughA new planning document detailing a provider-agnostic STT (Speech-to-Text) architecture for NeuroLink with Google Cloud Speech-to-Text as the initial provider, including SDK APIs, CLI integration, normalized output models, and an extensibility roadmap for additional providers. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~15 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@memory-bank/neurolink-stt-support-plan.md`:
- Around line 15-47: Replace the redundant phrase "SDK + CLI interfaces" with
"SDK + CLI APIs" throughout the document; locate the exact string "SDK + CLI
interfaces" (e.g., the top header currently "Consistent SDK + CLI APIs" or any
occurrences in the body/Table of Contents) and change it to "SDK + CLI APIs" (or
"SDK + CLI commands" where more appropriate), ensuring the Table of Contents
entries and any section headings or descriptive sentences are updated to
maintain consistency.
| * Consistent SDK + CLI APIs | ||
|
|
||
| --- | ||
|
|
||
| ## Table of Contents | ||
|
|
||
| 1. [Problem Statement & Solution](#problem-statement--solution) | ||
| 2. [Architecture Overview](#architecture-overview) | ||
| 3. [Core Components](#core-components) | ||
| 4. [NeuroLink SDK Integration](#neurolink-sdk-integration) | ||
| 5. [CLI Integration](#cli-integration) | ||
| 6. [Output Model](#output-model-sttresult) | ||
| 7. [Configuration](#configuration) | ||
| 8. [Error Handling](#error-handling) | ||
| 9. [Extensibility Roadmap](#extensibility-roadmap) | ||
| 10. [Conclusion](#conclusion) | ||
|
|
||
| --- | ||
|
|
||
| ## Problem Statement & Solution | ||
|
|
||
| ### Problem Statement | ||
|
|
||
| NeuroLink lacked a **unified Speech-to-Text (STT) layer**, forcing developers to deal with provider-specific APIs, inconsistent outputs, and duplicated logic across SDKs and CLI tools. This made audio workflows harder to build, extend, and maintain. | ||
|
|
||
| ### Solution | ||
|
|
||
| NeuroLink adds a **provider-agnostic STT pipeline** that mirrors the TTS architecture: | ||
|
|
||
| * A single processor for validation, routing, and error handling | ||
| * Pluggable provider handlers for easy extensibility | ||
| * Normalized transcription results with optional metadata | ||
| * Consistent SDK and CLI interfaces |
There was a problem hiding this comment.
Prefer “SDK + CLI APIs” over “SDK + CLI interfaces”
“CLI” already expands to “command-line interface,” so “CLI interfaces” is redundant. Consider “SDK + CLI APIs” or “SDK + CLI commands.”
🧰 Tools
🪛 LanguageTool
[style] ~47-~47: This phrase is redundant (‘I’ stands for ‘interfaces’). Use simply “CLIs”.
Context: ... optional metadata * Consistent SDK and CLI interfaces This makes Speech-to-Text a **first-cl...
(ACRONYM_TAUTOLOGY)
🤖 Prompt for AI Agents
In `@memory-bank/neurolink-stt-support-plan.md` around lines 15 - 47, Replace the
redundant phrase "SDK + CLI interfaces" with "SDK + CLI APIs" throughout the
document; locate the exact string "SDK + CLI interfaces" (e.g., the top header
currently "Consistent SDK + CLI APIs" or any occurrences in the body/Table of
Contents) and change it to "SDK + CLI APIs" (or "SDK + CLI commands" where more
appropriate), ensuring the Table of Contents entries and any section headings or
descriptive sentences are updated to maintain consistency.
b5e220d to
2556695
Compare
|
@shambhavik-25 is attempting to deploy a commit to the Sachin Sharma's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@coderabbitai can you review my pr? compare it with other multimodality support implementation plan and find out if mine aligns with the established architecture. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain
🏁 Script executed: Length of output: 30751 🏁 Script executed: Length of output: 123359
🎯 Executive SummaryOverall Alignment: 85% ✅ Your STT plan follows the established architecture well, with some areas needing adjustments to match the proven patterns from TTS, Video Analysis, and the Multimodal Implementation Guide. 📊 Detailed Architectural Comparison1. Core Architecture Pattern ✅ EXCELLENT
Finding: Your framing of STT as an "input modality" perfectly mirrors how TTS is an "output modality" and aligns with CSV/PDF processing patterns. 2. Implementation Strategy
|
|
@coderabbitai based on the code base, check the implementation plan put forward by this pr. Identify gaps, if any. Suggest ways to fix the gaps. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain
🏁 Script executed: Length of output: 32342 🏁 Script executed: Length of output: 143777 🏁 Script executed: Length of output: 48693
🔍 STT Implementation Plan - Gap Analysis & Recommendations✅ Executive SummaryOverall Alignment: 65% (Down from initial assessment due to deeper codebase analysis) Your STT plan has good conceptual alignment but suffers from implementation gaps due to missing codebase-specific details. Below are 10 critical gaps with concrete fixes. 🚨 Critical Gaps (Must Fix Before Implementation)Gap 1: Wrong Directory Structure ❌Issue: // ❌ Your Plan (doesn't exist in codebase)
src/lib/stt/stt-service.ts
src/lib/stt/language-registry.ts
src/lib/stt/audio-validator.ts
src/lib/types/sttTypes.tsFix: // ✅ Follow Existing Patterns
src/lib/utils/sttProcessor.ts // Like ttsProcessor.ts
src/lib/adapters/stt/googleSTTHandler.ts // Like googleTTSHandler.ts
src/lib/types/sttTypes.ts // ✅ CorrectEvidence from Codebase:
Gap 2: Missing STT Type Definitions ❌Issue: Fix: 📝 Add to src/lib/types/sttTypes.ts/**
* STT Type Definitions (following ttsTypes.ts pattern)
*/
export type STTOptions = {
/** Language code (default: "en-US") */
language?: string;
/** STT model variant */
model?: "chirp" | "chirp_2" | "long" | "short";
/** Enable word-level timestamps */
enableWordTimestamps?: boolean;
/** Enable automatic punctuation (default: true) */
enableAutomaticPunctuation?: boolean;
/** Filter profanity */
profanityFilter?: boolean;
/** Max alternative transcriptions (default: 1) */
maxAlternatives?: number;
/** Audio sample rate override */
sampleRateHertz?: number;
};
export type STTResult = {
/** Transcribed text */
transcript: string;
/** Confidence score 0-1 */
confidence: number;
/** Alternative transcriptions */
alternatives?: Array<{ transcript: string; confidence: number }>;
/** Word-level timestamps */
wordTimestamps?: Array<{
word: string;
startTime: number;
endTime: number;
confidence: number;
}>;
/** Audio duration in seconds */
duration?: number;
/** Provider metadata */
metadata?: {
provider: string;
model: string;
language: string;
latency: number;
};
};
// Type guards
export function isValidSTTOptions(options: unknown): options is STTOptions {
// Similar to isValidTTSOptions in ttsTypes.ts
if (!options || typeof options !== "object") return false;
const opts = options as STTOptions;
if (opts.sampleRateHertz && (opts.sampleRateHertz < 8000 || opts.sampleRateHertz > 48000)) {
return false;
}
return true;
}Add to import type { STTOptions, STTResult } from "./sttTypes.js";
export type GenerateOptions = {
input: {
text: string;
images?: Array<Buffer | string | ImageWithAltText>;
csvFiles?: Array<Buffer | string>;
pdfFiles?: Array<Buffer | string>;
audioFiles?: Array<Buffer | string>; // ✅ ADD THIS
videoFiles?: Array<Buffer | string>;
files?: Array<Buffer | string>;
};
// ✅ ADD STT OPTIONS
sttOptions?: STTOptions;
// ... rest of options
};
export type GenerateResult = {
content: string;
audio?: TTSResult;
transcript?: STTResult; // ✅ ADD THIS for STT output
// ... rest of result
};Gap 3: Wrong CLI Integration Approach ❌Issue: Fix: 📝 Add to src/cli/factories/commandFactory.ts// Around line 1087 (after stream command)
{
command: "transcribe <audio-file>",
describe: "Transcribe audio to text using Speech-to-Text",
builder: (yargs) => {
return yargs
.positional("audio-file", {
describe: "Audio file path to transcribe",
type: "string",
})
.option("stt-language", {
describe: "Language code (e.g., en-US, hi-IN)",
type: "string",
default: "en-US",
})
.option("stt-model", {
describe: "STT model: chirp, chirp_2, chirp_3, long, short",
type: "string",
default: "chirp_2",
choices: ["chirp", "chirp_2", "chirp_3", "long", "short"],
})
.option("stt-timestamps", {
describe: "Enable word-level timestamps",
type: "boolean",
default: false,
})
.option("output", {
describe: "Output file for transcript",
type: "string",
alias: "o",
})
.option("provider", {
describe: "Provider (google-ai or vertex)",
type: "string",
default: "google-ai",
choices: ["google-ai", "vertex"],
});
},
handler: async (argv) => {
// Implementation similar to generate handler
const neurolink = new NeuroLink();
const result = await neurolink.generate({
input: {
text: "Transcribe this audio",
audioFiles: [argv["audio-file"]],
},
provider: argv.provider,
sttOptions: {
language: argv["stt-language"],
model: argv["stt-model"],
enableWordTimestamps: argv["stt-timestamps"],
},
});
if (argv.output) {
fs.writeFileSync(argv.output, result.transcript?.transcript || result.content);
} else {
console.log(result.transcript?.transcript || result.content);
}
},
}Gap 4: Missing audioFiles Processing in MessageBuilder ❌Issue: Fix: 📝 Add to src/lib/utils/messageBuilder.ts (after processExplicitPdfFiles)/**
* Process explicit audio files array and inject transcriptions into prompt
* Follows the same pattern as processExplicitCsvFiles
*/
async function processExplicitAudioFiles(
options: GenerateOptions,
provider: string,
): Promise<void> {
if (!options.input.audioFiles || options.input.audioFiles.length === 0) {
return;
}
logger.info(`[STT] Processing ${options.input.audioFiles.length} audio file(s)`);
options.input.text = options.input.text || "";
for (let i = 0; i < options.input.audioFiles.length; i++) {
const audioFile = options.input.audioFiles[i];
try {
// Use FileDetector to process audio file with STT
const result = await FileDetector.detectAndProcess(audioFile, {
allowedTypes: ["audio"],
sttOptions: options.sttOptions,
provider: provider,
});
if (result.type === "audio" && result.content) {
const filename = extractFilename(audioFile, i);
let audioSection = `\n\n## Transcription from "${filename}":\n`;
if (result.metadata) {
audioSection += formatAudioMetadata(result.metadata) + '\n\n';
}
audioSection += '```\n' + result.content + '\n```\n';
options.input.text += audioSection;
logger.info(`[STT] ✅ Transcribed: ${filename}`);
}
} catch (error) {
logger.error(`[STT] ❌ Failed:`, error);
const filename = extractFilename(audioFile, i);
options.input.text += `\n\n## Audio Transcription Error: Failed to process "${filename}"`;
options.input.text += `\nReason: ${error instanceof Error ? error.message : "Unknown error"}`;
}
}
}
// Helper function
function formatAudioMetadata(metadata: any): string {
return `Duration: ${metadata.duration || 'Unknown'} | Language: ${metadata.language || 'Unknown'}`;
}
// ✅ Call this in buildMessages function (around line 1305)
// After: await processExplicitCsvFiles(options);
await processExplicitAudioFiles(options, provider);Gap 5: Missing FileDetector Audio Routing ❌Issue: Fix: 📝 Update src/lib/utils/fileDetector.ts// Add to processAudioFile method (around line 800)
private static async processAudioFile(
content: Buffer,
detection: FileDetectionResult,
options?: { sttOptions?: STTOptions; provider?: string }
): Promise<FileProcessingResult> {
const audioFilename = detection.metadata.filename || "audio";
try {
// Process with AudioProcessor (includes STT if configured)
const audioResult = await audioProcessor.processFile(
{
id: audioFilename,
name: audioFilename,
mimetype: detection.mimeType || "audio/mpeg",
size: content.length,
buffer: content,
},
{
sttOptions: options?.sttOptions, // ✅ Pass STT options
provider: options?.provider, // ✅ Pass provider
}
);
if (audioResult.success && audioResult.data) {
// Return transcription as text content
return {
type: "audio",
content: audioResult.data.transcript || audioResult.data.textContent,
mimeType: detection.mimeType,
metadata: {
...detection.metadata,
hasTranscript: audioResult.data.hasTranscript,
transcriptionProvider: audioResult.data.transcriptionProvider,
duration: audioResult.data.metadata.duration,
},
};
}
} catch (error) {
logger.warn(`[FileDetector] AudioProcessor failed, using fallback`);
}
// Fallback to metadata-only placeholder
return {
type: "audio",
content: FileDetector.formatInformativePlaceholder("Audio", audioFilename, content, detection),
mimeType: detection.mimeType,
metadata: detection.metadata,
};
}Gap 6: AudioProcessor STT Integration Incomplete ❌Issue: Fix: 📝 Update src/lib/processors/media/AudioProcessor.ts// Update attemptTranscription method (around line 399)
private async attemptTranscription(
buffer: Buffer,
filename: string,
mimetype: string | undefined,
sttOptions?: STTOptions, // ✅ Add STT options parameter
provider?: string // ✅ Add provider parameter
): Promise<{
transcript: string | undefined;
hasTranscript: boolean;
transcriptionProvider: string | undefined;
wordTimestamps?: Array<{ word: string; startTime: number; endTime: number }>;
}> {
// ✅ Priority 1: Try Google Cloud STT v2 if credentials + sttOptions present
if (sttOptions && provider && this.hasGoogleCloudCredentials(provider)) {
try {
const STTProcessor = await import('../utils/sttProcessor.js');
const result = await STTProcessor.STTProcessor.synthesize(
buffer,
provider,
sttOptions
);
return {
transcript: result.transcript,
hasTranscript: true,
transcriptionProvider: 'google-cloud-stt-v2',
wordTimestamps: result.wordTimestamps,
};
} catch (error) {
logger.warn('[AudioProcessor] Google Cloud STT failed, falling back to Whisper', error);
}
}
// ✅ Priority 2: Fallback to OpenAI Whisper (existing implementation)
return this.attemptWhisperTranscription(buffer, filename, mimetype);
}
// ✅ Add credential check method
private hasGoogleCloudCredentials(provider: string): boolean {
if (provider === 'google-ai') {
return !!process.env.GOOGLE_AI_API_KEY;
}
if (provider === 'vertex') {
return !!process.env.GOOGLE_APPLICATION_CREDENTIALS;
}
return false;
}
// ✅ Update processFile to accept sttOptions (around line 237)
override async processFile(
fileInfo: FileInfo,
options?: ProcessOptions & { sttOptions?: STTOptions; provider?: string }
): Promise<FileProcessingResult<ProcessedAudio>> {
// ... existing validation code ...
// Step 7: Attempt transcription (pass STT options)
const transcriptionResult = await this.attemptTranscription(
buffer,
filename,
fileInfo.mimetype,
options?.sttOptions, // ✅ Pass through
options?.provider // ✅ Pass through
);
// ... rest of method ...
}Gap 7: Missing STTProcessor + STTHandler Pattern ❌Issue: Fix: 📝 Create src/lib/utils/sttProcessor.ts (following ttsProcessor.ts)/**
* Speech-to-Text (STT) Processing Utility
* Central orchestrator for all STT operations across providers
* Follows the same pattern as TTSProcessor
*/
import { logger } from "./logger.js";
import type { STTOptions, STTResult } from "../types/sttTypes.js";
import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
import { NeuroLinkError } from "./errorHandling.js";
export const STT_ERROR_CODES = {
EMPTY_AUDIO: "STT_EMPTY_AUDIO",
AUDIO_TOO_LARGE: "STT_AUDIO_TOO_LARGE",
PROVIDER_NOT_SUPPORTED: "STT_PROVIDER_NOT_SUPPORTED",
PROVIDER_NOT_CONFIGURED: "STT_PROVIDER_NOT_CONFIGURED",
TRANSCRIPTION_FAILED: "STT_TRANSCRIPTION_FAILED",
INVALID_FORMAT: "STT_INVALID_FORMAT",
} as const;
export class STTError extends NeuroLinkError {
constructor(options: {
code: string;
message: string;
category?: ErrorCategory;
severity?: ErrorSeverity;
retriable?: boolean;
context?: Record<string, unknown>;
originalError?: Error;
}) {
super({
code: options.code,
message: options.message,
category: options.category ?? ErrorCategory.VALIDATION,
severity: options.severity ?? ErrorSeverity.MEDIUM,
retriable: options.retriable ?? false,
context: options.context,
originalError: options.originalError,
});
this.name = "STTError";
}
}
/**
* STT Handler interface (mirrors TTSHandler)
*/
export type STTHandler = {
transcribe(audioBuffer: Buffer, options: STTOptions): Promise<STTResult>;
isConfigured(): boolean;
maxAudioSizeMB?: number;
};
export class STTProcessor {
private static readonly handlers = new Map<string, STTHandler>();
private static readonly DEFAULT_MAX_AUDIO_SIZE_MB = 10;
static registerHandler(providerName: string, handler: STTHandler): void {
if (!providerName || !handler) {
throw new Error("Provider name and handler are required");
}
const normalizedName = providerName.toLowerCase();
this.handlers.set(normalizedName, handler);
logger.debug(`[STTProcessor] Registered STT handler for provider: ${normalizedName}`);
}
static supports(providerName: string): boolean {
return this.handlers.has(providerName.toLowerCase());
}
static async transcribe(
audioBuffer: Buffer,
provider: string,
options: STTOptions,
): Promise<STTResult> {
// Validation
if (!audioBuffer || audioBuffer.length === 0) {
throw new STTError({
code: STT_ERROR_CODES.EMPTY_AUDIO,
message: "Audio buffer is required for STT transcription",
severity: ErrorSeverity.LOW,
});
}
const handler = this.handlers.get(provider.toLowerCase());
if (!handler) {
throw new STTError({
code: STT_ERROR_CODES.PROVIDER_NOT_SUPPORTED,
message: `STT provider "${provider}" is not supported`,
context: { availableProviders: Array.from(this.handlers.keys()) },
});
}
const maxSizeMB = handler.maxAudioSizeMB ?? this.DEFAULT_MAX_AUDIO_SIZE_MB;
const sizeMB = audioBuffer.length / (1024 * 1024);
if (sizeMB > maxSizeMB) {
throw new STTError({
code: STT_ERROR_CODES.AUDIO_TOO_LARGE,
message: `Audio size ${sizeMB.toFixed(2)}MB exceeds ${maxSizeMB}MB limit`,
context: { sizeMB, maxSizeMB },
});
}
if (!handler.isConfigured()) {
throw new STTError({
code: STT_ERROR_CODES.PROVIDER_NOT_CONFIGURED,
message: `STT provider "${provider}" is not configured`,
category: ErrorCategory.CONFIGURATION,
});
}
try {
logger.debug(`[STTProcessor] Starting transcription with provider: ${provider}`);
const result = await handler.transcribe(audioBuffer, options);
logger.info(`[STTProcessor] Transcription successful: ${result.transcript.length} chars`);
return result;
} catch (err) {
if (err instanceof STTError) throw err;
const errorMessage = err instanceof Error ? err.message : String(err);
throw new STTError({
code: STT_ERROR_CODES.TRANSCRIPTION_FAILED,
message: `STT transcription failed: ${errorMessage}`,
category: ErrorCategory.EXECUTION,
retriable: true,
originalError: err instanceof Error ? err : undefined,
});
}
}
}📝 Create src/lib/adapters/stt/googleSTTHandler.ts/**
* Google Cloud Speech-to-Text v2 Handler
* Implements STTHandler interface for Google Cloud STT
*/
import type { STTHandler, STTOptions, STTResult } from "../../types/sttTypes.js";
import { logger } from "../../utils/logger.js";
export class GoogleSTTHandler implements STTHandler {
maxAudioSizeMB = 10;
async transcribe(audioBuffer: Buffer, options: STTOptions): Promise<STTResult> {
const { SpeechClient } = await import("@google-cloud/speech");
const client = new SpeechClient({
apiKey: process.env.GOOGLE_AI_API_KEY,
});
const audio = { content: audioBuffer.toString("base64") };
const config = {
encoding: "LINEAR16" as const,
sampleRateHertz: options.sampleRateHertz || 16000,
languageCode: options.language || "en-US",
model: options.model || "chirp_2",
enableWordTimeOffsets: options.enableWordTimestamps || false,
enableAutomaticPunctuation: options.enableAutomaticPunctuation !== false,
};
const request = { audio, config };
logger.debug("[GoogleSTTHandler] Sending transcription request");
const [response] = await client.recognize(request);
const transcription = response.results
?.map((result) => result.alternatives?.[0]?.transcript)
.join("\n") || "";
const wordTimestamps = options.enableWordTimestamps
? response.results?.flatMap((result) =>
result.alternatives?.[0]?.words?.map((word) => ({
word: word.word || "",
startTime: Number(word.startTime?.seconds || 0),
endTime: Number(word.endTime?.seconds || 0),
confidence: word.confidence || 0,
})) || []
)
: undefined;
return {
transcript: transcription,
confidence: response.results?.[0]?.alternatives?.[0]?.confidence || 0,
wordTimestamps,
metadata: {
provider: "google-cloud-stt-v2",
model: options.model || "chirp_2",
language: options.language || "en-US",
latency: 0, // Calculate if needed
},
};
}
isConfigured(): boolean {
return !!(
process.env.GOOGLE_AI_API_KEY ||
process.env.GOOGLE_APPLICATION_CREDENTIALS
);
}
}
// Auto-register handler
import { STTProcessor } from "../../utils/sttProcessor.js";
STTProcessor.registerHandler("google-ai", new GoogleSTTHandler());
STTProcessor.registerHandler("vertex", new GoogleSTTHandler());Gap 8: Missing Dependencies ❌Issue: Fix: {
"dependencies": {
"@google-cloud/speech": "^7.0.0"
}
}Gap 9: Wrong Phase Ordering
|
0eafcc8 to
e9e290a
Compare
|
4b2e1bd to
dec89f5
Compare
dec89f5 to
1fe5adc
Compare
09ff4a5 to
405e3e5
Compare
|
Closing — STT plan doc superseded by PR #846 (actual implementation attempt). Project audit (2026-03-29). |
docs(stt): Add implementation plan of speech-to-text support in neurolink
Pull Request
Description
What does this PR do?
Adds comprehensive documentation describing the Speech-to-Text (STT) implementation plan for NeuroLink.
The document outlines the architecture, provider abstractions, SDK and CLI integration, configuration, error handling, and extensibility roadmap for STT support, with initial focus on Google Cloud Speech-to-Text.
Related Issues
Does this PR close any issues?
N/A
Type of Change
Please select the type of change:
Motivation and Context
Why is this change needed? What problem does it solve?
NeuroLink previously lacked formal documentation describing how Speech-to-Text support is designed and integrated. This made it harder for contributors and reviewers to:
This documentation provides a clear, shared reference for the STT pipeline before and during implementation.
Changes Made
Breaking Changes
Does this PR introduce breaking changes?
Testing
How has this been tested?
Please describe the tests you ran and their results:
Code Quality
Have you followed code quality standards?
Documentation
Have you updated documentation?
Commit Message Format
Does your commit follow semantic commit conventions?
type(scope): descriptionExample:
feat(providers): add support for LiteLLM proxyDependencies
Does this PR add, update, or remove dependencies?
If yes, list dependencies and justification:
Performance Impact
Does this change affect performance?
Security Considerations
Are there any security implications?
Deployment Notes
Special deployment instructions?
Reviewer Checklist
For reviewers:
Thank you for contributing to NeuroLink!
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.