pinokiod 7.2.0 → 7.2.1

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.
@@ -1,34 +1,336 @@
1
- const fs = require('fs')
2
- const fetch = require('cross-fetch')
3
- const { rimraf } = require('rimraf')
4
- const decompress = require('decompress');
1
+ const crypto = require("crypto")
2
+ const fs = require("fs")
3
+ const path = require("path")
4
+ const { execFile } = require("child_process")
5
+ const semver = require("semver")
6
+ const { rimraf } = require("rimraf")
7
+ const decompress = require("decompress")
8
+
9
+ const RELEASE_VERSION = "8.1"
10
+ const RELEASE_RANGE = ">=8.1.0 <8.2.0"
11
+
12
+ const ARTIFACTS = {
13
+ win32: {
14
+ x64: {
15
+ version: RELEASE_VERSION,
16
+ source: "Gyan Windows essentials build",
17
+ archives: [{
18
+ url: "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1-essentials_build.zip",
19
+ sha256: "8748283d821613d930b0e7be685aaa9df4ca6f0ad4d0c42fd02622b3623463c6",
20
+ binaries: {
21
+ ffmpeg: "ffmpeg.exe",
22
+ ffprobe: "ffprobe.exe"
23
+ }
24
+ }]
25
+ }
26
+ },
27
+ darwin: {
28
+ x64: {
29
+ version: RELEASE_VERSION,
30
+ source: "Martin Riedl macOS amd64 release",
31
+ archives: [{
32
+ url: "https://ffmpeg.martin-riedl.de/download/macos/amd64/1774556648_8.1/ffmpeg.zip",
33
+ sha256: "eaa8aa619f8eccc7f548a730097f5d299cbf2d418888421c137557344d821130",
34
+ binaries: {
35
+ ffmpeg: "ffmpeg"
36
+ }
37
+ }, {
38
+ url: "https://ffmpeg.martin-riedl.de/download/macos/amd64/1774556648_8.1/ffprobe.zip",
39
+ sha256: "221bd0716dc15daf5745c5503773e5c23264c10c5ea956aa17ef492bbc0b0ac7",
40
+ binaries: {
41
+ ffprobe: "ffprobe"
42
+ }
43
+ }]
44
+ },
45
+ arm64: {
46
+ version: RELEASE_VERSION,
47
+ source: "Martin Riedl macOS arm64 release",
48
+ archives: [{
49
+ url: "https://ffmpeg.martin-riedl.de/download/macos/arm64/1774549676_8.1/ffmpeg.zip",
50
+ sha256: "cc3a7e0cce36c5eca6c17eeb93830984c657637a8e710dc98f19c8051201fa3a",
51
+ binaries: {
52
+ ffmpeg: "ffmpeg"
53
+ }
54
+ }, {
55
+ url: "https://ffmpeg.martin-riedl.de/download/macos/arm64/1774549676_8.1/ffprobe.zip",
56
+ sha256: "fd2e6b7fad9c9aa2bec17c0d7211b5afcc00b4b5c9b63c120985e80c3c198af6",
57
+ binaries: {
58
+ ffprobe: "ffprobe"
59
+ }
60
+ }]
61
+ }
62
+ },
63
+ linux: {
64
+ x64: {
65
+ version: RELEASE_VERSION,
66
+ source: "Martin Riedl Linux amd64 release",
67
+ archives: [{
68
+ url: "https://ffmpeg.martin-riedl.de/download/linux/amd64/1774550169_8.1/ffmpeg.zip",
69
+ sha256: "49f9a3642387626f82fd70dd6a268807efe23e0560d6934a6531d6e3e668f18f",
70
+ binaries: {
71
+ ffmpeg: "ffmpeg"
72
+ }
73
+ }, {
74
+ url: "https://ffmpeg.martin-riedl.de/download/linux/amd64/1774550169_8.1/ffprobe.zip",
75
+ sha256: "422082501af33fabb3946d101d098e5105f44492e5a16357c3fac79421544b0e",
76
+ binaries: {
77
+ ffprobe: "ffprobe"
78
+ }
79
+ }]
80
+ },
81
+ arm64: {
82
+ version: RELEASE_VERSION,
83
+ source: "Martin Riedl Linux arm64 release",
84
+ archives: [{
85
+ url: "https://ffmpeg.martin-riedl.de/download/linux/arm64/1774548896_8.1/ffmpeg.zip",
86
+ sha256: "87000dd625a4f409a5baf71ac177c22210db04ea144e01241713ab196ed39689",
87
+ binaries: {
88
+ ffmpeg: "ffmpeg"
89
+ }
90
+ }, {
91
+ url: "https://ffmpeg.martin-riedl.de/download/linux/arm64/1774548896_8.1/ffprobe.zip",
92
+ sha256: "eb56a190dea6bdd08da2c1e63d7c7523817384eff4dff227f4b088e56205414b",
93
+ binaries: {
94
+ ffprobe: "ffprobe"
95
+ }
96
+ }]
97
+ }
98
+ }
99
+ }
100
+
5
101
  class Ffmpeg {
6
- description = "Installs FFmpeg for audio and video processing."
7
- cmd() {
8
- return "ffmpeg=7.0.2"
102
+ description = "Installs standalone FFmpeg and FFprobe binaries with MP3 export support."
103
+
104
+ artifact() {
105
+ const platform = this.kernel.platform
106
+ const arch = this.kernel.arch
107
+ const spec = ARTIFACTS[platform] && ARTIFACTS[platform][arch]
108
+ if (!spec) {
109
+ throw new Error(`Standalone FFmpeg is not configured for ${platform}/${arch}`)
110
+ }
111
+ return spec
112
+ }
113
+
114
+ rootPath() {
115
+ return this.kernel.bin.path("ffmpeg")
116
+ }
117
+
118
+ binDir() {
119
+ return this.kernel.bin.path("ffmpeg", "bin")
120
+ }
121
+
122
+ tempDir() {
123
+ return this.kernel.bin.path("ffmpeg-tmp")
124
+ }
125
+
126
+ manifestPath() {
127
+ return this.kernel.bin.path("ffmpeg", "INSTALL.json")
9
128
  }
129
+
130
+ binaryFilename(tool) {
131
+ return this.kernel.platform === "win32" ? `${tool}.exe` : tool
132
+ }
133
+
134
+ binaryPath(tool) {
135
+ return path.resolve(this.binDir(), this.binaryFilename(tool))
136
+ }
137
+
138
+ env() {
139
+ return {
140
+ PATH: [this.binDir()],
141
+ FFMPEG_PATH: this.binaryPath("ffmpeg"),
142
+ FFPROBE_PATH: this.binaryPath("ffprobe")
143
+ }
144
+ }
145
+
146
+ hasLegacyCondaFfmpeg() {
147
+ const condaInstalled = this.kernel.bin.installed && this.kernel.bin.installed.conda
148
+ return !!(condaInstalled && condaInstalled.has("ffmpeg"))
149
+ }
150
+
10
151
  async install(req, ondata) {
11
- await this.kernel.bin.exec({
12
- message: [
13
- "conda clean -y --all",
14
- `conda install -y -c conda-forge ${this.cmd()}`
15
- ]
16
- }, ondata)
152
+ const spec = this.artifact()
153
+ const rootPath = this.rootPath()
154
+ const tempDir = this.tempDir()
155
+ const binDir = this.binDir()
156
+ const downloads = []
157
+
158
+ ondata({ raw: `preparing standalone ffmpeg ${spec.version} (${this.kernel.platform}/${this.kernel.arch})...\r\n` })
159
+ await rimraf(tempDir)
160
+ await rimraf(rootPath)
161
+
162
+ try {
163
+ await fs.promises.mkdir(tempDir, { recursive: true })
164
+ await fs.promises.mkdir(binDir, { recursive: true })
165
+
166
+ for (let index = 0; index < spec.archives.length; index++) {
167
+ const archive = spec.archives[index]
168
+ const filename = `${this.kernel.platform}-${this.kernel.arch}-${index}-${path.basename(new URL(archive.url).pathname)}`
169
+ const archivePath = this.kernel.bin.path(filename)
170
+ const extractDir = path.resolve(tempDir, `archive-${index}`)
171
+
172
+ downloads.push(archivePath)
173
+ await this.kernel.bin.download(archive.url, filename, ondata)
174
+ await this.verifyChecksum(archivePath, archive.sha256)
175
+
176
+ ondata({ raw: `extracting ${filename}...\r\n` })
177
+ await fs.promises.mkdir(extractDir, { recursive: true })
178
+ await decompress(archivePath, extractDir)
179
+
180
+ for (const [tool, expectedName] of Object.entries(archive.binaries)) {
181
+ const source = await this.findFileByName(extractDir, expectedName)
182
+ if (!source) {
183
+ throw new Error(`Could not find ${expectedName} inside ${filename}`)
184
+ }
185
+ const destination = this.binaryPath(tool)
186
+ await fs.promises.copyFile(source, destination)
187
+ if (this.kernel.platform !== "win32") {
188
+ await fs.promises.chmod(destination, 0o755)
189
+ }
190
+ }
191
+ }
192
+
193
+ await this.writeManifest(spec)
194
+ await this.selfTest(ondata)
195
+ await this.ensureLegacyCondaFfmpegRemoved(ondata)
196
+ ondata({ raw: `ffmpeg ${spec.version} installed from ${spec.source}\r\n` })
197
+ } catch (error) {
198
+ await rimraf(rootPath)
199
+ throw error
200
+ } finally {
201
+ for (const download of downloads) {
202
+ await fs.promises.rm(download, { force: true }).catch(() => {})
203
+ }
204
+ await rimraf(tempDir)
205
+ }
17
206
  }
207
+
18
208
  async installed() {
19
- console.log("conda_versions", this.kernel.bin.installed.conda_versions)
20
- if (this.kernel.bin.installed.conda && this.kernel.bin.installed.conda_versions) {
21
- let version = this.kernel.bin.installed.conda_versions.ffmpeg
22
- if (version !== "7.0.2") {
209
+ try {
210
+ await fs.promises.access(this.binaryPath("ffmpeg"))
211
+ await fs.promises.access(this.binaryPath("ffprobe"))
212
+ if (this.hasLegacyCondaFfmpeg()) {
213
+ console.log("standalone ffmpeg installed check failed: legacy conda ffmpeg is still present")
23
214
  return false
24
215
  }
216
+ await this.selfTest()
217
+ return true
218
+ } catch (error) {
219
+ console.log("standalone ffmpeg installed check failed", error && error.message ? error.message : error)
220
+ return false
25
221
  }
26
- return this.kernel.bin.installed.conda.has("ffmpeg")
27
222
  }
223
+
28
224
  async uninstall(req, ondata) {
225
+ ondata({ raw: "cleaning up\r\n" })
226
+ await rimraf(this.rootPath())
227
+ await rimraf(this.tempDir())
228
+ ondata({ raw: "finished cleaning up\r\n" })
229
+ }
230
+
231
+ async selfTest(ondata) {
232
+ if (ondata) {
233
+ ondata({ raw: "verifying ffmpeg binaries...\r\n" })
234
+ }
235
+
236
+ const ffmpegVersionOutput = await this.execBinary(this.binaryPath("ffmpeg"), ["-version"])
237
+ const ffmpegVersion = semver.coerce(ffmpegVersionOutput)
238
+ if (!ffmpegVersion || !semver.satisfies(ffmpegVersion, RELEASE_RANGE)) {
239
+ throw new Error(`Unexpected ffmpeg version: ${this.firstLine(ffmpegVersionOutput)}`)
240
+ }
241
+
242
+ const encoderOutput = await this.execBinary(this.binaryPath("ffmpeg"), ["-hide_banner", "-encoders"])
243
+ if (!/\blibmp3lame\b/i.test(encoderOutput)) {
244
+ throw new Error("FFmpeg was installed without libmp3lame support")
245
+ }
246
+
247
+ const ffprobeVersionOutput = await this.execBinary(this.binaryPath("ffprobe"), ["-version"])
248
+ const ffprobeVersion = semver.coerce(ffprobeVersionOutput)
249
+ if (!ffprobeVersion || !semver.satisfies(ffprobeVersion, RELEASE_RANGE)) {
250
+ throw new Error(`Unexpected ffprobe version: ${this.firstLine(ffprobeVersionOutput)}`)
251
+ }
252
+
253
+ return true
254
+ }
255
+
256
+ async ensureLegacyCondaFfmpegRemoved(ondata) {
257
+ await this.kernel.bin.refreshInstalled()
258
+ if (!this.hasLegacyCondaFfmpeg()) {
259
+ return
260
+ }
261
+
262
+ ondata({ raw: "removing legacy conda ffmpeg from the base environment...\r\n" })
29
263
  await this.kernel.bin.exec({
30
- message: "conda remove ffmpeg",
264
+ message: "conda remove -y ffmpeg"
31
265
  }, ondata)
266
+ await this.kernel.bin.refreshInstalled()
267
+ if (this.hasLegacyCondaFfmpeg()) {
268
+ throw new Error("Legacy conda ffmpeg is still installed in the base environment")
269
+ }
270
+ }
271
+
272
+ async verifyChecksum(filePath, expectedHash) {
273
+ const actualHash = await this.sha256(filePath)
274
+ if (actualHash.toLowerCase() !== expectedHash.toLowerCase()) {
275
+ throw new Error(`Checksum mismatch for ${path.basename(filePath)}: expected ${expectedHash}, got ${actualHash}`)
276
+ }
277
+ }
278
+
279
+ async sha256(filePath) {
280
+ const buffer = await fs.promises.readFile(filePath)
281
+ return crypto.createHash("sha256").update(buffer).digest("hex")
282
+ }
283
+
284
+ async writeManifest(spec) {
285
+ const manifest = {
286
+ version: spec.version,
287
+ platform: this.kernel.platform,
288
+ arch: this.kernel.arch,
289
+ source: spec.source,
290
+ installed_at: new Date().toISOString(),
291
+ binaries: {
292
+ ffmpeg: this.binaryPath("ffmpeg"),
293
+ ffprobe: this.binaryPath("ffprobe")
294
+ }
295
+ }
296
+ await fs.promises.writeFile(this.manifestPath(), JSON.stringify(manifest, null, 2))
297
+ }
298
+
299
+ async findFileByName(root, targetName) {
300
+ const entries = await fs.promises.readdir(root, { withFileTypes: true })
301
+ for (const entry of entries) {
302
+ const fullPath = path.resolve(root, entry.name)
303
+ if (entry.isDirectory()) {
304
+ const found = await this.findFileByName(fullPath, targetName)
305
+ if (found) {
306
+ return found
307
+ }
308
+ } else if (entry.name === targetName) {
309
+ return fullPath
310
+ }
311
+ }
312
+ return null
313
+ }
314
+
315
+ execBinary(file, args) {
316
+ return new Promise((resolve, reject) => {
317
+ execFile(file, args, {
318
+ windowsHide: true,
319
+ maxBuffer: 32 * 1024 * 1024
320
+ }, (error, stdout, stderr) => {
321
+ const output = `${stdout || ""}${stderr || ""}`
322
+ if (error) {
323
+ reject(new Error(output || error.message))
324
+ return
325
+ }
326
+ resolve(output)
327
+ })
328
+ })
329
+ }
330
+
331
+ firstLine(output) {
332
+ return String(output || "").split(/\r?\n/).find(Boolean) || ""
32
333
  }
33
334
  }
335
+
34
336
  module.exports = Ffmpeg
@@ -14,7 +14,6 @@ module.exports = {
14
14
  "node",
15
15
  "huggingface",
16
16
  "git",
17
- "ffmpeg",
18
17
  // "caddy"
19
18
  ]
20
19
  if (platform !== "win32") {
@@ -184,7 +183,6 @@ module.exports = {
184
183
  "node",
185
184
  "huggingface",
186
185
  "git",
187
- "ffmpeg",
188
186
  ]
189
187
  if (platform !== "win32") {
190
188
  conda_requirements.push("tmux")
@@ -223,7 +221,6 @@ module.exports = {
223
221
  "node",
224
222
  "huggingface",
225
223
  "git",
226
- "ffmpeg",
227
224
  "caddy",
228
225
  ]
229
226
  if (platform === "win32") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.2.0",
3
+ "version": "7.2.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -69,13 +69,13 @@ function escapeRegExp(value) {
69
69
  function applyTemplateValues(template, values) {
70
70
  let result = typeof template === "string" ? template : "";
71
71
  if (!values || typeof values !== "object") {
72
- return result;
72
+ return result.replace(/\r\n?/g, "\n");
73
73
  }
74
74
  Object.entries(values).forEach(([name, value]) => {
75
75
  const pattern = new RegExp(`{{\\s*${escapeRegExp(name)}\\s*}}`, "g");
76
76
  result = result.replace(pattern, value == null ? "" : String(value));
77
77
  });
78
- return result;
78
+ return result.replace(/\r\n?/g, "\n");
79
79
  }
80
80
 
81
81
  function extractInputValues(source) {
@@ -196,7 +196,7 @@
196
196
  const pattern = new RegExp(`{{\\s*${escapeRegExp(name)}\\s*}}`, 'g');
197
197
  result = result.replace(pattern, value);
198
198
  });
199
- return result;
199
+ return result.replace(/\r\n?/g, '\n');
200
200
  }
201
201
 
202
202
  function buildToolOptions(tools) {
@@ -70,7 +70,7 @@
70
70
  const pattern = new RegExp(`{{\\s*${escapeRegExp(name)}\\s*}}`, "g");
71
71
  result = result.replace(pattern, String(value));
72
72
  });
73
- return result;
73
+ return result.replace(/\r\n?/g, "\n");
74
74
  }
75
75
 
76
76
  function slugify(value) {