У цій статті ми опишемо, як було зроблено додаток “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);
   }
 }
view raw its1.dart hosted with ❤ by GitHub

конвертувати фотографію в 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);
   }
 }
view raw its2.dart hosted with ❤ by GitHub

Зіставлення даних для моделювання з відповіді:

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"]);
  }
}
view raw its3.dart hosted with ❤ by GitHub

надіслати 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);
 }
view raw its4.dart hosted with ❤ by GitHub

Отримати текст з моделі:

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);
       }
     }
   }
 }
view raw its5.dart hosted with ❤ by GitHub

Відповідь з хмари повертає нам текст і розпізнану  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;
    }
  }
view raw its6.dart hosted with ❤ by GitHub

де: 

  • '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;
    }
  }
view raw its7.dart hosted with ❤ by GitHub

Створення голосової моделі:

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();
  }
}
view raw its8.dart hosted with ❤ by GitHub

Зіставлення даних для моделювання з відповіді:

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;
    }
  }
view raw its9.dart hosted with ❤ by GitHub

Потім ми створюємо аудіофайл в каталозі додатків називаючи його за часом створення:

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;
  }
view raw its10.dart hosted with ❤ by GitHub

І ми можемо відтворити створений файл за допомогою flutter audioplayer plugin:

playAudio(String audioText){
       AudioPlayer audioPlugin = AudioPlayer();
       String audioPath = writeAudioFile();
       audioPlugin.play(audio, isLocal: true);
   }
view raw its11.dart hosted with ❤ by GitHub

Епілог.

Спасибі, що дочитали цю статтю до кінця, ми сподіваємося, що вам сподобається, і тепер ви знаєте кунг-фу.
Будь ласка, перевірте опублікований додаток:
У Google PlayMarket та Apple AppStore.

Flutter
ML

More like this

Get in touch

Зв'язатися з нами

Frankfurt am Main, Germany (Sales)

60354

Eckenheimer Schulstraße, 20

+38 (098) 630-49-85

info@a5.ua

Харків, Україна

61023

вул. Трінклера, 9

+38 (050) 908-31-07

info@a5.ua

Burgas, Bulgaria (Development)

8008

бул. „Транспортна“ 15, Northern Industrial Zone

+359 877 350129

info@a5.ua