@contentgrowth/llm-service 1.0.2 → 1.0.3

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/dist/index.cjs CHANGED
@@ -1381,6 +1381,58 @@ function sanitizeError(error, context = "general") {
1381
1381
 
1382
1382
  // src/transcription-service.js
1383
1383
  var import_openai2 = __toESM(require("openai"), 1);
1384
+
1385
+ // src/utils/google-auth.js
1386
+ var jose = __toESM(require("jose"), 1);
1387
+ var GoogleAuth = class {
1388
+ /**
1389
+ * @param {Object} credentials - Parsed Service Account JSON
1390
+ */
1391
+ constructor(credentials) {
1392
+ if (!credentials || !credentials.private_key || !credentials.client_email) {
1393
+ throw new Error("Invalid Google credentials. Missing private_key or client_email.");
1394
+ }
1395
+ this.credentials = credentials;
1396
+ this.tokenUrl = "https://oauth2.googleapis.com/token";
1397
+ this.scopes = ["https://www.googleapis.com/auth/cloud-platform"];
1398
+ }
1399
+ /**
1400
+ * Generate a signed JWT for the token exchange
1401
+ */
1402
+ async createSignedJWT() {
1403
+ const alg = "RS256";
1404
+ const privateKey = await jose.importPKCS8(this.credentials.private_key, alg);
1405
+ const jwt = await new jose.SignJWT({
1406
+ scope: this.scopes.join(" ")
1407
+ }).setProtectedHeader({ alg }).setIssuedAt().setIssuer(this.credentials.client_email).setAudience(this.tokenUrl).setExpirationTime("1h").sign(privateKey);
1408
+ return jwt;
1409
+ }
1410
+ /**
1411
+ * Get an access token from Google OAuth2
1412
+ * @returns {Promise<string>} Access Token
1413
+ */
1414
+ async getAccessToken() {
1415
+ const jwt = await this.createSignedJWT();
1416
+ const params = new URLSearchParams();
1417
+ params.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
1418
+ params.append("assertion", jwt);
1419
+ const response = await fetch(this.tokenUrl, {
1420
+ method: "POST",
1421
+ headers: {
1422
+ "Content-Type": "application/x-www-form-urlencoded"
1423
+ },
1424
+ body: params
1425
+ });
1426
+ if (!response.ok) {
1427
+ const errorText = await response.text();
1428
+ throw new Error(`Failed to get Google Access Token: ${response.status} ${errorText}`);
1429
+ }
1430
+ const data = await response.json();
1431
+ return data.access_token;
1432
+ }
1433
+ };
1434
+
1435
+ // src/transcription-service.js
1384
1436
  var TranscriptionService = class {
1385
1437
  /**
1386
1438
  * @param {Object} config
@@ -1396,6 +1448,9 @@ var TranscriptionService = class {
1396
1448
  this.model = config.model || "whisper-1";
1397
1449
  if (this.provider === "openai") {
1398
1450
  this.client = new import_openai2.default({ apiKey: this.apiKey });
1451
+ } else if (this.provider === "google") {
1452
+ this.googleAuth = new GoogleAuth(config.googleCredentials);
1453
+ this.projectId = config.googleCredentials.project_id;
1399
1454
  }
1400
1455
  }
1401
1456
  /**
@@ -1413,6 +1468,8 @@ var TranscriptionService = class {
1413
1468
  return this._transcribeOpenAI(audioFile, model, language);
1414
1469
  } else if (this.provider === "generic") {
1415
1470
  return this._transcribeGeneric(audioFile, model, language);
1471
+ } else if (this.provider === "google") {
1472
+ return this._transcribeGoogle(audioFile, model, language);
1416
1473
  } else {
1417
1474
  throw new TranscriptionServiceException(
1418
1475
  `Unsupported transcription provider: ${this.provider}`,
@@ -1471,6 +1528,60 @@ var TranscriptionService = class {
1471
1528
  );
1472
1529
  }
1473
1530
  }
1531
+ async _transcribeGoogle(audioFile, model, language) {
1532
+ try {
1533
+ const accessToken = await this.googleAuth.getAccessToken();
1534
+ const arrayBuffer = await audioFile.arrayBuffer();
1535
+ const audioBytes = Buffer.from(arrayBuffer).toString("base64");
1536
+ let location = "global";
1537
+ if (model === "chirp_3") {
1538
+ location = "us";
1539
+ } else if (model && model.includes("chirp")) {
1540
+ location = "us-central1";
1541
+ }
1542
+ const recognizer = `projects/${this.projectId}/locations/${location}/recognizers/_`;
1543
+ const apiEndpoint = location === "global" ? "https://speech.googleapis.com" : `https://${location}-speech.googleapis.com`;
1544
+ const url = `${apiEndpoint}/v2/${recognizer}:recognize`;
1545
+ const languageCodes = language ? [language] : ["en-US"];
1546
+ const body = {
1547
+ config: {
1548
+ autoDecodingConfig: {},
1549
+ languageCodes,
1550
+ // Sanitize model: Strict allowlist for Google v2 models
1551
+ model: model || "chirp_3"
1552
+ },
1553
+ content: audioBytes
1554
+ };
1555
+ const response = await fetch(url, {
1556
+ method: "POST",
1557
+ headers: {
1558
+ "Authorization": `Bearer ${accessToken}`,
1559
+ "Content-Type": "application/json",
1560
+ "X-Goog-User-Project": this.projectId
1561
+ },
1562
+ body: JSON.stringify(body)
1563
+ });
1564
+ if (!response.ok) {
1565
+ const errorText = await response.text();
1566
+ throw new Error(`Google Speech API Error ${response.status}: ${errorText}`);
1567
+ }
1568
+ const data = await response.json();
1569
+ let transcript = "";
1570
+ if (data.results) {
1571
+ transcript = data.results.map((r) => {
1572
+ var _a, _b;
1573
+ return ((_b = (_a = r.alternatives) == null ? void 0 : _a[0]) == null ? void 0 : _b.transcript) || "";
1574
+ }).join(" ");
1575
+ }
1576
+ return { text: transcript.trim() };
1577
+ } catch (error) {
1578
+ console.error("[TranscriptionService] Google transcription failed:", error);
1579
+ throw new TranscriptionServiceException(
1580
+ `Google Transcription failed: ${error.message}`,
1581
+ 500
1582
+ );
1583
+ }
1584
+ }
1474
1585
  };
1475
1586
  var TranscriptionServiceException = class extends Error {
1476
1587
  constructor(message, statusCode = 500) {
@@ -1485,9 +1596,14 @@ function createSpeechHandler(app, getConfig) {
1485
1596
  app.post("/transcribe", async (c) => {
1486
1597
  try {
1487
1598
  const config = await getConfig(c);
1488
- if (!config || !config.apiKey) {
1599
+ if (!config) {
1489
1600
  return c.json({ error: "Transcription service not configured" }, 503);
1490
1601
  }
1602
+ if (config.provider === "google" && !config.googleCredentials) {
1603
+ return c.json({ error: "Google credentials not configured" }, 503);
1604
+ } else if (config.provider !== "google" && !config.apiKey) {
1605
+ return c.json({ error: "API key not configured" }, 503);
1606
+ }
1491
1607
  const formData = await c.req.formData();
1492
1608
  const audioFile = formData.get("audio");
1493
1609
  if (!audioFile) {