株式会社ライブキャストロゴ 株式会社ライブキャスト

gTranslatorに韓国語と中国語の読み上げ機能を追加してみた(準備編)の続きです。

年をまたいで、かなり間が空いてしまいましたが、前回は、Windows Liveへのサインアップと、Microsoft® Translator ツールの各種APIコール時に必要なAppIDの作成までが完了しました。それでは、実装の部分に入りたいと思います。

APIについて

韓国語と中国語の読み上げには、Speak Methodを利用しています。
このAPIのURIは

http://api.microsofttranslator.com/V2/Http.svc/Speak

です。
音声にしたい文字列や、それが何国語なのか、などはパラメータで渡します。

  • appId:gTranslatorに韓国語と中国語の読み上げ機能を追加してみた(準備編)で作成したAppIDを指定します。これが正しく指定されていないと、正常な結果は得られません。
  • text:音声に変換したい文章です。gTranslatorでは翻訳結果をここに指定します。
  • language:textパラメータに指定した文章が何国語かを指定します。
  • format:オプションで音声ファイルの形式を指定します。デフォルトではwav形式となります。

3番目のlanguageパラメータには、サポートされている言語を指定します。サポートされている言語は、GetLanguagesForSpeak Methodで取得することができます。

ブラウザで、以下のようにURLを入力してAPIを実行すると、

http://api.microsofttranslator.com/v2/Http.svc/GetLanguagesForSpeak?appId=取得したAppID

以下のような結果が得られました。

<ArrayOfstring>
<string>en</string>
<string>de</string>
<string>es</string>
<string>fr</string>
<string>it</string>
<string>pt</string>
<string>ru</string>
<string>ja</string>
<string>ko</string>
<string>zh-chs</string>
<string>zh-cht</string>
</ArrayOfstring>

韓国語はkoです。中国語は2種類あります。繁体字中国語(Traditional Chinese)と簡体字中国語(Simplified Chinese)です。繁体字中国語はzh-chtで、簡体字中国語はzh-chsになります。

まとめると、、、
以下のように実行します。

http://api.microsofttranslator.com/v2/Http.svc/Speak?appId=取得したAppID&text=%EC%95%88%EB%85%95%ED%95%98%EC%84%B8%EC%9A%94&language=ko

APIコールの実装

翻訳結果読み上げに使うAPIの概要がだいたいわかったところで、そのAPIをコールする部分を実装していきたいと思います。

処理は、大きく分けると以下の2点が中心になります。

  • Speak Methodをコールし、取得したwavファイルを保存する。
  • 保存したwavファイルを再生する。

どちらも、サーバやネットワークの状態、端末の状態によっては処理に時間がかかる可能性がありますので、Threadクラスで実装することにしました。
※ 実装には、日本語の読み上げに利用させていただいているJaTTSのソースコードを参考にさせていただきました。
gimite/android-jatts – GitHub

では、まず、APIをコールしてwavファイルを保存するThreadクラスです。

package jp.flashcast.translator.android.thread;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import jp.flashcast.translator.android.manager.TTSManager.TTSState;
import jp.flashcast.translator.android.model.TTSModel;
import android.content.Context;
import android.os.Handler;

public class TTSThread extends Thread {
	private final Context context;
	private final Handler handler;
	private final Runnable runnable;
	private final TTSModel model;
	private final HttpClient client;
	private FileOutputStream file;
	
	public TTSThread(Context context, Handler handler, Runnable runnable, TTSModel model) {
		this.context = context;
		this.handler = handler;
		this.runnable = runnable;
		this.model = model;
		this.client = new DefaultHttpClient();
		try {
			this.context.deleteFile(TTSModel.MEDIA_FILE_NAME);
			this.file = context.openFileOutput(TTSModel.MEDIA_FILE_NAME, Context.MODE_WORLD_READABLE);
		} catch (FileNotFoundException e) {
			model.setDownloaded(false);
		}
	}
	
	@Override
	public void start() {
		model.setStatus(TTSState.LOADING);
		super.start();
	}
	
	@Override
	public void run() {
		String url = model.getURL();
		
		if (!url.equals("")) {
			model.setDownloaded(false);
			
			HttpGet get = new HttpGet(url);
			
			try {
				final HttpResponse response = client.execute(get);
				
				if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
					InputStream input = response.getEntity().getContent();
					
					int read;
					byte[] buffer = new byte[4096];
					
					while ((read = input.read(buffer)) > 0) {
						file.write(buffer, 0, read);
					}
					
					file.close();
					input.close();
					model.setDownloaded(true);
				}
			} catch (ClientProtocolException e) {
				model.setDownloaded(false);
			} catch (IOException e) {
				model.setDownloaded(false);
			}
		}
		else {
			model.setDownloaded(false);
		}
		
		handler.post(runnable);
	}
}

APIコールに必要な情報は、すべて1つのモデルクラスにまとめるよう実装しました。
※ 12行目のAppIDは適宜変更してください。

package jp.flashcast.translator.android.model;

import jp.flashcast.translator.android.manager.TTSManager.TTSState;
import android.net.Uri;

public class TTSModel {
	private String text;
	private String language;
	private boolean downloaded;
	private TTSState state;
	private final static String G_TRANSLATE_BING_TRANSLATE_API_URL = "http://api.microsofttranslator.com/V2/Http.svc/Speak";
	private final static String G_TRANSLATE_BING_TRANSLATE_API_APPID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
	public final static String MEDIA_FILE_NAME = "temp.wav";
	
	public TTSModel() {
		this.text = "";
		this.language = "";
		this.downloaded = false;
		this.state = TTSState.IDLE;
	}
	public void setText(String text) {
		this.text = text;
	}
	public String getText() {
		return text;
	}
	public void setLanguage(String language) {
		this.language = language;
	}
	public String getLanguage() {
		return language;
	}
	public void setDownloaded(boolean downloaded) {
		this.downloaded = downloaded;
	}
	public boolean isDownloaded() {
		return downloaded;
	}
	public void setStatus(TTSState state) {
		this.state = state;
	}
	public TTSState getStatus() {
		return state;
	}
	public String getURL() {
		String url ="";
		
		if (!text.equals("") && !language.equals("")) {
			url = G_TRANSLATE_BING_TRANSLATE_API_URL + 
			"?appid=" + G_TRANSLATE_BING_TRANSLATE_API_APPID +
			"&text=" + Uri.encode(text) +
			"&language=" + language;
		}
		
		return url;
	}
}

次に、保存したwavファイルを再生するThreadクラスです。

package jp.flashcast.translator.android.thread;

import java.io.IOException;

import jp.flashcast.translator.android.manager.TTSManager.TTSState;
import jp.flashcast.translator.android.model.TTSModel;
import android.content.Context;
import android.media.MediaPlayer;
import android.speech.tts.TextToSpeech;

public class PlayerThread extends Thread {
	private final Context context;
	private final MediaPlayer player;
	private final TTSModel model;
	
	public PlayerThread(Context context, MediaPlayer player, TTSModel model) {
		this.context = context;
		this.player = player;
		this.model = model;
	}
	
	@Override
	public void start() {
		model.setStatus(TTSState.SPEAKING);
		super.start();
	}
	
	@Override
	public void run() {
		if (player != null) {
			try {
				player.reset();
				player.setAudioStreamType(TextToSpeech.Engine.DEFAULT_STREAM);
				player.setDataSource(context.getFilesDir() + "/" + TTSModel.MEDIA_FILE_NAME);
				player.prepare();
				player.start();
			} catch (IllegalArgumentException e) {
				model.setStatus(TTSState.IDLE);
			} catch (IllegalStateException e) {
				model.setStatus(TTSState.IDLE);
			} catch (IOException e) {
				model.setStatus(TTSState.IDLE);
			}
		}
	}
}

この2つのThreadをコントロールするクラスを、以下のようにしました。

package jp.flashcast.translator.android.manager;

import android.content.Context;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.os.Handler;
import jp.flashcast.translator.android.model.TTSModel;
import jp.flashcast.translator.android.thread.PlayerThread;
import jp.flashcast.translator.android.thread.TTSThread;

public class TTSManager implements Runnable {
	private final Context context;
	private final MediaPlayer player;
	private final TTSModel model;
	private TTSThread thread;
	
	public enum TTSState {
		IDLE,
		LOADING,
		SPEAKING
	}
	
	public TTSManager(Context context) {
		this.context = context;
		this.player = new MediaPlayer();
		this.player.setOnCompletionListener(new OnCompletionListener() {

			public void onCompletion(MediaPlayer mp) {
				model.setStatus(TTSState.IDLE);
			}
			
		});
		this.model = new TTSModel();
	}
	
	public synchronized void speak(String text, String language) {
		model.setText(text);
		model.setLanguage(language);
		
		if (!isSpeaking()) {
			if (thread != null) {
				thread.interrupt();
			}
			player.stop();
		}
		thread = new TTSThread(context, new Handler(), this, model);
		thread.start();
	}

	public void run() {
		if (model.isDownloaded()) {
			new PlayerThread(context, player, model).start();
		}
	}
	
	public boolean isSpeaking() {
		return model.getStatus() != TTSState.IDLE; 
	}
	
	public void stop() {
		if (thread != null) {
			thread.interrupt();
		}
		if (player != null) {
			player.stop();
		}
	}
}

画面は以下のようにし、このTTSManagerクラスのみを呼び出すようにしました。

操作は簡単です。EditTextに音声にしたい文章を入力し、Spinnerコントロールから何国語を入力したか選択します。「Play」ボタンを押すと入力した文章が再生されるという仕組みです。

読み上げを中断するための「Stop」ボタンも用意してみました。

「Play」ボタンのクリックイベントハンドラでTTSManagerクラスのspeakメソッドを、「Stop」ボタンのクリックイベントハンドラでTTSManagerのstopメソッドをコールします。

 
       Button btnPlay = (Button)findViewById(R.id.play);
        
        btnPlay.setOnClickListener(new OnClickListener() {

			public void onClick(View view) {
				String text = txtTTS.getText().toString();
				
				if (!text.equals("")) {
					manager.speak(text, (String)language.getSelectedItem());
				}
			}
        	
        });
        
        Button btnStop = (Button)findViewById(R.id.stop);
        
        btnStop.setOnClickListener(new OnClickListener() {

			public void onClick(View view) {
				manager.stop();
			}
        	
        });

一応、4カ国語に対応しました。
Spinnerコントロールを押すと、以下のように表示されます。

ということで、韓国語と中国語の読み上げに対応したgTranslator、是非、試してみてください!

サンプルソース