@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.
@@ -6531,12 +6531,12 @@ __export(src_exports, {
6531
6531
  Response: () => Response,
6532
6532
  blobFrom: () => blobFrom,
6533
6533
  blobFromSync: () => blobFromSync,
6534
- default: () => fetch,
6534
+ default: () => fetch2,
6535
6535
  fileFrom: () => fileFrom,
6536
6536
  fileFromSync: () => fileFromSync,
6537
6537
  isRedirect: () => isRedirect
6538
6538
  });
6539
- async function fetch(url, options_) {
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(fetch(new Request(locationURL, requestOptions)));
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
- saved.push(dest.rel);
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 fetch2 = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
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 fetch2(url, {
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 fetch2 = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
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 fetch2(url, {
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({ projectId, secrets: {} });
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 [openaiApiKey, groqApiKey, anthropicApiKey, googleApiKey] = await Promise.all([
8443
- this.secrets.getApiKey("openai"),
8444
- this.secrets.getApiKey("groq"),
8445
- this.secrets.getApiKey("anthropic"),
8446
- this.secrets.getApiKey("google")
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: openaiApiKey || process.env.OPENAI_API_KEY,
8455
- groqApiKey: groqApiKey || process.env.GROQ_API_KEY,
8456
- anthropicApiKey: anthropicApiKey || process.env.ANTHROPIC_API_KEY,
8457
- googleApiKey: googleApiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_AI_API_KEY,
8487
+ openaiApiKey,
8488
+ groqApiKey,
8489
+ anthropicApiKey,
8490
+ googleApiKey,
8458
8491
  enableMetrics: true,
8459
8492
  enableFallback: true,
8460
8493
  maxRetries: 2
8461
8494
  });
8462
- await adapter.initialize();
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
- const { content } = await adapter.executeModelCall(messages, routeResult, modelDef);
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-4o", reasons: [...reasons, "fallback: openai key present"] };
8513
- if (input.keys.googleApiKey) return { provider: "google", model: "gemini-2.5-pro", reasons: [...reasons, "fallback: google key present"] };
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-3-5-sonnet-latest", reasons: [...reasons, "fallback: anthropic key present"] };
8516
- return { provider: "openai", model: "gpt-4o", reasons: [...reasons, "last-resort: no keys detected"] };
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.9",
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 status = error?.status || 500;
8668
- return res.status(status).json({ error: "ai_request_failed", message: error?.message || "AI request failed" });
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
- return res.status(500).json({
8764
- error: "internal_error",
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(500).json({ error: "internal_error", message: "No Google API key configured" });
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() + 6e5;
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, 2e3));
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 ensureFileMaterialized = async (p, waitMs = 1e4) => {
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) return;
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 found after download: ${p}`);
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, 15e3);
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
- for (let attempt = 1; attempt <= 3 && !videoBytes; attempt++) {
8847
- videoBytes = await trySdkDownloadOnce();
8848
- if (!videoBytes) await new Promise((r2) => setTimeout(r2, 600 * attempt));
8849
- }
8850
- if (!videoBytes) {
8851
- throw new Error("failed to download valid video bytes");
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 saved = await saveArtifacts({ root: process.cwd(), kind: "video", baseDir: "artifacts/media/videos", flat: false, skipManifest: true }, [{ bytes: videoBytes, ext: outExt }], manifest);
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
- return res.status(500).json({
8906
- error: "internal_error",
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) => {