diff --git a/app/app.py b/app/app.py index 849abc7..bcc0348 100644 --- a/app/app.py +++ b/app/app.py @@ -232,6 +232,7 @@ def update_settings(): def reset_settings(): print("Current working directory:", os.getcwd()) # Delete the existing config.ini file + os.chdir(str(utils.APP_DIR)) if os.path.exists('config.ini'): os.remove('config.ini') shutil.copy('config.example.ini', 'config.ini') @@ -277,6 +278,8 @@ def update_tesseract_path(): if __name__ == "__main__": host = "localhost" port = 5000 + if os.name == 'posix': + port = 5001 logging.basicConfig(filename="app.log", filemode="w", level=logging.DEBUG, format="%(levelname)s - %(message)s") print("[*] Starting OcrRoo Server") print(f"[*] OcrRoo Server running on http://{host}:{port}/") diff --git a/app/utils.py b/app/utils.py index 0ea4a4a..437a7de 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,13 +1,14 @@ import hashlib import json import os.path +from pathlib import Path import shutil import subprocess import logging import time import cv2 from json import JSONDecodeError -from typing import Union, Optional +from typing import Union, Optional, Any, List, Dict import openai import pytesseract from pytube import YouTube @@ -29,6 +30,9 @@ def playsound_notification(audio_file): if audio_file is not None: playsound(file_path) +FILE_PATH = Path(__file__).resolve() # Absolute Path of utils.py +APP_DIR = FILE_PATH.parent +PROJ_ROOT = APP_DIR.parent def config(section: str = None, option: str = None) -> Union[ConfigParser, str]: """ @@ -39,11 +43,14 @@ def config(section: str = None, option: str = None) -> Union[ConfigParser, str]: :param option: [Optional] Key/option of value to retrieve :return: Return string or ConfigParser object """ + if (section is None) != (option is None): raise SyntaxError("section AND option parameters OR no parameters must be passed to function config()") parser = ConfigParser() - if not os.path.exists("config.ini"): - shutil.copy("config.example.ini", "config.ini") + if not (APP_DIR / 'config.ini').exists(): + src_path = str(APP_DIR / 'config.example.ini') + dst_path = str(APP_DIR / 'config.ini') + shutil.copy(src_path, dst_path) parser.read("config.ini") if parser.get("AppSettings", "openai_api_key") != "your_openai_api_key_here": openai.api_key = parser.get("AppSettings", "openai_api_key") @@ -64,7 +71,8 @@ def hash_video_file(filename: str) -> str: :return: Returns hex based md5 hash """ hash_md5 = hashlib.md5() - with open(f"{get_vid_save_path()}{filename}", "rb") as f: + video_file_path = Path(get_vid_save_path(), f'{filename}').resolve() + with video_file_path.open('rb') as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() @@ -92,20 +100,21 @@ def format_timestamp(seconds: int) -> str: return f'{str(minutes).zfill(2)}:{str(remaining_seconds).zfill(2)}' -def read_user_data() -> json: +def read_user_data() -> Optional[Any]: """ Reads the users data from json file :return: Returns user data as json """ - if not os.path.exists("data\\userdata.json"): - if not os.path.exists("data\\"): - os.makedirs("data\\") - with open("data\\userdata.json", "w") as user_data: + user_data_path = APP_DIR / 'data' / 'userdata.json' + if not user_data_path.exists(): + data_dir = APP_DIR / 'data' + if not data_dir.exists(): + data_dir.mkdir(parents=True, exist_ok=True) # data directory + with user_data_path.open('w') as user_data: user_data.write(json.dumps({"all_videos": []})) - pass return None try: - with open("data\\userdata.json", "r") as user_data_json: + with user_data_path.open('r') as user_data_json: data = json.load(user_data_json) return data except JSONDecodeError: @@ -113,24 +122,42 @@ def read_user_data() -> json: return None +def directory_append_slash(directory: str) -> str: + """ + Append a trailing slash to a directory path if it doesn't already have one. + :param directory: The directory path to which a trailing slash will be appended, if missing. + :return: The directory path with a trailing slash appended, if necessary. + """ + + # Check if directory already have trailing slash + if directory.endswith(('/', '\\')): + return directory + + # Append trailing slash + directory += '\\' if os.name == 'nt' else '/' + + return directory + + def get_vid_save_path() -> str: """ Returns output path from config variables, will set default to root of project\\out\\videos\\ :return: file path as string """ vid_download_path = config("UserSettings", "video_save_path") + # Set default output path for video download path if vid_download_path == "output_path": - default_path = os.path.dirname(os.getcwd()) + "\\out\\videos\\" - if not os.path.exists(default_path): - os.makedirs(default_path) - return default_path - # Check if the path ends with a backslash - if not vid_download_path.endswith("\\"): - # If it doesn't end with a backslash, append one - vid_download_path += "\\" + default_path = PROJ_ROOT / 'out' / 'videos' + if not default_path.exists(): + default_path.mkdir(parents=True, exist_ok=True) + + default_path = str(default_path) + return directory_append_slash(default_path) - return vid_download_path + vid_download_path = str(Path(vid_download_path)) + + return directory_append_slash(vid_download_path) def get_output_path() -> str: @@ -140,16 +167,18 @@ def get_output_path() -> str: """ output_path = config("UserSettings", "capture_output_path") # Set default output path for code files + if output_path == "output_path": - default_path = os.path.dirname(os.getcwd()) + "\\out\\" - if not os.path.exists(default_path): - os.makedirs(default_path) - return default_path - # Check if the path ends with a backslash - if not output_path.endswith("\\"): - # If it doesn't end with a backslash, append one - output_path += "\\" - return output_path + default_path = PROJ_ROOT / 'out' + if not default_path.exists(): + default_path.mkdir(parents=True, exist_ok=True) + + default_path = str(default_path) + + return directory_append_slash(default_path) + + output_path = str(Path(output_path)) + return directory_append_slash(output_path) def send_code_snippet_to_ide(filename: str, code_snippet: str) -> bool: @@ -281,7 +310,8 @@ def update_user_video_data(filename: str, progress: Optional[float] = None, capt record["progress"] = round(progress) if capture is not None: record["captures"].append(capture) - with open("data/userdata.json", "w") as json_data: + + with (APP_DIR / 'data' / 'userdata.json').open('w') as json_data: json.dump(user_data, json_data, indent=4) @@ -296,7 +326,9 @@ def add_video_to_user_data(filename: str, video_title: str, video_hash: str, you user_data = read_user_data() if user_data is None: return - video_capture = cv2.VideoCapture(f'{get_vid_save_path()}{filename}') + + video_path = str(Path(get_vid_save_path(), f'{filename}').resolve()) + video_capture = cv2.VideoCapture(video_path) if not video_capture.isOpened(): logging.error(f"Failed to open video capture for {filename}") return @@ -309,9 +341,13 @@ def add_video_to_user_data(filename: str, video_title: str, video_hash: str, you return thumbnail = str(int(time.time())) + ".png" # Check if img dir exists if not create - if not os.path.exists("static/img"): - os.makedirs("static/img") - cv2.imwrite(f"static/img/{thumbnail}", frame) + + static_dir = APP_DIR / 'static' + img_dir = static_dir / 'img' + if not img_dir.exists(): + img_dir.mkdir(parents=True, exist_ok=True) + + cv2.imwrite(str(img_dir / f'{thumbnail}'), frame) new_video = { "video_hash": video_hash, "filename": filename, @@ -325,7 +361,7 @@ def add_video_to_user_data(filename: str, video_title: str, video_hash: str, you new_video["youtube_url"] = youtube_url video_capture.release() user_data["all_videos"].append(new_video) - with open("data/userdata.json", "w") as json_data: + with (APP_DIR / 'data' / 'userdata.json').open('w') as json_data: json.dump(user_data, json_data, indent=4) @@ -344,7 +380,7 @@ def file_already_exists(video_hash: str) -> bool: return False -def get_setup_progress() -> [str]: +def get_setup_progress() -> List[str]: """ Gets users set up progress from config file :return: Returns array of string containing strings relating to settings that are already set up @@ -362,7 +398,7 @@ def get_setup_progress() -> [str]: return setup_progress -def parse_video_data() -> []: +def parse_video_data() -> dict: """ Gets all video data from userdata storage and parses all data for in progress videos :return: Array containing two arrays, 1 with all videos 1 with in progress videos @@ -459,7 +495,8 @@ def delete_video_from_userdata(filename: str) -> None: if current_video["filename"] == filename: all_videos.remove(current_video) break - with open("data/userdata.json", "w") as json_data: + + with (APP_DIR / 'data' / 'userdata.json').open('w') as json_data: json.dump(user_data, json_data, indent=4) @@ -479,8 +516,9 @@ def update_configuration(new_values_dict) -> None: if isinstance(value, bool) or isinstance(value, int): value = str(value) config_file.set(section, key, value) + # save the file - with open('config.ini', 'w') as config_file_save: + with (APP_DIR / 'config.ini').open('w') as config_file_save: config_file.write(config_file_save) diff --git a/design-evidencing-guide.md b/design-evidencing-guide.md new file mode 100644 index 0000000..52a9000 --- /dev/null +++ b/design-evidencing-guide.md @@ -0,0 +1,108 @@ +# Overview + +As part of the project, you and your team are required to evidence that you have **designed** an advanced user interface that is fit for purpose. + +The following guide will help you ensure that you have met the evidencing requirements while ensuring that you can work in an agile/iterative way that is appropriate for the project and modern software development practices. + +## Design Evidencing + +Modern design approaches are lightweight and combine iterations with user feedback. This is in contrast to traditional design approaches that are heavy on documentation and require a lot of up-front work. + +However, often we cannot access the user in the frequency that allows rapid iteration. One way to mitigate this is to develop personas and scenarios that represent the user and their goals. This allows you to make design decisions based on the user's needs and goals. + +### Minimum Requirements + +1. In your project repository, create a folder called `design`. +2. In the `design` folder, create a file called `persona.md` that describes the key persona your team was focused on implementing the design (see below for detail) +3. Create a subfolder with any design artefacts you created: wireframes, sketches, mockups, etc. +4. If you are treating the application itself as a prototype, highlight what changes if any you made to the application by referencing the appropriate issues +5. Create at least three github issues related to the design of the application: + 1. Tag each issue with ui-design + 2. Assign each issue to a team member + 3. Include a user story in the format "As a [persona], I want to [goal], so that [reason]" + 4. Add any non-functional requirements as notes in the issue +6. Copy this document into your project and answer any relevant questions + +## Personas + +A persona is a fictional character that represents a user. It is a way to describe the user's goals, needs, and behaviors. They are focused on **empathy** and **understanding** the user, not demographics, and not a collection of features. + +> Describe the key persona your team was focused on implementing the design. You can describe the persona in a file called `persona.md` in the `design` folder. +> + +Pick the most representative persona from your group. Write a brief (2-3 sentence) justification for why you chose this persona. + +"I want to be able to access all the codes at once. I'm busy with school and work, I don't have much time." + +### Persona Template + +This is an optional template for how to structure your persona: + +```markdown +# Persona: [Persona Name] + +## Background +Give the person's background - make sure we can understand their level of skills, knowledge, and experience. + +## Goals +Why does this person use the application? What are they trying to achieve? + +## Needs +What does this person need from the application? What are their pain points? + +``` + +### Relevant issue +> +> Link to an issue that covers a pain point relevant to the persona and explain why it is relevant. +> + +### Validation + +You will validate your design by meeting with a user representative: the product owner (in this case, your lecturer). + +##### Meeting minutes + +- [x] Meeting held on [date] 11/06/2024 +- [x] Persona discussed: [persona name] All of them +- [x] Design artefacts reviewed: [list of artifacts] +- [x] Issues discussed: [list of issues] +- [x] Feedback provided: [feedback] + +##### What worked well + +- [ ] [list of things that worked well] +- different personas +- exploring different models +- watching youtube videos of blind users + +##### What could be improved + +- [ ] [list of things that could be improved] +- validating interaction models (trying it ourselves) +- start easy +- more thoughts on designing +- + +##### What will you change before the next meeting + +- [ ] [list of things that will be changed before the next meeting] (no commitment to complete this) +- create a wireframe for getting to the nearest capture. +- what if the next capture is 5 minutes away? +- what if the file isn't fully processed yet? +- examine how Ollama can be packed and shipped + +##### Were there any questions that needed to be discussed with the user + +- [ ] [list of questions that need to be discussed with the user] +- whether the user wants to start with the code, or with the video. + +#### Lecturer's checklist (to be used by the lecturer) + +- [ ] Persona is well defined +- [ ] Persona is relevant to the application +- [ ] Design artifacts are present and easy to follow +- [ ] Design decisions are based on user needs and goals +- [ ] Appropriate considerations of interaction patterns appropriate for the user +- [ ] Efforts towards realizing at least one significant issue involving user interaction +- [ ] Whole team engagement in the design process diff --git a/design/img/Brainstorming_1.jpg b/design/img/Brainstorming_1.jpg new file mode 100644 index 0000000..d9905dd Binary files /dev/null and b/design/img/Brainstorming_1.jpg differ diff --git a/design/img/Brainstorming_2.jpg b/design/img/Brainstorming_2.jpg new file mode 100644 index 0000000..2c4debb Binary files /dev/null and b/design/img/Brainstorming_2.jpg differ diff --git a/design/img/Sketch_1.jpg b/design/img/Sketch_1.jpg new file mode 100644 index 0000000..f9f5748 Binary files /dev/null and b/design/img/Sketch_1.jpg differ diff --git a/design/img/Sketch_2.jpg b/design/img/Sketch_2.jpg new file mode 100644 index 0000000..642c7a7 Binary files /dev/null and b/design/img/Sketch_2.jpg differ diff --git a/design/persona.md b/design/persona.md new file mode 100644 index 0000000..4c85feb --- /dev/null +++ b/design/persona.md @@ -0,0 +1,47 @@ +# Persona: [Alice] + +## Background +Alice is a blind computer science university student, learning to code. She carries a Macbook to school, +but she uses Windows desktop at home. + +## Goals +Alice wants to watch coding tutorials to learn how to code. + +## Needs +"I have a Macbook and Windows desktop. Some programs do not support cross-platform. +I want to make sure that the program runs well on both devices." + +# Persona: [Ben] + +## Background +Ben is a blind computer science university student. Ben is a full time student and also has a part time job. + +## Goals +Ben wants access to the whole extracted code from the video to save time. + +## Needs +"I want to see all the codes from a video at once, I don't have much time." + +# Persona: [Ben] + +## Background +Ben is a blind computer science university student. Ben is a full time student and also has a part time job. +Ben wants to watch 6 hours long videos. + +## Goals +Ben wants to watch 6 hours long videos to learn new programming languages. + +## Needs +"I will not watch the whole 6 hours video but if there is code on screen, I want the access. +And when something is loading, I want to be able to know the status." + +# Persona: [Dan] + +## Background +Dan is a blind computer science university student. Dan recently lost his sight, and he is not used to relying on his listening skill. + +## Goals +Dan uses this program to learn how to code. + +## Needs +"I often miss what the computer tells me, and I need to go back." \ No newline at end of file diff --git a/journals/journal_alex.md b/journals/journal_alex.md index 1e474e0..df80af8 100644 --- a/journals/journal_alex.md +++ b/journals/journal_alex.md @@ -11,8 +11,9 @@ Mark all that applied this week Discussed the user stories As a blind person, I would like; - to know the progress of video pre-processing -- the program to ignore special characters(that are not used in any programming languages) -- +- I often get weird special character read out when I watch coding video + (that are not used in any programming languages) +- To be added...(refer to [persona.md](../design/persona.md)) # Week 16 @@ -26,4 +27,36 @@ Mark all that applied this week ### Notes: Worked on a branch, 'feature/auditory-feedback' -Added a function that makes a sound when pre-processing the video. \ No newline at end of file +Added a function that makes a sound when pre-processing the video. + + + +# Week 17 + +### Evidence: +Mark all that applied this week +- [x] Attended class +- [ ] Responded to PRs/Issues +- [ ] Met with the team online. Forum ______ +- [x] Committed to group repo + +### Notes: +Noticed that there is a bug with auditory feedback feature. +Fixed the issue - When I upload a video using Youtube link, it does not lead to auditory_feedback.html. + + + +# Week 18 + +### Evidence: +Mark all that applied this week +- [x] Attended class +- [ ] Responded to PRs/Issues +- [ ] Met with the team online. Forum ______ +- [ ] Committed to group repo + +### Notes: +Our team modified the program so that it processes the video and extracts code in the background +rather than going to auditory_feedback.html. As a team, we all tried to add timestamps in player.html, +and have extracted code to appear on the right side of the screen, and make the timestamps to navigate the video +and the extracted code. diff --git a/journals/journal_jayden.md b/journals/journal_jayden.md index 3b1c9c9..6023b98 100644 --- a/journals/journal_jayden.md +++ b/journals/journal_jayden.md @@ -28,3 +28,20 @@ This week I went over the code to get a more in-depth understanding of it and I ### Other This week I experimented with different AI models to use (llama3, codellama and llava) to extract and format code from the video. I created a test script to extract formatted code, an explanation of the formatted code and the timestamp of the code. + +# Week 18 +### Mark all that applied this week +- [x] Attended class +- [ ] Responded to PRs/Issues +- [x] Met with the team online. Discord +- [x] Committed to group repo + +### Other +This week I implemented my experimental preprocess script (credits to Low Kok Wei) into the OcrRoo program under the features/preprocessing-and-timestamps branch, as well as added multiple settings that change the behaviour of the preprocessing. +Some of the features this introduced are the following: +1. Preprocessing Videos +2. Enable / Disable Preprocessing +3. Toggle on/off formatted code and code explanations +4. Setting to change the preprocessing interval +5. Setting to change the llama endpoint (incase the user isn't locally hosting it, or is using a different port) +6. Timestamps detected to contain working code that appear below the media player whilst the video is processing. Once clicked, the video will go to that timestamp (planning to make this a lot nicer, didn't get to do that just yet though) diff --git a/journals/journal_kok-wei.md b/journals/journal_kok-wei.md index a120850..46e27f5 100644 --- a/journals/journal_kok-wei.md +++ b/journals/journal_kok-wei.md @@ -8,3 +8,8 @@ May 22 May 27 - Gave a walk-through of the logic behind extract_all_code.py using Discord messaging + +Jun 10 +- Provided feedback to Rumsie on issue #20 with code guide as follow +- Set the static\audio path at utils.py as single point to change path when necessary. +- Fix the playsound error code 304 (invalide filename) by playing the mp3 sound files as wav didn't work. diff --git a/journals/journal_vinh.md b/journals/journal_vinh.md index 6d423a3..aa8f656 100644 --- a/journals/journal_vinh.md +++ b/journals/journal_vinh.md @@ -52,4 +52,19 @@ By the end of the week, I will try to contribute with the utils.py. I set up an issue for native OS agnostic compatibility for Cert IV students. I responded to one of the Cert IVs who make a pull request on the issue I've set up. I responded with a comment to try suggest this code instead. ### Retrospective -By the end of the week, I will look upon more on any issues and features. \ No newline at end of file +By the end of the week, I will look upon more on any issues and features. + +### <11/06/2024> + +#### Evidence +- [x] Attended class +- [x] Responded to PRs/Issues +- [ ] Met with the team online. Discord +- [ ] Commits to group repo + +#### Summary +What I'm working on is playing a sound when the capture is successful or not. I am using +the playsound package based on the comment in the issue I looked at. + +### Retrospective +By the end of the week, the notification sound when the capture is successful or not is implemented. \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py index 16dc7d7..82106dc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -117,11 +117,27 @@ def test_hash_string(): def test_get_output_path(mocker): + project_root = str(utils.PROJ_ROOT) + default_path = utils.directory_append_slash(os.path.join(project_root, 'out')) + test_output_paths = { - "c:\\users\\program files\\app": "c:\\users\\program files\\app\\", - "output_path": os.path.dirname(os.getcwd()) + "\\out\\", - "videos\\my_videos\\": "videos\\my_videos\\", + "output_path": default_path, } + + assert os.name in ['nt', 'posix'] + + # Windows + if os.name == 'nt': + test_output_paths["c:\\users\\program files\\app"] = "c:\\users\\program files\\app\\" + test_output_paths["videos\\my_videos\\"] = "videos\\my_videos\\" + + # Linux or macOS (Note: GitHub Runner is using ubuntu) + else: + assert os.name == 'posix' + users_dir = 'home' if 'home' in os.path.expanduser("~") else 'Users' + test_output_paths[f"/{users_dir}/program_files/app"] = f"/{users_dir}/program_files/app/" + test_output_paths["videos/my_videos/"] = "videos/my_videos/" + for paths in test_output_paths: mocker.patch("app.utils.config", return_value=paths) assert utils.get_output_path() == test_output_paths[paths]