diff --git a/karriz-mp3/mp3/README.md b/karriz-mp3/mp3/README.md new file mode 100644 index 0000000..abf0edc --- /dev/null +++ b/karriz-mp3/mp3/README.md @@ -0,0 +1,23 @@ +# mp3 web player +테스트를 위해 임시 서버 https://xym0.kalee.land:10443/docs 에서 확인 가능하도록 하였습니다. + +## 모듈 구성 +### mp3.reader +### mp3.trimmer +### mp3.streamer + +## 실행 방법 +해당 프로젝트는 `poetry`를 기반으로 만들어졌습니다. 반드시 실행 전 `poetry` 설치가 필요합니다. + +### poetry 명령어 +```sh +poetry install # 환경 기본 설치 +poetry shell # 가상 shell 진입 +poetry build # 패키지 빌드 +``` + +### 서버 실행 명령어 +```sh +uvicorn main:app --reload --host 0.0.0.0 --port 10001 # 외부 오픈을 위해서는 host가 0.0.0.0으로 되어야함 +``` + diff --git a/karriz-mp3/mp3/__pycache__/main.cpython-310.pyc b/karriz-mp3/mp3/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000..37259bb Binary files /dev/null and b/karriz-mp3/mp3/__pycache__/main.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/assets/empty.mp3 b/karriz-mp3/mp3/assets/empty.mp3 new file mode 100644 index 0000000..e69de29 diff --git a/karriz-mp3/mp3/assets/input.mp3 b/karriz-mp3/mp3/assets/input.mp3 new file mode 100644 index 0000000..c93f7e8 Binary files /dev/null and b/karriz-mp3/mp3/assets/input.mp3 differ diff --git a/karriz-mp3/mp3/assets/input.mp3.skip b/karriz-mp3/mp3/assets/input.mp3.skip new file mode 100644 index 0000000..ebb11e3 Binary files /dev/null and b/karriz-mp3/mp3/assets/input.mp3.skip differ diff --git a/karriz-mp3/mp3/assets/trimmed_input.mp3 b/karriz-mp3/mp3/assets/trimmed_input.mp3 new file mode 100644 index 0000000..c792d94 Binary files /dev/null and b/karriz-mp3/mp3/assets/trimmed_input.mp3 differ diff --git a/karriz-mp3/mp3/assets/wrong.mp3 b/karriz-mp3/mp3/assets/wrong.mp3 new file mode 100644 index 0000000..cc5ef6a Binary files /dev/null and b/karriz-mp3/mp3/assets/wrong.mp3 differ diff --git a/karriz-mp3/mp3/main.py b/karriz-mp3/mp3/main.py new file mode 100644 index 0000000..820fc10 --- /dev/null +++ b/karriz-mp3/mp3/main.py @@ -0,0 +1,33 @@ +from fastapi import FastAPI +from fastapi.responses import StreamingResponse + +from mp3.reader import MP3FileReader +from mp3.trimmer import MP3FileTrimmer +from mp3.streamer import MP3Streamer + +app = FastAPI() + +@app.get("/") +async def root(): + return {"code": 200, "message": "success", "data": None} + +@app.get("/mp3/play") +async def play(filename: str, offset: int = -1, size: int = -1, skip: int = 1): + reader = MP3FileReader(f"assets/{filename}") + + mp3_file = reader.read_skip_frame(skip) + + if offset < 0 and size < 0: + mp3_streamer = MP3Streamer(mp3_file) + return StreamingResponse(mp3_streamer.streaming(), media_type="audio/mpeg") + + mp3_file_trimmer = MP3FileTrimmer(mp3_file) + trimmed_mp3 = mp3_file_trimmer.trim(offset, size, f"assets/trimmed_{filename}") + + mp3_streamer = MP3Streamer(trimmed_mp3) + + mp3_streamer.set_speed("assets/input.x2.mp3", 2.0) + + return StreamingResponse(mp3_streamer.streaming(), media_type="audio/mpeg") + + \ No newline at end of file diff --git a/karriz-mp3/mp3/mp3/__init__.py b/karriz-mp3/mp3/mp3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/karriz-mp3/mp3/mp3/__pycache__/__init__.cpython-310.pyc b/karriz-mp3/mp3/mp3/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..7b638b9 Binary files /dev/null and b/karriz-mp3/mp3/mp3/__pycache__/__init__.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/mp3/__pycache__/const.cpython-310.pyc b/karriz-mp3/mp3/mp3/__pycache__/const.cpython-310.pyc new file mode 100644 index 0000000..719f13e Binary files /dev/null and b/karriz-mp3/mp3/mp3/__pycache__/const.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/mp3/__pycache__/enum.cpython-310.pyc b/karriz-mp3/mp3/mp3/__pycache__/enum.cpython-310.pyc new file mode 100644 index 0000000..4e773e0 Binary files /dev/null and b/karriz-mp3/mp3/mp3/__pycache__/enum.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/mp3/__pycache__/error.cpython-310.pyc b/karriz-mp3/mp3/mp3/__pycache__/error.cpython-310.pyc new file mode 100644 index 0000000..17bea2f Binary files /dev/null and b/karriz-mp3/mp3/mp3/__pycache__/error.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/mp3/__pycache__/model.cpython-310.pyc b/karriz-mp3/mp3/mp3/__pycache__/model.cpython-310.pyc new file mode 100644 index 0000000..1d06c5f Binary files /dev/null and b/karriz-mp3/mp3/mp3/__pycache__/model.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/mp3/__pycache__/parser.cpython-310.pyc b/karriz-mp3/mp3/mp3/__pycache__/parser.cpython-310.pyc new file mode 100644 index 0000000..913cce1 Binary files /dev/null and b/karriz-mp3/mp3/mp3/__pycache__/parser.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/mp3/__pycache__/reader.cpython-310.pyc b/karriz-mp3/mp3/mp3/__pycache__/reader.cpython-310.pyc new file mode 100644 index 0000000..390b277 Binary files /dev/null and b/karriz-mp3/mp3/mp3/__pycache__/reader.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/mp3/__pycache__/streamer.cpython-310.pyc b/karriz-mp3/mp3/mp3/__pycache__/streamer.cpython-310.pyc new file mode 100644 index 0000000..b6c290b Binary files /dev/null and b/karriz-mp3/mp3/mp3/__pycache__/streamer.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/mp3/__pycache__/trimmer.cpython-310.pyc b/karriz-mp3/mp3/mp3/__pycache__/trimmer.cpython-310.pyc new file mode 100644 index 0000000..8cd7144 Binary files /dev/null and b/karriz-mp3/mp3/mp3/__pycache__/trimmer.cpython-310.pyc differ diff --git a/karriz-mp3/mp3/mp3/const.py b/karriz-mp3/mp3/mp3/const.py new file mode 100644 index 0000000..4d89291 --- /dev/null +++ b/karriz-mp3/mp3/mp3/const.py @@ -0,0 +1,52 @@ +KB = 1024 +MB = KB * 1024 +GB = MB * 1024 + +SYNC_WORD_MASK = 0b11111111111 +VERSION_MASK = 0b11 +LAYER_MASK = 0b11 +PROTECTION_MASK = 0b1 +BITRATE_MASK = 0b1111 +SAMPLING_RATE_MASK = 0b11 +PADDING_MASK = 0b1 +PRIVATE_MASK = 0b1 +CHANNEL_MODE_MASK = 0b11 +MODE_EXTENSION_MASK = 0b11 +COPYRIGHT_MASK = 0b1 +ORIGINAL_MASK = 0b1 +EMPHASIS_MASK = 0b11 + +BITRATE_TABLE = [ + [ + [0], # Reserved + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2.5 Layer 3 + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2.5 Layer 2 + [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256], # MPEG-2.5 Layer 1 + ], + [ + # Reserved + [0], + [0], + [0], + [0], + ], + [ + [0], # Reserved + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2 Layer 3 + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2 Layer 2 + [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256] # MPEG-2 Layer 1 + ], + [ + [0], # Reserved + [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320], # MPEG-1 Layer 3 + [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384], # MPEG-1 Layer 2 + [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448] # MPEG-1 Layer 1 + ] +] + +SAMPLE_RATE_TABLE = [ + [11025, 12000, 8000], # MPEG2.5 + [0, 0, 0], # Reserved + [22050, 24000, 16000], # MPEG2 + [44100, 48000, 32000], # MPEG1 +] diff --git a/karriz/mp3_file_reader/enum.py b/karriz-mp3/mp3/mp3/enum.py similarity index 100% rename from karriz/mp3_file_reader/enum.py rename to karriz-mp3/mp3/mp3/enum.py diff --git a/karriz/mp3_file_reader/error.py b/karriz-mp3/mp3/mp3/error.py similarity index 76% rename from karriz/mp3_file_reader/error.py rename to karriz-mp3/mp3/mp3/error.py index 694116a..4a57704 100644 --- a/karriz/mp3_file_reader/error.py +++ b/karriz-mp3/mp3/mp3/error.py @@ -2,10 +2,14 @@ class FileStreamNotLoadedError(Exception): def __init__(self): super().__init__('MP3 파일이 로드 되지않았습니다.') -class FileStreamNotLoadedError(Exception): +class EmptySizeFileError(Exception): def __init__(self): - super().__init__('MP3 파일이 로드 되지않았습니다.') - + super().__init__('빈 파일은 사용할 수 없습니다.') + +class InValidFrameHeaderSizeError(ValueError): + def __init__(self): + super().__init__('프레임 헤더의 크기가 올바르지 않습니다.') + class InvalidFrameSyncError(Exception): def __init__(self): super().__init__('유효하지 않은 FrameSync 값 입니다.') diff --git a/karriz-mp3/mp3/mp3/model.py b/karriz-mp3/mp3/mp3/model.py new file mode 100644 index 0000000..c304802 --- /dev/null +++ b/karriz-mp3/mp3/mp3/model.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass + +from .enum import MP3Version, MP3Layer, MP3Protection, MP3PaddingBit, MP3ChannelMode ,MP3Copyright ,MP3Original, MP3Emphasis + +@dataclass +class MP3FrameHeader: + version: MP3Version = MP3Version.MPEG_2_5 + layer: MP3Layer = MP3Layer.RESERVED + protection: MP3Protection = MP3Protection.PROTECTED_BY_CRC + bitrate: int = 0 + sampling_rate_freq: int = 0 + padding: MP3PaddingBit = MP3PaddingBit.NOT_PADDED + private: int = 0 + channel_mode: MP3ChannelMode = MP3ChannelMode.STEREO + mode_extension: int = 0 + copyright: MP3Copyright = MP3Copyright.NOT_COPYRIGHTED + original: MP3Original = MP3Original.COPY + emphasis: MP3Emphasis = MP3Emphasis.NONE + +@dataclass +class MP3: + file_path: str = "" + total_frame: int = 0 + total_duration: float = 0 # seconds 단위 \ No newline at end of file diff --git a/karriz-mp3/mp3/mp3/parser.py b/karriz-mp3/mp3/mp3/parser.py new file mode 100644 index 0000000..8a337b8 --- /dev/null +++ b/karriz-mp3/mp3/mp3/parser.py @@ -0,0 +1,73 @@ +from . import model, const, enum, error + +class MP3FrameHeaderParser: + def __init__(self, data:bytes): + if len(data) != 4: + raise error.InValidFrameHeaderSizeError + + header_bits = int.from_bytes(data, byteorder='big') + + syncword = (header_bits >> 21) & const.SYNC_WORD_MASK + if syncword != const.SYNC_WORD_MASK: + raise error.InvalidFrameSyncError + + # parse & masking all attributes + version = (header_bits >> 19) & const.VERSION_MASK + layer = (header_bits >> 17) & const.LAYER_MASK + protection = (header_bits >> 16) & const.PROTECTION_MASK + bitrate = (header_bits >> 12) & const.BITRATE_MASK + sampling_rate_freq = (header_bits >> 10) & const.SAMPLING_RATE_MASK + padding = (header_bits >> 9) & const.PADDING_MASK + private = (header_bits >> 8) & const.PRIVATE_MASK + channel_mode = (header_bits >> 6) & const.CHANNEL_MODE_MASK + mode_extension = (header_bits >> 4) & const.MODE_EXTENSION_MASK + copyright = (header_bits >> 3) & const.COPYRIGHT_MASK + original = (header_bits >> 2) & const.ORIGINAL_MASK + emphasis = (header_bits >> 0) & const.EMPHASIS_MASK + + # instantiate mp3_frame_header dataclass + mp3_frame_header = model.MP3FrameHeader() + + # version + mp3_frame_header.version = enum.MP3Version(version) + + # layer + mp3_frame_header.layer = enum.MP3Layer(layer) + + # protection bit + mp3_frame_header.protection = enum.MP3Protection(protection) + + # bitrate + mp3_frame_header.bitrate = const.BITRATE_TABLE[version][layer][bitrate] * 1000 # kbps -> bps로 변환 + + # sampling rate frequency + mp3_frame_header.sampling_rate_freq = const.SAMPLE_RATE_TABLE[version][sampling_rate_freq] + + # padding + mp3_frame_header.padding = enum.MP3PaddingBit(padding) + + # private + mp3_frame_header.private = private + + # channel mode + mp3_frame_header.channel_mode = enum.MP3ChannelMode(channel_mode) + + # mode extension + mp3_frame_header.mode_extension = mode_extension + + # copyright + mp3_frame_header.copyright = enum.MP3Copyright(copyright) + + # original + mp3_frame_header.original = enum.MP3Original(original) + + #emphasis + mp3_frame_header.emphasis = enum.MP3Emphasis(emphasis) + + self.__mp3_frame_header = mp3_frame_header + + def get_frame_size(self) -> int: + return (144 * self.__mp3_frame_header.bitrate) // self.__mp3_frame_header.sampling_rate_freq + self.__mp3_frame_header.padding.value + + def get_frame_duration(self) -> float: + return 1152 / self.__mp3_frame_header.sampling_rate_freq \ No newline at end of file diff --git a/karriz-mp3/mp3/mp3/reader.py b/karriz-mp3/mp3/mp3/reader.py new file mode 100644 index 0000000..08ad96e --- /dev/null +++ b/karriz-mp3/mp3/mp3/reader.py @@ -0,0 +1,70 @@ +import os + +from . import model, error +from . import parser + +class MP3FileReader: + def __init__(self, file_path:str): + file_size = os.path.getsize(file_path) + + if file_size == 0: + raise error.EmptySizeFileError + + # CBR, VBR에 따라 비트레이트가 바뀔 수 있는데... reader가 필요할까 + # read -> trim -> 제공 하면 memory에 데이터가 너무 많이 쌓이는데 ..? + self.__mp3 = model.MP3() + self.__mp3.file_path = file_path + + def read(self) -> model.MP3: + mp3_file = open(self.__mp3.file_path, 'rb') + + while True: + try: + frame_header_parser = parser.MP3FrameHeaderParser(mp3_file.read(4)) + + self.__mp3.total_duration += frame_header_parser.get_frame_duration() + mp3_file.seek(frame_header_parser.get_frame_size()-4, 1) + + self.__mp3.total_frame += 1 + + except error.InValidFrameHeaderSizeError: + break + + mp3_file.close() + + return self.__mp3 + + def read_skip_frame(self, skipped_frame_size: int = 1) -> model.MP3: + mp3_file = open(self.__mp3.file_path, 'rb') + skipped_mp3_file = open(f"{self.__mp3.file_path}.skip", 'wb') + + frame_count = 0 + + skipped_mp3 = model.MP3() + skipped_mp3.file_path = f"{self.__mp3.file_path}.skip" + + while True: + try: + frame_header_bytes = mp3_file.read(4) + frame_header_parser = parser.MP3FrameHeaderParser(frame_header_bytes) + + frame_bytes = mp3_file.read(frame_header_parser.get_frame_size()-4) + + if frame_count % skipped_frame_size != 0: + skipped_mp3_file.write(frame_header_bytes) + skipped_mp3_file.write(frame_bytes) + + skipped_mp3.total_duration += frame_header_parser.get_frame_duration() + skipped_mp3.total_frame += 1 + + frame_count += 1 + + except error.InvalidFrameSyncError: + mp3_file.seek(-3, 1) + + except error.InValidFrameHeaderSizeError: + break + + mp3_file.close() + + return skipped_mp3 diff --git a/karriz-mp3/mp3/mp3/streamer.py b/karriz-mp3/mp3/mp3/streamer.py new file mode 100644 index 0000000..87575ec --- /dev/null +++ b/karriz-mp3/mp3/mp3/streamer.py @@ -0,0 +1,36 @@ +from pydub import AudioSegment +import numpy as np +from scipy.signal import resample +import lameenc + +from .const import KB + +from . import model + +class MP3Streamer: + def __init__(self, mp3:model.MP3): + self.__mp3 = mp3 + self.__audio = AudioSegment.from_mp3(mp3.file_path) + self.__pcm_data = np.array(self.__audio.get_array_of_samples()) + self.__sample_rate = self.__audio.frame_rate + self.__channels = self.__audio.channels + + def set_speed(self, output_file:str, speed_factor: float = 1.0): + num_samples = int(len(self.__pcm_data) / speed_factor) + adjusted_pcm_data = resample(self.__pcm_data, num_samples) + + encoder = lameenc.Encoder() + encoder.set_bit_rate(128) + encoder.set_in_sample_rate(self.__sample_rate) + encoder.set_channels(self.__channels) + encoder.set_quality(2) + + mp3_data = encoder.encode(adjusted_pcm_data.tobytes()) + mp3_data += encoder.flush() + + return mp3_data + + def streaming(self): + with open(self.__mp3.file_path, "rb") as file: + while chunk := file.read(50 * KB): + yield chunk \ No newline at end of file diff --git a/karriz-mp3/mp3/mp3/trimmer.py b/karriz-mp3/mp3/mp3/trimmer.py new file mode 100644 index 0000000..4553f21 --- /dev/null +++ b/karriz-mp3/mp3/mp3/trimmer.py @@ -0,0 +1,43 @@ +from . import model, parser, error + +class MP3FileTrimmer: + def __init__(self, mp3:model.MP3): + self.__mp3 = mp3 + + def trim(self, offset:int, size:int, output_file_path:str) -> model.MP3: + mp3_file = open(self.__mp3.file_path, 'rb') + trimmed_output_file = open(output_file_path, 'wb') + + trimmed_mp3 = model.MP3() + trimmed_mp3.file_path = output_file_path + + current_frame = 0 + frame_count = 0 + + while True: + try: + frame_header_bytes = mp3_file.read(4) + frame_header_parser = parser.MP3FrameHeaderParser(frame_header_bytes) + + trimmed_mp3.total_duration += frame_header_parser.get_frame_duration() + frame_bytes = mp3_file.read(frame_header_parser.get_frame_size()-4) + + current_frame += 1 + + if current_frame >= offset and current_frame < (offset+size): + trimmed_output_file.write(frame_header_bytes) + trimmed_output_file.write(frame_bytes) + frame_count += 1 + + if current_frame > (offset+size): + break + + except error.InValidFrameHeaderSizeError: + break + + trimmed_mp3.total_frame = frame_count + + mp3_file.close() + trimmed_output_file.close() + + return trimmed_mp3 diff --git a/karriz-mp3/mp3/poetry.lock b/karriz-mp3/mp3/poetry.lock new file mode 100644 index 0000000..c3fa1b9 --- /dev/null +++ b/karriz-mp3/mp3/poetry.lock @@ -0,0 +1,506 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.115.3" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.115.3-py3-none-any.whl", hash = "sha256:8035e8f9a2b0aa89cea03b6c77721178ed5358e1aea4cd8570d9466895c0638c"}, + {file = "fastapi-0.115.3.tar.gz", hash = "sha256:c091c6a35599c036d676fa24bd4a6e19fa30058d93d950216cdc672881f6f7db"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.42.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "lameenc" +version = "1.7.0" +description = "LAME encoding bindings" +optional = false +python-versions = "*" +files = [ + {file = "lameenc-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:666dae5544b82b7c7e2d45ee82b0b74a3b5d46c62f79df603a30f4b633c78556"}, + {file = "lameenc-1.7.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:73ba7b02685d9c17a3b083622b85c859f9db35254d4ea9ee8305d4a8d321876b"}, + {file = "lameenc-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:367c152dd1dc54c2d6c42837c1c149b0173eb4225e4782f88b1a391c95ff97af"}, + {file = "lameenc-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b34c529d719bf303ace7ec169965c516b0b94922837cf67f60a73931f1f4580"}, + {file = "lameenc-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ed1f49416e9531a49f462923ca0d2ae14baf9ceea0384cba2c5f9f1090f0df31"}, + {file = "lameenc-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3274cade2d3f00cc49748b968660e1297eaca2d15911deb566cc378ffa4e7d8b"}, + {file = "lameenc-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:790c250086dd38b26860ccb5574ad0f2c625b52bb9f2a367e07e50ffbf3de832"}, + {file = "lameenc-1.7.0-cp310-cp310-win32.whl", hash = "sha256:8b712902e02ea03e9a5272ee008062b08d99129e7a00a369b90848a9bfab050f"}, + {file = "lameenc-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:c8fd147d1faa904cb0b8ba3b038ab795daebec46147b2b9e3891dbb427084f5c"}, + {file = "lameenc-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5a5b322d82dfdf6132c563e1fa352487c82a0bb1e93dff8ef634c867c94d82ad"}, + {file = "lameenc-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a582a1f356c83be526ba97ce8465553cc55524992325ab1641b3ee0428e6453"}, + {file = "lameenc-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e3399b72c8adb965405aeb8ccc614db9b94e5842426289166c4fbf2588cf1e74"}, + {file = "lameenc-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93eedf217db3832ead5ea28ad89b2f4dd3d79e3a7d5641d09dbeeeffe6283768"}, + {file = "lameenc-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:63a2ce2d63706d766ad1686fbc5e12c807c027b7ccfc97de9b9a81f58216e770"}, + {file = "lameenc-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2cb91af4b25b3d650781e681032c469fd38979b180eddfbf5a55322372d71b9"}, + {file = "lameenc-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:e67f5e985d940f9f8037823e6a629a18acbfe02d0fcd68a8fc113ed46a555aa5"}, + {file = "lameenc-1.7.0-cp311-cp311-win32.whl", hash = "sha256:c835438c0e8b8d680e871095cec143abf655d071b1bb60ccf7a8e08245acd877"}, + {file = "lameenc-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:93f426a565e666227303257db67287f33836872dee62d982d521d8972467df46"}, + {file = "lameenc-1.7.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ff9b6e1fed60a1e1f54861b51628a5c99a4ebc8be76462b8995c7cae99722ec5"}, + {file = "lameenc-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a14a5f449cc32158ac2fd557a21b6a35dd1cb077e0372ede69572740f9b67905"}, + {file = "lameenc-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b425fec9430a0aa55877d8787f82b47ff2c5d2a36663a4ca562103d7bbaeb23a"}, + {file = "lameenc-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:002742147c99e819be4feb8ab05da08ff57f0f76480746ddc83f6a2c09d2639d"}, + {file = "lameenc-1.7.0-cp312-cp312-win32.whl", hash = "sha256:bbabad75799e62638c0e55537e50ceba81456600ce04fb604c006c1b7399e8c1"}, + {file = "lameenc-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:0faf238be8044ee706d5f410e19ca1ae38fafa75dedcdc69ed764964d01c8d8b"}, + {file = "lameenc-1.7.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:c194dd436ed1894e325f3cb8baebf76cfa5ee867810c3fb8af2a76a62e219ac1"}, + {file = "lameenc-1.7.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eec093a6673c7eebeebc269d7735ea09ec9cfda696ff92959b33bc1117b26954"}, + {file = "lameenc-1.7.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:2bed2133410206f61780c412e49c218bf40179016de0284f61ed5bead3cce95a"}, + {file = "lameenc-1.7.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96e54919790e2b4e3519f1696ac19a5efb864c0621e7a2baf89ecfa95908e044"}, + {file = "lameenc-1.7.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:26799cc2dac7d972f354489f33a5ab99d5ca4791edd1b0b3bd87e89bfa22f34f"}, + {file = "lameenc-1.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:62ec507686326a6baec8b997070e96328ba8ff03abdc688e36ab47114883a223"}, + {file = "lameenc-1.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e00c8771be3e002a539fd534f8df063a2dafc2d679803a7a4dfd7886ffa4304"}, + {file = "lameenc-1.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:be3cf0820f278996680a48d163f781e85431704c0551d90780b165798f2ea1c5"}, + {file = "lameenc-1.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bc05b160603665e570f2d8dd6a70dc6e337cb59dbd5e9a72044afb92c629096"}, + {file = "lameenc-1.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:0d6c195df3f22e39dd2e2963858b0f6a73db8b993ca4f1996c5c147d386c6151"}, + {file = "lameenc-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b18015e33d9530f47bdefc1a0dc91e4a8b94a3b0ff5fbdcac82b64b170388e8"}, + {file = "lameenc-1.7.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:d63cd0f1b52b7719b212d9fe8c1fdeb8aa8618b9f943e628001981c235ae8722"}, + {file = "lameenc-1.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:1ecd67a8270075885b5b6f3cdf0c4bb3fb0a4589a679e55298f2606ee2fcb5ce"}, + {file = "lameenc-1.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbbe822069bfadb4608c11a578f027f9c8ba01182332cd538a8ee55cf5d3bf23"}, + {file = "lameenc-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:110b781e7916849eb430411180ac8c3821ffc7e0d15a18b3f26129e1aff2ce8f"}, + {file = "lameenc-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f84bdcc3daa0b7635ba2a41d995b321b5d00b6637c89886f88e1d88e356c9baa"}, + {file = "lameenc-1.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:e6c6a934754ed32a7680aaaf15f7fd20e29575239b93a23ee86dafa9b0143608"}, + {file = "lameenc-1.7.0-cp38-cp38-win32.whl", hash = "sha256:4193472c4ec50397b234284551b52af23dd6f998634e0bd75e2caad5bb85ace7"}, + {file = "lameenc-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:ff836d4964cc2f7197a14dfc8e2d3787f5e37f1d809ed914995a95ea24abce8f"}, + {file = "lameenc-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c4dc7b13e871f9d4667be161823082f87c05252b36c1f1e3e0088765f0e81b5b"}, + {file = "lameenc-1.7.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:6667cfe1ab6c15960cfe5fdc6ffdc6c1818d1385da143fcb0692218500b60b1e"}, + {file = "lameenc-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d46fea0110812e89a4add722bc18e20e8146843395f9b4b9fad9f98a6ccf327e"}, + {file = "lameenc-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f8e056d48f07bd17eecca11643c59aaf2804f6c1f822e4ab5b2bf5a313ef1a4"}, + {file = "lameenc-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:4926c2b50b384c7640c78757b89a7faaccc6a6fcde5fd9016b554f946522e157"}, + {file = "lameenc-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d89e63c88e5a224299b310ac9430c2c2e16ca4f2b479e65baf4f17c92928e2a"}, + {file = "lameenc-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:6cec7e369067d54528b98023fb75c26cd539bbc9292e73fe58516b6964b35db5"}, + {file = "lameenc-1.7.0-cp39-cp39-win32.whl", hash = "sha256:53f1dc2574052354baecf07ba8d2da71dd9aab310c3efc8e029dae13be22a29f"}, + {file = "lameenc-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:c3589924788e26afd55a1faa3947ba70167d9ba9901d42ba2b372922999e0ea6"}, + {file = "lameenc-1.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f51808bd3f2da74ce586ef3e3bcae8b77a4a59e9c476a46681d7c052c1c2211"}, + {file = "lameenc-1.7.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c74611af25522db4bcca9221254151ed6133847aa355595152d343d430dd888b"}, + {file = "lameenc-1.7.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5bf4632fed752eca67c93d8ce89098cec8a58706d995151f27eec493771c4856"}, + {file = "lameenc-1.7.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29c9cc3c9012cf52640c071fc519f4b55c40e87b4e07f43ff80e939e6f6baae4"}, + {file = "lameenc-1.7.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a2d88a1e32246f6071ad18dc42c6a9a2af2d8a600de561a887d4dc3d335b2f3d"}, + {file = "lameenc-1.7.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9edec7fef66d0e1bd25690780ef697c3bea15f4f614e04de3fd930d558c43833"}, + {file = "lameenc-1.7.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:6b7e0d28c3982eabf9fd8c51f19593c4c1684bb0e261482284a3fd56134bc302"}, + {file = "lameenc-1.7.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:a84335effaa1f56202a359b9fb2524e4e486e3c37c72a66647626220b22e34bb"}, + {file = "lameenc-1.7.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d089d5c2ae6430531e2dad49d9d4bfa06ff5e6ee9a2b24f1e1ccb5c942be200b"}, + {file = "lameenc-1.7.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:02b7ab137f1fb624020210ae3c2f671a9006ec30d2bec704a55aaebb3850ab3d"}, + {file = "lameenc-1.7.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c76a21488ba35b1dd1d37d8b0671646cb52ca92f7d9b2bb8234bf106ba57db08"}, + {file = "lameenc-1.7.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:a46642b88d9461fd1377f2fcae387503d80753b954b2547ace91e5ba5fff11af"}, + {file = "lameenc-1.7.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0d0aa76ab3642c1025d8a47f0df4986147ae03f10d83c4b5ff34e8ddd1b62c45"}, + {file = "lameenc-1.7.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c103b3a021a000de7822249d91008e1adfcb70e7bdffa3483c5794045dc051dc"}, + {file = "lameenc-1.7.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:74fafaa009697a39b4dfae0d6763ad666ccda75386d68befd943decf46610722"}, + {file = "lameenc-1.7.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3fe077c20dd521fd261ae5dfccab80d94eace7b7474035f1584a08eca1baa7f"}, + {file = "lameenc-1.7.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:0bbd88b127bafce6da8ac8b03d5a87791d32b928dc61533e4752a935808fc851"}, +] + +[[package]] +name = "numpy" +version = "2.1.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "numpy-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee"}, + {file = "numpy-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884"}, + {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648"}, + {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7c1c60328bd964b53f8b835df69ae8198659e2b9302ff9ebb7de4e5a5994db3d"}, + {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdb606a7478f9ad91c6283e238544451e3a95f30fb5467fbf715964341a8a86"}, + {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d666cb72687559689e9906197e3bec7b736764df6a2e58ee265e360663e9baf7"}, + {file = "numpy-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6eef7a2dbd0abfb0d9eaf78b73017dbfd0b54051102ff4e6a7b2980d5ac1a03"}, + {file = "numpy-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:12edb90831ff481f7ef5f6bc6431a9d74dc0e5ff401559a71e5e4611d4f2d466"}, + {file = "numpy-2.1.2-cp310-cp310-win32.whl", hash = "sha256:a65acfdb9c6ebb8368490dbafe83c03c7e277b37e6857f0caeadbbc56e12f4fb"}, + {file = "numpy-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:860ec6e63e2c5c2ee5e9121808145c7bf86c96cca9ad396c0bd3e0f2798ccbe2"}, + {file = "numpy-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe"}, + {file = "numpy-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1"}, + {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f"}, + {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4"}, + {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a"}, + {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1"}, + {file = "numpy-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2"}, + {file = "numpy-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146"}, + {file = "numpy-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c"}, + {file = "numpy-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9"}, + {file = "numpy-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b"}, + {file = "numpy-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db"}, + {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1"}, + {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426"}, + {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0"}, + {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df"}, + {file = "numpy-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366"}, + {file = "numpy-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142"}, + {file = "numpy-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550"}, + {file = "numpy-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e"}, + {file = "numpy-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d"}, + {file = "numpy-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf"}, + {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e"}, + {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3"}, + {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8"}, + {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a"}, + {file = "numpy-2.1.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98"}, + {file = "numpy-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe"}, + {file = "numpy-2.1.2-cp313-cp313-win32.whl", hash = "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a"}, + {file = "numpy-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445"}, + {file = "numpy-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5"}, + {file = "numpy-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0"}, + {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17"}, + {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6"}, + {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8"}, + {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35"}, + {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62"}, + {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a"}, + {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952"}, + {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5"}, + {file = "numpy-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7"}, + {file = "numpy-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a7d80b2e904faa63068ead63107189164ca443b42dd1930299e0d1cb041cec2e"}, + {file = "numpy-2.1.2.tar.gz", hash = "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c"}, +] + +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydub" +version = "0.25.1" +description = "Manipulate audio with an simple and easy high level interface" +optional = false +python-versions = "*" +files = [ + {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, + {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, +] + +[[package]] +name = "scipy" +version = "1.14.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"}, + {file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"}, + {file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"}, + {file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"}, + {file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"}, + {file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"}, + {file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"}, + {file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"}, + {file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"}, + {file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.41.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.41.0-py3-none-any.whl", hash = "sha256:a0193a3c413ebc9c78bff1c3546a45bb8c8bcb4a84cae8747d650a65bd37210a"}, + {file = "starlette-0.41.0.tar.gz", hash = "sha256:39cbd8768b107d68bfe1ff1672b38a2c38b49777de46d2a592841d58e3bf7c2a"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "uvicorn" +version = "0.32.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82"}, + {file = "uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "103dad793b34fa870f77f818924791faaf1ea0a9fef501d7c56662e6900dca07" diff --git a/karriz-mp3/mp3/pyproject.toml b/karriz-mp3/mp3/pyproject.toml new file mode 100644 index 0000000..ba9d3f3 --- /dev/null +++ b/karriz-mp3/mp3/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "mp3" +version = "0.1.0" +description = "" +authors = ["karriz-dev "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +fastapi = "^0.115.3" +uvicorn = "^0.32.0" +pydub = "^0.25.1" +numpy = "^2.1.2" +scipy = "^1.14.1" +lameenc = "^1.7.0" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/karriz-mp3/mp3/tests/__init__.py b/karriz-mp3/mp3/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/karriz-mp3/mp3/tests/test_reader.py b/karriz-mp3/mp3/tests/test_reader.py new file mode 100644 index 0000000..d4a85d9 --- /dev/null +++ b/karriz-mp3/mp3/tests/test_reader.py @@ -0,0 +1,34 @@ +import unittest +from mp3.reader import MP3FileReader +from mp3.trimmer import MP3FileTrimmer + +class MP3FileReaderTests(unittest.TestCase): + def test_read_mp3_file(self): + reader = MP3FileReader("./assets/input.mp3") + + mp3_file = reader.read() + + # 읽은 파일 경로 확인 + self.assertEqual(mp3_file.file_path, "./assets/input.mp3") + + # 재생 시간 3분 54초를 기준으로 계산 + self.assertEqual(int(mp3_file.total_duration), 234) + +class MP3FileTrimmerTests(unittest.TestCase): + def setUp(self): + reader = MP3FileReader("./assets/input.mp3") + + self.mp3_file = reader.read() + + def test_trim_mp3_file(self): + mp3_file_trimmer = MP3FileTrimmer(self.mp3_file) + + # mp3 파일을 프레임 단위로 trimming 후 경로로 저장 + trimmed_mp3 = mp3_file_trimmer.trim(0, 1000, "./assets/trimmed_input.mp3") + + # trimming 된 이후 총 duration이 1000 프레임 만큼 잘렸는지 확인 + # 1 프레임당 대략 0.026s = 1000 프레임은 26s + self.assertEqual(int(trimmed_mp3.total_duration), 26) + +if __name__ == '__main__': + unittest.main() diff --git a/karriz/README.md b/karriz/README.md index 67cf004..5fa39e7 100644 --- a/karriz/README.md +++ b/karriz/README.md @@ -63,3 +63,10 @@ TDD는 말그대로 테스트 주도형 개발로, 개발 방법론 중 하나 ### 4 Week 8. MP3 스트리밍 9. 오디오 배속 조절 기능 + +## Usages +MP3 패키지는 Poetry를 사용하여 패키지 관리를 진행합니다. 아래 명령어를 통해 접근가능합니다. +``` +poetry install +poetry show +``` \ No newline at end of file diff --git a/karriz/dist/mp3_web_api-0.0.1.tar.gz b/karriz/dist/mp3_web_api-0.0.1.tar.gz new file mode 100644 index 0000000..8e0c4b4 Binary files /dev/null and b/karriz/dist/mp3_web_api-0.0.1.tar.gz differ diff --git a/karriz/mp3/README.md b/karriz/mp3/README.md new file mode 100644 index 0000000..3b5d61b --- /dev/null +++ b/karriz/mp3/README.md @@ -0,0 +1 @@ +# Python MP3 Package diff --git a/karriz/mp3/dist/mp3-0.0.4-py3-none-any.whl b/karriz/mp3/dist/mp3-0.0.4-py3-none-any.whl new file mode 100644 index 0000000..969bbee Binary files /dev/null and b/karriz/mp3/dist/mp3-0.0.4-py3-none-any.whl differ diff --git a/karriz/mp3/dist/mp3-0.0.4.tar.gz b/karriz/mp3/dist/mp3-0.0.4.tar.gz new file mode 100644 index 0000000..7bb7ec2 Binary files /dev/null and b/karriz/mp3/dist/mp3-0.0.4.tar.gz differ diff --git a/karriz/mp3/mp3/__init__.py b/karriz/mp3/mp3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/karriz/mp3/mp3/const.py b/karriz/mp3/mp3/const.py new file mode 100644 index 0000000..1f186b4 --- /dev/null +++ b/karriz/mp3/mp3/const.py @@ -0,0 +1,48 @@ +SYNC_WORD_MASK = 0b11111111111 +VERSION_MASK = 0b11 +LAYER_MASK = 0b11 +PROTECTION_MASK = 0b1 +BITRATE_MASK = 0b1111 +SAMPLING_RATE_MASK = 0b11 +PADDING_MASK = 0b1 +PRIVATE_MASK = 0b1 +CHANNEL_MODE_MASK = 0b11 +MODE_EXTENSION_MASK = 0b11 +COPYRIGHT_MASK = 0b1 +ORIGINAL_MASK = 0b1 +EMPHASIS_MASK = 0b11 + +BITRATE_TABLE = [ + [ + [0], # Reserved + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2.5 Layer 3 + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2.5 Layer 2 + [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256], # MPEG-2.5 Layer 1 + ], + [ + # Reserved + [0], + [0], + [0], + [0], + ], + [ + [0], # Reserved + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2 Layer 3 + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2 Layer 2 + [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256] # MPEG-2 Layer 1 + ], + [ + [0], # Reserved + [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320], # MPEG-1 Layer 3 + [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384], # MPEG-1 Layer 2 + [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448] # MPEG-1 Layer 1 + ] +] + +SAMPLE_RATE_TABLE = [ + [11025, 12000, 8000], # MPEG2.5 + [0, 0, 0], # Reserved + [22050, 24000, 16000], # MPEG2 + [44100, 48000, 32000], # MPEG1 +] diff --git a/karriz/mp3/mp3/enum.py b/karriz/mp3/mp3/enum.py new file mode 100644 index 0000000..be89ed9 --- /dev/null +++ b/karriz/mp3/mp3/enum.py @@ -0,0 +1,41 @@ +from enum import Enum + +class MP3Version(Enum): + MPEG_2_5 = 0 + RESERVED = 1 + MPEG_2 = 2 + MPEG_1 = 3 + +class MP3Layer(Enum): + RESERVED = 0 + LAYER_III = 1 + LAYER_II = 2 + LAYER_I = 3 + +class MP3Protection(Enum): + PROTECTED_BY_CRC = 0 + NOT_PROTECTED = 1 + +class MP3PaddingBit(Enum): + NOT_PADDED = 0 + PADDED = 1 + +class MP3ChannelMode(Enum): + STEREO = 0 + JOINT_STEREO = 1 + DUAL_MONO_CAHNNEL = 2 + SINGLE_MONO_CHANNEL = 3 + +class MP3Copyright(Enum): + NOT_COPYRIGHTED = 0 + COPYRIGHTED = 1 + +class MP3Original(Enum): + COPY = 0 + ORIGINAL = 1 + +class MP3Emphasis(Enum): + NONE = 0 + E_50_15_MS = 1 # 50/15µs emphasis + RESERVED = 2 + CCIT_J_17 = 3 \ No newline at end of file diff --git a/karriz/mp3/mp3/error.py b/karriz/mp3/mp3/error.py new file mode 100644 index 0000000..4a57704 --- /dev/null +++ b/karriz/mp3/mp3/error.py @@ -0,0 +1,31 @@ +class FileStreamNotLoadedError(Exception): + def __init__(self): + super().__init__('MP3 파일이 로드 되지않았습니다.') + +class EmptySizeFileError(Exception): + def __init__(self): + super().__init__('빈 파일은 사용할 수 없습니다.') + +class InValidFrameHeaderSizeError(ValueError): + def __init__(self): + super().__init__('프레임 헤더의 크기가 올바르지 않습니다.') + +class InvalidFrameSyncError(Exception): + def __init__(self): + super().__init__('유효하지 않은 FrameSync 값 입니다.') + +class VersionNotMatchedError(Exception): + def __init__(self): + super().__init__('MP3와 맞지 않는 버전 값 입니다.') + +class LayerNotMatchedError(Exception): + def __init__(self): + super().__init__('MP3와 맞지 않는 레이어 값 입니다.') + +class BadBitrateError(Exception): + def __init__(self): + super().__init__('잘못 된 Bitrate 값 입니다.') + +class InvalidFrequencyError(Exception): + def __init__(self): + super().__init__('잘못 된 Frequency 값 입니다.') diff --git a/karriz/mp3/mp3/model.py b/karriz/mp3/mp3/model.py new file mode 100644 index 0000000..c304802 --- /dev/null +++ b/karriz/mp3/mp3/model.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass + +from .enum import MP3Version, MP3Layer, MP3Protection, MP3PaddingBit, MP3ChannelMode ,MP3Copyright ,MP3Original, MP3Emphasis + +@dataclass +class MP3FrameHeader: + version: MP3Version = MP3Version.MPEG_2_5 + layer: MP3Layer = MP3Layer.RESERVED + protection: MP3Protection = MP3Protection.PROTECTED_BY_CRC + bitrate: int = 0 + sampling_rate_freq: int = 0 + padding: MP3PaddingBit = MP3PaddingBit.NOT_PADDED + private: int = 0 + channel_mode: MP3ChannelMode = MP3ChannelMode.STEREO + mode_extension: int = 0 + copyright: MP3Copyright = MP3Copyright.NOT_COPYRIGHTED + original: MP3Original = MP3Original.COPY + emphasis: MP3Emphasis = MP3Emphasis.NONE + +@dataclass +class MP3: + file_path: str = "" + total_frame: int = 0 + total_duration: float = 0 # seconds 단위 \ No newline at end of file diff --git a/karriz/mp3/mp3/parser.py b/karriz/mp3/mp3/parser.py new file mode 100644 index 0000000..8a337b8 --- /dev/null +++ b/karriz/mp3/mp3/parser.py @@ -0,0 +1,73 @@ +from . import model, const, enum, error + +class MP3FrameHeaderParser: + def __init__(self, data:bytes): + if len(data) != 4: + raise error.InValidFrameHeaderSizeError + + header_bits = int.from_bytes(data, byteorder='big') + + syncword = (header_bits >> 21) & const.SYNC_WORD_MASK + if syncword != const.SYNC_WORD_MASK: + raise error.InvalidFrameSyncError + + # parse & masking all attributes + version = (header_bits >> 19) & const.VERSION_MASK + layer = (header_bits >> 17) & const.LAYER_MASK + protection = (header_bits >> 16) & const.PROTECTION_MASK + bitrate = (header_bits >> 12) & const.BITRATE_MASK + sampling_rate_freq = (header_bits >> 10) & const.SAMPLING_RATE_MASK + padding = (header_bits >> 9) & const.PADDING_MASK + private = (header_bits >> 8) & const.PRIVATE_MASK + channel_mode = (header_bits >> 6) & const.CHANNEL_MODE_MASK + mode_extension = (header_bits >> 4) & const.MODE_EXTENSION_MASK + copyright = (header_bits >> 3) & const.COPYRIGHT_MASK + original = (header_bits >> 2) & const.ORIGINAL_MASK + emphasis = (header_bits >> 0) & const.EMPHASIS_MASK + + # instantiate mp3_frame_header dataclass + mp3_frame_header = model.MP3FrameHeader() + + # version + mp3_frame_header.version = enum.MP3Version(version) + + # layer + mp3_frame_header.layer = enum.MP3Layer(layer) + + # protection bit + mp3_frame_header.protection = enum.MP3Protection(protection) + + # bitrate + mp3_frame_header.bitrate = const.BITRATE_TABLE[version][layer][bitrate] * 1000 # kbps -> bps로 변환 + + # sampling rate frequency + mp3_frame_header.sampling_rate_freq = const.SAMPLE_RATE_TABLE[version][sampling_rate_freq] + + # padding + mp3_frame_header.padding = enum.MP3PaddingBit(padding) + + # private + mp3_frame_header.private = private + + # channel mode + mp3_frame_header.channel_mode = enum.MP3ChannelMode(channel_mode) + + # mode extension + mp3_frame_header.mode_extension = mode_extension + + # copyright + mp3_frame_header.copyright = enum.MP3Copyright(copyright) + + # original + mp3_frame_header.original = enum.MP3Original(original) + + #emphasis + mp3_frame_header.emphasis = enum.MP3Emphasis(emphasis) + + self.__mp3_frame_header = mp3_frame_header + + def get_frame_size(self) -> int: + return (144 * self.__mp3_frame_header.bitrate) // self.__mp3_frame_header.sampling_rate_freq + self.__mp3_frame_header.padding.value + + def get_frame_duration(self) -> float: + return 1152 / self.__mp3_frame_header.sampling_rate_freq \ No newline at end of file diff --git a/karriz/mp3/mp3/reader.py b/karriz/mp3/mp3/reader.py new file mode 100644 index 0000000..6cdf4d0 --- /dev/null +++ b/karriz/mp3/mp3/reader.py @@ -0,0 +1,35 @@ +import os + +from . import model, error +from . import parser + +class MP3FileReader: + def __init__(self, file_path:str): + file_size = os.path.getsize(file_path) + + if file_size == 0: + raise error.EmptySizeFileError + + # CBR, VBR에 따라 비트레이트가 바뀔 수 있는데... reader가 필요할까 + # read -> trim -> 제공 하면 memory에 데이터가 너무 많이 쌓이는데 ..? + self.mp3 = model.MP3() + self.mp3.file_path = file_path + + def read(self) -> model.MP3: + mp3_file = open(self.mp3.file_path, 'rb') + + while True: + try: + frame_header_parser = parser.MP3FrameHeaderParser(mp3_file.read(4)) + + self.mp3.total_duration += frame_header_parser.get_frame_duration() + mp3_file.seek(frame_header_parser.get_frame_size()-4, 1) + + self.mp3.total_frame += 1 + except error.InValidFrameHeaderSizeError: + break + + mp3_file.close() + + return self.mp3 + diff --git a/karriz/mp3/mp3/trimmer.py b/karriz/mp3/mp3/trimmer.py new file mode 100644 index 0000000..2540085 --- /dev/null +++ b/karriz/mp3/mp3/trimmer.py @@ -0,0 +1,39 @@ +from . import model, parser, error + +class MP3FileTrimmer: + def __init__(self, mp3:model.MP3): + self.__mp3 = mp3 + + def trim(self, start:int, end:int, output_file_path:str) -> model.MP3: + mp3_file = open(self.__mp3.file_path, 'rb') + trimmed_output_file = open(output_file_path, 'wb') + + trimmed_mp3 = model.MP3() + current_frame = 0 + + while True: + try: + frame_header_bytes = mp3_file.read(4) + frame_header_parser = parser.MP3FrameHeaderParser(frame_header_bytes) + + trimmed_mp3.total_duration += frame_header_parser.get_frame_duration() + frame_bytes = mp3_file.read(frame_header_parser.get_frame_size()-4) + + current_frame += 1 + + if current_frame >= start and current_frame <= end: + trimmed_output_file.write(frame_header_bytes) + trimmed_output_file.write(frame_bytes) + + if current_frame > end: + break + + except error.InValidFrameHeaderSizeError: + break + + trimmed_mp3.total_frame = current_frame + + mp3_file.close() + trimmed_output_file.close() + + return trimmed_mp3 diff --git a/karriz/mp3/pyproject.toml b/karriz/mp3/pyproject.toml new file mode 100644 index 0000000..c0fcdf6 --- /dev/null +++ b/karriz/mp3/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "mp3" +version = "0.0.4" +description = "MP3 File Processing Package" +authors = ["karriz-dev "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/karriz/mp3/tests/__init__.py b/karriz/mp3/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/karriz/mp3/tests/test_reader.py b/karriz/mp3/tests/test_reader.py new file mode 100644 index 0000000..614a58d --- /dev/null +++ b/karriz/mp3/tests/test_reader.py @@ -0,0 +1,33 @@ +import unittest +import mp3 + +class MP3FileReaderTests(unittest.TestCase): + def test_read_mp3_file(self): + reader = mp3.MP3FileReader("./assets/input.mp3") + + mp3_file = reader.read() + + # 읽은 파일 경로 확인 + self.assertEqual(mp3_file.file_path, "./assets/input.mp3") + + # 재생 시간 3분 54초를 기준으로 계산 + self.assertEqual(int(mp3_file.total_duration), 234) + +class MP3FileTrimmerTests(unittest.TestCase): + def setUp(self): + reader = mp3.MP3FileReader("./assets/input.mp3") + + self.mp3_file = reader.read() + + def test_trim_mp3_file(self): + mp3_file_trimmer = mp3.MP3FileTrimmer(self.mp3_file) + + # mp3 파일을 프레임 단위로 trimming 후 경로로 저장 + trimmed_mp3 = mp3_file_trimmer.trim(0, 1000, "./assets/trimmed_input.mp3") + + # trimming 된 이후 총 duration이 1000 프레임 만큼 잘렸는지 확인 + # 1 프레임당 대략 0.026s = 1000 프레임은 26s + self.assertEqual(int(trimmed_mp3.total_duration), 26) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/karriz/mp3_file_reader/__init__.py b/karriz/mp3_file_reader/__init__.py deleted file mode 100644 index 2361d09..0000000 --- a/karriz/mp3_file_reader/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from .parser import MP3FileParser -from .trimmer import MP3FileTrimmer -from .enum import MP3Version, MP3Layer, MP3Protection, MP3PaddingBit, MP3ChannelMode ,MP3Copyright ,MP3Original, MP3Emphasis -from .error import FileStreamNotLoadedError, InvalidFrameSyncError, VersionNotMatchedError, LayerNotMatchedError - -__all__ = [ - 'MP3FileParser', - 'MP3Version', # MP3 버전 - 'MP3Layer', # MP3 레이어 - 'MP3Protection', # MP3 보호 - 'MP3PaddingBit', # 패딩 비트 - 'MP3ChannelMode', # 채널 모드 - 'MP3Copyright', # 저작권 - 'MP3Original', # 오리지널 여부 - 'MP3Emphasis', # 강조 - 'FileStreamNotLoadedError', # 파일 스트림 오류 - 'InvalidFrameSyncError', # 프레임 동기화 오류 - 'VersionNotMatchedError', # 버전 불일치 오류 - 'LayerNotMatchedError' # 레이어 불일치 오류 -] \ No newline at end of file diff --git a/karriz/mp3_file_reader/parser.py b/karriz/mp3_file_reader/parser.py deleted file mode 100644 index 4327e6d..0000000 --- a/karriz/mp3_file_reader/parser.py +++ /dev/null @@ -1,192 +0,0 @@ -from dataclasses import dataclass -from bitstring import ConstBitStream - -from .enum import MP3Version, MP3Layer, MP3Protection, MP3PaddingBit, MP3ChannelMode ,MP3Copyright ,MP3Original, MP3Emphasis -from .error import FileStreamNotLoadedError, InvalidFrameSyncError, VersionNotMatchedError, LayerNotMatchedError, BadBitrateError, InvalidFrequencyError - -FRAME_SYNC_BITS = 0b11111111111 -BAD_BITRATE = 0b1111 -RESERVED_FREQ = 0b11 - -BITRATE_TABLE = [ - [ - [0], # Reserved - [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2.5 Layer 3 - [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2.5 Layer 2 - [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256], # MPEG-2.5 Layer 1 - ], - [ - # Reserved - [0], - [0], - [0], - [0], - ], - [ - [0], # Reserved - [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2 Layer 3 - [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2 Layer 2 - [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256] # MPEG-2 Layer 1 - ], - [ - [0], # Reserved - [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320], # MPEG-1 Layer 3 - [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384], # MPEG-1 Layer 2 - [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448] # MPEG-1 Layer 1 - ] -] - -SAMPLE_RATE_TABLE = [ - [11025, 12000, 8000], # MPEG2.5 - [0, 0, 0], # Reserved - [22050, 24000, 16000], # MPEG2 - [44100, 48000, 32000], # MPEG1 -] - -@dataclass -class MP3FrameHeader: - version: MP3Version = MP3Version.MPEG_2_5 - layer: MP3Layer = MP3Layer.RESERVED - protection: MP3Protection = MP3Protection.PROTECTED_BY_CRC - bitrate: int = 0 - samplingRateFreq: int = 0 - paddingBit: MP3PaddingBit = MP3PaddingBit.NOT_PADDED - privateBit: int = 0 - channelMode: MP3ChannelMode = MP3ChannelMode.STEREO - modeExtension: int = 0 - copyright: MP3Copyright = MP3Copyright.NOT_COPYRIGHTED - original: MP3Original = MP3Original.COPY - emphasis: MP3Emphasis = MP3Emphasis.NONE - -@dataclass -class MP3File: - totalDuration: int = 0 - -class MP3FileParser: - def __init__(self, filepath:str): - self.mp3_file = None - self.mp3_file_stream = ConstBitStream(filename=filepath) - - def __checkSyncBits(self): - readFrame = self.mp3_file_stream.read(11).uint - - if readFrame != FRAME_SYNC_BITS: - raise InvalidFrameSyncError - - def __parseVersion(self) -> MP3Version: - return MP3Version(self.mp3_file_stream.read(2).uint) - - def __parseLayer(self) -> MP3Layer: - return MP3Layer(self.mp3_file_stream.read(2).uint) - - def __parseProtection(self) -> MP3Protection: - return MP3Protection(self.mp3_file_stream.read(1).uint) - - def __parseBitrate(self, version:int, layer:int) -> int: - readBitrate = self.mp3_file_stream.read(4).uint - - if readBitrate is BAD_BITRATE: - raise BadBitrateError - - return BITRATE_TABLE[version][layer][readBitrate] * 1000 - - def __parseSamplingRateFrequency(self, version:int) -> int: - readFreq = self.mp3_file_stream.read(2).uint - - if readFreq is RESERVED_FREQ: - raise InvalidFrequencyError - - return SAMPLE_RATE_TABLE[version][readFreq] - - def __parsePaddingBit(self) -> MP3PaddingBit: - return MP3PaddingBit(self.mp3_file_stream.read(1).uint) - - def __parsePrivateBit(self) -> int: - return self.mp3_file_stream.read(1).uint - - def __parseChannelMode(self) -> MP3ChannelMode: - return MP3ChannelMode(self.mp3_file_stream.read(2).uint) - - def __parseModeExtension(self) -> int: - return self.mp3_file_stream.read(2).uint - - def __parseCopyright(self) -> MP3Copyright: - return MP3Copyright(self.mp3_file_stream.read(1).uint) - - def __parseOriginal(self) -> MP3Original: - return MP3Original(self.mp3_file_stream.read(1).uint) - - def __parseEmphasis(self) -> MP3Emphasis: - return MP3Emphasis(self.mp3_file_stream.read(2).uint) - - def parse(self) -> MP3File: - if self.mp3_file_stream is None: - raise FileStreamNotLoadedError - - total_duration = 0 - - while self.mp3_file_stream.pos + 32 <= self.mp3_file_stream.len: - frameheader = MP3FrameHeader() - - # check frameSync is valid - self.__checkSyncBits() - - # Parse version: 2bits - frameheader.version = self.__parseVersion() - - if frameheader.version != MP3Version.MPEG_1: - raise VersionNotMatchedError - - # Parse layer: 2bits - frameheader.layer = self.__parseLayer() - - if frameheader.layer != MP3Layer.LAYER_III: - raise LayerNotMatchedError - - # Parse protection: 1bit - frameheader.protection = self.__parseProtection() - - # Parse bitrate: 4bits - frameheader.bitrate = self.__parseBitrate(frameheader.version.value, frameheader.layer.value) - - # Parse samplingRateFreq: 2bits - frameheader.samplingRateFreq = self.__parseSamplingRateFrequency(frameheader.version.value) - - # Parse paddingBit: 1bit - frameheader.paddingBit = self.__parsePaddingBit() - - # Read privateBit: 1bit - frameheader.privateBit = self.__parsePrivateBit() - - # Read channelMode: 2bits - frameheader.channelMode = self.__parseChannelMode() - - # Read modeExtension: 2bits - frameheader.modeExtension = self.__parseModeExtension() - - # Read copyright: 1bit - frameheader.copyright = self.__parseCopyright() - - # Read original: 1bit - frameheader.original = self.__parseOriginal() - - # Read emphasis: 2bits - frameheader.emphasis = self.__parseEmphasis() - - frame_length = 144 * frameheader.bitrate // frameheader.samplingRateFreq + frameheader.paddingBit.value - frame_duration = 1152 / frameheader.samplingRateFreq - - print(frame_length * 8) - print(frame_duration) - - self.mp3_file_stream.pos += frame_length * 8 - total_duration += frame_duration - - mp3_file = MP3File() - mp3_file.totalDuration = total_duration - - return mp3_file - - def play_time(self): - pass - diff --git a/karriz/mp3_file_reader/trimmer.py b/karriz/mp3_file_reader/trimmer.py deleted file mode 100644 index c7824b8..0000000 --- a/karriz/mp3_file_reader/trimmer.py +++ /dev/null @@ -1,10 +0,0 @@ -from .parser import MP3File - -class MP3FileTrimmer: - def __init__(self, mp3file:MP3File): - self.mp3_file = mp3file - - def trim(self, start:int, end:int) -> MP3File: - trimmed_mp3_file = self.mp3_file - - return trimmed_mp3_file diff --git a/karriz/poetry.lock b/karriz/poetry.lock new file mode 100644 index 0000000..af513e5 --- /dev/null +++ b/karriz/poetry.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +package = [] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "53f2eabc9c26446fbcc00d348c47878e118afc2054778c3c803a0a8028af27d9" diff --git a/karriz/pyproject.toml b/karriz/pyproject.toml new file mode 100644 index 0000000..a8705aa --- /dev/null +++ b/karriz/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "mp3-web-api" +version = "0.0.1" +description = "MP3 File Processing Package" +authors = ["karriz-dev "] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/karriz/tests/test_reader.py b/karriz/tests/test_reader.py deleted file mode 100644 index 2232c72..0000000 --- a/karriz/tests/test_reader.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest -import mp3_file_reader - -from mp3_file_reader import InvalidFrameSyncError - -class MP3FileParserTests(unittest.TestCase): - # [실패] mp3 file이 존재 하지 않는 경로로 읽는다 - def test_new_mp3file_not_found(self): - with self.assertRaises(FileNotFoundError): - self.mp3_file_reader = mp3_file_reader.MP3FileParser("./test.mp3") - - # [실패] file이 아닌 경로를 읽는다 - def test_new_mp3file_read_directory(self): - with self.assertRaises(IsADirectoryError): - self.mp3_file_reader = mp3_file_reader.MP3FileParser("./assets") - - # [성공] 정상경로에 있는 파일을 읽는다 - def test_new_mp3file_read(self): - self.mp3_file_reader = mp3_file_reader.MP3FileParser("./assets/input.mp3") - - self.assertIsNotNone(self.mp3_file_reader) - - # [실패] 빈 MP3 파일을 파싱한다. - def test_parse_empty_file(self): - with self.assertRaises(ValueError): - self.mp3_file_reader = mp3_file_reader.MP3FileParser("./assets/empty.mp3") - - self.mp3_file_reader.parse() - - # [실패] 잘못된 헤더 타입을 가진 파일을 파싱한다. - def test_parse_wrong_file(self): - with self.assertRaises(InvalidFrameSyncError): - self.mp3_file_reader = mp3_file_reader.MP3FileParser("./assets/wrong.mp3") - - self.mp3_file_reader.parse() - - # [성공] 정상 테스트 MP3File 파싱 - def test_parse_file(self): - mp3filereader = mp3_file_reader.MP3FileParser("./assets/input.mp3") - - mp3filereader.parse() - -class MP3FileTrimmerTests(unittest.TestCase): - # [실패] 잘못 된 파일로 트리밍 후 파일 저장 실패 - def test_trim_wrong_file(self): - pass - -if __name__ == '__main__': - unittest.main() \ No newline at end of file