neoagent 2.3.1-beta.87 → 2.3.1-beta.89

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.
@@ -490,7 +490,9 @@ class BrowserController {
490
490
  url: currentUrl,
491
491
  status: response?.status() || 0,
492
492
  bodyText,
493
- screenshotPath: screenshot?.screenshotPath || null
493
+ screenshotPath: screenshot?.screenshotPath || null,
494
+ artifactId: screenshot?.artifactId || null,
495
+ fullPath: screenshot?.fullPath || null
494
496
  };
495
497
  } catch (err) {
496
498
  let screenshot = null;
@@ -498,7 +500,9 @@ class BrowserController {
498
500
  return {
499
501
  error: err.message,
500
502
  url,
501
- screenshotPath: screenshot?.screenshotPath || null
503
+ screenshotPath: screenshot?.screenshotPath || null,
504
+ artifactId: screenshot?.artifactId || null,
505
+ fullPath: screenshot?.fullPath || null
502
506
  };
503
507
  }
504
508
  }
@@ -540,7 +544,9 @@ class BrowserController {
540
544
  success: true,
541
545
  url: page.url(),
542
546
  title: await page.title(),
543
- screenshotPath: screenshotResult?.screenshotPath || null
547
+ screenshotPath: screenshotResult?.screenshotPath || null,
548
+ artifactId: screenshotResult?.artifactId || null,
549
+ fullPath: screenshotResult?.fullPath || null
544
550
  };
545
551
  } catch (err) {
546
552
  return { error: err.message };
@@ -569,7 +575,9 @@ class BrowserController {
569
575
  y: py,
570
576
  url: page.url(),
571
577
  title: await page.title(),
572
- screenshotPath: screenshotResult?.screenshotPath || null
578
+ screenshotPath: screenshotResult?.screenshotPath || null,
579
+ artifactId: screenshotResult?.artifactId || null,
580
+ fullPath: screenshotResult?.fullPath || null
573
581
  };
574
582
  } catch (err) {
575
583
  return { error: err.message };
@@ -593,7 +601,9 @@ class BrowserController {
593
601
  success: true,
594
602
  url: page.url(),
595
603
  title: await page.title(),
596
- screenshotPath: screenshotResult?.screenshotPath || null
604
+ screenshotPath: screenshotResult?.screenshotPath || null,
605
+ artifactId: screenshotResult?.artifactId || null,
606
+ fullPath: screenshotResult?.fullPath || null
597
607
  };
598
608
  } catch (err) {
599
609
  return { error: err.message };
@@ -624,7 +634,9 @@ class BrowserController {
624
634
  return {
625
635
  success: true,
626
636
  typed: text,
627
- screenshotPath: screenshotResult?.screenshotPath || null
637
+ screenshotPath: screenshotResult?.screenshotPath || null,
638
+ artifactId: screenshotResult?.artifactId || null,
639
+ fullPath: screenshotResult?.fullPath || null
628
640
  };
629
641
  } catch (err) {
630
642
  return { error: err.message };
@@ -650,7 +662,9 @@ class BrowserController {
650
662
  return {
651
663
  success: true,
652
664
  typed: String(text || ''),
653
- screenshotPath: screenshotResult?.screenshotPath || null
665
+ screenshotPath: screenshotResult?.screenshotPath || null,
666
+ artifactId: screenshotResult?.artifactId || null,
667
+ fullPath: screenshotResult?.fullPath || null
654
668
  };
655
669
  } catch (err) {
656
670
  return { error: err.message };
@@ -674,7 +688,9 @@ class BrowserController {
674
688
  return {
675
689
  success: true,
676
690
  key: normalized,
677
- screenshotPath: screenshotResult?.screenshotPath || null
691
+ screenshotPath: screenshotResult?.screenshotPath || null,
692
+ artifactId: screenshotResult?.artifactId || null,
693
+ fullPath: screenshotResult?.fullPath || null
678
694
  };
679
695
  } catch (err) {
680
696
  return { error: err.message };
@@ -165,13 +165,45 @@ class VmBrowserProvider {
165
165
  }
166
166
 
167
167
  async #materialize(result) {
168
- if (!result || !result.fullPath || !this.artifactStore || this.userId == null) {
168
+ if (!result || !this.artifactStore || this.userId == null) {
169
169
  return result;
170
170
  }
171
- const file = await this.client.request('POST', '/files/read', {
172
- path: result.fullPath,
173
- encoding: 'base64',
174
- });
171
+
172
+ const readablePathCandidates = [];
173
+ if (result.fullPath) {
174
+ readablePathCandidates.push(String(result.fullPath));
175
+ }
176
+ if (typeof result.screenshotPath === 'string' && result.screenshotPath.startsWith('/screenshots/')) {
177
+ readablePathCandidates.push(result.screenshotPath);
178
+ }
179
+ if (readablePathCandidates.length === 0) {
180
+ return result;
181
+ }
182
+
183
+ let file = null;
184
+ for (const candidate of readablePathCandidates) {
185
+ try {
186
+ file = await this.client.request('POST', '/files/read', {
187
+ path: candidate,
188
+ encoding: 'base64',
189
+ });
190
+ if (file?.content) {
191
+ break;
192
+ }
193
+ } catch {}
194
+ }
195
+ if (!file?.content) {
196
+ if (typeof result.screenshotPath === 'string' && result.screenshotPath.startsWith('/screenshots/')) {
197
+ return {
198
+ ...result,
199
+ screenshotPath: null,
200
+ artifactId: result.artifactId || null,
201
+ fullPath: result.fullPath || null,
202
+ };
203
+ }
204
+ return result;
205
+ }
206
+
175
207
  const allocation = this.artifactStore.allocateFile(this.userId, {
176
208
  kind: 'browser-screenshot',
177
209
  backend: 'vm',
@@ -5,15 +5,17 @@ const fsp = require('fs/promises');
5
5
  const path = require('path');
6
6
  const { randomUUID } = require('crypto');
7
7
 
8
+ const db = require('../../db/database');
8
9
  const { DATA_DIR } = require('../../../runtime/paths');
9
10
  const { CLIExecutor } = require('../cli/executor');
10
- const { isDeepgramConfigured, transcribeChunkWithDeepgram } = require('../recordings/deepgram');
11
11
  const { getAdapterForPlatform } = require('./adapters');
12
12
  const { decideTranscriptPath, parseCaptionText, pickCaptionTrack } = require('./captions');
13
13
  const { inferImageContentType, pickDeterministicFrameSecond } = require('./frame');
14
14
  const { extractPublicMetadataFromHtml } = require('./metadata');
15
15
  const { shapeSocialVideoResult } = require('./result');
16
16
  const { normalizeAndDetectPlatform } = require('./url');
17
+ const { isMainAgent } = require('../agents/manager');
18
+ const { resolveSttModel, transcribeVoiceInput } = require('../voice/providers');
17
19
 
18
20
  const SOCIAL_VIDEO_TMP_DIR = path.join(DATA_DIR, 'social-video-temp');
19
21
  fs.mkdirSync(SOCIAL_VIDEO_TMP_DIR, { recursive: true });
@@ -70,6 +72,60 @@ function unwrapBrowserExtractValue(payload) {
70
72
  return '';
71
73
  }
72
74
 
75
+ function parseStoredSettingValue(value) {
76
+ if (typeof value !== 'string') {
77
+ return value;
78
+ }
79
+ try {
80
+ return JSON.parse(value);
81
+ } catch {
82
+ return value;
83
+ }
84
+ }
85
+
86
+ function readStoredSetting(userId, agentId, key) {
87
+ if (!userId) {
88
+ return null;
89
+ }
90
+
91
+ if (agentId) {
92
+ const agentRow = db.prepare(
93
+ 'SELECT value FROM agent_settings WHERE user_id = ? AND agent_id = ? AND key = ?',
94
+ ).get(userId, agentId, key);
95
+ if (agentRow) {
96
+ return parseStoredSettingValue(agentRow.value);
97
+ }
98
+ }
99
+
100
+ if (!agentId || isMainAgent(userId, agentId)) {
101
+ const userRow = db.prepare(
102
+ 'SELECT value FROM user_settings WHERE user_id = ? AND key = ?',
103
+ ).get(userId, key);
104
+ if (userRow) {
105
+ return parseStoredSettingValue(userRow.value);
106
+ }
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ function resolveVoiceSttConfigFromSettings(settings = {}) {
113
+ const provider = String(
114
+ settings.voice_stt_provider
115
+ || settings.default_recording_transcription_provider
116
+ || '',
117
+ ).trim().toLowerCase() || 'openai';
118
+ const model = String(
119
+ settings.voice_stt_model
120
+ || settings.default_recording_transcription_model
121
+ || '',
122
+ ).trim();
123
+ return {
124
+ provider,
125
+ model: resolveSttModel(provider, model),
126
+ };
127
+ }
128
+
73
129
  function fileExists(filePath) {
74
130
  try {
75
131
  return fs.statSync(filePath).isFile();
@@ -124,6 +180,8 @@ class SocialVideoService {
124
180
  this.artifactStore = options.artifactStore || null;
125
181
  this.runtimeManager = options.runtimeManager || null;
126
182
  this.cliExecutor = options.cliExecutor || new CLIExecutor();
183
+ this.voiceTranscriber = options.voiceTranscriber || transcribeVoiceInput;
184
+ this.voiceSettingsResolver = options.voiceSettingsResolver || ((userId, agentId) => this.#resolveVoiceSttConfig(userId, agentId));
127
185
  this.ytDlpBin = String(process.env.YT_DLP_BIN || 'yt-dlp').trim() || 'yt-dlp';
128
186
  this.ffmpegBin = String(process.env.FFMPEG_BIN || 'ffmpeg').trim() || 'ffmpeg';
129
187
  this._healthCache = {
@@ -148,10 +206,7 @@ class SocialVideoService {
148
206
  ready: ytDlp.available && ffmpeg.available,
149
207
  dependencies: [ytDlp, ffmpeg],
150
208
  speechToText: {
151
- configured: isDeepgramConfigured(),
152
- note: isDeepgramConfigured()
153
- ? 'Deepgram is configured for speech-to-text fallback.'
154
- : 'DEEPGRAM_API_KEY is not configured. Extraction still works when platform captions are available.',
209
+ note: 'Transcript fallback uses the configured voice STT provider from Flutter settings.',
155
210
  },
156
211
  checkedAt: new Date().toISOString(),
157
212
  };
@@ -167,6 +222,7 @@ class SocialVideoService {
167
222
  const warnings = [];
168
223
  const errors = [];
169
224
  const source = String(sourceUrl || '').trim();
225
+ const agentId = options.agentId || null;
170
226
  let jobDir = null;
171
227
 
172
228
  try {
@@ -208,6 +264,8 @@ class SocialVideoService {
208
264
  captionTrack,
209
265
  transcriptDecision,
210
266
  jobDir,
267
+ userId,
268
+ agentId,
211
269
  warnings,
212
270
  });
213
271
 
@@ -375,14 +433,14 @@ class SocialVideoService {
375
433
  }
376
434
 
377
435
  async #readMediaInfo(normalizedUrl, jobDir) {
378
- const infoPath = path.join(jobDir, 'media-info.json');
379
- const command = `${shellEscape(this.ytDlpBin)} --no-playlist --skip-download --dump-single-json -- ${shellEscape(normalizedUrl)}`;
380
- const result = await this.#runCommand(command, { cwd: jobDir, timeout: 4 * 60 * 1000 });
381
- const raw = String(result.stdout || '').trim();
382
- if (!raw) {
383
- throw new Error('yt-dlp returned empty media metadata output.');
436
+ const infoTemplate = path.join(jobDir, 'media.%(ext)s');
437
+ const infoPath = path.join(jobDir, 'media.info.json');
438
+ const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist --skip-download --write-info-json --no-clean-infojson -o ${shellEscape(infoTemplate)} -- ${shellEscape(normalizedUrl)}`;
439
+ await this.#runCommand(command, { cwd: jobDir, timeout: 4 * 60 * 1000 });
440
+ if (!fileExists(infoPath)) {
441
+ throw new Error('yt-dlp did not produce an info JSON artifact.');
384
442
  }
385
- await fsp.writeFile(infoPath, `${raw}\n`, 'utf8');
443
+ const raw = String(await fsp.readFile(infoPath, 'utf8')).trim();
386
444
  let parsed;
387
445
  try {
388
446
  parsed = JSON.parse(raw);
@@ -407,15 +465,10 @@ class SocialVideoService {
407
465
  context.warnings.push('Caption track was present but transcript text was empty. Falling back to speech-to-text.');
408
466
  }
409
467
 
410
- if (!isDeepgramConfigured()) {
411
- context.warnings.push('Captions unavailable and DEEPGRAM_API_KEY is not configured; transcript could not be generated.');
412
- return {
413
- text: '',
414
- source: 'unavailable',
415
- };
416
- }
417
-
418
- const transcript = await this.#transcribeViaStt(context.sourceUrl, context.jobDir);
468
+ const transcript = await this.#transcribeViaStt(context).catch((error) => {
469
+ context.warnings.push(`Speech-to-text fallback failed: ${error.message}`);
470
+ return '';
471
+ });
419
472
  return {
420
473
  text: transcript,
421
474
  source: transcript ? 'stt' : 'unavailable',
@@ -431,23 +484,41 @@ class SocialVideoService {
431
484
  return parseCaptionText(raw, captionTrack.ext);
432
485
  }
433
486
 
434
- async #transcribeViaStt(sourceUrl, jobDir) {
435
- const template = path.join(jobDir, 'audio.%(ext)s');
436
- const command = `${shellEscape(this.ytDlpBin)} --no-playlist -f bestaudio -- ${shellEscape(sourceUrl)} -o ${shellEscape(template)}`;
437
- await this.#runCommand(command, { cwd: jobDir, timeout: 10 * 60 * 1000 });
487
+ async #transcribeViaStt(context) {
488
+ const template = path.join(context.jobDir, 'audio.%(ext)s');
489
+ const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist -o ${shellEscape(template)} -f bestaudio -- ${shellEscape(context.sourceUrl)}`;
490
+ await this.#runCommand(command, { cwd: context.jobDir, timeout: 10 * 60 * 1000 });
438
491
 
439
- const audioPath = firstFileMatching(jobDir, 'audio.');
492
+ const audioPath = firstFileMatching(context.jobDir, 'audio.');
440
493
  if (!audioPath || !fileExists(audioPath)) {
441
494
  throw new Error('Audio download succeeded but no audio file was created.');
442
495
  }
443
496
 
444
- const audioBytes = await fsp.readFile(audioPath);
445
- const deepgramResult = await transcribeChunkWithDeepgram({
446
- audioBytes,
497
+ const sttConfig = await Promise.resolve(
498
+ this.voiceSettingsResolver(context.userId, context.agentId),
499
+ );
500
+ return this.voiceTranscriber(audioPath, {
501
+ provider: sttConfig?.provider || 'openai',
502
+ model: sttConfig?.model || '',
447
503
  mimeType: detectMimeFromFile(audioPath),
448
504
  });
449
- const transcript = deepgramResult?.results?.channels?.[0]?.alternatives?.[0]?.transcript;
450
- return String(transcript || '').trim();
505
+ }
506
+
507
+ async #resolveVoiceSttConfig(userId, agentId) {
508
+ return resolveVoiceSttConfigFromSettings({
509
+ voice_stt_provider: readStoredSetting(userId, agentId, 'voice_stt_provider'),
510
+ voice_stt_model: readStoredSetting(userId, agentId, 'voice_stt_model'),
511
+ default_recording_transcription_provider: readStoredSetting(
512
+ userId,
513
+ agentId,
514
+ 'default_recording_transcription_provider',
515
+ ),
516
+ default_recording_transcription_model: readStoredSetting(
517
+ userId,
518
+ agentId,
519
+ 'default_recording_transcription_model',
520
+ ),
521
+ });
451
522
  }
452
523
 
453
524
  async #resolveFrameImage(context) {
@@ -469,7 +540,7 @@ class SocialVideoService {
469
540
 
470
541
  async #extractFrameFromVideo(context) {
471
542
  const template = path.join(context.jobDir, 'video.%(ext)s');
472
- const downloadCommand = `${shellEscape(this.ytDlpBin)} --no-playlist -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best" --merge-output-format mp4 -- ${shellEscape(context.sourceUrl)} -o ${shellEscape(template)}`;
543
+ const downloadCommand = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist -o ${shellEscape(template)} -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best" --merge-output-format mp4 -- ${shellEscape(context.sourceUrl)}`;
473
544
  await this.#runCommand(downloadCommand, { cwd: context.jobDir, timeout: 14 * 60 * 1000 });
474
545
 
475
546
  const videoPath = firstFileMatching(context.jobDir, 'video.');
@@ -479,7 +550,7 @@ class SocialVideoService {
479
550
 
480
551
  const framePath = path.join(context.jobDir, 'frame.jpg');
481
552
  const frameSecond = pickDeterministicFrameSecond(context.mediaInfo.duration);
482
- const frameCommand = `${shellEscape(this.ffmpegBin)} -y -hide_banner -loglevel error -ss ${frameSecond} -i ${shellEscape(videoPath)} -frames:v 1 -q:v 2 ${shellEscape(framePath)}`;
553
+ const frameCommand = `${shellEscape(this.ffmpegBin)} -hwaccel none -y -hide_banner -loglevel error -ss ${frameSecond} -i ${shellEscape(videoPath)} -frames:v 1 -q:v 2 ${shellEscape(framePath)}`;
483
554
  await this.#runCommand(frameCommand, { cwd: context.jobDir, timeout: 2 * 60 * 1000 });
484
555
 
485
556
  if (!fileExists(framePath)) {
@@ -505,7 +576,7 @@ class SocialVideoService {
505
576
  source: 'thumbnail',
506
577
  };
507
578
  }
508
- const allocation = this.artifactStore.allocateFile(userId, {
579
+ const allocation = await Promise.resolve(this.artifactStore.allocateFile(userId, {
509
580
  kind: 'social-video-frame',
510
581
  extension: guessedExtension,
511
582
  contentType: mimeType,
@@ -513,9 +584,11 @@ class SocialVideoService {
513
584
  metadata: {
514
585
  source: 'social-video-thumbnail',
515
586
  },
516
- });
587
+ }));
517
588
  await fsp.writeFile(allocation.storagePath, buffer);
518
- const finalized = this.artifactStore.finalizeFile(allocation.artifactId, allocation.storagePath);
589
+ const finalized = await Promise.resolve(
590
+ this.artifactStore.finalizeFile(allocation.artifactId, allocation.storagePath),
591
+ );
519
592
  return {
520
593
  url: finalized.url,
521
594
  artifactId: finalized.artifactId,
@@ -572,5 +645,6 @@ module.exports = {
572
645
  firstFileMatching,
573
646
  pickBestThumbnail,
574
647
  classifyExtractionError,
648
+ resolveVoiceSttConfigFromSettings,
575
649
  shellEscape,
576
650
  };