@bonginkan/maria 4.3.16 → 4.3.18
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 +4 -4
- package/dist/READY.manifest.json +1 -1
- package/dist/bin/maria.cjs +351 -262
- package/dist/bin/maria.cjs.map +1 -1
- package/dist/cli.cjs +351 -262
- package/dist/cli.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/server/express-server.cjs +239 -56
- package/dist/server/express-server.js +239 -56
- package/dist/server-express.cjs +239 -56
- package/dist/server-express.cjs.map +1 -1
- package/package.json +2 -2
- package/src/slash-commands/READY.manifest.json +1 -1
|
@@ -6531,12 +6531,12 @@ __export(src_exports, {
|
|
|
6531
6531
|
Response: () => Response,
|
|
6532
6532
|
blobFrom: () => blobFrom,
|
|
6533
6533
|
blobFromSync: () => blobFromSync,
|
|
6534
|
-
default: () =>
|
|
6534
|
+
default: () => fetch2,
|
|
6535
6535
|
fileFrom: () => fileFrom,
|
|
6536
6536
|
fileFromSync: () => fileFromSync,
|
|
6537
6537
|
isRedirect: () => isRedirect
|
|
6538
6538
|
});
|
|
6539
|
-
async function
|
|
6539
|
+
async function fetch2(url, options_) {
|
|
6540
6540
|
return new Promise((resolve2, reject) => {
|
|
6541
6541
|
const request = new Request(url, options_);
|
|
6542
6542
|
const { parsedURL, options } = getNodeRequestOptions(request);
|
|
@@ -6668,7 +6668,7 @@ async function fetch(url, options_) {
|
|
|
6668
6668
|
if (responseReferrerPolicy) {
|
|
6669
6669
|
requestOptions.referrerPolicy = responseReferrerPolicy;
|
|
6670
6670
|
}
|
|
6671
|
-
resolve2(
|
|
6671
|
+
resolve2(fetch2(new Request(locationURL, requestOptions)));
|
|
6672
6672
|
finalize();
|
|
6673
6673
|
return;
|
|
6674
6674
|
}
|
|
@@ -6991,6 +6991,15 @@ async function hasCaseInsensitiveCollision(dirFull, targetFile) {
|
|
|
6991
6991
|
async function atomicRename(stage, dest) {
|
|
6992
6992
|
try {
|
|
6993
6993
|
await fsp__namespace.rename(stage, dest);
|
|
6994
|
+
try {
|
|
6995
|
+
const fd = await fsp__namespace.open(dest, "r");
|
|
6996
|
+
try {
|
|
6997
|
+
await fd.sync();
|
|
6998
|
+
} finally {
|
|
6999
|
+
await fd.close();
|
|
7000
|
+
}
|
|
7001
|
+
} catch {
|
|
7002
|
+
}
|
|
6994
7003
|
} catch (e2) {
|
|
6995
7004
|
if (e2 && e2.code === "EXDEV") {
|
|
6996
7005
|
await fsp__namespace.copyFile(stage, dest);
|
|
@@ -7045,7 +7054,8 @@ async function saveArtifacts(ctx, items, manifest) {
|
|
|
7045
7054
|
}
|
|
7046
7055
|
await fsp__namespace.writeFile(stg, it.bytes);
|
|
7047
7056
|
await atomicRename(stg, dest.full);
|
|
7048
|
-
|
|
7057
|
+
const relPosix = dest.rel.replace(/\\/g, "/");
|
|
7058
|
+
saved.push(relPosix);
|
|
7049
7059
|
}
|
|
7050
7060
|
if (!ctx.skipManifest) {
|
|
7051
7061
|
const manifestObj = {
|
|
@@ -7064,7 +7074,7 @@ async function saveArtifacts(ctx, items, manifest) {
|
|
|
7064
7074
|
}
|
|
7065
7075
|
await atomicRename(manifestStage, manifestFull);
|
|
7066
7076
|
await fsp__namespace.rm(stage, { recursive: true, force: true });
|
|
7067
|
-
return { files: saved, manifestPath: manifestPathRel };
|
|
7077
|
+
return { files: saved, manifestPath: manifestPathRel.replace(/\\/g, "/") };
|
|
7068
7078
|
} else {
|
|
7069
7079
|
await fsp__namespace.rm(stage, { recursive: true, force: true });
|
|
7070
7080
|
return { files: saved, manifestPath: "" };
|
|
@@ -7111,6 +7121,13 @@ var GeminiMediaProvider = class {
|
|
|
7111
7121
|
`GeminiMediaProvider.generateImage request failed: model=${modelName}; prompt="${promptPreview}"; error=${errMsg}`
|
|
7112
7122
|
);
|
|
7113
7123
|
}
|
|
7124
|
+
const feedback = resp?.response?.promptFeedback;
|
|
7125
|
+
const blockReason = feedback?.blockReason || feedback?.block_reason;
|
|
7126
|
+
if (blockReason) {
|
|
7127
|
+
const modelName2 = this.primaryModel;
|
|
7128
|
+
const reason = String(blockReason);
|
|
7129
|
+
throw new Error(`GeminiMediaProvider.policy_violation: model=${modelName2}; reason=${reason}`);
|
|
7130
|
+
}
|
|
7114
7131
|
const parts = resp?.response?.candidates?.[0]?.content?.parts || [];
|
|
7115
7132
|
for (const p of parts) {
|
|
7116
7133
|
const data = p?.inlineData?.data || p?.inline_data?.data;
|
|
@@ -7801,12 +7818,12 @@ var UnifiedBaseProvider = class {
|
|
|
7801
7818
|
}
|
|
7802
7819
|
// Helper method for HTTP requests
|
|
7803
7820
|
async makeRequest(url, options) {
|
|
7804
|
-
const
|
|
7821
|
+
const fetch3 = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
|
|
7805
7822
|
const timeoutMs = options.timeout || 3e4;
|
|
7806
7823
|
const controller = new AbortController();
|
|
7807
7824
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
7808
7825
|
try {
|
|
7809
|
-
const response2 = await
|
|
7826
|
+
const response2 = await fetch3(url, {
|
|
7810
7827
|
method: "POST",
|
|
7811
7828
|
headers: {
|
|
7812
7829
|
"Content-Type": "application/json",
|
|
@@ -7830,12 +7847,12 @@ var UnifiedBaseProvider = class {
|
|
|
7830
7847
|
}
|
|
7831
7848
|
// Helper method for streaming requests
|
|
7832
7849
|
async makeStreamRequest(url, options) {
|
|
7833
|
-
const
|
|
7850
|
+
const fetch3 = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
|
|
7834
7851
|
const timeoutMs = options.timeout || 3e4;
|
|
7835
7852
|
const controller = new AbortController();
|
|
7836
7853
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
7837
7854
|
try {
|
|
7838
|
-
const response2 = await
|
|
7855
|
+
const response2 = await fetch3(url, {
|
|
7839
7856
|
method: "POST",
|
|
7840
7857
|
headers: {
|
|
7841
7858
|
"Content-Type": "application/json",
|
|
@@ -8432,34 +8449,55 @@ var IMSFacade = class {
|
|
|
8432
8449
|
constructor(options = {}) {
|
|
8433
8450
|
this.options = options;
|
|
8434
8451
|
const projectId = options.projectId || process.env.GCLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT || "maria-code-470602";
|
|
8435
|
-
this.secrets = new SecretManagerIntegration({
|
|
8452
|
+
this.secrets = new SecretManagerIntegration({
|
|
8453
|
+
projectId,
|
|
8454
|
+
secrets: {
|
|
8455
|
+
openAI: "openai-api-key",
|
|
8456
|
+
googleAI: "google-ai-api-key",
|
|
8457
|
+
anthropic: "anthropic-api-key",
|
|
8458
|
+
groq: "groq-api-key"
|
|
8459
|
+
}
|
|
8460
|
+
});
|
|
8436
8461
|
}
|
|
8437
8462
|
secrets;
|
|
8438
8463
|
/** Route a simple chat request through IMS selection + providers */
|
|
8439
8464
|
async routeChat(req) {
|
|
8440
8465
|
const traceId = `ims_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
8441
8466
|
const optional = await this.secrets.getOptionalConfig().catch(() => ({}));
|
|
8442
|
-
const
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
]);
|
|
8467
|
+
const allKeys = await this.secrets.getAllApiKeys().catch(() => ({}));
|
|
8468
|
+
const openaiApiKey = allKeys.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
8469
|
+
const groqApiKey = allKeys.groqApiKey || process.env.GROQ_API_KEY;
|
|
8470
|
+
const anthropicApiKey = allKeys.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
8471
|
+
const googleApiKey = allKeys.googleApiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_AI_API_KEY;
|
|
8448
8472
|
const selection = this.selectProviderAndModel({
|
|
8449
8473
|
defaultProvider: optional.defaultProvider,
|
|
8450
8474
|
defaultModel: optional.defaultModel,
|
|
8451
8475
|
keys: { openaiApiKey, groqApiKey, anthropicApiKey, googleApiKey }
|
|
8452
8476
|
});
|
|
8477
|
+
const anyKey = openaiApiKey || groqApiKey || anthropicApiKey || googleApiKey;
|
|
8478
|
+
if (!anyKey) {
|
|
8479
|
+
const fallback = this.buildPoliteFallback(req.prompt);
|
|
8480
|
+
return {
|
|
8481
|
+
success: true,
|
|
8482
|
+
content: fallback,
|
|
8483
|
+
meta: { provider: "none", model: "none", traceId, reasons: ["no-keys-detected"] }
|
|
8484
|
+
};
|
|
8485
|
+
}
|
|
8453
8486
|
const adapter = new IMSProviderAdapter({
|
|
8454
|
-
openaiApiKey
|
|
8455
|
-
groqApiKey
|
|
8456
|
-
anthropicApiKey
|
|
8457
|
-
googleApiKey
|
|
8487
|
+
openaiApiKey,
|
|
8488
|
+
groqApiKey,
|
|
8489
|
+
anthropicApiKey,
|
|
8490
|
+
googleApiKey,
|
|
8458
8491
|
enableMetrics: true,
|
|
8459
8492
|
enableFallback: true,
|
|
8460
8493
|
maxRetries: 2
|
|
8461
8494
|
});
|
|
8462
|
-
|
|
8495
|
+
try {
|
|
8496
|
+
await adapter.initialize();
|
|
8497
|
+
} catch {
|
|
8498
|
+
const fallback = this.buildPoliteFallback(req.prompt);
|
|
8499
|
+
return { success: true, content: fallback, meta: { provider: "none", model: "none", traceId, reasons: ["adapter-init-failed"] } };
|
|
8500
|
+
}
|
|
8463
8501
|
const modelDef = {
|
|
8464
8502
|
id: `${selection.provider}:${selection.model}`,
|
|
8465
8503
|
providerId: selection.provider,
|
|
@@ -8482,12 +8520,17 @@ var IMSFacade = class {
|
|
|
8482
8520
|
generationParams: gen,
|
|
8483
8521
|
trace: { traceId, routedAt: (/* @__PURE__ */ new Date()).toISOString(), policyUsed: this.options.defaultPolicyId || "dev", fallbackChain: [] }
|
|
8484
8522
|
};
|
|
8485
|
-
const sys = req.systemPrompt && String(req.systemPrompt).trim() || "You are a concise assistant.";
|
|
8523
|
+
const sys = req.systemPrompt && String(req.systemPrompt).trim() || "You are a concise assistant. Make sure you answer in plain text, as a natural chat.";
|
|
8486
8524
|
const messages = [
|
|
8487
8525
|
{ role: "system", content: sys },
|
|
8488
8526
|
{ role: "user", content: req.prompt }
|
|
8489
8527
|
];
|
|
8490
|
-
|
|
8528
|
+
let content = "";
|
|
8529
|
+
try {
|
|
8530
|
+
({ content } = await adapter.executeModelCall(messages, routeResult, modelDef));
|
|
8531
|
+
} catch {
|
|
8532
|
+
content = this.buildPoliteFallback(req.prompt);
|
|
8533
|
+
}
|
|
8491
8534
|
return {
|
|
8492
8535
|
success: true,
|
|
8493
8536
|
content,
|
|
@@ -8499,6 +8542,12 @@ var IMSFacade = class {
|
|
|
8499
8542
|
}
|
|
8500
8543
|
};
|
|
8501
8544
|
}
|
|
8545
|
+
buildPoliteFallback(prompt) {
|
|
8546
|
+
const p = (prompt || "").trim();
|
|
8547
|
+
return p ? `Sorry, I can't reach the AI service right now.
|
|
8548
|
+
Your request: "${p}"
|
|
8549
|
+
I can still help summarize, clarify, or suggest next steps. Try again in a moment, or ask me to rephrase.` : "Sorry, I can't reach the AI service right now. Please try again in a moment.";
|
|
8550
|
+
}
|
|
8502
8551
|
selectProviderAndModel(input) {
|
|
8503
8552
|
const reasons = [];
|
|
8504
8553
|
if (input.defaultProvider && input.defaultModel) {
|
|
@@ -8509,11 +8558,11 @@ var IMSFacade = class {
|
|
|
8509
8558
|
}
|
|
8510
8559
|
reasons.push("override: default-provider has no key");
|
|
8511
8560
|
}
|
|
8512
|
-
if (input.keys.openaiApiKey) return { provider: "openai", model: "gpt-
|
|
8513
|
-
if (input.keys.googleApiKey) return { provider: "google", model: "gemini-2.5-
|
|
8561
|
+
if (input.keys.openaiApiKey) return { provider: "openai", model: "gpt-5-mini", reasons: [...reasons, "fallback: openai key present"] };
|
|
8562
|
+
if (input.keys.googleApiKey) return { provider: "google", model: "gemini-2.5-flash", reasons: [...reasons, "fallback: google key present"] };
|
|
8514
8563
|
if (input.keys.groqApiKey) return { provider: "groq", model: "llama-3.1-70b-versatile", reasons: [...reasons, "fallback: groq key present"] };
|
|
8515
|
-
if (input.keys.anthropicApiKey) return { provider: "anthropic", model: "claude-
|
|
8516
|
-
return { provider: "openai", model: "gpt-
|
|
8564
|
+
if (input.keys.anthropicApiKey) return { provider: "anthropic", model: "claude-sonnet-4-20250514", reasons: [...reasons, "fallback: anthropic key present"] };
|
|
8565
|
+
return { provider: "openai", model: "gpt-5-mini", reasons: [...reasons, "last-resort: no keys detected"] };
|
|
8517
8566
|
}
|
|
8518
8567
|
hasKey(provider, keys) {
|
|
8519
8568
|
switch (provider) {
|
|
@@ -8548,7 +8597,7 @@ app.use(express__default.default.json({ limit: "10mb" }));
|
|
|
8548
8597
|
app.use(express__default.default.urlencoded({ extended: true }));
|
|
8549
8598
|
try {
|
|
8550
8599
|
const artifactsDir = path__namespace.default.resolve(process.cwd(), "artifacts");
|
|
8551
|
-
app.use("/artifacts", express__default.default.static(artifactsDir, { fallthrough: true, extensions: ["json", "png", "jpg", "webp", "mp4"] }));
|
|
8600
|
+
app.use("/artifacts", express__default.default.static(artifactsDir, { fallthrough: true, extensions: ["json", "png", "jpg", "webp", "mp4", "webm", "mov"] }));
|
|
8552
8601
|
} catch {
|
|
8553
8602
|
}
|
|
8554
8603
|
app.use((req, res, next) => {
|
|
@@ -8572,7 +8621,7 @@ app.get("/api/status", (req, res) => {
|
|
|
8572
8621
|
app.get("/", (req, res) => {
|
|
8573
8622
|
res.json({
|
|
8574
8623
|
name: "MARIA CODE API",
|
|
8575
|
-
version: "4.3.
|
|
8624
|
+
version: "4.3.18",
|
|
8576
8625
|
status: "running",
|
|
8577
8626
|
environment: process.env.NODE_ENV || "development",
|
|
8578
8627
|
endpoints: {
|
|
@@ -8587,6 +8636,30 @@ app.get("/", (req, res) => {
|
|
|
8587
8636
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8588
8637
|
});
|
|
8589
8638
|
});
|
|
8639
|
+
function classifyMediaError(err) {
|
|
8640
|
+
const raw = err;
|
|
8641
|
+
const msg = String(raw?.message || raw || "unknown error");
|
|
8642
|
+
const lower2 = msg.toLowerCase();
|
|
8643
|
+
if (lower2.includes("missing api key") || lower2.includes("api key") && lower2.includes("missing")) {
|
|
8644
|
+
return { status: 503, code: "provider_unavailable", message: "Provider API key is not configured", hint: "Set GOOGLE_API_KEY or GEMINI_API_KEY on the server" };
|
|
8645
|
+
}
|
|
8646
|
+
if (lower2.includes("invalid api key") || lower2.includes("permission denied") || lower2.includes("unauthorized")) {
|
|
8647
|
+
return { status: 502, code: "provider_auth_failed", message: "Provider authentication failed", hint: "Verify your Google AI Studio API key" };
|
|
8648
|
+
}
|
|
8649
|
+
if (lower2.includes("blockreason") || lower2.includes("safety") || lower2.includes("blocked") || lower2.includes("policy")) {
|
|
8650
|
+
return { status: 422, code: "policy_violation", message: "Request was blocked by provider policy", hint: "Modify the prompt to comply with safety policies" };
|
|
8651
|
+
}
|
|
8652
|
+
if (lower2.includes("no inline image returned") || lower2.includes("no video returned") || lower2.includes("refus")) {
|
|
8653
|
+
return { status: 422, code: "content_refused", message: "Model refused or returned no content", hint: "Try rephrasing the prompt or a different model" };
|
|
8654
|
+
}
|
|
8655
|
+
if (lower2.includes("timeout")) {
|
|
8656
|
+
return { status: 504, code: "timeout", message: "Generation timed out", hint: "Please retry later" };
|
|
8657
|
+
}
|
|
8658
|
+
if (lower2.includes("rate limit") || lower2.includes("429")) {
|
|
8659
|
+
return { status: 429, code: "rate_limited", message: "Rate limit exceeded", hint: "Slow down requests or try again shortly" };
|
|
8660
|
+
}
|
|
8661
|
+
return { status: 500, code: "internal_error", message: "Failed to generate media" };
|
|
8662
|
+
}
|
|
8590
8663
|
async function decodeFirebaseToken(token) {
|
|
8591
8664
|
try {
|
|
8592
8665
|
const admin = await import('firebase-admin').catch(() => null);
|
|
@@ -8664,8 +8737,12 @@ app.post("/api/ai", rateLimitMiddleware, async (req, res) => {
|
|
|
8664
8737
|
return res.json(resp);
|
|
8665
8738
|
} catch (error) {
|
|
8666
8739
|
console.error("[AI API] Error:", error?.message || error);
|
|
8667
|
-
const
|
|
8668
|
-
|
|
8740
|
+
const polite = typeof req.body?.prompt === "string" ? `Sorry, I couldn't reach the AI service right now. Here\u2019s a quick human-style reply to keep you moving:
|
|
8741
|
+
|
|
8742
|
+
${String(req.body.prompt)}
|
|
8743
|
+
|
|
8744
|
+
If you want, I can try again in a moment or help you rephrase.` : `Sorry, I couldn't reach the AI service right now. Please try again in a moment.`;
|
|
8745
|
+
return res.status(200).json({ success: true, data: { content: polite } });
|
|
8669
8746
|
}
|
|
8670
8747
|
});
|
|
8671
8748
|
app.post("/api/auth/revoke", async (req, res) => {
|
|
@@ -8716,9 +8793,10 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
|
|
|
8716
8793
|
try {
|
|
8717
8794
|
await loadProviderKeys();
|
|
8718
8795
|
const auth = req.headers.authorization;
|
|
8719
|
-
if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
|
|
8796
|
+
if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized", message: "Login required", hint: "Sign in and retry" });
|
|
8720
8797
|
const idToken = auth.substring("Bearer ".length).trim();
|
|
8721
8798
|
const decoded = await decodeFirebaseToken(idToken).catch(() => null);
|
|
8799
|
+
if (!decoded) return res.status(401).json({ error: "unauthorized", message: "Invalid login session", hint: "Re-login to continue" });
|
|
8722
8800
|
const uid = decoded?.uid || decoded?.sub || "current";
|
|
8723
8801
|
const { prompt, model, size = "1024x1024", format = "png", count = 1, seed } = req.body || {};
|
|
8724
8802
|
if (!prompt) return res.status(400).json({ error: "bad_request", message: "prompt required" });
|
|
@@ -8760,17 +8838,15 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
|
|
|
8760
8838
|
return res.json({ success: true, data: { url: saved.manifestPath, files: saved.files, filesInline, jobId: manifest.trace } });
|
|
8761
8839
|
} catch (error) {
|
|
8762
8840
|
console.error("[Image API] Error:", error);
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
message: "Failed to generate image"
|
|
8766
|
-
});
|
|
8841
|
+
const mapped = classifyMediaError(error);
|
|
8842
|
+
return res.status(mapped.status).json({ error: mapped.code, message: mapped.message, hint: mapped.hint });
|
|
8767
8843
|
}
|
|
8768
8844
|
});
|
|
8769
8845
|
app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
|
|
8770
8846
|
try {
|
|
8771
8847
|
await loadProviderKeys();
|
|
8772
8848
|
const auth = req.headers.authorization;
|
|
8773
|
-
if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
|
|
8849
|
+
if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized", message: "Login required", hint: "Sign in and retry" });
|
|
8774
8850
|
const { prompt, duration = 8, fps = 24, res: resStr = "1280x720", format = "mp4", model, seed } = req.body || {};
|
|
8775
8851
|
if (!prompt) return res.status(400).json({ error: "bad_request", message: "prompt required" });
|
|
8776
8852
|
const m2 = /^(\d{2,4})x(\d{2,4})$/.exec(String(resStr));
|
|
@@ -8778,7 +8854,7 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
|
|
|
8778
8854
|
const w = +m2[1], h2 = +m2[2];
|
|
8779
8855
|
const { GoogleGenAI } = __require("@google/genai");
|
|
8780
8856
|
const apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
|
|
8781
|
-
if (!apiKey) return res.status(
|
|
8857
|
+
if (!apiKey) return res.status(503).json({ error: "provider_unavailable", message: "Provider API key is not configured", hint: "Set GOOGLE_API_KEY or GEMINI_API_KEY on the server" });
|
|
8782
8858
|
const ai = new GoogleGenAI({ apiKey });
|
|
8783
8859
|
const veoModel = model && String(model).trim() || process.env.MARIA_VIDEO_MODEL || "veo-3.0-generate-001";
|
|
8784
8860
|
const aspectRatio = w >= h2 ? "16:9" : "9:16";
|
|
@@ -8793,14 +8869,15 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
|
|
|
8793
8869
|
// Pass duration/fps to provider to avoid default ~1s clips
|
|
8794
8870
|
config: { aspectRatio, durationSeconds: effectiveDuration, frameRate: effectiveFps }
|
|
8795
8871
|
});
|
|
8796
|
-
const deadline = Date.now() +
|
|
8872
|
+
const deadline = Date.now() + 72e4;
|
|
8797
8873
|
while (!operation?.done) {
|
|
8798
8874
|
if (Date.now() > deadline) {
|
|
8799
8875
|
return res.status(504).json({ error: "timeout", message: "video generation timed out" });
|
|
8800
8876
|
}
|
|
8801
|
-
await new Promise((r2) => setTimeout(r2,
|
|
8877
|
+
await new Promise((r2) => setTimeout(r2, 1e4));
|
|
8802
8878
|
operation = await ai.operations.getVideosOperation({ operation });
|
|
8803
8879
|
}
|
|
8880
|
+
await new Promise((r2) => setTimeout(r2, 2500));
|
|
8804
8881
|
const videoRef = operation?.response?.generatedVideos?.[0]?.video;
|
|
8805
8882
|
const videoMime = videoRef && (videoRef.mimeType || videoRef.mime_type) ? String(videoRef.mimeType || videoRef.mime_type) : void 0;
|
|
8806
8883
|
if (!videoRef) {
|
|
@@ -8820,36 +8897,143 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
|
|
|
8820
8897
|
const looksHtml = headStr.includes("<!doctype") || headStr.includes("<html") || headStr.includes("<?xml");
|
|
8821
8898
|
return (hasFtyp2 || isWebm2) && !looksHtml;
|
|
8822
8899
|
};
|
|
8823
|
-
const
|
|
8900
|
+
const readUint32BE = (buf, off) => (buf[off] << 24 | buf[off + 1] << 16 | buf[off + 2] << 8 | buf[off + 3]) >>> 0;
|
|
8901
|
+
const readUint64BE = (buf, off) => {
|
|
8902
|
+
const hi = readUint32BE(buf, off);
|
|
8903
|
+
const lo = readUint32BE(buf, off + 4);
|
|
8904
|
+
return hi * 2 ** 32 + lo;
|
|
8905
|
+
};
|
|
8906
|
+
const tryParseMp4DurationSec = (buf) => {
|
|
8907
|
+
try {
|
|
8908
|
+
for (let i2 = 0; i2 + 8 < buf.length; ) {
|
|
8909
|
+
const size = readUint32BE(buf, i2);
|
|
8910
|
+
const type = buf.subarray(i2 + 4, i2 + 8).toString("latin1");
|
|
8911
|
+
if (!size || size < 8) break;
|
|
8912
|
+
if (type === "moov") {
|
|
8913
|
+
const end = Math.min(buf.length, i2 + size);
|
|
8914
|
+
let j = i2 + 8;
|
|
8915
|
+
while (j + 8 < end) {
|
|
8916
|
+
const sz = readUint32BE(buf, j);
|
|
8917
|
+
const tp = buf.subarray(j + 4, j + 8).toString("latin1");
|
|
8918
|
+
if (!sz || sz < 8) break;
|
|
8919
|
+
if (tp === "mvhd") {
|
|
8920
|
+
const ver = buf[j + 8];
|
|
8921
|
+
if (ver === 1) {
|
|
8922
|
+
const timescale = readUint32BE(buf, j + 28);
|
|
8923
|
+
const duration2 = readUint64BE(buf, j + 32);
|
|
8924
|
+
if (timescale > 0) return duration2 / timescale;
|
|
8925
|
+
} else {
|
|
8926
|
+
const timescale = readUint32BE(buf, j + 20);
|
|
8927
|
+
const duration2 = readUint32BE(buf, j + 24);
|
|
8928
|
+
if (timescale > 0) return duration2 / timescale;
|
|
8929
|
+
}
|
|
8930
|
+
break;
|
|
8931
|
+
}
|
|
8932
|
+
j += sz;
|
|
8933
|
+
}
|
|
8934
|
+
break;
|
|
8935
|
+
}
|
|
8936
|
+
i2 += size;
|
|
8937
|
+
}
|
|
8938
|
+
} catch {
|
|
8939
|
+
}
|
|
8940
|
+
return null;
|
|
8941
|
+
};
|
|
8942
|
+
const hasAtom = (buf, atom) => {
|
|
8943
|
+
const tgt = Buffer.from(atom, "latin1");
|
|
8944
|
+
return buf.indexOf(tgt) !== -1;
|
|
8945
|
+
};
|
|
8946
|
+
const validateVideoBytes = (buf) => {
|
|
8947
|
+
if (!buf || buf.length < 1024) return { kind: "unknown", ok: false, durationSec: null, reason: "too_small" };
|
|
8948
|
+
const header2 = buf.subarray(0, 256);
|
|
8949
|
+
const headerStr2 = header2.toString("latin1");
|
|
8950
|
+
const hasFtyp2 = headerStr2.indexOf("ftyp") >= 0 || header2.includes(Buffer.from("ftyp", "ascii"));
|
|
8951
|
+
const isWebm2 = header2[0] === 26 && header2[1] === 69 && header2[2] === 223 && header2[3] === 163;
|
|
8952
|
+
if (isWebm2) {
|
|
8953
|
+
return { kind: "webm", ok: buf.length > 2e5, durationSec: null };
|
|
8954
|
+
}
|
|
8955
|
+
if (hasFtyp2) {
|
|
8956
|
+
const majorBrand = header2.subarray(8, 12).toString("latin1").toLowerCase();
|
|
8957
|
+
const kind = majorBrand.startsWith("qt") ? "mov" : "mp4";
|
|
8958
|
+
const hasMoov = hasAtom(buf, "moov");
|
|
8959
|
+
const dur = tryParseMp4DurationSec(buf);
|
|
8960
|
+
const nearTarget = typeof dur === "number" ? dur + 0.75 >= effectiveDuration : false;
|
|
8961
|
+
const ok = hasMoov && nearTarget;
|
|
8962
|
+
return { kind, ok, durationSec: dur, reason: ok ? void 0 : !hasMoov ? "missing_moov" : "short_duration" };
|
|
8963
|
+
}
|
|
8964
|
+
return { kind: "unknown", ok: false, durationSec: null, reason: "unknown_header" };
|
|
8965
|
+
};
|
|
8966
|
+
const ensureFileMaterialized = async (p, waitMs = 45e3) => {
|
|
8824
8967
|
const deadline2 = Date.now() + waitMs;
|
|
8968
|
+
let lastSize = -1;
|
|
8969
|
+
let stableCount = 0;
|
|
8825
8970
|
while (true) {
|
|
8826
8971
|
try {
|
|
8827
8972
|
const st = await fsp__namespace.default.stat(p);
|
|
8828
|
-
if (st && st.size > 0)
|
|
8973
|
+
if (st && st.size > 0) {
|
|
8974
|
+
if (st.size === lastSize) {
|
|
8975
|
+
stableCount++;
|
|
8976
|
+
if (stableCount >= 5) return;
|
|
8977
|
+
} else {
|
|
8978
|
+
stableCount = 0;
|
|
8979
|
+
lastSize = st.size;
|
|
8980
|
+
}
|
|
8981
|
+
}
|
|
8829
8982
|
} catch {
|
|
8830
8983
|
}
|
|
8831
|
-
if (Date.now() > deadline2) throw new Error(`video file not
|
|
8984
|
+
if (Date.now() > deadline2) throw new Error(`video file not fully materialized: ${p}`);
|
|
8832
8985
|
await new Promise((r2) => setTimeout(r2, 200));
|
|
8833
8986
|
}
|
|
8834
8987
|
};
|
|
8835
8988
|
const trySdkDownloadOnce = async () => {
|
|
8836
8989
|
try {
|
|
8837
8990
|
await ai.files.download({ file: videoRef, downloadPath: tmpOut });
|
|
8838
|
-
await ensureFileMaterialized(tmpOut,
|
|
8991
|
+
await ensureFileMaterialized(tmpOut, 45e3);
|
|
8839
8992
|
const bytes = await fsp__namespace.default.readFile(tmpOut);
|
|
8840
8993
|
if (await isLikelyValidVideo(bytes)) return bytes;
|
|
8841
8994
|
} catch {
|
|
8842
8995
|
}
|
|
8843
8996
|
return null;
|
|
8844
8997
|
};
|
|
8998
|
+
const tryManualDownload = async () => {
|
|
8999
|
+
try {
|
|
9000
|
+
const fileObj = videoRef;
|
|
9001
|
+
const direct = fileObj?.uri || fileObj?.url || fileObj?.downloadUri || fileObj?.downloadUrl || "";
|
|
9002
|
+
if (!direct || typeof direct !== "string") return null;
|
|
9003
|
+
const res2 = await fetch(direct, {
|
|
9004
|
+
method: "GET",
|
|
9005
|
+
headers: dlApiKey ? { "x-goog-api-key": dlApiKey } : void 0,
|
|
9006
|
+
redirect: "follow"
|
|
9007
|
+
});
|
|
9008
|
+
if (!res2.ok) return null;
|
|
9009
|
+
const lenHeader = res2.headers?.get?.("content-length");
|
|
9010
|
+
const expectedLen = lenHeader ? Number(lenHeader) : void 0;
|
|
9011
|
+
const ab = await res2.arrayBuffer();
|
|
9012
|
+
const buf = Buffer.from(ab);
|
|
9013
|
+
if (typeof expectedLen === "number" && Number.isFinite(expectedLen) && expectedLen > 0 && buf.length !== expectedLen) {
|
|
9014
|
+
return null;
|
|
9015
|
+
}
|
|
9016
|
+
await fsp__namespace.default.writeFile(tmpOut, buf);
|
|
9017
|
+
if (await isLikelyValidVideo(buf)) return buf;
|
|
9018
|
+
} catch {
|
|
9019
|
+
}
|
|
9020
|
+
return null;
|
|
9021
|
+
};
|
|
8845
9022
|
let videoBytes = null;
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
9023
|
+
const maxAttempts = 10;
|
|
9024
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
9025
|
+
let bytes = await trySdkDownloadOnce();
|
|
9026
|
+
if (!bytes) bytes = await tryManualDownload();
|
|
9027
|
+
if (bytes && await isLikelyValidVideo(bytes)) {
|
|
9028
|
+
const v = validateVideoBytes(bytes);
|
|
9029
|
+
if (v.ok) {
|
|
9030
|
+
videoBytes = bytes;
|
|
9031
|
+
break;
|
|
9032
|
+
}
|
|
9033
|
+
}
|
|
9034
|
+
await new Promise((r2) => setTimeout(r2, Math.min(8e3, 1500 * attempt)));
|
|
8852
9035
|
}
|
|
9036
|
+
if (!videoBytes) throw new Error("failed to obtain fully materialized video");
|
|
8853
9037
|
const header = videoBytes.subarray(0, 256);
|
|
8854
9038
|
const headerStr = header.toString("latin1");
|
|
8855
9039
|
let outExt = ".mp4";
|
|
@@ -8884,7 +9068,8 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
|
|
|
8884
9068
|
metrics: { durationMs: Date.now() - startedMs, retries: 0, fallbacks: 0 },
|
|
8885
9069
|
trace: traceId
|
|
8886
9070
|
};
|
|
8887
|
-
const
|
|
9071
|
+
const baseDir = path__namespace.default.join("artifacts", "media", "videos", traceId);
|
|
9072
|
+
const saved = await saveArtifacts({ root: process.cwd(), kind: "video", baseDir, flat: true }, [{ bytes: videoBytes, ext: outExt }], manifest);
|
|
8888
9073
|
const idemKey = req.headers["idempotency-key"] || void 0;
|
|
8889
9074
|
const idToken = auth.substring("Bearer ".length).trim();
|
|
8890
9075
|
const decoded = await decodeFirebaseToken(idToken).catch(() => null);
|
|
@@ -8899,13 +9084,11 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
|
|
|
8899
9084
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8900
9085
|
uid
|
|
8901
9086
|
});
|
|
8902
|
-
return res.json({ success: true, data: { url: saved.files[0] ? `/${saved.files[0]}` : void 0, files: saved.files, jobId: manifest.trace, applied: { durationSeconds: effectiveDuration, frameRate: effectiveFps } } });
|
|
9087
|
+
return res.json({ success: true, data: { url: saved.manifestPath || (saved.files[0] ? `/${saved.files[0]}` : void 0), files: saved.files, jobId: manifest.trace, applied: { durationSeconds: effectiveDuration, frameRate: effectiveFps } } });
|
|
8903
9088
|
} catch (error) {
|
|
8904
9089
|
console.error("[Video API] Error:", error);
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
message: "Failed to generate video"
|
|
8908
|
-
});
|
|
9090
|
+
const mapped = classifyMediaError(error);
|
|
9091
|
+
return res.status(mapped.status).json({ error: mapped.code, message: mapped.message, hint: mapped.hint });
|
|
8909
9092
|
}
|
|
8910
9093
|
});
|
|
8911
9094
|
app.get("/api/v1/jobs/:id", async (req, res) => {
|