Hoje, sexta-feira, gostaria de falar sobre um dos meus projetos favoritos, as coisas interessantes que tive de fazer enquanto trabalhava nele e que problemas não consegui resolver para o seu desenvolvimento posterior.
Então, eu tinha alguns projetos favoritos com vários graus de conclusão. Entre eles: uma rede social para escritores, um gerador de sprites CSS, um bot do Telegram para namoro por interesses e muito mais. Hoje vamos falar sobre meu último desenvolvimento.
Como muitas pessoas hoje em dia, estou aprendendo inglês. Acho que muitas pessoas também sabem que uma abordagem eficaz neste assunto é a máxima imersão no meio ambiente. Interface do telefone em inglês, anotações em notebook em inglês, assistir filmes em inglês com legendas em inglês. Assistindo a um filme no original, mais cedo ou mais tarde é necessário traduzir esta ou aquela palavra ou frase que pisca na tela a cada poucos minutos. Sem eles, nada é claro.
Ideia de projeto
Então, tive a ideia de um reprodutor de vídeo com legendas traduzíveis. O aplicativo permite que você traduza palavras e frases inteiras enquanto assiste a um filme. Com ele, não há necessidade de alternar entre aplicativos ou pegar um smartphone. Conheça LinguaPlayer .
O esquema de trabalho é simples. O usuário abre o arquivo de filme e o arquivo de legenda. Assiste ao filme normalmente. Porém, agora, além das teclas de atalho padrão, ele tem chaves para traduzir cada palavra separadamente, traduzir frases inteiras, retroceder de uma linha para outra. Também há uma tradução passando o cursor do mouse sobre as palavras ou destacando o trecho de texto desejado. O aplicativo está disponível para Windows e MacOS. Todos os detalhes podem ser encontrados na página de inscrição .
Pilha de tecnologia
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;
}
,
, MVP, proof of concept. . , -, Urban Dictionary , . , LinguaLeo Skyeng. . Anki. .
, , . , , . , – Chromium. , , H.264 FLAC MP3. , . – . , , .
Portanto, o principal fator de bloqueio agora é o conteúdo. Deve jogar sem problemas no aplicativo, deve ser capaz de obtê-lo de forma fácil e rápida, e também, não deve violar licenças e direitos autorais. Assim que o problema de conteúdo for resolvido, terei o maior prazer em continuar trabalhando no projeto. Nesse ínterim, se alguém estiver interessado, você pode baixar e experimentar a versão conceitual do aplicativo.