CSR SDK API 란?

Clova Speech Recognition API(이하 CSR API)는 사용자의 음성 입력을 스트리밍 형태로 입력받은 후 음성 인식한 결과를 텍스트로 반환합니다. CSR API는 사용자 음성 입력을 전달 받기 위해 자체 개발한 스트리밍 프로토콜을 사용하고 있습니다. 따라서, HTTP 기반의 REST API 형태가 아니라, Android와 iOS SDK 형태로 CSR API를 제공하고 있습니다.

사전 준비사항

  1. 콘솔의 AI·Application Service > AI·NAVER API > Application에서 애플리케이션을 등록합니다.(자세한 방법은 "Application 사용 가이드" 참고)

  2. AI·Application Service > AI·NAVER API > Application에서 등록한 애플리케이션을 선택해 Client ID와 Client Secret값을 확인합니다.

  3. AI·Application Service > AI·NAVER API > Application변경 화면에서 Clova Speech Recognition가 선택되어 있는지 확인합니다. 선택되어 있지 않으면 429 (Quota Exceed)가 발생하니 주의하시기 바랍니다.

API 사용하기

CSR API는 Android 용과 iOS용 SDK를 통해 제공되고 있습니다. 여기에서는 각 플랫폼별 CSR API를 사용하는 방법에 대해 설명합니다.

Android API 사용하기

Android API를 사용하려면 다음 절차를 따릅니다.

  1. 다음 구문을 app/build.gradle 파일에 추가합니다.
    repositories {
        jcenter()
    }
    dependencies {
        compile 'com.naver.speech.clientapi:naverspeech-ncp-sdk-android:1.1.6'
    }
  2. 다음과 같이 Android Manifest 파일(AndroidManifest.xml)을 설정합니다.
    • 패키지 이름 : manifest 요소의 package 속성 값이 사전 준비사항에서 등록한 안드로이드 앱 패키지 이름과 같아야 합니다.
    • 권한 설정 : 사용자의 음성 입력을 마이크를 통해 녹음해야 하고 녹음된 데이터를 서버로 전송해야 합니다. 따라서, android.permission.INTERNETandroid.permission.RECORD_AUDIO에 대한 권한이 반드시 필요합니다.
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
              package="com.naver.naverspeech.client"
              android:versionCode="1" android:versionName="1.0" >
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  3. (선택) proguard-rules.pro 파일에 다음을 추가합니다. 아래 코드는 앱을 보다 가볍고 안전하게 만들어줍니다.
    -keep class com.naver.speech.clientapi.SpeechRecognizer {
            protected private *;
    }

Note!

네이버 Open API는 Android SDK 버전 10 이상을 지원합니다. 따라서, build.gradle 파일의 minSdkVersion 값을 이에 맞게 설정해야 합니다.

클라이언트는 "준비", "녹음", "중간결과 출력", "끝점 추출", "최종결과 출력"과 같은 일련의 이벤트 흐름을 수행합니다. 애플리케이션 개발자는 SpeechRecognitioinListener 인터페이스를 상속받아 해당 이벤트가 발생할 때 처리할 동작을 구현하면 됩니다.

Note!

API에 대한 자세한 설명은 https://github.com/NaverCloudPlatform/naverspeech-sdk-android를 참고합니다.

iOS API 사용하기

iOS API를 사용하려면 다음 절차를 따릅니다.

  1. iOS용 예제를 clone하거나 Zip 파일로 다운로드하여 압축을 해제합니다.
    git clone https://github.com/NaverCloudPlatform/naverspeech-sdk-ios.git
    또는
    wget https://github.com/NaverCloudPlatform/naverspeech-sdk-ios/archive/ncp.zip
    unzip ncp.zip
  2. iOS 예제에서 framework/NaverSpeech.framework 디렉터리를 개발하는 앱의 Embedded Binaries에 추가합니다.
  3. 다음과 같이 iOS Bundle Identifier를 설정합니다.
    • Bundle Identifier : 사전 준비사항에서 등록한 iOS Bundle ID와 같아야 합니다.
    • 권한 설정 : 사용자의 음성 입력을 마이크를 통해 녹음해야 하고 녹음된 데이터를 서버로 전송해야 합니다. 따라서, key 값을 다음과 같이 설정합니다.
      <key>NSMicrophoneUsageDescription</key>
      <string></string>

Note!

  • iOS API를 제공하기 위해 Universal binary(Fat binary) 형태의 프레임워크를 제공하고 있습니다. 따라서 Build Setting에서 Enable Bitcode 옵션을 사용할 수 없으므로 No로 설정해야 합니다.
  • 네이버 Open API는 iOS 버전 8 이상을 지원합니다. 따라서, Deployment Target 값을 이에 맞게 설정해야 합니다.

클라이언트는 "준비", "중간결과 출력", "끝점 추출", "최종결과 출력"과 같은 일련의 이벤트 흐름을 수행합니다. 애플리케이션 개발자는 해당 이벤트가 발생할 때 원하는 동작을 수행하도록 NSKRecognizerDelegate protocol을 구현하면 됩니다.

Note!

API에 대한 자세한 설명은 http://naver.github.io/naverspeech-sdk-ios/Classes/NSKRecognizer.html를 참고합니다.

UX 고려사항

일반적으로 사용자는 음성 인식 버튼을 누르자마자 발화를 시작하려고 할 것입니다. 하지만 음성 인식을 시작하는 recognize() 메서드를 호출하면 음성 인식을 위한 메모리 할당, 마이크 자원 할당, 음성 인식 서버 접속 및 인증 등의 준비 과정을 수행해야 하기 때문에 사용자의 발화 일부가 누락될 수 있습니다. 따라서, 앱은 모든 준비가 완료된 후 사용자에게 발화해도 좋다는 정보를 전달해야 합니다. 이 방법은 다음과 같이 처리할 수 있습니다.

  • 모든 준비가 완료되면 onReady callback 메서드가 호출됩니다.
  • onReady callback 메서드가 호출되기 전까지 "준비 중입니다."와 같은 메시지를 표시하거나 준비 중임을 나타내는 UI 표시를 해야 합니다.
  • onReady callback 메서드가 호출되면 "이야기해주세요."와 같은 메시지를 표시하거나 사용 가능함을 나타내는 UI를 표시해야 합니다.

Note!

  • (Android API) SpeechRecognitionListeneronReady, onRecord 등의 callback 메서드는 Worker Thread에서 호출되는 메서드이며, Handler에 등록하여 사용해야 합니다.
  • (iOS API) cancel() 메서드를 호출하면 호출한 시점부터 delegation 메서드들이 호출되지 않습니다. 따라서, 음성 인식이 끝났을 때 처리해야 하는 작업들은 cancel() 메서드 호출 이후에 따로 수행해야 합니다.

오류 처리

CSR API를 사용할 때 다양한 원인으로 오류가 발생할 수 있으며 이때 오류 callback 함수를 통해 오류 코드가 전달됩니다. 오류 코드를 분석하면 원인을 분석하거나 오류 처리를 할 수 있습니다. CSR API의 오류 callback 함수가 전달하는 오류는 다음과 같습니다.

오류 이름 오류 코드 설명
ERROR_NETWORK_INITIALIZE 10 네트워크 자원 초기화 오류
ERROR_NETWORK_FINALIZE 11 네트워크 자원 해제 오류
ERROR_NETWORK_READ 12 네트워크 데이터 수신 오류. 클라이언트 기기의 네트워크 환경이 느려 Timeout 이 발생하는 경우 주로 발생합니다.
ERROR_NETWORK_WRITE 13 네트워크 데이터 전송 오류. 클라이언트 기기의 네트워크 환경이 느려 Timeout 이 발생하는 경우 주로 발생합니다.
ERROR_NETWORK_NACK 14 음성 인식 서버 오류. 느린 네트워크 환경으로 인해 클라이언트가 서버로 음성 패킷을 제시간에 보내지 못하면 서버는 Timeout를 발생시킵니다. 이때 발생하는 오류입니다.
ERROR_INVALID_PACKET 15 유효하지 않은 패킷 전송으로 인한 오류
ERROR_AUDIO_INITIALIZE 20 오디오 자원 초기화 오류. 오디오 사용 권한이 있는지 확인합니다.
ERROR_AUDIO_FINALIZE 21 오디오 자원 해제 오류
ERROR_AUDIO_RECORD 22 음성 입력(녹음) 오류. 오디오 사용 권한이 있는지 확인합니다.
ERROR_SECURITY 30 인증 권한 오류
ERROR_INVALID_RESULT 40 인식 결과 오류
ERROR_TIMEOUT 41 일정 시간 이상 서버로 음성을 전송하지 못하거나, 인식 결과를 받지 못함.
ERROR_NO_CLIENT_RUNNING 42 클라이언트가 음성 인식을 수행하지 않는 상황에서 특정 음성 인식 관련 이벤트가 감지됨.
ERROR_UNKNOWN_EVENT 50 클라이언트 내부에 규정되어 있지 않은 이벤트가 감지됨.
ERROR_VERSION 60 프로토콜 버전 오류
ERROR_CLIENTINFO 61 클라이언트 정보 오류
ERROR_SERVER_POOL 62 음성 인식 가용 서버 부족
ERROR_SESSION_EXPIRED 63 음성 인식 서버 세션 만료
ERROR_SPEECH_SIZE_EXCEEDED 64 음성 패킷 사이즈 초과
ERROR_EXCEED_TIME_LIMIT 65 인증용 타임 스탬프(time stamp) 불량
ERROR_WRONG_SERVICE_TYPE 66 올바른 서비스 타입(service type)이 아님.
ERROR_WRONG_LANGUAGE_TYPE 67 올바른 언어 타입(language type)이 아님.
ERROR_OPENAPI_AUTH 70 Open API 인증 오류. Client ID와 등록된 package 이름(Android) 또는 Bundle ID 정보(iOS)가 잘못되었을 때 발생합니다.
ERROR_QUOTA_OVERFLOW 71 정해진 API 호출 제한량(quota)을 모두 소진함.

위 오류 코드 외에도 다음과 같은 오류가 발생하거나 문의가 있을 수 있습니다.

현상 또는 문의 원인 또는 해결 방법
UnsatifiedLinkError 오류 발생 CSR API는 armeabi와 armeabi-v7a로 빌드된 라이브러리를 제공합니다. 만약 개발하는 앱에서 사용하는 라이브러리 중 armeabi와 armeabi-v7a를 지원하지 않는 것이 있다면 이 오류가 발생할 수 있습니다.
android fatal signal 11 (sigsegv) 오류 발생 CSR API를 사용하여 음성을 입력받기 전에 우선 자원을 준비해야 합니다. recognize() 메서드를 호출하기 전에 initialize() 메서드가 잘 호출되는지 확인해야 합니다. 또한 release() 메서드도 호출할 수 있어야 합니다.
인식 결과로 ""(null)이 반환됩니다. 사용자가 매우 작은 목소리로 발성하였거나, 주변 소리로 인해 목소리가 인식되지 않았을 경우 발생할 수 있습니다. 극히 드물게 발생하지만 인식 결과가 null 일 때도 예외 처리 해주는 것을 권장합니다.
오디오 파일 인식 CSR API는 오디오 파일 인식을 지원하지 않습니다.
저사양 스마트 폰에서 제대로 동작하지 않습니다. CSR API는 Android SDK 버전 10 이상과 iOS 버전 8 이상의 기기를 지원하고 있습니다.

구현 예제

다음은 각 플랫폼별 CSR API 구현 예제입니다.

Android 구현 예제

다음은 Android에서 CSR API를 사용한 예제 코드입니다.

// 1. Main Activity 클래스
public class MainActivity extends Activity {
    private static final String TAG = MainActivity.class.getSimpleName();
    private static final String CLIENT_ID = "YOUR CLIENT ID"; // "내 애플리케이션"에서 Client ID를 확인해서 이곳에 적어주세요.
    private RecognitionHandler handler;
    private NaverRecognizer naverRecognizer;
    private TextView txtResult;
    private Button btnStart;
    private String mResult;
    private AudioWriterPCM writer;
    // Handle speech recognition Messages.
    private void handleMessage(Message msg) {
        switch (msg.what) {
            case R.id.clientReady: // 음성인식 준비 가능
                txtResult.setText("Connected");
                writer = new AudioWriterPCM(Environment.getExternalStorageDirectory().getAbsolutePath() + "/NaverSpeechTest");
                writer.open("Test");
                break;
            case R.id.audioRecording:
                writer.write((short[]) msg.obj);
                break;
            case R.id.partialResult:
                mResult = (String) (msg.obj);
                txtResult.setText(mResult);
                break;
            case R.id.finalResult: // 최종 인식 결과
                SpeechRecognitionResult speechRecognitionResult = (SpeechRecognitionResult) msg.obj;
                List<String> results = speechRecognitionResult.getResults();
                StringBuilder strBuf = new StringBuilder();
                for(String result : results) {
                    strBuf.append(result);
                    strBuf.append("\n");
                }
                mResult = strBuf.toString();
                txtResult.setText(mResult);
                break;
            case R.id.recognitionError:
                if (writer != null) {
                    writer.close();
                }
                mResult = "Error code : " + msg.obj.toString();
                txtResult.setText(mResult);
                btnStart.setText(R.string.str_start);
                btnStart.setEnabled(true);
                break;
            case R.id.clientInactive:
                if (writer != null) {
                    writer.close();
                }
                btnStart.setText(R.string.str_start);
                btnStart.setEnabled(true);
                break;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        txtResult = (TextView) findViewById(R.id.txt_result);
        btnStart = (Button) findViewById(R.id.btn_start);
        handler = new RecognitionHandler(this);
        naverRecognizer = new NaverRecognizer(this, handler, CLIENT_ID);
        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!naverRecognizer.getSpeechRecognizer().isRunning()) {
                    mResult = "";
                    txtResult.setText("Connecting...");
                    btnStart.setText(R.string.str_stop);
                    naverRecognizer.recognize();
                } else {
                    Log.d(TAG, "stop and wait Final Result");
                    btnStart.setEnabled(false);
                    naverRecognizer.getSpeechRecognizer().stop();
                }
            }
        });
    }
    @Override
    protected void onStart() {
        super.onStart(); // 음성인식 서버 초기화는 여기서
        naverRecognizer.getSpeechRecognizer().initialize();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mResult = "";
        txtResult.setText("");
        btnStart.setText(R.string.str_start);
        btnStart.setEnabled(true);
    }
    @Override
    protected void onStop() {
        super.onStop(); // 음성인식 서버 종료
        naverRecognizer.getSpeechRecognizer().release();
    }
    // Declare handler for handling SpeechRecognizer thread's Messages.
    static class RecognitionHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;
        RecognitionHandler(MainActivity activity) {
            mActivity = new WeakReference<MainActivity>(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mActivity.get();
            if (activity != null) {
                activity.handleMessage(msg);
            }
        }
    }
}

// 2. SpeechRecognitionListener를 상속한 클래스
class NaverRecognizer implements SpeechRecognitionListener {
    private final static String TAG = NaverRecognizer.class.getSimpleName();
    private Handler mHandler;
    private SpeechRecognizer mRecognizer;
    public NaverRecognizer(Context context, Handler handler, String clientId) {
        this.mHandler = handler;
        try {
            // 공공기관을 위한 음성인식을 사용하기 위해서는 SpeechRecognizer가 아닌 SpeechGovRecognizer의 instance를 생성해야 합니다.
            mRecognizer = new SpeechGovRecognizer(context, clientId); 
        } catch (SpeechRecognitionException e) {
            e.printStackTrace();
        }
        mRecognizer.setSpeechRecognitionListener(this);
    }
    public SpeechRecognizer getSpeechRecognizer() {
        return mRecognizer;
    }
    public void recognize() {
        try {
            mRecognizer.recognize(new SpeechConfig(LanguageType.KOREAN, EndPointDetectType.AUTO));
        } catch (SpeechRecognitionException e) {
            e.printStackTrace();
        }
    }
    @Override
    @WorkerThread
    public void onInactive() {
        Message msg = Message.obtain(mHandler, R.id.clientInactive);
        msg.sendToTarget();
    }
    @Override
    @WorkerThread
    public void onReady() {
        Message msg = Message.obtain(mHandler, R.id.clientReady);
        msg.sendToTarget();
    }
    @Override
    @WorkerThread
    public void onRecord(short[] speech) {
        Message msg = Message.obtain(mHandler, R.id.audioRecording, speech);
        msg.sendToTarget();
    }
    @Override
    @WorkerThread
    public void onPartialResult(String result) {
        Message msg = Message.obtain(mHandler, R.id.partialResult, result);
        msg.sendToTarget();
    }
    @Override
    @WorkerThread
    public void onEndPointDetected() {
        Log.d(TAG, "Event occurred : EndPointDetected");
    }
    @Override
    @WorkerThread
    public void onResult(SpeechRecognitionResult result) {
        Message msg = Message.obtain(mHandler, R.id.finalResult, result);
        msg.sendToTarget();
    }
    @Override
    @WorkerThread
    public void onError(int errorCode) {
        Message msg = Message.obtain(mHandler, R.id.recognitionError, errorCode);
        msg.sendToTarget();
    }
    @Override
    @WorkerThread
    public void onEndPointDetectTypeSelected(EndPointDetectType epdType) {
        Message msg = Message.obtain(mHandler, R.id.endPointDetectTypeSelected, epdType);
        msg.sendToTarget();
    }
}

iOS 구현 예제

다음은 iOS에서 CSR API를 사용한 예제 코드입니다.

  • CSR API - iOS용 예제
  • 예제 코드 저장소 : https://github.com/NaverCloudPlatform/naverspeech-sdk-ios
  • 설명

    • recognizer를 동작시키기 위한 각 기관별 configuration객체를 생성하여 사용합니다.
    • Objective-C

      // 민간기관
      NSKRecognizerConfiguration *configuration = [NSKRecognizerConfiguration configurationWithClientID:kClientID];
      
      // 공공기관
      NSKRecognizerConfiguration *configuration = [NSKRecognizerConfiguration govConfigurationWithClientID:kClientID];
      
    • Swift

      // 민간기관
      let configuration = NSKRecognizerConfiguration(clientID: ClientID)
      
      // 공공기관
      let configuration = NSKRecognizerConfiguration.govConfiguration(withClientID: ClientID)
      
import UIKit
import NaverSpeech
import Common
let ClientID = "YOUR_CLIENT_ID"
class AutoViewController: UIViewController {
    required init?(coder aDecoder: NSCoder) { // NSKRecognizer를 초기화 하는데 필요한 NSKRecognizerConfiguration을 생성
        let configuration = NSKRecognizerConfiguration(clientID: ClientID)
        configuration?.canQuestionDetected = true
        self.speechRecognizer = NSKRecognizer(configuration: configuration)
        super.init(coder: aDecoder)
        self.speechRecognizer.delegate = self
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setupLanguagePicker()
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        if self.isViewLoaded && self.view.window == nil {
            self.view = nil
        }
    }
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        let x = languagePickerButton.frame.minX
        let y = languagePickerButton.frame.maxY
        self.pickerView.frame = CGRect.init(x: x, y: y, width: languagePickerButton.bounds.size.width, height: self.pickerView.bounds.size.height)
    }
    @IBAction func languagePickerButtonTapped(_ sender: Any) {
        self.pickerView.isHidden = false
    }
    @IBAction func recognitionButtonTapped(_ sender: Any) { // 버튼 누르면 음성인식 시작
        if self.speechRecognizer.isRunning {
            self.speechRecognizer.stop()
        } else {
            self.speechRecognizer.start(with: self.languages.selectedLanguage)
            self.recognitionButton.isEnabled = false
            self.statusLabel.text = "Connecting......"
        }
    }
    @IBOutlet weak var languagePickerButton: UIButton!
    @IBOutlet weak var recognitionResultLabel: UILabel!
    @IBOutlet weak var recognitionButton: UIButton!
    @IBOutlet weak var statusLabel: UILabel!
    fileprivate let speechRecognizer: NSKRecognizer
    fileprivate let languages = Languages()
    fileprivate let pickerView = UIPickerView()
}

extension AutoViewController: NSKRecognizerDelegate { //NSKRecognizerDelegate protocol 구현

    public func recognizerDidEnterReady(_ aRecognizer: NSKRecognizer!) {
        print("Event occurred: Ready")
        self.statusLabel.text = "Connected"
        self.recognitionResultLabel.text = "Recognizing......"
        self.setRecognitionButtonTitle(withText: "Stop", color: .red)
        self.recognitionButton.isEnabled = true
    }
    public func recognizerDidDetectEndPoint(_ aRecognizer: NSKRecognizer!) {
        print("Event occurred: End point detected")
    }
    public func recognizerDidEnterInactive(_ aRecognizer: NSKRecognizer!) {
        print("Event occurred: Inactive")
        self.setRecognitionButtonTitle(withText: "Record", color: .blue)
        self.recognitionButton.isEnabled = true
        self.statusLabel.text = ""
    }
    public func recognizer(_ aRecognizer: NSKRecognizer!, didRecordSpeechData aSpeechData: Data!) {
        print("Record speech data, data size: \(aSpeechData.count)")
    }
    public func recognizer(_ aRecognizer: NSKRecognizer!, didReceivePartialResult aResult: String!) {
        print("Partial result: \(aResult)")
        self.recognitionResultLabel.text = aResult
    }
    public func recognizer(_ aRecognizer: NSKRecognizer!, didReceiveError aError: Error!) {
        print("Error: \(aError)")
        self.setRecognitionButtonTitle(withText: "Record", color: .blue)
        self.recognitionButton.isEnabled = true
    }
    public func recognizer(_ aRecognizer: NSKRecognizer!, didReceive aResult: NSKRecognizedResult!) {
        print("Final result: \(aResult)")
        if let result = aResult.results.first as? String {
            self.recognitionResultLabel.text = "Result: " + result
        }
    }
}
extension AutoViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return self.languages.count
    }
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return languages.languageString(at: row)
    }
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        languages.selectLanguage(at: row)
        languagePickerButton.setTitle(languages.selectedLanguageString, for: .normal)
        self.pickerView.isHidden = true
        if self.speechRecognizer.isRunning { //음성인식 중 언어를 변경하게 되면 음성인식을 즉시 중지(cancel)
            self.speechRecognizer.cancel()
            self.recognitionResultLabel.text = "Canceled"
            self.setRecognitionButtonTitle(withText: "Record", color: .blue)
            self.recognitionButton.isEnabled = true
        }
    }
}
fileprivate extension AutoViewController {
    func setupLanguagePicker() {
        self.view.addSubview(self.pickerView)
        self.pickerView.dataSource = self
        self.pickerView.delegate = self
        self.pickerView.showsSelectionIndicator = true
        self.pickerView.backgroundColor = UIColor.white
        self.pickerView.isHidden = true
    }
    func setRecognitionButtonTitle(withText text: String, color: UIColor) {
        self.recognitionButton.setTitle(text, for: .normal)
        self.recognitionButton.setTitleColor(color, for: .normal)
    }
}

""에 대한 건이 검색되었습니다.

    ""에 대한 검색 결과가 없습니다.

    처리중...