① Upload Video File↓
+Select the mp4 file you want to analyze
+ + {% if messages %} + + {% endif %} +② Enter Arguments↓
+ +③ Analysis Result↓
+ {% block content %} +Result:
+{{ result }}
+ diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..83def9e Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index 59cd84f..1442a01 100644 --- a/README.md +++ b/README.md @@ -10,58 +10,62 @@ Download a video of a Synthesia song, run it through the program, and receive th ### Basic Tutorial > This program is in its early stages so don't expect very high quality output at this time -> If the beat doesn't sound quite right, remove all occurrences of 'z/8' and it might sound better - Program Arguments ```text (venv) >python -m copycat.main --help -usage: main.py [-h] -f PATH [-k NOTE] [-t TEMPO] [--skip-frames SKIP_FRAMES] [--debug] {manual} ... +usage: main.py [-h] --file PATH [--tempo TEMPO] {manual} ... positional arguments: {manual} +required arguments: + -f PATH, --file PATH The path of the mp4 synthesia video + -t TEMPO, --tempo TEMPO The tempo of the piece in BPM + +optional arguments: + -h, --help show this help message and exit +``` + +At the moment, the only supported parsing format is manual, +meaning the program requires more basic information + +```text +(venv) >python -m copycat.main manual --help +usage: main.py manual [-h] --bounds X Y WIDTH HEIGHT --first-key NOTE [--skip-frames SKIP_FRAMES] [--detector-line-offset DETECTOR_LINE_OFFSET] + [--min-speed MIN_SPEED] [--debug] + optional arguments: -h, --help show this help message and exit - -f PATH, --file PATH The path of the mp4 synthesia video - -k NOTE, --first-key NOTE - The first white key in the bounds. - -t TEMPO, --tempo TEMPO - The tempo of the piece in BPM + --bounds X Y WIDTH HEIGHT + The boundaries around the piano keys space separated + --first-key NOTE The first white key in the bounds. --skip-frames SKIP_FRAMES How many frame to skip in case there is an introduction + --detector-line-offset DETECTOR_LINE_OFFSET + Number of pixels from the top of the boundary to offset the detector line + --min-speed MIN_SPEED + Defines a minimum note duration --debug Show debugged version + +(venv) F:\Development\copycat> ``` +The only required arguments are **bounds** and **first key**. + ### Example Here is an example of a song with the correct parameters running in debug mode (`--debug`) -`main.py --file /tmp/taylor_swift.mp4 --first-key D2 --debug` +`main.py --file /tmp/taylor_swift.mp4 manual --bounds "20 632 1780 200" --first-key A2 --skip-frames 150 --debug`  ##### Explained -1. The orange line is the detection line. It is where the key detection actually happens. Make sure no *special effects* are seen at this line. -2. The purple key text has been placed above the respective key. +1. The green rectangle is the `--bounds` property. It should begin at a white key. +2. The orange line is the detection line `--detector-line-offset`. It is where the key detection actually happens. Make sure no *special effects* are seen at this line. +3. The purple key text has been placed above the respective key. If everything looks good you can run without the `--debug` flag -Example output: -```text -T: -C: -Q: 120 -[B,1] [D1] [A1] [G1] [A1] [G1] [D1] [G1] [B,1] % -[D1] [A1] [G1] [A1] [G1] [D1] [G1] [A,1] [D1] [A1] % -[G1] [A1] [G1] [D1] [G1] [A,1] [D1] [A1] [G1] [A1] % -[G1] [D1] [G1] [B,1] [E1] [A1] [G1] [A1] [G1] [E1] % -[G1] [B,1] [E1] [A1] [G1] [A1] [G1] [E1] [G1] [C1] % -[D1] [A1] [G1] [A1] [G1] [D1] [G1] [C1] [D1] [A1] % -[G1] [A1] [G1] [D1] [G1] [^F1] [G1] [G2] [G3] % -[G1] [^F1] [G2] [A2] [G2] [G1] [^F1] [G2] % -[G3] [G1] [G1] [^F1] [G2] [A2] [G1] [^F1] % -[G1] [E8] z2 [G1] [G1] [G1] [^F1] [^F1] [D1] % -``` - Take the output string when the program finishes and paste it into your favorite [abc notation editor](https://www.abcjs.net/abcjs-editor.html) \ No newline at end of file diff --git a/catsite/__init__.py b/catsite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/catsite/asgi.py b/catsite/asgi.py new file mode 100644 index 0000000..a7d692e --- /dev/null +++ b/catsite/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for catsite project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'catsite.settings') + +application = get_asgi_application() diff --git a/catsite/settings.py b/catsite/settings.py new file mode 100644 index 0000000..cf926a9 --- /dev/null +++ b/catsite/settings.py @@ -0,0 +1,131 @@ +""" +Django settings for catsite project. + +Generated by 'django-admin startproject' using Django 4.1.3. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-3ptyh_lmj^$0s(+@q^2sougb$aj=aa3mygqdj06^bs5$w81l@3' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'copycat.apps.CopycatConfig', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'catsite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'catsite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +import os + +STATIC_URL = '/static/' +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] + +MEDIA_URL = '/media/' + +MEDIA_ROOT = BASE_DIR / 'media' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/catsite/urls.py b/catsite/urls.py new file mode 100644 index 0000000..1d5ea1b --- /dev/null +++ b/catsite/urls.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from django.urls import path,include +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('copycat.urls')), +] + +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/catsite/wsgi.py b/catsite/wsgi.py new file mode 100644 index 0000000..8fc01a8 --- /dev/null +++ b/catsite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for catsite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'catsite.settings') + +application = get_wsgi_application() diff --git a/copycat/admin.py b/copycat/admin.py new file mode 100644 index 0000000..f138666 --- /dev/null +++ b/copycat/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import Uploadmp4 + +admin.site.register(Uploadmp4) \ No newline at end of file diff --git a/copycat/apps.py b/copycat/apps.py new file mode 100644 index 0000000..13e2143 --- /dev/null +++ b/copycat/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CopycatConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'copycat' diff --git a/copycat/debugging/debugging.py b/copycat/debugging/debugging.py index 1d17732..15667f9 100644 --- a/copycat/debugging/debugging.py +++ b/copycat/debugging/debugging.py @@ -3,7 +3,7 @@ import cv2 from globals.global_types import Image, Bounds -from globals.paino_key import PianoKey, PianoKeyContour +from globals.paino_key import PianoKey def show_video(video: Iterable[Image]): @@ -28,22 +28,13 @@ def show_contours(original_image, contours: List, slideshow=0): cv2.imshow("contoured", image) -def draw_contours_for_keys(original_image, keys: Iterable[PianoKeyContour]): +def draw_contours_for_keys(original_image, keys: Iterable[PianoKey]): image = original_image.copy() for key in keys: - bounds = Bounds(*cv2.boundingRect(key.__contour)) + bounds = Bounds(*cv2.boundingRect(key.contour)) cv2.putText(image, key.note, (bounds.x + round(bounds.width / 10), bounds.y + round(bounds.height / 2)), cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.4, color=(255, 105, 180)) - __outline_contour(image, key.__contour) - return image - - -def draw_notes_for_keys(original_image, keys: Iterable[PianoKey], detection_height): - image = original_image.copy() - for key in keys: - cv2.putText(image, key.note, - (key.section.start + round((key.section.end - key.section.start) / 10), detection_height - 50), - cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.4, color=(255, 105, 180)) + __outline_contour(image, key.contour) return image @@ -78,14 +69,10 @@ def __outline_contour(base_image, contour): return image -def debug_params(control_frame, keys, detector, bounds=None): - image = control_frame.copy() - if bounds is not None: - image = draw_rectangle(image, bounds.x, bounds.y, bounds.width, bounds.height) +def debug_params(control_frame, frames, keys, detector, bounds): + original_image = control_frame.copy() + image = draw_rectangle(original_image, bounds.x, bounds.y, bounds.width, bounds.height) image = draw_line(image, detector._detection_height) - if isinstance(keys[0], PianoKeyContour): - image = draw_contours_for_keys(image, keys.values()) - else: - image = draw_notes_for_keys(image, keys, detector._detection_height) + image = draw_contours_for_keys(image, keys.values()) cv2.imshow("debug", image) cv2.waitKey() diff --git a/copycat/forms.py b/copycat/forms.py new file mode 100644 index 0000000..228e9b8 --- /dev/null +++ b/copycat/forms.py @@ -0,0 +1,7 @@ +from django import forms +from .models import Uploadmp4 + +class Mp4form(forms.ModelForm): + class Meta: + model = Uploadmp4 + fields = ('files',) diff --git a/copycat/globals/color.py b/copycat/globals/color.py index ebb52f5..204d4b8 100644 --- a/copycat/globals/color.py +++ b/copycat/globals/color.py @@ -37,13 +37,7 @@ def __sub__(self, other): return sqrt(self.r ** 2 + self.g ** 2 + self.b ** 2) - sqrt(other.r ** 2 + other.g ** 2 + other.b ** 2) def __str__(self): - try: - return rgb_to_name((self.r, self.g, self.b)) - except ValueError: - return f"{self.r},{self.g},{self.b}" - - def __repr__(self): - return str(self) + return f"{self.r},{self.g},{self.b}" def to_tuple(self): return self.b, self.g, self.r @@ -59,5 +53,5 @@ def from_bgr(b: int, g: int, r: int): return Color(b=b, g=g, r=r) @staticmethod - def from_rgb(r: int, g: int, b: int): + def from_rbg(r: int, b: int, g: int): return Color(b=b, g=g, r=r) diff --git a/copycat/globals/global_types.py b/copycat/globals/global_types.py index 6352718..6776310 100644 --- a/copycat/globals/global_types.py +++ b/copycat/globals/global_types.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from typing import NewType, Tuple -from cv2 import cv2 +import cv2 from numpy import ndarray Image = NewType("Image", ndarray) diff --git a/copycat/globals/math_utils.py b/copycat/globals/math_utils.py index 949cfa3..7a6e155 100644 --- a/copycat/globals/math_utils.py +++ b/copycat/globals/math_utils.py @@ -23,4 +23,4 @@ def take_closest(thing_list: List[T], thing: T): if after - thing <= thing - before: return pos else: - return pos - 1 + return pos - 1 \ No newline at end of file diff --git a/copycat/image_processing/image_manipulations.py b/copycat/image_processing/image_manipulations.py index 84fdeb4..ddcaae8 100644 --- a/copycat/image_processing/image_manipulations.py +++ b/copycat/image_processing/image_manipulations.py @@ -1,4 +1,4 @@ -from cv2 import cv2 +import cv2 import numpy as np from globals.global_types import Image diff --git a/copycat/image_processing/key_extraction.py b/copycat/image_processing/key_extraction.py index 589bfd1..cc7b4e5 100644 --- a/copycat/image_processing/key_extraction.py +++ b/copycat/image_processing/key_extraction.py @@ -1,7 +1,6 @@ -from typing import List, Tuple, Optional +from typing import List, Tuple import numpy as np -from tqdm import tqdm from globals.color import Color from globals.global_types import Image, Section @@ -17,68 +16,22 @@ def get_piano_keys(control_frame: Image, key_offset: int, detection_height: int) detection_line = binary_keyboard[detection_height] sections = _get_sections_by_border_detection(detection_line) + sections = [Section(x[0], x[1]) for x in sections] sections = _filter_noise(sections, 5) piano_keys = [] - for i, section in tqdm(enumerate(sections), desc="Parsing keys"): + for i, section in enumerate(sections): piano_keys.append( PianoKey( absolute_index=i + key_offset, - section=Section(section[0], section[1]) + section=section ) ) return piano_keys -def automatically_detect_keyboard_line(control_frame: Image) -> Optional[int]: - """ - The algorithm used to detect where the keyboard is: - 1. Go through each horizontal line on the frame - 2. Everytime there is a significant color change, we save its coordinates, we treat each color change as a key. - 3. Remove noise by filtering things that are two small to be keys - 4. We check to see if what we think are keys really are keys by making sure they're all relatively the same size. - 5. We check to see that the ratio black to white keys is 5 black keys per 7 white keys. - 6. We give a percentage of how accurate our previous 2 checks were. - 7. If we get 100 lines in a row where the accuracy percentage is over 80%, we treat that as the keyboard - - :return: The height of a location on the keyboard. - """ - frame_height = control_frame.shape[0] - binary_frame = reduce_colors(control_frame, 2) - guess_counter = 0 - with tqdm(total=frame_height, desc="Attempting to automatically detect piano keyboard...") as pbar: - for i in range(1, frame_height): - pbar.update(1) - section = _get_sections_by_border_detection(binary_frame[i]) - filtered_section = _filter_noise(section, 5) - if len(filtered_section) < 12: - continue - guess = _get_keyboard_guess_percentage(filtered_section, binary_frame[i], 5) - if guess > 0.8: - guess_counter += 1 - else: - guess_counter = 0 - - if guess_counter == 100: - pbar.update(frame_height - i) - return i - return None - - def _get_sections_by_border_detection(detection_line: np.ndarray) -> List[Tuple[int, int]]: - """ - Split an array of pixels each time there is a change in color. - - Take for example an array: - - |0|0|0|0|0|1|1|1|1|1|1|0|0|0|0| - - This would be split every time we change from 0 to 1 or from 1 to 0 - The result would be the two indexes at which that change occurs (on either side of the key, marking its borders). - - :return: The "x" and "x + width" coordinates of a key - """ sections = [] previous_color = detection_line[0] previous_border_index = 0 @@ -92,47 +45,10 @@ def _get_sections_by_border_detection(detection_line: np.ndarray) -> List[Tuple[ return sections -def _filter_noise(sections: List[Tuple[int, int]], minimum_key_width: int) -> List[Tuple[int, int]]: - """ - Remove sections that are smaller than the minimum key width. - """ - return list(filter(lambda section: section[1] - section[0] > minimum_key_width, sections)) +def _filter_noise(sections: List[Section], minimum_key_width: int) -> List[Section]: + return list(filter(lambda section: section.end - section.start > minimum_key_width, sections)) -def _get_key_color(detection_line: np.ndarray, section: Tuple[int, int]) -> KeyColor: - """ - Checks whether the section is a black or white key. - """ - color = Color.from_bgr(*detection_line[section[0]]) +def _get_key_color(detection_line: np.ndarray, section: Section) -> KeyColor: + color = Color.from_bgr(*detection_line[section.start]) return KeyColor.BLACK_KEY if color.closer_to(BLACK, WHITE) == BLACK else KeyColor.WHITE_KEY - - -def _get_keyboard_guess_percentage( - sections: List[Tuple[int, int]], - detection_line: np.ndarray, - max_separation: int -) -> float: - """ - Returns a percentage (0 to 1) of how confident we are that this is part of the piano keyboard. - """ - white_widths = [end - start for start, end in sections if - _get_key_color(detection_line, (start, end)) == KeyColor.WHITE_KEY] - black_widths = [end - start for start, end in sections if - _get_key_color(detection_line, (start, end)) == KeyColor.BLACK_KEY] - if len(white_widths) == 0 or len(black_widths) == 0: - return 0 - - white_average_width = sum(white_widths) / len(white_widths) - black_average_width = sum(black_widths) / len(black_widths) - uniformity_counter = 0 - for width in white_widths: - if white_average_width - max_separation < width < white_average_width + max_separation: - uniformity_counter += 1 - - for width in black_widths: - if black_average_width - max_separation < width < black_average_width + max_separation: - uniformity_counter += 1 - - uniformity_percentage = uniformity_counter / len(sections) - black_to_white_percentage = np.clip(1 - (abs((len(black_widths) / len(white_widths)) - 0.7)), 0, 1) - return round((uniformity_percentage + black_to_white_percentage) / 2, 2) diff --git a/copycat/image_processing/note_press_detection.py b/copycat/image_processing/note_press_detection.py index 477bf20..c3f19e7 100644 --- a/copycat/image_processing/note_press_detection.py +++ b/copycat/image_processing/note_press_detection.py @@ -2,29 +2,25 @@ from globals.color import Color from globals.global_types import Image, Clef, Section -from cv2 import cv2 + BLUE = Color(b=255, g=0, r=0) GREEN = Color(b=0, g=255, r=0) class NoteDetector: - def __init__(self, control_frame: Image, detection_height: int, detection_threshold: float, - treble_color: Color, bass_color: Color): + def __init__(self, control_frame: Image, detection_height: int, detection_threshold: float): self._detection_height = detection_height self._detection_threshold = detection_threshold self._control_detection_line = control_frame[detection_height] - self._treble_color = treble_color - self._bass_color = bass_color - def is_note_detected(self, section: Section, image: Image) -> Clef: control_color = _get_mean_color_at_slice(self._control_detection_line[section.start: section.end]) checked_color = _get_mean_color_at_slice(image[self._detection_height][section.start: section.end]) if control_color.diff(checked_color) <= self._detection_threshold: return Clef.NONE - return Clef.BASS if checked_color.closer_to(self._bass_color, self._treble_color) == self._bass_color \ - else Clef.TREBLE + + return Clef.BASS if checked_color.closer_to(BLUE, GREEN) == BLUE else Clef.TREBLE def _get_mean_color_at_slice(color_slice: np.ndarray) -> Color: diff --git a/copycat/main.py b/copycat/main.py index 93355b1..3256fa6 100644 --- a/copycat/main.py +++ b/copycat/main.py @@ -1,15 +1,15 @@ import argparse -from cv2 import cv2 +import cv2 from webcolors import hex_to_rgb -from copycat.debugging.debugging import debug_params -from copycat.image_processing.note_press_detection import NoteDetector -from copycat.media_parsing.video_to_frames import Video -from copycat.notation.notation import Notation -from globals.color import Color -from globals.global_types import Clef -from globals.paino_key import PianoKey +from debugging.debugging import debug_params +from image_processing.note_press_detection import NoteDetector +from media_parsing.video_to_frames import Video +from notation.notation import Notation +from globallll.color import Color +from globallll.global_types import Clef +from globallll.piano_key import PianoKey from image_processing.key_extraction import get_piano_keys, automatically_detect_keyboard_line from media_parsing.skip_frames import skip_video_frames from notation.note_stream import get_note_stream @@ -64,13 +64,13 @@ def main( type=float) parser.add_argument("--debug", help="Show debugged version", default=False, action="store_true") - parser.add_argument("--treble-color", help="The (approximate) color of the pressed key for treble clef", + parser.add_argument("-tc","--treble-color", help="The (approximate) color of the pressed key for treble clef", default="#00FF00", type=str) - parser.add_argument("--bass-color", help="The (approximate) color of the pressed key for bass clef", + parser.add_argument("-bc","--bass-color", help="The (approximate) color of the pressed key for bass clef", default="#0000FF", type=str) args = parser.parse_args() - + main( file_path=args.file, first_key=args.first_key, @@ -78,5 +78,5 @@ def main( tempo=args.tempo, debug=args.debug, treble_color=args.treble_color, - bass_color=args.bass_color - ) + bass_color=args.bass_color, + ) \ No newline at end of file diff --git a/copycat/media_parsing/skip_frames.py b/copycat/media_parsing/skip_frames.py index fe8b5ad..22ea8ea 100644 --- a/copycat/media_parsing/skip_frames.py +++ b/copycat/media_parsing/skip_frames.py @@ -1,6 +1,6 @@ from typing import Iterator -def skip_video_frames(amount_of_frames_to_skip: int, frames: Iterator) -> None: +def skip_frames(amount_of_frames_to_skip: int, frames: Iterator) -> None: for i in range(amount_of_frames_to_skip): next(frames) diff --git a/copycat/media_parsing/video_to_frames.py b/copycat/media_parsing/video_to_frames.py index 9d05205..1ce05b9 100644 --- a/copycat/media_parsing/video_to_frames.py +++ b/copycat/media_parsing/video_to_frames.py @@ -9,27 +9,13 @@ class Video: def __init__(self, video_file_path: str): self.capture = VideoCapture(video_file_path) - self.__fps = self.capture.get(cv2.CAP_PROP_FPS) - self.__total_frames = self.capture.get(cv2.CAP_PROP_FRAME_COUNT) @property def fps(self): - return self.__fps + return self.capture.get(cv2.CAP_PROP_FPS) - @property - def total_frames(self): - return self.__total_frames - - def extract_frames(self, seconds=-1) -> Iterator[Image]: - if seconds != -1: - self.__total_frames = seconds * self.fps - elapsed_frames = 0 + def extract_frames(self) -> Iterator[Image]: while self.capture.isOpened(): _, frame = self.capture.read() - if frame is None: - break yield frame - elapsed_frames += 1 - if elapsed_frames >= seconds * self.fps and seconds != -1: - break self.capture.release() diff --git a/copycat/migrations/0001_initial.py b/copycat/migrations/0001_initial.py new file mode 100644 index 0000000..9e3884f --- /dev/null +++ b/copycat/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.7 on 2024-07-20 06:48 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, null=True)), + ('description', models.TextField()), + ], + ), + migrations.CreateModel( + name='Uploadmp4', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(blank=True, max_length=100, null=True)), + ('files', models.FileField(null=True, upload_to='files')), + ('user', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/copycat/migrations/__init__.py b/copycat/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/copycat/models.py b/copycat/models.py new file mode 100644 index 0000000..8a02318 --- /dev/null +++ b/copycat/models.py @@ -0,0 +1,15 @@ +from django.db import models +from django.db import models + +class Uploadmp4(models.Model): + title = models.CharField(max_length=100,null=True,blank=True) + files = models.FileField(upload_to='files',null=True) + user = models.ForeignKey('auth.User', on_delete=models.CASCADE,default=None,null=True) + + def __str__(self): + return self.title + + +class Item(models.Model): + name = models.CharField(max_length=100,null=True) + description = models.TextField() diff --git a/copycat/notation/consts.py b/copycat/notation/consts.py index afa8a11..58b9112 100644 --- a/copycat/notation/consts.py +++ b/copycat/notation/consts.py @@ -1,4 +1,5 @@ OCTAVES = { + "A1": "A,,,", "B1": "B,,,", "C1": "C,,", @@ -22,7 +23,7 @@ "G3": "G", "A4": "A", "B4": "B", - "C4": "c", + "C4": "C", "D4": "d", "E4": "e", "F4": "f", @@ -55,4 +56,4 @@ 3: "6", 3.5: "7", 4: "8", # whole -} +} \ No newline at end of file diff --git a/copycat/notation/notation.py b/copycat/notation/notation.py index 4c99c42..f545710 100644 --- a/copycat/notation/notation.py +++ b/copycat/notation/notation.py @@ -1,57 +1,202 @@ +import math +from bisect import bisect_left from collections import defaultdict from dataclasses import dataclass -from typing import Dict, List, Optional, Tuple +from functools import reduce +from typing import Dict, List, Type, Tuple -from tqdm import tqdm +from globals.global_types import Clef -from notation.note_parser import NoteParser -from notation.note_stream import NoteStream, NoteInstance +OCTAVES = { + "A2": "A,,", + "B2": "B,,", + "C2": "C,", + "D2": "D,", + "E2": "E,", + "F2": "F,", + "G2": "G,", + "A3": "A,", + "B3": "B,", + "C3": "C", + "D3": "D", + "E3": "E", + "F3": "F", + "G3": "G", + "A4": "A", + "B4": "B", + "C4": "c", + "D4": "d", + "E4": "e", + "F4": "f", + "G4": "g", + "A5": "a", + "B5": "b", + "C5": "c'", + "D5": "d'", + "E5": "e'", + "F5": "f'", + "G5": "g'", + "A6": "a'", + "B6": "b'", + "C6": "c''", + "D6": "d''", + "E6": "e''", + "F6": "f''", + "G6": "g''", +} + +LENGTHS = { + 0.0625: "/8", # 64ths + 0.125: "/4", # 32nds + 0.25: "/2", # sixteenth + 0.5: "1", # eight + 1: "2", # quarter + 1.5: "3", # quarter with dot + 2: "4", # half + 2.5: "5", + 3: "6", + 3.5: "7", + 4: "8", # whole +} class Notation: - def __init__(self, fps: float, tempo: int, octave_offset: int): - self._tempo = tempo - self.note_parser = NoteParser(fps, tempo, octave_offset) - - def parse_stream(self, stream: NoteStream): - song_frames: Dict[int, List[Tuple[NoteInstance, int]]] = defaultdict(list) - notes: Dict[NoteInstance, NoteState] = defaultdict(NoteState) - with tqdm(total=len(stream.stream) * 2, desc="creating abc notation...") as pbar: - for i, frame in enumerate(stream.stream): - pbar.update(1) - for key in frame: - note_state = notes[key] - note_state.frame_count += 1 - note_state.iteration = i - for note, state in tuple(notes.items()): - if state.iteration != i: # a note has ended - song_frames[state.iteration - state.frame_count].append((note, state.frame_count)) - del notes[note] - - notation_string = "" - rest_index: Optional[int] = None - rest_clear = 0 - for i in range(len(stream.stream)): - pbar.update(1) - if len(notation_string) - notation_string.rfind("%") > 50: - notation_string += " %\n" - if i in song_frames: - if rest_index is not None: - notation_string += f"{self.note_parser.get_rest_notation(i - rest_index)} " - rest_index = None - - notes_notation = [] - for notes_in_frame in song_frames[i]: - notes_notation.append(self.note_parser.get_notation(notes_in_frame[0], notes_in_frame[1])) - rest_clear = max(rest_clear, i + notes_in_frame[1]) - notation_string += f"[{' '.join(notes_notation)}] " - elif rest_index is None and i > rest_clear: - rest_index = i - - return notation_string - - def get_abc_notation(self, notation_string: str, title: str = "", composer: str = ""): - return f"T: {title}\nC: {composer}\nQ: {self._tempo}\n{notation_string}" + def __init__(self, fps: float, tempo: int, octave_offset: int, min_note_speed: int): + self.fps = round(fps) + self.tempo = tempo + self.octave_offset = octave_offset + self.min_note_speed = min_note_speed + + self.treble_notation_string: List[str] = [""] + self.bass_notation_string: List[str] = [""] + + self.treble_notes: Dict[str, NoteState] = defaultdict(NoteState) + self.bass_notes: Dict[str, NoteState] = defaultdict(NoteState) + self.iteration = 0 + self.notation_segment = 0 + + def push_note(self, note: str, clef: Clef = Clef.TREBLE): + if clef == Clef.TREBLE: + note_state = self.treble_notes[note] + else: + note_state = self.bass_notes[note] + + note_state.frame_count += 1 + note_state.iteration = self.iteration + + def apply_frame(self): + self._add_rest(self.treble_notes) + self._add_rest(self.bass_notes) + + self._set_notes_notation(self.treble_notes, Clef.TREBLE) + self._set_notes_notation(self.bass_notes, Clef.BASS) + + self.iteration += 1 + + self.notation_segment = self._iteration_to_segment(self.iteration) + + # If a new segment is reached + if self.notation_segment != self._iteration_to_segment(self.iteration - 1): + self.treble_notation_string.append("") + self.bass_notation_string.append("") + + def get_abc_notation(self, title="", composer="", clef: Clef = Clef.NONE): + final_notation = f"T: {title}\nC: {composer}\nQ: {self.tempo}\nV:2 bass\n" + for i in range(self.notation_segment): + final_notation += f"[V:1]{self.treble_notation_string[i] if clef != Clef.BASS else ''}\n" \ + f"[V:2]{self.bass_notation_string[i] if clef != Clef.TREBLE else ''}\n%\n" + + return final_notation + + def _add_rest(self, notes): + if len(notes) == 0 or (notes.get("0") is not None and len(notes) == 1): + notes["0"].iteration = self.iteration + notes["0"].frame_count += 1 + + def _set_notes_notation(self, notes, clef: Clef): + section = "[" + iteration = self.iteration + frame_count = 0 + for note, state in list(notes.items()): + if state.iteration == self.iteration: + continue + + if note == "0": + length, index = length_parser(state.frame_count / self.fps, self.tempo) + if index >= 3: + if clef == Clef.TREBLE: + self.treble_notation_string[self._iteration_to_segment(state.iteration - state.frame_count)] += f"z{length} " + else: + self.bass_notation_string[self._iteration_to_segment(state.iteration - state.frame_count)] += f"z{length} " + notes.pop("0") + continue + + parsed_note = sharp_parser(note) + parsed_note = octave_parser(parsed_note, self.octave_offset) + length = length_parser(state.frame_count / self.fps, self.tempo, min_length_index=self.min_note_speed)[0] + section += f"{parsed_note}{length}" + iteration = state.iteration + frame_count = state.frame_count + notes.pop(note) + section += "] " + + if section != "[] ": + if clef == Clef.TREBLE: + self.treble_notation_string[self._iteration_to_segment(iteration - frame_count)] += section + else: + self.bass_notation_string[self._iteration_to_segment(iteration - frame_count)] += section + + def _iteration_to_segment(self, iteration): + return math.floor(iteration / (round(self.fps) * 3)) + + +def octave_parser(note: str, offset: int = 0) -> str: + letter = reduce(lambda a, b: a if 65 <= ord(a) <= 90 else b, note) + octave = int(note[-1]) + offset + + note_with_octave = OCTAVES[f"{letter}{octave}"] + + note = note.replace(letter, note_with_octave) + return note.replace(str(octave - offset), "") + + +def sharp_parser(note: str) -> str: + if "#" in note: + return f"^{note.replace('#', '')}" + return note + + +def length_parser(seconds: float, tempo: int, min_length_index=0) -> Tuple[str, int]: + crotchet = 60 / tempo + press_seconds = [x * crotchet for x in LENGTHS.keys()] + note_index = take_closest(press_seconds, seconds) + + return list(LENGTHS.values())[max(note_index, min_length_index)], note_index + + +T = Type["T"] + + +def take_closest(thing_list: List[T], thing: T): + """ + Assumes thing_list is sorted. Returns the index of the closest value to thing. + + If two numbers are equally close, return the largest number index. + + Based on: + https://stackoverflow.com/questions/12141150/from-list-of-integers-get-number-closest-to-a-given-value + """ + pos = bisect_left(thing_list, thing) + if pos == 0: + return 0 + if pos == len(thing_list): + return len(thing_list) - 1 + before = thing_list[pos - 1] + after = thing_list[pos] + if after - thing <= thing - before: + return pos + else: + return pos - 1 @dataclass diff --git a/copycat/notation/note_parser.py b/copycat/notation/note_parser.py index aa71162..e2a0a0a 100644 --- a/copycat/notation/note_parser.py +++ b/copycat/notation/note_parser.py @@ -1,4 +1,4 @@ -from globals.math_utils import take_closest +from globallll.math_utils import take_closest from notation.consts import LENGTHS, OCTAVES from notation.note_stream import NoteInstance @@ -36,4 +36,4 @@ def round_beat_length(beat_count: float): @staticmethod def octave_parser(letter: str, octave: int) -> str: - return OCTAVES[f"{letter}{octave}"] + return OCTAVES[f"{letter}{octave}"] \ No newline at end of file diff --git a/copycat/notation/note_stream.py b/copycat/notation/note_stream.py index 592084d..129bcc0 100644 --- a/copycat/notation/note_stream.py +++ b/copycat/notation/note_stream.py @@ -3,8 +3,8 @@ from tqdm import tqdm -from globals.global_types import Clef, Image -from globals.paino_key import BasePianoKey +from globallll.global_types import Clef, Image +from globallll.piano_key import BasePianoKey from image_processing.note_press_detection import NoteDetector @@ -58,4 +58,4 @@ def apply_frame(self): @property def stream(self): - return self.__stream + return self.__stream \ No newline at end of file diff --git a/copycat/output/__init__.py b/copycat/output/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/copycat/static/copycat/css/reset.css b/copycat/static/copycat/css/reset.css new file mode 100644 index 0000000..6c051d3 --- /dev/null +++ b/copycat/static/copycat/css/reset.css @@ -0,0 +1,489 @@ +/*! destyle.css v2.0.2 | MIT License | https://github.com/nicolas-cusan/destyle.css */ + +/* Reset box-model and set borders */ +/* ============================================ */ + +*, +::before, +::after { + box-sizing: border-box; + border-style: solid; + border-width: 0; +} + +/* Document */ +/* ============================================ */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + * 3. Remove gray overlay on links for iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -webkit-tap-highlight-color: transparent; /* 3*/ +} + +/* Sections */ +/* ============================================ */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/* Vertical rhythm */ +/* ============================================ */ + +p, +table, +blockquote, +address, +pre, +iframe, +form, +figure, +dl { + margin: 0; +} + +/* Headings */ +/* ============================================ */ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + line-height: inherit; + font-weight: inherit; + margin: 0; +} + +/* Lists (enumeration) */ +/* ============================================ */ + +ul, +ol { + margin: 0; + padding: 0; + list-style: none; +} + +/* Lists (definition) */ +/* ============================================ */ + +dt { + font-weight: bold; +} + +dd { + margin-left: 0; +} + +/* Grouping content */ +/* ============================================ */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ + border-top-width: 1px; + margin: 0; + clear: both; + color: inherit; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: inherit; /* 2 */ +} + +address { + font-style: inherit; +} + +/* Text-level semantics */ +/* ============================================ */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; + text-decoration: none; + color: inherit; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: inherit; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content */ +/* ============================================ */ + +/** + * Prevent vertical alignment issues. + */ + +img, +embed, +object, +iframe { + vertical-align: bottom; +} + +/* Forms */ +/* ============================================ */ + +/** + * Reset form fields to make them styleable + */ + +button, +input, +optgroup, +select, +textarea { + -webkit-appearance: none; + appearance: none; + vertical-align: middle; + color: inherit; + font: inherit; + background: transparent; + padding: 0; + margin: 0; + outline: 0; + border-radius: 0; + text-align: inherit; +} + +/** + * Reset radio and checkbox appearance to preserve their look in iOS. + */ + +[type="checkbox"] { + -webkit-appearance: checkbox; + appearance: checkbox; +} + +[type="radio"] { + -webkit-appearance: radio; + appearance: radio; +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { + /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { + /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + cursor: pointer; + -webkit-appearance: none; + appearance: none; +} + +button[disabled], +[type="button"][disabled], +[type="reset"][disabled], +[type="submit"][disabled] { + cursor: default; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Remove arrow in IE10 & IE11 + */ + +select::-ms-expand { + display: none; +} + +/** + * Remove padding + */ + +option { + padding: 0; +} + +/** + * Reset to invisible + */ + +fieldset { + margin: 0; + padding: 0; + min-width: 0; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the outline style in Safari. + */ + +[type="search"] { + outline-offset: -2px; /* 1 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/** + * Clickable labels + */ + +label[for] { + cursor: pointer; +} + +/* Interactive */ +/* ============================================ */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* + * Remove outline for editable content. + */ + +[contenteditable] { + outline: none; +} + +/* Table */ +/* ============================================ */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +caption { + text-align: left; +} + +td, +th { + vertical-align: top; + padding: 0; +} + +th { + text-align: left; + font-weight: bold; +} + +/* Misc */ +/* ============================================ */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} \ No newline at end of file diff --git a/copycat/static/copycat/css/style.css b/copycat/static/copycat/css/style.css new file mode 100644 index 0000000..3bd91e8 --- /dev/null +++ b/copycat/static/copycat/css/style.css @@ -0,0 +1,207 @@ +/* style.css */ +body > header { + background: hsl(0, 0%, 20%); + color: #fff; + padding: 20px 0; + max-width: 100%; + margin-left: 0px; + text-align: center; + } + + body { + font-family: Arial, sans-serif; + line-height: 1.6; + margin: 0; + padding: 0; + background-color: #f4f4f9; + color: #333; + padding-left: 0px; + margin-left: 0px; + margin-right: 0px; + } + + h1 { + font-weight: bold; + margin: 20px 0; + margin-left: 50px; + color: #333; + font-size: 24px; + } + + h2 { + margin: 10px 0; + margin-left: 50px; + } + + h3 { + margin: 10px 0; + margin-left: 0px; + } + + p { + margin: 10px 0; + } + + p.expo { + font-size: 12px; + margin: 20px 0; + } + + span { + font-size: 18px; + margin: 10px 0; + } + + form { + background: #fff; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + margin-left: 50px; + margin-bottom: 20px; + max-width: 800px; /* フォーム全体の幅を制限 */ + } + + form p { + margin-bottom: 10px; + } + + form input[type="text"], + form input[type="file"] { + display: block; + width: 100%; + padding: 10px; + margin-bottom: 10px; + border: 1px solid #ddd; + border-radius: 5px; + } + + form input[type="submit"], + form input[type="reset"], + form button { + display: inline-block; + width: auto; + padding: 10px 20px; + margin-left: 0px; + margin-right: 10px; + border: 1px solid #ddd; + border-radius: 5px; + background: #333; + color: #fff; + cursor: pointer; + } + + form input[type="submit"]:hover, + form input[type="reset"]:hover, + form button:hover { + background: #555; + } + + .messages { + list-style: none; + padding: 0; + } + + .messages li { + padding: 10px; + margin-bottom: 10px; + border-radius: 5px; + } + + .messages li.error { + background: #f2dede; + color: #a94442; + margin-left: 50px; + } + + .messages li.success { + background: #dff0d8; + color: #3c763d; + margin-left: 50px; + } + + .box { + background: #fff; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + margin-left: 50px; + margin-bottom: 20px; + max-width: 800px; + } + + pre { + background: #f4f4f9; + padding: 10px; + border-radius: 5px; + overflow: auto; + } + + button { + background: #333; + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + margin-left: 50px; + } + + button:hover { + background: #555; + } + + footer { + background: hsl(0, 0%, 20%); + color: #fff; + padding: 20px 0; + max-width: 100%; + margin-top: 100px; + margin-left: 0px; + text-align: center; + } + + + + /* Below is the css on the right side of the page */ + .container { + display: flex; + justify-content: space-between; + max-width: 1000px; + margin: 0 auto; + padding: 20px; + } + + .main-content { + flex: 1; + margin-right: 20px; + } + + .sidebar { + text-align: center; + width: 250px; + background-color: #fff; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + } + + .sidebar h4 { + background-color: #333; + color: #fff; + border: none; + font-weight: bold; + text-align: center; + border-radius: 25px; + border-width: 2px; + padding: 5px; + margin-top: 0; + margin-bottom: 10px; + } + + .sidebar h5 { + font-weight: bold; + text-align: center; + margin-top: 17px; + } + \ No newline at end of file diff --git a/copycat/test/test_keyboard_detection.py b/copycat/test/test_keyboard_detection.py deleted file mode 100644 index dffe3d1..0000000 --- a/copycat/test/test_keyboard_detection.py +++ /dev/null @@ -1,14 +0,0 @@ -from cv2 import cv2 -from copycat.image_processing.key_extraction import automatically_detect_keyboard_line - - -def test_video_one(): - value = automatically_detect_keyboard_line(cv2.imread("images/fullscreen_youtube_capture.png")) - print(value) - assert 800 < value < 950 - - -def test_video_two(): - value = automatically_detect_keyboard_line(cv2.imread("images/fullscreen_youtube_capture2.png")) - print(value) - assert 760 < value < 880 diff --git a/copycat/test/test_note.py b/copycat/test/test_note.py deleted file mode 100644 index 681fac4..0000000 --- a/copycat/test/test_note.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from notation.note_parser import NoteParser - - -@pytest.mark.parametrize( - "frame_count,tempo,expected_beat_count", - [ - (30, 60, 1), (60, 60, 2), (15, 120, 1), (7, 120, 0.47) - ] -) -def test_beat_count(frame_count: int, tempo: int, expected_beat_count: int): - assert NoteParser.get_beat_count(frame_count, 30, tempo) == expected_beat_count - - -@pytest.mark.parametrize( - "beat_count,expected_beat_length", - [ - (1, 1), (0.23, 0.25), (0.08, 0.0625) - ] -) -def test_beat_length(beat_count: float, expected_beat_length: float): - assert NoteParser.round_beat_length(beat_count) == expected_beat_length diff --git a/copycat/tests.py b/copycat/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/copycat/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/copycat/urls.py b/copycat/urls.py new file mode 100644 index 0000000..dbf92f6 --- /dev/null +++ b/copycat/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('', views.index_view, name='index'), + #path('upload/', views.upload, name='upload'), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/copycat/views.py b/copycat/views.py new file mode 100644 index 0000000..bc03126 --- /dev/null +++ b/copycat/views.py @@ -0,0 +1,37 @@ +from django.shortcuts import render +import subprocess +from django.urls import reverse_lazy, reverse +from django.http import HttpResponse +from .forms import Mp4form +from .models import Uploadmp4 +from django.shortcuts import redirect +from django.contrib import messages +import os + + + +def index_view(request): + if request.method == 'POST': + form = Mp4form(request.POST, request.FILES) + if form.is_valid(): + form.save() + messages.add_message(request, messages.SUCCESS, 'Video upload completed') + return redirect('index') + else: + form = Mp4form() + return render(request, 'index.html', {'form':form}) + + if request.method == 'POST': + input_num1 = request.POST.get('input_num1','') + input_num2 = request.POST.get('input_num2','') + input_num3 = request.POST.get('input_num3','') + input_num4 = request.POST.get('input_num4','') + input_num5 = request.POST.get('input_num5','') + file_path = os.path.join('media', 'files', input_num1) + result_baby = subprocess.run(['python3', 'copycat/main.py', '-f', file_path, '-k', input_num2, '-t', input_num3, '-s', input_num4, '-tc', input_num5], capture_output=True, text=True) + result = result_baby.stdout + return render(request, 'index.html', {'result': result}) + else: + return render(request, 'index.html') + + \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..eac83ec --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'catsite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/media/files/test2.mp4 b/media/files/test2.mp4 new file mode 100644 index 0000000..6df5875 Binary files /dev/null and b/media/files/test2.mp4 differ diff --git a/public/debug_example.png b/public/debug_example.png index 26305fc..f0a8e21 100644 Binary files a/public/debug_example.png and b/public/debug_example.png differ diff --git a/setup.py b/setup.py index ecf3f40..4e498f5 100644 --- a/setup.py +++ b/setup.py @@ -4,9 +4,9 @@ name='copycat', version='1.0', description='Synthesia to Notes Converter', - author='', + author='Sivan Shani', author_email='', url='', packages=['copycat'], - install_requires=["numpy", "opencv-python", "pytest", "tqdm", "webcolors"] + install_requires=["numpy", "opencv-python", "pytest"] ) diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..27da6d3 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,79 @@ +{% load static %} + + +
+ +{{ result }}
+