mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
* fix(media-use): tag HeyGen TTS generation with attribution header
Centralizes the X-HeyGen-Client-Source header into HEYGEN_CLIENT_SOURCE_ARGV
in heygen-cli.mjs and reuses it in heygen-search.mjs (dropping the duplicated
inline literal) so voice-provider's `voice speech create` call carries it too.
The generation call was previously untagged, making media-use TTS usage
invisible in HeyGen's billing/analytics warehouse; the read-only `voice list`
discovery call intentionally stays untagged.
* feat(media-use): add local LTX video generate provider
* feat(media-use): add HeyGen avatar-video generate provider
* feat(media-use): register video as a real provider type
* docs(media-use): document the wired video type and full HeyGen tagging coverage
resolve --type video is now the default path (HeyGen avatar video first,
local LTX fallback, sign-in nudge on auth failure) instead of a manual
recipe; correct the claim that only search requests are tagged now that
TTS and avatar-video generation carry the attribution header too.
* fix(media-use): wire --avatar-id/--voice-id CLI flags and close video-provider auth/cache gaps
- resolve.mjs never implemented the --avatar-id/--voice-id override that
operations.md documented, so following the docs crashed with
ERR_PARSE_ARGS_UNKNOWN_OPTION; wire the flags through to ctx.
- defaultAvatarId/defaultStarfishVoiceId cached a failed discovery lookup
as a permanent null, disabling heygen.video after one transient miss;
cache only a truthy id, matching the same fix in voice-provider.mjs's
defaultVoiceId.
- the avatar-video onboarding nudge only fired on a video-create failure,
never when avatar/voice discovery itself was unauthenticated (the
common unauthenticated case) -- propagate the discovery failure reason
so onboarding fires either way.
- dedupe the CLI-shelling JSON helper (heygen-cli.mjs's new runHeygenJson)
and the local-model argv-template builder (local-models.mjs's new
buildArgv) instead of leaving byte-identical copies in each provider.
* fix(media-use): address avatar-video PR review feedback
- heygenVideoGenerate short-circuits after the first discovery-call
failure instead of always attempting both avatar list and voice list,
so an unauthenticated caller gets one onboarding message and one
provider-error telemetry ping instead of a double-fire.
- runHeygenJson logs a diagnostic when a CLI call succeeds but returns
unparseable JSON, instead of silently returning null.
- dedupe the "avatar video is free" onboarding string into one constant
(was duplicated across three call sites).
* fix(media-use): match review-requested naming and message conventions
- export AVATAR_VIDEO_SIGNIN_MESSAGE from heygen-video-provider.mjs so
the test imports the canonical string instead of redeclaring it.
- runHeygenJson's non-JSON diagnostic now matches heygen-search.mjs's
existing wording ("returned non-JSON output").
137 lines
3.8 KiB
JavaScript
137 lines
3.8 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { dirname } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { ltxVideoGenerate } from "./ltx-video-provider.mjs";
|
|
|
|
const fittingSpecs = { availableRamMB: 20000, gpu: { present: true } };
|
|
|
|
test("no fitting local model: falls through without checking for a binary", async (t) => {
|
|
t.mock.method(console, "error", () => {});
|
|
const calls = [];
|
|
|
|
const result = await ltxVideoGenerate(
|
|
"a calm ocean wave at sunset",
|
|
{ specs: { availableRamMB: 100, gpu: { present: true } } },
|
|
(...call) => calls.push(call),
|
|
() => true,
|
|
);
|
|
|
|
assert.equal(result, null);
|
|
assert.deepEqual(calls, []);
|
|
});
|
|
|
|
test("binary missing from PATH: prints the model install hint and falls through", async (t) => {
|
|
const errors = [];
|
|
t.mock.method(console, "error", (message) => errors.push(message));
|
|
const calls = [];
|
|
const fakeExec = (...call) => {
|
|
calls.push(call);
|
|
throw new Error("not found");
|
|
};
|
|
|
|
const result = await ltxVideoGenerate(
|
|
"a calm ocean wave at sunset",
|
|
{ specs: fittingSpecs },
|
|
fakeExec,
|
|
);
|
|
|
|
assert.equal(result, null);
|
|
assert.equal(calls.length, 1);
|
|
assert.deepEqual(calls[0].slice(0, 2), ["which", ["ltx-2-mlx"]]);
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /git clone https:\/\/github\.com\/dgrauet\/ltx-2-mlx/);
|
|
});
|
|
|
|
test("generate argv substitutes a spaced prompt after tokenizing and uses verified defaults", async () => {
|
|
const calls = [];
|
|
const checkedPaths = [];
|
|
const fakeExec = (...call) => calls.push(call);
|
|
const pathExists = (path) => {
|
|
checkedPaths.push(path);
|
|
return false;
|
|
};
|
|
const intent = "a calm ocean wave at sunset";
|
|
|
|
const result = await ltxVideoGenerate(intent, { specs: fittingSpecs }, fakeExec, pathExists);
|
|
|
|
assert.equal(result, null);
|
|
assert.equal(calls.length, 2);
|
|
const [bin, argv, opts] = calls[1];
|
|
assert.equal(bin, "ltx-2-mlx");
|
|
assert.equal(opts.timeout, 1_800_000);
|
|
|
|
const expectedPairs = [
|
|
["--prompt", intent],
|
|
["--width", "512"],
|
|
["--height", "320"],
|
|
["--frames", "33"],
|
|
["--output", checkedPaths[0]],
|
|
];
|
|
let previousIndex = -1;
|
|
for (const [flag, value] of expectedPairs) {
|
|
const index = argv.indexOf(flag);
|
|
assert.ok(index > previousIndex, `${flag} should follow the previous required option`);
|
|
assert.equal(argv[index + 1], value);
|
|
previousIndex = index;
|
|
}
|
|
assert.equal(argv.filter((arg) => arg === intent).length, 1);
|
|
});
|
|
|
|
test("successful generation returns the generated MP4 result", async () => {
|
|
const calls = [];
|
|
const fakeExec = (...call) => calls.push(call);
|
|
const intent = "a calm ocean wave at sunset";
|
|
|
|
const result = await ltxVideoGenerate(intent, { specs: fittingSpecs }, fakeExec, () => true);
|
|
|
|
assert.ok(result);
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(dirname(result.localPath), tmpdir());
|
|
assert.match(result.localPath, /media-use-ltx-\d+-\d+\.mp4$/);
|
|
assert.deepEqual(result, {
|
|
localPath: result.localPath,
|
|
ext: ".mp4",
|
|
source: "generated",
|
|
metadata: {
|
|
description: intent,
|
|
provider: "ltx.local",
|
|
provenance: { prompt: intent },
|
|
},
|
|
});
|
|
});
|
|
|
|
test("generate failure returns null instead of throwing", async (t) => {
|
|
t.mock.method(console, "error", () => {});
|
|
let calls = 0;
|
|
const fakeExec = () => {
|
|
calls += 1;
|
|
if (calls === 2) {
|
|
const error = new Error("generation failed");
|
|
error.stderr = "LTX failed";
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const result = await ltxVideoGenerate(
|
|
"storm clouds",
|
|
{ specs: fittingSpecs },
|
|
fakeExec,
|
|
() => true,
|
|
);
|
|
|
|
assert.equal(result, null);
|
|
assert.equal(calls, 2);
|
|
});
|
|
|
|
test("missing generated output returns null", async () => {
|
|
const result = await ltxVideoGenerate(
|
|
"storm clouds",
|
|
{ specs: fittingSpecs },
|
|
() => {},
|
|
() => false,
|
|
);
|
|
|
|
assert.equal(result, null);
|
|
});
|