私のペットはLinguaPlayerです

今日は金曜日に、私のペットプロジェクトの1つ、それに取り組んでいる間に私がしなければならなかった興味深いこと、そしてそのさらなる発展のために解決できなかった問題について話したいと思います。





それで、私はさまざまな完成度のペットプロジェクトをかなりたくさん持っていました。その中には、作家のためのソーシャルネットワーク、CSSスプライトジェネレーター、興味を持ってデートするためのTelegramボットなどがあります。今日は私の最新の開発についてお話します。





最近の多くの人と同じように、私は英語を学んでいます。この問題への効果的なアプローチは、環境に最大限に浸ることであることも多くの人が知っていると思います。英語の電話インターフェース、英語のノートブックのメモ、英語の字幕付きの英語の映画を見る。オリジナルの映画を見ると、遅かれ早かれ、画面上で数分ごとにちらつくこの単語やフレーズを翻訳する必要があります。それらがなければ、何も明確ではありません。





プロジェクトのアイデア

だから私は翻訳可能な字幕付きのビデオプレーヤーのアイデアを思いつきました。このアプリケーションを使用すると、映画を見ながら単語やフレーズ全体を翻訳できます。これにより、アプリケーションを切り替えたり、スマートフォンを手に取ったりする必要がありません。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. , . – . , , .





したがって、現在の主な阻害要因はコンテンツです。アプリケーションで問題なく再生でき、簡単かつ迅速に取得でき、ライセンスや著作権を侵害してはなりません。コンテンツの問題が解決され次第、プロジェクトに取り組んでいきます。それまでの間、興味のある方  は、アプリケーションのコンセプトバージョンをダウンロードしてお試しください








All Articles