# [FCT Sharing - FCT0427] - ỨNG DỤNG HỖ TRỢ GIAO TIẾP NGƯỜI KHIẾM THÍNH - *1*
**README:** :point_down:
:::spoiler
:::warning
:warning: **ĐÂY LÀ DỰ ÁN THAM GIA "KÌ THI KHOA HỌC KĨ THUẬT DÀNH CHO HỌC SINH TRUNG HỌC NĂM 2023-2024" CỦA EM VÀ MỘT ANH CHUNG TRƯỜNG Ở TỈNH BÌNH THUẬN (vào ngày 23-24/12/2023).** \
|| Và một tin vui là mặc dù dự án còn nhiều thiếu sót và thời gian làm rất ngắn nhưng bọn em đã giành được Giải Tư của Tỉnh. [LINK](https://baobinhthuan.com.vn/khoi-day-dam-me-sang-tao-khoa-hoc-ky-thuat-cho-hoc-sinh-trung-hoc-115697.html) ||.\
Lí do là bài này khá dài nên ở Chapter 1 em sẽ giới thiệu và chủ yếu chú tâm vào một vài tính năng của ứng dụng.
:::
### Giới Thiệu:
:::info
:bulb: **LÍ DO CHỌN ĐỀ TÀI NÀY?**\
Đầu tiên, thì hẳn ai cũng biết khiếm thính là gì?\
Người khiếm thính thường sẽ bị ảnh hưởng nhiều đến cuộc sống của họ đặc biệt là trong giao tiếp đời sống hằng ngày, xem những video clip trên mạng Internet hay các nền tảng mạng xã hội khác.\
Từ lúc bắt đầu học lập trình em đã thử tự hỏi có cách nào để giúp những người đó tiếp nhận thông tin âm thanh hiệu quả không ? Nhưng vì lúc đó kiến thức em vẫn hạn hẹp nên vẫn chưa thể làm hiện tại thì cũng thế :smiley: nhưng em cùng với một anh khác đã hợp sức làm công cụ để hỗ trợ chính những người khiếm thính đó.
:::
### Mở Đầu:
___
#### Nghiên Cứu:
Em thử đặt ra câu hỏi:
:::success
:question: **Có cách nào để người khiếm thính giao tiếp với đời sống và tiếp nhận nội dung giao tiếp không?**
:::
Và rồi trong quá trình nghiên cứu và tìm hiểu thì em đã biết đến ngôn ngữ kí hiệu. Mặc dù có những cái lợi bên cạnh đó thì hạn chế lớn nhất của ngôn ngữ kí hiệu là số lượng người biết và sử dụng vẫn là rất ít. Nên là có thể ứng dụng chỉ hướng đến người khiếm thính biết ngôn ngữ kí hiệu.
___
#### Cách Hoạt Động:

___

Ở trên là cách hoạt động của ứng dụng\
|| em để tạm cái flow này, có gì em sửa lại nó sau ||
Ở nội dung của phần bài này em sẽ hướng đến tính năng chuyển từ âm thanh (system audio, microphone) sang ngôn ngữ kí hiệu.
#### Một Số Thứ Được Dùng:
:::danger
**Ngôn Ngữ Lập Trình:** `python`\
**Thư Viện:**
```
# Nhiều quá nên có gì mọi người đọc source nhe!
```
**Các Tài Nguyên Khác:** [QIPEDC](https://qipedc.moet.gov.vn/)\
**Chức Năng Chính:**
- Chuyển nội dung âm thanh sang ngôn ngữ kí hiệu.
- Chuyển nội dung văn bản sang ngôn ngữ kí hiệu.
- Nhận dạng ngôn ngữ kí hiệu và chuyển sang văn bản.
:::
### Thực Hiện:
#### Chuyển Từ Âm Thanh Thành NNKH:
Vấn đề đặt ra ở đây là em sẽ chuyển từ âm thanh sang văn bản đầu tiên, sau đó từ văn bản đó em sẽ call video nnkh từ assets.
Bắt đầu với file ghi nhận âm thanh thành text\
Ở đây em dùng ```SpeechRecognition 3.10.1```
> **File: `audio_record_data.py`**
```python=
# Import Libs
import soundcard as sc
import numpy as np
import io
import speech_recognition as sr
import wave
from queue import Queue
import pyaudio
from pydub import AudioSegment
import load_text as lt
```
```python=
# Settings Sth
SAMPLE_RATE = 48000
RECORD_SEC = 2
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 48000
global_trans_mic_text = ""
global_trans_sysaudio_text = ""
recorded_data = io.BytesIO()
queue_recorded_audio_data = Queue(maxsize=10)
recognizer = sr.Recognizer()
queue_recorded_audio_data_mic = Queue(maxsize=10)
recorded_data_mic = io.BytesIO()
io_test = io.BytesIO()
global_text = ""
base = ""
mic_text = ""
MAX_SILENT_COUNT = 3 # max_silent_count * record_sec = latetime
MAX_LENGTH = 8
running_state = True
```
**Các hàm chức năng chính:**
- `record_audio()`: Ghi âm từ micro theo thông số đã thiết lập và trả về dữ liệu ghi âm.
- `write_data(data, addition)`: Ghi dữ liệu âm thanh ra một `io.BytesIO` dựa trên dữ liệu được cung cấp và thông số thêm vào (nếu có).
- `add_queue_data()`, `get_queue_data()`: Thêm dữ liệu vào hàng đợi và lấy dữ liệu từ hàng đợi, cụ thể là dữ liệu ghi âm.
- `get_tran_text()`: Chuyển đổi dữ liệu âm thanh thành văn bản bằng việc sử dụng thư viện `speech_recognition`.
- `method_tran(data, text_return)`: Hàm hỗ trợ cho việc chuyển đổi dữ liệu âm thanh thành văn bản.
- `save_audio(frames)`: Lưu dữ liệu âm thanh từ frames vào một `io.BytesIO`.
- `read_audio()`: Hàm chính để ghi và chuyển đổi âm thanh từ microphone thành văn bản, sử dụng `PyAudio` và `speech_recognition`.
- `concatenate_audio(byteio1, byteio2, max_length_seconds)`: Nối hai tệp âm thanh và giới hạn độ dài tối đa của tệp kết quả.
- `record()`: Ghi âm từ microphone và nối chúng lại với tệp đã ghi trước đó, sau đó xuất ra một tệp WAV tạm thời.
***Code:***
:::spoiler
```py=
#-----------------------------------------------------------------------------------------------
# record the audio
def record_audio():
with sc.get_microphone(id=str(sc.default_speaker().name), include_loopback=True).recorder(samplerate=SAMPLE_RATE) as mic:
recorded_data = mic.record(numframes=SAMPLE_RATE*RECORD_SEC)
return recorded_data[:,0]
# write out the data recorded -----------------------------------------------
def write_data(data,addition = 0):
audio_io = io.BytesIO()
with wave.open(audio_io,'wb') as wave_file:
wave_file.setnchannels(1)
wave_file.setsampwidth(RECORD_SEC+int(addition/SAMPLE_RATE))
wave_file.setframerate(SAMPLE_RATE)
wave_file.writeframes((data*32767).astype(np.int16).tobytes())
audio_io.seek(0)
return audio_io
#-------------------------------------------------------------------------------------------
def add_queue_data():
global queue_recorded_audio_data
queue_recorded_audio_data.put(record_audio())
def get_queue_data():
return write_data(queue_recorded_audio_data.get())
#---------------------------------------------------------------------------------------
def get_tran_text():
data = get_queue_data()
with sr.AudioFile(data) as source:
audio_data = recognizer.record(source)
try:
text = recognizer.recognize_google(audio_data,language = "vi-VN")
return text
except:
return ""
def method_tran(data,text_return):
global global_text,base
with sr.AudioFile(data) as source:
audio_data = recognizer.record(source)
try:
base = global_text
text = recognizer.recognize_google(audio_data,language = "vi-VN")
global_text = text
text_return[0] = text
except:
text_return[0] = ""
def save_audio(frames):
audio_io = io.BytesIO()
with wave.open(audio_io, 'wb') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(SAMPLE_RATE)
wf.writeframes(b''.join(frames))
audio_io.seek(0)
return audio_io
#mic main function --------------------------------------------------
def read_audio():
global SAMPLE_RATE
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paInt16,
channels=1,
rate=SAMPLE_RATE,
input=True,
frames_per_buffer=CHUNK)
frames =[]
rec = 2
silent_check = 0
def record_Nsec(rec):
nonlocal frames
for _ in range(0, int(SAMPLE_RATE / CHUNK * rec)):
data = stream.read(CHUNK)
frames.append(data)
def trans_check():
nonlocal silent_check,frames,rec
global mic_text
data = save_audio(frames)
return_data = [""]
method_tran(data,return_data)
if return_data[0] =="" or return_data[0] == " " or return_data[0]==mic_text:
silent_check +=1
else:
silent_check = 0
if silent_check < MAX_SILENT_COUNT:
record_Nsec(rec)
mic_text = return_data[0]
return_data = [""]
trans_check()
else:
lt.add_vid_buffer(mic_text)
frames = []
mic_text = ""
if running_state:
silent_check = 0
record_Nsec(2)
trans_check()
record_Nsec(2)
trans_check()
stream.stop_stream()
stream.close()
p.terminate()
# connect 2 audio with maxlength -----------------------------------------------------------------
def concatenate_audio(byteio1, byteio2, max_length_seconds):
audio1 = AudioSegment.from_file(byteio1, format="wav")
audio2 = AudioSegment.from_file(byteio2, format="wav")
duration_audio1 = len(audio1) / 1000
excess_duration = duration_audio1 - max_length_seconds
if excess_duration > 0:
audio1 = audio1[int(excess_duration * 1000):]
concatenated = audio1 + audio2
output_bytesio = io.BytesIO()
concatenated.export(output_bytesio, format="wav")
output_bytesio.seek(0)
return output_bytesio
def record():
global MAX_LENGTH,new,io_test
new = write_data(record_audio())
io_test = concatenate_audio(io_test,new,MAX_LENGTH)
with open("temp.wav","wb") as wav:
wav.write(io_test.getvalue())
```
:::
---
> **File: `load_text.py`**
```python=
# Import
import csv
from queue import Queue
import os
import create_window as cw
import math
```
```python=
data_array = []
#asset dir------------------------------
csv_file_path = 'index.csv'
letter_dir ="letter_assets" #folder chứa chữ cái
asset_dir = "asset" # folder chứa từ
stress_dir = "stress" # folder chứa dấu âm
```
Hình dung một cách là khi mà từ ngữ có trong từ điển `index.csv` thì nó sẽ gọi video của từ đấy ra trong `asset`. Còn trường hợp từ ngữ đó không có thì chương trình sẽ bắt đầu đánh vần sử dụng của `letter_assets` và `stress`.
**Các hàm chức năng chính:**
- `convert_string(string)`: Chuyển đổi chuỗi sang dạng mã hóa UTF-8, sau đó chuyển về dạng "latin-1" để xử lý ký tự đặc biệt.
- `remove_space(text_data)`: Loại bỏ khoảng trắng từ đầu chuỗi.
- `delete_word(string)`: Xóa từ đầu tiên của chuỗi.
- `split_letter(word)`: Chia một từ thành các ký tự đơn lẻ.
- `check_text(text_data)`: Kiểm tra và trả về tên video và chuỗi còn lại sau khi loại bỏ từ đầu tiên.
- `check_letter(letter)`: Kiểm tra ký tự và thêm video tương ứng vào hàng đợi.
- `playback_speed_adjust(queue)`: Chịu trách nhiệm xử lý chuỗi văn bản và thêm các video tương ứng vào hàng đợi `vid_buffer` dựa trên dữ liệu từ file index và các tệp âm thanh và video đã được lưu trữ.
- `call_vid()`: Gọi để lấy tên video tiếp theo trong hàng đợi. Nếu hàng đợi không rỗng, nó sẽ trả về tên video; nếu không, nó sẽ trả về `None`.
:::spoiler
```python=
#main video buffer----------------------------------
vid_buffer = Queue() # global this buffer
#load index---------------------------------
with open(csv_file_path, newline='',encoding='utf-8') as csvfile:
csv_reader = csv.reader(csvfile)
for row in csv_reader:
data_array.append(row)
#preprocess string-----------------------------
def convert_string(string):
if string!= None:
encoded_string = string.encode("utf-8")
return encoded_string.decode("latin-1",errors='replace')
return None
def remove_space(text_data):
count = 0
if text_data !="":
while True:
count +=1
if count >=30:
break
if text_data[0] == " ":
text_data = text_data[1:]
else:
pass
return text_data
def delete_word(string): # delete first word of the string
word = string.split(maxsplit=1)
if len(word)>1:
return word[1]
else:
return ""
def split_letter(word):
return [letter for letter in word]
#check dir array------------------------------------
def check_text(text_data): #return vid name and remain string
#print(text_data)
# pre process data-------------------
count =0
if text_data !="":
while True:
count +=1
if count >=30:
break
if text_data[0] == " ":
text_data = text_data[1:]
else:
pass
#main logic----------------------------------------------
if text_data !="" or text_data !=" ":
for text in data_array:
if text_data.startswith(text[0]):
text_data = text_data.replace(text[0],"")
return text[0],text_data
return None,text_data
#letter check session -------------------------
sac_index = ["á","ắ","ấ","é","ế","ó","ố","ớ","í","ý","ú","ứ"]
huyen_index = ["à","ằ","ầ","è","ề","ò","ồ","ờ","ì","ỳ","ù","ừ"]
nang_index = ["ạ","ặ","ậ","ẹ","ệ","ọ","ộ","ợ","ị","ỵ","ụ","ự"]
nga_index = ["ã","ẵ","ẫ","ẽ","ễ","õ","ỗ","ỡ","ĩ","ỹ","ũ","ữ"]
hoi_index = ["ả","ẳ","ẩ","ẻ","ể","ỏ","ổ","ở","ỉ","ỷ","ủ","ử"]
letter_index = ["a","ă","â","e","ê","o","ô","ơ","i","y","u","ư"]
#main letter generate function---------------------------------
def check_letter(letter):
if letter !=" ":
if letter in sac_index:
letter_n = sac_index.index(letter)
vid_buffer.put(os.path.join(letter_dir,letter_index[letter_n]+".mp4"))
vid_buffer.put(os.path.join(stress_dir,"sắc"+".mp4"))
elif letter in huyen_index:
#call vid huyen
letter_n = huyen_index.index(letter)
vid_buffer.put(os.path.join(letter_dir,letter_index[letter_n]+".mp4"))
vid_buffer.put(os.path.join(stress_dir,"huyền"+".mp4"))
elif letter in nang_index:
#call vid nang
letter_n = nang_index.index(letter)
vid_buffer.put(os.path.join(letter_dir,letter_index[letter_n]+".mp4"))
vid_buffer.put(os.path.join(stress_dir,"nặng"+".mp4"))
elif letter in nga_index:
#call vid nga
letter_n = nga_index.index(letter)
vid_buffer.put(os.path.join(letter_dir,letter_index[letter_n]+".mp4"))
vid_buffer.put(os.path.join(stress_dir,"ngã"+".mp4"))
elif letter in hoi_index:
letter_n = hoi_index.index(letter)
vid_buffer.put(os.path.join(stress_dir,"hỏi.mp4"))
else:
vid_buffer.put(os.path.join(letter_dir,letter+".mp4"))
#playback speed adjust base one length of vid_buffer-------------------------
def playback_speed_adjust(queue):
base = 1.5
scaled_output = math.log(queue + 1, base) + 2
scaled_output = max(2, min(5, scaled_output))
return scaled_output
#string setting--------------------
text = ""
#main function for vid_buffer-------------------------------
def add_vid_buffer(text):
#set up
remain_text = text
remain_text = remain_text.replace("\n"," ")
remain_text = remain_text.rstrip()
while remain_text!="":
vid_name,remain_text = check_text(remain_text)
if vid_name != None:
vid_buffer.put(os.path.join(asset_dir,vid_name + ".mp4"))
#print(os.path.join(asset_dir,vid_name + ".mp4"))
else:
#call check letter function
word = remain_text.split()[0]
#print(word)
letter_array = split_letter(word)
for letter in letter_array:
check_letter(letter) #add buffer inside
remain_text = delete_word(remain_text)
cw.PLAYBACK_SPEED = playback_speed_adjust(vid_buffer.qsize())
#calling video function --------------------------------
def call_vid():
if not vid_buffer.empty():
vid_name = vid_buffer.get_nowait()
if vid_name!=None:
return vid_name
else:
return None
```
:::
---
> **File: `create_window.py`**
File này chịu trách nhiệm cho hiện video nnkh ra ngoài và luôn cho nó `topmost`. Cái này cũng như việc trên các chương trình ở Đài Truyền hình Việt Nam (VTV) có người hay đứng ở góc trái phía dưới dùng ngôn ngữ kí hiệu.
:::spoiler
```python=
# -*- coding: utf-8 -*-
#import part---------------------------------------
import tkinter as tk
import cv2
from PIL import Image, ImageTk
import load_text as lt
import os
from queue import Queue
video_window = None
save_position = "+0+0"
#setting----------------------------
WID,HEI = 200, 250
PLAYBACK_SPEED = 2 #1 for 24 frame/s
#video player class---------------------------------
class vid_player():
def __init__(self,video_window):
self.cap = None
self.canvas = None
self.is_playing = True
self.vid_dir = None
self.video_window = video_window
self.x = None
self.y = None
self.x1 = None
self.y1 = None
#click and drag function --------------------------------------------------
def on_left_click(self,event):
self.x, self.y = event.x, event.y
def on_left_drag(self,event):
self.x1, self.y1 = event.x, event.y
self.video_window.geometry(f"+{self.video_window.winfo_x() + (self.x1 - self.x)}+{self.video_window.winfo_y() + (self.y1 - self.y)}")
#re-update calling video method------------------------------------------
def refresh(self):
self.video_window.after(10,self.update_video)
#update video -------------------------------------------------------------
def update_video(self):
self.vid_dir = lt.call_vid()
if self.vid_dir !=None:
if not os.path.exists(self.vid_dir):
self.update_video()
else:
self.cap = cv2.VideoCapture(self.vid_dir)
self.play_video()
else:
self.refresh()
#show frame on window---------------------------------------
def play_video(self):
global PLAYBACK_SPEED
ret, frame = self.cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
cropped_frame = frame[53:720, 300:956] #crop
resized_frame = cv2.resize(cropped_frame, (WID, HEI))#resize frame to fit window
img = ImageTk.PhotoImage(Image.fromarray(resized_frame))
self.canvas.create_image(0, 0, anchor=tk.NW, image=img)
self.canvas.img = img
self.video_window.after(int(24/PLAYBACK_SPEED), self.play_video)
else:
image_path = "place_holder.jpg" #place holder
PLAYBACK_SPEED = 2 # reset to default
pil_image = Image.open(image_path)
pil_image = pil_image.crop((300,53,956,720))
pil_image = pil_image.resize((WID, HEI))
img = ImageTk.PhotoImage(pil_image)
self.canvas.create_image(0, 0, anchor=tk.NW, image=img)
self.canvas.img = img
self.update_video()
#start function --------------------------------------
def start(self):
self.canvas = tk.Canvas(self.video_window, width=WID, height=HEI) #set resolution
self.canvas.pack()
self.canvas.bind('<Button-1>', lambda event: self.on_left_click(event))
self.canvas.bind('<B1-Motion>', lambda event: self.on_left_drag(event))
self.update_video()
# destroy window------------------------------------------
def destroy():
global video_window,save_position
if video_window and video_window.winfo_exists():
save_position = video_window.geometry()[7:]
lt.vid_buffer = Queue()
video_window.destroy()
video_window = None
#open window------------------------------------------
def open_video_window():
global video_window
video_window = tk.Toplevel()
video_window.overrideredirect(True)
video_window.attributes('-topmost', True)
video_window.attributes('-alpha', 0.9)
video_window.geometry(save_position)
vid = vid_player(video_window=video_window)
vid.start()
```
:::
Xong! Chỉ cần gắn vào file GUI và chạy thôi.
Ở phần này em không đề cập đến file GUI vì nhiều thứ, nên em xin để lại phần sau và chỉ đề cập đến chức năng chính của ứng dụng.\
À lưu ý rằng ứng dụng chỉ hỗ trợ tiếng Việt thôi ạ.
**DEMO THỰC TẾ:**
[VIDEO DEMO](https://youtu.be/DtiLrZjPcnw)
Như vậy em đã chia sẻ một trong chức năng chính của ứng dụng mà em đã làm. Sẽ có bài sharing tiếp theo để hoàn thành toàn bộ ứng dụng này! Chúc thầy cô, các bạn, anh chị có một cái tết thật vui ạ! Happy New Year!
Và xin cảm ơn mọi người đã đọc bài sharing của em 🫶.
