@bonginkan/maria 4.3.15 → 4.3.17

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);
@@ -7008,7 +7017,7 @@ async function atomicRename(stage, dest) {
7008
7017
  }
7009
7018
  async function saveArtifacts(ctx, items, manifest) {
7010
7019
  const root = ctx.root;
7011
- const base = ctx.baseDir || (ctx.kind === "image" ? "artifacts/media/images" : "artifacts/media/videos");
7020
+ const base = typeof ctx.baseDir === "string" ? ctx.baseDir : "";
7012
7021
  const trace = ctx.trace || Math.random().toString(36).slice(2, 10).toUpperCase();
7013
7022
  const stage = stageDir(root, trace);
7014
7023
  ensureDirSync(stage);
@@ -7016,7 +7025,7 @@ async function saveArtifacts(ctx, items, manifest) {
7016
7025
  const dateSeg = datePath(/* @__PURE__ */ new Date());
7017
7026
  const reqHash = manifest.request && manifest.request.promptHash || "sha256:unknown";
7018
7027
  const slug = hashPrefix(reqHash, 6);
7019
- const outDirSeg = `${base}/${dateSeg}/${slug}`;
7028
+ const outDirSeg = ctx.flat ? base || "" : `${base ? base + "/" : ""}${dateSeg}/${slug}`;
7020
7029
  const outDir = safeJoin(root, outDirSeg).full;
7021
7030
  ensureDirSync(outDir);
7022
7031
  const saved = [];
@@ -7026,7 +7035,8 @@ async function saveArtifacts(ctx, items, manifest) {
7026
7035
  const ext = it.ext.startsWith(".") ? it.ext : `.${it.ext}`;
7027
7036
  const baseName = it.logicalName ? `${it.logicalName}` : `${contentHash}`;
7028
7037
  const fname = `${baseName}${ext}`;
7029
- const dest = safeJoin(root, `${outDirSeg}/${fname}`);
7038
+ const relPath = outDirSeg ? `${outDirSeg}/${fname}` : `${fname}`;
7039
+ const dest = safeJoin(root, relPath);
7030
7040
  validateWinPathEdge(dest.full);
7031
7041
  if (await hasCaseInsensitiveCollision(path__namespace.dirname(dest.full), path__namespace.basename(dest.full))) {
7032
7042
  throw new Error("case-insensitive filename collision");
@@ -7044,25 +7054,31 @@ async function saveArtifacts(ctx, items, manifest) {
7044
7054
  }
7045
7055
  await fsp__namespace.writeFile(stg, it.bytes);
7046
7056
  await atomicRename(stg, dest.full);
7047
- saved.push(dest.rel);
7057
+ const relPosix = dest.rel.replace(/\\/g, "/");
7058
+ saved.push(relPosix);
7048
7059
  }
7049
- const manifestObj = {
7050
- manifestVersion: 1,
7051
- ...manifest,
7052
- createdAt: manifest.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
7053
- artifacts: manifest.artifacts && manifest.artifacts.length > 0 ? manifest.artifacts : saved.map((file) => ({ file, hash: `sha256:${path__namespace.basename(file).split(".")[0]}` }))
7054
- };
7055
- const manifestPathRel = `${outDirSeg}/manifest.json`;
7056
- const manifestStage = path__namespace.join(stage, "manifest.json.part");
7057
- const manifestFull = safeJoin(root, manifestPathRel).full;
7058
- await fsp__namespace.writeFile(manifestStage, JSON.stringify(manifestObj, null, 2), "utf8");
7059
- try {
7060
- console.log(`[store] manifest -> ${manifestPathRel}`);
7061
- } catch {
7060
+ if (!ctx.skipManifest) {
7061
+ const manifestObj = {
7062
+ manifestVersion: 1,
7063
+ ...manifest,
7064
+ createdAt: manifest.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
7065
+ artifacts: manifest.artifacts && manifest.artifacts.length > 0 ? manifest.artifacts : saved.map((file) => ({ file, hash: `sha256:${path__namespace.basename(file).split(".")[0]}` }))
7066
+ };
7067
+ const manifestPathRel = `${outDirSeg ? outDirSeg + "/" : ""}manifest.json`;
7068
+ const manifestStage = path__namespace.join(stage, "manifest.json.part");
7069
+ const manifestFull = safeJoin(root, manifestPathRel).full;
7070
+ await fsp__namespace.writeFile(manifestStage, JSON.stringify(manifestObj, null, 2), "utf8");
7071
+ try {
7072
+ console.log(`[store] manifest -> ${manifestPathRel}`);
7073
+ } catch {
7074
+ }
7075
+ await atomicRename(manifestStage, manifestFull);
7076
+ await fsp__namespace.rm(stage, { recursive: true, force: true });
7077
+ return { files: saved, manifestPath: manifestPathRel.replace(/\\/g, "/") };
7078
+ } else {
7079
+ await fsp__namespace.rm(stage, { recursive: true, force: true });
7080
+ return { files: saved, manifestPath: "" };
7062
7081
  }
7063
- await atomicRename(manifestStage, manifestFull);
7064
- await fsp__namespace.rm(stage, { recursive: true, force: true });
7065
- return { files: saved, manifestPath: manifestPathRel };
7066
7082
  } catch (e2) {
7067
7083
  await fsp__namespace.rm(stage, { recursive: true, force: true });
7068
7084
  throw e2;
@@ -7105,6 +7121,13 @@ var GeminiMediaProvider = class {
7105
7121
  `GeminiMediaProvider.generateImage request failed: model=${modelName}; prompt="${promptPreview}"; error=${errMsg}`
7106
7122
  );
7107
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
+ }
7108
7131
  const parts = resp?.response?.candidates?.[0]?.content?.parts || [];
7109
7132
  for (const p of parts) {
7110
7133
  const data = p?.inlineData?.data || p?.inline_data?.data;
@@ -7795,12 +7818,12 @@ var UnifiedBaseProvider = class {
7795
7818
  }
7796
7819
  // Helper method for HTTP requests
7797
7820
  async makeRequest(url, options) {
7798
- const fetch2 = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
7821
+ const fetch3 = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
7799
7822
  const timeoutMs = options.timeout || 3e4;
7800
7823
  const controller = new AbortController();
7801
7824
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
7802
7825
  try {
7803
- const response2 = await fetch2(url, {
7826
+ const response2 = await fetch3(url, {
7804
7827
  method: "POST",
7805
7828
  headers: {
7806
7829
  "Content-Type": "application/json",
@@ -7824,12 +7847,12 @@ var UnifiedBaseProvider = class {
7824
7847
  }
7825
7848
  // Helper method for streaming requests
7826
7849
  async makeStreamRequest(url, options) {
7827
- const fetch2 = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
7850
+ const fetch3 = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
7828
7851
  const timeoutMs = options.timeout || 3e4;
7829
7852
  const controller = new AbortController();
7830
7853
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
7831
7854
  try {
7832
- const response2 = await fetch2(url, {
7855
+ const response2 = await fetch3(url, {
7833
7856
  method: "POST",
7834
7857
  headers: {
7835
7858
  "Content-Type": "application/json",
@@ -8532,12 +8555,17 @@ var app = express__default.default();
8532
8555
  var port = process.env.PORT || 8080;
8533
8556
  app.use(helmet__default.default());
8534
8557
  app.use(cors__default.default());
8535
- app.use(compression__default.default());
8558
+ app.use(compression__default.default({
8559
+ filter: (req, res) => {
8560
+ if (/\.(mp4|webm|mov)$/i.test(req.url)) return false;
8561
+ return compression.filter(req, res);
8562
+ }
8563
+ }));
8536
8564
  app.use(express__default.default.json({ limit: "10mb" }));
8537
8565
  app.use(express__default.default.urlencoded({ extended: true }));
8538
8566
  try {
8539
8567
  const artifactsDir = path__namespace.default.resolve(process.cwd(), "artifacts");
8540
- app.use("/artifacts", express__default.default.static(artifactsDir, { fallthrough: true, extensions: ["json", "png", "jpg", "webp", "mp4"] }));
8568
+ app.use("/artifacts", express__default.default.static(artifactsDir, { fallthrough: true, extensions: ["json", "png", "jpg", "webp", "mp4", "webm", "mov"] }));
8541
8569
  } catch {
8542
8570
  }
8543
8571
  app.use((req, res, next) => {
@@ -8561,7 +8589,7 @@ app.get("/api/status", (req, res) => {
8561
8589
  app.get("/", (req, res) => {
8562
8590
  res.json({
8563
8591
  name: "MARIA CODE API",
8564
- version: "4.3.9",
8592
+ version: "4.3.17",
8565
8593
  status: "running",
8566
8594
  environment: process.env.NODE_ENV || "development",
8567
8595
  endpoints: {
@@ -8576,6 +8604,30 @@ app.get("/", (req, res) => {
8576
8604
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8577
8605
  });
8578
8606
  });
8607
+ function classifyMediaError(err) {
8608
+ const raw = err;
8609
+ const msg = String(raw?.message || raw || "unknown error");
8610
+ const lower2 = msg.toLowerCase();
8611
+ if (lower2.includes("missing api key") || lower2.includes("api key") && lower2.includes("missing")) {
8612
+ 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" };
8613
+ }
8614
+ if (lower2.includes("invalid api key") || lower2.includes("permission denied") || lower2.includes("unauthorized")) {
8615
+ return { status: 502, code: "provider_auth_failed", message: "Provider authentication failed", hint: "Verify your Google AI Studio API key" };
8616
+ }
8617
+ if (lower2.includes("blockreason") || lower2.includes("safety") || lower2.includes("blocked") || lower2.includes("policy")) {
8618
+ return { status: 422, code: "policy_violation", message: "Request was blocked by provider policy", hint: "Modify the prompt to comply with safety policies" };
8619
+ }
8620
+ if (lower2.includes("no inline image returned") || lower2.includes("no video returned") || lower2.includes("refus")) {
8621
+ return { status: 422, code: "content_refused", message: "Model refused or returned no content", hint: "Try rephrasing the prompt or a different model" };
8622
+ }
8623
+ if (lower2.includes("timeout")) {
8624
+ return { status: 504, code: "timeout", message: "Generation timed out", hint: "Please retry later" };
8625
+ }
8626
+ if (lower2.includes("rate limit") || lower2.includes("429")) {
8627
+ return { status: 429, code: "rate_limited", message: "Rate limit exceeded", hint: "Slow down requests or try again shortly" };
8628
+ }
8629
+ return { status: 500, code: "internal_error", message: "Failed to generate media" };
8630
+ }
8579
8631
  async function decodeFirebaseToken(token) {
8580
8632
  try {
8581
8633
  const admin = await import('firebase-admin').catch(() => null);
@@ -8705,9 +8757,10 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
8705
8757
  try {
8706
8758
  await loadProviderKeys();
8707
8759
  const auth = req.headers.authorization;
8708
- if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
8760
+ if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized", message: "Login required", hint: "Sign in and retry" });
8709
8761
  const idToken = auth.substring("Bearer ".length).trim();
8710
8762
  const decoded = await decodeFirebaseToken(idToken).catch(() => null);
8763
+ if (!decoded) return res.status(401).json({ error: "unauthorized", message: "Invalid login session", hint: "Re-login to continue" });
8711
8764
  const uid = decoded?.uid || decoded?.sub || "current";
8712
8765
  const { prompt, model, size = "1024x1024", format = "png", count = 1, seed } = req.body || {};
8713
8766
  if (!prompt) return res.status(400).json({ error: "bad_request", message: "prompt required" });
@@ -8749,42 +8802,46 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
8749
8802
  return res.json({ success: true, data: { url: saved.manifestPath, files: saved.files, filesInline, jobId: manifest.trace } });
8750
8803
  } catch (error) {
8751
8804
  console.error("[Image API] Error:", error);
8752
- return res.status(500).json({
8753
- error: "internal_error",
8754
- message: "Failed to generate image"
8755
- });
8805
+ const mapped = classifyMediaError(error);
8806
+ return res.status(mapped.status).json({ error: mapped.code, message: mapped.message, hint: mapped.hint });
8756
8807
  }
8757
8808
  });
8758
8809
  app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
8759
8810
  try {
8760
8811
  await loadProviderKeys();
8761
8812
  const auth = req.headers.authorization;
8762
- if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
8763
- const { prompt, duration = 5, fps = 24, res: resStr = "1280x720", format = "mp4", model, seed } = req.body || {};
8813
+ if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized", message: "Login required", hint: "Sign in and retry" });
8814
+ const { prompt, duration = 8, fps = 24, res: resStr = "1280x720", format = "mp4", model, seed } = req.body || {};
8764
8815
  if (!prompt) return res.status(400).json({ error: "bad_request", message: "prompt required" });
8765
8816
  const m2 = /^(\d{2,4})x(\d{2,4})$/.exec(String(resStr));
8766
8817
  if (!m2) return res.status(400).json({ error: "bad_request", message: "res must be WxH" });
8767
8818
  const w = +m2[1], h2 = +m2[2];
8768
8819
  const { GoogleGenAI } = __require("@google/genai");
8769
8820
  const apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
8770
- if (!apiKey) return res.status(500).json({ error: "internal_error", message: "No Google API key configured" });
8821
+ 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" });
8771
8822
  const ai = new GoogleGenAI({ apiKey });
8772
8823
  const veoModel = model && String(model).trim() || process.env.MARIA_VIDEO_MODEL || "veo-3.0-generate-001";
8773
8824
  const aspectRatio = w >= h2 ? "16:9" : "9:16";
8774
8825
  const startedMs = Date.now();
8826
+ const requestedDuration = Number(duration);
8827
+ const effectiveDuration = Number.isFinite(requestedDuration) && requestedDuration === 8 ? 8 : 8;
8828
+ const requestedFps = Number(fps);
8829
+ const effectiveFps = Number.isFinite(requestedFps) ? Math.min(60, Math.max(1, Math.floor(requestedFps))) : 24;
8775
8830
  let operation = await ai.models.generateVideos({
8776
8831
  model: veoModel,
8777
8832
  prompt: String(prompt),
8778
- config: { aspectRatio }
8833
+ // Pass duration/fps to provider to avoid default ~1s clips
8834
+ config: { aspectRatio, durationSeconds: effectiveDuration, frameRate: effectiveFps }
8779
8835
  });
8780
- const deadline = Date.now() + 6e5;
8836
+ const deadline = Date.now() + 72e4;
8781
8837
  while (!operation?.done) {
8782
8838
  if (Date.now() > deadline) {
8783
8839
  return res.status(504).json({ error: "timeout", message: "video generation timed out" });
8784
8840
  }
8785
- await new Promise((r2) => setTimeout(r2, 2e3));
8841
+ await new Promise((r2) => setTimeout(r2, 1e4));
8786
8842
  operation = await ai.operations.getVideosOperation({ operation });
8787
8843
  }
8844
+ await new Promise((r2) => setTimeout(r2, 2500));
8788
8845
  const videoRef = operation?.response?.generatedVideos?.[0]?.video;
8789
8846
  const videoMime = videoRef && (videoRef.mimeType || videoRef.mime_type) ? String(videoRef.mimeType || videoRef.mime_type) : void 0;
8790
8847
  if (!videoRef) {
@@ -8794,22 +8851,153 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
8794
8851
  const tmpDir = path__namespace.default.join(process.cwd(), ".stage", traceId);
8795
8852
  await fsp__namespace.default.mkdir(tmpDir, { recursive: true });
8796
8853
  const tmpOut = path__namespace.default.join(tmpDir, "video.bin");
8797
- await ai.files.download({ file: videoRef, downloadPath: tmpOut });
8798
- {
8799
- const deadline2 = Date.now() + 1e4;
8854
+ const dlApiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || "";
8855
+ const isLikelyValidVideo = async (buf) => {
8856
+ if (!buf || buf.length < 16) return false;
8857
+ const head = buf.subarray(0, Math.min(256, buf.length));
8858
+ const headStr = head.toString("latin1");
8859
+ const hasFtyp2 = headStr.indexOf("ftyp") >= 0 || head.includes(Buffer.from("ftyp", "ascii"));
8860
+ const isWebm2 = head[0] === 26 && head[1] === 69 && head[2] === 223 && head[3] === 163;
8861
+ const looksHtml = headStr.includes("<!doctype") || headStr.includes("<html") || headStr.includes("<?xml");
8862
+ return (hasFtyp2 || isWebm2) && !looksHtml;
8863
+ };
8864
+ const readUint32BE = (buf, off) => (buf[off] << 24 | buf[off + 1] << 16 | buf[off + 2] << 8 | buf[off + 3]) >>> 0;
8865
+ const readUint64BE = (buf, off) => {
8866
+ const hi = readUint32BE(buf, off);
8867
+ const lo = readUint32BE(buf, off + 4);
8868
+ return hi * 2 ** 32 + lo;
8869
+ };
8870
+ const tryParseMp4DurationSec = (buf) => {
8871
+ try {
8872
+ for (let i2 = 0; i2 + 8 < buf.length; ) {
8873
+ const size = readUint32BE(buf, i2);
8874
+ const type = buf.subarray(i2 + 4, i2 + 8).toString("latin1");
8875
+ if (!size || size < 8) break;
8876
+ if (type === "moov") {
8877
+ const end = Math.min(buf.length, i2 + size);
8878
+ let j = i2 + 8;
8879
+ while (j + 8 < end) {
8880
+ const sz = readUint32BE(buf, j);
8881
+ const tp = buf.subarray(j + 4, j + 8).toString("latin1");
8882
+ if (!sz || sz < 8) break;
8883
+ if (tp === "mvhd") {
8884
+ const ver = buf[j + 8];
8885
+ if (ver === 1) {
8886
+ const timescale = readUint32BE(buf, j + 28);
8887
+ const duration2 = readUint64BE(buf, j + 32);
8888
+ if (timescale > 0) return duration2 / timescale;
8889
+ } else {
8890
+ const timescale = readUint32BE(buf, j + 20);
8891
+ const duration2 = readUint32BE(buf, j + 24);
8892
+ if (timescale > 0) return duration2 / timescale;
8893
+ }
8894
+ break;
8895
+ }
8896
+ j += sz;
8897
+ }
8898
+ break;
8899
+ }
8900
+ i2 += size;
8901
+ }
8902
+ } catch {
8903
+ }
8904
+ return null;
8905
+ };
8906
+ const hasAtom = (buf, atom) => {
8907
+ const tgt = Buffer.from(atom, "latin1");
8908
+ return buf.indexOf(tgt) !== -1;
8909
+ };
8910
+ const validateVideoBytes = (buf) => {
8911
+ if (!buf || buf.length < 1024) return { kind: "unknown", ok: false, durationSec: null, reason: "too_small" };
8912
+ const header2 = buf.subarray(0, 256);
8913
+ const headerStr2 = header2.toString("latin1");
8914
+ const hasFtyp2 = headerStr2.indexOf("ftyp") >= 0 || header2.includes(Buffer.from("ftyp", "ascii"));
8915
+ const isWebm2 = header2[0] === 26 && header2[1] === 69 && header2[2] === 223 && header2[3] === 163;
8916
+ if (isWebm2) {
8917
+ return { kind: "webm", ok: buf.length > 2e5, durationSec: null };
8918
+ }
8919
+ if (hasFtyp2) {
8920
+ const majorBrand = header2.subarray(8, 12).toString("latin1").toLowerCase();
8921
+ const kind = majorBrand.startsWith("qt") ? "mov" : "mp4";
8922
+ const hasMoov = hasAtom(buf, "moov");
8923
+ const dur = tryParseMp4DurationSec(buf);
8924
+ const nearTarget = typeof dur === "number" ? dur + 0.75 >= effectiveDuration : false;
8925
+ const ok = hasMoov && nearTarget;
8926
+ return { kind, ok, durationSec: dur, reason: ok ? void 0 : !hasMoov ? "missing_moov" : "short_duration" };
8927
+ }
8928
+ return { kind: "unknown", ok: false, durationSec: null, reason: "unknown_header" };
8929
+ };
8930
+ const ensureFileMaterialized = async (p, waitMs = 45e3) => {
8931
+ const deadline2 = Date.now() + waitMs;
8932
+ let lastSize = -1;
8933
+ let stableCount = 0;
8800
8934
  while (true) {
8801
8935
  try {
8802
- const st = await fsp__namespace.default.stat(tmpOut);
8803
- if (st && st.size > 0) break;
8936
+ const st = await fsp__namespace.default.stat(p);
8937
+ if (st && st.size > 0) {
8938
+ if (st.size === lastSize) {
8939
+ stableCount++;
8940
+ if (stableCount >= 5) return;
8941
+ } else {
8942
+ stableCount = 0;
8943
+ lastSize = st.size;
8944
+ }
8945
+ }
8804
8946
  } catch {
8805
8947
  }
8806
- if (Date.now() > deadline2) {
8807
- throw new Error(`video file not found after download: ${tmpOut}`);
8808
- }
8948
+ if (Date.now() > deadline2) throw new Error(`video file not fully materialized: ${p}`);
8809
8949
  await new Promise((r2) => setTimeout(r2, 200));
8810
8950
  }
8951
+ };
8952
+ const trySdkDownloadOnce = async () => {
8953
+ try {
8954
+ await ai.files.download({ file: videoRef, downloadPath: tmpOut });
8955
+ await ensureFileMaterialized(tmpOut, 45e3);
8956
+ const bytes = await fsp__namespace.default.readFile(tmpOut);
8957
+ if (await isLikelyValidVideo(bytes)) return bytes;
8958
+ } catch {
8959
+ }
8960
+ return null;
8961
+ };
8962
+ const tryManualDownload = async () => {
8963
+ try {
8964
+ const fileObj = videoRef;
8965
+ const direct = fileObj?.uri || fileObj?.url || fileObj?.downloadUri || fileObj?.downloadUrl || "";
8966
+ if (!direct || typeof direct !== "string") return null;
8967
+ const res2 = await fetch(direct, {
8968
+ method: "GET",
8969
+ headers: dlApiKey ? { "x-goog-api-key": dlApiKey } : void 0,
8970
+ redirect: "follow"
8971
+ });
8972
+ if (!res2.ok) return null;
8973
+ const lenHeader = res2.headers?.get?.("content-length");
8974
+ const expectedLen = lenHeader ? Number(lenHeader) : void 0;
8975
+ const ab = await res2.arrayBuffer();
8976
+ const buf = Buffer.from(ab);
8977
+ if (typeof expectedLen === "number" && Number.isFinite(expectedLen) && expectedLen > 0 && buf.length !== expectedLen) {
8978
+ return null;
8979
+ }
8980
+ await fsp__namespace.default.writeFile(tmpOut, buf);
8981
+ if (await isLikelyValidVideo(buf)) return buf;
8982
+ } catch {
8983
+ }
8984
+ return null;
8985
+ };
8986
+ let videoBytes = null;
8987
+ const maxAttempts = 10;
8988
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
8989
+ let bytes = await trySdkDownloadOnce();
8990
+ if (!bytes) bytes = await tryManualDownload();
8991
+ if (bytes && await isLikelyValidVideo(bytes)) {
8992
+ const v = validateVideoBytes(bytes);
8993
+ if (v.ok) {
8994
+ videoBytes = bytes;
8995
+ break;
8996
+ }
8997
+ }
8998
+ await new Promise((r2) => setTimeout(r2, Math.min(8e3, 1500 * attempt)));
8811
8999
  }
8812
- const videoBytes = await fsp__namespace.default.readFile(tmpOut);
9000
+ if (!videoBytes) throw new Error("failed to obtain fully materialized video");
8813
9001
  const header = videoBytes.subarray(0, 256);
8814
9002
  const headerStr = header.toString("latin1");
8815
9003
  let outExt = ".mp4";
@@ -8819,23 +9007,33 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
8819
9007
  if (videoMime && /webm/i.test(videoMime)) {
8820
9008
  outExt = ".webm";
8821
9009
  outMime = "video/webm";
9010
+ } else if (videoMime && /quicktime/i.test(videoMime)) {
9011
+ outExt = ".mov";
9012
+ outMime = "video/quicktime";
8822
9013
  } else if (isWebm) {
8823
9014
  outExt = ".webm";
8824
9015
  outMime = "video/webm";
8825
9016
  } else if (hasFtyp) {
8826
- outExt = ".mp4";
8827
- outMime = "video/mp4";
9017
+ const majorBrand = header.subarray(8, 12).toString("latin1");
9018
+ if ((majorBrand || "").toLowerCase().startsWith("qt")) {
9019
+ outExt = ".mov";
9020
+ outMime = "video/quicktime";
9021
+ } else {
9022
+ outExt = ".mp4";
9023
+ outMime = "video/mp4";
9024
+ }
8828
9025
  }
8829
9026
  await fsp__namespace.default.rm(tmpDir, { recursive: true, force: true });
8830
9027
  const promptHash = hashPrompt(prompt);
8831
9028
  const manifest = {
8832
9029
  kind: "video",
8833
- request: { promptHash, seed, params: { size: [w, h2], fps, duration, format: outExt.replace(".", "") }, model: veoModel, provider: "google" },
9030
+ request: { promptHash, seed, params: { size: [w, h2], fps: effectiveFps, duration: effectiveDuration, format: outExt.replace(".", "") }, model: veoModel, provider: "google" },
8834
9031
  artifacts: [],
8835
9032
  metrics: { durationMs: Date.now() - startedMs, retries: 0, fallbacks: 0 },
8836
9033
  trace: traceId
8837
9034
  };
8838
- const saved = await saveArtifacts({ root: process.cwd(), kind: "video" }, [{ bytes: videoBytes, ext: outExt }], manifest);
9035
+ const baseDir = path__namespace.default.join("artifacts", "media", "videos", traceId);
9036
+ const saved = await saveArtifacts({ root: process.cwd(), kind: "video", baseDir, flat: true }, [{ bytes: videoBytes, ext: outExt }], manifest);
8839
9037
  const idemKey = req.headers["idempotency-key"] || void 0;
8840
9038
  const idToken = auth.substring("Bearer ".length).trim();
8841
9039
  const decoded = await decodeFirebaseToken(idToken).catch(() => null);
@@ -8850,13 +9048,11 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
8850
9048
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8851
9049
  uid
8852
9050
  });
8853
- return res.json({ success: true, data: { url: saved.manifestPath, files: saved.files, jobId: manifest.trace } });
9051
+ 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 } } });
8854
9052
  } catch (error) {
8855
9053
  console.error("[Video API] Error:", error);
8856
- return res.status(500).json({
8857
- error: "internal_error",
8858
- message: "Failed to generate video"
8859
- });
9054
+ const mapped = classifyMediaError(error);
9055
+ return res.status(mapped.status).json({ error: mapped.code, message: mapped.message, hint: mapped.hint });
8860
9056
  }
8861
9057
  });
8862
9058
  app.get("/api/v1/jobs/:id", async (req, res) => {