読者です 読者をやめる 読者になる 読者になる

しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Xamarin.Forms で Microsoft Translator API を使って翻訳したい

Android C# Windows Phone Windows ランタイムアプリ Xamarin XAML

とりあえず英文の読み上げはできていますがわからないときに和訳を知りたい・・・

というわけで、翻訳サービス Microsoft Translator API を利用して記事のダイジェストを和訳して読み上げるようにしてみたいと思います

Google 翻訳 API は有償にも関わらず、Microsoft Translator API は無料で1ヶ月 2,000,000 字まで翻訳できるという便利なサービスです!

まずは下記のページから Microsoft Translator API のサービスプラットフォームである Microsoft Azure Marketplace にサインアップします

https://datamarket.azure.com/register?redirect=%2Faccount


f:id:matatabi_ux:20150822221302p:plain

必要事項を入力してそのままサインインするので、今度は下記のページから Microsoft Translator API 利用開始のサインアップを行います

プライバシーポリシーの同意チェックを忘れずに

f:id:matatabi_ux:20150822221759p:plain

サインアップが終わったら API を利用するアプリを下記のページから追加

https://datamarket.azure.com/developer/applications/register

クライアントID もシークレットコードも自分で指定することもできるようです

f:id:matatabi_ux:20150822222117p:plain

リダイレクト Uri は今回 WebView の出番がないので不要なのですが、facebook の例の Uri を入力しておくことにしました

ここまで終わったら API の利用準備は整ったのでコード修正に着手します

まずは翻訳サービスクラス・・・インタフェースも実装してますが今回はプラットフォーム依存コードがないので無理して用意する必要はないです

/// <summary>
/// Translate service class
/// </summary>
public class TranslateService : ITranslateService
{
    /// <summary>
    /// Client ID
    /// </summary>
    private static readonly string ClientId = @"{Input client ID}";

    /// <summary>
    /// Client Secret
    /// </summary>
    private static readonly string ClientSecret = @"{Input client secret}";

    /// <summary>
    /// OAuth authentication endpoint uri
    /// </summary>
    private static readonly string OAuthUri = @"https://datamarket.accesscontrol.windows.net/v2/OAuth2-13";

    /// <summary>
    /// Translate service api endpoint uri
    /// </summary>
    private static readonly string TranslateUri = @"http://api.microsofttranslator.com/v2/Http.svc/Translate?text={0}&from=en&to=ja";

    /// <summary>
    /// Expiration datetime translate api access token
    /// </summary>
    private DateTime accessTokenExpires = DateTime.MinValue;

    /// <summary>
    /// Translate api access token
    /// </summary>
    private string accessToken = string.Empty;

    /// <summary>
    /// Initialize translator service
    /// </summary>
    public async Task InitializeAsync()
    {
        var client = new HttpClient();
        var content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            {"client_id", ClientId},
            {"client_secret", ClientSecret},
            {"grant_type", @"client_credentials" },
            {"scope", @"http://api.microsofttranslator.com" },
        });
        var result = await client.PostAsync(OAuthUri, content);

        var json = await result.Content.ReadAsStringAsync();

        var response = JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JContainer>(json);
        var now = DateTime.Now;

        Debug.WriteLine(response);

        this.accessToken = response.Value<string>("access_token");
        var expiresIn = response.Value<long>("expires_in");
        this.accessTokenExpires = now.AddSeconds(expiresIn);
    }

    /// <summary>
    /// Translate to english from japanese
    /// </summary>
    /// <param name="english">english sentence</param>
    /// <returns>japanese sentence</returns>
    public async Task<string> Translate(string english)
    {
        if (string.IsNullOrEmpty(this.accessToken)
            || accessTokenExpires.CompareTo(DateTime.Now) < 0)
        {
            await this.InitializeAsync();
        }

        var client = new HttpClient();
        client.DefaultRequestHeaders.Add("Authorization", string.Format("Bearer {0}", this.accessToken));
        var xml = await client.GetStringAsync(string.Format(TranslateUri, WebUtility.UrlEncode(english)));

        string japanese = null;
        using (var stream = new MemoryStream(Encoding.Unicode.GetBytes(xml)))
        {
            var desirializer = new DataContractSerializer(typeof(string));
            japanese = desirializer.ReadObject(stream) as string;
        }

        return japanese;
    }
}

Microsoft Translator API は OAuth でアクセストークンを発行して利用するタイプのようなので、上記のように InitilizeAsync で初期化処理としてアクセストークンを取得するようにしました

アクセストークンは 10 分の有効期限が設定されるようなので、翻訳要求時に有効期限が切れていたら再取得しています

仕組みは比較的単純なので上記のようにわりとかんたんに利用できます

さらにこのサービスを利用できるように App.cs に DI コンテナ用の記述を追加

/// <summary>
/// Application class
/// </summary>
public class App : Application
{
    /// <summary>
    /// Dependency injection container
    /// </summary>
    public IUnityContainer Container = new UnityContainer();

    /// <summary>
    /// Costructor
    /// </summary>
    public App()
    {
        this.Container.RegisterType<INewsFeedService, NewsFeedService>(new ContainerControlledLifetimeManager());
        this.Container.RegisterType<ITranslateService, TranslateService>(new ContainerControlledLifetimeManager());

        this.MainPage = new TopPage();
    }
}

次に XAML に翻訳機能を利用するための UI を追加します

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamarinReader.Views.TopPage"
             x:Name="root"
             BindingContext="{Binding Path=ViewModel, Source={x:Reference Name=root}}">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0,20,0,0"/>
    </ContentPage.Padding>
    <StackLayout Orientation="Vertical"
                 Spacing="10">
        <StackLayout Orientation="Horizontal"
                     Padding="15,5"
                     BackgroundColor="#33ffffff"
                     Spacing="5">
            <Label Text="TechCrunch"
                   TextColor="Lime"
                   FontSize="Medium"/>
            <Label Text="Reader"
                   FontSize="Medium"/>
        </StackLayout>
        <ListView ItemsSource="{Binding Items}"
                  SeparatorColor="Transparent"
                  HasUnevenRows="True"
                  IsRefreshing="{Binding IsRefresing}"
                  IsPullToRefreshEnabled="True"
                  ItemSelected="OnListItemSelected"
                  RefreshCommand="{Binding RefreshCommand}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <ViewCell.ContextActions>
                            <MenuItem Text="Translate"
                                      Command="{Binding TranslateCommand}"
                                      CommandParameter="{Binding}"/>
                            <MenuItem Text="Read more"
                                      Command="{Binding LaunchLinkUriCommand}"
                                      CommandParameter="{Binding}"/>
                        </ViewCell.ContextActions>
                        
                        <StackLayout Orientation="Vertical"
                                     Spacing="0"
                                     Padding="15,10">
                            <Image Source="{Binding Thumbnail}"
                                   HeightRequest="150"
                                   VerticalOptions="StartAndExpand"
                                   Aspect="AspectFill"/>
                            <StackLayout Orientation="Vertical"
                                         Padding="10"
                                         BackgroundColor="#66000000"
                                         Spacing="10">
                                <Label Text="{Binding Title}"
                                       TextColor="Accent"
                                       FontSize="Medium"/>
                                <Label Text="{Binding Description}"
                                       TextColor="Default"
                                       FontSize="Small"/>
                            </StackLayout>
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>

といっても ViewCell.ContextActions に Translate という MenuItem を追加しただけ!

                        <ViewCell.ContextActions>
                            <MenuItem Text="Translate"
                                      Command="{Binding TranslateCommand}"
                                      CommandParameter="{Binding}"/>
                            <MenuItem Text="Read more"
                                      Command="{Binding LaunchLinkUriCommand}"
                                      CommandParameter="{Binding}"/>
                        </ViewCell.ContextActions>

最後にコードビハインドを修正

/// <summary>
/// Top page
/// </summary>
public partial class TopPage : ContentPage
{
    ~ 中略 ~

    /// <summary>
    /// Translate service
    /// </summary>
    private ITranslateService translate = null;

    /// <summary>
    /// ViewModel
    /// </summary>
    public TopPageViewModel ViewModel { get; private set; }

    /// <summary>
    /// Constructir
    /// </summary>
    public TopPage()
    {
        this.ViewModel = new TopPageViewModel();
        this.ViewModel.RefreshCommand = DelegateCommand.FromAsyncHandler(this.Refresh);
        this.newsFeed = ((App)App.Current).Container.Resolve<INewsFeedService>();
        this.translate = ((App)App.Current).Container.Resolve<ITranslateService>();

        InitializeComponent();
    }

    /// <summary>
    /// Appearing event handler
    /// </summary>
    protected override async void OnAppearing()
    {
        base.OnAppearing();

        this.speech = ((App)App.Current).Container.Resolve<ITextSpeechService>();
        await this.translate.InitializeAsync();
        await this.Refresh();
    }

    /// <summary>
    /// Refresh news feed items
    /// </summary>
    /// <returns>Task</returns>
    private async Task Refresh()
    {
        this.ViewModel.IsRefresing = true;

        await this.newsFeed.Update();

        // Add items on UI thread
        foreach (var item in this.newsFeed.Feed.Channel.Items.OrderBy(i => i.PubDate))
        {
            var vm = new NewsItemViewModel
            {
                UniqueId = item.Guid,
                Categories = new ObservableCollection<string>(item.Categories),
                Title = item.Title,
                Link = item.Link,
                LastUpdated = item.PubDate,
                Description = ReadMoreRegex.Replace(
                                WebUtility.HtmlDecode(
                                HtmlTagRegex.Replace(item.Description, string.Empty)),
                                string.Empty),
                LaunchLinkUriCommand = new DelegateCommand<NewsItemViewModel>(this.LaunchLinkUri),
                TranslateCommand = DelegateCommand<NewsItemViewModel>.FromAsyncHandler(this.Translate),
            };
            var imgMatch = ImgTagRegex.Match(item.Description);
            if (imgMatch.Success)
            {
                vm.Thumbnail = ImageSource.FromUri(new Uri(imgMatch.Groups["uri"].Value));
            }

            var oldItem = this.ViewModel.Items.FirstOrDefault(i => i.UniqueId.Equals(vm.UniqueId));
            if (oldItem != null)
            {
                if (oldItem.LastUpdated.CompareTo(vm.LastUpdated) >= 0)
                {
                    // no update
                    continue;
                }

                // updated
                oldItem.Categories = vm.Categories;
                oldItem.Title = vm.Title;
                oldItem.Link = vm.Link;
                oldItem.LastUpdated = vm.LastUpdated;
                oldItem.Description = vm.Description;
                oldItem.Thumbnail = vm.Thumbnail;
                continue;
            }

            // new item
            this.ViewModel.Items.Insert(0, vm);
        }

        this.ViewModel.IsRefresing = false;
    }

    ~ 中略 ~

    /// <summary>
    /// Translate to english from japanese and speak
    /// </summary>
    /// <param name="item">Select item</param>
    /// <returns>Task</returns>
    private async Task Translate(NewsItemViewModel item)
    {
        var japanse = await this.translate.Translate(string.Format("{0}: {1}", item.Title, item.Description));
        this.speech.SetLanguage("Japanese");
        this.speech.Speak(japanse);
    }
}

TranslateService の初期化処理を追加したり、ViewModel.TranslateCommand に和訳して読み上げる処理を設定したりしています

さておたちあい!

Androidエミュレータ上で日本語の読み上げ機能をインストールできなかったので Windows Phone で試してみました

まぁ翻訳結果は難ありでしたが動作としては思い通りにいきました