pinokiod 7.2.0 → 7.2.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.
@@ -1,13 +1,83 @@
1
- const fs = require('fs')
2
- const fetch = require('cross-fetch')
3
- const { rimraf } = require('rimraf')
4
- const decompress = require('decompress');
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+ const { execFile } = require("child_process")
4
+ const ParcelWatcher = require("@parcel/watcher")
5
+ const semver = require("semver")
6
+ const { rimraf } = require("rimraf")
7
+
8
+ const RELEASE_VERSION = "8.0.1"
9
+ const RELEASE_RANGE = ">=8.0.1 <8.1.0"
10
+ const CONDA_SPEC = `ffmpeg=${RELEASE_VERSION}`
11
+
12
+ const WINDOWS_GDK_PIXBUF_POST_LINK_NOOP = `@echo off
13
+ rem Pinokio intentionally skips gdk-pixbuf loader cache generation for FFmpeg installs.
14
+ exit /b 0
15
+ `
16
+
5
17
  class Ffmpeg {
6
18
  description = "Installs FFmpeg for audio and video processing."
19
+
7
20
  cmd() {
8
- return "ffmpeg=7.0.2"
21
+ return CONDA_SPEC
9
22
  }
23
+
24
+ env(kernel) {
25
+ const activeKernel = kernel || this.kernel
26
+ const env = {
27
+ FFMPEG_PATH: this.binaryPath("ffmpeg", activeKernel),
28
+ FFPROBE_PATH: this.binaryPath("ffprobe", activeKernel)
29
+ }
30
+ if (activeKernel.platform === "linux") {
31
+ env.LD_LIBRARY_PATH = [this.libraryDir(activeKernel)]
32
+ }
33
+ return env
34
+ }
35
+
36
+ binaryPath(tool, kernel = this.kernel) {
37
+ const filename = kernel.platform === "win32" ? `${tool}.exe` : tool
38
+ if (kernel.platform === "win32") {
39
+ return kernel.bin.path("miniconda", "Library", "bin", filename)
40
+ }
41
+ return kernel.bin.path("miniconda", "bin", filename)
42
+ }
43
+
44
+ libraryDir(kernel = this.kernel) {
45
+ if (kernel.platform === "win32") {
46
+ return kernel.bin.path("miniconda", "Library", "bin")
47
+ }
48
+ return kernel.bin.path("miniconda", "lib")
49
+ }
50
+
51
+ legacyStandalonePaths() {
52
+ return [
53
+ this.kernel.bin.path("ffmpeg"),
54
+ this.kernel.bin.path("ffmpeg-tmp")
55
+ ]
56
+ }
57
+
58
+ async start() {
59
+ if (this.kernel.platform !== "darwin") {
60
+ return
61
+ }
62
+ if (!this.isInstalledVersion()) {
63
+ return
64
+ }
65
+ await this.syncMacUvLibraryShims()
66
+ await this.startMacUvLibraryWatcher()
67
+ }
68
+
10
69
  async install(req, ondata) {
70
+ await this.cleanupLegacyStandalone(ondata)
71
+ if (this.kernel.platform === "win32") {
72
+ await this.installWindows(ondata)
73
+ } else {
74
+ await this.installStandard(ondata)
75
+ }
76
+ await this.syncMacUvLibraryShims(ondata)
77
+ await this.selfTest(ondata)
78
+ }
79
+
80
+ async installStandard(ondata) {
11
81
  await this.kernel.bin.exec({
12
82
  message: [
13
83
  "conda clean -y --all",
@@ -15,20 +85,391 @@ class Ffmpeg {
15
85
  ]
16
86
  }, ondata)
17
87
  }
88
+
89
+ async installWindows(ondata) {
90
+ await this.kernel.bin.exec({
91
+ message: [
92
+ "conda clean -y --all",
93
+ `conda install -y --download-only -c conda-forge ${this.cmd()}`
94
+ ]
95
+ }, ondata)
96
+
97
+ await this.patchWindowsGdkPixbufPostLink(ondata)
98
+
99
+ await this.kernel.bin.exec({
100
+ message: `conda install -y --offline -c conda-forge ${this.cmd()}`
101
+ }, ondata)
102
+ }
103
+
104
+ async patchWindowsGdkPixbufPostLink(ondata) {
105
+ const pkgsDir = this.kernel.bin.path("miniconda", "pkgs")
106
+ const entries = await fs.promises.readdir(pkgsDir, { withFileTypes: true })
107
+ const packageDirs = entries
108
+ .filter((entry) => entry.isDirectory() && /^gdk-pixbuf-/.test(entry.name))
109
+ .map((entry) => path.resolve(pkgsDir, entry.name))
110
+
111
+ if (packageDirs.length === 0) {
112
+ throw new Error("Could not find downloaded gdk-pixbuf package in the Conda cache after --download-only")
113
+ }
114
+
115
+ let patchedCount = 0
116
+ for (const packageDir of packageDirs) {
117
+ const scripts = [
118
+ path.resolve(packageDir, "Scripts", ".gdk-pixbuf-post-link.bat"),
119
+ path.resolve(packageDir, "info", "recipe", "post-link.bat")
120
+ ]
121
+
122
+ for (const script of scripts) {
123
+ try {
124
+ await fs.promises.access(script)
125
+ await fs.promises.writeFile(script, WINDOWS_GDK_PIXBUF_POST_LINK_NOOP)
126
+ patchedCount += 1
127
+ } catch (error) {
128
+ if (error && error.code !== "ENOENT") {
129
+ throw error
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ if (patchedCount === 0) {
136
+ throw new Error("Found gdk-pixbuf in the Conda cache, but did not find any post-link scripts to patch")
137
+ }
138
+
139
+ ondata({ raw: `patched ${patchedCount} gdk-pixbuf post-link script(s) in the Conda cache...\r\n` })
140
+ }
141
+
18
142
  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") {
143
+ try {
144
+ if (!this.isInstalledVersion()) {
23
145
  return false
24
146
  }
147
+
148
+ await fs.promises.access(this.binaryPath("ffmpeg"))
149
+ await fs.promises.access(this.binaryPath("ffprobe"))
150
+ await this.syncMacUvLibraryShims()
151
+ await this.startMacUvLibraryWatcher()
152
+ await this.selfTest()
153
+ return true
154
+ } catch (error) {
155
+ console.log("conda ffmpeg installed check failed", error && error.message ? error.message : error)
156
+ return false
25
157
  }
26
- return this.kernel.bin.installed.conda.has("ffmpeg")
27
158
  }
159
+
28
160
  async uninstall(req, ondata) {
161
+ await this.stopMacUvLibraryWatcher()
162
+ await this.removeMacUvLibraryShims(ondata)
29
163
  await this.kernel.bin.exec({
30
- message: "conda remove ffmpeg",
164
+ message: "conda remove -y ffmpeg"
31
165
  }, ondata)
166
+ await this.cleanupLegacyStandalone(ondata)
167
+ }
168
+
169
+ isInstalledVersion() {
170
+ if (!this.kernel.bin.installed?.conda?.has("ffmpeg")) {
171
+ return false
172
+ }
173
+ return this.kernel.bin.installed?.conda_versions?.ffmpeg === RELEASE_VERSION
174
+ }
175
+
176
+ async cleanupLegacyStandalone(ondata) {
177
+ for (const target of this.legacyStandalonePaths()) {
178
+ const exists = await fs.promises.access(target).then(() => true).catch(() => false)
179
+ if (exists) {
180
+ if (ondata) {
181
+ ondata({ raw: `removing legacy standalone ffmpeg files from ${target}...\r\n` })
182
+ }
183
+ await rimraf(target)
184
+ }
185
+ }
186
+ }
187
+
188
+ uvPythonRoot(kernel = this.kernel) {
189
+ return kernel.path("cache", "XDG_DATA_HOME", "uv", "python")
190
+ }
191
+
192
+ async startMacUvLibraryWatcher() {
193
+ if (this.kernel.platform !== "darwin" || this.macUvLibraryWatcher) {
194
+ return
195
+ }
196
+
197
+ const root = this.uvPythonRoot()
198
+ await fs.promises.mkdir(root, { recursive: true })
199
+ this.macUvLibraryWatcher = await ParcelWatcher.subscribe(root, (error, events) => {
200
+ if (error) {
201
+ console.warn("ffmpeg uv library watcher error", error && error.message ? error.message : error)
202
+ return
203
+ }
204
+ if (!events || events.length === 0) {
205
+ return
206
+ }
207
+ this.scheduleMacUvLibraryShimSync()
208
+ })
209
+ }
210
+
211
+ async stopMacUvLibraryWatcher() {
212
+ if (this.macUvLibraryShimSyncTimer) {
213
+ clearTimeout(this.macUvLibraryShimSyncTimer)
214
+ this.macUvLibraryShimSyncTimer = null
215
+ }
216
+ if (this.macUvLibraryWatcher) {
217
+ await this.macUvLibraryWatcher.unsubscribe()
218
+ this.macUvLibraryWatcher = null
219
+ }
220
+ }
221
+
222
+ scheduleMacUvLibraryShimSync() {
223
+ if (this.macUvLibraryShimSyncTimer) {
224
+ clearTimeout(this.macUvLibraryShimSyncTimer)
225
+ }
226
+ this.macUvLibraryShimSyncTimer = setTimeout(async () => {
227
+ this.macUvLibraryShimSyncTimer = null
228
+ try {
229
+ await this.syncMacUvLibraryShims()
230
+ } catch (error) {
231
+ console.warn("ffmpeg uv library shim sync error", error && error.message ? error.message : error)
232
+ }
233
+ }, 250)
234
+ }
235
+
236
+ async uvLibraryDirs(kernel = this.kernel) {
237
+ if (kernel.platform !== "darwin") {
238
+ return []
239
+ }
240
+
241
+ const root = this.uvPythonRoot(kernel)
242
+ const entries = await fs.promises.readdir(root, { withFileTypes: true }).catch(() => [])
243
+ const dirs = []
244
+
245
+ for (const entry of entries) {
246
+ if (!entry.isDirectory()) {
247
+ continue
248
+ }
249
+ const libDir = path.resolve(root, entry.name, "lib")
250
+ const exists = await fs.promises.access(libDir).then(() => true).catch(() => false)
251
+ if (exists) {
252
+ dirs.push(libDir)
253
+ }
254
+ }
255
+
256
+ return dirs
257
+ }
258
+
259
+ async ffmpegLibraryFiles(kernel = this.kernel) {
260
+ if (kernel.platform !== "darwin") {
261
+ return []
262
+ }
263
+
264
+ const dir = this.libraryDir(kernel)
265
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true }).catch(() => [])
266
+ return entries
267
+ .filter((entry) => entry.isFile() || entry.isSymbolicLink())
268
+ .map((entry) => entry.name)
269
+ .filter((name) => /^lib(?:av|sw)[^.]+(?:\.\d+)*\.dylib$/i.test(name))
270
+ .sort()
271
+ }
272
+
273
+ async syncMacUvLibraryShims(ondata) {
274
+ if (this.kernel.platform !== "darwin") {
275
+ return
276
+ }
277
+
278
+ const [libraryDirs, libraryFiles] = await Promise.all([
279
+ this.uvLibraryDirs(),
280
+ this.ffmpegLibraryFiles()
281
+ ])
282
+
283
+ if (libraryDirs.length === 0 || libraryFiles.length === 0) {
284
+ return
285
+ }
286
+
287
+ let createdCount = 0
288
+ let refreshedCount = 0
289
+ const sourceDir = this.libraryDir()
290
+
291
+ for (const libDir of libraryDirs) {
292
+ for (const filename of libraryFiles) {
293
+ const source = path.resolve(sourceDir, filename)
294
+ const target = path.resolve(libDir, filename)
295
+ const desiredLink = path.relative(libDir, source)
296
+
297
+ let stat
298
+ try {
299
+ stat = await fs.promises.lstat(target)
300
+ } catch (error) {
301
+ if (!error || error.code !== "ENOENT") {
302
+ throw error
303
+ }
304
+ }
305
+
306
+ if (!stat) {
307
+ await fs.promises.symlink(desiredLink, target)
308
+ createdCount += 1
309
+ continue
310
+ }
311
+
312
+ if (!stat.isSymbolicLink()) {
313
+ continue
314
+ }
315
+
316
+ const currentLink = await fs.promises.readlink(target)
317
+ if (currentLink === desiredLink) {
318
+ continue
319
+ }
320
+
321
+ await fs.promises.unlink(target)
322
+ await fs.promises.symlink(desiredLink, target)
323
+ refreshedCount += 1
324
+ }
325
+ }
326
+
327
+ if (ondata && (createdCount > 0 || refreshedCount > 0)) {
328
+ ondata({
329
+ raw: `synced ${createdCount + refreshedCount} FFmpeg dylib shim(s) into uv Python runtime libraries...\r\n`
330
+ })
331
+ }
332
+ }
333
+
334
+ async removeMacUvLibraryShims(ondata) {
335
+ if (this.kernel.platform !== "darwin") {
336
+ return
337
+ }
338
+
339
+ const [libraryDirs, libraryFiles] = await Promise.all([
340
+ this.uvLibraryDirs(),
341
+ this.ffmpegLibraryFiles()
342
+ ])
343
+
344
+ if (libraryDirs.length === 0 || libraryFiles.length === 0) {
345
+ return
346
+ }
347
+
348
+ const sourceDir = this.libraryDir()
349
+ let removedCount = 0
350
+
351
+ for (const libDir of libraryDirs) {
352
+ for (const filename of libraryFiles) {
353
+ const target = path.resolve(libDir, filename)
354
+
355
+ let stat
356
+ try {
357
+ stat = await fs.promises.lstat(target)
358
+ } catch (error) {
359
+ if (!error || error.code !== "ENOENT") {
360
+ throw error
361
+ }
362
+ }
363
+
364
+ if (!stat || !stat.isSymbolicLink()) {
365
+ continue
366
+ }
367
+
368
+ const currentLink = await fs.promises.readlink(target)
369
+ const resolved = path.resolve(libDir, currentLink)
370
+ if (path.dirname(resolved) !== sourceDir) {
371
+ continue
372
+ }
373
+
374
+ await fs.promises.unlink(target)
375
+ removedCount += 1
376
+ }
377
+ }
378
+
379
+ if (ondata && removedCount > 0) {
380
+ ondata({ raw: `removed ${removedCount} FFmpeg dylib shim(s) from uv Python runtime libraries...\r\n` })
381
+ }
382
+ }
383
+
384
+ async selfTest(ondata) {
385
+ if (ondata) {
386
+ ondata({ raw: "verifying ffmpeg installation...\r\n" })
387
+ }
388
+
389
+ const ffmpegVersionOutput = await this.execBinary(this.binaryPath("ffmpeg"), ["-version"])
390
+ const ffmpegVersion = semver.coerce(ffmpegVersionOutput)
391
+ if (!ffmpegVersion || !semver.satisfies(ffmpegVersion, RELEASE_RANGE)) {
392
+ throw new Error(`Unexpected ffmpeg version: ${this.firstLine(ffmpegVersionOutput)}`)
393
+ }
394
+
395
+ const encoderOutput = await this.execBinary(this.binaryPath("ffmpeg"), ["-hide_banner", "-encoders"])
396
+ if (!/\blibmp3lame\b/i.test(encoderOutput)) {
397
+ throw new Error("FFmpeg was installed without libmp3lame support")
398
+ }
399
+
400
+ const ffprobeVersionOutput = await this.execBinary(this.binaryPath("ffprobe"), ["-version"])
401
+ const ffprobeVersion = semver.coerce(ffprobeVersionOutput)
402
+ if (!ffprobeVersion || !semver.satisfies(ffprobeVersion, RELEASE_RANGE)) {
403
+ throw new Error(`Unexpected ffprobe version: ${this.firstLine(ffprobeVersionOutput)}`)
404
+ }
405
+
406
+ await this.assertSharedLibraries()
407
+ return true
408
+ }
409
+
410
+ async assertSharedLibraries() {
411
+ const dir = this.libraryDir()
412
+ const entries = await fs.promises.readdir(dir).catch(() => {
413
+ throw new Error(`Missing FFmpeg library directory: ${dir}`)
414
+ })
415
+
416
+ const patterns = this.sharedLibraryPatterns()
417
+ for (const pattern of patterns) {
418
+ if (!entries.some((name) => pattern.test(name))) {
419
+ throw new Error(`Missing FFmpeg shared library matching ${pattern}`)
420
+ }
421
+ }
422
+ }
423
+
424
+ sharedLibraryPatterns() {
425
+ if (this.kernel.platform === "win32") {
426
+ return [
427
+ /^avcodec-\d+\.dll$/i,
428
+ /^avformat-\d+\.dll$/i,
429
+ /^avutil-\d+\.dll$/i,
430
+ /^swresample-\d+\.dll$/i,
431
+ /^swscale-\d+\.dll$/i
432
+ ]
433
+ }
434
+
435
+ if (this.kernel.platform === "darwin") {
436
+ return [
437
+ /^libavcodec(\.\d+)*\.dylib$/i,
438
+ /^libavformat(\.\d+)*\.dylib$/i,
439
+ /^libavutil(\.\d+)*\.dylib$/i,
440
+ /^libswresample(\.\d+)*\.dylib$/i,
441
+ /^libswscale(\.\d+)*\.dylib$/i
442
+ ]
443
+ }
444
+
445
+ return [
446
+ /^libavcodec\.so(\.\d+)*$/i,
447
+ /^libavformat\.so(\.\d+)*$/i,
448
+ /^libavutil\.so(\.\d+)*$/i,
449
+ /^libswresample\.so(\.\d+)*$/i,
450
+ /^libswscale\.so(\.\d+)*$/i
451
+ ]
452
+ }
453
+
454
+ execBinary(file, args) {
455
+ return new Promise((resolve, reject) => {
456
+ execFile(file, args, {
457
+ windowsHide: true,
458
+ maxBuffer: 32 * 1024 * 1024
459
+ }, (error, stdout, stderr) => {
460
+ const output = `${stdout || ""}${stderr || ""}`
461
+ if (error) {
462
+ reject(new Error(output || error.message))
463
+ return
464
+ }
465
+ resolve(output)
466
+ })
467
+ })
468
+ }
469
+
470
+ firstLine(output) {
471
+ return String(output || "").split(/\r?\n/).find(Boolean) || ""
32
472
  }
33
473
  }
474
+
34
475
  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.4",
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) {