# [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: ![image](https://hackmd.io/_uploads/S11rbBRva.png) ___ ![image](https://hackmd.io/_uploads/Hksrfr0vp.png) Ở 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 🫶. ![fctclunarnewyear](https://hackmd.io/_uploads/ByCzYvCvT.png)