tune-sdk 0.2.4 → 0.2.5
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/.github/workflows/publish.yml +27 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/tools/README.md +0 -636
- package/tools/append.schema.json +0 -17
- package/tools/append.tool.mjs +0 -5
- package/tools/brave.schema.json +0 -13
- package/tools/brave.tool.mjs +0 -32
- package/tools/claude.txt +0 -1
- package/tools/clipboard.txt +0 -1
- package/tools/default.llm.js +0 -2
- package/tools/dev.txt +0 -6
- package/tools/echo.txt +0 -1
- package/tools/editor-filename.chat +0 -10
- package/tools/gemini-dev.txt +0 -7
- package/tools/gemini25.txt +0 -1
- package/tools/gemini_files.schema.json +0 -13
- package/tools/gemini_files.tool.mjs +0 -204
- package/tools/gemini_ocr.schema.json +0 -21
- package/tools/gemini_ocr.tool.mjs +0 -211
- package/tools/gemini_tts.schema.json +0 -59
- package/tools/gemini_tts.tool.mjs +0 -175
- package/tools/gemini_veo.schema.json +0 -12
- package/tools/gemini_veo.tool.mjs +0 -233
- package/tools/groq_whisper.schema.json +0 -48
- package/tools/groq_whisper.tool.mjs +0 -59
- package/tools/head.proc.js +0 -24
- package/tools/init.proc.js +0 -19
- package/tools/jina_r.schema.json +0 -21
- package/tools/jina_r.tool.mjs +0 -27
- package/tools/js.schema.json +0 -19
- package/tools/js.tool.mjs +0 -60
- package/tools/json_format.proc.mjs +0 -22
- package/tools/linenum.proc.js +0 -21
- package/tools/list.schema.json +0 -19
- package/tools/list.tool.mjs +0 -20
- package/tools/llm-utils.js +0 -150
- package/tools/log.proc.js +0 -15
- package/tools/mcp.proc.mjs +0 -174
- package/tools/message.schema.json +0 -21
- package/tools/message.tool.js +0 -14
- package/tools/mock.proc.js +0 -35
- package/tools/nu.schema.json +0 -13
- package/tools/nu.tool.mjs +0 -14
- package/tools/openai.js +0 -27
- package/tools/openai_imgen.schema.json +0 -35
- package/tools/openai_imgen.tool.mjs +0 -83
- package/tools/openai_stt.schema.json +0 -49
- package/tools/openai_stt.tool.mjs +0 -66
- package/tools/openai_tts.schema.json +0 -26
- package/tools/openai_tts.tool.mjs +0 -26
- package/tools/osa.schema.json +0 -13
- package/tools/osa.tool.mjs +0 -12
- package/tools/package.json +0 -7
- package/tools/patch.schema.json +0 -17
- package/tools/patch.tool.mjs +0 -38
- package/tools/prop.proc.mjs +0 -34
- package/tools/py.schema.json +0 -17
- package/tools/py.tool.py +0 -22
- package/tools/queryimage.schema.json +0 -17
- package/tools/queryimage.tool.chat +0 -4
- package/tools/resolve.proc.js +0 -10
- package/tools/rf.schema.json +0 -17
- package/tools/rf.tool.mjs +0 -21
- package/tools/schema.schema.json +0 -13
- package/tools/schema.tool.chat +0 -81
- package/tools/sh.schema.json +0 -13
- package/tools/sh.tool.mjs +0 -12
- package/tools/short.txt +0 -1
- package/tools/shp.proc.mjs +0 -31
- package/tools/slice.proc.js +0 -55
- package/tools/tail.proc.js +0 -35
- package/tools/text.proc.js +0 -13
- package/tools/turn.schema.json +0 -17
- package/tools/turn.tool.mjs +0 -8
- package/tools/wf.schema.json +0 -17
- package/tools/wf.tool.mjs +0 -16
- package/tools/yandex_tts.schema.json +0 -41
- package/tools/yandex_tts.tool.mjs +0 -31
package/tools/brave.schema.json
DELETED
package/tools/brave.tool.mjs
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export default async function brave({text}, ctx) {
|
|
2
|
-
const key = await ctx.read("BRAVE_KEY");
|
|
3
|
-
|
|
4
|
-
if (!key) {
|
|
5
|
-
throw new Error("BRAVE_KEY not found in context. Please set up your Brave Search API key.");
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
if (!text) {
|
|
9
|
-
throw new Error("Query parameter is required.");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const encodedQuery = encodeURIComponent(text);
|
|
13
|
-
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}`;
|
|
14
|
-
|
|
15
|
-
const response = await fetch(url, {
|
|
16
|
-
method: 'GET',
|
|
17
|
-
headers: {
|
|
18
|
-
'Accept': 'application/json',
|
|
19
|
-
'X-Subscription-Token': key
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!response.ok) {
|
|
25
|
-
throw new Error(`Brave Search API returned ${response.status}: ${response.statusText}\n`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const data = await response.json();
|
|
29
|
-
return data.web.results.map(item =>
|
|
30
|
-
`${item.title}\n${item.url}\n${item.description}`
|
|
31
|
-
).join("\n\n").replace(/@/g, "\\@");
|
|
32
|
-
}
|
package/tools/claude.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@claude-sonnet-4-20250514
|
package/tools/clipboard.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@{|shp pbpaste }
|
package/tools/default.llm.js
DELETED
package/tools/dev.txt
DELETED
package/tools/echo.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
you are echo, you print everything back
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
s: @{ gpt-4.1-mini | json_format }
|
|
2
|
-
You are giving name for chat based on the chat contents
|
|
3
|
-
chat file content:
|
|
4
|
-
```
|
|
5
|
-
@{ editor/buffer | head 100}
|
|
6
|
-
```
|
|
7
|
-
example json output:
|
|
8
|
-
{ "filename": "how-to-make-hero-costume.chat"}
|
|
9
|
-
or
|
|
10
|
-
{ "filename": "accounting-database-zig.chat"}
|
package/tools/gemini-dev.txt
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
@rf @wf @sh @@gemini25 @patch
|
|
2
|
-
|
|
3
|
-
Use sh tool to know more about environment you are in
|
|
4
|
-
use rf if you need to read source code of a file
|
|
5
|
-
use wf if you need to create a new file or overwrite existing
|
|
6
|
-
use patch for small changes in a file
|
|
7
|
-
Do not include code into your answers except using wf tool
|
package/tools/gemini25.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@google/gemini-2.5-pro-exp-03-25
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"description": "CLI-style wrapper around the Gemini Files API. Put a command in the \"text\" field (e.g. \"upload './path/to/file.j'pg\", \"list\", \"get files/abc123\", \"delete files/abc123\"). Returned value is a human-readable string or JSON for list/get/upload operations.",
|
|
3
|
-
"parameters": {
|
|
4
|
-
"type": "object",
|
|
5
|
-
"properties": {
|
|
6
|
-
"text": {
|
|
7
|
-
"type": "string",
|
|
8
|
-
"description": "Command to execute. Allowed commands: upload <filepath> [displayName], delete <file_name>, get <file_name>, list, help."
|
|
9
|
-
}
|
|
10
|
-
},
|
|
11
|
-
"required": ["text"]
|
|
12
|
-
}
|
|
13
|
-
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Lightweight CLI-style wrapper around Gemini Files API.
|
|
6
|
-
* It exposes the most common operations (upload, list, get, delete)
|
|
7
|
-
* through a single `text` parameter that looks like a shell command.
|
|
8
|
-
*
|
|
9
|
-
* Examples
|
|
10
|
-
* upload ./assets/photo.jpg -> uploads the file
|
|
11
|
-
* upload ./assets/photo.jpg "My photo" -> uploads with display name
|
|
12
|
-
* list -> lists uploaded files (first page)
|
|
13
|
-
* get files/abc123def -> shows metadata for a file
|
|
14
|
-
* delete files/abc123def -> deletes a file
|
|
15
|
-
* help -> prints usage
|
|
16
|
-
*
|
|
17
|
-
* The function returns a human-readable string describing the result
|
|
18
|
-
* (or JSON pretty-printed for list/get/upload responses).
|
|
19
|
-
*/
|
|
20
|
-
export default async function geminiFilesCli({ text }, ctx) {
|
|
21
|
-
// -------------------------------------------------------------
|
|
22
|
-
// 1. Helpers
|
|
23
|
-
// -------------------------------------------------------------
|
|
24
|
-
const usage = () => `Gemini Files CLI
|
|
25
|
-
Usage:
|
|
26
|
-
upload <filepath> [displayName] Upload local file and return metadata
|
|
27
|
-
delete <file_name> Delete file by name/id (e.g. files/abc123)
|
|
28
|
-
get <file_name> Show metadata for file (e.g. files/abc123)
|
|
29
|
-
list List uploaded files (first page)
|
|
30
|
-
help Show this message
|
|
31
|
-
Examples:
|
|
32
|
-
upload ./samples/song.mp3
|
|
33
|
-
upload ./samples/doc.pdf "My Doc"
|
|
34
|
-
delete files/abc123def
|
|
35
|
-
get files/abc123def
|
|
36
|
-
list
|
|
37
|
-
`;
|
|
38
|
-
|
|
39
|
-
const EXTENSION_TO_MIME = {
|
|
40
|
-
// Documents
|
|
41
|
-
'.pdf': 'application/pdf',
|
|
42
|
-
'.js': 'application/x-javascript',
|
|
43
|
-
'.mjs': 'application/x-javascript',
|
|
44
|
-
'.cjs': 'application/x-javascript',
|
|
45
|
-
'.py': 'text/x-python',
|
|
46
|
-
'.txt': 'text/plain',
|
|
47
|
-
'.html': 'text/html',
|
|
48
|
-
'.htm': 'text/html',
|
|
49
|
-
'.css': 'text/css',
|
|
50
|
-
'.md': 'text/markdown',
|
|
51
|
-
'.markdown': 'text/markdown',
|
|
52
|
-
'.csv': 'text/csv',
|
|
53
|
-
'.xml': 'text/xml',
|
|
54
|
-
'.rtf': 'text/rtf',
|
|
55
|
-
|
|
56
|
-
// Images
|
|
57
|
-
'.png': 'image/png',
|
|
58
|
-
'.jpg': 'image/jpeg',
|
|
59
|
-
'.jpeg': 'image/jpeg',
|
|
60
|
-
'.webp': 'image/webp',
|
|
61
|
-
'.heic': 'image/heic',
|
|
62
|
-
'.heif': 'image/heif',
|
|
63
|
-
|
|
64
|
-
// Videos
|
|
65
|
-
'.mp4': 'video/mp4',
|
|
66
|
-
'.mpeg': 'video/mpeg',
|
|
67
|
-
'.mpg': 'video/mpeg',
|
|
68
|
-
'.mov': 'video/quicktime',
|
|
69
|
-
'.avi': 'video/avi',
|
|
70
|
-
'.flv': 'video/x-flv',
|
|
71
|
-
'.webm': 'video/webm',
|
|
72
|
-
'.wmv': 'video/x-ms-wmv',
|
|
73
|
-
'.3gp': 'video/3gpp',
|
|
74
|
-
'.3gpp': 'video/3gpp',
|
|
75
|
-
|
|
76
|
-
// Audio
|
|
77
|
-
'.wav': 'audio/wav',
|
|
78
|
-
'.mp3': 'audio/mpeg',
|
|
79
|
-
'.aiff': 'audio/aiff',
|
|
80
|
-
'.aac': 'audio/aac',
|
|
81
|
-
'.ogg': 'audio/ogg',
|
|
82
|
-
'.flac': 'audio/flac'
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
function pretty(json) {
|
|
86
|
-
return JSON.stringify(json, null, 2);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// -------------------------------------------------------------
|
|
90
|
-
// 2. Setup / validation
|
|
91
|
-
// -------------------------------------------------------------
|
|
92
|
-
if (!text || typeof text !== 'string' || text.trim() === '') {
|
|
93
|
-
return usage();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const key = await ctx.read('GEMINI_KEY');
|
|
97
|
-
if (!key) {
|
|
98
|
-
throw new Error('GEMINI_KEY not found in environment. Set it in your .env file.');
|
|
99
|
-
} /**
|
|
100
|
-
* Split cmd line into args (spaces outside quotes don't break up arguments)
|
|
101
|
-
* e.g. upload "foo bar.mp4" "My Title" -> [upload, foo bar.mp4, My Title]
|
|
102
|
-
*/
|
|
103
|
-
function parseArgsWithQuotes(input) {
|
|
104
|
-
const re = /"([^"]*)"|'([^']*)'|`([^`]*)`|([^\s]+)/g;
|
|
105
|
-
const args = [];
|
|
106
|
-
let match;
|
|
107
|
-
while ((match = re.exec(input))) {
|
|
108
|
-
args.push(match[1] || match[2] || match[3] || match[4]);
|
|
109
|
-
}
|
|
110
|
-
return args;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const args = parseArgsWithQuotes(text.trim());
|
|
114
|
-
const [cmd, ...rest] = args;
|
|
115
|
-
const command = cmd?.toLowerCase();
|
|
116
|
-
|
|
117
|
-
// -------------------------------------------------------------
|
|
118
|
-
// 3. Command handlers
|
|
119
|
-
// -------------------------------------------------------------
|
|
120
|
-
switch (command) {
|
|
121
|
-
case 'help':
|
|
122
|
-
return usage();
|
|
123
|
-
|
|
124
|
-
case 'list': {
|
|
125
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/files?key=${key}`;
|
|
126
|
-
const resp = await fetch(url);
|
|
127
|
-
if (!resp.ok) {
|
|
128
|
-
throw new Error(`Gemini API list failed (${resp.status}): ${await resp.text()}`);
|
|
129
|
-
}
|
|
130
|
-
const json = await resp.json();
|
|
131
|
-
return pretty(json);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
case 'get': {
|
|
135
|
-
if (rest.length === 0) {
|
|
136
|
-
return 'Error: "get" requires a file name.\n' + usage();
|
|
137
|
-
}
|
|
138
|
-
const name = rest[0];
|
|
139
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/${name}?key=${key}`;
|
|
140
|
-
const resp = await fetch(url);
|
|
141
|
-
if (!resp.ok) {
|
|
142
|
-
throw new Error(`Gemini API get failed (${resp.status}): ${await resp.text()}`);
|
|
143
|
-
}
|
|
144
|
-
const json = await resp.json();
|
|
145
|
-
return pretty(json);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
case 'delete': {
|
|
149
|
-
if (rest.length === 0) {
|
|
150
|
-
return 'Error: "delete" requires a file name.\n' + usage();
|
|
151
|
-
}
|
|
152
|
-
const name = rest[0];
|
|
153
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/${name}?key=${key}`;
|
|
154
|
-
const resp = await fetch(url, { method: 'DELETE' });
|
|
155
|
-
if (!resp.ok) {
|
|
156
|
-
throw new Error(`Gemini API delete failed (${resp.status}): ${await resp.text()}`);
|
|
157
|
-
}
|
|
158
|
-
return `Deleted ${name}`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
case 'upload': {
|
|
162
|
-
if (rest.length === 0) {
|
|
163
|
-
return 'Error: "upload" requires a local file path.\n' + usage();
|
|
164
|
-
}
|
|
165
|
-
const filepath = rest[0];
|
|
166
|
-
const displayName = rest.slice(1).join(' ') || path.basename(filepath);
|
|
167
|
-
|
|
168
|
-
let fileBuffer;
|
|
169
|
-
let stats;
|
|
170
|
-
try {
|
|
171
|
-
stats = await fs.stat(filepath);
|
|
172
|
-
fileBuffer = await fs.readFile(filepath);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
throw new Error(`Cannot read file ${filepath}: ${err.message}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const ext = path.extname(filepath).toLowerCase();
|
|
178
|
-
const mimeType = EXTENSION_TO_MIME[ext] || 'application/octet-stream';
|
|
179
|
-
|
|
180
|
-
// 3.1 Perform raw upload.
|
|
181
|
-
const uploadUrl = `https://generativelanguage.googleapis.com/upload/v1beta/files?uploadType=media&key=${key}`;
|
|
182
|
-
const resp = await fetch(uploadUrl, {
|
|
183
|
-
method: 'POST',
|
|
184
|
-
headers: {
|
|
185
|
-
'Content-Type': mimeType,
|
|
186
|
-
'X-Goog-Upload-Protocol': 'raw',
|
|
187
|
-
'X-Goog-Upload-Header-Content-Length': stats.size.toString(),
|
|
188
|
-
// Attach metadata via JSON content header? Docs suggest passing metadata in body for resumable.
|
|
189
|
-
// For raw simple upload the display_name is auto-derived; we will ignore displayName here.
|
|
190
|
-
},
|
|
191
|
-
body: fileBuffer
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
if (!resp.ok) {
|
|
195
|
-
throw new Error(`Gemini API upload failed (${resp.status}): ${await resp.text()}`);
|
|
196
|
-
}
|
|
197
|
-
const json = await resp.json();
|
|
198
|
-
return pretty(json);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
default:
|
|
202
|
-
return `Unknown command: ${command}\n` + usage();
|
|
203
|
-
}
|
|
204
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"description": "Process a local file (image, audio, PDF, or text-based documents) using the Gemini API. Provide the file path and an instruction or question. Supported image types: PNG, JPEG, WEBP, HEIC, HEIF. Supported audio types: WAV, MP3, AIFF, AAC, OGG, FLAC. Supported document types: PDF, TXT, HTML, HTM, MD, PY, JS, JSON, CSS, JAVA, C, CS, CPP, GO, PHP, RB, TS, XML, YAML, CSV, TSV, RTF. or video mp4 etc mov",
|
|
3
|
-
"parameters": {
|
|
4
|
-
"type": "object",
|
|
5
|
-
"properties": {
|
|
6
|
-
"filename": {
|
|
7
|
-
"type": "string",
|
|
8
|
-
"description": "Path to the local file (image, audio, PDF, document)"
|
|
9
|
-
},
|
|
10
|
-
"model": {
|
|
11
|
-
"type": "string",
|
|
12
|
-
"description": "Gemini model to use for the query, default is gemini-2.0-flash"
|
|
13
|
-
},
|
|
14
|
-
"text": {
|
|
15
|
-
"type": "string",
|
|
16
|
-
"description": "Instruction or question about the file (e.g., 'Extract all text', 'Summarize this', 'Transcribe audio')"
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"required": ["filename", "text"]
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Process a local file (image, audio, document, etc.) using Gemini API
|
|
6
|
-
*
|
|
7
|
-
* @param {Object} params
|
|
8
|
-
* @param {string} params.filename - Local path to the file
|
|
9
|
-
* @param {string} params.text - Instruction or question about the file content (e.g. "Extract text", "Summarize", "Transcribe audio", "What is this image?")
|
|
10
|
-
* @param {Object} ctx - Tune context object
|
|
11
|
-
* @returns {Promise<string>} Result text from the API
|
|
12
|
-
*/
|
|
13
|
-
export default async function geminiFileProcessor({ filename, text, model }, ctx) {
|
|
14
|
-
const key = await ctx.read('GEMINI_KEY');
|
|
15
|
-
if (!key) {
|
|
16
|
-
throw new Error('GEMINI_KEY not found in environment. Please set it in your .env file.');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
model = model || "gemini-2.5-pro-preview-03-25";
|
|
20
|
-
|
|
21
|
-
const EXTENSION_TO_MIME = {
|
|
22
|
-
// Documents
|
|
23
|
-
'.pdf': 'application/pdf',
|
|
24
|
-
'.js': 'application/x-javascript',
|
|
25
|
-
'.mjs': 'application/x-javascript',
|
|
26
|
-
'.cjs': 'application/x-javascript',
|
|
27
|
-
'.py': 'text/x-python',
|
|
28
|
-
'.txt': 'text/plain',
|
|
29
|
-
'.html': 'text/html',
|
|
30
|
-
'.htm': 'text/html',
|
|
31
|
-
'.css': 'text/css',
|
|
32
|
-
'.md': 'text/md',
|
|
33
|
-
'.markdown': 'text/md',
|
|
34
|
-
'.csv': 'text/csv',
|
|
35
|
-
'.xml': 'text/xml',
|
|
36
|
-
'.rtf': 'text/rtf',
|
|
37
|
-
|
|
38
|
-
// Images
|
|
39
|
-
'.png': 'image/png',
|
|
40
|
-
'.jpg': 'image/jpeg',
|
|
41
|
-
'.jpeg': 'image/jpeg',
|
|
42
|
-
'.webp': 'image/webp',
|
|
43
|
-
'.heic': 'image/heic',
|
|
44
|
-
'.heif': 'image/heif',
|
|
45
|
-
|
|
46
|
-
// Videos
|
|
47
|
-
'.mp4': 'video/mp4',
|
|
48
|
-
'.mpeg': 'video/mpeg',
|
|
49
|
-
'.mpg': 'video/mpg',
|
|
50
|
-
'.mov': 'video/mov',
|
|
51
|
-
'.avi': 'video/avi',
|
|
52
|
-
'.flv': 'video/x-flv',
|
|
53
|
-
'.webm': 'video/webm',
|
|
54
|
-
'.wmv': 'video/wmv',
|
|
55
|
-
'.3gp': 'video/3gpp',
|
|
56
|
-
'.3gpp': 'video/3gpp',
|
|
57
|
-
|
|
58
|
-
// Audio
|
|
59
|
-
'.wav': 'audio/wav',
|
|
60
|
-
'.mp3': 'audio/mp3',
|
|
61
|
-
'.aiff': 'audio/aiff',
|
|
62
|
-
'.aac': 'audio/aac',
|
|
63
|
-
'.ogg': 'audio/ogg',
|
|
64
|
-
'.flac': 'audio/flac'
|
|
65
|
-
};
|
|
66
|
-
// Determine if "filename" is a local path or a Gemini Files reference/URI
|
|
67
|
-
const isRemote = /^https?:\/\//i.test(filename) || filename.startsWith('files/');
|
|
68
|
-
|
|
69
|
-
const FILE_SIZE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
|
|
70
|
-
let parts;
|
|
71
|
-
let mimeType;
|
|
72
|
-
|
|
73
|
-
if (isRemote) {
|
|
74
|
-
// ---------------------- Remote (Gemini Files) ----------------------
|
|
75
|
-
// Extract the canonical file name ("files/abc123") so we can query metadata
|
|
76
|
-
let fileName = filename;
|
|
77
|
-
if (!fileName.startsWith('files/')) {
|
|
78
|
-
// try to extract between /files/ and the next /
|
|
79
|
-
const m = filename.match(/\/files\/([^/?]+)/i);
|
|
80
|
-
if (!m) {
|
|
81
|
-
throw new Error('Could not extract Gemini file name (files/ID) from URI');
|
|
82
|
-
}
|
|
83
|
-
fileName = `files/${m[1]}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Poll until state == ACTIVE (max 60s)
|
|
87
|
-
const metadataEndpoint = `https://generativelanguage.googleapis.com/v1beta/${fileName}`;
|
|
88
|
-
const deadline = Date.now() + 60000;
|
|
89
|
-
let fileMeta;
|
|
90
|
-
while (Date.now() < deadline) {
|
|
91
|
-
const resp = await fetch(`${metadataEndpoint}?key=${key}`);
|
|
92
|
-
if (!resp.ok) {
|
|
93
|
-
const errTxt = await resp.text();
|
|
94
|
-
throw new Error(`Failed to fetch file metadata (${resp.status}): ${errTxt}`);
|
|
95
|
-
}
|
|
96
|
-
const json = await resp.json();
|
|
97
|
-
fileMeta = json.file || json;
|
|
98
|
-
if (fileMeta.state === 'ACTIVE') break;
|
|
99
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
100
|
-
}
|
|
101
|
-
if (!fileMeta || fileMeta.state !== 'ACTIVE') {
|
|
102
|
-
throw new Error(`File ${fileName} is not ACTIVE (state=${fileMeta?.state}). Try again later.`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const fileUri = fileMeta.uri || fileMeta.fileUri;
|
|
106
|
-
mimeType = fileMeta.mimeType || fileMeta.mime_type;
|
|
107
|
-
if (!fileUri || !mimeType) {
|
|
108
|
-
throw new Error(`Could not determine uri/mimeType from metadata: ${JSON.stringify(fileMeta)}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
parts = [
|
|
112
|
-
{ file_data: { mime_type: mimeType, file_uri: fileUri } },
|
|
113
|
-
{ text }
|
|
114
|
-
];
|
|
115
|
-
} else {
|
|
116
|
-
// --------------------------- Local file ---------------------------
|
|
117
|
-
const ext = path.extname(filename).toLowerCase();
|
|
118
|
-
mimeType = EXTENSION_TO_MIME[ext];
|
|
119
|
-
if (!mimeType) {
|
|
120
|
-
throw new Error(`Unsupported or unknown file extension: ${ext}. Gemini only supports specific MIME types.`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Get file size
|
|
124
|
-
let stats;
|
|
125
|
-
try {
|
|
126
|
-
stats = await fs.stat(filename);
|
|
127
|
-
} catch (err) {
|
|
128
|
-
throw new Error(`Error stating file ${filename}: ${err.message}`);
|
|
129
|
-
}
|
|
130
|
-
const fileSize = stats.size;
|
|
131
|
-
|
|
132
|
-
if (fileSize > FILE_SIZE_THRESHOLD) {
|
|
133
|
-
throw new Error(`File ${filename} is larger than 10 MB. Please upload it first using gemini_files tool and then pass its uri/name here.`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let fileBuffer;
|
|
137
|
-
try {
|
|
138
|
-
fileBuffer = await fs.readFile(filename);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
throw new Error(`Error reading file ${filename}: ${err.message}`);
|
|
141
|
-
}
|
|
142
|
-
const encodedData = fileBuffer.toString('base64');
|
|
143
|
-
parts = [
|
|
144
|
-
{ inline_data: { mime_type: mimeType, data: encodedData } },
|
|
145
|
-
{ text }
|
|
146
|
-
];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const body = {
|
|
150
|
-
contents: [{ parts }]
|
|
151
|
-
// generationConfig can be added here if needed
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
|
|
155
|
-
|
|
156
|
-
let response;
|
|
157
|
-
try {
|
|
158
|
-
response = await fetch(url, {
|
|
159
|
-
method: 'POST',
|
|
160
|
-
headers: { 'Content-Type': 'application/json' },
|
|
161
|
-
body: JSON.stringify(body)
|
|
162
|
-
});
|
|
163
|
-
} catch (err) {
|
|
164
|
-
throw new Error(`Network error calling Gemini API: ${err.message}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!response.ok) {
|
|
168
|
-
const errorText = await response.text();
|
|
169
|
-
let detailedError = errorText;
|
|
170
|
-
try {
|
|
171
|
-
// Try to parse the JSON error response from Gemini for better details
|
|
172
|
-
const errorJson = JSON.parse(errorText);
|
|
173
|
-
detailedError = JSON.stringify(errorJson.error || errorJson, null, 2);
|
|
174
|
-
} catch (parseError) {
|
|
175
|
-
// Ignore if parsing fails, just use the raw text
|
|
176
|
-
}
|
|
177
|
-
throw new Error(`Gemini API Error ${response.status}: ${detailedError}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const json = await response.json();
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
// Extract the response text, handling potential variations in structure
|
|
184
|
-
const candidates = json?.candidates;
|
|
185
|
-
if (!candidates || candidates.length === 0) {
|
|
186
|
-
return `Gemini API returned no candidates. Full response: ${JSON.stringify(json, null, 2)}`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Check for issues like finishReason other than "STOP" or safety blocks
|
|
190
|
-
const candidate = candidates[0];
|
|
191
|
-
const finishReason = candidate?.finishReason;
|
|
192
|
-
const safetyRatings = candidate?.safetyRatings;
|
|
193
|
-
|
|
194
|
-
if (finishReason && finishReason !== "STOP" && finishReason !== "MAX_TOKENS") {
|
|
195
|
-
return `Gemini API finished prematurely. Reason: ${finishReason}. Safety Ratings: ${JSON.stringify(safetyRatings)}. Full response: ${JSON.stringify(json, null, 2)}`;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const content = candidate?.content;
|
|
199
|
-
if (!content || !content.parts || content.parts.length === 0) {
|
|
200
|
-
// If no parts but finished normally, might be an empty response or an issue
|
|
201
|
-
return `Gemini API returned no text parts (Finish Reason: ${finishReason}). Full response: ${JSON.stringify(json, null, 2)}`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Concatenate text from all parts
|
|
205
|
-
return content.parts.map(p => p.text).join('\n').trim();
|
|
206
|
-
|
|
207
|
-
} catch (e) {
|
|
208
|
-
console.error("Error processing Gemini response:", e);
|
|
209
|
-
return `Error processing response structure. Full response: ${JSON.stringify(json, null, 2)}`;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"description": "Generate speech from text using Gemini's Text-to-Speech API. Supports both single-speaker and multi-speaker audio generation with controllable style, tone, accent, and pace using natural language prompts.",
|
|
3
|
-
"parameters": {
|
|
4
|
-
"type": "object",
|
|
5
|
-
"properties": {
|
|
6
|
-
"text": {
|
|
7
|
-
"type": "string",
|
|
8
|
-
"description": "Text to convert to speech. For single-speaker, you can include style instructions like 'Say cheerfully: Have a wonderful day!' For multi-speaker, format as dialogue with speaker names, e.g., 'TTS the following conversation between Joe and Jane: Joe: How's it going? Jane: Not too bad!'"
|
|
9
|
-
},
|
|
10
|
-
"filename": {
|
|
11
|
-
"type": "string",
|
|
12
|
-
"description": "Output filename for the audio file. Will automatically add .wav extension if not present."
|
|
13
|
-
},
|
|
14
|
-
"model": {
|
|
15
|
-
"type": "string",
|
|
16
|
-
"enum": ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"],
|
|
17
|
-
"description": "Gemini model to use for TTS generation. Default is gemini-2.5-flash-preview-tts"
|
|
18
|
-
},
|
|
19
|
-
"voice": {
|
|
20
|
-
"type": "string",
|
|
21
|
-
"enum": [
|
|
22
|
-
"Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda",
|
|
23
|
-
"Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus", "Iapetus",
|
|
24
|
-
"Umbriel", "Algieba", "Despina", "Erinome", "Algenib", "Rasalgethi",
|
|
25
|
-
"Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux", "Pulcherrima",
|
|
26
|
-
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager", "Sulafat"
|
|
27
|
-
],
|
|
28
|
-
"description": "Voice name for single-speaker mode. Each voice has different characteristics: Zephyr (Bright), Puck (Upbeat), Charon (Informative), Kore (Firm), etc. Only used when speakers array is not provided."
|
|
29
|
-
},
|
|
30
|
-
"speakers": {
|
|
31
|
-
"type": "array",
|
|
32
|
-
"description": "Array of speaker configurations for multi-speaker mode (max 2 speakers). Each speaker should have 'name' and 'voice' properties. Speaker names must match those used in the text dialogue.",
|
|
33
|
-
"items": {
|
|
34
|
-
"type": "object",
|
|
35
|
-
"properties": {
|
|
36
|
-
"name": {
|
|
37
|
-
"type": "string",
|
|
38
|
-
"description": "Name of the speaker as used in the dialogue text"
|
|
39
|
-
},
|
|
40
|
-
"voice": {
|
|
41
|
-
"type": "string",
|
|
42
|
-
"enum": [
|
|
43
|
-
"Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda",
|
|
44
|
-
"Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus", "Iapetus",
|
|
45
|
-
"Umbriel", "Algieba", "Despina", "Erinome", "Algenib", "Rasalgethi",
|
|
46
|
-
"Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux", "Pulcherrima",
|
|
47
|
-
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager", "Sulafat"
|
|
48
|
-
],
|
|
49
|
-
"description": "Voice name for this speaker"
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
"required": ["name", "voice"]
|
|
53
|
-
},
|
|
54
|
-
"maxItems": 2
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
"required": ["text", "filename"]
|
|
58
|
-
}
|
|
59
|
-
}
|