[컴퓨터비전 프로젝트] 수어 양방향 소통 프로그램-2(끝)
서론
3월 7일 드디어 3차 프로젝트가 끝났다... 중간에 블로그를 쓸 시간이 없어 프로젝트가 끝나고 블로그를 작성한다ㅎㅎ 이전프로젝트 글에서는 google drive 데이터 업로드에서 끝이났는데 그 글을 이어서 작성해보도록 하겠습니다😊😊
데이터 업로드-2
우리 팀은 2월 26일부터 google drive에 데이터를 업로드하기로 하였다 keypoint 팀은 keypoint 데이터를 영상팀은 원천 데이터인 영상을 올리기로 하였는데 문제는 keypoint가 전부 json 형태로 되어있었는데 이 데이터의 양이 너무 많아 아무리 5명의 사람만 학습시키다고 하더라도 데이터 업로드를 하는 데에 시간이 오래 걸린다는 점이었다.
한 폴더당 15,000개의 json 파일이 있었고 한 단어당 keypoint json 파일이 5개당 있었다 파일이 5개인 이유는 위아래 정면 왼쪽 옆쪽에서 찍은 동영상들의 keypoint 파일이었고 우리는 어차피 핸드폰을 정면에 놓고 사용한다는 가정하에 작업을 진행할 예정이라 정면의 데이터만 사용하기로 했다. 하지만... 한 명당 json 파일은 어마어마했고.. 한 폴더다 드라이브에 올리는 시간은 약 60시간 정도가 소요되었다 그래서 우리 팀은 학원에 있는 컴퓨터를 켜고 3일 내내 드라이브에 업로드를 진행하고 있는 중 2월 29일.... 다른 팀 팀원들이 우리가 업로드하던 인터넷 창을 꺼버리는 엄청난 일이 일어났다.
3월 1일 나랑 keypoint팀 팀원 한명은(엄씨) 이 keypoint를 계속 업로드하는것보다 다른 방법을 찾는게 더 나을거같다는 판단이 들어 우선 기존에 올라간 데이터중 1개의 keypoint 데이터를 파악해보았다. 확인해보니 mediapipe로 뽑은 keypoint와 aihub에 있는 keypoint를 대조해보았는데.. 이럴수가 이 둘의 데이터가 일치하지않았다. 그래서 우리는 aihub에 있는 데이터를 그냥 아예 사용하지 말자는 판단을 하였고 비슷한 프로젝트를 진행한 사람들의 논문을 찾아보았다.
다른 학습방법 찾기
열심히 논문을 찾아본 결과
https://kw.dcollection.net/public_resource/pdf/200000651674_20240306151530.pdf
위 논문을 찾을 수 있었고 우리는 위에 논문을 읽어본 후 위에 방법대로 keypoint를 이미지로 만들고 그 이미지를 학습시키기로 하였다.
위 방법은 시공간지도사상기법을 이용한 키포인트 학습이며 여기서 시공간지도사상기법은 각 프레임 별 신체 키 포인트 의 x축,y축, 정확도를 정규화 하여 각각을 색상 좌표로 변환하여 픽셀에 매핑하는것이다.
전처리 과정
학습을 하기전에 우선 몇가지의 전처리 과정이 필요한데 그 과정들은
1️⃣ 영상에서 keypoint 추출
2️⃣ X, Y 좌표 정규화
3️⃣ 키포인트 픽셀 매핑
4️⃣ 시공간 지도 생성
이렇게 4가지 과정이 있다. 우선 영상들을 불러와야하는데 우리는 영상팀이 드라이브에 올린 영상들을 이용하였다.
from google.colab import drive
drive.mount('/content/drive')
우선 위 코드를 사용하여 코랩에 google drive를 마운트해주고 코랩에는 mediapipe와 cv2가 기본적으로 깔려있지 않기때문에 우선 install을 해주었고 아래 모듈들을 import해주었다
pip install mediapipe
pip install opencv-python
import cv2
import mediapipe as mp
import json
import numpy as np
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import os
1️⃣ 영상에서 keypoint 추출
import cv2
import mediapipe as mp
mp_hands = mp.solutions.hands
def video_landmark_dic(path):
# 영상 파일 열기
cap = cv2.VideoCapture(path)
# Mediapipe Hands 모델 초기화
landmark_dic = {}
landmark_x = []
landmark_y = []
with mp_hands.Hands(static_image_mode=False, max_num_hands=2, min_detection_confidence=0.5, min_tracking_confidence=0.5) as hands:
# 영상에서 첫 번째 프레임만 읽기
while cap.isOpened():
success, image = cap.read()
if not success:
break
# Mediapipe에 이미지 전달
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = hands.process(image_rgb)
frame_number = cap.get(cv2.CAP_PROP_POS_FRAMES)
# 손이 감지된 경우
if results.multi_hand_world_landmarks:
for hand_landmarks, handedness in zip(results.multi_hand_world_landmarks, results.multi_handedness):
# 손의 라벨과 스코어 출력
hand_label = handedness.classification[0].label
hand_score = handedness.classification[0].score
# 각 랜드마크의 x, y 좌표와 손의 정확도, 프레임 번호, 키포인트 번호를 딕셔너리에 저장
for idx, landmark in enumerate(hand_landmarks.landmark):
landmark_x.append(landmark.x)
landmark_y.append(landmark.y)
landmark_dic[len(landmark_dic) + 1] = {'X': landmark.x, 'Y': landmark.y, 'C': hand_score, 't': int(frame_number), 'n': idx}
X_max, X_min, Y_max, Y_min = max(landmark_x), min(landmark_x), max(landmark_y), min(landmark_y)
cap.release()
return landmark_dic, X_max, X_min, Y_max, Y_min
나중에 사용하기 편하게 모든 작업을 함수로 만들어주었다. cv2로 영상 파일을 열어준 후 mp_hands 객체에 있는 Hands를 사용하여 좌표와 정확도를 뽑았고 영상에서 프레임 수를 뽑아 아래 형태로 딕셔너리를 만들어주었다.
2️⃣ X, Y 좌표 정규화
그리고 정규화를 위해 max(), min() 함수를 이용하여 각 x축,y축 keypoint의 최대값 최소값을 뽑아주었다.
def dic_normalization(dic):
for idx, (k, v) in enumerate(dic.items()):
X = (dic[k]['X'] - X_min) / (X_max - X_min)
Y = (dic[k]['Y'] - Y_min) / (Y_max - Y_min)
dic[k]['X'] = X
dic[k]['Y'] = Y
return dic
정규화 코드는 위에처럼 작성해주었다.
3️⃣ 키포인트 픽셀 매핑
위에서 정규화한 키포인트를 이미지로 만들기위해 xy좌표계에서 RGB 색상 좌표계로 변환하는 과정을 진행해준다.
def mapping_fn(norm_dic):
for i, (k, v) in enumerate(norm_dic.items()):
R = norm_dic[k]['X'] * 255
G = norm_dic[k]['Y'] * 255
B = norm_dic[k]['C'] * 255
norm_dic[k]['X'] = R
norm_dic[k]['Y'] = G
norm_dic[k]['C'] = B
return norm_dic
4️⃣ 시공간 지도 생성
def save_image_from_dict(data, output_path):
# 이미지 크기 설정
max_x = max(data[key]['n'] for key in data)
# max_y = max(data[key]['t'] for key in data)
# image_width = int(max_x) + 1 # x 좌표의 최댓값 + 1
# image_height = int(max_y) + 1 # y 좌표의 최댓값 + 1
image_width = 224
image_height = 224
# 이미지 생성 (흰색 배경)
image = np.full((image_height, image_width, 3), 0, dtype=np.uint8)
# 데이터를 이미지에 플로팅
for key, value in data.items():
# C 값을 BGR 색상으로 변환
color_r = value['X'] # X 값을 R로 사용
color_g = value['Y'] # Y 값을 G로 사용
color_b = value['C']
x = value['n'] # n 값을 x좌표로 사용
y = value['t'] # t 값을 y좌표로 사용
# 이미지에 점 그리기
cv2.circle(image, (x, y), radius=2, color=(color_b, color_g, color_r), thickness=-1)
# 이미지 저장
cv2.imwrite(output_path, image)
print(f"Image saved to {output_path}")
# 주어진 데이터를 사용하여 이미지 생성 및 저장
save_image_from_dict(mapping_dic, '/content/drive/MyDrive/KDT 3차 프로젝트/데이터_2/NIA_SL_WORD0001_REAL01_F.png')
numpy로 먼저 하얀색 빈 이미지를 생성해주고 cv2.circle로 만들어준 흰색 이미지에 점을찍어 색상 이미지를 만들어주고 만든 시공간지도를 cv2.imwrite()를 이용하여 드라이브에 이미지를 저장해주었다.
이렇게 준비된 시공간지도 데이터를 확인해보면 아래와 같은 이미지처럼 나온다 아래 이미지는 3개의 수어 동영상을 시공간지도로 만든건데 정규화를 진행했기때문에 비슷한 이미지로 생성되었다.
여기서 정규화를 진행해 준 이유는 보통 AI 허브에서 다운로드한 자료들은 깔끔하게 정제되어 있는 데이터들이 만지만 실제 사용할 데이터들은 사람이 왼쪽에 있을 수도 있고 오른쪽에도 있을 수가 있어 좌표값이 다 달라지기 때문에 사람이 왼쪽에 있던 오른쪽에 있던 다 비슷한 이미지를 만들어주기 위해 정규화를 진행해 주었다.
데이터 학습
위에 만든 시공간지도를 가지고 학습을 진행해야하는데 우선 이미지 학습이기때문에 CNN, RNN 등을 이용하여 학습을 진행하기로하였다. 사실 더 많이 알아보고 학습을 진행하고싶었지만... 시간이 너무 부족한 관계로 이전에 학원 실습 시 사용했던 AlexNet, ResNet 둘중 하나를 이용하여 학습을 진행하기로 하였다.
AlexNet과 ResNet을 비교해본 결과 정확도가 더 좋은 모델은 ResNet이라고하여 ResNet으로 학습을 진행하기로하였다. 여기서 우리가 학습할 단어는 3000개인데 ResNet은 최종 output이 1000개라서 3000개의 단어 중 982개의 단어만 정제하여 학습을 시키기로 하였다.
데이터 학습 과정
1️⃣ 학습, 벨리데이션 경로 및 데이터셋 생성
2️⃣ 데이터로더 생성
3️⃣ ResNet 모델 커스텀
4️⃣ 학습 및 validation
1️⃣ 학습, 벨리데이션 경로 및 데이터셋 생성
import os
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
from torchvision import datasets, models, transforms
from torch.utils.data import DataLoader
필요한 모듈을 import하고 train, validation 경로를 설정한다.
from google.colab import drive
drive.mount('/content/drive')
train_dir = os.listdir('/content/drive/MyDrive/KDT 3차 프로젝트/ImageFolder/train')
val_dir = os.listdir('/content/drive/MyDrive/KDT 3차 프로젝트/ImageFolder/validation')
우리팀은 GPU를 사용할 예정이라 device 변수를 선언해주었다.
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(device)
이미지 증강기법을 이용하여 불러온 데이터셋을 tensor로 변경해주었다.
# 이미지 증강 기법
# data_trainsforms
data_trainsforms = {
'train': transforms.Compose([
transforms.ToTensor()
]),
'validation': transforms.Compose([
transforms.ToTensor()
])
}
def target_transforms(target):
return torch.FloatTensor([target])
image_datasets = {
'train': datasets.ImageFolder('/content/drive/MyDrive/KDT 3차 프로젝트/ImageFolder/train', data_trainsforms['train']),
'validation': datasets.ImageFolder('/content/drive/MyDrive/KDT 3차 프로젝트/ImageFolder/validation', data_trainsforms['validation']),
}
2️⃣ 데이터로더 생성
train, validation batch_size는 16 train은 셔플을 true로 해주고 validation은 셔플을 false로 설정해주었다.
dataloaders = {
'train': DataLoader(
image_datasets['train'],
batch_size=16,
shuffle=True
)
,
'validation': DataLoader(
image_datasets['validation'],
batch_size=16,
shuffle=False
)
}
3️⃣ ResNet 모델 커스텀
위에서도 말했지만 기존 ResNet은 output이 1000개이기때문에 우리 모델에 맞추려면 모델 fc부분을 수정해주어야한다. 그래서 우리팀은 ResNetCustom이라는 클래스를 만들어 모델을 수정해주었다.
class ResNetCustom(nn.Module):
def __init__(self, num_classes=982):
super(ResNetCustom, self).__init__()
self.resnet = models.resnet50(pretrained=True)
self.resnet.fc = nn.Linear(2048, num_classes)
def forward(self, x):
x = self.resnet.conv1(x)
x = self.resnet.bn1(x)
x = self.resnet.relu(x)
x = self.resnet.maxpool(x)
x = self.resnet.layer1(x)
x = self.resnet.layer2(x)
x = self.resnet.layer3(x)
x = self.resnet.layer4(x)
x = self.resnet.avgpool(x)
x = torch.flatten(x, 1)
x = self.resnet.fc(x)
return x
# 모델 생성
model = ResNetCustom().to(device)
# 모델 출력
print(model)
4️⃣ 학습 및 validation
optimizer를 Adam으로 설정해주고 학습률을 0.001로 설정해주었다. 위에 만들어준 모델을 이용하여 10번정도 에폭을 돌려 학습을 진행해주었다.
epochs = 10
optimizer = optim.Adam(model.resnet.fc.parameters(), lr=0.001)
for epoch in range(epochs + 1):
for phase in ['train', 'validation']:
if phase == 'train':
model.train()
else:
model.eval()
sum_losses = 0
sum_accs = 0
for x_batch, y_batch in dataloaders[phase]:
x_batch, y_batch = x_batch.to(device), y_batch.to(device)
y_pred = model(x_batch).to(device)
loss = nn.CrossEntropyLoss()(y_pred, y_batch)
if phase == 'train':
optimizer.zero_grad()
loss.backward()
optimizer.step()
sum_losses += loss.item() # loss.item()을 사용하여 loss 값을 가져옵니다.
y_prob = nn.Softmax(1)(y_pred)
y_pred_index = torch.argmax(y_prob, axis=1)
acc = (y_batch == y_pred_index).float().sum() / len(y_batch) * 100
sum_accs += acc.item() # acc.item()을 사용하여 acc 값을 가져옵니다.
avg_loss = sum_losses / len(dataloaders[phase])
avg_acc = sum_accs / len(dataloaders[phase])
print(f'{phase:10s}: Epoch {epoch+1:4d}/{epochs} Loss: {avg_loss:.4f} Accuracy: {avg_acc:.2f}%')
위 코드대로 학습을 진행하니까 에폭을 10번 돌았을 때 loss가 1.00%로 떨어지고 Accuracy가 40%대로 올라갔지만 Accuracy가 너무 낮아 학습이 잘 되지 않았다는것을 확인하였다....
하지만 우리팀이 해당 모델을 만든 시점이 마감 이틀전이라 우선 학습은 여기서 그만하고 모델을 실행할 프론트 부분을 작업하기로 하였다.
프론트 및 서버 작업
팀원 5명 중에 2명(나, 엄 씨)만 리액트 네이티브를 1차 프로젝트 때 해보았기 때문에 우리 둘이 프론트 부분을 담당하고 나머지 3사람이 python 서버를 진행하기로 하였다. 사실 1차 때 리액트 네이티브를 사용해 봤다고 하지만.. 웹뷰 방식으로 진행하였기 때문에 순수 리액트 네이티브로 진행해 보는 것은 처음이었다.ㅠㅠㅠ😂😂😂😂😂😂😂
마감이 3일 남은 상태에서 아무것도 모르는 리액트 네이티브를 사용해 보려고 하니까.. 진짜 눈앞이 캄캄했다... 우리 팀은 카메라를 사용해야 하고 앱 배포를 진행하려고 했기 때문에 리액트를 꼭 서야했다.. 그래서 인터넷에 열심히 검색하여 리액트 프로젝트를 만들고 카메라를 연동하는것을 성공했다!!
그치만 문제가 또 발생하였는데... 카메라 연동은 하였으나 녹화가 되지 않는 것이었다.
확인해보니 카메라 권한만 받았고 마이크, 음성, 갤리리 권한을 받지 않아서 녹화가 진행되지 않는것이었다. 그래서 나랑 엄씨는 아래 코드를 이용하여 권한을 받는데에 성공하여 녹화를 성공적으로 진행하였다.
import { Camera } from 'expo-camera';
import { Video } from 'expo-av';
import * as MediaLibrary from 'expo-media-library';
useEffect(() => {
(async () => {
const cameraPermission = await Camera.requestCameraPermissionsAsync();
const microphonePermission = await Camera.requestMicrophonePermissionsAsync();
const mediaLibraryPermission = await MediaLibrary.requestPermissionsAsync();
setHasCameraPermission(cameraPermission.status === "granted");
setHasMicrophonePermission(microphonePermission.status === "granted");
setHasMediaLibraryPermission(mediaLibraryPermission.status === "granted");
})();
}, []);
이렇게 모든것이 성공적으로 끝나는가 싶었지만.. 세상은 그렇게 쉽게 흘러가지 않았고.. 또 다시 나한테 시련을 안겨주었다...😂😂😂😂😂😂😂 이제 마감이 이틀 남았는데.. 문제를 파악하고 오류를 해결해야한다니 눈앞이 캄캄했다...
발생한 오류는 카메라로 찍은 영상객체가 python 서버로 넘어가지 않는 부분이었는데 확인해보니 해당 영상의 객체가 undifined로 나와 넘어가지 않는거였다. log를 찍어 확인해본 결과 뒤로가기를 누르거나 앱을 껐다가 키면 영상객체가 제대로 생성되는것을 확인하였다. 이 문제를 해결하기위해 이 방법 저 방법 시도해보았지만 결국 나 혼자서는 오류를 해결하지 못하였고 그래서 엄씨한테 오류를 공유하고 같이 문제를 풀기로하였다.
한참 오류와 씨름하고있는데 우리의 엄씨가 유튜브를 보고! 카메라 연동하는 코드를 찾아 해당 코드로 다시 만들어보더니 말끔하게 오류가 해결된 모습을 확인할 수 있었다.
아래 코드는 우리가 유튜브에서 가지고온 코드와 python서버로 파일을 전달하는 코드를 작성한것이다.
import { StyleSheet, Text, View, Button, SafeAreaView, TouchableOpacity, Dimensions } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useEffect, useState, useRef } from 'react';
import { Camera } from 'expo-camera';
import { Video } from 'expo-av';
import { shareAsync } from 'expo-sharing';
import * as MediaLibrary from 'expo-media-library';
import axios from 'axios';
import * as FileSystem from 'expo-file-system';
import { styles } from './styles';
const { width } = Dimensions.get('window');
import * as Speech from 'expo-speech';
export default function CameraScreen() {
const navigation = useNavigation(); // navigation 객체 생성
let cameraRef = useRef();
const [hasCameraPermission, setHasCameraPermission] = useState();
const [hasMicrophonePermission, setHasMicrophonePermission] = useState();
const [hasMediaLibraryPermission, setHasMediaLibraryPermission] = useState();
const [isRecording, setIsRecording] = useState(false);
const [getAnswer] = useState(false);
const [video, setVideo] = useState();
const [showSavedMessage, setShowSavedMessage] = useState(false);
const [translatedText, setTranslatedText] = useState('');
useEffect(() => {
(async () => {
const cameraPermission = await Camera.requestCameraPermissionsAsync();
const microphonePermission = await Camera.requestMicrophonePermissionsAsync();
const mediaLibraryPermission = await MediaLibrary.requestPermissionsAsync();
setHasCameraPermission(cameraPermission.status === "granted");
setHasMicrophonePermission(microphonePermission.status === "granted");
setHasMediaLibraryPermission(mediaLibraryPermission.status === "granted");
})();
}, []);
if (hasCameraPermission === undefined || hasMicrophonePermission === undefined) {
return <Text>Requestion permissions...</Text>
} else if (!hasCameraPermission) {
return <Text>Permission for camera not granted.</Text>
}
let recordVideo = () => {
setIsRecording(true);
let options = {
quality: "1080p",
maxDuration: 60,
mute: false
};
cameraRef.current.recordAsync(options).then((recordedVideo) => {
setVideo(recordedVideo);
setIsRecording(false);
});
};
let stopRecording = () => {
setIsRecording(false);
cameraRef.current.stopRecording();
if (video) {
const getBase64 = async (videoUri) => {
try {
console.log(videoUri)
const fileData = await MediaLibrary.getAssetInfoAsync(videoUri, { mediaType: 'video' });
return fileData.base64;
} catch (error) {
console.error("Error getting base64:", error);
}
};
const uploadFileToServer = async (fileUri) => {
console.log(fileUri);
try {
// 파일을 읽어와서 바이너리 데이터로 변환
const fileInfo = await FileSystem.getInfoAsync(fileUri);
if (!fileInfo.exists) {
throw new Error('File does not exist');
}
const fileData = await FileSystem.readAsStringAsync(fileUri, {
encoding: FileSystem.EncodingType.Base64,
});
// FormData에 파일 데이터 추가
const formData = new FormData();
formData.append('video', {
uri: fileUri,
type: 'video/mp4', // 파일 형식에 따라 변경
name: 'video.mp4', // 파일 이름에 따라 변경
data: fileData,
});
// 서버로 파일 전송
const response = await axios.post('http://192.168.162.23:5000/file', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
// setTranslatedText(response.data);
Speech.speak(response.data);
} catch (error) {
console.error('Error uploading file:', error);
throw error;
}
};
setShowSavedMessage(true);
setTimeout(() => {
setShowSavedMessage(false);
}, 2000);
MediaLibrary.saveToLibraryAsync(video.uri).then(() => {
uploadFileToServer(video.uri)
});
}
};
let goBack = () => {
navigation.goBack(); // 뒤로가기 버튼 클릭 시 뒤로 이동
};
if (video) {
let shareVideo = () => {
shareAsync(video.uri).then(() => {
setVideo(undefined);
});
};
}
return (
<Camera style={styles.container} ref={cameraRef} type={'front'}>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button} onPress={isRecording ? stopRecording : recordVideo}>
<Text style={styles.buttonText}>{isRecording ? "번역하기" : "시작하기"}</Text>
</TouchableOpacity>
</View>
<TouchableOpacity style={styles.backButton} onPress={goBack}>
<Text style={styles.backButtonText}>← 뒤로가기</Text>
</TouchableOpacity>
{showSavedMessage && (
<Text style={styles.savedMessage}>저장되었습니다.</Text>
)}
</Camera>
);
}
우리는 메인화면에서 "수어 번역 시작하기" 버튼을 클릭하면 카메라 페이지로 이동되고 카메라 페이지에서 시작하기 버튼을 누르면 녹화가 되고 번역하기 버튼을 누르면 동영상이 갤러리에 저장되고 수어가 번역되어 음성으로 출력되게 작업을 진행하였다.
위 이미지는 실제로 우리팀에서 만든 어플 화면이다 기본적으로 카메라는 전면카메라로 고정해놓았다.
1️⃣ App.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import 'react-native-gesture-handler';
import HomeScreen from './HomeScreen.js'
import CameraScreen from './CameraScreen.js';
const Stack = createStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} options={{ headerShown: false }} />
<Stack.Screen name="Camera" component={CameraScreen} options={{ headerShown: false }} />
</Stack.Navigator>
</NavigationContainer>
);
}
2️⃣Home.js
// HomeScreen.js
import React from 'react';
import { View, TouchableOpacity, Text, Image } from 'react-native';
import { styles } from './styles';
export default function HomeScreen({ navigation }) {
return (
<View style={styles.container}>
<Image source={require('./logo.png')} style={styles.logo} />
<Text>모두를 위한 언어:</Text>
<Text><Text style={styles.color}>소통</Text>의 장벽을 넘어서</Text>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button} onPress={() => navigation.navigate('Camera')}>
<Text style={styles.buttonText}>수어 번역 시작하기</Text>
</TouchableOpacity>
</View>
</View>
);
}
camera.js는 위에 있으므로 따로 또 올리지는 않으려고한다... 아무튼 위에 코드로 작업한 결과 카메라연동 녹화, 저장, tts까지 작업을 완료했다 이시점이 마감 하루전인데 우리는 stt까지 진행하려고 했기 때문에 엄씨와 같이 밤샘을 하기로 마음먹고 stt 작업을 시작하였다.
사실 녹음하고 저장하는것은 camera와 크게 다르지않다. 똑같이 권한을 확인하고 버튼을 누르면 녹음을 시작하고 다시 누르면 녹음이 저장되고 해당 파일을 서버에 전송한 후 python 서버에서 음성을 텍스트로 변환하여 return을 해주면 된다고 생각했다.
우선 녹음하고 python서버로 보내는건 어렵지않게 성공했다 아무래도 한번 해봤기때문에..ㅎㅎ 생각보다 쉽게 끝났는데 문제는... 서버로 전송된 파일이 텍스트로 변환되지 않는 오류가 발생했다.
확인해보니 기본적으로 녹음파일은 m4a 확장자로 변환되는데 우리가 사용한 코드에서는 m4a 확장자 사용이 불가능해서 오류가 발생한것이었다.. 그래서 우리는 mp4또는 wav파일로 변환 후 텍스트를 뽑아내기로 하였다.
# 음성으로 들어온 내용을 텍스트로 변환하는 과정
import speech_recognition as sr
from pydub import AudioSegment
import os
from openai import OpenAI
from dotenv import load_dotenv
from pyffmpeg import FFmpeg
load_dotenv('./.env')
# .env 파일에서 OPENAI_API_KEY 가져오기
openai_api_key = os.getenv("OPENAI_API_KEY")
# OpenAI 클래스 인스턴스 생성
client = OpenAI(api_key=openai_api_key)
input_path = './audio.m4a'
output_path = './audio.wav'
def convert_to_wav(input_path, output_path):
ffmpeg = FFmpeg()
ffmpeg.convert(input_path, output_path)
def speach_to_text():
convert_to_wav(input_path, output_path)
r = sr.Recognizer()
with sr.AudioFile('./audio.wav') as source:
audio_data = r.record(source, duration=120)
try:
text = r.recognize_google_cloud(audio_data=audio_data, language='ko-KR')
return text
except sr.UnknownValueError:
print("Google Cloud Speech-to-Text could not understand the audio.")
return None
except sr.RequestError as e:
print(f"Could not request results from Google Cloud Speech-to-Text service; {e}")
return None
처음에는 pip install pydub 를 이용하여 변환을 진행하였다.
import pydub
sound = pydub.AudioSegment.from_wav("audio.m4a")
sound.export("audio.wav", format="wav")
변환은 성공적으로 진행되었으나. 파일이 깨졌는지 텍스트로 정상적으로 변환되지않았다. 그래서 확인해보니
from pyffmpeg import FFmpeg
input_path = './audio.m4a'
output_path = './audio.wav'
def convert_to_wav(input_path, output_path):
ffmpeg = FFmpeg()
ffmpeg.convert(input_path, output_path)
FFmpeg로도 변환할 수 있다고하여 위 코드를 사용하여 변환을 진행하였으나... 이것도 파일이 깨져 텍스트로 변환되지않았다.. 더 해보고싶었지만 우리는 아직 발표 ppt도 다 만들지 못하여 tts까지 작업을 진행하고 ppt 작업을 진행하였다..
프로젝트 후기
프로젝트 기간이 2주로 너무 짧아 모델도 프론트도.. 완벽하게 하지못한게 너무 아쉬웠지만 그래도 우리가 생각했던 앱을 만들 수 있어서 좋았다. 한 일주일만 더 있었다면... 이라는 생각도 들기는 했지만 이번 프로젝트를 기반으로 다음에 프로젝트를 진행할때에는 시간을 잘 분배하여 더 완성도 높은 프로젝트를 해봐야겠다는 생각을 했다.
그리고 모델을 잘 만들지 못하여 마음이 안좋았는데 다음에는 모델을 더 공부하여 더 좋은 모델을 만들어보고싶다.😂😂😂😂