ai-cli 0.1.1 → 0.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.
- package/README.md +30 -3
- package/dist/index.js +30027 -0
- package/{src/lib/openh264.wasm → dist/openh264-d6yed0d8.wasm} +0 -0
- package/package.json +9 -5
- package/src/cli.test.ts +0 -95
- package/src/commands/completions.ts +0 -296
- package/src/commands/image.ts +0 -136
- package/src/commands/models.ts +0 -117
- package/src/commands/text.ts +0 -117
- package/src/commands/video.ts +0 -113
- package/src/index.ts +0 -30
- package/src/lib/color.ts +0 -5
- package/src/lib/h264-wasm.ts +0 -164
- package/src/lib/h264.test.ts +0 -48
- package/src/lib/jobs.ts +0 -192
- package/src/lib/kitty.ts +0 -55
- package/src/lib/models.test.ts +0 -197
- package/src/lib/models.ts +0 -163
- package/src/lib/mp4.test.ts +0 -231
- package/src/lib/mp4.ts +0 -560
- package/src/lib/openh264.d.mts +0 -28
- package/src/lib/openh264.mjs +0 -423
- package/src/lib/openh264.wasm.d.ts +0 -2
- package/src/lib/output.ts +0 -97
- package/src/lib/p-map.test.ts +0 -63
- package/src/lib/p-map.ts +0 -30
- package/src/lib/parse.test.ts +0 -114
- package/src/lib/parse.ts +0 -44
- package/src/lib/png.test.ts +0 -104
- package/src/lib/png.ts +0 -90
- package/src/lib/progress.ts +0 -214
- package/src/lib/shimmer.test.ts +0 -39
- package/src/lib/shimmer.ts +0 -42
- package/src/lib/stdin.ts +0 -31
package/src/lib/kitty.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
const SUPPORTED_TERMS = new Set(["xterm-kitty"]);
|
|
2
|
-
|
|
3
|
-
const SUPPORTED_TERM_PROGRAMS = new Set([
|
|
4
|
-
"kitty",
|
|
5
|
-
"ghostty",
|
|
6
|
-
"wezterm",
|
|
7
|
-
"warpterminal",
|
|
8
|
-
]);
|
|
9
|
-
|
|
10
|
-
export function supportsKittyGraphics(): boolean {
|
|
11
|
-
if (process.env.AI_CLI_PREVIEW === "0") return false;
|
|
12
|
-
if (process.env.AI_CLI_PREVIEW === "1") return true;
|
|
13
|
-
|
|
14
|
-
const term = process.env.TERM ?? "";
|
|
15
|
-
if (SUPPORTED_TERMS.has(term)) return true;
|
|
16
|
-
|
|
17
|
-
const termProgram = (process.env.TERM_PROGRAM ?? "").toLowerCase();
|
|
18
|
-
if (SUPPORTED_TERM_PROGRAMS.has(termProgram)) return true;
|
|
19
|
-
|
|
20
|
-
const lcTerminal = (process.env.LC_TERMINAL ?? "").toLowerCase();
|
|
21
|
-
if (lcTerminal === "iterm2") return true;
|
|
22
|
-
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
import { decodeIDR } from "./h264-wasm.js";
|
|
27
|
-
import { extractKeyframe } from "./mp4.js";
|
|
28
|
-
import { encodePNG } from "./png.js";
|
|
29
|
-
|
|
30
|
-
const CHUNK_SIZE = 4096;
|
|
31
|
-
|
|
32
|
-
export async function displayVideoFrame(buf: Buffer): Promise<void> {
|
|
33
|
-
try {
|
|
34
|
-
const kf = extractKeyframe(new Uint8Array(buf));
|
|
35
|
-
if (!kf) return;
|
|
36
|
-
const frame = await decodeIDR(kf.sps, kf.pps, kf.sliceData);
|
|
37
|
-
if (!frame) return;
|
|
38
|
-
const png = encodePNG(frame.yuv, frame.width, frame.height);
|
|
39
|
-
displayImage(png);
|
|
40
|
-
} catch {
|
|
41
|
-
// Preview is best-effort; skip silently on any failure
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function displayImage(buf: Buffer): void {
|
|
46
|
-
const encoded = buf.toString("base64");
|
|
47
|
-
for (let i = 0; i < encoded.length; i += CHUNK_SIZE) {
|
|
48
|
-
const chunk = encoded.slice(i, i + CHUNK_SIZE);
|
|
49
|
-
const isLast = i + CHUNK_SIZE >= encoded.length;
|
|
50
|
-
const control =
|
|
51
|
-
i === 0 ? `a=T,f=100,m=${isLast ? 0 : 1}` : `m=${isLast ? 0 : 1}`;
|
|
52
|
-
process.stderr.write(`\x1b_G${control};${chunk}\x1b\\`);
|
|
53
|
-
}
|
|
54
|
-
process.stderr.write("\n");
|
|
55
|
-
}
|
package/src/lib/models.test.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
resolveModels,
|
|
5
|
-
fetchGatewayModels,
|
|
6
|
-
FALLBACK_TEXT_MODELS,
|
|
7
|
-
FALLBACK_IMAGE_MODELS,
|
|
8
|
-
FALLBACK_VIDEO_MODELS,
|
|
9
|
-
} from "./models.js";
|
|
10
|
-
|
|
11
|
-
describe("resolveModels", () => {
|
|
12
|
-
test("returns default when no user model", () => {
|
|
13
|
-
expect(resolveModels("text")[0]).toContain("/");
|
|
14
|
-
expect(resolveModels("image")[0]).toContain("/");
|
|
15
|
-
expect(resolveModels("video")[0]).toContain("/");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("returns fully-qualified model as-is", () => {
|
|
19
|
-
expect(resolveModels("text", "openai/gpt-4")).toEqual(["openai/gpt-4"]);
|
|
20
|
-
expect(resolveModels("image", "openai/gpt-image-1")).toEqual([
|
|
21
|
-
"openai/gpt-image-1",
|
|
22
|
-
]);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("expands short image model names", () => {
|
|
26
|
-
expect(resolveModels("image", "gpt-image-1")).toEqual([
|
|
27
|
-
"openai/gpt-image-1",
|
|
28
|
-
]);
|
|
29
|
-
expect(resolveModels("image", "flux-2-pro")).toEqual(["bfl/flux-2-pro"]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("expands short video model names", () => {
|
|
33
|
-
expect(resolveModels("video", "seedance-2.0")).toEqual([
|
|
34
|
-
"bytedance/seedance-2.0",
|
|
35
|
-
]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("expands short text model names", () => {
|
|
39
|
-
expect(resolveModels("text", "gpt-5.5")).toEqual(["openai/gpt-5.5"]);
|
|
40
|
-
expect(resolveModels("text", "o3")).toEqual(["openai/o3"]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("returns unknown short names as-is for text", () => {
|
|
44
|
-
expect(resolveModels("text", "gpt-image-1")).toEqual(["gpt-image-1"]);
|
|
45
|
-
expect(resolveModels("text", "my-model")).toEqual(["my-model"]);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("returns unknown short names as-is for image/video", () => {
|
|
49
|
-
expect(resolveModels("image", "nonexistent-model")).toEqual([
|
|
50
|
-
"nonexistent-model",
|
|
51
|
-
]);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe("resolveModels multi", () => {
|
|
56
|
-
test("returns default when no user model", () => {
|
|
57
|
-
const result = resolveModels("text");
|
|
58
|
-
expect(result).toHaveLength(1);
|
|
59
|
-
expect(result[0]).toContain("/");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("splits comma-separated models", () => {
|
|
63
|
-
const result = resolveModels("image", "openai/gpt-image-1,bfl/flux-2-pro");
|
|
64
|
-
expect(result).toEqual(["openai/gpt-image-1", "bfl/flux-2-pro"]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("trims whitespace around model names", () => {
|
|
68
|
-
const result = resolveModels(
|
|
69
|
-
"image",
|
|
70
|
-
"openai/gpt-image-1 , bfl/flux-2-pro"
|
|
71
|
-
);
|
|
72
|
-
expect(result).toEqual(["openai/gpt-image-1", "bfl/flux-2-pro"]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("expands short names in comma list", () => {
|
|
76
|
-
const result = resolveModels("image", "gpt-image-1,flux-2-pro");
|
|
77
|
-
expect(result).toEqual(["openai/gpt-image-1", "bfl/flux-2-pro"]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("filters empty segments from trailing comma", () => {
|
|
81
|
-
const result = resolveModels("image", "openai/gpt-image-1,");
|
|
82
|
-
expect(result).toEqual(["openai/gpt-image-1"]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("falls back to default when all segments are empty", () => {
|
|
86
|
-
const result = resolveModels("image", ",,,");
|
|
87
|
-
expect(result).toHaveLength(1);
|
|
88
|
-
expect(result[0]).toContain("/");
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("fetchGatewayModels", () => {
|
|
93
|
-
test("partitions models by modelType", async () => {
|
|
94
|
-
const { gateway } = await import("ai");
|
|
95
|
-
const original = gateway.getAvailableModels;
|
|
96
|
-
gateway.getAvailableModels = mock(() =>
|
|
97
|
-
Promise.resolve({
|
|
98
|
-
models: [
|
|
99
|
-
{ id: "openai/gpt-5", name: "GPT 5", modelType: "language" },
|
|
100
|
-
{
|
|
101
|
-
id: "openai/gpt-image-2",
|
|
102
|
-
name: "GPT Image 2",
|
|
103
|
-
description: "Image gen",
|
|
104
|
-
modelType: "image",
|
|
105
|
-
},
|
|
106
|
-
{ id: "google/veo-3.0", name: "Veo 3", modelType: "video" },
|
|
107
|
-
{
|
|
108
|
-
id: "openai/text-embedding-3",
|
|
109
|
-
name: "Embedding",
|
|
110
|
-
modelType: "embedding",
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
})
|
|
114
|
-
) as unknown as typeof gateway.getAvailableModels;
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const result = await fetchGatewayModels();
|
|
118
|
-
|
|
119
|
-
expect(result.text).toEqual([
|
|
120
|
-
{ id: "openai/gpt-5", name: "GPT 5", description: undefined },
|
|
121
|
-
]);
|
|
122
|
-
expect(result.image).toEqual([
|
|
123
|
-
{
|
|
124
|
-
id: "openai/gpt-image-2",
|
|
125
|
-
name: "GPT Image 2",
|
|
126
|
-
description: "Image gen",
|
|
127
|
-
},
|
|
128
|
-
]);
|
|
129
|
-
expect(result.video).toEqual([
|
|
130
|
-
{ id: "google/veo-3.0", name: "Veo 3", description: undefined },
|
|
131
|
-
]);
|
|
132
|
-
} finally {
|
|
133
|
-
gateway.getAvailableModels = original;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("falls back to static lists on gateway error", async () => {
|
|
138
|
-
const { gateway } = await import("ai");
|
|
139
|
-
const original = gateway.getAvailableModels;
|
|
140
|
-
gateway.getAvailableModels = mock(() =>
|
|
141
|
-
Promise.reject(new Error("network error"))
|
|
142
|
-
) as typeof gateway.getAvailableModels;
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const result = await fetchGatewayModels();
|
|
146
|
-
|
|
147
|
-
expect(result.text.map((m) => m.id)).toEqual(FALLBACK_TEXT_MODELS);
|
|
148
|
-
expect(result.image.map((m) => m.id)).toEqual(FALLBACK_IMAGE_MODELS);
|
|
149
|
-
expect(result.video.map((m) => m.id)).toEqual(FALLBACK_VIDEO_MODELS);
|
|
150
|
-
} finally {
|
|
151
|
-
gateway.getAvailableModels = original;
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("uses fallbacks when gateway returns no image/video models", async () => {
|
|
156
|
-
const { gateway } = await import("ai");
|
|
157
|
-
const original = gateway.getAvailableModels;
|
|
158
|
-
gateway.getAvailableModels = mock(() =>
|
|
159
|
-
Promise.resolve({
|
|
160
|
-
models: [{ id: "openai/gpt-5", name: "GPT 5", modelType: "language" }],
|
|
161
|
-
})
|
|
162
|
-
) as unknown as typeof gateway.getAvailableModels;
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const result = await fetchGatewayModels();
|
|
166
|
-
|
|
167
|
-
expect(result.text).toHaveLength(1);
|
|
168
|
-
expect(result.text[0].id).toBe("openai/gpt-5");
|
|
169
|
-
expect(result.image.map((m) => m.id)).toEqual(FALLBACK_IMAGE_MODELS);
|
|
170
|
-
expect(result.video.map((m) => m.id)).toEqual(FALLBACK_VIDEO_MODELS);
|
|
171
|
-
} finally {
|
|
172
|
-
gateway.getAvailableModels = original;
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("uses text fallbacks when gateway returns no text models", async () => {
|
|
177
|
-
const { gateway } = await import("ai");
|
|
178
|
-
const original = gateway.getAvailableModels;
|
|
179
|
-
gateway.getAvailableModels = mock(() =>
|
|
180
|
-
Promise.resolve({
|
|
181
|
-
models: [
|
|
182
|
-
{ id: "openai/gpt-image-2", name: "GPT Image 2", modelType: "image" },
|
|
183
|
-
],
|
|
184
|
-
})
|
|
185
|
-
) as unknown as typeof gateway.getAvailableModels;
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
const result = await fetchGatewayModels();
|
|
189
|
-
|
|
190
|
-
expect(result.text.map((m) => m.id)).toEqual(FALLBACK_TEXT_MODELS);
|
|
191
|
-
expect(result.image).toHaveLength(1);
|
|
192
|
-
expect(result.video.map((m) => m.id)).toEqual(FALLBACK_VIDEO_MODELS);
|
|
193
|
-
} finally {
|
|
194
|
-
gateway.getAvailableModels = original;
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
});
|
package/src/lib/models.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { gateway } from "ai";
|
|
2
|
-
|
|
3
|
-
export type Modality = "text" | "image" | "video";
|
|
4
|
-
|
|
5
|
-
const DEFAULTS: Record<Modality, string> = {
|
|
6
|
-
text: process.env.AI_CLI_TEXT_MODEL ?? "openai/gpt-5.5",
|
|
7
|
-
image: process.env.AI_CLI_IMAGE_MODEL ?? "openai/gpt-image-2",
|
|
8
|
-
video: process.env.AI_CLI_VIDEO_MODEL ?? "bytedance/seedance-2.0",
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const FALLBACK_TEXT_MODELS = [
|
|
12
|
-
"anthropic/claude-sonnet-4",
|
|
13
|
-
"google/gemini-2.5-pro",
|
|
14
|
-
"meta/llama-4-maverick",
|
|
15
|
-
"openai/gpt-4.1",
|
|
16
|
-
"openai/gpt-4.1-mini",
|
|
17
|
-
"openai/gpt-4.1-nano",
|
|
18
|
-
"openai/gpt-5.5",
|
|
19
|
-
"openai/o3",
|
|
20
|
-
"openai/o4-mini",
|
|
21
|
-
"xai/grok-3",
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
export const FALLBACK_IMAGE_MODELS = [
|
|
25
|
-
"bfl/flux-2-flex",
|
|
26
|
-
"bfl/flux-2-klein-4b",
|
|
27
|
-
"bfl/flux-2-klein-9b",
|
|
28
|
-
"bfl/flux-2-max",
|
|
29
|
-
"bfl/flux-2-pro",
|
|
30
|
-
"bfl/flux-kontext-max",
|
|
31
|
-
"bfl/flux-kontext-pro",
|
|
32
|
-
"bfl/flux-pro-1.0-fill",
|
|
33
|
-
"bfl/flux-pro-1.1",
|
|
34
|
-
"bfl/flux-pro-1.1-ultra",
|
|
35
|
-
"bytedance/seedream-4.0",
|
|
36
|
-
"bytedance/seedream-4.5",
|
|
37
|
-
"bytedance/seedream-5.0-lite",
|
|
38
|
-
"google/imagen-4.0-fast-generate-001",
|
|
39
|
-
"google/imagen-4.0-generate-001",
|
|
40
|
-
"google/imagen-4.0-ultra-generate-001",
|
|
41
|
-
"openai/gpt-image-1",
|
|
42
|
-
"openai/gpt-image-1-mini",
|
|
43
|
-
"openai/gpt-image-1.5",
|
|
44
|
-
"openai/gpt-image-2",
|
|
45
|
-
"prodia/flux-fast-schnell",
|
|
46
|
-
"recraft/recraft-v2",
|
|
47
|
-
"recraft/recraft-v3",
|
|
48
|
-
"recraft/recraft-v4",
|
|
49
|
-
"recraft/recraft-v4-pro",
|
|
50
|
-
"xai/grok-imagine-image",
|
|
51
|
-
"xai/grok-imagine-image-pro",
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
export const FALLBACK_VIDEO_MODELS = [
|
|
55
|
-
"alibaba/wan-v2.5-t2v-preview",
|
|
56
|
-
"alibaba/wan-v2.6-i2v",
|
|
57
|
-
"alibaba/wan-v2.6-i2v-flash",
|
|
58
|
-
"alibaba/wan-v2.6-r2v",
|
|
59
|
-
"alibaba/wan-v2.6-r2v-flash",
|
|
60
|
-
"alibaba/wan-v2.6-t2v",
|
|
61
|
-
"bytedance/seedance-2.0",
|
|
62
|
-
"bytedance/seedance-2.0-fast",
|
|
63
|
-
"bytedance/seedance-v1.0-lite-i2v",
|
|
64
|
-
"bytedance/seedance-v1.0-lite-t2v",
|
|
65
|
-
"bytedance/seedance-v1.0-pro",
|
|
66
|
-
"bytedance/seedance-v1.0-pro-fast",
|
|
67
|
-
"bytedance/seedance-v1.5-pro",
|
|
68
|
-
"google/veo-3.0-fast-generate-001",
|
|
69
|
-
"google/veo-3.0-generate-001",
|
|
70
|
-
"google/veo-3.1-fast-generate-001",
|
|
71
|
-
"google/veo-3.1-generate-001",
|
|
72
|
-
"klingai/kling-v2.5-turbo-i2v",
|
|
73
|
-
"klingai/kling-v2.5-turbo-t2v",
|
|
74
|
-
"klingai/kling-v2.6-i2v",
|
|
75
|
-
"klingai/kling-v2.6-motion-control",
|
|
76
|
-
"klingai/kling-v2.6-t2v",
|
|
77
|
-
"klingai/kling-v3.0-i2v",
|
|
78
|
-
"klingai/kling-v3.0-t2v",
|
|
79
|
-
"xai/grok-imagine-video",
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
export interface ModelEntry {
|
|
83
|
-
id: string;
|
|
84
|
-
name?: string;
|
|
85
|
-
description?: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface GatewayModels {
|
|
89
|
-
text: ModelEntry[];
|
|
90
|
-
image: ModelEntry[];
|
|
91
|
-
video: ModelEntry[];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const MODEL_TYPE_TO_MODALITY: Record<string, Modality> = {
|
|
95
|
-
language: "text",
|
|
96
|
-
image: "image",
|
|
97
|
-
video: "video",
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
export async function fetchGatewayModels(): Promise<GatewayModels> {
|
|
101
|
-
const result: GatewayModels = { text: [], image: [], video: [] };
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const { models } = await gateway.getAvailableModels();
|
|
105
|
-
for (const m of models) {
|
|
106
|
-
const modality =
|
|
107
|
-
MODEL_TYPE_TO_MODALITY[(m as { modelType?: string }).modelType ?? ""];
|
|
108
|
-
if (!modality) continue;
|
|
109
|
-
result[modality].push({
|
|
110
|
-
id: m.id,
|
|
111
|
-
name: m.name,
|
|
112
|
-
description: m.description ?? undefined,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
} catch {
|
|
116
|
-
result.text = FALLBACK_TEXT_MODELS.map((id) => ({ id }));
|
|
117
|
-
result.image = FALLBACK_IMAGE_MODELS.map((id) => ({ id }));
|
|
118
|
-
result.video = FALLBACK_VIDEO_MODELS.map((id) => ({ id }));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (result.text.length === 0) {
|
|
122
|
-
result.text = FALLBACK_TEXT_MODELS.map((id) => ({ id }));
|
|
123
|
-
}
|
|
124
|
-
if (result.image.length === 0) {
|
|
125
|
-
result.image = FALLBACK_IMAGE_MODELS.map((id) => ({ id }));
|
|
126
|
-
}
|
|
127
|
-
if (result.video.length === 0) {
|
|
128
|
-
result.video = FALLBACK_VIDEO_MODELS.map((id) => ({ id }));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function expandModelId(input: string, modality: Modality): string {
|
|
135
|
-
if (input.includes("/")) return input;
|
|
136
|
-
|
|
137
|
-
const knownLists: string[][] = [];
|
|
138
|
-
if (modality === "text") knownLists.push(FALLBACK_TEXT_MODELS);
|
|
139
|
-
else if (modality === "image") knownLists.push(FALLBACK_IMAGE_MODELS);
|
|
140
|
-
else if (modality === "video") knownLists.push(FALLBACK_VIDEO_MODELS);
|
|
141
|
-
|
|
142
|
-
for (const list of knownLists) {
|
|
143
|
-
for (const fullId of list) {
|
|
144
|
-
const name = fullId.slice(fullId.indexOf("/") + 1);
|
|
145
|
-
if (name === input) return fullId;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return input;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function resolveModels(
|
|
153
|
-
modality: Modality,
|
|
154
|
-
userModel?: string
|
|
155
|
-
): string[] {
|
|
156
|
-
if (!userModel) return [DEFAULTS[modality]];
|
|
157
|
-
const models = userModel
|
|
158
|
-
.split(",")
|
|
159
|
-
.map((m) => m.trim())
|
|
160
|
-
.filter(Boolean)
|
|
161
|
-
.map((m) => expandModelId(m, modality));
|
|
162
|
-
return models.length > 0 ? models : [DEFAULTS[modality]];
|
|
163
|
-
}
|
package/src/lib/mp4.test.ts
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { extractKeyframe } from "./mp4.js";
|
|
4
|
-
|
|
5
|
-
describe("extractKeyframe", () => {
|
|
6
|
-
test("returns null for empty buffer", () => {
|
|
7
|
-
expect(extractKeyframe(new Uint8Array(0))).toBeNull();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("returns null for buffer too small", () => {
|
|
11
|
-
expect(extractKeyframe(new Uint8Array(4))).toBeNull();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("returns null when no moov box found", () => {
|
|
15
|
-
// Valid-looking box header but wrong type
|
|
16
|
-
const buf = new Uint8Array(16);
|
|
17
|
-
const view = new DataView(buf.buffer);
|
|
18
|
-
view.setUint32(0, 16);
|
|
19
|
-
buf[4] = 0x66;
|
|
20
|
-
buf[5] = 0x74;
|
|
21
|
-
buf[6] = 0x79;
|
|
22
|
-
buf[7] = 0x70; // ftyp
|
|
23
|
-
expect(extractKeyframe(buf)).toBeNull();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("returns null for minimal moov with no video track", () => {
|
|
27
|
-
// Construct a minimal moov box with no trak inside
|
|
28
|
-
const moovPayload = new Uint8Array(0);
|
|
29
|
-
const moovSize = 8 + moovPayload.length;
|
|
30
|
-
const buf = new Uint8Array(moovSize);
|
|
31
|
-
const view = new DataView(buf.buffer);
|
|
32
|
-
view.setUint32(0, moovSize);
|
|
33
|
-
buf[4] = 0x6d;
|
|
34
|
-
buf[5] = 0x6f;
|
|
35
|
-
buf[6] = 0x6f;
|
|
36
|
-
buf[7] = 0x76; // moov
|
|
37
|
-
expect(extractKeyframe(buf)).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("builds a minimal MP4 structure and extracts keyframe data", () => {
|
|
41
|
-
// This test creates a valid-ish MP4 structure to exercise the parsing path.
|
|
42
|
-
// The actual H.264 data is minimal/dummy, so we just verify the parser
|
|
43
|
-
// successfully extracts SPS, PPS, and slice data.
|
|
44
|
-
const mp4 = buildMinimalMP4();
|
|
45
|
-
const result = extractKeyframe(mp4);
|
|
46
|
-
|
|
47
|
-
// If the MP4 was well-formed enough, we should get non-null result
|
|
48
|
-
if (result) {
|
|
49
|
-
expect(result.sps.length).toBeGreaterThan(0);
|
|
50
|
-
expect(result.pps.length).toBeGreaterThan(0);
|
|
51
|
-
expect(result.sliceData.length).toBeGreaterThan(0);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
function writeBox(type: string, payload: Uint8Array): Uint8Array {
|
|
57
|
-
const size = 8 + payload.length;
|
|
58
|
-
const buf = new Uint8Array(size);
|
|
59
|
-
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
60
|
-
view.setUint32(0, size);
|
|
61
|
-
for (let i = 0; i < 4; i++) buf[4 + i] = type.charCodeAt(i);
|
|
62
|
-
buf.set(payload, 8);
|
|
63
|
-
return buf;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function concatArrays(...arrs: Uint8Array[]): Uint8Array {
|
|
67
|
-
const total = arrs.reduce((s, a) => s + a.length, 0);
|
|
68
|
-
const result = new Uint8Array(total);
|
|
69
|
-
let off = 0;
|
|
70
|
-
for (const a of arrs) {
|
|
71
|
-
result.set(a, off);
|
|
72
|
-
off += a.length;
|
|
73
|
-
}
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function buildMinimalMP4(): Uint8Array {
|
|
78
|
-
// SPS NAL unit (minimal baseline profile, 16x16 resolution)
|
|
79
|
-
const sps = new Uint8Array([
|
|
80
|
-
0x67, 0x42, 0x00, 0x0a, 0xe9, 0x40, 0x40, 0x04, 0x00, 0x00, 0x00, 0x04,
|
|
81
|
-
0x00, 0x00, 0x00, 0xc8, 0x40,
|
|
82
|
-
]);
|
|
83
|
-
const pps = new Uint8Array([0x68, 0xce, 0x38, 0x80]);
|
|
84
|
-
|
|
85
|
-
// avcC box
|
|
86
|
-
const avcCPayload = new Uint8Array([
|
|
87
|
-
1, // version
|
|
88
|
-
0x42, // profile
|
|
89
|
-
0x00, // compat
|
|
90
|
-
0x0a, // level
|
|
91
|
-
0xff, // nal length size = 4
|
|
92
|
-
0xe1, // num SPS = 1
|
|
93
|
-
// SPS length + data
|
|
94
|
-
(sps.length >> 8) & 0xff,
|
|
95
|
-
sps.length & 0xff,
|
|
96
|
-
...sps,
|
|
97
|
-
1, // num PPS = 1
|
|
98
|
-
(pps.length >> 8) & 0xff,
|
|
99
|
-
pps.length & 0xff,
|
|
100
|
-
...pps,
|
|
101
|
-
]);
|
|
102
|
-
const avcC = writeBox("avcC", avcCPayload);
|
|
103
|
-
|
|
104
|
-
// avc1 box: 78 bytes fixed header + avcC child
|
|
105
|
-
const avc1Fixed = new Uint8Array(78);
|
|
106
|
-
// data_ref_index at offset 6 = 1
|
|
107
|
-
avc1Fixed[6] = 0;
|
|
108
|
-
avc1Fixed[7] = 1;
|
|
109
|
-
// width at offset 24
|
|
110
|
-
avc1Fixed[24] = 0;
|
|
111
|
-
avc1Fixed[25] = 16;
|
|
112
|
-
// height at offset 26
|
|
113
|
-
avc1Fixed[26] = 0;
|
|
114
|
-
avc1Fixed[27] = 16;
|
|
115
|
-
const avc1Payload = concatArrays(avc1Fixed, avcC);
|
|
116
|
-
const avc1 = writeBox("avc1", avc1Payload);
|
|
117
|
-
|
|
118
|
-
// stsd box: version(4) + entry_count(4) + avc1
|
|
119
|
-
const stsdInner = new Uint8Array(8);
|
|
120
|
-
const stsdView = new DataView(stsdInner.buffer);
|
|
121
|
-
stsdView.setUint32(4, 1); // entry count
|
|
122
|
-
const stsd = writeBox("stsd", concatArrays(stsdInner, avc1));
|
|
123
|
-
|
|
124
|
-
// stsz (1 sample, size 10)
|
|
125
|
-
const stszPayload = new Uint8Array(12);
|
|
126
|
-
const stszView = new DataView(stszPayload.buffer);
|
|
127
|
-
stszView.setUint32(4, 0); // sample_size = variable
|
|
128
|
-
stszView.setUint32(8, 1); // sample_count = 1
|
|
129
|
-
const stszEntry = new Uint8Array(4);
|
|
130
|
-
new DataView(stszEntry.buffer).setUint32(0, 10);
|
|
131
|
-
const stsz = writeBox("stsz", concatArrays(stszPayload, stszEntry));
|
|
132
|
-
|
|
133
|
-
// stsc (1 entry: chunk 1, 1 sample/chunk, sdi 1)
|
|
134
|
-
const stscPayload = new Uint8Array(16);
|
|
135
|
-
const stscView = new DataView(stscPayload.buffer);
|
|
136
|
-
stscView.setUint32(4, 1); // entry count
|
|
137
|
-
stscView.setUint32(8, 1); // first chunk
|
|
138
|
-
stscView.setUint32(12, 1); // samples per chunk
|
|
139
|
-
const stscExtra = new Uint8Array(4);
|
|
140
|
-
new DataView(stscExtra.buffer).setUint32(0, 1);
|
|
141
|
-
const stsc = writeBox("stsc", concatArrays(stscPayload, stscExtra));
|
|
142
|
-
|
|
143
|
-
// stco (1 chunk offset, will be filled in later)
|
|
144
|
-
const stcoPayload = new Uint8Array(8);
|
|
145
|
-
const stcoView = new DataView(stcoPayload.buffer);
|
|
146
|
-
stcoView.setUint32(4, 1); // entry count
|
|
147
|
-
// offset will be set after we know the layout
|
|
148
|
-
const stcoOffsetPos = 8; // position within stco payload for the offset value
|
|
149
|
-
const stco = writeBox("stco", concatArrays(stcoPayload, new Uint8Array(4)));
|
|
150
|
-
|
|
151
|
-
// stss (1 sync sample: sample 1)
|
|
152
|
-
const stssPayload = new Uint8Array(8);
|
|
153
|
-
const stssView = new DataView(stssPayload.buffer);
|
|
154
|
-
stssView.setUint32(4, 1); // entry count
|
|
155
|
-
const stssSample = new Uint8Array(4);
|
|
156
|
-
new DataView(stssSample.buffer).setUint32(0, 1);
|
|
157
|
-
const stss = writeBox("stss", concatArrays(stssPayload, stssSample));
|
|
158
|
-
|
|
159
|
-
const stbl = writeBox("stbl", concatArrays(stsd, stsz, stsc, stco, stss));
|
|
160
|
-
const minf = writeBox("minf", stbl);
|
|
161
|
-
|
|
162
|
-
// hdlr box (video handler)
|
|
163
|
-
const hdlrPayload = new Uint8Array(20);
|
|
164
|
-
// handler_type at offset 8..11 = "vide"
|
|
165
|
-
hdlrPayload[8] = 0x76;
|
|
166
|
-
hdlrPayload[9] = 0x69;
|
|
167
|
-
hdlrPayload[10] = 0x64;
|
|
168
|
-
hdlrPayload[11] = 0x65;
|
|
169
|
-
const hdlr = writeBox("hdlr", hdlrPayload);
|
|
170
|
-
|
|
171
|
-
const mdia = writeBox("mdia", concatArrays(hdlr, minf));
|
|
172
|
-
const trak = writeBox("trak", mdia);
|
|
173
|
-
const moov = writeBox("moov", trak);
|
|
174
|
-
|
|
175
|
-
// mdat with a dummy IDR NAL unit (length-prefixed)
|
|
176
|
-
const idrNal = new Uint8Array([0x65, 0x88, 0x80, 0x40, 0x00, 0x00]);
|
|
177
|
-
const mdatPayload = new Uint8Array(4 + idrNal.length);
|
|
178
|
-
new DataView(mdatPayload.buffer).setUint32(0, idrNal.length);
|
|
179
|
-
mdatPayload.set(idrNal, 4);
|
|
180
|
-
|
|
181
|
-
// ftyp box
|
|
182
|
-
const ftyp = writeBox(
|
|
183
|
-
"ftyp",
|
|
184
|
-
new Uint8Array([
|
|
185
|
-
0x69,
|
|
186
|
-
0x73,
|
|
187
|
-
0x6f,
|
|
188
|
-
0x6d, // brand: isom
|
|
189
|
-
0x00,
|
|
190
|
-
0x00,
|
|
191
|
-
0x02,
|
|
192
|
-
0x00, // version
|
|
193
|
-
])
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
const mdat = writeBox("mdat", mdatPayload);
|
|
197
|
-
|
|
198
|
-
// Calculate chunk offset
|
|
199
|
-
const mdatOffset = ftyp.length + moov.length + 8; // 8 for mdat header
|
|
200
|
-
const fullMp4 = concatArrays(ftyp, moov, mdat);
|
|
201
|
-
|
|
202
|
-
// Patch the stco chunk offset
|
|
203
|
-
// We need to find the stco entry in the final buffer and patch it
|
|
204
|
-
const stcoMarker = findStcoOffset(fullMp4);
|
|
205
|
-
if (stcoMarker >= 0) {
|
|
206
|
-
const view = new DataView(
|
|
207
|
-
fullMp4.buffer,
|
|
208
|
-
fullMp4.byteOffset,
|
|
209
|
-
fullMp4.byteLength
|
|
210
|
-
);
|
|
211
|
-
view.setUint32(stcoMarker, mdatOffset);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return fullMp4;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function findStcoOffset(buf: Uint8Array): number {
|
|
218
|
-
// Find the stco box and return the position of its first chunk offset entry
|
|
219
|
-
for (let i = 0; i < buf.length - 12; i++) {
|
|
220
|
-
if (
|
|
221
|
-
buf[i] === 0x73 &&
|
|
222
|
-
buf[i + 1] === 0x74 &&
|
|
223
|
-
buf[i + 2] === 0x63 &&
|
|
224
|
-
buf[i + 3] === 0x6f
|
|
225
|
-
) {
|
|
226
|
-
// Found "stco" - the chunk offset entry is at i + 4 (version/flags) + 4 (entry_count) + 4
|
|
227
|
-
return i + 4 + 8;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return -1;
|
|
231
|
-
}
|