ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 라즈베리파이로 NUGU 스피커 프로젝트 해 본 이야기 #3
    삽질기/라즈베리파이 2020. 5. 22. 09:38

    누구 스피커 프로젝트를 다룰 마지막 포스팅으로 백엔드 서버에 대해 얘기해보겠다. 본 프로젝트에서 제일 시간이 많이 걸렸던 부분이었고 공을 많이 들이기도 했다. 실력자 분들이 보시기에 읽기 힘들고 가독성이 엉망인 코드로 보일 수 있겠지만, 경력이 부족한 프로그래머가 맨땅에 헤딩해가며 일단 작동되는 코드를 구현한 것이기에 귀엽게 봐주셨으면 한다. 코드에 대한 조언이 있다면 댓글에 써주시길 바란다. 겸허히 받아들여 피드백하도록 하겠다.

     

     

    kyc3492/NUgaller

    Contribute to kyc3492/NUgaller development by creating an account on GitHub.

    github.com

    해당 링크의 파일은 node 명령어를 사용하여 실제 서버가 구동되는 메인 파일이다. 파일 이름이 ImageReceiver2.js 인 이유는 프로젝트를 시작할 때 누구 스피커로 사진을 찾는 것뿐만 아니라 앱으로 사진을 찍으면 서버에 업로드 되는 부분부터 구현했기 때문이다. 시연 때는 보여드리지 못했지만 구현을 완료하여 시연 가능한 수준이긴 했으니 참고하시려면 참고하여도 좋다.

     

    또한 깃 허브에서 코드를 보실 때 다른 파일도 보려고 프로젝트 상위 폴더로 올라가면 수 많은 파일들을 볼 수 있을 것이다. 그러나 ImageReceiver2.js 상단 부분에 require 로 불러낸 파일들을 제외하곤 테스트용이거나 필요없는 파일이므로 해당 파일에서 언급된 파일만 확인하면 되겠다.

     


     

    //ImageReceiver.js
    //...중략...
    Date.prototype.yyyymmdd = function(){
      var mm = this.getMonth() + 1;
      var dd = this.getDate();
    
      return [this.getFullYear(),
        (mm > 9 ? '' : '0') + mm,
        (dd > 9 ? '' : '0') + dd
      ].join('');
    }
    
    Date.prototype.hhmmss = function() {
      var hh = this.getHours();
      var mm = this.getMinutes();
      var ss = this.getSeconds();
      var modifiedDate = "";
    
      return [(hh>9 ? '' : '0') + hh,
              (mm>9 ? '' : '0') + mm,
              (ss>9 ? '' : '0') + ss,
             ].join('');
    };
    
    //...중략...
    
    app.post('/upload', upload.single('upload'), function(req, res){
      console.log(req.file);
      res.send('Uploaded : '+req.file.filename);
    
      analyzer.detectStart('uploads/' + req.file.filename,  req.file.filename, modifiedDate).catch(console.error);
    });
    //...중략...

     

    앱으로부터 사진을 업로드 받는 부분이다. 같은 사진으로 테스트를 했을 때 파일명이 중복되는 것을 방지하기 위해 날짜를 원하는 대로 양식을 바꾸고 파일명 뒤에 붙이도록 하였다. multer 패키지를 사용했으며 여러 파일의 업로드는 구현하지 않았고 한 번에 하나의 사진 파일만을 업로드 하도록 하였다. 사진 업로드가 주요 기능이었다면 다중 업로드도 구현하였겠지만, 주요 기능은 사진 찾기이므로 단일 업로드만을 간단히 구현했다.

     

    그리고 마지막 줄에 analyzer.detectStart 함수를 호출하는데, 주요 기능은 업로드 된 사진을 Google Vision API를 이용하여 태그를 붙이고 전체 사진 목록 테이블에 추가하는 것이다. 자세한 내용은 ImageAnalyzerModule.js 파일에 있으니 참고하시면 되겠다. Google Vision API의 예제 함수를 많이 참고하였으니 공식 문서를 확인하는 것도 좋다.

     

     

    Vision AI | 머신러닝을 통한 이미지 정보 도출  |  Cloud Vision API  |  Google Cloud

    AutoML Vision을 사용하여 클라우드나 에지 이미지에서 유용한 정보를 도출하거나 선행 학습된 Vision API 모델을 사용하여 감정, 텍스트 등을 인식합니다.

    cloud.google.com

     


     

    //ImageReceiver.js
    //...중략...
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({extended: true}));
    app.post('/' ,function(req, res){
      console.log("NUGU init...");
      console.log(req.body.action.actionName);
      console.log(req.body.action.parameters);
    
      if(req.body.action.actionName == "find_photo"){
        var response = '{ "version": "2.0", "resultCode": "OK", "output": {}}';
        //JSON continued...
        //res.json(JSON.parse(response));
        console.log("Received Tag From NUGU: " + req.body.action.parameters.TAG.value);
        res.json(JSON.parse(response));
        tag = req.body.action.parameters.TAG.value;
        var deleteQuery = "DELETE FROM NUtellerRequested";
        console.log(deleteQuery);
        dbconn.query(deleteQuery, function(err, records){
          if(err) throw err;
          console.log("Requested Table Initialized");
          var insertQuery = "INSERT INTO NUtellerRequested SELECT * FROM NUtellerData WHERE labels LIKE UPPER('%" + tag + "%');"
          console.log(insertQuery);
          dbconn.query(insertQuery, function(err, records){
            if(err) throw err;
            console.log("Requested Table is Generated!");
            var selectQuery = "SELECT * FROM NUtellerRequested";
            console.log(selectQuery);
            dbconn.query(selectQuery, function(err, records){
              if(records.length > 0){
                notificator.sendSuccessNotification();
                console.log('Query sent.');
              } else {
                notificator.sendFailNotification();
                console.log('Nothing in query.');
              }
            });
          });
        });
      } //...중략...

    위 함수는 누구 스피커로부터 태그가 담긴 요청을 받았을 때 사진을 찾아 이전에 요청한 사진들로 이루어진 테이블을 초기화하고 새로 채워 넣는다. 누구 스피커에서 보내는 json 기반의 요청에선 위 코드에서 볼 수 있듯이 req.body.action.parameters를 통해 플레이 빌더로 작성한 파라미터가 전달된다. response 변수가 도대체 무엇인가 할 것 같은데 이는 누구 스피커로 반드시 보내야하는 통신 성공 응답이다. 이 응답이 제대로 보내지지 않으면 누구 스피커는 2번의 통신 시도를 더 시행한다.

     

    처리속도가 다소 늦었던 라즈베리파이에서는(지금 생각해보면 성능이 좋지 못한 sd카드 문제 같지만) 이러한 재시도 방식 때문에 문제가 생겼었다. 처음엔 누구 스피커로 보내는 해당 메시지에 파라미터를 설정하여 전송할 수 있기에 검색된 사진의 개수를 발화하려 하였고 이에 따라 응답 메시지를 함수의 마지막 부분에서 보내려 하였다. 그러나 이를 기다리지 못한 누구 스피커가 2차례에 걸쳐 재시도를 하였고 이로 인해 라즈베리파이는 정신 못차리고 코드 진행이 꼬여버렸다. 결국 위와 같이 곧바로 응답을 보내고, 누구 스피커의 발화는 "사진을 열심히 찾고 있으니 찾으면 알림을 보내드릴게요."와 같이 변경되었다. 결과적으론 이로 인해 칭찬 받았으니 됐다.

     

    앱으로의 알림은 Firebase Cloud Messaging 서비스로 간단히 구현할 수 있었다. 궁금하시다면 검색하시면 되지만 FCM Token 발급 등 클라이언트 준비사항이 모두 끝난 상태에서 서버 코드만 보고 싶으시다면 부족한 제 깃허브(fcmNotificator.js)를 참고하셔도 좋다.

     


     

    //ImageReceiver.js
    //...중략...
    } else if(req.body.action.actionName == "create_receive_name"){
        console.log("Make Selfie Album.");
        var response = '{ "version": "2.0", "resultCode": "OK", "output": {}}';
        var updateQuery = "UPDATE NUtellerData SET album = 'selfie' WHERE album = '0'";
        dbconn.query(updateQuery, function(err, records){
          notificator.sendCreatedNotification();
          console.log('Album Created!');
          var selectQuery = "SELECT * FROM NUtellerAlbums WHERE albumName = '" + req.body.action.parameters.CREATE_SELFIE + "'";
          dbconn.query(selectQuery, function(err, records){
            if(records.length == 0){
              var insertQuery = "INSERT INTO NUtellerAlbums VALUES ('"+ req.body.action.parameters.CREATE_SELFIE.value +"')";
              dbconn.query(insertQuery, function(err, records){
                console.log(insertQuery);
                notificator.sendCreatedNotification();
              });
            } else {
              notificator.sendAlreadyExistNotification();
            }
            res.json(JSON.parse(response));
          });
        });
      } else if(req.body.action.actionName == "move_receive_name"){
        console.log("Moving Photos");
        var response = '{ "version": "2.0", "resultCode": "OK", "output": {}}';
        var updateQuery = "UPDATE NUtellerData SET album = 'selfie' WHERE labels LIKE UPPER('%myselfie%')";
        dbconn.query(updateQuery, function(err, records){
          //console.log('Album Created!');
          res.json(JSON.parse(response));
          notificator.sendMoveSuccessNotification();
        });
    //...중략...    

    위는 앨범을 생성하는 함수이고 다음은 검색한 사진을 앨범으로 이동하는 함수이다. 그러나 시연을 위해 테스트 케이스에 대해서만 대응하게끔 작성한 코드이다. 시연을 준비한 과정을 부분적으로만 말해보자면 조원의 셀카를 찍어 분석 후 myselfie라는 태그를 직접 달았다. 앱으로 직접 구현은 하지 못했지만 사용자의 얼굴을 인식하여 태그가 달렸다고 가정한 것이다. 시나리오는 이렇게 myselfie 태그가 달린 사진들을 검색하여 검색된 결과를 selfie 앨범으로 옮기는 것이었다. 발화들로만 구성해보자면,

     

    "아리아, 누구사진으로 내 사진 찾아줘."

    "네, 열심히 찾고 있어요. 완료되면 알림을 보낼게요."

    <검색 완료 후>

    "아리아, 누구사진으로 이 사진들 옮겨줘."

    "네, 어떤 앨범으로 옮길까요?"

    "내 사진"

    "이동을 완료했어요"

     

    이런 식으로 진행되는 건데, 여기서 중점으로 볼 점은 누구 빌더를 통해 작성된 멀티턴 액션이다. 누구 스피커는 발화문에서 보듯이 앨범의 이름을 한 번 더 묻는 멀티턴 액션을 수행하는데 파라미터 넘기는 건 어렵지 않다. 멀티턴 액션으로 엮인 모든 파라미터들은 멀티턴 액션까지 끝나고 한 번에 넘어오기 때문이다. 결과적으로 처리애햐 할 json 데이터는 하나뿐이고 이에 맞추어 코드를 작성하면 되겠다.

     


     

    나머지 함수들은 위에서 설명한 함수들과 겹치는 부분이 많거나 코드를 보면 이해할 수 있을 것이다. 이번 프로젝트는 라즈베리파이로 꽤 실용적인 서비스를 만들어봤다는 점에서 개인적으로 마음에 들었던 프로젝트이다. 쟁쟁했던 프로젝트들 사이에서 3등이라는 결과를 거머쥔 것도 만족한다. 앞으로도 라즈베리파이로 이것저것 재밌는 것들을 더 만들어보고 싶다.

    댓글

Designed by Tistory.