Que es el Microsoft Translator API
El servicio de Traducción de Azure permite, entre otras cosas, traducir textos de un idioma a otro automáticamente, permitiendo integrar textos en múltiples idiomas desde cualquier tipo de aplicación. El servicio forma parte del conjunto de Cognitive Services de Azure, que es una colección de algoritmos de Inteligencia Artificial que se pueden utilizar en la nube de Microsoft.
El servicio de Traducción ha utilizado desde su principio un algoritmo de Traducción Estadística Mecánica (SMT, Statistical Machine Translation), pero como esta plataforma ya ha alcanzado su máximo desarrollo y no es posible mejorarla, Microsoft ha comenzado a utilizar otro tipo de algoritmo basado en Inteligencia Artificial llamado DNN, Deep Neural Networks. El cambio entre los dos algoritmos se esta haciendo incrementalmente, pero los usuarios solamente notaran las mejoras en cuanto a la calidad de las traducciones pues la API de comunicación no cambia. DNN ofrece mejores traducciones porque utiliza el contexto de toda la frase para crear el texto traducido, no solamente el contexto de algunas palabras antes y después de la palabra a traducir.
El Azure Translator API es una herramienta ideal para utilizar en SharePoint: una tarea recurrente en la creación de Portales es poder ofrecer la misma información en varios idiomas simultáneamente. Aunque en este momento la calidad de las traducciones no es totalmente perfecta (y probablemente nunca lo será), el nivel es suficientemente alto como para poder reducir considerablemente los costos de traducción manual ya que los traductores solamente necesitan revisar los textos, no traducirlos desde cero. La calidad de las traducciones varia dependiendo de los idiomas: traducir de un idioma cualquiera al inglés tiene generalmente mucha mas calidad que traducir desde el inglés a otro idioma. También se puede observar que la calidad depende del idioma: lenguajes muy utilizados como el español y francés producen resultados de traducción superiores a idiomas menos usados. En este momento el servicio de traducción soporta 62 idiomas, incluyendo dos dialectos de Klingon, Yucatec Maya y Hmong Daw. Microsoft trabaja constantemente para aumentar el numero de idiomas, pero hay que tener en cuenta que se necesita aproximadamente un millón de palabras en textos originales para entrenar los algoritmos de IA, lo que no facilita el trabajo.
Configuración del Azure Translator API
Para utilizar el Translator API es necesario crear primero el servicio en Azure:
Entre al portal de manejo de Azure ( https://portal.azure.com ) utilizando sus credenciales.
Vaya a la sección de "Resource Groups" y cree un nuevo Grupo de Recursos (también es posible reutilizar un grupo ya existente).
Cree un servicio de "Translator Text API":
· En el Resource Group, utilice el botón de "+Add" para crear un recurso, busque por "translator" en la casilla de búsqueda y seleccione "Translator Text API" en los resultados.
· Asígnele un nombre al servicio y utilice el Grupo de Recursos deseado. En la casilla de "Pricing tier" seleccione un servicio dependiendo de la cantidad de traducciones a esperar por mes, lo que determina el precio del servicio (gratis si se utilizan menos de 2.000.000 de caracteres por mes). Acepte el anuncio de privacidad que aparece en la configuración (Microsoft utilizara los datos enviados para mejorar automáticamente los algoritmos de traducción).
4 . Una vez creado el servicio, haga clic sobre su nombre en la lista de recursos del Resource Group, vaya a "Keys" y copie el valor de "Key 1".
Utilizando el Azure Translation API con SharePoint
En el siguiente ejemplo se van a utilizar dos Listas de Anuncios de SharePoint en donde el campo de "Body" de un elemento nuevo creado en la Lista en Español será traducido al inglés y utilizado para crear otro anuncio en la Lista de Anuncios en inglés. Cuando se introduce un nuevo elemento en la Lista de Anuncios en español, un WebHook hace que una Función de Azure comience a funcionar, utilice el texto del "Body" para hacer una llamada al Azure Translation API y cree el elemento en la Lista en Ingles.
Nota: la creación y configuración de una Función de Azure se puede encontrar el en artículo "SharePoint y Azure – Azure Functions" ( http://www.compartimoss.com/revistas/numero-30/sharepoint-y-azure-azure-functions ). La configuración y utilización de WebHooks de SharePoint se puede encontrar en el artículo "Eventos sobre SharePoint Online con Webhooks" ( http://www.compartimoss.com/revistas/numero-32/eventos-sobre-sharepoint-online-con-webhooks ).
5 . Cree una cuenta de Funciones básica en el Grupo de Recursos, asignándole un nombre, Plan de Servicios y cuenta de Azure Storage.
6 . Utilizando Visual Studio 2017 (o Visual Studio 2016 con el AddIn para programar Funciones de Azure), cree una nueva solución del tipo "Azure Function". Una vez creada la solución, agréguele una Función del tipo "Http Trigger" con derechos de acceso anónimos.
7 . Agregue a la solución los paquetes NuGet, "AppForSharePointOnlineWebToolkit" y "Newtonsoft.Json".
8 . Reemplace toda la rutina "Run" con el siguiente código:
1[FunctionName("TranslationFunction")]23public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]HttpRequestMessage req, TraceWriter log)45{67// Registration89string validationToken = req.GetQueryNameValuePairs()1011.FirstOrDefault(q => string.Compare(q.Key, "validationtoken", true) == 0)1213.Value;1415if (validationToken != null)1617{1819var myResponse = req.CreateResponse(HttpStatusCode.OK);2021myResponse.Content = new StringContent(validationToken);2223return myResponse;2425}26272829// Changes3031var myContent = await req.Content.ReadAsStringAsync();3233var allNotifications = JsonConvert.DeserializeObject<ResponseModel<NotificationModel>>(myContent).Value;34353637if (allNotifications.Count > 0)3839{4041foreach (var oneNotification in allNotifications)4243{4445// Login in SharePoint4647string baseUrl = "https://gustavo.sharepoint.com";4849string myUserName = "gustavo@gustavo.onmicrosoft.com";5051string myPassword = ConfigurationManager.AppSettings["gustavoPW"];52535455SecureString securePassword = new SecureString();5657foreach (char oneChar in myPassword) securePassword.AppendChar(oneChar);5859SharePointOnlineCredentials myCredentials = new SharePointOnlineCredentials(myUserName, securePassword);60616263ClientContext SPClientContext = new ClientContext(baseUrl + oneNotification.SiteUrl);6465SPClientContext.Credentials = myCredentials;66676869// Get the Changes7071GetChanges(SPClientContext, oneNotification.Resource, log);7273}7475}76777879return new HttpResponseMessage(HttpStatusCode.OK);8081}82
Esta rutina primero se encarga de hacer el registro del WebHook (si la consulta contiene un parámetro "validationtoken" en el Query String). Después de registrado el WebHook, cada consulta es procesada para extraer las notificaciones que contiene. En cada notificación de la colección de notificaciones se hace un logeo en SharePoint para obtener los cambios detectados en la Lista (por medio de la rutina "GetChanges").
9 . La rutina "GetChanges" recibe el contexto de SharePoint y el identificador de la Lista, y tiene la forma:
1static void GetChanges(ClientContext SPClientContext, string ListId, TraceWriter log)23{45// Get the List67Web spWeb = SPClientContext.Site.RootWeb;89List listaAnunciosES = spWeb.Lists.GetByTitle("AnnouncementsES");1011SPClientContext.Load(listaAnunciosES);1213List listaAnunciosEN = spWeb.Lists.GetByTitle("AnnouncementsEN");1415SPClientContext.Load(listaAnunciosEN);1617SPClientContext.ExecuteQuery();18192021// Create the ChangeToken and Change Query2223ChangeToken lastChangeToken = new ChangeToken();2425lastChangeToken.StringValue = string.Format("1;3;{0};{1};-1", ListId, DateTime.Now.AddMinutes(-1).ToUniversalTime().Ticks.ToString());2627ChangeToken newChangeToken = new ChangeToken();2829newChangeToken.StringValue = string.Format("1;3;{0};{1};-1", ListId, DateTime.Now.ToUniversalTime().Ticks.ToString());3031ChangeQuery myChangeQuery = new ChangeQuery(false, false);3233myChangeQuery.Item = true; // Get only Item changes3435myChangeQuery.Add = true; // Get only the new Items3637myChangeQuery.ChangeTokenStart = lastChangeToken;3839myChangeQuery.ChangeTokenEnd = newChangeToken;40414243// Get all the Changes4445var allChanges = listaAnunciosES.GetChanges(myChangeQuery);4647SPClientContext.Load(allChanges);4849SPClientContext.ExecuteQuery();50515253foreach (Change oneChange in allChanges)5455{5657if (oneChange is ChangeItem)5859{6061// Get what is changed6263ListItem anuncioES = listaAnunciosES.GetItemById((oneChange as ChangeItem).ItemId);6465SPClientContext.Load(anuncioES);6667SPClientContext.ExecuteQuery();68697071string textoParaTraducir = anuncioES["Body"].ToString().Replace("\"", "'");7273string lenguajeDe = "es";7475string lenguajeA = "en";7677string apiKey = ConfigurationManager.AppSettings["apiKey"];7879string textoTraducido = getTraduccion(textoParaTraducir, lenguajeDe, lenguajeA, apiKey);80818283// Insert the values in the EN List8485ListItemCreationInformation newAnuncioENInfo = new ListItemCreationInformation();8687ListItem anuncioEN = listaAnunciosEN.AddItem(newAnuncioENInfo);8889anuncioEN["Title"] = anuncioES["Title"];9091anuncioEN["Body"] = textoTraducido;9293anuncioEN.Update();9495SPClientContext.ExecuteQuery();9697}9899}100101}102
Primero se crean dos objetos que contienen las Listas de Anuncios de SharePoint. Luego se crea una consulta de cambio (variable "myChangeQuery") que especifica que se requieren los cambios ocurridos en el ultimo minuto, que ocurren en elementos de la Lista y que sean del tipo "Add", es decir, elementos nuevos. Luego de ejecutar la consulta, se examina cada uno de los cambios y se obtiene un objeto con el Anuncio agregado (el que contiene, a su vez, el texto del "Body").
En la misma rutina se llama a la rutina "getTraduccion", la que se encarga de hacer la consulta en Azure para traducir el texto, utilizando como parámetros de entrada el texto a traducir, el idioma de origen, el idioma de destino y la llave obtenida en el punto 4. Cuando ya se ha obtenido el texto traducido, se crea un nuevo elemento en la Lista de Anuncios en inglés.
10 . La rutina "getTraduccion" recibe como parámetros de entrada el texto a traducir, los idiomas de origen y de destino y la clave del servicio de Traducción:
1static string getTraduccion(string TextoParaTraducir, string LenguajeDe, string LenguajeA, string ApiKey)23{45string txtTraducido = string.Empty;6789AzureAuthToken authTokenSource = new AzureAuthToken(ApiKey.Trim());1011string authToken = authTokenSource.GetAccessTokenAsync().Result;12131415string uriTraduccion = "https://api.microsofttranslator.com/v2/Http.svc/Translate?text=" +1617HttpUtility.UrlEncode(TextoParaTraducir) +1819"&from=" + LenguajeDe + "&to=" + LenguajeA;2021HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(uriTraduccion);2223httpWebRequest.Headers.Add("Authorization", authToken);2425using (WebResponse myResponse = httpWebRequest.GetResponse())2627using (Stream stream = myResponse.GetResponseStream())2829{3031DataContractSerializer mySerializer = new DataContractSerializer(Type.GetType("System.String"));3233txtTraducido = (string)mySerializer.ReadObject(stream);3435}36373839return txtTraducido;4041}42
Cada consulta al Translation API se realiza por medio de una llamada REST a un URL pre-especificado del servicio de Traducción, utilizando como parámetros los dos valores de los idiomas y el texto a traducir. En este caso se está utilizando el método "Translate" en la llamada REST, pero existen otros 12 métodos que se pueden utilizar, incluyendo métodos para traducir no un texto solo sino también un array de textos en una sola llamada, detectar el idioma original, enumerar los idiomas que se pueden utilizar y separar palabras en un texto completo.
Esta rutina llama inicialmente el método "GetAccessTokenAsync" de la clase "AzureAuthToken" para obtener el token de acceso al servicio de traducción utilizando la llave obtenida al momento de configuración descrito en el punto 4. Esta clase tiene un método sincrónico y otro asincrónico para obtener el token, pero Microsoft especifica que es preferible utilizar el método asincrónico. La clase ha sido creada por Microsoft, se puede encontrar en la información de Azure y tiene la forma:
1using System;23using System.Net;45using System.Net.Http;67using System.Threading.Tasks;891011namespace TranslationFunction1213{1415/// <summary>1617/// Client to call Cognitive Services Azure Auth Token service in order to get an access token.1819/// Exposes asynchronous as well as synchronous methods.2021/// </summary>2223public class AzureAuthToken2425{2627/// URL of the token service2829private static readonly Uri ServiceUrl = new Uri("https://api.cognitive.microsoft.com/sts/v1.0/issueToken");30313233/// Name of header used to pass the subscription key to the token service3435private const string OcpApimSubscriptionKeyHeader = "Ocp-Apim-Subscription-Key";36373839/// After obtaining a valid token, this class will cache it for this duration.4041/// Use a duration of 5 minutes, which is less than the actual token lifetime of 10 minutes.4243private static readonly TimeSpan TokenCacheDuration = new TimeSpan(0, 5, 0);44454647/// Cache the value of the last valid token obtained from the token service.4849private string \_storedTokenValue = string.Empty;50515253/// When the last valid token was obtained.5455private DateTime \_storedTokenTime = DateTime.MinValue;56575859/// Gets the subscription key.6061public string SubscriptionKey { get; }62636465/// Gets the HTTP status code for the most recent request to the token service.6667public HttpStatusCode RequestStatusCode { get; private set; }68697071/// <summary>7273/// Creates a client to obtain an access token.7475/// </summary>7677/// <param name="key">Subscription key to use to get an authentication token.</param>7879public AzureAuthToken(string key)8081{8283if (string.IsNullOrEmpty(key))8485{8687throw new ArgumentNullException(nameof(key), "A subscription key is required");8889}90919293this.SubscriptionKey = key;9495this.RequestStatusCode = HttpStatusCode.InternalServerError;9697}9899100101/// <summary>102103/// Gets a token for the specified subscription.104105/// </summary>106107/// <returns>The encoded JWT token prefixed with the string "Bearer ".</returns>108109/// <remarks>110111/// This method uses a cache to limit the number of request to the token service.112113/// A fresh token can be re-used during its lifetime of 10 minutes. After a successful114115/// request to the token service, this method caches the access token. Subsequent116117/// invocations of the method return the cached token for the next 5 minutes. After118119/// 5 minutes, a new token is fetched from the token service and the cache is updated.120121/// </remarks>122123public async Task<string> GetAccessTokenAsync()124125{126127if (string.IsNullOrWhiteSpace(this.SubscriptionKey))128129{130131return string.Empty;132133}134135136137// Re-use the cached token if there is one.138139if ((DateTime.Now - \_storedTokenTime) < TokenCacheDuration)140141{142143return \_storedTokenValue;144145}146147148149using (var client = new HttpClient())150151using (var request = new HttpRequestMessage())152153{154155request.Method = HttpMethod.Post;156157request.RequestUri = ServiceUrl;158159request.Content = new StringContent(string.Empty);160161request.Headers.TryAddWithoutValidation(OcpApimSubscriptionKeyHeader, this.SubscriptionKey);162163client.Timeout = TimeSpan.FromSeconds(2);164165var response = await client.SendAsync(request);166167this.RequestStatusCode = response.StatusCode;168169response.EnsureSuccessStatusCode();170171var token = await response.Content.ReadAsStringAsync();172173\_storedTokenTime = DateTime.Now;174175\_storedTokenValue = "Bearer " + token;176177return \_storedTokenValue;178179}180181}182183184185/// <summary>186187/// Gets a token for the specified subscription. Synchronous version.188189/// Use of async version preferred190191/// </summary>192193/// <returns>The encoded JWT token prefixed with the string "Bearer ".</returns>194195/// <remarks>196197/// This method uses a cache to limit the number of request to the token service.198199/// A fresh token can be re-used during its lifetime of 10 minutes. After a successful200201/// request to the token service, this method caches the access token. Subsequent202203/// invocations of the method return the cached token for the next 5 minutes. After204205/// 5 minutes, a new token is fetched from the token service and the cache is updated.206207/// </remarks>208209public string GetAccessToken()210211{212213// Re-use the cached token if there is one.214215if ((DateTime.Now - \_storedTokenTime) < TokenCacheDuration)216217{218219return \_storedTokenValue;220221}222223224225string accessToken = null;226227var task = Task.Run(async () =>228229{230231accessToken = await this.GetAccessTokenAsync();232233});234235236237while (!task.IsCompleted)238239{240241System.Threading.Thread.Yield();242243}244245if (task.IsFaulted)246247{248249throw task.Exception;250251}252253if (task.IsCanceled)254255{256257throw new Exception("Timeout obtaining access token.");258259}260261return accessToken;262263}264265266267}268269}27027111 . Otras tres clases definen objetos utilizados por el WebHook:272273public class ResponseModel<T>274275{276277[JsonProperty(PropertyName = "value")]278279public List<T> Value { get; set; }280281}282283284285public class NotificationModel286287{288289[JsonProperty(PropertyName = "subscriptionId")]290291public string SubscriptionId { get; set; }292293294295[JsonProperty(PropertyName = "clientState")]296297public string ClientState { get; set; }298299300301[JsonProperty(PropertyName = "expirationDateTime")]302303public DateTime ExpirationDateTime { get; set; }304305306307[JsonProperty(PropertyName = "resource")]308309public string Resource { get; set; }310311312313[JsonProperty(PropertyName = "tenantId")]314315public string TenantId { get; set; }316317318319[JsonProperty(PropertyName = "siteUrl")]320321public string SiteUrl { get; set; }322323324325[JsonProperty(PropertyName = "webId")]326327public string WebId { get; set; }328329}330331332333public class SubscriptionModel334335{336337[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]338339public string Id { get; set; }340341342343[JsonProperty(PropertyName = "clientState", NullValueHandling = NullValueHandling.Ignore)]344345public string ClientState { get; set; }346347348349[JsonProperty(PropertyName = "expirationDateTime")]350351public DateTime ExpirationDateTime { get; set; }352353354355[JsonProperty(PropertyName = "notificationUrl")]356357public string NotificationUrl { get; set; }358359360361[JsonProperty(PropertyName = "resource", NullValueHandling = NullValueHandling.Ignore)]362363public string Resource { get; set; }364365}366
12 . Registre el WebHook en la Lista de Anuncios en español (utilizando Postman, por ejemplo, como se indica en el artículo mencionado al principio del artículo) y cree un anuncio nuevo en la Lista. El WebHook hará que la Función realice su trabajo, traduzca el texto, y cree un nuevo Anuncio en la Lista de Anuncios en inglés:
Conclusiones
El servicio de Traducción de Texto de Azure puede llegar a ser una herramienta muy valiosa en SharePoint, especialmente en lo que se refiere a la creación de contenido traducido automáticamente a diferentes idiomas. El Azure Translation API es fácil de utilizar desde cualquiera lenguaje de programación, y produce resultados confiables rápida y seguramente. El API utiliza algoritmos de Inteligencia Artificial que se mejoran con el uso, por lo no es necesario crear ni entrenar algoritmos propios.
Gustavo Velez MVP Office Servers and Services gustavo@gavd.net http://www.gavd.net