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.
Files changed (127) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/connection-runtime/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/contracts/package.json +1 -1
  24. package/packages/cookie-sanitizer/package.json +1 -1
  25. package/packages/cost-optimizer/package.json +1 -1
  26. package/packages/dependency-analyzer/package.json +1 -1
  27. package/packages/django-plugin/package.json +1 -1
  28. package/packages/doc-generator/package.json +1 -1
  29. package/packages/durable-review/package.json +1 -1
  30. package/packages/e2e-tester/package.json +1 -1
  31. package/packages/evaluation-engine/package.json +1 -1
  32. package/packages/evaluation-runner/package.json +1 -1
  33. package/packages/event-bus/package.json +1 -1
  34. package/packages/event-sink/package.json +1 -1
  35. package/packages/event-store/package.json +1 -1
  36. package/packages/experience-replay/package.json +1 -1
  37. package/packages/feedback-collector/package.json +1 -1
  38. package/packages/file-copy-helper/package.json +1 -1
  39. package/packages/framework-detector/package.json +1 -1
  40. package/packages/framework-plugin-sdk/package.json +1 -1
  41. package/packages/frappe-plugin/package.json +1 -1
  42. package/packages/generic-web-plugin/package.json +1 -1
  43. package/packages/health-monitor/package.json +1 -1
  44. package/packages/intent-analyzer/package.json +1 -1
  45. package/packages/jev-judge/package.json +1 -1
  46. package/packages/knowledge-graph/package.json +1 -1
  47. package/packages/knowledge-retrieval/package.json +1 -1
  48. package/packages/laravel-plugin/package.json +1 -1
  49. package/packages/learning-engine/package.json +1 -1
  50. package/packages/mcp-adapter/package.json +1 -1
  51. package/packages/memory-engine/package.json +1 -1
  52. package/packages/milestone-manager/package.json +1 -1
  53. package/packages/model-registry/package.json +1 -1
  54. package/packages/nextjs-plugin/package.json +1 -1
  55. package/packages/notification/package.json +1 -1
  56. package/packages/observability/package.json +1 -1
  57. package/packages/okf-indexer/package.json +1 -1
  58. package/packages/paperclip-integration/package.json +1 -1
  59. package/packages/performance-optimizer/package.json +1 -1
  60. package/packages/permission-mode/package.json +1 -1
  61. package/packages/privilege-broker/package.json +1 -1
  62. package/packages/project-analyzer/package.json +1 -1
  63. package/packages/project-bootstrap/package.json +1 -1
  64. package/packages/projection-engine/package.json +1 -1
  65. package/packages/prompt-compiler/package.json +1 -1
  66. package/packages/prompt-versioning/package.json +1 -1
  67. package/packages/provider-adapter-sdk/package.json +1 -1
  68. package/packages/provider-router/package.json +1 -1
  69. package/packages/provider-selector/package.json +1 -1
  70. package/packages/providers/package.json +1 -1
  71. package/packages/qdrant-skills/package.json +1 -1
  72. package/packages/quota-manager/package.json +1 -1
  73. package/packages/rate-limiter/package.json +1 -1
  74. package/packages/react-vite-plugin/package.json +1 -1
  75. package/packages/relay/package.json +1 -1
  76. package/packages/release-manager/package.json +1 -1
  77. package/packages/requirement-compiler/package.json +1 -1
  78. package/packages/runtime/package.json +1 -1
  79. package/packages/scheduler/package.json +1 -1
  80. package/packages/scheduler-adapter/package.json +1 -1
  81. package/packages/session/package.json +1 -1
  82. package/packages/session-api/package.json +1 -1
  83. package/packages/session-export/package.json +1 -1
  84. package/packages/shared-context/package.json +1 -1
  85. package/packages/skill-mcp-client/package.json +1 -1
  86. package/packages/skill-registry/package.json +1 -1
  87. package/packages/skills/package.json +1 -1
  88. package/packages/sprint-planner/package.json +1 -1
  89. package/packages/subscription-engine/package.json +1 -1
  90. package/packages/task-compiler/package.json +1 -1
  91. package/packages/telegram/package.json +1 -1
  92. package/packages/tencentdb-memory/package.json +1 -1
  93. package/packages/tencentdb-sync/package.json +1 -1
  94. package/packages/test-data-generator/package.json +1 -1
  95. package/packages/test-generator/package.json +1 -1
  96. package/packages/todo-bd-sync/package.json +1 -1
  97. package/packages/token-estimation/package.json +1 -1
  98. package/packages/token-optimizer/package.json +1 -1
  99. package/packages/trajectory/package.json +1 -1
  100. package/packages/tui/package.json +1 -1
  101. package/packages/types/package.json +1 -1
  102. package/packages/voice-api/dist/index.d.ts +14 -0
  103. package/packages/voice-api/dist/index.js +12 -0
  104. package/packages/voice-api/dist/line.d.ts +100 -0
  105. package/packages/voice-api/dist/line.js +147 -0
  106. package/packages/voice-api/dist/provider.d.ts +23 -0
  107. package/packages/voice-api/dist/provider.js +154 -0
  108. package/packages/voice-api/dist/telegram.d.ts +74 -0
  109. package/packages/voice-api/dist/telegram.js +119 -0
  110. package/packages/voice-api/dist/transcriber.d.ts +48 -0
  111. package/packages/voice-api/dist/transcriber.js +118 -0
  112. package/packages/voice-api/dist/types.d.ts +75 -0
  113. package/packages/voice-api/dist/types.js +4 -0
  114. package/packages/voice-api/package.json +24 -0
  115. package/packages/voice-api/src/index.ts +15 -0
  116. package/packages/voice-api/src/line.ts +229 -0
  117. package/packages/voice-api/src/provider.ts +186 -0
  118. package/packages/voice-api/src/telegram.ts +191 -0
  119. package/packages/voice-api/src/transcriber.ts +136 -0
  120. package/packages/voice-api/src/types.ts +79 -0
  121. package/packages/webmcp-tester/package.json +1 -1
  122. package/packages/wiki/package.json +1 -1
  123. package/packages/workflow-events/package.json +1 -1
  124. package/packages/workspace-scanner/package.json +1 -1
  125. package/packages/worktree/package.json +1 -1
  126. package/packages/write-review/package.json +1 -1
  127. 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
+ }