Как я год строил расширениe для браузера которое читает статьи голосом (с синхронизацией в подкаст)

в 12:28, , рубрики: Amazon Web Services, AWS, dynamodb, Go, python, text-to-speech, WebExtensions, подкасты, Расширения для браузеров

Не один раз я пробовал использовать сторонние API для получения голоса из текста который мне интересно прочитать — можно переключить чтение на уши когда глаза устали, или слушать во время комьюта. Знаю я такой не один, даже люди далекие от айти бывает загружают куда-то текст и скачивают mp3. И подкасты/аудиокниги становятся все популярнее, и голосовые интерфейсы. Очевидно что аудитория есть, топовые экстеншены в маркете Хрома на эту тематику имеют сотни тысяч пользователей. Но голоса от Амазона обычно у них нет (лучший из доступных, лучше нового от Гугла), а где есть нет чего-то другого, например возможности слушать в экстеншене — а не только добавлять в свой подкаст. Предложил идею проекта внутри компании — был получен апрув — пошла разработка.
image

Экстеншены для браузера это уникальное явление любимое многими — другие методы распространения ПО не имеют подобного механизма «мокинга» клиентской части. Например, десктопный клиент Evernote — невозможно сделать шрифт больше, невозможно сделать темную тему — только если хакать частично бинарные файлы ну и частично CSS — редактор в клиенте использует веб технологии. Тогда как мой поиск Гугла последние годы выглядит так:
google serp with extensions preview
Тут несколько расширений — темная тема, два превью картинкой (главной и страницы) и загрузка-превью в iframe по наведению мыши, без JS — благодаря этому загружается быстрее — и интересно видеть какой сайт без скриптов может работать какой нет — и понимать что все оставшиеся анимации сделаны чистыми стилями. И эти экстеншены были найдены и установлены из сторов — а там скриншоты и какая-то проверка на безопасность — то есть каждый сайт/сервис живет в среде где аддоны существуют имплисивно. Если ваш популярный сервис где-то неудобен — сообщество настрогает свои изменялки, иногда вплодь до мешапов — когда на одной странице могут оказаться данные из нескольких сервисов. Изменение владельцем сервиса DOMа или названий CSS классов может сломать чье-нибудь воркфлоу.

Благодаря userscrypt и userstyle мы можем менять внешний вид и функциональность сайтов — многим из нас иногда приятнее иметь свои кастомные заточки для сервисов ежедневного использования. Вот в Маке например Finder темным не сделать если авторы не позаботились, а они позаботились чтобы сторонние хаки для темирования работать перестали. В браузере все opensource — пока мы не дождались WebAssembly где разные языки смогут компилироваться в бинарники.

Несколько лет назад я делал свой первый экстеншен для Firefox — не было на нем переводчика через Яндекс. Я сделал так что перевод отображался в системном всплывающем окне — чтобы не засорять DOM страницы.
firefox yandex translate
Работало шустро. На пике чуть больше 8k пользователей, спад в графике — после Firefox 57 где расширение перестало работать:
firefox stats for YandexTranslate
Давно не смотрел, думал сегодня вообще сто человек на старом браузере, оказывается еще несколько тысяч. Положительный рейтинг того опыта и добрые отзывы добавляли мотивации.
githab comments
То расширение было некоммерческим личным pet project, тут же — первый опыт инкубатора внутри компании и расширение должно стать коммерчески успешным. В сердце продукта — лучший из доступных сервисов генерации голоса из текста — Amazon Polly. Недавно вышел синтез от Гугла: WaveNet который в рекламе звучал как человек, на самом деле оказался хуже качеством и в четыре раза дороже.

Первая версия бекэнда, точнее первый прототип для локальной машины, был написан на Питоне на встроенном сервере (интересно знать built in прежде чем переходить к фреймворкам, если они вообще будут нужны). Главная «проблема» была в разбиении текста по чанкам — лимит 1500 символов (у всех text-to-speech APIs приблизительно такой же лимит). Первый прототип был готов за несколько недель:

Казалось уже почти все и готово — ну еще UI, сайт, что-то еще и будет много пользователей.

Главной целевой платформой очевидно стал популярный Google Chrome. Начал изучать как устроен его WebExtensions. Оказалось что я попал в удачное время — как раз появился Firefox 57 где поддерживается этот формат — то есть можно написать один код для Chrome и Firefox, и даже для Edge, ну и для Opera — она вообще на движке Хрома. Здорово, тот мой старый экстеншен работал только в Firefox, ну а теперь и только до версии 57. Если вы в недавнем прошлом разработали расширение для Google Chrome — скорее всего сегодня оно будет работать и в Firefox. А также даже если автор расширения не озаботился этим — вы можете сами скачать архив аддона с Chrome Store и установить в Firefox — думаю только сейчас, с 60 версии Firefox, можно сказать что его имплементация WebExtensions стабилизировалась и избавилась от многих детских проблем.

Сначала я играл звук в так называемом popup UI — это то что вы видите когда нажимаете на кнопку экстеншена и появляется его интерфейс, но оказалось что как только это окно закрывается — эта обычная страница выгружается из памяти и звук перестает играть. Ок, играем из пространства страницы — это называется Isolated Worlds где с вашим экстеншеном шарится только DOM, origin остается экстеншеновский (если вы не запрашиваете при установке права на доступ к страницам). Звук играл и с закрытым экстеншеном, но тут я на практике познакомился с Content Security Policy — на Medium не играло — оказалось там явно прописано откуда могут поступать медиа элементы. Осталось третье место откуда можно играть — background/event page. У каждого расширение есть три «страницы» — общаются они сообщениями. Event page значит что экстеншен существует в памяти только когда нужен — например отреагировал на ивент (клик), пожил несколько секунд и выгрузился (похоже на Service Worker). Firefox пока поддерживает только background page — экстеншен всегда в фоне. Проверил все свои установленные расширения — нашел несколько таких которые всегда в памяти, хотя функционально они только на ивенты реагируют, самое известное из них — Evernote Clipper. Отказаться от него я не могу, хотя после установки браузер работает явно медленнее. Они вставляют много своего кода в каждую открывающуюся страницу. Возможно это ускоряет отклик при клике на их кнопку, но думаю глобальное торможение это не оправдывает. Написал им об этом.

Вставляют код в страницу, даже в ваш приватный Google Doc — каждый маркетплейс с расширениями имеет автоматическую и ручную проверку кода на безопасность. В Chrome, насколько я понял, проверка автоматическая — люди смотрят только при варнингах, Firefox — всегда смотрит человек — и иногда вижу у них в блоге объявления что ищутся новые волонтеры на эту позицию. Opera — требует чтобы была приложена девелоперская версия кода с инструкцией как воспроизвести билд. Популярная проблема с Оперой — ревью происходит очень медленно, например наш экстеншен висит в очереди уже несколько месяцев — и еще не вышел в их магазине. Когда однажды все-же кто-то из Оперы начал проверять аддон на безопасность — дал красный свет по причине что хеш собранного архива отличался от хеша архива мною предоставленного.

Chrome Store несколько месяцев удалял расширение при каждом обновлении — и высылал стандартное письмо о возможных причинах — где не было описано ничего из нашего продукта. Каждый раз мы слали им письма и даже звонили.
image

Edge как всегда — там что-то не работало, но devtools не открывался, попробовал предрелизную сборку Windows — то же самое, спросил на StackOverflow — ответа нет.

Столкнулся с проблемой что играние звука не мешает экстеншену выгрузиться — завел баг, написал забавный workaround:

function _doNotSleep() {
    if (audioCurrent) {
        setTimeout(_ => {
            // only some http request, neither console.log() nor creating obj
            fetch(chrome.runtime.getURL('manifest.json'));
            _doNotSleep();
        }, 2000);
    }
}

Параллельно велась работа на серверной стороне. Ментор, который сам тоже и разработчик (хотя в этом проекте код не писал), убедил использовать DynamoDB для базы данных (NotOnlySQL от Amazon) и Lambda вместо классического сервера. Сегодня я наслаждаюсь лямбдами — это виртуальные машины (Amazon Linux, based on Red Hat) которые запускаются на ивенты, в моем случае — на HTTP запросы которые идут через Amazon API Gateway. Сегодня смешно вспоминать — но сначала когда я не понимал что лямбды это не EC2 — пытался использовать сервер Питона для обработки HTTP — только потом узнал что лямбды с интернетом связываются через API Gateway — то есть получается что отдельный сервис вызывает контейнер, на каждый запрос. Помимо HTTP лямбды могут просыпаться на другие ивенты — например обновление базы или добавление файла в S3. Если нагрузка повышается в сотни раз — запускаются сотни лямбд одновременно — scalability это один из продающих пунктов этой технологии. Лямбда живет максимум пять минут. Контейнер всегда обрабатывает только один запрос, контейнер может быть автоматически переиспользован для следующего запроса а может и нет. Все это заставляет немного изменить стиль разработки. Stateless — жесткий диск (пол гигабайта) только для временных файлов (у меня уже давно такой образ жизни что жесткий диск лаптопа тоже должен быть stateless — чтобы урон был минимален от потери данных — весь state в облаках, конфиги в git). Удивляюсь, но сейчас для такого маленького продукта уже используется десяток лямбд. Изоляция этих микросервисов делает код проще — вот этот экран листинга это и есть весь микросервис, вот этот экран кода — второй независимый микросервис. Понял почему lose coupling это хорошо.
aws lambda analytics

Лямбды стали популярны, у Google Cloud и Microsoft Azure и Openstack есть свои аналоги. Встроенное ПО обновляется автоматически — security updates и обновления Питона происходят сами. На прочих проектах мы внутри компании для сервера первым делом рассматриваем Лямбды. Для своих личных задачек-микропроектов лямбды хороши еще и тем что стоят они доли цента, и если например вам нужно что-то автоматически запускать раз в месяц — лямбда может быть хорошим решением. При высокой нагрузке EC2 будет стоить меньше денег — но это если у вас не будет проблем с масштабированием и прочим maintenance.

DynamoDB просто работает, пока про него сказать нечего (пока вся база всего несколько мегабайт). Обещают автоматически масштабироваться. База популярна так что есть много готовых инструментов, например для экспорта. Плюс перед например открытым Postgres — можно сделать связь через API Gateway напрямую к API DynamoDB — без промежуточной Лямбды, полиси доступа можно настраивать на уровне индексов и колонок таблицы. У меня почта пользователя (ключ таблицы) берется из верифицированного токена — API Gateway сам проверяет:
aws apigateway authorizer cognito

При создании HTTP endpoint без Лямбды можно написать входной запрос на сервис Амазона который имеет API и модифицировать выходную структуру — используя Apache Velocity синтакс, например обновление базы:

#set( $unixtime = $context.requestTimeEpoch / 1000 )

{
    "TableName": "history",
    "UpdateExpression": "SET isNew = :isNew, updated = :updated",
    "ExpressionAttributeValues": {
        ":isNew": {"BOOL": $input.body},
        ":updated": {"N": "$unixtime"}
    },
    "Key": {
        "email": {"S": "$context.authorizer.claims.email"},
        "utc": {"N": "$input.params('id')"}
    }
}

Плохо что при экспорте гейтвея вот этот код будет сохранен в одну строку, в каше с прочей информацией про эндпоинт, да еще при каждом экспорте может меняться сортировка строк — в Гите образуется месиво, труднее отслеживать изменения в таком коде:
Как я год строил расширениe для браузера которое читает статьи голосом (с синхронизацией в подкаст) - 9

При каждом открытии окна экстеншена идет запрос на «сервер» — для синхронизации истории и статуса (прослушано или нет) — API Gateway напрямую к HTTP API DynamoDB.

Получилось сделать так что при установке экстеншен не требует никаких разрешений — они не нужны если все что нужно это работа с текущим DOM и отправка его на сервер (даже без пермишенов — можно догадаться что ищут секьюрити сканы при публикации). Думал было придется вводить разрешение на коммуникацию с родительским сайтом для аутентификации — но обошлось — придумал что при открытии окна если не найден id_token от Amazon Cognito — делается запрос на API Gateway -> Lambda где проверяется таблица state_on_tokens — есть ли там такой стейт (криптографическая строка), если есть — возвращается токен а эта запись в таблице удаляется.

Пока текст из HTML вынимается только на сервере (на Лямбде) — это позволяет использовать единый микросервис при получении текста/HTML из браузера и из писем. При наборе популярности и повышении нагрузки можно будет вынимать текст на клиенте (есть библиотеки), возможно так и работать будет быстрее.

Тексты и HTML мы у себя не храним, только полученный звук на S3. У каждого пользователя своя папка. При добавлении статьи проверяем — есть ли подпапка с таким же хешем, если есть — звук переиспользуется. Пока можно добавить две одинаковые статьи, возможно в будущем будем предупреждать если хеши совпадают.

Мы согласились что для такого экстеншена который добавляет и читает тексты логично было бы синхронизироваться с личным подкаст фидом — ну разумеется, я же сервис для себя делаю в том числе, мне такая функциональность нужна. Оказалось что кроме не лучшего сегодня mp3 можно использовать более эффективный aac — Андроид воспроизводит его с четвертой версии. Месяц возился с разными методами сшивания ogg чанков в единый файл, пробовал ffmpeg, libav, что-то еще, переписывался с Амазоном и багтрекерами проектов — звук получался с дефектами. Остановился на минимальном решении, как я и люблю — ничего личшего — одна бинарная программа декодирует ogg в wav (oggdec), вторая из пайпа энкодит aac (fdkaac) — Питон выполняет shell комманду:

f'''cd /tmp;
curl {urls} |
/var/task/oggdec - -o - |
/var/task/fdkaac -m5 - -o {episode_filename}
--title="{title_escaped}"
--artist='Intelligent Speaker'
--album-artist='Intelligent Speaker'
--album='Intelligent Speaker'
--genre='Podcast'
--date='{date}'
--comment='Voiced text from {url_article or "email"}'
'''

Тут в --comments может быть линк на оригинальную страницу (если текст не был отправлен почтой) — оказалось что на iOS встроенный подкаст проигрыватель может удобно показать что за линкой.

Когда пользователь получает ответ что статья добавлена — на самом деле мы только выняли текст из HTML и вернули хеш этого текста (который работает как ссылка), пока пользователь двигает мышь к кнопке Play — звук еще только синтезируется. Когда начал слушать статью — звук возможно готов еще не для всего текста, так что при перемотке иногда нужно ждать. После рендера последнего чанка звука — запускается еще одна Лямбда в этой цепочке — которая генерирует m4a для подкаста и модифицирует xml файл фида.

У Лямбд есть лимит на передачу payload в другую лямбду — всего 128 килобайт. Написал обработчик исключения — использовать отдельный транзитный бакет а передавать в лямбду синтезирования звука хеш файла, в бакете настроено полиси на удаление файла через сутки (минимальное значение).

Для кодирования звука рассматривал Amazon Elastic Transcoder, но удивился что звук там можно получить только со статическим битрейдом — а я хотел динамический для лучшей компрессии. Даже письмо в саппорт писал — как может быть что нет такой базовой опции, может она в другом месте или я не понял чего-то? Ответили что и правда кодирование только в статический:
image

Если текста много — пяти минут дешевой лямбды может не хватит — поэтому выставил максимальный CPU — хватит скодировать часов шесть за раз.

У каждого пользователя есть inbound email как в Эверноут — вижу в телефоне интересную статью — шарю в почтовую программу и отправляю линк или текст (из любой программы) на личный адрес, переключаюсь на программу с подкастами — новый эпизод уже здесь — запускаю проигрывание и продолжаю крутить педали на снегу. Это значит что можно использовать сторонний сервис который отправляет письмо на каждый новый пост из RSS — это значит что вы можете подписаться на блог в виде подкаста. На телефоне пока экстеншен не работает — хотя Firefox и YandexBrowser для Андроида поддерживают расширения но там я вижу только UI с нерабочими кнопоками; или дождусь Progressive Web Apps для iOS когда в него можно будет шарить как в нативную программу. Хотя входящая почта уже предлагает такую функциональность — еще и в отправленных останутся линки.

При имплементации поиска на клиенте столкнулся с предупреждением Хроме Violation Long running JavaScript task took xx ms. Ну, что-то там медленно работало, не критично, ладно, решил покопать, поизучать красивый профайлер Хрома:
google chrome profiler
Много операций, перерисовки, поэтому подтормаживает.

Нашел что это происходит при фильтрации и изменении класса нужных нод:

search.addEventListener('keyup', function() {
    for (const node of nodes)
        if (node.innerText.toLowerCase().includes(this.value.toLowerCase()))
            node.classList.remove('hidden');
        else
            node.classList.add('hidden');
});

Ну что тут поделаешь, но предчувствие продиктовало мне попробовать оптовый подход:

search.addEventListener('keyup', function() {
    const nodesToHide = [];
    const nodesToShow = [];
    for (const node of nodes)
        if (node.innerText.toLowerCase().includes(this.value.toLowerCase()))
            nodesToShow.push(node);
        else
            nodesToHide.push(node);

    nodesToHide.forEach(node => node.classList.add('hidden'));
    nodesToShow.forEach(node => node.classList.remove('hidden'));
});

Операций больше, кода больше, а работать стало сильно быстрее:
image
Тут видно что на самом деле браузер делает гораздо меньше операций.

Одно из лучших решений при разработке продукта — после удаления показывать форму обратной связи. Использовал обычный Google Forms:
google forms spreadsheet

Заказали сайт/landing — их портфолио было красивым. Но оказалось что наш сайт делали другие люди:
intelligent speaker first site

Был у нас на примете один похожий продукт, я и маркетолог посмеивались над его сайтиком с нерабочей главной линкой на стор — тут стало ясно что наш выглядит хуже. Так мы и жили несколько месяцев с таким дизайном. Потом ментор решил заказать другой сайт у другой комманды, с нашими доработками получилось лучше:

intelligent speaker site new
Вот так он и выглядит сейчас. Не идеально, надо дорабатывать, но мне нравится agile подход — сначала сделаем как-нибудь, потом сделаем получше, потом еще чуть лучше. Потому что не знаю что можно улучшить. Одна из причин написания этой статьи — получение обратной связи.

Отзыв от Larry настоящий — ему так понравился продукт (точнее голос Амазона) что он на своей новой книге поместил наш логотип, вот это да. Я был удивлен что люди пользуются Интеллиджент Спикером для пруфридинга (когда писал статью и читаешь ушами чтобы почувствовать под другим углом) и при дислексии. Например пришло благодарственное письмо от американского тренера что как ей здорово теперь слушать тексты.

Для хостинга сайтика изначально был выбрал GitHub — я уже имел опыт работы со статическими сайтами используя его возможность добавить свой домен. Но как тогда оказалось — свой домен невозможно держать на Гитхабе с SSL сертификатом. Сегодня уже можно (с этого месяца кстати), но в 2017 по этой причине я смигрировался на GitLab. Пришлось писать первый в моей жизни ci чтобы сайт копировался из одной папки в другую — минимальный процесс работы со static pages. Не всегда этот (бесплатный замечу) ci работает — какие то проблемы GitLab, но в общем я доволен им, недавно узнал что у них был вот такой старый логотип
gitlab logo old
Ого, эта харизма мне любее шаблонного
gitlab logo new

LastPass показывает лучшую версию:
image

Хм а еще нет экстеншена чтобы был старый логотип? Видел похожие шуточки.

Сегодня ci для сайта в том числе минифицирует HTML/CSS/JS — давно хотел имплементировать эту best practice, обычно этим не заморачиваются — есть задачи приоритетнее. После минификации — создаются gz архивчики для каждого файла — теперь даже на этом бесплатном статическом хостинге мы получили Content-Encoding: gzip, надеюсь Гугл от этого будет ранжировать выше и у пользователей будет быстрее открываться (а кажется отдает Гитлаб не быстро).

Ci написан и для сборки-публикации экстеншена: минификация и HTTP POST. Запускается локально вручную — можно было бы ввернуть на тот сервер где живет Git экстеншена — в нашем случае VSTS, но пока незачем. Сайт живет в submodule — возможно стоило иметь один репозиторий, но тогда я думал что незачем гонять в Гитлаб код который не относится к сайту — тем паче еще чтобы ci запускался, хотя да можно настроить чтобы сайт собирался только при пуше в определенный бранч.

Первая версия UI экстеншена выглядела так:
image

Потом ментор сказал что путь студия сделает нормальный дизайн:
image

Чувствовали что надо лучше.

Решили попробовать Material — мы не против нераздражающей безликости, пусть чувствуется как часть браузера, и думаю это повышает шансы стать Featured в Chrome Store — ведь визуальный стиль от той же компании. Хотя сегодня нам уже объяснили что Материал это не значит когда все одинаковое. Сегодняшний итог:
intelligent speaker ui current

Фишка продукта в том что он будет не только детектить текст для чтения как readability mode, но и хитро адаптировать текст для озвучивания — например чтобы было удобно слушать дерево комментариев когда ники, даты, цитирования будут читаться другой интонацией. Это еще в планах, начать планируем с Reddit. Но уже на ранней стадии разработки стало понятно что возможностей библиотек для дистилляции HTML не хватает — например в Википедии[0] остаются[1] вот[2] такие[3] штуки[4], Google Doc вообще не подхватывался (а хотелось получить плашку в Chrome Store что мы его поддерживаем). Решение — для Википедии используется их API, для Google Doc — хитрый код для получения выделенного текста и всего документа — чтобы не просить у пользователя разрешения на доступ ко всему Google Drive. Выбрали десяток других популярных сайтов и проверили/адаптировали чтение на них.

Ментор с маркетологом решили что обязательно нужен блог с социальными сетями. Начал искать статический блогогенератор — для скорости, простоты, дешевизны и безопасности. Если я себе когда-нибудь открою блог — то только в виде продукта статического генератора — чтобы в Гите хранились все посты. От моего доброго друга Марселя Пруста umputun слышал что Hugo хороший. Несколько дней изучения, тестов, выбора темы — блог готов. Сначала мы хранили в Гите сгенерированные асеты, потом я дописал ci на Гитлабе чтобы собирал там же — чище Гит, маркетологу проще добавлять посты (думал вообще не получится научить это прекрасную девушку писать посты в блокноте маркдауном и пушить). Для сборки сайта используется контейнер Ubuntu в который устанавливается все что нужно — в том числе и Hugo — и получается что когда Hugo обновится — при пуше нового поста в Гит — на сайте «движок» блога тоже обновится, красота. Хотя для безопасности лучше не иметь внешних зависимостей — чтобы сайт собрался даже когда GitHub недоступен.

Однажды начитавшись про Go, любимши быстрое выполнение и всякие оптимизации, написал минимальный тест — HTTP GET — оказалось такая Лямбда отрабатывала быстрее, нагугленные бенчмарки показывали высокую скорость. Следующий микросервис писал уже на Go — теперь это мой основной серверный язык вместо Питона. Понравилось наличие встроенного инструмента `go fmt` для форматирования кода, ну вы знаете, это всем нравится, хоть там и табы. Для преобразования JSON строки в объект надо заранее описывать структуру — после Питона и JS это казалось невероятным — зачем? Еще более невероятным оказалось что в массивах (arrays или slices) нет метода contains() — приветствую минимализм, но неужели это корректный дизайн языка когда нужно писать свой цикл для такой базовой вещи? Другие незначительные минусы Go перед Питоном — деплой на Лямбду это скопмилированный исполняемый файл — то есть например 6 мегабайт а не килобайт кода (надо ждать несколько секунд), а также все-же больше кода (уровень абстракции иногда ниже, но мне нравится лучше понимать как оно работает). Приятно что компилируеммый код безопаснее — меньше ошибок в рантайме, а типы в Питоне я и так писал с 3.5 — с типами языки быстрее (но в Питоне типы только для валидации), понятнее и безопаснее. Golang тоже имеет аналог duck typing через имплисивные интерфейсы — если есть нужные методы — значит объект подходит под интерфейс.

Я думал — почему Go а не например Java? Я не тестировал сам, но читал что на Лямбде холодный старт будет дольше. Возможно можно написать лямбду которая будет держать все другие лямбды теплыми и использовать Java, возможно. Не люблю прыгать по технологиям, мне больше нравится углубляться и совершенствоваться в текущем стеке, но тут уж скорость Go плюс его общая положительная оценка индустрией и то что Лямды его теперь поддерживают да так что Амазон все свои SDK написал и на Go тоже — хотя это технология от конкурирующей корпорации — переход того стоил. В нашей небольшой компании на другом проекте тоже переходят с Питона на Go.

Из Go мне понравился один из стейтментов что A little copying is better than a little dependency, это тоже один из моих девизов, то есть я стараюсь не бабахать библиотеку ради экономии пяти строк. Один из моих любимых экстеншенов имеет 639 зависимостей, можете себе представить:
Как я год строил расширениe для браузера которое читает статьи голосом (с синхронизацией в подкаст) - 22

С ранней стадии экстеншена была возможность проигрывать из контекстного меню, а когда играет — ставить на паузу и перематывать на чанк вперед-назад:

intelligent speaker old context menu rewind

Но было принято решение сделать UI удобнее в ущерб функциональности — вместо вложенного меню теперь только один пункт который добавляет страницу или выделенное в список:

intelligent speaker context menu

Это решение далось трудновато, но я вижу основной историей добавление страниц для прослушивания потом, а слушать сразу ведь все еще можно — только нужно открыть окно экстеншена и play.

Логотип как видите тоже поменялся — во время прототипирования сайта девочка дизайнер поставила загрушку — этот квадрат нам показался лучше попугая.

Тестирование: Селениум не может открыть окно расширения, поэтому использую его в связке с pyautogui для кликов и scikit-image для сравнения ожидаемого и текущего изображения. Никогда еще такого не делал, это интересно — таким образом я могу тестировать CSS.

Весь код на протяжении года (и раньше) я писал в Виме. Узнал еще больше об этом великолепном редакторе. Настало то время когда я почувствовал что в Виме действительно продуктивен. Волшебный peace of software. Во всем айти для меня Вим стал особенностью индустрии которая составляет the best parts. В нем есть свой стиль. И предустановлен во многих местах — ssh кудаугодно — рабочая среда остается удобной, он даже у меня в Андроиде из коробки есть. Плагины расширяют функциональность, а с недавнего времени поддерживается их асинхронная работа. У меня есть линтеры-анализаторы, возможность посмотреть предыдущую версию ханка гита, какой-то рефакторинг (переименования функций во всех местах), букмарки в коде, хексы цветов выделены его цветом, и везде темный цвет — помню в Intelligent IDEA на нескольких мониторах во внешних окнах было невозможно избавится от белой рамки маковского окна. Видел и более sophisticated плагины как-то подсказки, документации при написании, интеграции с внешними утилитами, но в Виме и так много всего спрятано — и внешние комманды к выделенному тексту можно легко выполнять — например посчитать количество символов (wc) или перевод выделенного текста (trans). Несколько раз слышал жалобы что в Виме что-то нельзя сделать — а на самом деле можно. Довелось работать и на Salesforce используя соответственный плагин — прямо из Вима запускал тесты на удаленном сервере. Помню как воспринимал фрикерством когда слышал что кто-то работает в Виме или Емаксе, но со мной это тоже произошло. Не призываю попробовать — действительно нужно время чтобы разобраться как делать базовые операции, нужны инвестиции времени и сил чтобы добраться до мякоти.

Сегодняшняя аналитика в Chrome Store:
chrome store stats
Изменение темпов роста между декабрем и февралем — добавили локализацию в экстеншен на все языки что читает Polly — так что теперь например для франзузской выдачи мы стали выше. Думаю локализация сайта даст такой же эффект. Статистика сайта:
google analytics intelligent speaker

google analytics intelligent speaker traffic source

Рост оказался ниже планируемого, хотя платной рекламы нет — только органический трафик. Шесть человек подписались за $6.99 в месяц. Возможно со временем цифры начнут расти. Что думаете?

Вот такая история моего года.

Автор: zdanevich-vitaly

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js