@mrgoonie/multix 0.13.0 → 0.14.0
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/CHANGELOG.md +12 -0
- package/README.md +8 -4
- package/dist/cli.js +251 -79
- package/package.json +1 -1
- package/skill/SKILL.md +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.14.0](https://github.com/mrgoonie/multix-cli/compare/v0.13.0...v0.14.0) (2026-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* **doc:** read convert input from piped stdin ([#84](https://github.com/mrgoonie/multix-cli/issues/84)) ([4596f7f](https://github.com/mrgoonie/multix-cli/commit/4596f7f09da2e8e7a95582296c220f4e007e3171))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* **heygen:** harden base URL, output downloads and multi-output paths ([#83](https://github.com/mrgoonie/multix-cli/issues/83)) ([fea7a95](https://github.com/mrgoonie/multix-cli/commit/fea7a950bdf37836a5344301829b028efe4c9a6d))
|
|
19
|
+
|
|
8
20
|
## [0.13.0](https://github.com/mrgoonie/multix-cli/compare/v0.12.0...v0.13.0) (2026-10-11)
|
|
9
21
|
|
|
10
22
|
|
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ Set at least one provider key. Add to `.env` in your project root or `~/.multix/
|
|
|
71
71
|
| `FAL_IMAGE_MODEL` | No | Default fal image model (default `fal-ai/flux/schnell`) |
|
|
72
72
|
| `FAL_VIDEO_MODEL` | No | Default fal text-to-video model (default `fal-ai/kling-video/v1.6/standard/text-to-video`) |
|
|
73
73
|
| `HEYGEN_API_KEY` | For HeyGen | [HeyGen API settings](https://app.heygen.com/developers/api). API usage is billed separately from HeyGen web plans |
|
|
74
|
-
| `HEYGEN_BASE_URL` | No | Override the HeyGen API base (default `https://api.heygen.com`; proxies/tests) |
|
|
74
|
+
| `HEYGEN_BASE_URL` | No | Override the HeyGen API base (default `https://api.heygen.com`; proxies/tests). Must be `https://`, except `http://` on `localhost`/`127.0.0.1`/`[::1]` |
|
|
75
75
|
| `FAL_VIDEO_IMAGE_MODEL` | No | Default fal image-to-video model, used when `--image-url` is passed without `-m` (default `fal-ai/kling-video/v1.6/standard/image-to-video`) |
|
|
76
76
|
| `ATLASCLOUD_API_KEY` | For Atlas Cloud | [Atlas Cloud](https://www.atlascloud.ai/console/api-keys) |
|
|
77
77
|
| `ATLASCLOUD_BASE_URL` | No | Override Atlas Cloud media API base (default `https://api.atlascloud.ai/api/v1`) |
|
|
@@ -581,7 +581,7 @@ multix heygen video --avatar <look-id> --audio ./voiceover.mp3
|
|
|
581
581
|
|
|
582
582
|
# Translation (one job per language; all are downloaded), lipsync, TTS
|
|
583
583
|
multix heygen languages
|
|
584
|
-
multix heygen translate ./talk.mp4 --languages "Spanish, Chinese (Cantonese, Traditional)" [--mode speed|precision] [--output <dir>]
|
|
584
|
+
multix heygen translate ./talk.mp4 --languages "Spanish, Chinese (Cantonese, Traditional)" [--mode speed|precision] [--output <dir|dub.mp4>] # dub.mp4 -> dub-Spanish.mp4, ...
|
|
585
585
|
multix heygen lipsync --video <ref> --audio <ref>
|
|
586
586
|
multix heygen tts "Welcome to the show." --voice <voice-id> [--output hello.mp3]
|
|
587
587
|
|
|
@@ -624,13 +624,17 @@ multix media batch --input-dir <dir> --output-dir <dir> [--quality 85] [--max-wi
|
|
|
624
624
|
### `multix doc`
|
|
625
625
|
|
|
626
626
|
```bash
|
|
627
|
-
multix doc convert --input <files...> [--output <path>] [--auto-name] [--model gemini-3.6-flash] [--prompt <str>] [-v]
|
|
627
|
+
multix doc convert --input <files...> [--output <path>] [--auto-name] [--stdin-ext <ext>] [--model gemini-3.6-flash] [--prompt <str>] [-v]
|
|
628
|
+
|
|
629
|
+
# Read piped stdin with "-" (PDF, images and text auto-detected; else pass --stdin-ext, e.g. pdf; "-" only once, ./- for a file named -)
|
|
630
|
+
cat report.pdf | multix doc convert --input - --output report.md
|
|
628
631
|
```
|
|
629
632
|
|
|
630
633
|
Converts PDFs, DOCX/XLSX/PPTX, images, and HTML/text files to Markdown via Gemini.
|
|
631
634
|
Multiple inputs are concatenated with `---` separators into a single output.
|
|
635
|
+
Piped stdin is labelled `<stdin>` in the output and error messages; its temp file is removed when the command ends.
|
|
632
636
|
|
|
633
|
-
**Default output:** `./multix-output/document-extraction.md`
|
|
637
|
+
**Default output:** `./multix-output/document-extraction.md` (with `--auto-name`: `<input-basename>-extraction.md`, or `stdin-extraction.md` for piped input)
|
|
634
638
|
|
|
635
639
|
## Output
|
|
636
640
|
|
package/dist/cli.js
CHANGED
|
@@ -1428,14 +1428,14 @@ function detectThumbUrl(obj) {
|
|
|
1428
1428
|
}
|
|
1429
1429
|
return null;
|
|
1430
1430
|
}
|
|
1431
|
-
async function downloadThumbBeside(thumbUrl, videoPath, logger) {
|
|
1431
|
+
async function downloadThumbBeside(thumbUrl, videoPath, logger, download = downloadFile) {
|
|
1432
1432
|
const match = thumbUrl.match(IMAGE_EXT_RE);
|
|
1433
1433
|
const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
|
|
1434
1434
|
const dir = path6.dirname(videoPath);
|
|
1435
1435
|
const base = path6.basename(videoPath, path6.extname(videoPath));
|
|
1436
1436
|
const dest = path6.join(dir, `${base}_thumb.${ext}`);
|
|
1437
1437
|
try {
|
|
1438
|
-
await
|
|
1438
|
+
await download(thumbUrl, dest);
|
|
1439
1439
|
logger?.success(`Saved thumb ${dest}`);
|
|
1440
1440
|
return dest;
|
|
1441
1441
|
} catch (e) {
|
|
@@ -1450,7 +1450,7 @@ async function maybeDownloadThumb(source, videoPath, opts = {}) {
|
|
|
1450
1450
|
opts.logger?.debug("No thumbnail URL detected in provider response");
|
|
1451
1451
|
return null;
|
|
1452
1452
|
}
|
|
1453
|
-
const saved = await downloadThumbBeside(url, videoPath, opts.logger);
|
|
1453
|
+
const saved = await downloadThumbBeside(url, videoPath, opts.logger, opts.download);
|
|
1454
1454
|
if (saved && opts.copyTo) {
|
|
1455
1455
|
const copyExt = path6.extname(saved);
|
|
1456
1456
|
const copyBase = path6.basename(opts.copyTo, path6.extname(opts.copyTo));
|
|
@@ -4611,11 +4611,15 @@ function trackTempDir(dir) {
|
|
|
4611
4611
|
for (const s of SIGNALS) process.once(s, onSignal);
|
|
4612
4612
|
return dispose;
|
|
4613
4613
|
}
|
|
4614
|
+
var DEFAULT_FLAG = "--files";
|
|
4615
|
+
var DEFAULT_EXAMPLE = "cat clip.mp3 | multix gemini transcribe --files -";
|
|
4616
|
+
var DEFAULT_EXT_EXAMPLE = "mp3";
|
|
4614
4617
|
async function stdinToTempFile(opts = {}) {
|
|
4615
4618
|
const input = opts.input ?? process.stdin;
|
|
4619
|
+
const flag = opts.flag ?? DEFAULT_FLAG;
|
|
4616
4620
|
if (input.isTTY) {
|
|
4617
4621
|
throw new ValidationError(
|
|
4618
|
-
|
|
4622
|
+
`\`${flag} -\` reads piped input, but stdin is a terminal. Example: ${opts.example ?? DEFAULT_EXAMPLE}`
|
|
4619
4623
|
);
|
|
4620
4624
|
}
|
|
4621
4625
|
const forcedExt = opts.ext ? normalizeExt(opts.ext) : void 0;
|
|
@@ -4624,7 +4628,8 @@ async function stdinToTempFile(opts = {}) {
|
|
|
4624
4628
|
const cleanup = trackTempDir(dir);
|
|
4625
4629
|
const rawPath = path23.join(dir, "input");
|
|
4626
4630
|
const hint = setTimeout(() => {
|
|
4627
|
-
process.stderr.write(
|
|
4631
|
+
process.stderr.write(`Waiting for data on stdin (\`${flag} -\`)... press Ctrl+C to cancel
|
|
4632
|
+
`);
|
|
4628
4633
|
}, WAIT_HINT_MS);
|
|
4629
4634
|
hint.unref?.();
|
|
4630
4635
|
let head = Buffer.alloc(0);
|
|
@@ -4651,7 +4656,7 @@ async function stdinToTempFile(opts = {}) {
|
|
|
4651
4656
|
clearTimeout(hint);
|
|
4652
4657
|
fs20.closeSync(fd);
|
|
4653
4658
|
}
|
|
4654
|
-
if (size === 0) throw new ValidationError(
|
|
4659
|
+
if (size === 0) throw new ValidationError(`\`${flag} -\` was given but stdin was empty`);
|
|
4655
4660
|
const detectedExt = forcedExt ?? detectExtension(head);
|
|
4656
4661
|
const finalPath = path23.join(dir, `stdin${detectedExt ?? ".bin"}`);
|
|
4657
4662
|
fs20.renameSync(rawPath, finalPath);
|
|
@@ -4665,10 +4670,11 @@ function labelError(message, input) {
|
|
|
4665
4670
|
return input.path === input.label ? message : message.split(input.path).join(input.label);
|
|
4666
4671
|
}
|
|
4667
4672
|
async function resolveFileInputs(files, opts = {}) {
|
|
4673
|
+
const flag = opts.flag ?? DEFAULT_FLAG;
|
|
4668
4674
|
const stdinCount = files.filter((f) => f === STDIN_ARG).length;
|
|
4669
|
-
if (stdinCount > 1) throw new ValidationError(
|
|
4675
|
+
if (stdinCount > 1) throw new ValidationError(`\`-\` (stdin) can only be passed once to ${flag}`);
|
|
4670
4676
|
if (opts.stdinExt && stdinCount === 0) {
|
|
4671
|
-
throw new ValidationError(
|
|
4677
|
+
throw new ValidationError(`--stdin-ext only applies when ${flag} includes \`-\``);
|
|
4672
4678
|
}
|
|
4673
4679
|
for (const f of files) {
|
|
4674
4680
|
if (f !== STDIN_ARG && !fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
@@ -4680,12 +4686,14 @@ async function resolveFileInputs(files, opts = {}) {
|
|
|
4680
4686
|
const stdin = await stdinToTempFile({
|
|
4681
4687
|
input: opts.input,
|
|
4682
4688
|
ext: opts.stdinExt,
|
|
4683
|
-
maxBytes: opts.maxBytes
|
|
4689
|
+
maxBytes: opts.maxBytes,
|
|
4690
|
+
flag: opts.flag,
|
|
4691
|
+
example: opts.example
|
|
4684
4692
|
});
|
|
4685
4693
|
if (!stdin.detected) {
|
|
4686
4694
|
stdin.cleanup();
|
|
4687
4695
|
throw new ValidationError(
|
|
4688
|
-
|
|
4696
|
+
`Could not detect the type of piped stdin; pass --stdin-ext (e.g. --stdin-ext ${opts.extExample ?? DEFAULT_EXT_EXAMPLE})`
|
|
4689
4697
|
);
|
|
4690
4698
|
}
|
|
4691
4699
|
return {
|
|
@@ -6072,8 +6080,10 @@ var HEYGEN_OPERATIONS = {
|
|
|
6072
6080
|
};
|
|
6073
6081
|
|
|
6074
6082
|
// src/providers/heygen/node-helpers.ts
|
|
6083
|
+
import { once as once2 } from "events";
|
|
6075
6084
|
import fs29 from "fs";
|
|
6076
6085
|
import path32 from "path";
|
|
6086
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
6077
6087
|
init_errors();
|
|
6078
6088
|
init_redact();
|
|
6079
6089
|
|
|
@@ -6503,6 +6513,7 @@ function videoState(video, kind = "video", id = video.id, opts = {}) {
|
|
|
6503
6513
|
return state;
|
|
6504
6514
|
}
|
|
6505
6515
|
function translationState(t) {
|
|
6516
|
+
const audioOnly = !str(t.video_url) && !!str(t.audio_url);
|
|
6506
6517
|
return {
|
|
6507
6518
|
kind: "translation",
|
|
6508
6519
|
id: t.id,
|
|
@@ -6510,6 +6521,7 @@ function translationState(t) {
|
|
|
6510
6521
|
done: t.status === "completed" || t.status === "failed",
|
|
6511
6522
|
succeeded: t.status === "completed",
|
|
6512
6523
|
outputUrl: str(t.video_url) ?? str(t.audio_url),
|
|
6524
|
+
...audioOnly ? { outputIsAudio: true } : {},
|
|
6513
6525
|
captionUrl: str(t.srt_caption_url) ?? str(t.vtt_caption_url),
|
|
6514
6526
|
outputLanguage: str(t.output_language),
|
|
6515
6527
|
failureMessage: str(t.failure_message),
|
|
@@ -6632,13 +6644,29 @@ function getJobState(client, kind, id, opts = {}) {
|
|
|
6632
6644
|
return createJobStatusReader(client, kind, id, opts)();
|
|
6633
6645
|
}
|
|
6634
6646
|
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
6635
|
-
|
|
6636
|
-
|
|
6647
|
+
var STAGGER_STEP_MS = 500;
|
|
6648
|
+
var STAGGER_MAX_MS = 5e3;
|
|
6649
|
+
function staggerOffsetMs(index, intervalMs) {
|
|
6650
|
+
if (!Number.isFinite(index) || index <= 0) return 0;
|
|
6651
|
+
const step = Math.min(STAGGER_STEP_MS, Math.max(0, intervalMs));
|
|
6652
|
+
return Math.min(index * step, STAGGER_MAX_MS);
|
|
6653
|
+
}
|
|
6654
|
+
var defaultSleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6655
|
+
async function waitForJob(client, kind, id, opts) {
|
|
6656
|
+
const { burnCaptions, startDelayMs = 0, ...poll2 } = opts;
|
|
6657
|
+
let pendingDelay = Math.min(Math.max(0, startDelayMs), Math.floor(poll2.timeoutMs / 2));
|
|
6658
|
+
const read = createJobStatusReader(client, kind, id, { burnCaptions });
|
|
6637
6659
|
return pollUntil({
|
|
6638
6660
|
intervalMs: DEFAULT_POLL_INTERVAL_MS,
|
|
6639
6661
|
maxIntervalMs: 3e4,
|
|
6640
6662
|
...poll2,
|
|
6641
|
-
fetchStatus:
|
|
6663
|
+
fetchStatus: async (ctx) => {
|
|
6664
|
+
const delay = pendingDelay;
|
|
6665
|
+
if (delay <= 0) return read(ctx);
|
|
6666
|
+
pendingDelay = 0;
|
|
6667
|
+
await (poll2.sleep ?? defaultSleep3)(delay);
|
|
6668
|
+
return read({ remainingMs: Math.max(1, ctx.remainingMs - delay) });
|
|
6669
|
+
},
|
|
6642
6670
|
isTerminal: (s) => s.done
|
|
6643
6671
|
});
|
|
6644
6672
|
}
|
|
@@ -6662,6 +6690,7 @@ function resumeCommand2(kind, id, opts = {}) {
|
|
|
6662
6690
|
|
|
6663
6691
|
// src/providers/heygen/request-builders.ts
|
|
6664
6692
|
init_errors();
|
|
6693
|
+
init_redact();
|
|
6665
6694
|
function oneOf(value, allowed, flag) {
|
|
6666
6695
|
if (value === void 0) return void 0;
|
|
6667
6696
|
if (!allowed.includes(value)) {
|
|
@@ -6683,6 +6712,30 @@ function requireHttps(url, flag) {
|
|
|
6683
6712
|
if (!/^https:\/\//i.test(url)) throw new ValidationError(`${flag} must be an https:// URL`);
|
|
6684
6713
|
return url;
|
|
6685
6714
|
}
|
|
6715
|
+
function isHttpsUrl(url) {
|
|
6716
|
+
try {
|
|
6717
|
+
return new URL(url).protocol === "https:";
|
|
6718
|
+
} catch {
|
|
6719
|
+
return false;
|
|
6720
|
+
}
|
|
6721
|
+
}
|
|
6722
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
6723
|
+
function checkBaseUrl(raw, variable) {
|
|
6724
|
+
const value = raw.trim();
|
|
6725
|
+
let url;
|
|
6726
|
+
try {
|
|
6727
|
+
url = new URL(value);
|
|
6728
|
+
} catch {
|
|
6729
|
+
throw new ConfigError(`${variable} must be an absolute https:// URL`);
|
|
6730
|
+
}
|
|
6731
|
+
const loopbackHttp = url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
|
|
6732
|
+
if (url.protocol !== "https:" && !loopbackHttp) {
|
|
6733
|
+
throw new ConfigError(
|
|
6734
|
+
`${variable} must use https:// (got ${redactUrl(value)}); plain http:// is only allowed for localhost, 127.0.0.1, or [::1]`
|
|
6735
|
+
);
|
|
6736
|
+
}
|
|
6737
|
+
return value;
|
|
6738
|
+
}
|
|
6686
6739
|
function classifyMediaRef(value) {
|
|
6687
6740
|
const v = value.trim();
|
|
6688
6741
|
if (!v) throw new ValidationError("Media reference must not be empty");
|
|
@@ -6948,9 +7001,11 @@ function requireHeygenKey() {
|
|
|
6948
7001
|
return key;
|
|
6949
7002
|
}
|
|
6950
7003
|
function createHeygenClientFromEnv() {
|
|
7004
|
+
const apiKey = requireHeygenKey();
|
|
7005
|
+
const override = resolveKey("HEYGEN_BASE_URL")?.trim();
|
|
6951
7006
|
return new HeygenClient({
|
|
6952
|
-
apiKey
|
|
6953
|
-
baseUrl:
|
|
7007
|
+
apiKey,
|
|
7008
|
+
baseUrl: override ? checkBaseUrl(override, "HEYGEN_BASE_URL") : DEFAULT_HEYGEN_BASE_URL
|
|
6954
7009
|
});
|
|
6955
7010
|
}
|
|
6956
7011
|
async function uploadLocalFile(client, filePath, logger) {
|
|
@@ -7012,9 +7067,15 @@ function addWaitOptions(cmd, defaultTimeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
|
|
|
7012
7067
|
"--poll-interval <ms>",
|
|
7013
7068
|
"Initial poll interval (backs off to 30s)",
|
|
7014
7069
|
String(DEFAULT_POLL_INTERVAL_MS)
|
|
7015
|
-
).option("--no-download", "Do not download outputs; print output URLs instead").option(
|
|
7016
|
-
|
|
7017
|
-
|
|
7070
|
+
).option("--no-download", "Do not download outputs; print output URLs instead").option(
|
|
7071
|
+
"--output <path>",
|
|
7072
|
+
"Output file or directory. A path ending in / or \\, an existing directory, or a path without an extension is a directory; with several outputs, a media file name gets a per-language suffix (dub.mp4 -> dub-Spanish.mp4)"
|
|
7073
|
+
).option("--captions", "Also download the caption/subtitle file when one is available").option("--no-thumb", "Skip downloading the video thumbnail").option("--json", "Print a JSON summary instead of human-readable output").option("-v, --verbose", "Verbose logging");
|
|
7074
|
+
}
|
|
7075
|
+
var VIDEO_EXTS = /* @__PURE__ */ new Set(["mp4", "webm", "mov"]);
|
|
7076
|
+
var AUDIO_EXTS = /* @__PURE__ */ new Set(["mp3", "wav", "m4a", "aac"]);
|
|
7077
|
+
var MEDIA_EXTS = /* @__PURE__ */ new Set([...VIDEO_EXTS, ...AUDIO_EXTS]);
|
|
7078
|
+
var OUTPUT_EXTS = /* @__PURE__ */ new Set([...MEDIA_EXTS, "srt", "vtt"]);
|
|
7018
7079
|
function inferExtension(url, fallback) {
|
|
7019
7080
|
try {
|
|
7020
7081
|
const ext = path32.extname(new URL(url).pathname).slice(1).toLowerCase();
|
|
@@ -7026,16 +7087,57 @@ function inferExtension(url, fallback) {
|
|
|
7026
7087
|
function safeSlug(value) {
|
|
7027
7088
|
return value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "out";
|
|
7028
7089
|
}
|
|
7090
|
+
var MAX_REDIRECTS = 5;
|
|
7091
|
+
var isRedirect = (status) => [301, 302, 303, 307, 308].includes(status);
|
|
7092
|
+
async function downloadHttpsOnly(url, dest, timeoutMs) {
|
|
7093
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
7094
|
+
let current = url;
|
|
7095
|
+
for (let hop = 0; ; hop++) {
|
|
7096
|
+
if (!isHttpsUrl(current)) {
|
|
7097
|
+
throw new HttpError(0, "refusing to follow a redirect to a non-https URL", current);
|
|
7098
|
+
}
|
|
7099
|
+
let response;
|
|
7100
|
+
try {
|
|
7101
|
+
response = await globalThis.fetch(current, { signal, redirect: "manual" });
|
|
7102
|
+
} catch (cause) {
|
|
7103
|
+
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), current);
|
|
7104
|
+
}
|
|
7105
|
+
if (response.url && !isHttpsUrl(response.url)) {
|
|
7106
|
+
await response.body?.cancel();
|
|
7107
|
+
throw new HttpError(0, "redirected to a non-https URL", response.url);
|
|
7108
|
+
}
|
|
7109
|
+
const location = response.headers.get("location");
|
|
7110
|
+
if (isRedirect(response.status) && location) {
|
|
7111
|
+
await response.body?.cancel();
|
|
7112
|
+
if (hop >= MAX_REDIRECTS) throw new HttpError(0, "too many redirects", url);
|
|
7113
|
+
current = new URL(location, current).href;
|
|
7114
|
+
continue;
|
|
7115
|
+
}
|
|
7116
|
+
if (!response.ok) throw new HttpError(response.status, "(download failed)", current);
|
|
7117
|
+
if (!response.body) throw new HttpError(0, "Response body is null", current);
|
|
7118
|
+
fs29.mkdirSync(path32.dirname(dest), { recursive: true });
|
|
7119
|
+
const fileStream = fs29.createWriteStream(dest);
|
|
7120
|
+
try {
|
|
7121
|
+
await pipeline2(response.body, fileStream);
|
|
7122
|
+
} catch (err2) {
|
|
7123
|
+
if (!fileStream.closed) await once2(fileStream, "close");
|
|
7124
|
+
throw err2;
|
|
7125
|
+
}
|
|
7126
|
+
return;
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7029
7129
|
async function downloadOutput(url, dest, resumeHint) {
|
|
7030
7130
|
if (!url) {
|
|
7031
7131
|
throw new ValidationError(`HeyGen reported success but returned no output URL. ${resumeHint}`);
|
|
7032
7132
|
}
|
|
7033
|
-
if (
|
|
7034
|
-
throw new ValidationError(
|
|
7133
|
+
if (!isHttpsUrl(url)) {
|
|
7134
|
+
throw new ValidationError(
|
|
7135
|
+
`Refusing to download HeyGen output from a non-https URL (${redactUrl(url)})`
|
|
7136
|
+
);
|
|
7035
7137
|
}
|
|
7036
7138
|
const tmp = `${dest}.${process.pid}-${Date.now()}.part`;
|
|
7037
7139
|
try {
|
|
7038
|
-
await
|
|
7140
|
+
await downloadHttpsOnly(url, tmp, 6e5);
|
|
7039
7141
|
fs29.renameSync(tmp, dest);
|
|
7040
7142
|
} catch (e) {
|
|
7041
7143
|
fs29.rmSync(tmp, { force: true });
|
|
@@ -7052,15 +7154,55 @@ function defaultOutputPath(state, ext) {
|
|
|
7052
7154
|
const lang = state.outputLanguage ? `-${safeSlug(state.outputLanguage)}` : "";
|
|
7053
7155
|
return path32.join(getOutputDir(), `heygen-${state.kind}${lang}-${safeSlug(state.id)}.${ext}`);
|
|
7054
7156
|
}
|
|
7055
|
-
function
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
return path32.join(dir, path32.basename(defaultOutputPath(state, ext)));
|
|
7157
|
+
function statOrUndefined(p) {
|
|
7158
|
+
try {
|
|
7159
|
+
return fs29.statSync(p);
|
|
7160
|
+
} catch {
|
|
7161
|
+
return void 0;
|
|
7061
7162
|
}
|
|
7062
|
-
return path32.resolve(output);
|
|
7063
7163
|
}
|
|
7164
|
+
function isDirectoryOutput2(output, target) {
|
|
7165
|
+
if (/[\\/]$/.test(output)) return true;
|
|
7166
|
+
const stat = statOrUndefined(target);
|
|
7167
|
+
if (stat) return stat.isDirectory();
|
|
7168
|
+
return path32.extname(target) === "";
|
|
7169
|
+
}
|
|
7170
|
+
function withMediaExt(file, ext) {
|
|
7171
|
+
const given = path32.extname(file).slice(1).toLowerCase();
|
|
7172
|
+
const mismatch = VIDEO_EXTS.has(given) && AUDIO_EXTS.has(ext) || AUDIO_EXTS.has(given) && VIDEO_EXTS.has(ext);
|
|
7173
|
+
return mismatch ? `${file.slice(0, -(given.length + 1))}.${ext}` : file;
|
|
7174
|
+
}
|
|
7175
|
+
function outputPathFor(state, ext, output, count) {
|
|
7176
|
+
if (!output) return defaultOutputPath(state, ext);
|
|
7177
|
+
const target = path32.resolve(output);
|
|
7178
|
+
const givenExt = path32.extname(target);
|
|
7179
|
+
const isTemplate = count > 1 && MEDIA_EXTS.has(givenExt.slice(1).toLowerCase());
|
|
7180
|
+
if (isDirectoryOutput2(output, target) || count > 1 && !isTemplate) {
|
|
7181
|
+
fs29.mkdirSync(target, { recursive: true });
|
|
7182
|
+
return path32.join(target, path32.basename(defaultOutputPath(state, ext)));
|
|
7183
|
+
}
|
|
7184
|
+
if (!isTemplate) return withMediaExt(target, ext);
|
|
7185
|
+
const base = path32.basename(target, givenExt);
|
|
7186
|
+
const suffix = safeSlug(state.outputLanguage ?? state.id);
|
|
7187
|
+
return withMediaExt(path32.join(path32.dirname(target), `${base}-${suffix}${givenExt}`), ext);
|
|
7188
|
+
}
|
|
7189
|
+
function claimPath(dest, state, used) {
|
|
7190
|
+
const ext = path32.extname(dest);
|
|
7191
|
+
const stem = dest.slice(0, dest.length - ext.length);
|
|
7192
|
+
let candidate = dest;
|
|
7193
|
+
for (let n = 1; used.has(candidate.toLowerCase()); n++) {
|
|
7194
|
+
const id = safeSlug(state.id);
|
|
7195
|
+
candidate = n === 1 ? `${stem}-${id}${ext}` : `${stem}-${id}-${n}${ext}`;
|
|
7196
|
+
}
|
|
7197
|
+
used.add(candidate.toLowerCase());
|
|
7198
|
+
return candidate;
|
|
7199
|
+
}
|
|
7200
|
+
function captionPathFor(dest, capExt) {
|
|
7201
|
+
const stem = path32.join(path32.dirname(dest), path32.basename(dest, path32.extname(dest)));
|
|
7202
|
+
const capDest = `${stem}.${capExt}`;
|
|
7203
|
+
return capDest.toLowerCase() === dest.toLowerCase() ? `${stem}.captions.${capExt}` : capDest;
|
|
7204
|
+
}
|
|
7205
|
+
var downloadThumbHttpsOnly = (url, dest) => downloadHttpsOnly(url, dest, 12e4);
|
|
7064
7206
|
function summarizeState(state) {
|
|
7065
7207
|
const out = {
|
|
7066
7208
|
kind: state.kind,
|
|
@@ -7078,7 +7220,7 @@ function summarizeState(state) {
|
|
|
7078
7220
|
if (state.failureMessage) out.failure_message = state.failureMessage;
|
|
7079
7221
|
return out;
|
|
7080
7222
|
}
|
|
7081
|
-
async function deliverOne(state, opts, count, logger, files) {
|
|
7223
|
+
async function deliverOne(state, opts, count, logger, files, usedPaths) {
|
|
7082
7224
|
const hint = `Refresh with: ${resumeCommand2(state.kind, state.id, opts)}`;
|
|
7083
7225
|
if (opts.download === false) {
|
|
7084
7226
|
if (!state.outputUrl) {
|
|
@@ -7091,39 +7233,42 @@ async function deliverOne(state, opts, count, logger, files) {
|
|
|
7091
7233
|
};
|
|
7092
7234
|
return { state, files, urls };
|
|
7093
7235
|
}
|
|
7094
|
-
const ext = inferExtension(state.outputUrl ?? "", "mp4");
|
|
7095
|
-
const dest = outputPathFor(state, ext, opts.output, count);
|
|
7236
|
+
const ext = inferExtension(state.outputUrl ?? "", state.outputIsAudio ? "mp3" : "mp4");
|
|
7237
|
+
const dest = claimPath(outputPathFor(state, ext, opts.output, count), state, usedPaths);
|
|
7096
7238
|
await downloadOutput(state.outputUrl, dest, hint);
|
|
7097
7239
|
logger.success(`Saved ${dest}`);
|
|
7098
7240
|
files.push(dest);
|
|
7099
7241
|
if (opts.captions && state.captionUrl) {
|
|
7100
7242
|
const capExt = inferExtension(state.captionUrl, "srt");
|
|
7101
|
-
const capDest =
|
|
7102
|
-
path32.dirname(dest),
|
|
7103
|
-
`${path32.basename(dest, path32.extname(dest))}.${capExt}`
|
|
7104
|
-
);
|
|
7243
|
+
const capDest = claimPath(captionPathFor(dest, capExt), state, usedPaths);
|
|
7105
7244
|
await downloadOutput(state.captionUrl, capDest, hint);
|
|
7106
7245
|
logger.success(`Saved ${capDest}`);
|
|
7107
7246
|
files.push(capDest);
|
|
7108
7247
|
} else if (opts.captions) {
|
|
7109
7248
|
logger.warn(`No caption file available for ${state.kind} ${state.id}`);
|
|
7110
7249
|
}
|
|
7111
|
-
if (state.kind === "video" || state.kind === "agent") {
|
|
7112
|
-
const
|
|
7113
|
-
|
|
7114
|
-
logger
|
|
7115
|
-
}
|
|
7116
|
-
|
|
7250
|
+
if ((state.kind === "video" || state.kind === "agent") && opts.thumb !== false) {
|
|
7251
|
+
const thumbUrl = detectThumbUrl(state.raw);
|
|
7252
|
+
if (thumbUrl && !isHttpsUrl(thumbUrl)) {
|
|
7253
|
+
logger.warn(`Skipping thumbnail from a non-https URL (${redactUrl(thumbUrl)})`);
|
|
7254
|
+
} else {
|
|
7255
|
+
const thumb = await maybeDownloadThumb(state.raw, dest, {
|
|
7256
|
+
logger,
|
|
7257
|
+
download: downloadThumbHttpsOnly
|
|
7258
|
+
});
|
|
7259
|
+
if (thumb) files.push(thumb);
|
|
7260
|
+
}
|
|
7117
7261
|
}
|
|
7118
7262
|
return { state, files };
|
|
7119
7263
|
}
|
|
7120
7264
|
async function deliverOutputs(states, opts, logger) {
|
|
7121
7265
|
const finished = [];
|
|
7122
7266
|
const failures = [];
|
|
7267
|
+
const usedPaths = /* @__PURE__ */ new Set();
|
|
7123
7268
|
for (const state of states) {
|
|
7124
7269
|
const files = [];
|
|
7125
7270
|
try {
|
|
7126
|
-
finished.push(await deliverOne(state, opts, states.length, logger, files));
|
|
7271
|
+
finished.push(await deliverOne(state, opts, states.length, logger, files, usedPaths));
|
|
7127
7272
|
} catch (e) {
|
|
7128
7273
|
failures.push(`${state.kind} ${state.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
7129
7274
|
if (files.length > 0) finished.push({ state, files });
|
|
@@ -7176,9 +7321,10 @@ async function completeJobs(client, kind, ids, opts, logger) {
|
|
|
7176
7321
|
);
|
|
7177
7322
|
const settled = await Promise.allSettled(
|
|
7178
7323
|
ids.map(
|
|
7179
|
-
(id) => waitForJob(client, kind, id, {
|
|
7324
|
+
(id, index) => waitForJob(client, kind, id, {
|
|
7180
7325
|
timeoutMs,
|
|
7181
7326
|
intervalMs,
|
|
7327
|
+
startDelayMs: staggerOffsetMs(index, intervalMs),
|
|
7182
7328
|
maxIntervalMs: Math.max(intervalMs, 3e4),
|
|
7183
7329
|
burnCaptions: opts.burnCaptions,
|
|
7184
7330
|
resumeHint: `Resume with: ${resumeCommand2(kind, id, opts)}`,
|
|
@@ -10079,8 +10225,8 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
10079
10225
|
}
|
|
10080
10226
|
fs43.mkdirSync(path50.dirname(dest), { recursive: true });
|
|
10081
10227
|
const ws = fs43.createWriteStream(dest);
|
|
10082
|
-
const { pipeline:
|
|
10083
|
-
await
|
|
10228
|
+
const { pipeline: pipeline4 } = await import("stream/promises");
|
|
10229
|
+
await pipeline4(r.body, ws);
|
|
10084
10230
|
logger.success(`Saved ${dest}`);
|
|
10085
10231
|
if (opts.output) {
|
|
10086
10232
|
fs43.mkdirSync(path50.dirname(path50.resolve(opts.output)), { recursive: true });
|
|
@@ -10408,7 +10554,7 @@ function assertSucceeded(prediction2) {
|
|
|
10408
10554
|
PROVIDER
|
|
10409
10555
|
);
|
|
10410
10556
|
}
|
|
10411
|
-
var
|
|
10557
|
+
var defaultSleep4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10412
10558
|
var ReplicateClient = class {
|
|
10413
10559
|
apiKey;
|
|
10414
10560
|
baseUrl;
|
|
@@ -10419,7 +10565,7 @@ var ReplicateClient = class {
|
|
|
10419
10565
|
this.apiKey = opts.apiKey;
|
|
10420
10566
|
this.baseUrl = (opts.baseUrl || DEFAULT_REPLICATE_BASE_URL).replace(/\/+$/, "");
|
|
10421
10567
|
this.doFetch = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
10422
|
-
this.sleep = opts.sleep ??
|
|
10568
|
+
this.sleep = opts.sleep ?? defaultSleep4;
|
|
10423
10569
|
}
|
|
10424
10570
|
headers(extra = {}) {
|
|
10425
10571
|
return { accept: "application/json", authorization: `Bearer ${this.apiKey}`, ...extra };
|
|
@@ -10610,11 +10756,11 @@ init_errors();
|
|
|
10610
10756
|
// src/providers/replicate/node-outputs.ts
|
|
10611
10757
|
init_errors();
|
|
10612
10758
|
init_redact();
|
|
10613
|
-
import { once as
|
|
10759
|
+
import { once as once3 } from "events";
|
|
10614
10760
|
import fs44 from "fs";
|
|
10615
10761
|
import path51 from "path";
|
|
10616
10762
|
import { Readable } from "stream";
|
|
10617
|
-
import { pipeline as
|
|
10763
|
+
import { pipeline as pipeline3 } from "stream/promises";
|
|
10618
10764
|
|
|
10619
10765
|
// src/providers/replicate/output.ts
|
|
10620
10766
|
init_redact();
|
|
@@ -10785,12 +10931,12 @@ async function writeResponse(response, dest) {
|
|
|
10785
10931
|
if (!response.body) throw new HttpError(0, "Response body is null", response.url);
|
|
10786
10932
|
const fileStream = fs44.createWriteStream(dest);
|
|
10787
10933
|
try {
|
|
10788
|
-
await
|
|
10934
|
+
await pipeline3(
|
|
10789
10935
|
Readable.fromWeb(response.body),
|
|
10790
10936
|
fileStream
|
|
10791
10937
|
);
|
|
10792
10938
|
} catch (err2) {
|
|
10793
|
-
if (!fileStream.closed) await
|
|
10939
|
+
if (!fileStream.closed) await once3(fileStream, "close");
|
|
10794
10940
|
throw err2;
|
|
10795
10941
|
}
|
|
10796
10942
|
}
|
|
@@ -11460,7 +11606,6 @@ function registerCheckCommand(program2) {
|
|
|
11460
11606
|
}
|
|
11461
11607
|
|
|
11462
11608
|
// src/commands/doc/convert.ts
|
|
11463
|
-
init_errors();
|
|
11464
11609
|
import fs47 from "fs";
|
|
11465
11610
|
import path53 from "path";
|
|
11466
11611
|
|
|
@@ -11478,27 +11623,45 @@ Requirements:
|
|
|
11478
11623
|
Output only the markdown content without any preamble or explanation.`;
|
|
11479
11624
|
|
|
11480
11625
|
// src/commands/doc/convert.ts
|
|
11626
|
+
function autoNameBase(input) {
|
|
11627
|
+
if (input === STDIN_ARG) return "stdin";
|
|
11628
|
+
return path53.basename(input, path53.extname(input));
|
|
11629
|
+
}
|
|
11481
11630
|
async function convertDocuments(opts) {
|
|
11482
|
-
const {
|
|
11631
|
+
const { output, autoName = false, verbose, logger } = opts;
|
|
11483
11632
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
11484
11633
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
}
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11634
|
+
const { inputs, cleanup } = await resolveFileInputs(opts.inputs, {
|
|
11635
|
+
stdinExt: opts.stdinExt,
|
|
11636
|
+
input: opts.stdin,
|
|
11637
|
+
flag: "--input",
|
|
11638
|
+
example: "cat report.pdf | multix doc convert --input -",
|
|
11639
|
+
extExample: "pdf"
|
|
11640
|
+
});
|
|
11641
|
+
let failed;
|
|
11642
|
+
try {
|
|
11643
|
+
let outputPath;
|
|
11644
|
+
if (output) {
|
|
11645
|
+
outputPath = path53.resolve(output);
|
|
11646
|
+
} else if (autoName && opts.inputs.length === 1 && opts.inputs[0]) {
|
|
11647
|
+
outputPath = path53.join(getOutputDir(), `${autoNameBase(opts.inputs[0])}-extraction.md`);
|
|
11648
|
+
} else {
|
|
11649
|
+
outputPath = path53.join(getOutputDir(), "document-extraction.md");
|
|
11650
|
+
}
|
|
11651
|
+
fs47.mkdirSync(path53.dirname(outputPath), { recursive: true });
|
|
11652
|
+
failed = await convertResolved(inputs, { outputPath, model, prompt, verbose, logger });
|
|
11653
|
+
} finally {
|
|
11654
|
+
cleanup();
|
|
11496
11655
|
}
|
|
11497
|
-
|
|
11656
|
+
if (failed > 0) process.exit(1);
|
|
11657
|
+
}
|
|
11658
|
+
async function convertResolved(inputs, ctx) {
|
|
11659
|
+
const { outputPath, model, prompt, verbose, logger } = ctx;
|
|
11498
11660
|
const results = [];
|
|
11499
11661
|
for (let i = 0; i < inputs.length; i++) {
|
|
11500
|
-
const
|
|
11501
|
-
|
|
11662
|
+
const input = inputs[i];
|
|
11663
|
+
const filePath = input.path;
|
|
11664
|
+
logger?.debug(`[${i + 1}/${inputs.length}] Converting: ${input.label}`);
|
|
11502
11665
|
let markdown;
|
|
11503
11666
|
let errorMsg;
|
|
11504
11667
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
@@ -11530,7 +11693,7 @@ async function convertDocuments(opts) {
|
|
|
11530
11693
|
markdown = extractText(resp);
|
|
11531
11694
|
break;
|
|
11532
11695
|
} catch (e) {
|
|
11533
|
-
errorMsg = e instanceof Error ? e.message : String(e);
|
|
11696
|
+
errorMsg = labelError(e instanceof Error ? e.message : String(e), input);
|
|
11534
11697
|
if (isGeminiRateLimitError(e)) break;
|
|
11535
11698
|
if (attempt < 2) {
|
|
11536
11699
|
const wait = 2 ** attempt * 1e3;
|
|
@@ -11539,8 +11702,11 @@ async function convertDocuments(opts) {
|
|
|
11539
11702
|
}
|
|
11540
11703
|
}
|
|
11541
11704
|
}
|
|
11705
|
+
if (markdown === void 0) {
|
|
11706
|
+
logger?.error(`Failed: ${input.label} \u2014 ${errorMsg}`);
|
|
11707
|
+
}
|
|
11542
11708
|
results.push({
|
|
11543
|
-
file:
|
|
11709
|
+
file: input.label,
|
|
11544
11710
|
status: markdown !== void 0 ? "success" : "error",
|
|
11545
11711
|
markdown,
|
|
11546
11712
|
error: errorMsg
|
|
@@ -11554,7 +11720,7 @@ async function convertDocuments(opts) {
|
|
|
11554
11720
|
`);
|
|
11555
11721
|
for (const r of results) {
|
|
11556
11722
|
lines.push(`
|
|
11557
|
-
## ${path53.basename(r.file)}
|
|
11723
|
+
## ${path53.basename(r.file).replace(/[<>]/g, "\\$&")}
|
|
11558
11724
|
`);
|
|
11559
11725
|
if (r.status === "success" && r.markdown) {
|
|
11560
11726
|
lines.push(`${r.markdown}
|
|
@@ -11580,8 +11746,7 @@ ${"=".repeat(50)}`);
|
|
|
11580
11746
|
console.log(` [${r.status}] ${r.file}`);
|
|
11581
11747
|
}
|
|
11582
11748
|
}
|
|
11583
|
-
|
|
11584
|
-
if (failed > 0) process.exit(1);
|
|
11749
|
+
return results.length - succeeded;
|
|
11585
11750
|
}
|
|
11586
11751
|
function sleep4(ms) {
|
|
11587
11752
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -11594,14 +11759,21 @@ function registerDocCommands(program2) {
|
|
|
11594
11759
|
);
|
|
11595
11760
|
doc.command("convert").description("Convert documents to Markdown using Gemini Files API").requiredOption(
|
|
11596
11761
|
"--input <files...>",
|
|
11597
|
-
"Input file(s) to convert",
|
|
11762
|
+
"Input file(s) to convert (use - to read piped stdin)",
|
|
11598
11763
|
(val, prev) => [...prev, val],
|
|
11599
11764
|
[]
|
|
11600
|
-
).alias("-i").option(
|
|
11765
|
+
).alias("-i").option(
|
|
11766
|
+
"--stdin-ext <ext>",
|
|
11767
|
+
"Extension for piped stdin when auto-detection fails (e.g. pdf, txt)"
|
|
11768
|
+
).option("--output <path>", "Output markdown file path").option(
|
|
11769
|
+
"--auto-name",
|
|
11770
|
+
"Derive output filename from input basename (single file only; stdin becomes stdin-extraction.md)"
|
|
11771
|
+
).option("--model <id>", `Gemini model to use (default: ${DOC_MODEL_DEFAULT})`).option("--prompt <text>", "Custom conversion prompt").option("-v, --verbose", "Verbose logging").action(
|
|
11601
11772
|
async (opts) => {
|
|
11602
11773
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
11603
11774
|
await convertDocuments({
|
|
11604
11775
|
inputs: opts.input,
|
|
11776
|
+
stdinExt: opts.stdinExt,
|
|
11605
11777
|
output: opts.output,
|
|
11606
11778
|
autoName: opts.autoName,
|
|
11607
11779
|
model: opts.model,
|
|
@@ -11623,13 +11795,13 @@ import fs48 from "fs";
|
|
|
11623
11795
|
import path54 from "path";
|
|
11624
11796
|
|
|
11625
11797
|
// src/commands/media/detect-type.ts
|
|
11626
|
-
var
|
|
11627
|
-
var
|
|
11798
|
+
var VIDEO_EXTS2 = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
11799
|
+
var AUDIO_EXTS2 = /* @__PURE__ */ new Set([".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg", ".aiff"]);
|
|
11628
11800
|
var IMAGE_EXTS2 = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".gif", ".bmp"]);
|
|
11629
11801
|
function extToKind(ext) {
|
|
11630
11802
|
const lower = ext.toLowerCase();
|
|
11631
|
-
if (
|
|
11632
|
-
if (
|
|
11803
|
+
if (VIDEO_EXTS2.has(lower)) return "video";
|
|
11804
|
+
if (AUDIO_EXTS2.has(lower)) return "audio";
|
|
11633
11805
|
if (IMAGE_EXTS2.has(lower)) return "image";
|
|
11634
11806
|
return void 0;
|
|
11635
11807
|
}
|
|
@@ -11805,7 +11977,7 @@ async function optimizeVideo(opts) {
|
|
|
11805
11977
|
}
|
|
11806
11978
|
|
|
11807
11979
|
// src/commands/media/batch.ts
|
|
11808
|
-
var ALL_EXTS = /* @__PURE__ */ new Set([...
|
|
11980
|
+
var ALL_EXTS = /* @__PURE__ */ new Set([...VIDEO_EXTS2, ...AUDIO_EXTS2, ...IMAGE_EXTS2]);
|
|
11809
11981
|
async function batchOptimize(opts) {
|
|
11810
11982
|
const {
|
|
11811
11983
|
inputDir,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrgoonie/multix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "AI multimodal CLI — OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, HeyGen, ffmpeg, ImageMagick, doc-to-md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/skill/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: multix
|
|
3
3
|
description: AI multimodal CLI — generate and edit images (OpenAI, Codex driver, Gemini Nano Banana, Imagen, MiniMax, OpenRouter, Leonardo, BytePlus Seedream, Cloudflare Workers AI FLUX.1 Schnell, fal.ai FLUX, Replicate FLUX), generate video (Gemini Veo, MiniMax Hailuo, OpenRouter via Veo/Seedance/Kling, Leonardo via Motion/Veo/Kling/Hailuo/Seedance/LTX, BytePlus Seedance, Cloudflare AI Gateway via Replicate, fal.ai queue models, Replicate direct API models incl. P-Video, HeyGen Video Agent and scripted avatar videos), video translation and lipsync (HeyGen), TTS/STT/music (OpenAI, Gemini Flash TTS incl. Gemini 3.8 with delivery-style control, MiniMax, Cloudflare Workers AI MeloTTS, ElevenLabs), 3D models (BytePlus Hyper3D/Hitem3d), analyze/transcribe media, convert documents to Markdown, optimize media via ffmpeg/ImageMagick, and self-update the CLI. Use whenever the user wants to create, edit, or transform images/video/audio/3D/documents from a CLI, mentions any of: OpenAI, Codex image driver, image-to-image, i2i, image edit, watercolor/cyberpunk style transfer, reference image, OpenRouter, Nano Banana, Flux, Seedream, Veo, Hailuo, Seedance, Kling, Cloudflare Workers AI, AI Gateway, fal.ai, Atlas Cloud, Replicate, replicate.com models, Replicate predictions, HeyGen, avatar video, talking head, Video Agent, video translation, dubbing, lipsync, video generation, text-to-video, image-to-video, i2v, ElevenLabs voice cloning, updating/upgrading multix, or asks for batch media optimization.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|
|
@@ -194,7 +194,7 @@ multix heygen video-agent "45-second explainer for a note app" [--avatar <look-i
|
|
|
194
194
|
multix heygen avatars [--type photo_avatar] [--cursor <token>]
|
|
195
195
|
multix heygen voices [--language English]
|
|
196
196
|
multix heygen video "Hello!" --avatar <look-id> [--voice <id>] [--aspect-ratio 9:16] [--engine avatar_iv|avatar_v|avatar_iii] [--burn-captions]
|
|
197
|
-
multix heygen translate <ref> --languages "Spanish, Chinese (Cantonese, Traditional)" [--output <dir>]
|
|
197
|
+
multix heygen translate <ref> --languages "Spanish, Chinese (Cantonese, Traditional)" [--output <dir|dub.mp4>] # several languages: dub.mp4 -> dub-Spanish.mp4, ...
|
|
198
198
|
multix heygen lipsync --video <ref> --audio <ref>
|
|
199
199
|
multix heygen tts "Hello" --voice <voice-id>
|
|
200
200
|
multix heygen template <template-id> --input @vars.json
|
|
@@ -354,6 +354,7 @@ multix byteplus 3d --input-image ./front.png ./side.png --prompt "Generate from
|
|
|
354
354
|
|
|
355
355
|
# Docs → Markdown (Gemini)
|
|
356
356
|
multix doc convert --input report.pdf [--auto-name] [--output ./out.md]
|
|
357
|
+
cat report.pdf | multix doc convert --input - --output ./out.md # "-" reads stdin; --stdin-ext <ext> (e.g. pdf) if undetected
|
|
357
358
|
|
|
358
359
|
# Media optimization (ffmpeg + ImageMagick)
|
|
359
360
|
multix media optimize --input <file> --output <file> [--target-size <MB>] [--quality 85] [--max-width 1920]
|