풀스택 음성 처리 환경 구축

SangYeon Min·2023년 12월 19일
0

PROJECT-HEARUS-GOORM-KDT

목록 보기
1/10
post-thumbnail

위 구조와 같이 음성 처리를 중점으로 온프레미스 환경에서 서비스 구축한다.

음성 처리 Express 서버

Hiearchy

└─BACKEND-ARCHITECTURE
    ├─controllers	비즈니스 로직 구현
    ├─middlewares	라우터에 대한 미들웨어 구현
    ├─models		DB 모델 구현
    ├─public		public 리소스 저장
    ├─routes		라우링 구현
    ├─views			view 코드
    ├─app.js		app 환경설정을 위한 코드
    └─server.js		app listen을 위한 코드

필수 패키지 설치

npm i helmet hpp cross-env nodemon sanitize-html csurf winston pm2 morgan dotenv nunjucks

nodemon node.js 애플리케이션을 자동으로 재시작하는 패키지
dotenv .env에서 변수들을 불러올 수 있도록 하는 패키지
morgan HTTP req logger middleware
winston 다중 transport를 위한 범용 logger
nunjucks js를 위한 templating engine
cross-env env 변수를 플랫폼에서 세팅하고 사용하는 패키지
pm2 built-in load balancer를 사용하는 프로덕션 프로세스 매니저
helmet HTTP res header 보안을 위한 패키지
hpp HTTP Parameter Pollution attack을 보호하기 위한 패키지
csurf Cross-Site Request Forgery 방지 패키지

app, server.js 구축

const express = require('express');
const path = require('path');

const morgan = require('morgan');
const helmet = require('helmet');
const dotenv = require('dotenv');
const nunjucks = require('nunjucks');
dotenv.config();

const indexRouter = require('./routes/index');

const app = express();
app.set('port', process.env.PORT || 3000);
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});

if (process.env.NODE_ENV === 'production') {
  app.enable('trust proxy');
  app.use(morgan('combined'));
  app.use(
    helmet({
      contentSecurityPolicy: false,
      crossOriginEmbedderPolicy: false,
      crossOriginResourcePolicy: false,
    }),
  );
  app.use(hpp());
} else {
  app.use(morgan('dev'));
}
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use('/', indexRouter);

app.use((req, res, next) => {
  const error = new Error(`${req.method} ${req.url} Router doesn't exists`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  console.error(err);
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

app.js에서 서비스를 위한 라우터 설정, 보안 설정들을 완료한다

const app = require('./app');

app.listen(app.get('port'), () => {
    console.log('Server is listening at ', app.get('port'));
});

이후 모듈화된 app을 server.js에서 listen하여 req를 받는다

socket.io 연결

npm install socket.io

socket 통신을 위한 express socket.io 패키지를 설치한다.

const socketIO = require('socket.io');

function initSocket(server, app) {
    console.log('Configuring Socket');
    const io = socketIO(server, {
        cors: {
            credentials: true,
        },
        allowEIO3: true,
    });
    app.set('io', io);

    io.on('connection', (clientSocket) => {
        console.log('Client connected');

        clientSocket.on('audioData', (data) => {
            const recognitionResult = 'Recognition result from the server';
            clientSocket.emit('recognitionResult', recognitionResult);
        });

        clientSocket.on('disconnect', () => {
            console.log('Client disconnected');
        });
    });
}

module.exports = initSocket;

이후 socket.js에 위와 같이 socket 서비스를 구현한다.
clientSocket으로부터 요청이 들어오면 메세지로 변환하여 emit한다.

//Set Socket
const initSocket = require('./socket');
const http = require('http');
const server = http.createServer(app);
initSocket(server, app);

...

module.exports = app;

이후 위와 같이 socket을 init하여 connection을 대기하고 요청을 받는다.

실시간 음성 데이터 처리

...
	io.on('connection', (clientSocket) => {
        console.log('Socket Client connected');

        clientSocket.on('audioData', (data) => {
            console.log(data);
            // Buffer Data to Audio
            text = data;
            // Speech top Text Logic
            const recognitionResult = text;
            clientSocket.emit('recognitionResult', recognitionResult);
        });

        clientSocket.on('disconnect', () => {
            console.log('Socket Client disconnected');
        });
    });
}

아직은 인공지능 로직이 구현되어 있지 않기 때문에 주석으로 이를 표시한다.
현재는 ArrayBuffer를 그대로 돌려주어 통신을 확인할 수 있도록 한다.


Vue.js 프론트엔드 구축

기본 UI 구성

<template>
  <div class="background d-flex justify-content-center align-items-center vh-100">
    <div class="card-container text-center">
      <img src="@/assets/logo.png" alt="Logo" class="card-item logo-img mb-3" />
      <button @click="startRecording" class="card-item btn btn-primary rounded-pill mb-2">음성 인식 시작</button>
      <button @click="stopRecording" class="card-item btn btn-danger rounded-pill">음성 인식 중단</button>
    </div>
  </div>
</template>
<style>
body {
  background-color: #121212;
  margin: 0;
}

.background {
  height: 100vh;
}

.card-container {
  border-radius: 20px;
  width: 30%;
  height: 50%;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #1e1e1e;
}

.card-item {
  margin: 15px;
}

.logo-img {
  border-radius: 100px;
  width: 150px;
}

.btn {
  font-size: 20px;
  border-radius: 10px;
  background-color: #337ea9;
  color: white;
}
</style>

socket.io 연결

yarn add socket.io-client

vue.js에서 socket을 사용할 수 있도록 패키지를 설치한다.

import { createApp } from 'vue';
import App from './App.vue';
import io from 'socket.io-client';

const socket = io(process.env.VUE_APP_BACKEND_LOCALHOST, {
    transports: ["websocket"],
    withCredentials: true,
});

const app = createApp(App);
app.provide('socket', socket);
app.config.productionTip = false;

app.mount('#app');

main.js에서 위와 같이 app에 socket을 사용할 수 있도록 한다.
또한 withCredentials를 설정하여 CORS 에러에 대응할 수 있도록 한다.

<script>
import { inject } from 'vue';

export default {
  data() {
    return {
      socket: null,
      ...
    };
  },
  methods: {
  ...
  mounted() {
    this.socket = inject('socket');
    this.socket.on('recognitionResult', (result) => {
      this.recognitionResult = result;
      console.log(this.recognitionResult);
    });
  },
};
</script>

inject('socket')으로 main.js에서 기존에 정의된 socket 객체를 가져온다.

실시간 음성 데이터 처리

<script>
import { inject } from 'vue';

export default {
  data() {
    return {
      socket: null,
      recognitionResult: '',
      mediaRecorder: null,
      isRecording: false,
      logoImageSrc: require('@/assets/logo.png'),
    };
  },
  methods: {
    async startRecording() {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      this.mediaRecorder = new MediaRecorder(stream);

      this.mediaRecorder.ondataavailable = (event) => {
        if (event.data.size > 0) {
          this.socket.emit('audioData', event.data);
        }
      };

      this.mediaRecorder.onstop = () => {
        stream.getTracks().forEach(track => track.stop());
        this.isRecording = false;
      };

      this.mediaRecorder.start();
      this.isRecording = true;

      this.sendAudioDataInterval = setInterval(() => {
        this.mediaRecorder.requestData();
      }, 1000);
    },

    stopRecording() {
      this.isRecording = false;
      if (this.mediaRecorder.state === 'recording') {
        this.mediaRecorder.stop();
      }
      clearInterval(this.sendAudioDataInterval);
    },
  },
  mounted() {
    this.socket = inject('socket');
    this.socket.on('recognitionResult', (result) => {
      const enc = new TextDecoder("utf-8");
      this.recognitionResult = enc.decode(result);
    });
  },
};
</script>

...
.subtitles {
  position: fixed;
  bottom: 100px;
  left: 50%;
  transform: translateX(-50%);
  color: #121212;
  background-color: white;
  font-size: 18px;
  max-width: 300px;
  max-height: 50px;
}
</style>

startRecording() 함수를 호출하면 setInterval을 통해 1초에 한번씩 this.mediaRecorder.requestData();를 호출하여 socket에 emit한다

최종적으로 subtitles를 추가하고 현재는 enc.decode(result)으로 인코딩한 BufferedArray를 화면에 노출시켜서 자막 기능을 구현하였다.

0개의 댓글