skillfree 0.1.2 → 0.1.4

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/SKILL.md CHANGED
@@ -2,36 +2,85 @@
2
2
 
3
3
  ## 功能
4
4
 
5
- SkillFree 是一个统一的 AI 能力调用层。通过一个 API Key,调用图像生成、语音合成、视频生成、智能对话等多种 AI 能力。
5
+ SkillFree 是一个统一的 AI 能力调用层。通过一个 API Key,调用对话、图像生成、语音合成、视频生成、音乐生成、文档 OCR、向量 Embedding 等 56 个模型。
6
6
 
7
7
  ## 使用方式
8
8
 
9
+ ### 对话(chat)
9
10
  ```bash
10
- # 对话(1 积分)
11
11
  skillfree pilot --type chat --prompt "帮我写一首诗"
12
+ skillfree pilot --type chat --model claude-sonnet-4-6 --prompt "分析一下最近的 AI 趋势"
13
+ ```
12
14
 
13
- # 图像生成(5 积分)
15
+ ### 图像生成(image)
16
+ ```bash
14
17
  skillfree pilot --type image --prompt "赛博朋克的上海" --output ./out.png
18
+ skillfree pilot --type image --model doubao-seedream-5.0-lite --prompt "一只龙虾" --output ./lobster.png
19
+ ```
15
20
 
16
- # 语音合成(2 积分)
21
+ ### 语音合成(tts)
22
+ ```bash
17
23
  skillfree pilot --type tts --text "你好,龙虾" --output hello.mp3
24
+ ```
18
25
 
19
- # 视频生成(20 积分)
26
+ ### 视频生成(video)
27
+ ```bash
20
28
  skillfree pilot --type video --prompt "海浪拍打礁石" --output ./video.mp4
21
29
  ```
22
30
 
23
- ## 配置
31
+ ### 音乐生成(music)
32
+ ```bash
33
+ skillfree pilot --type music --prompt "轻松欢快的背景音乐" --output ./bgm.mp3
34
+ ```
24
35
 
36
+ ### 查看所有模型
25
37
  ```bash
26
- export SKILLFREE_API_KEY=sk-sf-xxxxxxxx
38
+ skillfree models
27
39
  ```
28
40
 
29
- 或通过登录:
41
+ ### 查看余额
42
+ ```bash
43
+ skillfree balance
44
+ ```
45
+
46
+ ## 积分参考(2026-03 定价)
47
+
48
+ | 类型 | 模型 | 积分/次 |
49
+ |------|------|---------|
50
+ | chat | DeepSeek V3.2 Fast | 1 |
51
+ | chat | Kimi K2.5 | 1 |
52
+ | chat | Gemini 2.5 Pro | 7 |
53
+ | chat | GPT-5.4 | 9 |
54
+ | chat | Claude Sonnet 4.6 | 12 |
55
+ | chat | Claude Opus 4.6 | 59 |
56
+ | image | 即梦 Seedream 5.0 Lite | 11 |
57
+ | image | Gemini 3 Pro Image | 54 |
58
+ | tts | MiniMax TTS 2.8 HD | 18 |
59
+ | video | 拍我 5.6 文生视频 | 160 |
60
+ | video | 可灵 2.6 文生视频 | 427 |
61
+ | music | MiniMax Music 2.5 | 80 |
62
+ | music | Suno V5 | 107 |
63
+ | ocr | 合合图文混排解析 | 3 |
64
+ | embedding | 豆包 Embedding Vision | 1 |
65
+
66
+ 最新定价以 https://skillfree.tech/app/models 为准。
67
+
68
+ ## 配置
30
69
 
31
70
  ```bash
71
+ # 方式一:登录(推荐)
32
72
  skillfree auth login
73
+
74
+ # 方式二:环境变量
75
+ export SKILLFREE_API_KEY=sk-sf-xxxxxxxx
76
+
77
+ # 方式三:直接调用 API
78
+ curl https://skillfree.tech/v1/chat/completions \
79
+ -H "Authorization: Bearer sk-sf-xxxxxxxx" \
80
+ -H "Content-Type: application/json" \
81
+ -d '{"model":"DeepSeek-V3.2-Fast","messages":[{"role":"user","content":"你好"}]}'
33
82
  ```
34
83
 
35
84
  ## 充值
36
85
 
37
- 充多少用多少,积分永不过期:https://skillfree.tech/billing
86
+ 充多少用多少,积分永不过期:https://skillfree.tech/app/billing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillfree",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "🦞 一个 API,满足所有龙虾技能需求",
5
5
  "main": "bin/skillfree.js",
6
6
  "bin": {
@@ -1,100 +1,271 @@
1
- const { post, postStream, BASE_URL } = require('../lib/client')
1
+ const { post, postStream, get, request, getApiKey, BASE_URL } = require('../lib/client')
2
2
  const fs = require('fs')
3
3
  const path = require('path')
4
4
 
5
+ // ─── 工具:下载文件并保存 ──────────────────────────────────────────────────────
6
+ async function downloadAndSave(url, output) {
7
+ const resp = await fetch(url)
8
+ if (!resp.ok) throw new Error(`下载失败: ${resp.status}`)
9
+ fs.writeFileSync(output, Buffer.from(await resp.arrayBuffer()))
10
+ console.log(`✅ 已保存到 ${output}`)
11
+ }
12
+
13
+ // ─── 工具:封装 PCM 为 WAV ─────────────────────────────────────────────────────
14
+ function pcmToWav(pcmBytes, sampleRate = 24000, channels = 1, bitsPerSample = 16) {
15
+ const dataSize = pcmBytes.length
16
+ const wav = Buffer.alloc(44 + dataSize)
17
+ wav.write('RIFF', 0); wav.writeUInt32LE(36 + dataSize, 4); wav.write('WAVE', 8)
18
+ wav.write('fmt ', 12); wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20)
19
+ wav.writeUInt16LE(channels, 22); wav.writeUInt32LE(sampleRate, 24)
20
+ wav.writeUInt32LE(sampleRate * channels * bitsPerSample / 8, 28)
21
+ wav.writeUInt16LE(channels * bitsPerSample / 8, 32); wav.writeUInt16LE(bitsPerSample, 34)
22
+ wav.write('data', 36); wav.writeUInt32LE(dataSize, 40)
23
+ pcmBytes.copy(wav, 44)
24
+ return wav
25
+ }
26
+
5
27
  async function pilot(flags) {
6
28
  const type = flags.type || 'chat'
7
29
  const prompt = flags.prompt || flags.text || ''
8
30
  const output = flags.output || null
31
+ const model = flags.model || null
9
32
 
10
- // 构造请求体
11
- const body = { type, inputs: {} }
12
-
13
- switch (type) {
14
- case 'chat':
15
- body.model = flags.model
16
- body.inputs.messages = [{ role: 'user', content: prompt }]
17
- break
18
- case 'image':
19
- body.model = flags.model
20
- body.inputs.prompt = prompt
21
- if (flags.size) body.inputs.size = flags.size
22
- break
23
- case 'tts':
24
- body.model = flags.model
25
- body.inputs.text = prompt
26
- body.inputs.input = prompt
27
- body.inputs.voice = flags.voice || 'nova'
28
- break
29
- case 'stt':
30
- if (flags.file) {
31
- body.inputs.audio_data = fs.readFileSync(path.resolve(flags.file)).toString('base64')
32
- body.inputs.filename = path.basename(flags.file)
33
+ // ── CHAT ─────────────────────────────────────────────────────────────────────
34
+ if (type === 'chat') {
35
+ if (!output) {
36
+ // 流式输出
37
+ const res = await postStream('/chat/completions', {
38
+ model: model || 'DeepSeek-V3.2-Fast',
39
+ messages: [{ role: 'user', content: prompt }],
40
+ stream: true,
41
+ })
42
+ const reader = res.body.getReader()
43
+ const decoder = new TextDecoder()
44
+ let buf = ''
45
+ process.stdout.write('\n')
46
+ while (true) {
47
+ const { done, value } = await reader.read()
48
+ if (done) break
49
+ buf += decoder.decode(value, { stream: true })
50
+ const lines = buf.split('\n')
51
+ buf = lines.pop()
52
+ for (const line of lines) {
53
+ if (line.startsWith('data: ')) {
54
+ const chunk = line.slice(6)
55
+ if (chunk === '[DONE]') { process.stdout.write('\n'); return }
56
+ try {
57
+ const d = JSON.parse(chunk)
58
+ const text = d.choices?.[0]?.delta?.content
59
+ if (text) process.stdout.write(text)
60
+ } catch {}
61
+ }
62
+ }
33
63
  }
34
- break
35
- case 'video':
36
- body.model = flags.model
37
- body.inputs.prompt = prompt
38
- body.inputs.duration = flags.duration ? parseInt(flags.duration) : 5
39
- break
40
- case 'music':
41
- body.model = flags.model
42
- body.inputs.prompt = prompt
43
- break
44
- default:
45
- body.inputs.prompt = prompt
64
+ return
65
+ }
66
+ const result = await post('/chat/completions', {
67
+ model: model || 'DeepSeek-V3.2-Fast',
68
+ messages: [{ role: 'user', content: prompt }],
69
+ })
70
+ const text = result.choices?.[0]?.message?.content || JSON.stringify(result, null, 2)
71
+ if (output) { fs.writeFileSync(output, text); console.log(`✅ 已保存到 ${output}`) }
72
+ else console.log(text)
73
+ return
46
74
  }
47
75
 
48
- // 流式输出(chat 默认流式)
49
- if (type === 'chat' && !output) {
50
- const res = await postStream('/chat/completions', {
51
- model: flags.model || 'DeepSeek-V3.2-Fast',
52
- messages: [{ role: 'user', content: prompt }],
53
- stream: true,
76
+ // ── IMAGE ─────────────────────────────────────────────────────────────────────
77
+ if (type === 'image') {
78
+ const res = await post('/images/generations', {
79
+ model: model || 'gemini-3.1-flash-image-preview',
80
+ prompt,
81
+ n: 1,
82
+ size: flags.size || '1024x1024',
54
83
  })
84
+ if (res.error) throw new Error(res.error.message || JSON.stringify(res.error))
85
+ const url = res.data?.[0]?.url || res.data?.[0]?.b64_json
86
+ if (!url) throw new Error('未返回图像数据: ' + JSON.stringify(res).slice(0, 200))
87
+ if (output) {
88
+ if (url.startsWith('http')) {
89
+ await downloadAndSave(url, output)
90
+ } else {
91
+ fs.writeFileSync(output, Buffer.from(url, 'base64'))
92
+ console.log(`✅ 已保存到 ${output}`)
93
+ }
94
+ } else {
95
+ console.log('图像 URL:', url)
96
+ }
97
+ return
98
+ }
55
99
 
56
- const reader = res.body.getReader()
57
- const decoder = new TextDecoder()
58
- let buf = ''
59
- process.stdout.write('\n')
60
- while (true) {
61
- const { done, value } = await reader.read()
62
- if (done) break
63
- buf += decoder.decode(value, { stream: true })
64
- const lines = buf.split('\n')
65
- buf = lines.pop()
66
- for (const line of lines) {
67
- if (line.startsWith('data: ')) {
68
- const chunk = line.slice(6)
69
- if (chunk === '[DONE]') { process.stdout.write('\n'); return }
70
- try {
71
- const d = JSON.parse(chunk)
72
- const text = d.choices?.[0]?.delta?.content
73
- if (text) process.stdout.write(text)
74
- } catch {}
75
- }
100
+ // ── TTS ───────────────────────────────────────────────────────────────────────
101
+ if (type === 'tts') {
102
+ const ttsModel = model || 'speech-2.6-hd'
103
+ const text = prompt
104
+
105
+ if (ttsModel === 'speech-2.8-hd' || ttsModel === 'minimax-clone-lastversion') {
106
+ // /v1/responses,返回 hex 音频
107
+ const res = await request('/responses', {
108
+ method: 'POST',
109
+ body: JSON.stringify({
110
+ model: ttsModel,
111
+ input: text,
112
+ stream: false,
113
+ voice_setting: {
114
+ voice_id: flags.voice || 'female-shaonv',
115
+ speed: 1, vol: 1, pitch: 0, emotion: 'fluent',
116
+ },
117
+ audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3', channel: 1 },
118
+ output_format: 'hex',
119
+ }),
120
+ })
121
+ const data = await res.json()
122
+ if (!data.data?.audio) throw new Error(JSON.stringify(data).slice(0, 200))
123
+ const audioBuf = Buffer.from(data.data.audio, 'hex')
124
+ if (output) { fs.writeFileSync(output, audioBuf); console.log(`✅ 已保存到 ${output}(${audioBuf.length} bytes)`) }
125
+ else console.log(`✅ TTS 成功,时长约 ${(data.extra_info?.audio_length / 1000).toFixed(1)} 秒`)
126
+
127
+ } else if (ttsModel === 'gemini-2.5-pro-preview-tts' || ttsModel === 'gemini-2.5-flash-preview-tts') {
128
+ // Gemini TTS:走 /v1beta/models/:id:generateContent,认证无 Bearer
129
+ const apiKey = getApiKey()
130
+ const res = await fetch(BASE_URL.replace('skillfree.tech', 'dmxapi.cn') + '/v1beta/models/' + ttsModel + ':generateContent', {
131
+ method: 'POST',
132
+ headers: { 'Authorization': apiKey, 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({
134
+ contents: [{ parts: [{ text }] }],
135
+ generationConfig: {
136
+ responseModalities: ['AUDIO'],
137
+ speechConfig: {
138
+ voiceConfig: { prebuiltVoiceConfig: { voiceName: flags.voice || 'Kore' } }
139
+ }
140
+ }
141
+ }),
142
+ })
143
+ const data = await res.json()
144
+ if (data.error) throw new Error(data.error.message || JSON.stringify(data.error))
145
+ const pcm = Buffer.from(data.candidates[0].content.parts[0].inlineData.data, 'base64')
146
+ const ext = (output || '').endsWith('.mp3') ? 'mp3' : 'wav'
147
+ const finalBuf = ext === 'wav' ? pcmToWav(pcm) : pcm
148
+ if (output) { fs.writeFileSync(output, finalBuf); console.log(`✅ 已保存到 ${output}(${finalBuf.length} bytes)`) }
149
+ else console.log(`✅ Gemini TTS 成功,时长约 ${(pcm.length / (24000 * 2)).toFixed(1)} 秒`)
150
+
151
+ } else {
152
+ // 标准 OpenAI 兼容(speech-2.6-hd 等)
153
+ const res = await request('/audio/speech', {
154
+ method: 'POST',
155
+ body: JSON.stringify({
156
+ model: ttsModel,
157
+ input: text,
158
+ voice: flags.voice || 'female-shaonv',
159
+ }),
160
+ })
161
+ if (res.status !== 200) {
162
+ const err = await res.json()
163
+ throw new Error(err.error?.message || JSON.stringify(err))
76
164
  }
165
+ const buf = Buffer.from(await res.arrayBuffer())
166
+ if (output) { fs.writeFileSync(output, buf); console.log(`✅ 已保存到 ${output}(${buf.length} bytes)`) }
167
+ else console.log(`✅ TTS 成功,${buf.length} bytes`)
77
168
  }
78
169
  return
79
170
  }
80
171
 
81
- // 非流式
82
- const result = await post('/pilot', body)
172
+ // ── MUSIC ─────────────────────────────────────────────────────────────────────
173
+ if (type === 'music') {
174
+ const musicModel = model || 'chirp-v5'
175
+
176
+ if (musicModel === 'chirp-v5') {
177
+ // Suno 异步接口
178
+ const res = await request('/suno/submit/music', {
179
+ method: 'POST',
180
+ headers: { 'Accept': 'application/json' },
181
+ body: JSON.stringify({
182
+ gpt_description_prompt: prompt,
183
+ make_instrumental: true,
184
+ mv: 'chirp-v5',
185
+ notify_hook: '',
186
+ }),
187
+ })
188
+ const submitData = await res.json()
189
+ if (submitData.code !== 'success') throw new Error(submitData.message || JSON.stringify(submitData))
190
+ const taskId = submitData.data
191
+ console.log(`🎵 Suno 任务已提交,task_id: ${taskId},等待生成(约 60-90 秒)...`)
192
+
193
+ // 轮询结果
194
+ for (let i = 0; i < 15; i++) {
195
+ await new Promise(r => setTimeout(r, 10000))
196
+ const poll = await request('/suno/fetch/' + taskId, { method: 'GET' })
197
+ const result = await poll.json()
198
+ if (result.code !== 'success') throw new Error(result.message || JSON.stringify(result))
199
+ const status = result.data?.status
200
+ const progress = result.data?.progress || '0%'
201
+ process.stdout.write(`\r状态: ${status} 进度: ${progress} `)
202
+ if (status === 'SUCCESS') {
203
+ const songs = result.data?.data || []
204
+ process.stdout.write('\n')
205
+ console.log(`✅ 生成成功!共 ${songs.length} 首`)
206
+ songs.forEach((s, i) => {
207
+ console.log(` 歌曲${i+1}: ${s.title} (${Math.floor(s.duration/60)}m${Math.floor(s.duration%60)}s)`)
208
+ console.log(` 音频: ${s.audio_url}`)
209
+ })
210
+ // 如果指定 output,下载第一首
211
+ if (output && songs[0]?.audio_url) await downloadAndSave(songs[0].audio_url, output)
212
+ return
213
+ }
214
+ if (status === 'FAILED') throw new Error('Suno 任务失败: ' + JSON.stringify(result.data))
215
+ }
216
+ throw new Error('Suno 生成超时(150s),请手动查询 task_id: ' + taskId)
83
217
 
84
- // 如果有媒体 URL 且指定了输出文件,下载保存
85
- const mediaUrl = result.image_url || result.video_url || result.audio_url
86
- || result.data?.[0]?.url || result.data?.[0]
87
- if (output && mediaUrl) {
88
- const resp = await fetch(typeof mediaUrl === 'string' ? mediaUrl : mediaUrl.url)
89
- fs.writeFileSync(output, Buffer.from(await resp.arrayBuffer()))
90
- console.log(`✅ 已保存到 ${output}`)
218
+ } else {
219
+ // music-2.5(MiniMax)暂不支持(需要 lyrics 参数,建议直接调 /v1/responses)
220
+ throw new Error(`music-2.5 需要通过 /v1/responses 直接调用,请参考文档`)
221
+ }
222
+ }
223
+
224
+ // ── OCR ───────────────────────────────────────────────────────────────────────
225
+ if (type === 'ocr') {
226
+ let input = prompt
227
+ // 如果是本地文件路径,读取为 base64
228
+ if (flags.file && fs.existsSync(path.resolve(flags.file))) {
229
+ input = fs.readFileSync(path.resolve(flags.file)).toString('base64')
230
+ }
231
+ const res = await request('/responses', {
232
+ method: 'POST',
233
+ body: JSON.stringify({
234
+ model: model || 'hehe-tywd',
235
+ input,
236
+ parse_mode: 'auto',
237
+ dpi: 144,
238
+ page_start: 0,
239
+ page_count: 1000,
240
+ markdown_details: 1,
241
+ page_details: 0,
242
+ table_flavor: 'html',
243
+ }),
244
+ })
245
+ const data = await res.json()
246
+ if (data.code !== 200) throw new Error(data.message || JSON.stringify(data).slice(0, 200))
247
+ const markdown = data.result?.markdown || ''
248
+ if (output) { fs.writeFileSync(output, markdown); console.log(`✅ OCR 结果已保存到 ${output}`) }
249
+ else console.log(markdown)
250
+ return
251
+ }
252
+
253
+ // ── STT ───────────────────────────────────────────────────────────────────────
254
+ if (type === 'stt') {
255
+ if (!flags.file) throw new Error('--file 是必需的(音频文件路径)')
256
+ const audioBase64 = fs.readFileSync(path.resolve(flags.file)).toString('base64')
257
+ const result = await post('/v1/audio/transcriptions', {
258
+ model: 'whisper-1',
259
+ file: audioBase64,
260
+ filename: path.basename(flags.file),
261
+ })
262
+ const text = result.text || JSON.stringify(result, null, 2)
263
+ if (output) { fs.writeFileSync(output, text); console.log(`✅ 已保存到 ${output}`) }
264
+ else console.log(text)
91
265
  return
92
266
  }
93
267
 
94
- // 文本输出
95
- const text = result.choices?.[0]?.message?.content
96
- || result.text || result.transcript || JSON.stringify(result, null, 2)
97
- console.log(text)
268
+ throw new Error(`不支持的类型: ${type},可选: chat | image | tts | stt | music | ocr | video`)
98
269
  }
99
270
 
100
271
  module.exports = { pilot }
Binary file