pi-harness-runtime 1.1.337 → 1.1.339
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/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/dist/index.d.ts +14 -0
- package/packages/voice-api/dist/index.js +12 -0
- package/packages/voice-api/dist/line.d.ts +100 -0
- package/packages/voice-api/dist/line.js +147 -0
- package/packages/voice-api/dist/provider.d.ts +23 -0
- package/packages/voice-api/dist/provider.js +154 -0
- package/packages/voice-api/dist/telegram.d.ts +74 -0
- package/packages/voice-api/dist/telegram.js +119 -0
- package/packages/voice-api/dist/transcriber.d.ts +48 -0
- package/packages/voice-api/dist/transcriber.js +118 -0
- package/packages/voice-api/dist/types.d.ts +75 -0
- package/packages/voice-api/dist/types.js +4 -0
- package/packages/voice-api/package.json +24 -0
- package/packages/voice-api/src/index.ts +15 -0
- package/packages/voice-api/src/line.ts +229 -0
- package/packages/voice-api/src/provider.ts +186 -0
- package/packages/voice-api/src/telegram.ts +191 -0
- package/packages/voice-api/src/transcriber.ts +136 -0
- package/packages/voice-api/src/types.ts +79 -0
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/voice-api-example.ts +112 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINE Voice Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles voice messages from LINE Messaging API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { VoiceMessage, TranscriptionResult } from './types.js';
|
|
8
|
+
import { VoiceTranscriber } from './transcriber.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* LINE webhook event
|
|
12
|
+
*/
|
|
13
|
+
export interface LineWebhookEvent {
|
|
14
|
+
type: string;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
source: {
|
|
17
|
+
type: string;
|
|
18
|
+
userId?: string;
|
|
19
|
+
groupId?: string;
|
|
20
|
+
roomId?: string;
|
|
21
|
+
};
|
|
22
|
+
replyToken?: string;
|
|
23
|
+
message?: {
|
|
24
|
+
id: string;
|
|
25
|
+
type: string;
|
|
26
|
+
duration?: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* LINE webhook body
|
|
32
|
+
*/
|
|
33
|
+
export interface LineWebhookBody {
|
|
34
|
+
destination: string;
|
|
35
|
+
events: LineWebhookEvent[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* LINE Messaging API response
|
|
40
|
+
*/
|
|
41
|
+
interface LineMessageContent {
|
|
42
|
+
displayName?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* LINE Voice Handler options
|
|
47
|
+
*/
|
|
48
|
+
export interface LineVoiceHandlerOptions {
|
|
49
|
+
/** LINE channel access token */
|
|
50
|
+
channelAccessToken: string;
|
|
51
|
+
/** LINE user ID secret (for validation) */
|
|
52
|
+
channelSecret?: string;
|
|
53
|
+
/** Voice transcriber instance */
|
|
54
|
+
transcriber: VoiceTranscriber;
|
|
55
|
+
/** Base URL for LINE API */
|
|
56
|
+
apiBase?: string;
|
|
57
|
+
/** Callback when voice is transcribed */
|
|
58
|
+
onTranscribed?: (userId: string, text: string, result: TranscriptionResult) => Promise<void>;
|
|
59
|
+
/** Callback on error */
|
|
60
|
+
onError?: (userId: string, error: Error) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* LINE Voice Handler
|
|
65
|
+
*
|
|
66
|
+
* Processes voice messages from LINE Messaging API and transcribes them.
|
|
67
|
+
*/
|
|
68
|
+
export class LineVoiceHandler {
|
|
69
|
+
private channelAccessToken: string;
|
|
70
|
+
private channelSecret?: string;
|
|
71
|
+
private transcriber: VoiceTranscriber;
|
|
72
|
+
private apiBase: string;
|
|
73
|
+
private onTranscribed?: (userId: string, text: string, result: TranscriptionResult) => Promise<void>;
|
|
74
|
+
private onError?: (userId: string, error: Error) => void;
|
|
75
|
+
|
|
76
|
+
constructor(options: LineVoiceHandlerOptions) {
|
|
77
|
+
this.channelAccessToken = options.channelAccessToken;
|
|
78
|
+
this.channelSecret = options.channelSecret;
|
|
79
|
+
this.transcriber = options.transcriber;
|
|
80
|
+
this.apiBase = options.apiBase || 'https://api.line.me';
|
|
81
|
+
this.onTranscribed = options.onTranscribed;
|
|
82
|
+
this.onError = options.onError;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verify LINE webhook signature
|
|
87
|
+
*/
|
|
88
|
+
verifySignature(body: string, signature: string): boolean {
|
|
89
|
+
if (!this.channelSecret) return true; // Skip verification if no secret
|
|
90
|
+
|
|
91
|
+
// Note: In production, implement proper HMAC verification
|
|
92
|
+
// This is a simplified version
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Handle incoming webhook from LINE
|
|
98
|
+
*/
|
|
99
|
+
async handleWebhook(body: LineWebhookBody): Promise<{ success: boolean; responses: string[] }> {
|
|
100
|
+
const responses: string[] = [];
|
|
101
|
+
|
|
102
|
+
for (const event of body.events) {
|
|
103
|
+
if (event.type !== 'message' || event.message?.type !== 'audio') {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const userId = event.source.userId || 'unknown';
|
|
108
|
+
const messageId = event.message.id;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Get audio content from LINE
|
|
112
|
+
const audioContent = await this.getAudioContent(messageId);
|
|
113
|
+
if (!audioContent) {
|
|
114
|
+
responses.push(`Failed to get audio for message ${messageId}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Transcribe
|
|
119
|
+
const result = await this.transcriber.transcribe({
|
|
120
|
+
audioBuffer: audioContent,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (result.error) {
|
|
124
|
+
this.onError?.(userId, new Error(result.error));
|
|
125
|
+
responses.push(`Transcription error: ${result.error}`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Notify callback
|
|
130
|
+
await this.onTranscribed?.(userId, result.text, result);
|
|
131
|
+
responses.push(`Transcribed: ${result.text}`);
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
135
|
+
this.onError?.(userId, err);
|
|
136
|
+
responses.push(`Error: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { success: true, responses };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get audio content from LINE
|
|
145
|
+
*/
|
|
146
|
+
private async getAudioContent(messageId: string): Promise<Uint8Array | null> {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch(
|
|
149
|
+
`${this.apiBase}/v2/bot/message/${messageId}/content`,
|
|
150
|
+
{
|
|
151
|
+
headers: {
|
|
152
|
+
'Authorization': `Bearer ${this.channelAccessToken}`,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
162
|
+
return new Uint8Array(arrayBuffer);
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Reply to user with text
|
|
170
|
+
*/
|
|
171
|
+
async reply(replyToken: string, messages: Array<{ type: string; text?: string }>): Promise<boolean> {
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(
|
|
174
|
+
`${this.apiBase}/v2/bot/message/reply`,
|
|
175
|
+
{
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: {
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
'Authorization': `Bearer ${this.channelAccessToken}`,
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify({ replyToken, messages }),
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return response.ok;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Push message to user
|
|
193
|
+
*/
|
|
194
|
+
async push(userId: string, messages: Array<{ type: string; text?: string }>): Promise<boolean> {
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(
|
|
197
|
+
`${this.apiBase}/v2/bot/message/push`,
|
|
198
|
+
{
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: {
|
|
201
|
+
'Content-Type': 'application/json',
|
|
202
|
+
'Authorization': `Bearer ${this.channelAccessToken}`,
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify({ to: userId, messages }),
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return response.ok;
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create webhook handler for Express/Fastify/etc.
|
|
216
|
+
*/
|
|
217
|
+
createWebhookHandler() {
|
|
218
|
+
return async (body: LineWebhookBody, headers: Record<string, string>): Promise<{ status: number; body: object }> => {
|
|
219
|
+
// Verify signature if provided
|
|
220
|
+
const signature = headers['x-line-signature'];
|
|
221
|
+
if (signature && !this.verifySignature(JSON.stringify(body), signature)) {
|
|
222
|
+
return { status: 401, body: { error: 'Invalid signature' } };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = await this.handleWebhook(body);
|
|
226
|
+
return { status: 200, body: { success: result.success, responses: result.responses } };
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whisper Provider - Abstract interface for speech-to-text
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TranscriptionResult, TranscribeOptions } from './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whisper provider interface
|
|
9
|
+
*/
|
|
10
|
+
export interface WhisperProvider {
|
|
11
|
+
/** Provider name */
|
|
12
|
+
name: 'openai' | 'replicate';
|
|
13
|
+
/** Transcribe audio to text */
|
|
14
|
+
transcribe(options: TranscribeOptions): Promise<TranscriptionResult>;
|
|
15
|
+
/** Check if provider is configured */
|
|
16
|
+
isConfigured(): boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create OpenAI Whisper provider
|
|
21
|
+
*/
|
|
22
|
+
export function createOpenAIProvider(apiKey: string): WhisperProvider {
|
|
23
|
+
return new OpenAIProvider(apiKey);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create Replicate Whisper provider
|
|
28
|
+
*/
|
|
29
|
+
export function createReplicateProvider(apiToken: string): WhisperProvider {
|
|
30
|
+
return new ReplicateProvider(apiToken);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* OpenAI Whisper Provider
|
|
35
|
+
*/
|
|
36
|
+
class OpenAIProvider implements WhisperProvider {
|
|
37
|
+
name = 'openai' as const;
|
|
38
|
+
private apiKey: string;
|
|
39
|
+
|
|
40
|
+
constructor(apiKey: string) {
|
|
41
|
+
this.apiKey = apiKey;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
isConfigured(): boolean {
|
|
45
|
+
return Boolean(this.apiKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async transcribe(options: TranscribeOptions): Promise<TranscriptionResult> {
|
|
49
|
+
const startTime = Date.now();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Dynamic import to avoid hard dependency
|
|
53
|
+
const OpenAI = (await import('openai')).default;
|
|
54
|
+
const openai = new OpenAI({ apiKey: this.apiKey });
|
|
55
|
+
|
|
56
|
+
let audioData: Buffer | ReadableStream | string;
|
|
57
|
+
|
|
58
|
+
if (options.audioBuffer) {
|
|
59
|
+
audioData = Buffer.from(options.audioBuffer);
|
|
60
|
+
} else if (options.audioUrl) {
|
|
61
|
+
// Fetch audio from URL
|
|
62
|
+
const response = await fetch(options.audioUrl);
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`Failed to fetch audio: ${response.status}`);
|
|
65
|
+
}
|
|
66
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
67
|
+
audioData = Buffer.from(arrayBuffer);
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error('No audio data provided');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result = await openai.audio.transcriptions.create({
|
|
73
|
+
file: audioData as unknown as File,
|
|
74
|
+
model: options.model || 'whisper-1',
|
|
75
|
+
language: options.language,
|
|
76
|
+
response_format: 'verbose_json',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
text: result.text,
|
|
81
|
+
language: result.language || options.language,
|
|
82
|
+
duration: result.duration,
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
provider: 'openai',
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
text: '',
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
provider: 'openai',
|
|
91
|
+
error: error instanceof Error ? error.message : String(error),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Replicate Whisper Provider
|
|
99
|
+
*/
|
|
100
|
+
class ReplicateProvider implements WhisperProvider {
|
|
101
|
+
name = 'replicate' as const;
|
|
102
|
+
private apiToken: string;
|
|
103
|
+
|
|
104
|
+
constructor(apiToken: string) {
|
|
105
|
+
this.apiToken = apiToken;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
isConfigured(): boolean {
|
|
109
|
+
return Boolean(this.apiToken);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async transcribe(options: TranscribeOptions): Promise<TranscriptionResult> {
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Get audio data
|
|
117
|
+
let audioData: Uint8Array;
|
|
118
|
+
|
|
119
|
+
if (options.audioBuffer) {
|
|
120
|
+
audioData = options.audioBuffer;
|
|
121
|
+
} else if (options.audioUrl) {
|
|
122
|
+
const response = await fetch(options.audioUrl);
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`Failed to fetch audio: ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
127
|
+
audioData = new Uint8Array(arrayBuffer);
|
|
128
|
+
} else {
|
|
129
|
+
throw new Error('No audio data provided');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Convert to base64 for Replicate
|
|
133
|
+
const base64 = btoa(String.fromCharCode(...audioData));
|
|
134
|
+
const dataUrl = `data:audio/wav;base64,${base64}`;
|
|
135
|
+
|
|
136
|
+
// Call Replicate API
|
|
137
|
+
const response = await fetch('https://api.replicate.com/v1/predictions', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
'Authorization': `Token ${this.apiToken}`,
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
version: 'e39e7d2f3c2b7a7d8c9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b',
|
|
145
|
+
input: {
|
|
146
|
+
audio: dataUrl,
|
|
147
|
+
model: options.model || 'large-v3',
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`Replicate API error: ${response.status}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const prediction = await response.json();
|
|
157
|
+
|
|
158
|
+
// Poll for completion
|
|
159
|
+
let result = prediction;
|
|
160
|
+
while (result.status !== 'succeeded' && result.status !== 'failed') {
|
|
161
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
162
|
+
const pollResponse = await fetch(`https://api.replicate.com/v1/predictions/${result.id}`, {
|
|
163
|
+
headers: { 'Authorization': `Token ${this.apiToken}` },
|
|
164
|
+
});
|
|
165
|
+
result = await pollResponse.json();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (result.status === 'failed') {
|
|
169
|
+
throw new Error(result.error || 'Transcription failed');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
text: result.output,
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
provider: 'replicate',
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
text: '',
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
provider: 'replicate',
|
|
182
|
+
error: error instanceof Error ? error.message : String(error),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Voice Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles voice messages from Telegram bots.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { VoiceMessage, TranscriptionResult } from './types.js';
|
|
8
|
+
import { VoiceTranscriber } from './transcriber.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Telegram voice message from bot update
|
|
12
|
+
*/
|
|
13
|
+
export interface TelegramVoiceUpdate {
|
|
14
|
+
update_id: number;
|
|
15
|
+
message?: {
|
|
16
|
+
message_id: number;
|
|
17
|
+
voice?: {
|
|
18
|
+
file_id: string;
|
|
19
|
+
duration: number;
|
|
20
|
+
mime_type: string;
|
|
21
|
+
file_size?: number;
|
|
22
|
+
};
|
|
23
|
+
chat: {
|
|
24
|
+
id: number;
|
|
25
|
+
type: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Telegram API response
|
|
32
|
+
*/
|
|
33
|
+
interface TelegramFileResponse {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
result?: {
|
|
36
|
+
file_id: string;
|
|
37
|
+
file_unique_id: string;
|
|
38
|
+
file_path: string;
|
|
39
|
+
};
|
|
40
|
+
description?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Telegram Voice Handler options
|
|
45
|
+
*/
|
|
46
|
+
export interface TelegramVoiceHandlerOptions {
|
|
47
|
+
/** Telegram bot token */
|
|
48
|
+
botToken: string;
|
|
49
|
+
/** Voice transcriber instance */
|
|
50
|
+
transcriber: VoiceTranscriber;
|
|
51
|
+
/** Base URL for Telegram API */
|
|
52
|
+
apiBase?: string;
|
|
53
|
+
/** Callback when voice is transcribed */
|
|
54
|
+
onTranscribed?: (chatId: string, text: string, result: TranscriptionResult) => Promise<void>;
|
|
55
|
+
/** Callback on error */
|
|
56
|
+
onError?: (chatId: string, error: Error) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Telegram Voice Handler
|
|
61
|
+
*
|
|
62
|
+
* Processes voice messages from Telegram and transcribes them.
|
|
63
|
+
*/
|
|
64
|
+
export class TelegramVoiceHandler {
|
|
65
|
+
private botToken: string;
|
|
66
|
+
private transcriber: VoiceTranscriber;
|
|
67
|
+
private apiBase: string;
|
|
68
|
+
private onTranscribed?: (chatId: string, text: string, result: TranscriptionResult) => Promise<void>;
|
|
69
|
+
private onError?: (chatId: string, error: Error) => void;
|
|
70
|
+
|
|
71
|
+
constructor(options: TelegramVoiceHandlerOptions) {
|
|
72
|
+
this.botToken = options.botToken;
|
|
73
|
+
this.transcriber = options.transcriber;
|
|
74
|
+
this.apiBase = options.apiBase || 'https://api.telegram.org';
|
|
75
|
+
this.onTranscribed = options.onTranscribed;
|
|
76
|
+
this.onError = options.onError;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle incoming update from Telegram webhook
|
|
81
|
+
*/
|
|
82
|
+
async handleUpdate(update: TelegramVoiceUpdate): Promise<{ success: boolean; text?: string; error?: string }> {
|
|
83
|
+
const message = update.message;
|
|
84
|
+
const voice = message?.voice;
|
|
85
|
+
|
|
86
|
+
if (!message || !voice) {
|
|
87
|
+
return { success: false, error: 'No voice message in update' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const chatId = String(message.chat.id);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Get file path from Telegram
|
|
94
|
+
const filePath = await this.getFilePath(voice.file_id);
|
|
95
|
+
if (!filePath) {
|
|
96
|
+
return { success: false, error: 'Failed to get file path' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Construct download URL
|
|
100
|
+
const fileUrl = `${this.apiBase}/file/bot${this.botToken}/${filePath}`;
|
|
101
|
+
|
|
102
|
+
// Transcribe
|
|
103
|
+
const result = await this.transcriber.transcribe({ audioUrl: fileUrl });
|
|
104
|
+
|
|
105
|
+
if (result.error) {
|
|
106
|
+
this.onError?.(chatId, new Error(result.error));
|
|
107
|
+
return { success: false, error: result.error };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Notify callback
|
|
111
|
+
await this.onTranscribed?.(chatId, result.text, result);
|
|
112
|
+
|
|
113
|
+
return { success: true, text: result.text };
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
116
|
+
this.onError?.(chatId, err);
|
|
117
|
+
return { success: false, error: err.message };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get file path from Telegram
|
|
123
|
+
*/
|
|
124
|
+
private async getFilePath(fileId: string): Promise<string | null> {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetch(
|
|
127
|
+
`${this.apiBase}/bot${this.botToken}/getFile?file_id=${fileId}`
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const data = await response.json() as TelegramFileResponse;
|
|
135
|
+
return data.result?.file_path || null;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Send text response to chat
|
|
143
|
+
*/
|
|
144
|
+
async sendMessage(chatId: string, text: string): Promise<boolean> {
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(
|
|
147
|
+
`${this.apiBase}/bot${this.botToken}/sendMessage`,
|
|
148
|
+
{
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
chat_id: chatId,
|
|
153
|
+
text,
|
|
154
|
+
parse_mode: 'Markdown',
|
|
155
|
+
}),
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return response.ok;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create webhook handler middleware
|
|
167
|
+
*/
|
|
168
|
+
createWebhookHandler() {
|
|
169
|
+
return async (request: Request): Promise<Response> => {
|
|
170
|
+
try {
|
|
171
|
+
const update = await request.json() as TelegramVoiceUpdate;
|
|
172
|
+
const result = await this.handleUpdate(update);
|
|
173
|
+
|
|
174
|
+
if (result.success && result.text) {
|
|
175
|
+
// Optionally echo back the transcription
|
|
176
|
+
// await this.sendMessage(String(update.message?.chat.id), `🎤: ${result.text}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
180
|
+
status: 200,
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return new Response(JSON.stringify({ ok: false, error: String(error) }), {
|
|
185
|
+
status: 500,
|
|
186
|
+
headers: { 'Content-Type': 'application/json' },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|