diff --git a/src/backend/src/modules/puterai/AWSPollyService.js b/src/backend/src/modules/puterai/AWSPollyService.js
index a02a674c..3dc2aed2 100644
--- a/src/backend/src/modules/puterai/AWSPollyService.js
+++ b/src/backend/src/modules/puterai/AWSPollyService.js
@@ -1,18 +1,18 @@
/*
* Copyright (C) 2024-present Puter Technologies Inc.
- *
+ *
* This file is part of Puter.
- *
+ *
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
- *
+ *
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
@@ -22,11 +22,12 @@ const { PollyClient, SynthesizeSpeechCommand, DescribeVoicesCommand } = require(
const BaseService = require("../../services/BaseService");
const { TypedValue } = require("../../services/drivers/meta/Runtime");
const APIError = require("../../api/APIError");
+const { Context } = require("../../util/context");
// Polly price calculation per engine
const ENGINE_PRICING = {
'standard': 400, // $4.00 per 1M characters
- 'neural': 1600, // $16.00 per 1M characters
+ 'neural': 1600, // $16.00 per 1M characters
'long-form': 10000, // $100.00 per 1M characters
'generative': 3000, // $30.00 per 1M characters
};
@@ -43,10 +44,12 @@ const VALID_ENGINES = ['standard', 'neural', 'long-form', 'generative'];
* @extends BaseService
*/
class AWSPollyService extends BaseService {
+ /** @type {import('../../services/abuse-prevention/MeteringService/MeteringService').MeteringAndBillingService} */
+ meteringAndBillingService;
+
static MODULES = {
kv: globalThis.kv,
- }
-
+ };
/**
* Initializes the service by creating an empty clients object.
@@ -54,15 +57,19 @@ class AWSPollyService extends BaseService {
* the internal state needed for AWS Polly client management.
* @returns {Promise}
*/
- async _construct () {
+ async _construct() {
this.clients_ = {};
}
+ async _init() {
+ this.meteringAndBillingService = this.services.get('meteringService').meteringAndBillingService;
+ }
+
static IMPLEMENTS = {
['driver-capabilities']: {
- supports_test_mode (iface, method_name) {
+ supports_test_mode(iface, method_name) {
return iface === 'puter-tts' && method_name === 'synthesize';
- }
+ },
},
['puter-tts']: {
/**
@@ -73,16 +80,14 @@ class AWSPollyService extends BaseService {
* @property {Object} synthesize - Converts text to speech using specified voice/language
* @property {Function} supports_test_mode - Indicates test mode support for methods
*/
- async list_voices ({ engine } = {}) {
+ async list_voices({ engine } = {}) {
const polly_voices = await this.describe_voices();
let voices = polly_voices.Voices;
- if (engine) {
- if (VALID_ENGINES.includes(engine)) {
- voices = voices.filter(
- (voice) => voice.SupportedEngines?.includes(engine)
- );
+ if ( engine ) {
+ if ( VALID_ENGINES.includes(engine) ) {
+ voices = voices.filter((voice) => voice.SupportedEngines?.includes(engine));
} else {
throw APIError.create('invalid_engine', null, { engine, valid_engines: VALID_ENGINES });
}
@@ -96,25 +101,25 @@ class AWSPollyService extends BaseService {
code: voice.LanguageCode,
},
supported_engines: voice.SupportedEngines || ['standard'],
- }))
+ }));
return voices;
},
- async list_engines () {
+ async list_engines() {
return VALID_ENGINES.map(engine => ({
id: engine,
name: engine.charAt(0).toUpperCase() + engine.slice(1),
pricing_per_million_chars: ENGINE_PRICING[engine] / 100, // Convert microcents to dollars
}));
},
- async synthesize ({
+ async synthesize({
text, voice,
ssml, language,
engine = 'standard',
test_mode,
}) {
if ( test_mode ) {
- const url = 'https://puter-sample-data.puter.site/tts_example.mp3'
+ const url = 'https://puter-sample-data.puter.site/tts_example.mp3';
return new TypedValue({
$: 'string:url:web',
content_type: 'audio',
@@ -122,13 +127,13 @@ class AWSPollyService extends BaseService {
}
// Validate engine
- if (!VALID_ENGINES.includes(engine)) {
+ if ( !VALID_ENGINES.includes(engine) ) {
throw APIError.create('invalid_engine', null, { engine, valid_engines: VALID_ENGINES });
}
-
+
const microcents_per_character = ENGINE_PRICING[engine];
const exact_cost = microcents_per_character * text.length;
-
+
const svc_cost = this.services.get('cost');
const usageAllowed = await svc_cost.get_funding_allowed({
minimum: exact_cost,
@@ -139,7 +144,7 @@ class AWSPollyService extends BaseService {
}
// We can charge immediately
await svc_cost.record_cost({ cost: exact_cost });
-
+
const polly_speech = await this.synthesize_speech(text, {
format: 'mp3',
voice_id: voice,
@@ -147,31 +152,38 @@ class AWSPollyService extends BaseService {
language,
engine,
});
-
+
+ // Metering integration for TTS usage
+ const actor = Context.get('actor');
+ // AWS Polly TTS metering: track character count, voice, engine, cost, audio duration if available
+ const trackedUsage = {
+ character: text.length,
+ };
+ this.meteringAndBillingService.utilRecordUsageObject(trackedUsage, actor, `aws-polly:${engine}`);
+
const speech = new TypedValue({
$: 'stream',
content_type: 'audio/mpeg',
}, polly_speech.AudioStream);
-
- return speech;
- }
- }
- }
+ return speech;
+ },
+ },
+ };
/**
* Creates AWS credentials object for authentication
* @private
* @returns {Object} Object containing AWS access key ID and secret access key
*/
- _create_aws_credentials () {
+ _create_aws_credentials() {
return {
accessKeyId: this.config.aws.access_key,
secretAccessKey: this.config.aws.secret_key,
};
}
- _get_client (region) {
+ _get_client(region) {
if ( ! region ) {
region = this.config.aws?.region ?? this.global_config.aws?.region
?? 'us-west-2';
@@ -186,14 +198,13 @@ class AWSPollyService extends BaseService {
return this.clients_[region];
}
-
/**
* Describes available AWS Polly voices and caches the results
* @returns {Promise