Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ migrate_working_dir/
.dart_tool/
.packages
build/
test/live_tts_credentials.local.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
package io.flutter.plugins;

import io.flutter.plugin.common.PluginRegistry;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import io.flutter.Log;

import io.flutter.embedding.engine.FlutterEngine;

/**
* Generated file. Do not edit.
* This file is generated by the Flutter tool based on the
* plugins that support the Android platform.
*/
@Keep
public final class GeneratedPluginRegistrant {
public static void registerWith(PluginRegistry registry) {
if (alreadyRegisteredWith(registry)) {
return;
}
}

private static boolean alreadyRegisteredWith(PluginRegistry registry) {
final String key = GeneratedPluginRegistrant.class.getCanonicalName();
if (registry.hasPlugin(key)) {
return true;
}
registry.registrarFor(key);
return false;
}
private static final String TAG = "GeneratedPluginRegistrant";
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
}
}
4 changes: 2 additions & 2 deletions android/local.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
sdk.dir=/Users/marko/Library/Android/sdk
flutter.sdk=/Users/marko/.flutter-sdk
sdk.dir=/opt/android-sdk
flutter.sdk=/home/solstice/Applications/flutter
32 changes: 32 additions & 0 deletions ios/Flutter/ephemeral/flutter_lldb_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#
# Generated file, do not edit.
#

import lldb

def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()

# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'

error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return

def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")
5 changes: 5 additions & 0 deletions ios/Flutter/ephemeral/flutter_lldbinit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#
# Generated file, do not edit.
#

command script import --relative-to-command-file flutter_lldb_helper.py
2 changes: 1 addition & 1 deletion ios/Runner/GeneratedPluginRegistrant.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
NS_ASSUME_NONNULL_BEGIN

@interface GeneratedPluginRegistrant : NSObject
+ (void)registerWithRegistry:(NSObject <FlutterPluginRegistry> *)registry;
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
@end

NS_ASSUME_NONNULL_END
Expand Down
2 changes: 1 addition & 1 deletion ios/Runner/GeneratedPluginRegistrant.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@implementation GeneratedPluginRegistrant

+ (void)registerWithRegistry:(NSObject <FlutterPluginRegistry> *)registry {
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
}

@end
71 changes: 48 additions & 23 deletions lib/src/amazon/audio/audio_client.dart
Original file line number Diff line number Diff line change
@@ -1,33 +1,58 @@
import 'package:cloud_text_to_speech/src/common/http/base_client.dart';
import 'package:http/http.dart' as http;
import 'package:sigv4/sigv4.dart';
// ignore_for_file: implementation_imports

import '../common/config.dart';
import 'dart:typed_data';

class AudioClientAmazon extends BaseClient {
AudioClientAmazon({required http.Client client}) : super(client: client);
import 'package:aws_client/src/shared/shared.dart' as aws;
import 'package:cloud_text_to_speech/src/amazon/common/config.dart';

@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
request.headers['Content-Type'] = "application/json";

final sigv4Client = Sigv4Client(
keyId: ConfigAmazon.keyId,
accessKey: ConfigAmazon.accessKey,
class AudioClientAmazon {
Future<Uint8List> synthesizeSpeech({
required String audioFormat,
required String text,
required String voiceCode,
required List<String> supportedEngines,
}) async {
final protocol = aws.RestJsonProtocol(
service: const aws.ServiceMetadata(endpointPrefix: 'polly'),
region: ConfigAmazon.region,
serviceName: 'polly',
credentials: ConfigAmazon.credentials,
);

final headers = sigv4Client.signedHeaders(
request.url.toString(),
method: request.method,
query: request.url.queryParameters,
headers: request.headers,
body: request is http.Request ? request.body : null,
);
try {
final response = await protocol.sendRaw(
method: 'POST',
requestUri: '/v1/speech',
exceptionFnMap: const <String, aws.AwsExceptionFn>{},
payload: <String, dynamic>{
'OutputFormat': audioFormat,
'Text': text,
'TextType': 'ssml',
'VoiceId': voiceCode,
if (_resolveEngine(supportedEngines) case final engine?)
'Engine': engine,
},
);

return await response.stream.toBytes();
} finally {
protocol.close();
}
}

String? _resolveEngine(List<String> supportedEngines) {
const preferredOrder = <String>[
'standard',
'neural',
'long-form',
'generative',
];

request.headers.addAll(headers);
for (final engineName in preferredOrder) {
if (supportedEngines.contains(engineName)) {
return engineName;
}
}

return client.send(request);
return supportedEngines.isEmpty ? null : supportedEngines.first;
}
}
38 changes: 11 additions & 27 deletions lib/src/amazon/audio/audio_handler.dart
Original file line number Diff line number Diff line change
@@ -1,43 +1,27 @@
import 'dart:convert';

import 'package:cloud_text_to_speech/src/amazon/audio/audio_client.dart';
import 'package:cloud_text_to_speech/src/amazon/audio/audio_request_param.dart';
import 'package:cloud_text_to_speech/src/amazon/audio/audio_response_mapper.dart';
import 'package:cloud_text_to_speech/src/amazon/audio/audio_responses.dart';
import 'package:cloud_text_to_speech/src/amazon/common/constants.dart';
import 'package:cloud_text_to_speech/src/amazon/common/aws_exception_mapper.dart';
import 'package:cloud_text_to_speech/src/amazon/ssml/ssml.dart';
import 'package:http/http.dart' as http;

class AudioHandlerAmazon {
Future<AudioSuccessAmazon> getAudio(AudioRequestParamsAmazon params) async {
final client = http.Client();
final audioClient = AudioClientAmazon(client: client);
final mapper = AudioResponseMapperAmazon();
final audioClient = AudioClientAmazon();

try {
final ssml =
SsmlAmazon(text: params.text, rate: params.rate, pitch: params.pitch);

final Map<String, dynamic> body = {
'OutputFormat': params.audioFormat,
'Text': ssml.sanitizedSsml,
'TextType': 'ssml',
'VoiceId': params.voice.code,
// 'Engine': params.voice.engines
};

final String bodyJson = jsonEncode(body);
final response = await audioClient.synthesizeSpeech(
audioFormat: params.audioFormat,
text: ssml.sanitizedSsml,
voiceCode: params.voice.code,
supportedEngines: params.voice.engines,
);

final response = await audioClient.post(Uri.parse(EndpointsAmazon.tts),
body: bodyJson);
final audioResponse = mapper.map(response);
if (audioResponse is AudioSuccessAmazon) {
return audioResponse;
} else {
throw audioResponse;
}
} catch (e) {
rethrow;
return AudioSuccessAmazon(audio: response);
} catch (error) {
throw mapAudioExceptionAmazon(error);
}
}
}
2 changes: 1 addition & 1 deletion lib/src/amazon/audio/audio_responses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class AudioFailedUnsupportedAmazon extends AudioResponseAmazon {
"Unsupported Media Type It's possible that the wrong Content-Type was provided. Content-Type should be set to application/ssml+xml.");
}

class AudioFailedTooManyRequestAmazon extends BaseResponse {
class AudioFailedTooManyRequestAmazon extends AudioResponseAmazon {
AudioFailedTooManyRequestAmazon()
: super(
code: 429,
Expand Down
82 changes: 82 additions & 0 deletions lib/src/amazon/common/aws_exception_mapper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// ignore_for_file: implementation_imports

import 'package:aws_client/src/shared/shared.dart' as aws;
import 'package:cloud_text_to_speech/src/amazon/audio/audio_responses.dart';
import 'package:cloud_text_to_speech/src/amazon/voices/voices_responses.dart';

VoicesResponseAmazon mapVoicesExceptionAmazon(Object error) {
if (error is aws.GenericAwsException) {
final code = int.tryParse(error.code);
if (code == 400) {
return VoicesFailedBadRequestAmazon(reasonPhrase: error.message);
}
if (code == 401 || code == 403 || _looksUnauthorized(error)) {
return VoicesFailedUnauthorizedAmazon();
}
if (code == 429 || _looksRateLimited(error)) {
return VoicesFailedTooManyRequestsAmazon();
}
if (code == 500 || code == 502) {
return VoicesFailedBadGateWayAmazon();
}
}

return VoicesFailedUnknownErrorAmazon(
code: _statusCodeFrom(error) ?? 500,
reason: error.toString(),
);
}

AudioResponseAmazon mapAudioExceptionAmazon(Object error) {
if (error is aws.GenericAwsException) {
final code = int.tryParse(error.code);
final type = (error.type ?? '').toLowerCase();
if (type.contains('marksnotsupported') ||
type.contains('ssmlmarksnotsupportedfortexttype') ||
code == 415) {
return AudioFailedUnsupportedAmazon();
}
if (code == 400) {
return AudioFailedBadRequestAmazon(reasonPhrase: error.message);
}
if (code == 401 || code == 403 || _looksUnauthorized(error)) {
return AudioFailedUnauthorizedAmazon();
}
if (code == 429 || _looksRateLimited(error)) {
return AudioFailedTooManyRequestAmazon();
}
if (code == 500 || code == 502) {
return AudioFailedBadGatewayAmazon();
}
}
if (error is FormatException || error is ArgumentError) {
return AudioFailedUnsupportedAmazon();
}

return AudioFailedUnknownErrorAmazon(
code: _statusCodeFrom(error) ?? 500,
reason: error.toString(),
);
}

bool _looksUnauthorized(Object error) {
final message = error.toString().toLowerCase();
return message.contains('accessdenied') ||
message.contains('access denied') ||
message.contains('invalidclienttokenid') ||
message.contains('missing authentication token') ||
message.contains('signaturedoesnotmatch') ||
message.contains('unrecognizedclient') ||
message.contains('expiredtoken') ||
message.contains('403');
}

bool _looksRateLimited(Object error) {
final message = error.toString().toLowerCase();
return message.contains('throttl') || message.contains('429');
}

int? _statusCodeFrom(Object error) {
final match = RegExp(r'\b([1-5]\d\d)\b').firstMatch(error.toString());
return match == null ? null : int.tryParse(match.group(1)!);
}
7 changes: 7 additions & 0 deletions lib/src/amazon/common/config.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:aws_client/polly_2016_06_10.dart' show AwsClientCredentials;

///Holds all configurations
class ConfigAmazon {
static late final String _keyId;
Expand Down Expand Up @@ -35,4 +37,9 @@ class ConfigAmazon {
}
return ConfigAmazon._region;
}

static AwsClientCredentials get credentials => AwsClientCredentials(
accessKey: keyId,
secretKey: accessKey,
);
}
Loading