opencode-pollinations-plugin 6.1.0-beta.12 → 6.1.0-beta.22

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 (73) hide show
  1. package/README.md +11 -6
  2. package/dist/index.js +40 -10
  3. package/dist/server/commands.d.ts +4 -0
  4. package/dist/server/commands.js +296 -12
  5. package/dist/server/config.d.ts +5 -0
  6. package/dist/server/config.js +163 -35
  7. package/dist/server/connect-response.d.ts +2 -0
  8. package/dist/server/connect-response.js +141 -0
  9. package/dist/server/generate-config.js +10 -24
  10. package/dist/server/logger.d.ts +8 -0
  11. package/dist/server/logger.js +36 -0
  12. package/dist/server/models/cache.d.ts +35 -0
  13. package/dist/server/models/cache.js +160 -0
  14. package/dist/server/models/fetcher.d.ts +18 -0
  15. package/dist/server/models/fetcher.js +150 -0
  16. package/dist/server/models/index.d.ts +6 -0
  17. package/dist/server/models/index.js +5 -0
  18. package/dist/server/models/manual.d.ts +15 -0
  19. package/dist/server/models/manual.js +92 -0
  20. package/dist/server/models/types.d.ts +55 -0
  21. package/dist/server/models/types.js +7 -0
  22. package/dist/server/models/worker.d.ts +21 -0
  23. package/dist/server/models/worker.js +97 -0
  24. package/dist/server/pollinations-api.js +1 -8
  25. package/dist/server/proxy.js +52 -27
  26. package/dist/server/quota.d.ts +2 -8
  27. package/dist/server/quota.js +47 -89
  28. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  29. package/dist/server/scripts/pollinations_pricing.js +246 -0
  30. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  31. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  32. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  33. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  34. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  35. package/dist/server/scripts/test_freetier_audit.js +215 -0
  36. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  37. package/dist/server/scripts/test_parallel_cost.js +104 -0
  38. package/dist/server/toast.d.ts +4 -1
  39. package/dist/server/toast.js +27 -10
  40. package/dist/tools/ffmpeg.d.ts +24 -0
  41. package/dist/tools/ffmpeg.js +54 -0
  42. package/dist/tools/index.d.ts +10 -8
  43. package/dist/tools/index.js +27 -25
  44. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  45. package/dist/tools/pollinations/beta_discovery.js +197 -0
  46. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  47. package/dist/tools/pollinations/cost-guard.js +141 -0
  48. package/dist/tools/pollinations/gen_audio.d.ts +1 -1
  49. package/dist/tools/pollinations/gen_audio.js +65 -23
  50. package/dist/tools/pollinations/gen_image.d.ts +5 -7
  51. package/dist/tools/pollinations/gen_image.js +146 -160
  52. package/dist/tools/pollinations/gen_music.d.ts +1 -1
  53. package/dist/tools/pollinations/gen_music.js +57 -16
  54. package/dist/tools/pollinations/gen_video.d.ts +1 -1
  55. package/dist/tools/pollinations/gen_video.js +99 -65
  56. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  57. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  58. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  59. package/dist/tools/pollinations/polli_status.js +31 -0
  60. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  61. package/dist/tools/pollinations/polli_web_search.js +164 -0
  62. package/dist/tools/pollinations/shared.d.ts +34 -39
  63. package/dist/tools/pollinations/shared.js +300 -89
  64. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  65. package/dist/tools/pollinations/test_estimators.js +22 -0
  66. package/dist/tools/pollinations/transcribe_audio.d.ts +5 -9
  67. package/dist/tools/pollinations/transcribe_audio.js +31 -72
  68. package/dist/tools/power/extract_audio.js +26 -27
  69. package/dist/tools/power/extract_frames.js +24 -27
  70. package/dist/tools/power/remove_background.js +2 -1
  71. package/dist/tools/power/rmbg_keys.js +2 -1
  72. package/dist/tools/shared.js +9 -3
  73. package/package.json +2 -2
@@ -3,40 +3,36 @@
3
3
  *
4
4
  * Updated: 2026-02-12 - Verified API Reference
5
5
  *
6
- * Two STT options:
7
- * 1. openai-audio (DEFAULT): GPT-4o Audio Preview - uses /v1/chat/completions with modalities
8
- * - Least expensive option
9
- * - Can handle both audio input and output
6
+ * 1. whisper-large-v3 (DEFAULT): High accuracy Whisper model
7
+ * 2. whisper-1: Standard Whisper model
8
+ * 3. scribe: ElevenLabs Scribe v2
10
9
  *
11
- * 2. whisper: OpenAI Whisper v3 - uses /v1/audio/transcriptions
12
- * - POST ONLY with multipart/form-data
13
- * - Specialized for transcription
14
- * - Higher accuracy for long audio
10
+ * All models use /v1/audio/transcriptions (POST multipart)
15
11
  */
16
12
  import { tool } from '@opencode-ai/plugin/tool';
17
13
  import * as fs from 'fs';
18
14
  import * as path from 'path';
19
- import { getApiKey, httpsPost, httpsPostMultipart, ensureDir, formatFileSize, AUDIO_MODELS, } from './shared.js';
15
+ import { getApiKey, httpsPostMultipart, ensureDir, formatFileSize, getAudioModels, } from './shared.js';
20
16
  // ─── Constants ─────────────────────────────────────────────────────────────
21
- const DEFAULT_MODEL = 'openai-audio';
22
- const SUPPORTED_FORMATS = ['mp3', 'wav', 'm4a', 'webm', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg'];
17
+ const DEFAULT_MODEL = 'whisper-large-v3';
18
+ const SUPPORTED_FORMATS = ['mp3', 'wav', 'm4a', 'webm', 'mp4', 'mpeg', 'mpga', 'ogg'];
23
19
  // ─── Tool Definition ──────────────────────────────────────────────────────
24
- export const transcribeAudioTool = tool({
20
+ export const polliSttTool = tool({
25
21
  description: `Transcribe audio to text using Pollinations AI.
26
22
 
27
23
  **🎙️ Models:**
28
24
 
29
- | Model | Endpoint | Best For | Notes |
30
- |-------|----------|----------|-------|
31
- | openai-audio | /v1/chat/completions | Short-medium audio | **DEFAULT** - lowest cost |
32
- | whisper | /v1/audio/transcriptions | Long audio, high accuracy | POST multipart only |
25
+ | Model | Supplier | Notes |
26
+ |-------|----------|-------|
27
+ | whisper-large-v3 | OpenAI | **DEFAULT** - High accuracy, long audio |
28
+ | whisper-1 | OpenAI | Standard accuracy |
29
+ | scribe | ElevenLabs | Scribe v2 model |
33
30
 
34
31
  **📁 Supported Formats:**
35
- mp3, wav, m4a, webm, mp4, mpeg, mpga, oga, ogg
32
+ mp3, wav, m4a, webm, mp4, mpeg, mpga, ogg
36
33
 
37
34
  **💡 Tips:**
38
- - Use \`openai-audio\` for cost-effective transcription
39
- - Use \`whisper\` for highest accuracy on long recordings
35
+ - Use \`whisper-large-v3\` for the highest accuracy on long recordings
40
36
  - Supports both local files and URLs
41
37
 
42
38
  **📋 Output:**
@@ -57,10 +53,11 @@ mp3, wav, m4a, webm, mp4, mpeg, mpga, oga, ogg
57
53
  }
58
54
  const model = args.model || DEFAULT_MODEL;
59
55
  // Validate model
60
- const modelInfo = AUDIO_MODELS[model];
56
+ const audioModels = getAudioModels();
57
+ const modelInfo = audioModels[model];
61
58
  if (!modelInfo || (modelInfo.type !== 'stt' && modelInfo.type !== 'both')) {
62
59
  return `❌ Modèle STT inconnu: ${model}
63
- 💡 Modèles STT disponibles: ${Object.entries(AUDIO_MODELS)
60
+ 💡 Modèles STT disponibles: ${Object.entries(audioModels)
64
61
  .filter(([, info]) => info.type === 'stt' || info.type === 'both')
65
62
  .map(([name]) => name)
66
63
  .join(', ')}`;
@@ -130,58 +127,20 @@ mp3, wav, m4a, webm, mp4, mpeg, mpga, oga, ogg
130
127
  try {
131
128
  let transcript = '';
132
129
  let detectedLanguage = '';
133
- if (model === 'openai-audio') {
134
- // === OpenAI Audio: Use modalities endpoint ===
135
- // Convert audio to base64
136
- const base64Audio = audioBuffer.toString('base64');
137
- const mimeType = fileName.endsWith('.mp3') ? 'audio/mpeg' :
138
- fileName.endsWith('.wav') ? 'audio/wav' :
139
- 'audio/mp4';
140
- const response = await httpsPost('https://gen.pollinations.ai/v1/chat/completions', {
141
- model: 'openai-audio',
142
- modalities: ['text', 'audio'],
143
- messages: [
144
- {
145
- role: 'user',
146
- content: [
147
- {
148
- type: 'text',
149
- text: args.language
150
- ? `Transcribe this audio to text. Language: ${args.language}`
151
- : 'Transcribe this audio to text.'
152
- },
153
- {
154
- type: 'input_audio',
155
- input_audio: {
156
- data: base64Audio,
157
- format: fileName.endsWith('.mp3') ? 'mp3' : 'wav'
158
- }
159
- }
160
- ]
161
- }
162
- ]
163
- }, {
164
- 'Authorization': `Bearer ${apiKey}`,
165
- });
166
- const data = JSON.parse(response.data.toString());
167
- transcript = data.choices?.[0]?.message?.content || '';
168
- }
169
- else if (model === 'whisper') {
170
- // === Whisper: Use multipart endpoint ===
171
- const fields = {
172
- file: audioBuffer,
173
- model: 'whisper',
174
- };
175
- if (args.language) {
176
- fields.language = args.language;
177
- }
178
- const response = await httpsPostMultipart('https://gen.pollinations.ai/v1/audio/transcriptions', fields, {
179
- 'Authorization': `Bearer ${apiKey}`,
180
- });
181
- const data = JSON.parse(response.data.toString());
182
- transcript = data.text || '';
183
- detectedLanguage = data.language || '';
130
+ // === All STT models use multipart endpoint ===
131
+ const fields = {
132
+ file: audioBuffer,
133
+ model: model,
134
+ };
135
+ if (args.language) {
136
+ fields.language = args.language;
184
137
  }
138
+ const response = await httpsPostMultipart('https://gen.pollinations.ai/v1/audio/transcriptions', fields, {
139
+ 'Authorization': `Bearer ${apiKey}`,
140
+ });
141
+ const data = JSON.parse(response.data.toString());
142
+ transcript = data.text || '';
143
+ detectedLanguage = data.language || '';
185
144
  if (!transcript) {
186
145
  return `❌ Aucune transcription générée.
187
146
  💡 Vérifiez que l'audio contient de la parole claire.`;
@@ -5,6 +5,7 @@ import * as os from 'os';
5
5
  import * as https from 'https';
6
6
  import * as http from 'http';
7
7
  import { resolveOutputDir, formatFileSize, safeName, formatTimestamp, TOOL_DIRS } from '../shared.js';
8
+ import { hasSystemFFmpeg, getFFmpegInstallInstructions, runFFmpeg, runFFprobe } from '../ffmpeg.js';
8
9
  // ─── Download helper ────────────────────────────────────────────────────────
9
10
  function downloadFile(url) {
10
11
  return new Promise((resolve, reject) => {
@@ -27,16 +28,6 @@ function downloadFile(url) {
27
28
  req.setTimeout(120000, () => { req.destroy(); reject(new Error('Timeout (120s)')); });
28
29
  });
29
30
  }
30
- // ─── FFmpeg check ───────────────────────────────────────────────────────────
31
- function hasSystemFFmpeg() {
32
- try {
33
- require('child_process').execSync('ffmpeg -version', { stdio: 'ignore' });
34
- return true;
35
- }
36
- catch {
37
- return false;
38
- }
39
- }
40
31
  // ─── Tool Definition ────────────────────────────────────────────────────────
41
32
  export const extractAudioTool = tool({
42
33
  description: `Extract the audio track from a video file or URL.
@@ -63,9 +54,7 @@ Free to use — no API key needed.`,
63
54
  `❌ FFmpeg non trouvé!`,
64
55
  ``,
65
56
  `Cet outil nécessite ffmpeg :`,
66
- ` • Linux: sudo apt install ffmpeg`,
67
- ` • macOS: brew install ffmpeg`,
68
- ` • Windows: choco install ffmpeg`,
57
+ getFFmpegInstallInstructions().split('\n').map(l => ` • ${l}`).join('\n')
69
58
  ].join('\n');
70
59
  }
71
60
  // Resolve source
@@ -89,8 +78,14 @@ Free to use — no API key needed.`,
89
78
  }
90
79
  // Check if video has audio
91
80
  try {
92
- const { execSync } = require('child_process');
93
- const probe = execSync(`ffprobe -v quiet -select_streams a -show_entries stream=codec_type -of csv=p=0 "${videoPath}"`, { timeout: 10000, encoding: 'utf-8' }).trim();
81
+ // Using runFFprobe helper
82
+ const probe = runFFprobe([
83
+ '-v', 'quiet',
84
+ '-select_streams', 'a',
85
+ '-show_entries', 'stream=codec_type',
86
+ '-of', 'csv=p=0',
87
+ videoPath
88
+ ], { timeout: 10000 }).trim();
94
89
  if (!probe) {
95
90
  if (isRemote)
96
91
  try {
@@ -109,31 +104,30 @@ Free to use — no API key needed.`,
109
104
  const outputFile = path.join(outputDir, `${baseName}.${outputFormat}`);
110
105
  try {
111
106
  context.metadata({ title: `🎵 Extraction audio...` });
112
- const { execSync } = require('child_process');
113
- // Build ffmpeg command
114
- let cmd = `ffmpeg -y -i "${videoPath}" -vn`;
107
+ // Build ffmpeg args
108
+ const ffmpegArgs = ['-y', '-i', videoPath, '-vn'];
115
109
  // Time range
116
110
  if (args.start)
117
- cmd += ` -ss ${args.start}`;
111
+ ffmpegArgs.push('-ss', args.start);
118
112
  if (args.end)
119
- cmd += ` -to ${args.end}`;
113
+ ffmpegArgs.push('-to', args.end);
120
114
  // Format-specific encoding
121
115
  switch (outputFormat) {
122
116
  case 'mp3':
123
- cmd += ` -acodec libmp3lame -q:a 2`;
117
+ ffmpegArgs.push('-acodec', 'libmp3lame', '-q:a', '2');
124
118
  break;
125
119
  case 'wav':
126
- cmd += ` -acodec pcm_s16le`;
120
+ ffmpegArgs.push('-acodec', 'pcm_s16le');
127
121
  break;
128
122
  case 'aac':
129
- cmd += ` -acodec aac -b:a 192k`;
123
+ ffmpegArgs.push('-acodec', 'aac', '-b:a', '192k');
130
124
  break;
131
125
  case 'flac':
132
- cmd += ` -acodec flac`;
126
+ ffmpegArgs.push('-acodec', 'flac');
133
127
  break;
134
128
  }
135
- cmd += ` "${outputFile}"`;
136
- execSync(cmd, { stdio: 'ignore', timeout: 120000 });
129
+ ffmpegArgs.push(outputFile);
130
+ runFFmpeg(ffmpegArgs, { timeout: 120000 });
137
131
  // Cleanup
138
132
  if (isRemote && fs.existsSync(videoPath)) {
139
133
  try {
@@ -148,7 +142,12 @@ Free to use — no API key needed.`,
148
142
  // Get audio duration
149
143
  let durationStr = 'N/A';
150
144
  try {
151
- const durRaw = execSync(`ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${outputFile}"`, { timeout: 5000, encoding: 'utf-8' }).trim();
145
+ const durRaw = runFFprobe([
146
+ '-v', 'quiet',
147
+ '-show_entries', 'format=duration',
148
+ '-of', 'csv=p=0',
149
+ outputFile
150
+ ], { timeout: 5000 }).trim();
152
151
  const dur = parseFloat(durRaw);
153
152
  if (!isNaN(dur))
154
153
  durationStr = formatTimestamp(dur);
@@ -5,12 +5,17 @@ import * as os from 'os';
5
5
  import * as https from 'https';
6
6
  import * as http from 'http';
7
7
  import { resolveOutputDir, formatFileSize, safeName, formatTimestamp, TOOL_DIRS } from '../shared.js';
8
+ import { hasSystemFFmpeg, getFFmpegInstallInstructions, runFFmpeg, runFFprobe } from '../ffmpeg.js';
8
9
  function extractMetadata(videoPath) {
9
10
  try {
10
- const { execSync } = require('child_process');
11
11
  // Use ffprobe JSON output for reliable parsing
12
- const probeCmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`;
13
- const raw = execSync(probeCmd, { timeout: 15000, encoding: 'utf-8' });
12
+ const raw = runFFprobe([
13
+ '-v', 'quiet',
14
+ '-print_format', 'json',
15
+ '-show_format',
16
+ '-show_streams',
17
+ videoPath
18
+ ]);
14
19
  const data = JSON.parse(raw);
15
20
  const videoStream = data.streams?.find((s) => s.codec_type === 'video');
16
21
  const audioStream = data.streams?.find((s) => s.codec_type === 'audio');
@@ -82,38 +87,32 @@ function downloadVideo(url) {
82
87
  req.setTimeout(120000, () => { req.destroy(); reject(new Error('Timeout téléchargement (120s)')); });
83
88
  });
84
89
  }
85
- // ─── FFmpeg availability ────────────────────────────────────────────────────
86
- function hasSystemFFmpeg() {
87
- try {
88
- const { execSync } = require('child_process');
89
- execSync('ffmpeg -version', { stdio: 'ignore' });
90
- return true;
91
- }
92
- catch {
93
- return false;
94
- }
95
- }
96
90
  // ─── Frame extraction ───────────────────────────────────────────────────────
97
91
  function extractWithSystemFFmpeg(videoPath, outputDir, baseName, options) {
98
- const { execSync } = require('child_process');
99
92
  const outputs = [];
100
- let cmd = `ffmpeg -y -i "${videoPath}"`;
101
93
  if (options.at_time) {
94
+ // Single Frame Extraction
102
95
  const singleOutput = path.join(outputDir, `${baseName}_at_${options.at_time.replace(/:/g, '-')}.png`);
103
- cmd += ` -ss ${options.at_time} -frames:v 1 "${singleOutput}"`;
104
- execSync(cmd, { stdio: 'ignore', timeout: 60000 });
96
+ runFFmpeg([
97
+ '-y', '-i', videoPath,
98
+ '-ss', options.at_time,
99
+ '-frames:v', '1',
100
+ singleOutput
101
+ ], { timeout: 60000 });
105
102
  if (fs.existsSync(singleOutput))
106
103
  outputs.push(singleOutput);
107
104
  }
108
105
  else {
109
- if (options.start)
110
- cmd += ` -ss ${options.start}`;
111
- if (options.end)
112
- cmd += ` -to ${options.end}`;
106
+ // Range Extraction
113
107
  const fps = options.fps || 1;
114
108
  const outputPattern = path.join(outputDir, `${baseName}_%03d.png`);
115
- cmd += ` -vf "fps=${fps}" "${outputPattern}"`;
116
- execSync(cmd, { stdio: 'ignore', timeout: 120000 });
109
+ const args = ['-y', '-i', videoPath];
110
+ if (options.start)
111
+ args.push('-ss', options.start);
112
+ if (options.end)
113
+ args.push('-to', options.end);
114
+ args.push('-vf', `fps=${fps}`, outputPattern);
115
+ runFFmpeg(args, { timeout: 120000 });
117
116
  fs.readdirSync(outputDir)
118
117
  .filter(f => f.startsWith(baseName) && f.endsWith('.png'))
119
118
  .sort()
@@ -146,9 +145,7 @@ Free to use — no API key needed.`,
146
145
  `❌ FFmpeg non trouvé!`,
147
146
  ``,
148
147
  `Cet outil nécessite ffmpeg. Installez-le :`,
149
- ` • Linux: sudo apt install ffmpeg`,
150
- ` • macOS: brew install ffmpeg`,
151
- ` • Windows: choco install ffmpeg`,
148
+ getFFmpegInstallInstructions().split('\n').map(l => ` • ${l}`).join('\n')
152
149
  ].join('\n');
153
150
  }
154
151
  // Resolve source: URL → download, path → validate
@@ -4,12 +4,13 @@ import * as https from 'https';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import { resolveOutputDir, formatFileSize, TOOL_DIRS } from '../shared.js';
7
+ import { getConfigDir } from '../../server/config.js';
7
8
  // ─── Provider Defaults ───────────────────────────────────────────────────────
8
9
  const CUT_API_URL = 'https://cut.esprit-artificiel.com';
9
10
  const CUT_API_KEY = 'sk-cut-fkomEA2026-hybridsoap161200';
10
11
  const BACKGROUNDCUT_API_URL = 'https://backgroundcut.co/api/v1/cut/';
11
12
  // ─── Key Storage ─────────────────────────────────────────────────────────────
12
- const KEYS_FILE = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.pollinations', 'backgroundcut_keys.json');
13
+ const KEYS_FILE = path.join(getConfigDir(), 'backgroundcut_keys.json');
13
14
  function loadKeys() {
14
15
  try {
15
16
  if (fs.existsSync(KEYS_FILE)) {
@@ -2,7 +2,8 @@ import { tool } from '@opencode-ai/plugin/tool';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  // ─── Shared Logic (Duplicated from rmbg to avoid circular deps if not using shared.ts for this) ──
5
- const KEYS_FILE = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.pollinations', 'backgroundcut_keys.json');
5
+ import { CONFIG_DIR } from '../../server/config.js';
6
+ const KEYS_FILE = path.join(CONFIG_DIR, 'backgroundcut_keys.json');
6
7
  function loadKeys() {
7
8
  try {
8
9
  if (fs.existsSync(KEYS_FILE)) {
@@ -29,9 +29,15 @@ export function resolveOutputDir(subdir, customPath) {
29
29
  if (customPath) {
30
30
  // If customPath is absolute, use it directly
31
31
  // If relative, resolve from cwd
32
- dir = path.isAbsolute(customPath)
33
- ? customPath
34
- : path.resolve(process.cwd(), customPath);
32
+ // Handle tilde ~ manual expansion for cross-platform support
33
+ if (customPath.startsWith('~')) {
34
+ dir = path.join(os.homedir(), customPath.slice(1));
35
+ }
36
+ else {
37
+ dir = path.isAbsolute(customPath)
38
+ ? customPath
39
+ : path.resolve(process.cwd(), customPath);
40
+ }
35
41
  }
36
42
  else {
37
43
  dir = path.join(DEFAULT_BASE, subdir);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
3
  "displayName": "Pollinations AI (V5.9)",
4
- "version": "6.1.0-beta.12",
4
+ "version": "6.1.0-beta.22",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {
@@ -61,4 +61,4 @@
61
61
  "@types/qrcode": "^1.5.6",
62
62
  "typescript": "^5.0.0"
63
63
  }
64
- }
64
+ }