Русский
Русский
English
Статистика
Реклама

Мой питомец LinguaPlayer

Сегодня в пятничном формате хотел бы рассказать об одном из своих пет-проектов, чем интересным пришлось заниматься во время работы над ним и какие вопросы мне так и не удалось решить для его дальнейшего развития.

И так, у меня было достаточно много пет-проектов разной степени готовности. Среди них: социальная сеть для писателей, генератор CSS-спрайтов, Телеграм бот для знакомств по интересам и многое другое. Сегодня речь пойдёт о моей последней разработке.

Как многие в наши дни, я учу английский. Думаю, так же многие знают, что эффективным подходом в этом деле является максимальное погружение в среду. Интерфейс телефона на английском, записи в блокноте на английском, смотреть кино на английском с английскими субтитрами. Смотря кино в оригинале, рано или поздно возникает потребность перевести то или иное слово или фразу, которые мелькают на экране каждые несколько минут. Без них вообще ничего не понятно.

Идея проекта

Так у меня и родилась идея видеоплеера с переводимыми субтитрами. Приложение позволяет переводить слова и целые фразы прямо во время просмотра кино. С ним отпадает необходимость переключаться между приложениями или брать в руки смартфон. Знакомьтесь LinguaPlayer.

Схема работы простая. Пользователь открывает файл с фильмом и файл субтитров. Смотрит фильм как обычно. Однако теперь в его распоряжении помимо стандартных горячих клавиш есть клавиши для перевода каждого слова по отдельности, перевода целых предложений, перемотки от реплики к реплике. Также присутствует перевод посредством наведения курсора мыши на слова или выделения нужного куска текста. Приложение доступно для Windows и MacOS. Все подробности можно почитать на страничке приложения.

Технологический стек

Плеер реализован на платформе Electron, т. е. по сути это браузер Chromium внутри которого бежит обычное веб-приложение. На этой технологии построено большое количество различных приложений удачных и не очень. Наиболее известные примеры это Visual Studio Code, Skype, Slack. Electron предоставляет некоторые системные API, которые недоступны JavaScript, запущенному в обычном браузере. Это позволяет делать приложения более функциональными и близкими по возможностям и пользовательскому опыту к нативным. Что касается всего остального разработчику доступен абсолютно любой стек, применимый в вебе. Будь то чистый JavaScript, Angular, jQuery, Vue что угодно.

Для LinguaPlayer я выбрал привычный стек, с которым работаю каждый день: TypeScript, React, MobX, Webpack. Я планировал сделать прототип за вечер, так как задача выглядела легко: дал приложению файл с видео и файл с субтитрами, прикрутил переводчик и готово. С этой частью проблем не возникло. Однако, как оказалось, встроенный в браузеры движок отображения субтитров не имеет никаких средств взаимодействия с текстом реплик. Что я имею в виду. Реплики титров хоть и появляются поверх видео, но в DOM никаких упоминаний о них нет. Другими словами, нет возможности распарсить текст, разбить его по словам и предложениям, повесить обработчики нажатий клавиш или зарегистрировать события мыши.

Решение интересных задач

Таким образом, мне пришлось решить две задачи. Первая это парсинг srt-файлов с титрами с последующей синхронизацией реплик с видеопотоком. Вторая разбивка реплик на токены, чтобы была возможность взаимодействовать с каждым словом и предложением по отдельности.

Для парсинга титров я взял библиотекуnode-webvtt. Для синхронизации титров изначально я написал логику в лоб. Подписавшись на событие video-элемента timeupdate, я просто ходил по массиву реплик и сравнивал текущее время видео с временем реплик. Однако, событие timeupdate срабатывает четыре раза за секунду, а реплик в среднем фильме несколько тысяч. Всё это дело жутко тормозило.

Для оптимизации данного процесса я сделал простой hash map. Его ключом является секунда (целая, без миллисекунд), а значением массив номеров реплик, которые должны быть показаны в эту секунду. Выглядело это примерно так:

{// на пятой секунде 2 реплики5: [1, 2]// на седьмой секунде 3 короткие реплики7: [3, 4, 5]}

В каждой секунде может быть от 0 до примерно 4 реплик человек имеет ограничения и вряд ли сможет прочитать большее количество текста за секунду. Таки образом, при обновлении времени воспроизведения, всё что необходимо сделать, это выделить из него целую часть и обратиться с этим значением к hash map. Если на этой секунде реплик нет, значит ничего больше не делаем. Если на этой секунде есть реплики, то итерируемся по вернувшемуся массиву и ищем, какая именно реплика должна быть показана сейчас. Так как на секунду может быть до 4 реплик, то цикл завершается быстро. Возможно, многим будет проще воспринять описанный алгоритм в виде кода:

// Объект реплики: порядковый номер, время начала отображения (в миллисекундах), время конца отображения, текстclass Cue {  public readonly index: number;  public readonly startTime: number;  public readonly endTime: number;  public readonly text: string;  constructor(index: number, startTime: number, endTime: number, text: string) {    this.index = index;    this.startTime = startTime;    this.endTime = endTime;    this.text = text;  }}interface CueIndex {  // Ключ индекса это целая секунда (без миллисекунд) и массив порядковых номеров реплик,  // которые начинают или заканчиваются в течение этой секунды  [key: number]: number[];}class SubtitlesTrack {  private readonly cues: Cue[];  private index: CueIndex = {};  constructor(cues: Cue[]) {    this.cues = cues;    // На входе у нас просто массив реплик, нужно проиндексировать    this.indexCues();  }  private indexCues() {    this.cues.forEach((cue: Cue) => {      // Переводим время начала и конца реплики из миллисекунд в секунды и берём только целую часть      const startSecond = Math.floor(cue.startTime / 1000);      const endSecond = Math.floor(cue.endTime / 1000);      // Добавляем реплику (её порядковый номер) в индекс      this.addToIndex(startSecond, cue);      // Бывает, что реплика началась в одной секунде, и отображается до следующей или даже держится несколько секунд      // Такую реплику следуют добавить также в индекс секунды окончания      if (endSecond !== startSecond) {        this.addToIndex(endSecond, cue);      }    });  }  private addToIndex(secondNumber: number, cue: Cue): void {    // Если это первая реплика в данной секунде, инициализируем ключ индекса пустым массивом    if (!this.index[secondNumber]) {      this.index[secondNumber] = [];    }    // Затем в массив реплик добавим порядковый номер новой реплики    this.index[secondNumber].push(cue.index);  }  // Метод поиска реплики  public findCueForTime(timeInSeconds: number): Cue|null {    // Событие плеера timeupdate присылает время сразу в секундах    // Поэтому сразу берём целую часть    const flooredTime = Math.floor(timeInSeconds);    // Выбираем проиндексированные реплики для этой секунды    const cues = this.index[flooredTime];    let currentCue = null;    // Если на данной секунде есть реплики    if (cues) {      // Проходимся по каждой      for (let index of cues) {        const cue = this.cues[index];        // И смотрим, совпадает ли интервал времени начала и конца реплики с текущем временем в плеере        if (this.isCueInTime(timeInSeconds, cue)) {          // Если да, то устанавливаем значение текущей реплики и останавливаем цикл          currentCue = cue;          break;        }      }    }    // Вернём текущую реплику или null, если для данного времени реплики нет    return currentCue;  }  public isCueInTime(timeInSeconds: number, cue: Cue): boolean {    const timeInMilliseconds: number = timeInSeconds * 1000;    return timeInMilliseconds >= cue.startTime && timeInMilliseconds <= cue.endTime;  }}

Таким образом, вместо того, чтобы 4 раза в секунду обходить массив из тысяч элементов, необходимо лишь сделать одно обращение по индексу, и пройтись по массиву длинной от 1 до 4.

Перед тем как выводить получаемые реплики я разбивал их на предложения и на слова с помощью библиотекиnode-sentence-tokenizer. Всё это дело я оборачивал в элементы div с классами sentence и word соответственно, чтобы иметь возможность вешать на них события в дальнейшем и выполнять перевод. Вот так выглядит код:

import Tokenizer from 'sentence-tokenizer';function formatCue(text: string): string {  const brMark: string = '[br]';  const tokenizer = new Tokenizer();  // Заменяем переносы строк на псевдотэг для удобной работы с переносами в дальнейшем  text = text    .replace(/\r\n/g, ` ${brMark}`)    .replace(/\r/g, ` ${brMark}`)    .replace(/\n/g, ` ${brMark}`);  // Устанавливаем text как сущность для обработки  tokenizer.setEntry(text);  // Разбиваем текст на предложения  const sentenceTokens: string[] = tokenizer.getSentences();  // Проходимся по предложениям  const sentencesHtml: string[] = sentenceTokens.map((sentenceToken: string, index: number) => {    // Разбиваем предложение по словам    const wordTokens: string[] = tokenizer.getTokens(index);    // Идём по каждому слову    const wordsHtml: string[] = wordTokens.map((wordToken: string) => {      let brTag: string = '';      // Если после слова есть псевдотэг переноса, удаляем его и устанавливаем html тэг переноса строки      if (wordToken.includes(brMark)) {        wordToken = wordToken.replace(brMark, '');        brTag = '<br/>';      }      // Оборачиваем слово в span с классом word и добавляем тэг br, если надо      return `${brTag}<span class="word">${wordToken}</span>`;    });    // Склеиваем слова обратно, в строку, оборачиваем предложение в в span с классом sentence    return `<span class="sentence">${wordsHtml.join(' ')}</span>`;  });  // Склеиваем предложение обратно в строку  const html: string = sentencesHtml.join(' ');  return html;}

Далее я прикрутилMicrosoft Translatorдля осуществления перевода, и плеер для изучения английского был готов.

Чего не хватает проекту, чтобы развиваться

Конечно, на данный момент плеер даже не является MVP, это скорее proof of concept. И у меня есть множество идей по развитию данного проекта. В первую очередь хочется добавить поддержку словарей, например английского и англо-русского, а также интегрировать Urban Dictionary для распознавания сленга, а также различных современных слов и фраз. Во вторую очередь есть мысль реализовать интеграции с сервисами по изучению иностранных языков, такими какLinguaLeoилиSkyeng. Это позволило бы добавлять незнакомые слова в персональный словарь на сервисе и учить их позже. Или же импортировать свой словарь вAnki. Также можно было бы добавить поддержку изучения других языков.

Но, прежде чем приступить к реализации задуманных функций, необходимо решить ряд технических и концептуальных проблем. Моя изначальная идея заключалась в том, чтобы можно было смотреть свои старые и излюбленные фильмы, пылящиеся на жёстком диске. И здесь возникает серьёзная техническая проблема, а именно слабая поддержка кодеков в браузере Chromium. Глядя насписок поддерживаемых форматов, можно заметить, что по факту в приложении можно проиграть лишь файлы с видеокодеком H.264 и аудиокодеком FLAC либо MP3. Нужно очень постараться чтобы найти именно такой файл. Да никто и не будет заниматься подобным сейчас все избалованы стриминговыми сервисами. Не говоря уже о вопросах лицензирования, что является большой, концептуальной проблемой.

Таким образом, главный блокирующий фактор сейчас это контент. Он должен без проблем играть в приложении, должна быть возможность легко и быстро его получать, а также, он не должен нарушать лицензии и авторские права. Как только удастся решить вопрос с контентом, я с радостью продолжу работу над проектом. А пока, если кому интересно, можноскачать и попробоватьконцепт-версию приложения.

Источник: habr.com
К списку статей
Опубликовано: 19.02.2021 14:12:20
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Развитие стартапа

Pet-project

Electron

Experience

Examples

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru