Coursera по-русски: про достижения и награды

в 20:01, , рубрики: coursera, Блог компании ABBYY, краудсорсинг, локализация, перевод

Кажется, мы только объявили об официальном запуске проекта «Переведём Coursera», а наши добровольцы уже преодолели рубеж в первый миллион переведенных слов. Благодарим всех участников — вы настоящие молодцы. Для вас мы подготовили специальную страницу, где собрали поздравления и инфографику в честь такого события.

Coursera по русски: про достижения и наградыКак вы знаете, в основе проекта лежит SmartCAT — собственная облачная платформа ABBYY Language Services для автоматизации перевода. Она позволяет всем переводчикам Coursera работать с современными технологиями для перевода в удобном интерфейсе: участники могут использовать Translation Memory, автоматизированную поддержку целостности терминологии, машинный перевод, просматривать видеофрагменты лекций. Мы постоянно обновляем этот технологический фундамент, чтобы ресурс стал удобнее и лучше помогал нашим участникам с переводом лекций.

Думаем мы не только об удобстве, но и о том, как волонтеров заинтересовать. Недавно на проекте заработала система наград — ачивки. Они отражают успехи участников в переводе Coursera. Наши разработчики поделились, как устроена эта система и как она работает. Вот об этом мы вам сегодня и расскажем.

Дано
Ачивки выдаются пользователю в процессе его работы на платформе и являются формой нематериального поощрения за хорошие результаты. Для участников они выглядят как медали и отображаются рядом с аватаркой. Они выдаются за определенные успехи в переводе. Например, бейдж Специалист можно получить, выполнив перевод определенной части курса (1, 7,… 60, 100 %%). Это многоуровневая ачивка — чем больше переводишь, тем выше уровень. Есть и одноуровневые: ачивка после вручения обмену не подлежит.
Coursera по русски: про достижения и награды
Полный список:

Coursera по русски: про достижения и награды Номер Один — чтобы получить эту почётную награду, нужно хотя бы раз занять первое место.
Coursera по русски: про достижения и награды Лидер — ачивка зарабатывается с попаданием в первую десятку переводчиков.
Coursera по русски: про достижения и награды Эксперт — выдаётся за активность в голосовании: чем больше вы оцениваете работы других участников, тем выше навыки эксперта, а значит и ценнее награда. Эксперт первого уровня выдаётся за оценку пяти переводов.
Coursera по русски: про достижения и награды Специалист — награда в несколько уровней, которую можно получить за перевод определенного процента курса. Для первого уровня достаточно перевести 1 % от всего курса — а это больше, как кажется. За каждый курс можно получить своего Специалиста.
Coursera по русски: про достижения и награды Финалист — ачивку зарабатывают переводчики, чьи работы стали лучшими и выбраны для размещения на Coursera. Первый уровень можно получить за один перевод, вошедший в финальный вариант субтитров для сайта Coursera.

Технически выдача награды сводится к двум действиям программы:

  1. Расчёт для пользователя некоторой величины — «прогресса» по ачивке;
  2. При достижении некоторых пороговых значений «прогресса» происходит выдача награды или повышение её уровня.

Награда должна быть видна:

  1. Сразу после её получения в рабочем окне приложения, где выполняется перевод;
  2. Все полученные награды отображаются в профиле пользователя;
  3. И все они видны в рейтинге участников.

Важно помнить, что величина, на основе которой присуждается очередной уровень ачивки, со временем может как увеличиваться, так и уменьшаться. К примеру, награда Лидер выдаётся за попадание в первую десятку рейтинга, и очевидно, что, однажды попав в топ-10, можно со временем выбыть. Мы исходим из того, что однажды полученная награда или её уровень закрепляются за пользователем и могут меняться только в сторону увеличения.

Решение
Что делает система наград большую часть времени? Периодически пересчитывает величину прогресса по ачивке и при превышении некоторого порога генерирует событие выдачи новой ачивки или её нового уровня.

Наши разработчики решили, что лучше всего разместить код, ответственный за расчёт прогресса, поближе к сборкам ачивок и подальше от сборок, ответственных за предметную область. Что они и сделали.

В итоге код, ответственный за ачивки, разместился в двух сборках:

  1. Achieve.Module — содержит логику, не зависящую от предметной области, конкретной награды и условий её присвоения. Эта сборка может быть использована «как есть» в любом другом проекте.
  2. Achieve.Configuration.Module — содержит информацию об условиях присвоения ачивок и алгоритмах расчёта прогресса. Локализация алгоритмов расчёта в данной сборке позволила минимизировать изменения сборок предметной области из-за внедрения ачивок. В частности, доработка сборок предметной области свелась к объявлению публичных интерфейсов обработчиков событий, например, IRatingUpdatedHandler, и вызову их методов в соответствующих местах.

Если чуть подробнее, то:

  1. Сборка предметной области объявляет публичный интерфейс обработчика события ISomeEventHandler и внедряет зависимость от
    ICollection<ISomeEventHandler>

    в сервисы, которые должны порождать данное событие;

  2. Сборка Achieve.Configuration.Module ссылается на сборку предметной области и реализует ISomeEventHandler нужное количество раз, регистрируя действия в контейнере Dependency Injection;
  3. Сборка предметной области вызывает ISomeEventHandler.Handle(some event params) после сохранения изменений в базе данных;
  4. ISomeEventHandler.Handle(), реализованный в Achieve.Configuration.Module, использует любые службы предметной области, чтобы прочесть информацию, необходимую для пересчёта прогресса;
  5. ISomeEventHandler.Handle() рассчитывает прогресс и, если необходимо, генерирует события присвоения новых наград или новых уровней.

Другими словами, расчёт прогресса отделяется от сборок предметной области с помощью механизма событий. А чтобы избежать проблем, связанных с «обычными» событиями .net, они реализованы на основе сочетания интерфейсов и Dependency Injection.

Например, код модуля, который регистрирует в DI контейнере все классы, необходимые для расчёта награды Финалист:

using System.Collections.Generic;
using AbbyyLS.SmartCAT.BL.Crowd.Interfaces;
using Ninject;
using Ninject.Activation;
using Ninject.Modules;

namespace AbbyyLS.Achieve
{
	class AchieveFinalistModule : NinjectModule
	{
		public override void Load()
		{
			Kernel.MultiBind<AchievmentConfig>()
				.ToMethodSafe(CreateFinalistAchievmentConfig);

			Kernel.MultiBind<IInitialAchieveProgressCalculator>()
				.To<ConfirmedVariantsCountCalculator>();

			Kernel.MultiBind<IConfirmedTranslationVariantHandler>()
				.To<ConfirmedTranslationVariantHandler>();
		}

		private AchievmentConfig CreateFinalistAchievmentConfig(IContext context)
		{
			var result = new AchievmentConfig
			{
				AchievmentName = AchievmentNames.Finalist,

				LevelSteps = new List<Dictionary<string, int>>
				{
					new Dictionary<string, int>{{ProgressNames.ConfirmedVariantsCount, 1}},
					new Dictionary<string, int>{{ProgressNames.ConfirmedVariantsCount, 5}},
					new Dictionary<string, int>{{ProgressNames.ConfirmedVariantsCount, 25}},
					new Dictionary<string, int>{{ProgressNames.ConfirmedVariantsCount, 75}},
					new Dictionary<string, int>{{ProgressNames.ConfirmedVariantsCount, 150}},
				},
				ProgressSteps = new List<Dictionary<string, int>>(),
				Ventures = new[] { KnownVentures.Coursera }
			};

			return result;
		}
	}
}

Итак, мы знаем, что при некоторых действиях пользователя происходит пересчёт прогресса и генерация событий присвоения наград или повышения их уровня. Что должно было произойти при первом запуске системы после внедрения ачивок? Очевидно, что требовалось рассчитать прогресс каждого пользователя без всяких событий.

Это можно было бы сделать с помощью методов, которые вызываются в обработчиках событий, влияющих на ачивки. Но такие методы заточены под расчёт прогресса для одного пользователя и могут использовать некоторые данные, передаваемые в метод ISomeEventHandler.Handle(...) со стороны сборки предметной области, которые нельзя получить при старте приложения. Например, ISegmentTranslatedHandler.Handle(userId, segmentId). То есть нужно отдельно от алгоритмов пересчёта прогресса реализовать алгоритм его первоначальной калькуляции. В нашем случае в сборке Achieve.Module мы объявили интерфейс IInitialProgressCalculator, действия которого реализованы и зарегистрированы в DI-контейнере в сборке Achieve.Configuration.Module

Конкурентная инициализация
Один из самых неожиданных сюрпризов на этапе тестирования был связан как раз с первоначальным расчётом прогресса. Мы не учли, что его инициализация может происходить одновременно на нескольких серверах нашего приложения. Соответственно, если пользователь получал награду или её уровень ещё на этапе исходного расчёта прогресса, то уведомление об этом отправлялось сразу со всех серверов приложения. Проблему решили с помощью эксклюзивной блокировки на первоначальный расчёт через базу данных: сервер, которому не доставался эксклюзивный доступ на исходный расчёт, пропускал его.

Многократный пересчёт прогресса при обработке событий
На этапе тестирования выяснилась ещё одна интересная история: оказалось, что событие, требующее одного и того же расчёта, может срабатывать массово. Очевидно, что выполнять одновременно и многократно обработчик события с идентичными параметрами — это не лучшая тактика. Поэтому мы снова воспользовались механизмом эксклюзивной блокировки через базу данных — в этот раз на выполнение обработчика события с конкретными значениями параметров события. Блокировку внедрили не везде, а только там, где зафиксировали проблему.

Push-уведомления и периодический опрос
Информация о заработанных наградах сразу отображается в рабочем окне приложения благодаря SignalR — системе push-уведомлений веб-клиента со стороны сервера. Так как мы уже использовали его для отправки других уведомлений, добавить события ачивок было нетрудно. Безусловно, оповещение о получении очередной награды нужно показывать на всех страницах. Однако часть из них — статические веб-страницы, и внедрять туда использование SignalR было бы слишком накладно и поэтому нецелесообразно. Поэтому мы решили использовать для этого периодический опрос сервера: он проверяет, появились ли новые ачивки или их уровни с момента последнего запроса, и только потом даёт команду системе уведомлений.

Хранение данных
Achieve.Module хранит следующие данные:

  1. Конфигурацию ачивок — пороговые значения прогресса, при которых происходит присвоение награды участнику;
  2. Текущее значение прогресса по всем ачивкам;
  3. Максимальный уровень, достигнутый пользователем по каждой награде;
  4. Не просмотренные события присвоения ачивки.

Локализация текстов
Мы — лингвистическая компания, а потому не могли обойти стороной этот вопрос. При построении текстов, которые видит участник вместе с получением награды, возникают нюансы, связанные с использованием числительных в русском языке: 1 перевод, 2 перевода, 5 переводов, 11 переводов, 21 перевод. (Как мы знаем, в английском языке всё гораздо проще и сводится к различию единственного числа и множественного: 1 translation, 2 translations.)

Поэтому мы просто ввели разные текстовые шаблоны для разных числительных:

  • 1, 21, xxx1: перевод;
  • 2, 22, xxx2: перевода (то же для 3 и 4);
  • все остальные числа, включая 5-20: переводов.

Coursera по русски: про достижения и награды
Пример того, как выглядит шаблон для локализации текста ачивки

Метод для автотестов
Поскольку наиболее важная часть логики ачивок — первоначальный расчёт прогресса — выполняется однажды, то при написании тестов (особенно интеграционных) возникает потребность в методе API, который позволит воспроизводить это действие многократно. Для этого наши разработчики сделали метод Recalculate(), который вызывает такой же пересчёт прогресса, как и при первом запуске приложения после внедрения системы наград.

Пример кода обработчика события, вызывающего пересчёт прогресса:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AbbyyLS.Core.Helpers;
using AbbyyLS.SmartCAT.BL.Crowd;
using Ninject;
using NLog;

namespace AbbyyLS.Achieve
{
	class VotedVariantCountChangedHandler : ITranslationVotedHandler
	{
		private static readonly Logger Log = LogManager.GetCurrentClassLogger();

		[Inject]
		public IAchieveService AchieveService { private get; set; }

		private readonly ITranslationVariantRepository _translationVariantRepository;

		public VotedVariantCountChangedHandler(ITranslationVariantRepository translationVariantRepository)
		{
			_translationVariantRepository = translationVariantRepository;
		}

		public void TranslationVoted(Guid accountId, Guid userId, Guid variantId)
		{
			Task.Run(() => translationVotedTask(accountId, userId));
		}

		private void translationVotedTask(Guid accountId, Guid userId)
		{
			Log.Debug("Achieve event VotedVariantCountChangedHandler.TranslationVoted start");

			try
			{
				using (new TimerLog(t => Log.Trace("Achieve event VotedVariantCountChangedHandler.TranslationVoted complete in {0} ms", t)))
				{
					var votedSegmentCount = 
						(int) _translationVariantRepository.GetVotedTranslationsCount(userId, accountId);
					
					AchieveService.SetProgress(accountId, userId, new Dictionary<string, int>
					{
						{ProgressNames.VotedVariantCount, votedSegmentCount}
					});
				}
			}
			catch (Exception ex)
			{
				Log.ErrorException(string.Format("VotedVariantCountChangedHandler.TranslationVotedTask accountId : {0} userId: {1}", accountId, userId), ex);
			}
		}
	}
}

Собственно, на сегодня это всё. Чтобы посмотреть на практике, как это работает, присоединяйтесь к переводу Coursera на русский вот здесь — будем рады вас видеть. А все вопросы, пожелания и предложения присылайте на coursera@abbyy-ls.com или отставляйте здесь в комментариях.

Автор: denisfrolov

Источник

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


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