У цій статті ми опишемо, як було зроблено додаток “Image to Speech”. Деякі підказки коду та посилання на документацію . Додаток читає вголос і зберігає в звукову доріжку будь-який текст на зображенні і заснований на технології Google Cloud ML. Додаток побудований з використанням фреймворку Flutter з використанням мови Dart і доступний на Google PlayMarket і Apple AppStore.
Ви можете перевірити вихідний код програми в загальнодоступному repository GitHub.
Пролог.
Перш ніж ми почнемо, трохи розберемося з історією: створюючи додаток, ми почали з розпізнавання зображень на пристрої до тексту, але пізніше ми перейшли на хмарний API через бібліотеки на пристрої для Flutter, який на той момент підтримував тільки англійську мову. Сподіваюся, с тих пір, це було покращено.
Епізод 1: Візьміть зображення і розпізнайте його в тексті.
Хіба не було б здорово мати додаток, який може розпізнавати текст з картинки або фотографії, і навіть читати цей текст і зберігати звукову доріжку окремо? Це буде дуже корисно для людей з вадами зору, для іноземців, які не знають правильної вимови, або для любителів аудіокниг.
Отже, створіть новий проект Flutter, а потім підключіть Firebase для iOS і Android, як описано в цьому document.
У цьому додатку ми будемо використовувати Google cloud OCR і Google cloud TTS, звичайно, вже є готові залежності, такі як firebase_ml_vision або mlkit, які зроблять все за вас і будуть працювати без інтернету, але їх функціональність буде урізана, вони будуть розпізнавати тільки англійську мову. Documentation щодо хмарного бачення можна знайти тут.
Тепер в Google Cloud Platform потрібно додати до проекту:
- Cloud Functions API
- Cloud Vision API
- Google Cloud APIs
Додайте залежності camera, image_picker і http, за допомогою яких ми будемо фотографувати або додавати вже зроблені фотографії з галереї і відправляти цю фотографію на сервер.
Отже, виберіть фотографію з галереї:
Future pickGallery() async { | |
var tempStore = await ImagePicker.pickImage(source: ImageSource.gallery); | |
if (tempStore != null) { | |
recognizePhoto(tempStore.path); | |
} | |
} |
конвертувати фотографію в base64:
recognizePhoto(filePath) async { | |
try { | |
File image = File(filePath); | |
List<int> imageBytes = image.readAsBytesSync(); | |
String base64Image = base64Encode(imageBytes); | |
TextRecognize text = await rep.convert(base64Image); | |
getVoice(text); | |
} catch (e) { | |
print(e); | |
} | |
} |
Зіставлення даних для моделювання з відповіді:
class TextRecognize { | |
List<Response> responses; | |
TextRecognize({this.responses}); | |
factory TextRecognize.fromJson(Map<String, dynamic> parsedJson) { | |
var list = parsedJson["responses"] as List; | |
List<Response> response = list.map((e) => Response.fromJson(e)).toList(); | |
return TextRecognize(responses: response); | |
} | |
} | |
class Response { | |
List<TextAnnotations> textAnnotations; | |
Response({this.textAnnotations}); | |
factory Response.fromJson(Map<String, dynamic> parsedJson) { | |
var list = parsedJson["textAnnotations"] as List; | |
List<TextAnnotations> textAnnotation = | |
list.map((e) => TextAnnotations.fromJson(e)).toList(); | |
return Response(textAnnotations: textAnnotation); | |
} | |
} | |
class TextAnnotations { | |
String locale; | |
String description; | |
BoundingPoly boundingPoly; | |
TextAnnotations({this.locale, this.description, this.boundingPoly}); | |
factory TextAnnotations.fromJson(Map<String, dynamic> parsedJson) { | |
return TextAnnotations( | |
locale: parsedJson["locale"], | |
description: parsedJson[“description"], boundingPoly:BoundingPoly.fromJson(parsedJson["boundingPoly"])); | |
} | |
} | |
class BoundingPoly { | |
List<Vertices> vertices; | |
BoundingPoly({this.vertices}); | |
factory BoundingPoly.fromJson(Map<String, dynamic> parsedJson) { | |
var list = parsedJson["vertices"] as List; | |
List<Vertices> vertice = list.map((i) => Vertices.fromJson(i)).toList(); | |
return BoundingPoly(vertices: vertice); | |
} | |
} | |
class Vertices { | |
int x; | |
int y; | |
Vertices({this.x, this.y}); | |
factory Vertices.fromJson(Map<String, dynamic> parseJson) { | |
return Vertices(x: parseJson["x"], y: parseJson["y"]); | |
} | |
} |
надіслати json з base64Image в google vision
static const _apiKey = "Your Api Key"; | |
String url = "https://vision.googleapis.com/v1/images:annotate?key=$_apiKey"; | |
Future<TextRecognize> convert(base64Image) async { | |
var body = json.encode({ | |
"requests": [ | |
{ | |
"image": {"content": base64Image}, | |
"features": [ | |
{"type": "TEXT_DETECTION"} | |
] | |
} | |
] | |
}); | |
final response = await http.post(url, body: body); | |
var jsonResponse = json.decode(response.body); | |
return TextRecognize.fromJson(jsonResponse); | |
} |
Отримати текст з моделі:
getVoice(TextRecognize text) async { | |
for (var response in text.responses) { | |
for (var textAnnotation in response.textAnnotations) { | |
print("${textAnnotation.description}"); | |
if (textAnnotation.locale != null) { | |
var locale = textAnnotation.locale; | |
Voice voice = await rep.getVoice(locale); | |
writeAudio(voice); | |
} | |
} | |
} | |
} |
Відповідь з хмари повертає нам текст і розпізнану locale.
Епізод 2: перетворення тексту в мову і збереження треку в локальний файл.
Коли ми отримали текстові дані та locale від ml-vision, ми встановили ці дані в Google Text To Speech API.
Для цього ми створюємо HTTP запит за допомогою методу text.synthesizeioEncoding:
Future<dynamic> synthesizeText( | |
String text, String name, String languageCode) async { | |
try { | |
final uri = Uri.https(‘texttospeech.googleapis.com’, '/v1beta1/text:synthesize'); | |
final Map json = { | |
'input': {'text': text}, | |
'voice': {'name': name, 'languageCode': languageCode}, | |
'audioConfig': {'audioEncoding': 'MP3', "speakingRate": 1} | |
}; | |
final jsonResponse = await _postJson(uri, json); | |
if (jsonResponse == null) return null; | |
final String audioContent = await jsonResponse['audioContent']; | |
return audioContent; | |
} on Exception catch (e) { | |
print("$e"); | |
return null; | |
} | |
} |
де:
- 'input' - це Тип SynthesisInput з полем "text" - це необроблений текст, що підлягає синтезу;
- ми встановлюємо ‘voice' - це VoiceSelectionParams type
- “name” - тип голосу
- та languageCode - мова;
- ‘audioConfig’ - це опис звукових даних, що підлягають синтезу, AudioConfig.
Ми створюємо запит за допомогою методу '_postJson' :
Future<Map<String, dynamic>> _postJson(Uri uri, Map jsonMap) async { | |
try { | |
final httpRequest = await _httpClient.postUrl(uri); | |
final jsonData = utf8.encode(json.encode(jsonMap)); | |
final jsonResponse = | |
await _processRequestIntoJsonResponse(httpRequest, jsonData); | |
return jsonResponse; | |
} on Exception catch (e) { | |
print("$e"); | |
return null; | |
} | |
} | |
Future<Map<String, dynamic>> _processRequestIntoJsonResponse( | |
HttpClientRequest httpRequest, List<int> data) async { | |
try { | |
httpRequest.headers.add('X-Goog-Api-Key', ‘Google API Key’); | |
httpRequest.headers.add(HttpHeaders.CONTENT_TYPE, 'application/json'); | |
if (data != null) { | |
httpRequest.add(data); | |
} | |
final httpResponse = await httpRequest.close(); | |
if (httpResponse.statusCode != HttpStatus.OK) { | |
print("httpResponse.statusCode " + httpResponse.statusCode.toString()); | |
throw Exception('Bad Response'); | |
} | |
final responseBody = await httpResponse.transform(utf8.decoder).join(); | |
print("responseBody " + responseBody.toString()); | |
return json.decode(responseBody); | |
} on Exception catch (e) { | |
print("$e"); | |
return null; | |
} | |
} |
Створення голосової моделі:
class Voice { | |
final String name; | |
final String gender; | |
final List<String> languageCodes; | |
Voice(this.name, this.gender, this.languageCodes); | |
static List<Voice> mapJSONStringToList(List<dynamic> jsonList) { | |
return jsonList.map((v) { | |
return Voice( | |
v['name'], v['ssmlGender'], List<String>.from(v['languageCodes'])); | |
}).toList(); | |
} | |
} |
Зіставлення даних для моделювання з відповіді:
Future<List<Voice>> getVoices() async { | |
try { | |
final uri = Uri.https(‘texttospeech.googleapis.com’, '/v1beta1/voices'); | |
final jsonResponse = await _getJson(uri); | |
if (jsonResponse == null) { | |
return null; | |
} | |
final List<dynamic> voicesJSON = jsonResponse['voices'].toList(); | |
if (voicesJSON == null) { | |
return null; | |
} | |
final voices = Voice.mapJSONStringToList(voicesJSON); | |
return voices; | |
} on Exception catch (e) { | |
return null; | |
} | |
} | |
Future<Map<String, dynamic>> _getJson(Uri uri) async { | |
try { | |
final httpRequest = await _httpClient.getUrl(uri); | |
final jsonResponse = | |
await _processRequestIntoJsonResponse(httpRequest, null); | |
return jsonResponse; | |
} on Exception catch (e) { | |
return null; | |
} | |
} |
Потім ми створюємо аудіофайл в каталозі додатків називаючи його за часом створення:
String _getTimestamp() => DateTime.now().millisecondsSinceEpoch.toString(); | |
writeAudioFile(String text) async { | |
Voice voice = getVoices(); | |
final String audioContent = await TextToSpeechAPI() | |
.synthesizeText(text, voice.name, voice.languageCodes.first); | |
bytes = Base64Decoder().convert(audioContent, 0, audioContent.length); | |
final dir = await getTemporaryDirectory(); | |
final audioFile = File('${dir.path}/${_getTimestamp()}.mp3'); | |
await audioFile.writeAsBytes(bytes); | |
return audioFile.path; | |
} |
І ми можемо відтворити створений файл за допомогою flutter audioplayer plugin:
playAudio(String audioText){ | |
AudioPlayer audioPlugin = AudioPlayer(); | |
String audioPath = writeAudioFile(); | |
audioPlugin.play(audio, isLocal: true); | |
} |
Епілог.
Спасибі, що дочитали цю статтю до кінця, ми сподіваємося, що вам сподобається, і тепер ви знаєте кунг-фу.
Будь ласка, перевірте опублікований додаток:
У Google PlayMarket та Apple AppStore.