vidspotai-shared 1.0.51 → 1.0.53

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.
@@ -0,0 +1,35 @@
1
+ import { FieldValue } from "firebase-admin/firestore";
2
+ /**
3
+ * Daily rollup keyed by `${YYYY-MM-DD}_${modelKey}`.
4
+ * Written via FieldValue.increment from job finalization so that the
5
+ * admin dashboard never has to scan `videoJobs` for per-model stats.
6
+ */
7
+ export interface IAnalyticsDailyModel {
8
+ date: string;
9
+ modelKey: string;
10
+ jobsCompleted: number;
11
+ jobsFailed: number;
12
+ jobsPartial: number;
13
+ scenesCompleted: number;
14
+ scenesFailed: number;
15
+ scenesTimedOut: number;
16
+ totalCreditsUsed: number;
17
+ totalDurationSeconds: number;
18
+ jobLatencyMsSum: number;
19
+ jobLatencyMsCount: number;
20
+ updatedAt: FieldValue;
21
+ }
22
+ /** Cross-model daily rollup for summary cards. */
23
+ export interface IAnalyticsDailyGlobalModel {
24
+ date: string;
25
+ jobsCompleted: number;
26
+ jobsFailed: number;
27
+ jobsPartial: number;
28
+ scenesCompleted: number;
29
+ scenesFailed: number;
30
+ totalCreditsUsed: number;
31
+ jobLatencyMsSum: number;
32
+ jobLatencyMsCount: number;
33
+ updatedAt: FieldValue;
34
+ }
35
+ //# sourceMappingURL=analytics.model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.model.d.ts","sourceRoot":"","sources":["../../src/models/analytics.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEtD;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,kDAAkD;AAClD,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,UAAU,CAAC;CACvB"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,4 +1,5 @@
1
1
  export * from "./video.model";
2
2
  export * from "./user.model";
3
3
  export * from "./script.model";
4
+ export * from "./analytics.model";
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/models/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/models/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC"}
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./video.model"), exports);
18
18
  __exportStar(require("./user.model"), exports);
19
19
  __exportStar(require("./script.model"), exports);
20
+ __exportStar(require("./analytics.model"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA6BlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAO/C;;;;OAIG;YACW,kBAAkB;IA0B1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyC3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAuEjD,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,EAAE,iBAAiB,GAAG,MAAM;CAiB7G"}
1
+ {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA0DlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAO/C;;;;OAIG;YACW,kBAAkB;IA0B1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqE3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAuFjD,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,EAAE,iBAAiB,GAAG,MAAM;CAiB7G"}
@@ -12,6 +12,7 @@ const types_1 = require("../../../../globals/types");
12
12
  const firebase_1 = require("../../../../libs/firebase");
13
13
  const helpers_1 = require("../../../../utils/helpers");
14
14
  const logger_1 = require("../../../../utils/logger");
15
+ const errors_1 = require("../../../../utils/errors");
15
16
  const helpers_2 = require("../../helpers");
16
17
  const baseAiGenProvider_service_1 = require("../baseAiGenProvider.service");
17
18
  const fs_1 = require("fs");
@@ -31,6 +32,35 @@ const TRANSIENT_NETWORK_CODES = new Set([
31
32
  "UND_ERR_HEADERS_TIMEOUT",
32
33
  "UND_ERR_BODY_TIMEOUT",
33
34
  ]);
35
+ // Pick a Veo-compatible image MIME type. Veo accepts image/jpeg, image/png,
36
+ // image/webp. We prefer the upstream Content-Type header (Firebase Storage
37
+ // returns the type set at upload), then fall back to URL extension, then to
38
+ // image/jpeg as a reasonable last resort for phone-camera uploads.
39
+ const VEO_SUPPORTED_IMAGE_MIMES = new Set([
40
+ "image/jpeg",
41
+ "image/png",
42
+ "image/webp",
43
+ ]);
44
+ function pickImageMimeType(contentType, url) {
45
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase();
46
+ if (normalized && VEO_SUPPORTED_IMAGE_MIMES.has(normalized)) {
47
+ return normalized;
48
+ }
49
+ // Map common but non-canonical types
50
+ if (normalized === "image/jpg")
51
+ return "image/jpeg";
52
+ // Extension fallback. Strip query string first (Firebase URLs have ?alt=media).
53
+ const pathOnly = url.split("?")[0]?.toLowerCase() ?? "";
54
+ if (pathOnly.endsWith(".png"))
55
+ return "image/png";
56
+ if (pathOnly.endsWith(".webp"))
57
+ return "image/webp";
58
+ if (pathOnly.endsWith(".jpg") || pathOnly.endsWith(".jpeg"))
59
+ return "image/jpeg";
60
+ // Phone uploads default to JPEG far more often than PNG, so it's a safer
61
+ // last-resort than the previous hardcoded "image/png".
62
+ return "image/jpeg";
63
+ }
34
64
  function isTransientFetchError(err) {
35
65
  if (!err)
36
66
  return false;
@@ -93,9 +123,28 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
93
123
  };
94
124
  if (params.inputImageUrl) {
95
125
  const imgResp = await this.withTransientRetry("input-image fetch", () => fetch(params.inputImageUrl));
126
+ // BUG #3 fix: fetch() does NOT throw on 4xx/5xx — it returns a Response
127
+ // with the error body. Without this check we'd encode an HTML error page
128
+ // as if it were image bytes, and Veo would silently reject it.
129
+ if (!imgResp.ok) {
130
+ throw new errors_1.UserFacingError(`Input image could not be downloaded (HTTP ${imgResp.status}). The image URL may have expired or been deleted.`);
131
+ }
96
132
  const imgBuffer = Buffer.from(await imgResp.arrayBuffer());
133
+ // BUG #2 fix: a 0-byte body slips through fetch().ok if the upstream
134
+ // wrote an empty file. Veo treats empty bytes as content-filtered (no
135
+ // error, no video). Surface the real cause to the user.
136
+ if (imgBuffer.length === 0) {
137
+ throw new errors_1.UserFacingError("Input image is empty (0 bytes). Please re-upload the image.");
138
+ }
139
+ // BUG #1 fix: previously hardcoded "image/png" regardless of the actual
140
+ // upload type. Veo rejects JPEG bytes labeled as PNG, with no error
141
+ // detail. Prefer the Content-Type from the response (authoritative —
142
+ // Firebase Storage sets it from upload metadata), fall back to URL
143
+ // extension, then default to JPEG (modern phone-camera default).
144
+ const respContentType = imgResp.headers.get("content-type") ?? "";
145
+ const mimeType = pickImageMimeType(respContentType, params.inputImageUrl);
97
146
  request.image = {
98
- mimeType: "image/png", // or infer from URL (jpg/png)
147
+ mimeType,
99
148
  imageBytes: imgBuffer.toString("base64"),
100
149
  };
101
150
  }
@@ -118,6 +167,22 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
118
167
  }
119
168
  const videoUri = result.response?.generatedVideos?.[0]?.video?.uri;
120
169
  if (!videoUri) {
170
+ // Veo silently filters content via RAI without setting result.error.
171
+ // When raiMediaFilteredCount > 0, the request was blocked by safety
172
+ // filters. Surface the reasons so we can show the user what to fix,
173
+ // and prefix with `user_input_error:` so sceneMonitor demotes the log
174
+ // level (these are expected user-input failures, not bugs).
175
+ const filteredCount = result.response?.raiMediaFilteredCount ?? 0;
176
+ const filteredReasons = result.response?.raiMediaFilteredReasons ?? [];
177
+ if (filteredCount > 0 || filteredReasons.length > 0) {
178
+ const reasons = filteredReasons.length > 0
179
+ ? filteredReasons.join("; ")
180
+ : "Content blocked by Google safety filters";
181
+ return {
182
+ status: types_1.EVideoSceneStatus.FAILED,
183
+ errorMessage: `user_input_error: ${reasons}`,
184
+ };
185
+ }
121
186
  return {
122
187
  status: types_1.EVideoSceneStatus.FAILED,
123
188
  errorMessage: "No video URL or VideoBytes found in response",
@@ -0,0 +1,23 @@
1
+ import { EVideoJobStatus } from "../globals/types";
2
+ import { IVideoScene } from "../models/video.model";
3
+ export interface RecordJobOutcomeParams {
4
+ modelKey: string;
5
+ finalStatus: EVideoJobStatus;
6
+ scenes: IVideoScene[];
7
+ totalCreditsUsed: number;
8
+ /** ms between job creation and finalization */
9
+ jobLatencyMs?: number;
10
+ /** Defaults to now (UTC). Override only when backfilling. */
11
+ finalizedAt?: Date;
12
+ }
13
+ /**
14
+ * AnalyticsService
15
+ * ----------------
16
+ * Write-side helper for incrementing daily rollup counters. Reads happen
17
+ * from the admin app (server side) — kept here so a future BigQuery export
18
+ * has a single source of truth for shape.
19
+ */
20
+ export declare class AnalyticsService {
21
+ static recordJobOutcome(p: RecordJobOutcomeParams): Promise<void>;
22
+ }
23
+ //# sourceMappingURL=analytics.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.service.d.ts","sourceRoot":"","sources":["../../src/services/analytics.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAqB,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAUpD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,eAAe,CAAC;IAC7B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,IAAI,CAAC;CACpB;AAED;;;;;;GAMG;AACH,qBAAa,gBAAgB;WACd,gBAAgB,CAAC,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;CAqDxE"}
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AnalyticsService = void 0;
4
+ const firestore_1 = require("firebase-admin/firestore");
5
+ const firebase_1 = require("../libs/firebase");
6
+ const types_1 = require("../globals/types");
7
+ const DAILY_COL = "analytics_daily";
8
+ const DAILY_GLOBAL_COL = "analytics_daily_global";
9
+ function dateKey(d = new Date()) {
10
+ // UTC YYYY-MM-DD
11
+ return d.toISOString().slice(0, 10);
12
+ }
13
+ /**
14
+ * AnalyticsService
15
+ * ----------------
16
+ * Write-side helper for incrementing daily rollup counters. Reads happen
17
+ * from the admin app (server side) — kept here so a future BigQuery export
18
+ * has a single source of truth for shape.
19
+ */
20
+ class AnalyticsService {
21
+ static async recordJobOutcome(p) {
22
+ const date = dateKey(p.finalizedAt);
23
+ const completed = p.scenes.filter((s) => s.status === types_1.EVideoSceneStatus.COMPLETED).length;
24
+ const failed = p.scenes.filter((s) => s.status === types_1.EVideoSceneStatus.FAILED).length;
25
+ const timedOut = p.scenes.filter((s) => s.status === types_1.EVideoSceneStatus.TIMED_OUT).length;
26
+ const totalDuration = p.scenes
27
+ .filter((s) => s.status === types_1.EVideoSceneStatus.COMPLETED)
28
+ .reduce((sum, s) => sum + (s.duration ?? 0), 0);
29
+ const jobsCompleted = p.finalStatus === types_1.EVideoJobStatus.COMPLETED ? 1 : 0;
30
+ const jobsFailed = p.finalStatus === types_1.EVideoJobStatus.FAILED ? 1 : 0;
31
+ const jobsPartial = p.finalStatus === types_1.EVideoJobStatus.PARTIALLY_COMPLETED ? 1 : 0;
32
+ const inc = firestore_1.FieldValue.increment;
33
+ const latencyMs = p.jobLatencyMs ?? 0;
34
+ const perModelRef = firebase_1.firestore.collection(DAILY_COL).doc(`${date}_${p.modelKey}`);
35
+ const globalRef = firebase_1.firestore.collection(DAILY_GLOBAL_COL).doc(date);
36
+ const perModelPayload = {
37
+ date,
38
+ modelKey: p.modelKey,
39
+ jobsCompleted: inc(jobsCompleted),
40
+ jobsFailed: inc(jobsFailed),
41
+ jobsPartial: inc(jobsPartial),
42
+ scenesCompleted: inc(completed),
43
+ scenesFailed: inc(failed),
44
+ scenesTimedOut: inc(timedOut),
45
+ totalCreditsUsed: inc(p.totalCreditsUsed),
46
+ totalDurationSeconds: inc(totalDuration),
47
+ jobLatencyMsSum: inc(latencyMs),
48
+ jobLatencyMsCount: inc(latencyMs > 0 ? 1 : 0),
49
+ updatedAt: firestore_1.FieldValue.serverTimestamp(),
50
+ };
51
+ const globalPayload = {
52
+ date,
53
+ jobsCompleted: inc(jobsCompleted),
54
+ jobsFailed: inc(jobsFailed),
55
+ jobsPartial: inc(jobsPartial),
56
+ scenesCompleted: inc(completed),
57
+ scenesFailed: inc(failed),
58
+ totalCreditsUsed: inc(p.totalCreditsUsed),
59
+ jobLatencyMsSum: inc(latencyMs),
60
+ jobLatencyMsCount: inc(latencyMs > 0 ? 1 : 0),
61
+ updatedAt: firestore_1.FieldValue.serverTimestamp(),
62
+ };
63
+ const batch = firebase_1.firestore.batch();
64
+ batch.set(perModelRef, perModelPayload, { merge: true });
65
+ batch.set(globalRef, globalPayload, { merge: true });
66
+ await batch.commit();
67
+ }
68
+ }
69
+ exports.AnalyticsService = AnalyticsService;
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GCPService = void 0;
4
- const googleapis_1 = require("googleapis");
4
+ const sheets_1 = require("@googleapis/sheets");
5
5
  class GCPService {
6
6
  constructor() {
7
7
  if (!process.env.GOOGLE_CLIENT_EMAIL || !process.env.GOOGLE_PRIVATE_KEY) {
8
8
  throw new Error("Missing GCP credentials");
9
9
  }
10
- this.auth = new googleapis_1.google.auth.JWT({
10
+ this.auth = new sheets_1.auth.JWT({
11
11
  email: process.env.GOOGLE_CLIENT_EMAIL,
12
12
  key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n"),
13
13
  scopes: ["https://www.googleapis.com/auth/spreadsheets"],
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GSheetService = void 0;
4
- const googleapis_1 = require("googleapis");
4
+ const sheets_1 = require("@googleapis/sheets");
5
5
  const gcp_service_1 = require("./gcp.service");
6
6
  class GSheetService extends gcp_service_1.GCPService {
7
7
  constructor() {
8
8
  super();
9
- this.sheets = googleapis_1.google.sheets({
9
+ this.sheets = (0, sheets_1.sheets)({
10
10
  version: "v4",
11
11
  auth: this.auth,
12
12
  });
@@ -4,5 +4,6 @@ export * from "./aiGen";
4
4
  export * from "./bullmq.service";
5
5
  export * from "./gcp";
6
6
  export * from "./credit.service";
7
+ export * from "./analytics.service";
7
8
  export * from "./tts";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,OAAO,CAAC;AACtB,cAAc,kBAAkB,CAAC;AACjC,cAAc,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,OAAO,CAAC;AACtB,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,OAAO,CAAC"}
@@ -20,4 +20,5 @@ __exportStar(require("./aiGen"), exports);
20
20
  __exportStar(require("./bullmq.service"), exports);
21
21
  __exportStar(require("./gcp"), exports);
22
22
  __exportStar(require("./credit.service"), exports);
23
+ __exportStar(require("./analytics.service"), exports);
23
24
  __exportStar(require("./tts"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidspotai-shared",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "exports": {
@@ -16,12 +16,12 @@
16
16
  "dependencies": {
17
17
  "@google-cloud/storage": "*",
18
18
  "@google/genai": "^1.22.0",
19
+ "@googleapis/sheets": "^13.0.1",
19
20
  "@runwayml/sdk": "^2.11.0",
20
21
  "axios": "^1.12.2",
21
22
  "bullmq": "^5.61.0",
22
23
  "firebase-admin": "^13.5.0",
23
24
  "fs-extra": "^11.3.2",
24
- "googleapis": "^170.0.0",
25
25
  "ioredis": "^5.8.0",
26
26
  "jsonwebtoken": "^9.0.2",
27
27
  "logform": "^2.7.0",