@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.
- package/README.md +5 -5
- package/dist/READY.manifest.json +1 -1
- package/dist/bin/maria.cjs +147 -61
- package/dist/bin/maria.cjs.map +1 -1
- package/dist/cli.cjs +147 -61
- 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 +255 -59
- package/dist/server/express-server.js +255 -59
- package/dist/server-express.cjs +255 -59
- 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);
|
|
@@ -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
|
|
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}
|
|
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
|
|
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
|
-
|
|
7057
|
+
const relPosix = dest.rel.replace(/\\/g, "/");
|
|
7058
|
+
saved.push(relPosix);
|
|
7048
7059
|
}
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
8753
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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() +
|
|
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,
|
|
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
|
-
|
|
8798
|
-
{
|
|
8799
|
-
|
|
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(
|
|
8803
|
-
if (st && st.size > 0)
|
|
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
|
-
|
|
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
|
-
|
|
8827
|
-
|
|
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
|
|
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
|
-
|
|
8857
|
-
|
|
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) => {
|