@tyvm/knowhow 0.0.69 → 0.0.70
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/docs/shell-commands.md +174 -0
- package/package.json +1 -1
- package/src/agents/base/base.ts +1 -3
- package/src/agents/developer/developer.ts +21 -13
- package/src/agents/tools/agentCall.ts +4 -2
- package/src/agents/tools/fileSearch.ts +5 -1
- package/src/agents/tools/startAgentTask.ts +131 -22
- package/src/chat/CliChatService.ts +57 -11
- package/src/chat/modules/AgentModule.ts +72 -12
- package/src/chat/modules/CustomCommandsModule.ts +79 -0
- package/src/chat/modules/InternalChatModule.ts +11 -1
- package/src/chat/modules/ShellCommandModule.ts +96 -0
- package/src/chat/modules/index.ts +1 -0
- package/src/chat/types.ts +14 -2
- package/src/chat.ts +16 -13
- package/src/cli.ts +16 -6
- package/src/clients/anthropic.ts +41 -90
- package/src/clients/gemini.ts +445 -87
- package/src/clients/index.ts +125 -0
- package/src/clients/knowhow.ts +81 -0
- package/src/clients/openai.ts +256 -145
- package/src/clients/pricing/anthropic.ts +90 -0
- package/src/clients/pricing/google.ts +65 -0
- package/src/clients/pricing/index.ts +4 -0
- package/src/clients/pricing/openai.ts +134 -0
- package/src/clients/pricing/xai.ts +62 -0
- package/src/clients/types.ts +170 -1
- package/src/clients/xai.ts +275 -46
- package/src/config.ts +61 -15
- package/src/embeddings.ts +9 -1
- package/src/microphone.ts +15 -16
- package/src/migrations.ts +151 -0
- package/src/plugins/AgentsMdPlugin.ts +118 -0
- package/src/plugins/PluginBase.ts +8 -0
- package/src/plugins/downloader/downloader.ts +5 -6
- package/src/plugins/embedding.ts +10 -8
- package/src/plugins/exec.ts +70 -0
- package/src/plugins/github.ts +120 -74
- package/src/plugins/language.ts +11 -13
- package/src/plugins/plugins.ts +25 -4
- package/src/plugins/tmux.ts +132 -0
- package/src/plugins/types.ts +1 -0
- package/src/plugins/vim.ts +14 -1
- package/src/services/AgentSyncFs.ts +417 -0
- package/src/services/{AgentSynchronization.ts → AgentSyncKnowhowWeb.ts} +2 -2
- package/src/services/EventService.ts +0 -1
- package/src/services/KnowhowClient.ts +106 -0
- package/src/services/index.ts +4 -2
- package/src/types.ts +57 -4
- package/src/worker.ts +11 -6
- package/tests/manual/modalities/README.md +157 -0
- package/tests/manual/modalities/google.modalities.test.ts +335 -0
- package/tests/manual/modalities/openai.modalities.test.ts +329 -0
- package/tests/manual/modalities/streaming.test.ts +260 -0
- package/tests/manual/modalities/xai.modalities.test.ts +307 -0
- package/tests/plugins/language/languagePlugin-content-triggers.test.ts +5 -5
- package/tests/plugins/language/languagePlugin-integration.test.ts +1 -1
- package/tests/plugins/language/languagePlugin.test.ts +17 -8
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.js +1 -1
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/developer/developer.js +21 -12
- package/ts_build/src/agents/developer/developer.js.map +1 -1
- package/ts_build/src/agents/tools/agentCall.js +4 -2
- package/ts_build/src/agents/tools/agentCall.js.map +1 -1
- package/ts_build/src/agents/tools/executeScript/index.d.ts +1 -1
- package/ts_build/src/agents/tools/fileSearch.js +2 -1
- package/ts_build/src/agents/tools/fileSearch.js.map +1 -1
- package/ts_build/src/agents/tools/github/index.d.ts +1 -1
- package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -1
- package/ts_build/src/agents/tools/startAgentTask.js +118 -17
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/chat/CliChatService.d.ts +4 -0
- package/ts_build/src/chat/CliChatService.js +39 -5
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +4 -1
- package/ts_build/src/chat/modules/AgentModule.js +49 -11
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/CustomCommandsModule.d.ts +9 -0
- package/ts_build/src/chat/modules/CustomCommandsModule.js +58 -0
- package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -0
- package/ts_build/src/chat/modules/InternalChatModule.d.ts +2 -0
- package/ts_build/src/chat/modules/InternalChatModule.js +10 -0
- package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/ShellCommandModule.d.ts +8 -0
- package/ts_build/src/chat/modules/ShellCommandModule.js +83 -0
- package/ts_build/src/chat/modules/ShellCommandModule.js.map +1 -0
- package/ts_build/src/chat/modules/index.d.ts +1 -0
- package/ts_build/src/chat/modules/index.js +3 -1
- package/ts_build/src/chat/modules/index.js.map +1 -1
- package/ts_build/src/chat/types.d.ts +11 -1
- package/ts_build/src/chat.js +16 -13
- package/ts_build/src/chat.js.map +1 -1
- package/ts_build/src/cli.js +10 -3
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +5 -1
- package/ts_build/src/clients/anthropic.js +18 -91
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/gemini.d.ts +80 -2
- package/ts_build/src/clients/gemini.js +336 -74
- package/ts_build/src/clients/gemini.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +9 -1
- package/ts_build/src/clients/index.js +65 -0
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/knowhow.d.ts +9 -1
- package/ts_build/src/clients/knowhow.js +43 -0
- package/ts_build/src/clients/knowhow.js.map +1 -1
- package/ts_build/src/clients/openai.d.ts +9 -1
- package/ts_build/src/clients/openai.js +201 -133
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/anthropic.d.ts +17 -0
- package/ts_build/src/clients/pricing/anthropic.js +93 -0
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -0
- package/ts_build/src/clients/pricing/google.d.ts +73 -0
- package/ts_build/src/clients/pricing/google.js +68 -0
- package/ts_build/src/clients/pricing/google.js.map +1 -0
- package/ts_build/src/clients/pricing/index.d.ts +4 -0
- package/ts_build/src/clients/pricing/index.js +14 -0
- package/ts_build/src/clients/pricing/index.js.map +1 -0
- package/ts_build/src/clients/pricing/openai.d.ts +7 -0
- package/ts_build/src/clients/pricing/openai.js +137 -0
- package/ts_build/src/clients/pricing/openai.js.map +1 -0
- package/ts_build/src/clients/pricing/xai.d.ts +26 -0
- package/ts_build/src/clients/pricing/xai.js +59 -0
- package/ts_build/src/clients/pricing/xai.js.map +1 -0
- package/ts_build/src/clients/types.d.ts +135 -0
- package/ts_build/src/clients/xai.d.ts +9 -1
- package/ts_build/src/clients/xai.js +178 -46
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/src/config.d.ts +1 -0
- package/ts_build/src/config.js +45 -16
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/embeddings.js +8 -1
- package/ts_build/src/embeddings.js.map +1 -1
- package/ts_build/src/microphone.js +7 -9
- package/ts_build/src/microphone.js.map +1 -1
- package/ts_build/src/migrations.d.ts +17 -0
- package/ts_build/src/migrations.js +86 -0
- package/ts_build/src/migrations.js.map +1 -0
- package/ts_build/src/plugins/AgentsMdPlugin.d.ts +13 -0
- package/ts_build/src/plugins/AgentsMdPlugin.js +118 -0
- package/ts_build/src/plugins/AgentsMdPlugin.js.map +1 -0
- package/ts_build/src/plugins/PluginBase.d.ts +1 -0
- package/ts_build/src/plugins/PluginBase.js +3 -0
- package/ts_build/src/plugins/PluginBase.js.map +1 -1
- package/ts_build/src/plugins/downloader/downloader.js +5 -5
- package/ts_build/src/plugins/downloader/downloader.js.map +1 -1
- package/ts_build/src/plugins/embedding.js +9 -8
- package/ts_build/src/plugins/embedding.js.map +1 -1
- package/ts_build/src/plugins/exec.d.ts +10 -0
- package/ts_build/src/plugins/exec.js +56 -0
- package/ts_build/src/plugins/exec.js.map +1 -0
- package/ts_build/src/plugins/github.js +93 -51
- package/ts_build/src/plugins/github.js.map +1 -1
- package/ts_build/src/plugins/language.js +14 -11
- package/ts_build/src/plugins/language.js.map +1 -1
- package/ts_build/src/plugins/plugins.d.ts +1 -0
- package/ts_build/src/plugins/plugins.js +19 -1
- package/ts_build/src/plugins/plugins.js.map +1 -1
- package/ts_build/src/plugins/tmux.d.ts +14 -0
- package/ts_build/src/plugins/tmux.js +108 -0
- package/ts_build/src/plugins/tmux.js.map +1 -0
- package/ts_build/src/plugins/types.d.ts +1 -0
- package/ts_build/src/plugins/vim.js +11 -1
- package/ts_build/src/plugins/vim.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +34 -0
- package/ts_build/src/services/AgentSyncFs.js +325 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +29 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +178 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -0
- package/ts_build/src/services/AgentSynchronization.d.ts +1 -1
- package/ts_build/src/services/AgentSynchronization.js +3 -3
- package/ts_build/src/services/AgentSynchronization.js.map +1 -1
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +9 -1
- package/ts_build/src/services/KnowhowClient.js +58 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/index.d.ts +2 -1
- package/ts_build/src/services/index.js +2 -1
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/types.d.ts +26 -1
- package/ts_build/src/types.js +45 -4
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/utils/PersistentInputManager.d.ts +28 -0
- package/ts_build/src/utils/PersistentInputManager.js +293 -0
- package/ts_build/src/utils/PersistentInputManager.js.map +1 -0
- package/ts_build/src/worker.js +2 -2
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/tests/manual/modalities/google.modalities.test.d.ts +1 -0
- package/ts_build/tests/manual/modalities/google.modalities.test.js +252 -0
- package/ts_build/tests/manual/modalities/google.modalities.test.js.map +1 -0
- package/ts_build/tests/manual/modalities/openai.modalities.test.d.ts +1 -0
- package/ts_build/tests/manual/modalities/openai.modalities.test.js +252 -0
- package/ts_build/tests/manual/modalities/openai.modalities.test.js.map +1 -0
- package/ts_build/tests/manual/modalities/streaming.test.d.ts +1 -0
- package/ts_build/tests/manual/modalities/streaming.test.js +206 -0
- package/ts_build/tests/manual/modalities/streaming.test.js.map +1 -0
- package/ts_build/tests/manual/modalities/xai.modalities.test.d.ts +1 -0
- package/ts_build/tests/manual/modalities/xai.modalities.test.js +226 -0
- package/ts_build/tests/manual/modalities/xai.modalities.test.js.map +1 -0
- package/ts_build/tests/manual/persistent-input-test.d.ts +1 -0
- package/ts_build/tests/manual/persistent-input-test.js +35 -0
- package/ts_build/tests/manual/persistent-input-test.js.map +1 -0
- package/ts_build/tests/plugins/language/languagePlugin-content-triggers.test.js +5 -5
- package/ts_build/tests/plugins/language/languagePlugin-content-triggers.test.js.map +1 -1
- package/ts_build/tests/plugins/language/languagePlugin-integration.test.js +1 -1
- package/ts_build/tests/plugins/language/languagePlugin-integration.test.js.map +1 -1
- package/ts_build/tests/plugins/language/languagePlugin.test.js +17 -7
- package/ts_build/tests/plugins/language/languagePlugin.test.js.map +1 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google (Gemini) Modalities Manual Test
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* 1. Audio generation (Gemini TTS) → saved to disk
|
|
6
|
+
* 2. Image generation (Gemini 2.0 Flash inline) → saved to disk
|
|
7
|
+
* 3. Image generation (Imagen 3) → saved to disk
|
|
8
|
+
* 4. Send generated image to Gemini vision model and describe it
|
|
9
|
+
* 5. Video generation (Veo 2) → saved to disk
|
|
10
|
+
*
|
|
11
|
+
* Run with:
|
|
12
|
+
* npx jest tests/manual/modalities/google.modalities.test.ts --testTimeout=300000
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import { AIClient } from "../../../src/clients";
|
|
18
|
+
import { Models } from "../../../src/types";
|
|
19
|
+
|
|
20
|
+
const OUTPUT_DIR = path.join(__dirname, "outputs", "google");
|
|
21
|
+
|
|
22
|
+
function ensureOutputDir() {
|
|
23
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("Google (Gemini) Modalities", () => {
|
|
27
|
+
let client: AIClient;
|
|
28
|
+
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
31
|
+
console.warn("GEMINI_API_KEY not set – skipping Google modality tests");
|
|
32
|
+
}
|
|
33
|
+
ensureOutputDir();
|
|
34
|
+
client = new AIClient();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── 1. Audio Generation (Gemini TTS) ───────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
test("1. Gemini TTS – generate audio and save to disk", async () => {
|
|
40
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
41
|
+
console.log("Skipping: GEMINI_API_KEY not set");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const outputPath = path.join(OUTPUT_DIR, "tts-output.wav");
|
|
46
|
+
if (fs.existsSync(outputPath)) {
|
|
47
|
+
console.log(`⏭️ Skipping: ${outputPath} already exists`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const text =
|
|
52
|
+
"Hello! This is a test of the Google Gemini text-to-speech system. " +
|
|
53
|
+
"We are verifying that Gemini audio generation is working correctly.";
|
|
54
|
+
|
|
55
|
+
const response = await client.createAudioGeneration("google", {
|
|
56
|
+
model: Models.google.Gemini_25_Flash_TTS,
|
|
57
|
+
input: text,
|
|
58
|
+
voice: "Puck",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
fs.writeFileSync(outputPath, response.audio);
|
|
62
|
+
|
|
63
|
+
console.log(`✅ Audio saved to: ${outputPath}`);
|
|
64
|
+
console.log(` Format: ${response.format}`);
|
|
65
|
+
console.log(` Size: ${response.audio.length} bytes`);
|
|
66
|
+
|
|
67
|
+
expect(response.audio).toBeInstanceOf(Buffer);
|
|
68
|
+
expect(response.audio.length).toBeGreaterThan(0);
|
|
69
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
70
|
+
}, 60000);
|
|
71
|
+
|
|
72
|
+
// ─── 2. Image Generation (Gemini 2.0 Flash inline) ──────────────────────────
|
|
73
|
+
|
|
74
|
+
test("2. Gemini 2.0 Flash – inline image generation and save to disk", async () => {
|
|
75
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
76
|
+
console.log("Skipping: GEMINI_API_KEY not set");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const outputPath = path.join(OUTPUT_DIR, "gemini-flash-image.png");
|
|
81
|
+
if (fs.existsSync(outputPath)) {
|
|
82
|
+
console.log(`⏭️ Skipping: ${outputPath} already exists`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const prompt =
|
|
87
|
+
"A watercolor painting of a serene mountain lake at sunrise, " +
|
|
88
|
+
"with reflections of pine trees in the calm water";
|
|
89
|
+
|
|
90
|
+
const response = await client.createImageGeneration("google", {
|
|
91
|
+
model: Models.google.Gemini_20_Flash_Preview_Image_Generation,
|
|
92
|
+
prompt,
|
|
93
|
+
n: 1,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(response.data.length).toBeGreaterThan(0);
|
|
97
|
+
|
|
98
|
+
const imageData = response.data[0];
|
|
99
|
+
|
|
100
|
+
if (imageData.b64_json) {
|
|
101
|
+
const buffer = Buffer.from(imageData.b64_json, "base64");
|
|
102
|
+
fs.writeFileSync(outputPath, buffer);
|
|
103
|
+
console.log(`✅ Gemini Flash image saved to: ${outputPath}`);
|
|
104
|
+
console.log(` Size: ${buffer.length} bytes`);
|
|
105
|
+
} else if (imageData.url) {
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.join(OUTPUT_DIR, "gemini-flash-image-url.txt"),
|
|
108
|
+
imageData.url
|
|
109
|
+
);
|
|
110
|
+
console.log(`✅ Gemini Flash image URL saved`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(` Estimated cost: $${response.usd_cost?.toFixed(6)}`);
|
|
114
|
+
|
|
115
|
+
expect(response.created).toBeGreaterThan(0);
|
|
116
|
+
expect(response.data.length).toBeGreaterThan(0);
|
|
117
|
+
}, 90000);
|
|
118
|
+
|
|
119
|
+
// ─── 3. Image Generation (Imagen 3) ─────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
test("3. Imagen 3 – generate image and save to disk", async () => {
|
|
122
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
123
|
+
console.log("Skipping: GEMINI_API_KEY not set");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const outputPath = path.join(OUTPUT_DIR, "imagen3-output.png");
|
|
128
|
+
if (fs.existsSync(outputPath)) {
|
|
129
|
+
console.log(`⏭️ Skipping: ${outputPath} already exists`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const prompt =
|
|
134
|
+
"A photorealistic close-up of a red rose with dewdrops, " +
|
|
135
|
+
"dramatic lighting, shallow depth of field, professional photography";
|
|
136
|
+
|
|
137
|
+
const response = await client.createImageGeneration("google", {
|
|
138
|
+
model: Models.google.Imagen_3,
|
|
139
|
+
prompt,
|
|
140
|
+
n: 1,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(response.data.length).toBeGreaterThan(0);
|
|
144
|
+
|
|
145
|
+
const imageData = response.data[0];
|
|
146
|
+
|
|
147
|
+
if (imageData.b64_json) {
|
|
148
|
+
console.log(imageData.b64_json.slice(0, 100) + "..."); // Log the beginning of the base64 string for debugging
|
|
149
|
+
const buffer = Buffer.from(imageData.b64_json, "base64");
|
|
150
|
+
fs.writeFileSync(outputPath, buffer);
|
|
151
|
+
console.log(`✅ Imagen 3 image saved to: ${outputPath}`);
|
|
152
|
+
console.log(` Size: ${buffer.length} bytes`);
|
|
153
|
+
} else if (imageData.url) {
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
path.join(OUTPUT_DIR, "imagen3-output-url.txt"),
|
|
156
|
+
imageData.url
|
|
157
|
+
);
|
|
158
|
+
console.log(`✅ Imagen 3 image URL saved`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(` Estimated cost: $${response.usd_cost?.toFixed(6)}`);
|
|
162
|
+
|
|
163
|
+
expect(response.created).toBeGreaterThan(0);
|
|
164
|
+
}, 90000);
|
|
165
|
+
|
|
166
|
+
// ─── 4. Vision – send generated image back to Gemini ────────────────────────
|
|
167
|
+
|
|
168
|
+
test("4. Gemini Vision – describe the generated Imagen 3 image", async () => {
|
|
169
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
170
|
+
console.log("Skipping: GEMINI_API_KEY not set");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const outputPath = path.join(OUTPUT_DIR, "vision-description.txt");
|
|
175
|
+
if (fs.existsSync(outputPath)) {
|
|
176
|
+
console.log(`⏭️ Skipping: ${outputPath} already exists`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const imagePath = path.join(OUTPUT_DIR, "imagen3-output.png");
|
|
181
|
+
if (!fs.existsSync(imagePath)) {
|
|
182
|
+
console.log(
|
|
183
|
+
"Skipping: imagen3-output.png not found – run Imagen 3 test first"
|
|
184
|
+
);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const imageBuffer = fs.readFileSync(imagePath);
|
|
189
|
+
const base64Image = imageBuffer.toString("base64");
|
|
190
|
+
const dataUrl = `data:image/png;base64,${base64Image}`;
|
|
191
|
+
|
|
192
|
+
const response = await client.createCompletion("google", {
|
|
193
|
+
model: Models.google.Gemini_20_Flash,
|
|
194
|
+
messages: [
|
|
195
|
+
{
|
|
196
|
+
role: "user",
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: "Please describe this image in detail. What do you see?",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: "image_url",
|
|
204
|
+
image_url: { url: dataUrl },
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
max_tokens: 500,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const description = response.choices[0]?.message?.content || "";
|
|
213
|
+
fs.writeFileSync(outputPath, description);
|
|
214
|
+
|
|
215
|
+
console.log(`✅ Vision description saved to: ${outputPath}`);
|
|
216
|
+
console.log(` Description: ${description.substring(0, 200)}...`);
|
|
217
|
+
console.log(` Estimated cost: $${response.usd_cost?.toFixed(6)}`);
|
|
218
|
+
|
|
219
|
+
expect(description).toBeTruthy();
|
|
220
|
+
expect(description.length).toBeGreaterThan(10);
|
|
221
|
+
}, 60000);
|
|
222
|
+
|
|
223
|
+
// ─── 5. Video Generation (Veo 3.1) ──────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
test("5. Google Veo 3.1 – generate video and save to disk", async () => {
|
|
226
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
227
|
+
console.log("Skipping: GEMINI_API_KEY not set");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const outputPath = path.join(OUTPUT_DIR, "veo31-output.mp4");
|
|
232
|
+
const jobNamePath = path.join(OUTPUT_DIR, "veo31-job-name.txt");
|
|
233
|
+
|
|
234
|
+
// If final output already exists, skip entirely
|
|
235
|
+
if (fs.existsSync(outputPath)) {
|
|
236
|
+
console.log(`⏭️ Skipping: ${outputPath} already exists`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Helper: poll a known jobId until done, then download and save
|
|
241
|
+
async function pollAndDownload(jobId: string) {
|
|
242
|
+
const maxPollingTime = 20 * 60 * 1000; // 20 minutes
|
|
243
|
+
const pollingInterval = 10000; // 10 seconds
|
|
244
|
+
const startTime = Date.now();
|
|
245
|
+
|
|
246
|
+
console.log(`⏳ Polling job: ${jobId}`);
|
|
247
|
+
|
|
248
|
+
while (Date.now() - startTime < maxPollingTime) {
|
|
249
|
+
await new Promise((resolve) => setTimeout(resolve, pollingInterval));
|
|
250
|
+
|
|
251
|
+
const status = await client.getVideoStatus("google", { jobId });
|
|
252
|
+
console.log(` Status: ${status.status}`);
|
|
253
|
+
|
|
254
|
+
if (status.status === "failed") {
|
|
255
|
+
throw new Error(`Veo video generation failed: ${status.error}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (status.status === "completed") {
|
|
259
|
+
// Get the URI from the status response
|
|
260
|
+
const videoData = status.data?.[0];
|
|
261
|
+
const uri = videoData?.url || videoData?.fileUri;
|
|
262
|
+
|
|
263
|
+
if (!uri) {
|
|
264
|
+
throw new Error("No video URI in completed status response");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log(` Video URI: ${uri}`);
|
|
268
|
+
console.log(`⏳ Downloading video via Files API...`);
|
|
269
|
+
|
|
270
|
+
// Use the client's downloadFile method, passing filePath so the SDK
|
|
271
|
+
// writes directly to the destination (no extra read/write cycle).
|
|
272
|
+
const downloaded = await client.downloadFile("google", {
|
|
273
|
+
fileId: uri,
|
|
274
|
+
uri,
|
|
275
|
+
filePath: outputPath,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
console.log(
|
|
279
|
+
`✅ Veo 3.1 video saved to: ${outputPath} (${downloaded.data.length} bytes)`
|
|
280
|
+
);
|
|
281
|
+
console.log(` MIME type: ${downloaded.mimeType}`);
|
|
282
|
+
|
|
283
|
+
// Clean up job name file
|
|
284
|
+
if (fs.existsSync(jobNamePath)) fs.unlinkSync(jobNamePath);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// otherwise status is "in_progress" or "queued" – keep polling
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
throw new Error(
|
|
291
|
+
"Veo 3.1 video generation timed out after 20 minutes of polling"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// If we already have a job ID from a previous (timed-out) run, resume polling
|
|
296
|
+
if (fs.existsSync(jobNamePath)) {
|
|
297
|
+
const jobId = fs.readFileSync(jobNamePath, "utf8").trim();
|
|
298
|
+
console.log(`🔄 Resuming poll for existing job: ${jobId}`);
|
|
299
|
+
await pollAndDownload(jobId);
|
|
300
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Otherwise start a new video generation job
|
|
305
|
+
const prompt =
|
|
306
|
+
"A timelapse of clouds moving over a mountain range at golden hour, " +
|
|
307
|
+
"cinematic quality, smooth motion";
|
|
308
|
+
|
|
309
|
+
console.log("⏳ Submitting Veo 3.1 video generation job...");
|
|
310
|
+
|
|
311
|
+
const response = await client.createVideoGeneration("google", {
|
|
312
|
+
model: Models.google.Veo_3_1,
|
|
313
|
+
prompt,
|
|
314
|
+
n: 1,
|
|
315
|
+
duration: 6,
|
|
316
|
+
aspect_ratio: "16:9",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const jobId = response.jobId;
|
|
320
|
+
if (!jobId) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`No jobId returned from video generation: ${JSON.stringify(response)}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Persist the job ID so subsequent runs can resume if this run times out
|
|
327
|
+
fs.writeFileSync(jobNamePath, jobId);
|
|
328
|
+
console.log(`📝 Job ID saved to: ${jobNamePath}`);
|
|
329
|
+
console.log(` Job ID: ${jobId}`);
|
|
330
|
+
|
|
331
|
+
await pollAndDownload(jobId);
|
|
332
|
+
|
|
333
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
334
|
+
}, 1500000); // 25 minute timeout
|
|
335
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Modalities Manual Test
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* 1. Audio generation (TTS) → saved to disk
|
|
6
|
+
* 2. Audio transcription (Whisper) of generated audio
|
|
7
|
+
* 3. Image generation (DALL-E 3) → saved to disk
|
|
8
|
+
* 4. Send generated image to a vision model and describe it
|
|
9
|
+
* 5. Video generation (Sora) → not yet available in the OpenAI SDK (documents error)
|
|
10
|
+
*
|
|
11
|
+
* Run with:
|
|
12
|
+
* npx ts-node --project tsconfig.json tests/manual/modalities/openai.modalities.test.ts
|
|
13
|
+
* Or via jest (manual run):
|
|
14
|
+
* npx jest tests/manual/modalities/openai.modalities.test.ts --testTimeout=120000
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
import { AIClient } from "../../../src/clients";
|
|
20
|
+
import { Models } from "../../../src/types";
|
|
21
|
+
|
|
22
|
+
const OUTPUT_DIR = path.join(__dirname, "outputs", "openai");
|
|
23
|
+
|
|
24
|
+
function ensureOutputDir() {
|
|
25
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("OpenAI Modalities", () => {
|
|
29
|
+
let client: AIClient;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
if (!process.env.OPENAI_KEY) {
|
|
33
|
+
console.warn("OPENAI_KEY not set – skipping OpenAI modality tests");
|
|
34
|
+
}
|
|
35
|
+
ensureOutputDir();
|
|
36
|
+
client = new AIClient();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── 1. Audio Generation (TTS) ──────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
test("1. OpenAI TTS – generate audio and save to disk", async () => {
|
|
42
|
+
if (!process.env.OPENAI_KEY) {
|
|
43
|
+
console.log("Skipping: OPENAI_KEY not set");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const text =
|
|
48
|
+
"Hello! This is a test of the OpenAI text-to-speech system. " +
|
|
49
|
+
"We are verifying that audio generation is working correctly.";
|
|
50
|
+
|
|
51
|
+
const response = await client.createAudioGeneration("openai", {
|
|
52
|
+
model: "tts-1",
|
|
53
|
+
input: text,
|
|
54
|
+
voice: "alloy",
|
|
55
|
+
response_format: "mp3",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const outputPath = path.join(OUTPUT_DIR, "tts-output.mp3");
|
|
59
|
+
fs.writeFileSync(outputPath, response.audio);
|
|
60
|
+
|
|
61
|
+
console.log(`✅ Audio saved to: ${outputPath}`);
|
|
62
|
+
console.log(` Format: ${response.format}`);
|
|
63
|
+
console.log(` Size: ${response.audio.length} bytes`);
|
|
64
|
+
console.log(` Estimated cost: $${response.usd_cost?.toFixed(6)}`);
|
|
65
|
+
|
|
66
|
+
expect(response.audio).toBeInstanceOf(Buffer);
|
|
67
|
+
expect(response.audio.length).toBeGreaterThan(0);
|
|
68
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
69
|
+
}, 60000);
|
|
70
|
+
|
|
71
|
+
// ─── 2. Audio Transcription (Whisper) ───────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
test("2. OpenAI Whisper – transcribe generated audio", async () => {
|
|
74
|
+
if (!process.env.OPENAI_KEY) {
|
|
75
|
+
console.log("Skipping: OPENAI_KEY not set");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const audioPath = path.join(OUTPUT_DIR, "tts-output.mp3");
|
|
80
|
+
if (!fs.existsSync(audioPath)) {
|
|
81
|
+
console.log("Skipping: tts-output.mp3 not found – run TTS test first");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const audioStream = fs.createReadStream(audioPath);
|
|
86
|
+
|
|
87
|
+
const response = await client.createAudioTranscription("openai", {
|
|
88
|
+
file: audioStream,
|
|
89
|
+
model: "whisper-1",
|
|
90
|
+
response_format: "verbose_json",
|
|
91
|
+
language: "en",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const outputPath = path.join(OUTPUT_DIR, "transcription.json");
|
|
95
|
+
fs.writeFileSync(outputPath, JSON.stringify(response, null, 2));
|
|
96
|
+
|
|
97
|
+
console.log(`✅ Transcription saved to: ${outputPath}`);
|
|
98
|
+
console.log(` Text: ${response.text}`);
|
|
99
|
+
console.log(` Language: ${response.language}`);
|
|
100
|
+
console.log(` Duration: ${response.duration}s`);
|
|
101
|
+
console.log(` Estimated cost: $${response.usd_cost?.toFixed(6)}`);
|
|
102
|
+
|
|
103
|
+
expect(response.text).toBeTruthy();
|
|
104
|
+
expect(response.text.toLowerCase()).toContain("hello");
|
|
105
|
+
}, 60000);
|
|
106
|
+
|
|
107
|
+
// ─── 3. Image Generation (DALL-E 3) ─────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
test("3. OpenAI DALL-E 3 – generate image and save to disk", async () => {
|
|
110
|
+
if (!process.env.OPENAI_KEY) {
|
|
111
|
+
console.log("Skipping: OPENAI_KEY not set");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const prompt =
|
|
116
|
+
"A photorealistic image of a futuristic robot reading a book in a cozy library, warm lighting, 4k detail";
|
|
117
|
+
|
|
118
|
+
const response = await client.createImageGeneration("openai", {
|
|
119
|
+
model: "dall-e-3",
|
|
120
|
+
prompt,
|
|
121
|
+
n: 1,
|
|
122
|
+
size: "1024x1024",
|
|
123
|
+
quality: "standard",
|
|
124
|
+
response_format: "b64_json",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(response.data).toHaveLength(1);
|
|
128
|
+
|
|
129
|
+
const imageData = response.data[0];
|
|
130
|
+
const outputPath = path.join(OUTPUT_DIR, "dalle3-output.png");
|
|
131
|
+
|
|
132
|
+
if (imageData.b64_json) {
|
|
133
|
+
const buffer = Buffer.from(imageData.b64_json, "base64");
|
|
134
|
+
fs.writeFileSync(outputPath, buffer);
|
|
135
|
+
console.log(`✅ Image saved to: ${outputPath}`);
|
|
136
|
+
console.log(` Size: ${buffer.length} bytes`);
|
|
137
|
+
} else if (imageData.url) {
|
|
138
|
+
fs.writeFileSync(
|
|
139
|
+
path.join(OUTPUT_DIR, "dalle3-output-url.txt"),
|
|
140
|
+
imageData.url
|
|
141
|
+
);
|
|
142
|
+
console.log(`✅ Image URL saved: ${imageData.url}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (imageData.revised_prompt) {
|
|
146
|
+
console.log(` Revised prompt: ${imageData.revised_prompt}`);
|
|
147
|
+
}
|
|
148
|
+
console.log(` Estimated cost: $${response.usd_cost?.toFixed(6)}`);
|
|
149
|
+
|
|
150
|
+
expect(response.data.length).toBeGreaterThan(0);
|
|
151
|
+
expect(response.created).toBeGreaterThan(0);
|
|
152
|
+
}, 60000);
|
|
153
|
+
|
|
154
|
+
// ─── 4. Vision – send generated image back to a model ───────────────────────
|
|
155
|
+
|
|
156
|
+
test("4. OpenAI Vision – describe the generated DALL-E image", async () => {
|
|
157
|
+
if (!process.env.OPENAI_KEY) {
|
|
158
|
+
console.log("Skipping: OPENAI_KEY not set");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const imagePath = path.join(OUTPUT_DIR, "dalle3-output.png");
|
|
163
|
+
if (!fs.existsSync(imagePath)) {
|
|
164
|
+
console.log(
|
|
165
|
+
"Skipping: dalle3-output.png not found – run image generation test first"
|
|
166
|
+
);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const imageBuffer = fs.readFileSync(imagePath);
|
|
171
|
+
const base64Image = imageBuffer.toString("base64");
|
|
172
|
+
const dataUrl = `data:image/png;base64,${base64Image}`;
|
|
173
|
+
|
|
174
|
+
const response = await client.createCompletion("openai", {
|
|
175
|
+
model: Models.openai.GPT_4o,
|
|
176
|
+
messages: [
|
|
177
|
+
{
|
|
178
|
+
role: "user",
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: "Please describe this image in detail. What do you see?",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: "image_url",
|
|
186
|
+
image_url: { url: dataUrl },
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
max_tokens: 500,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const description = response.choices[0]?.message?.content || "";
|
|
195
|
+
const outputPath = path.join(OUTPUT_DIR, "vision-description.txt");
|
|
196
|
+
fs.writeFileSync(outputPath, description);
|
|
197
|
+
|
|
198
|
+
console.log(`✅ Vision description saved to: ${outputPath}`);
|
|
199
|
+
console.log(` Description: ${description.substring(0, 200)}...`);
|
|
200
|
+
console.log(` Estimated cost: $${response.usd_cost?.toFixed(6)}`);
|
|
201
|
+
|
|
202
|
+
expect(description).toBeTruthy();
|
|
203
|
+
expect(description.length).toBeGreaterThan(10);
|
|
204
|
+
}, 60000);
|
|
205
|
+
|
|
206
|
+
// ─── 5. Video Generation (Sora 2) ────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
test("5. OpenAI Sora 2 – generate video and save to disk", async () => {
|
|
209
|
+
if (!process.env.OPENAI_KEY) {
|
|
210
|
+
console.log("Skipping: OPENAI_KEY not set");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const outputPath = path.join(OUTPUT_DIR, "sora-output.mp4");
|
|
215
|
+
const outputUrlPath = path.join(OUTPUT_DIR, "sora-output-url.txt");
|
|
216
|
+
const jobIdPath = path.join(OUTPUT_DIR, "sora-job-id.txt");
|
|
217
|
+
|
|
218
|
+
// If final output already exists, skip entirely
|
|
219
|
+
if (fs.existsSync(outputPath) || fs.existsSync(outputUrlPath)) {
|
|
220
|
+
console.log(`⏭️ Skipping: output already exists`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const apiKey = process.env.OPENAI_KEY!;
|
|
225
|
+
|
|
226
|
+
// Helper: poll a known video ID until done, then download and save
|
|
227
|
+
async function pollAndSave(videoId: string) {
|
|
228
|
+
const maxPollingTime = 20 * 60 * 1000; // 20 minutes
|
|
229
|
+
const pollingInterval = 15000; // 15 seconds
|
|
230
|
+
const startTime = Date.now();
|
|
231
|
+
|
|
232
|
+
console.log(`⏳ Polling video ID: ${videoId}`);
|
|
233
|
+
|
|
234
|
+
while (Date.now() - startTime < maxPollingTime) {
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, pollingInterval));
|
|
236
|
+
|
|
237
|
+
const statusResponse = await fetch(
|
|
238
|
+
`https://api.openai.com/v1/videos/${videoId}`,
|
|
239
|
+
{ headers: { Authorization: `Bearer ${apiKey}` } }
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (!statusResponse.ok) {
|
|
243
|
+
const errorText = await statusResponse.text();
|
|
244
|
+
throw new Error(`OpenAI video status check failed: ${statusResponse.status} ${errorText}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const statusData = await statusResponse.json();
|
|
248
|
+
console.log(` Status: ${statusData.status}`);
|
|
249
|
+
|
|
250
|
+
if (statusData.status === "completed") {
|
|
251
|
+
// Download the video content
|
|
252
|
+
const contentResponse = await fetch(
|
|
253
|
+
`https://api.openai.com/v1/videos/${videoId}/content`,
|
|
254
|
+
{ headers: { Authorization: `Bearer ${apiKey}` } }
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (!contentResponse.ok) {
|
|
258
|
+
const errorText = await contentResponse.text();
|
|
259
|
+
throw new Error(`OpenAI video download failed: ${contentResponse.status} ${errorText}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const videoBuffer = Buffer.from(await contentResponse.arrayBuffer());
|
|
263
|
+
fs.writeFileSync(outputPath, videoBuffer);
|
|
264
|
+
console.log(`✅ Video saved to: ${outputPath} (${videoBuffer.length} bytes)`);
|
|
265
|
+
|
|
266
|
+
// Clean up job ID file now that we have the video
|
|
267
|
+
if (fs.existsSync(jobIdPath)) fs.unlinkSync(jobIdPath);
|
|
268
|
+
return statusData;
|
|
269
|
+
} else if (statusData.status === "failed") {
|
|
270
|
+
throw new Error(`OpenAI video generation failed: ${JSON.stringify(statusData)}`);
|
|
271
|
+
}
|
|
272
|
+
// queued or in_progress – keep polling
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw new Error("OpenAI video generation timed out after 20 minutes of polling");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// If we already have a job ID from a previous (timed-out) run, resume polling
|
|
279
|
+
if (fs.existsSync(jobIdPath)) {
|
|
280
|
+
const videoId = fs.readFileSync(jobIdPath, "utf8").trim();
|
|
281
|
+
console.log(`🔄 Resuming poll for existing job ID: ${videoId}`);
|
|
282
|
+
const statusData = await pollAndSave(videoId);
|
|
283
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Otherwise start a new job
|
|
288
|
+
const prompt =
|
|
289
|
+
"A serene mountain lake at sunrise, mist rising from the water, " +
|
|
290
|
+
"golden light filtering through pine trees, cinematic wide shot";
|
|
291
|
+
|
|
292
|
+
console.log("⏳ Submitting OpenAI Sora 2 video generation job...");
|
|
293
|
+
|
|
294
|
+
const createPayload: any = {
|
|
295
|
+
model: Models.openai.Sora_2,
|
|
296
|
+
prompt,
|
|
297
|
+
seconds: "5",
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const createResponse = await fetch("https://api.openai.com/v1/videos", {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: {
|
|
303
|
+
"Content-Type": "application/json",
|
|
304
|
+
Authorization: `Bearer ${apiKey}`,
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify(createPayload),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (!createResponse.ok) {
|
|
310
|
+
const errorText = await createResponse.text();
|
|
311
|
+
throw new Error(`OpenAI video creation failed: ${createResponse.status} ${errorText}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const createData = await createResponse.json();
|
|
315
|
+
const videoId = createData.id;
|
|
316
|
+
|
|
317
|
+
if (!videoId) {
|
|
318
|
+
throw new Error(`No video ID in response: ${JSON.stringify(createData)}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Persist the job ID so subsequent runs can resume if this run times out
|
|
322
|
+
fs.writeFileSync(jobIdPath, videoId);
|
|
323
|
+
console.log(`📝 Job ID saved to: ${jobIdPath} (ID: ${videoId})`);
|
|
324
|
+
|
|
325
|
+
await pollAndSave(videoId);
|
|
326
|
+
|
|
327
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
328
|
+
}, 1500000); // 25 minute timeout
|
|
329
|
+
});
|