Commit a149914f authored by Matteo's avatar Matteo
Browse files

first commit

parents
[run]
source = mpai_cae_arp/
[report]
omit = */__init__.py
exclude_lines =
pragma: no cover
except OSError
def __repr__
def __str__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
\ No newline at end of file
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
\ No newline at end of file
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# latex
/docs/*.aux
/docs/*.log
/docs/*.out
/docs/*.toc
/docs/*.pdf
/docs/*.bbl
/docs/*.blg
/docs/*.dvi
/docs/*.fdb_latexmk
/docs/*.fls
/docs/*.synctex.gz
/docs/*.gz
/docs/*.mtc
/docs/*.mtc0
/docs/*.maf
/docs/*.run.xml
/docs/*.bcf
/docs/*.xml
/docs/*.xdv
# data folders and files
/data/
/data/*
# sphinx-gallery
/examples/.ipynb_checkpoints
/examples/.ipynb_checkpoints/*
/docs/source/auto_examples
**/gen_modules/
This diff is collapsed.
UNAME := $(shell uname)
POETRY := poetry run
DOCS := cd docs && $(POETRY) make clean
ifeq ($(UNAME), Linux)
OPEN = xdg-open
endif
ifeq ($(UNAME), Darwin)
OPEN = open
endif
ifeq ($(UNAME), Windows)
OPEN = start
endif
.PHONY: help clean docs
clean: clean-build clean-pyc clean-test
clean-build:
rm -fr build/
rm -fr dist/
rm -fr .eggs/
find . -name '*.egg-info' -exec rm -fr {} +
find . -name '*.egg' -exec rm -fr {} +
clean-pyc:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -fr {} +
clean-test:
rm -fr .tox/
rm -f .coverage
rm -fr htmlcov/
rm -fr .pytest_cache
install:
poetry install
test:
$(POETRY) pytest
cd docs && $(POETRY) make doctest
test-coverage:
$(POETRY) pytest --cov-config .coveragerc --cov-report term-missing --cov-report html --cov=mpai_cae_arp
format:
$(POETRY) yapf --in-place --recursive ./mpai_cae_arp ./tests
lint:
$(POETRY) pylint ./mpai_cae_arp ./tests
docs:
$(DOCS) && $(POETRY) make html && $(OPEN) build/html/index.html
# MPAI CAE-ARP API
[![LICENSE](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://img.shields.io/badge/license-GPLv3-blue.svg)
## Description
This package provides a set of tools for common task in MPAI CAE-ARP standard. It is usend in the official implementation of the standard and can be used as well to develop your own.
## License
This software is licensed under the GPLv3 license. See the [official site](http://www.gnu.org/licenses/gpl-3.0.html) for more information.
from enum import Enum
class EqualizationStandard(Enum):
IEC = "IEC"
CCIR = "IEC1"
NAB = "IEC2"
class SpeedStandard(Enum):
I = 0.9375
II = 1.875
III = 3.75
IV = 7.5
V = 15
VI = 30
import json
import yaml
def get_file_content(file_name: str, format: str) -> dict:
with open(file_name) as fd:
if format == "yaml":
content = yaml.safe_load(fd)
return content
elif format == "json":
content = json.load(fd)
else:
raise ValueError("Format not supported")
return content
import datetime
def seconds_to_string(seconds: float) -> str:
"""Converts seconds to a human readable time format (hh:mm:ss.msc).
Parameters
----------
seconds : float
The number of seconds to convert
Returns
-------
str
A human readable time format
"""
seconds = float(seconds)
# convert seconds in the format hh:mm:ss.msc
time = str(datetime.timedelta(seconds=seconds))
if seconds.is_integer():
time = time + ".000"
# convert days to hours
if "day" in time:
split_time = time.split(", ")
days = int(split_time[0].split(" ")[0])
hours = int(split_time[1].split(":")[0])
time = f'{days * 24 + hours}:{":".join(split_time[1].split(":")[1::])}'
time = ':'.join([x.zfill(2) for x in time.split(":")]) # add leading zeros
split_time = time.split(".")
time = split_time[0] + "." + split_time[1][:3] # msc precision is 3
return time
def time_to_seconds(time: str) -> float:
"""
Converts a time string in the format hh:mm:ss.msc in seconds.
Parameters
----------
time : str
The time string to convert
Raises
------
ValueError
If the time string is not in the correct format
Returns
-------
float
The number of seconds
"""
if not time.replace(":", "").replace(".", "").isdigit():
raise ValueError("The time string is not in the correct format")
split_time = time.split(":")
hours = int(split_time[0])
minutes = int(split_time[1])
seconds = float(split_time[2])
return hours * 3600 + minutes * 60 + seconds
def frames_to_seconds(frames: int, fps: int) -> float:
"""
Converts a number of frames in seconds.
Parameters
----------
frames : int
The number of frames
fps : int
The number of frames per second
Raises
------
ValueError
If the number of frames per second is less than or equal to 0 or if the number of frames is less than 0
Returns
-------
float
The number of seconds
"""
if fps <= 0:
raise ValueError(
"The number of frames per second must be greater than 0")
if frames < 0:
raise ValueError(
"The number of frames must be greater than or equal to 0")
return frames / fps
def seconds_to_frames(seconds: float, fps: int) -> int:
"""
Converts a number of seconds in frames.
Parameters
----------
seconds : float
The number of seconds
fps : int
The number of frames per second
Raises
------
ValueError
If the number of frames per second is less than or equal to 0 or if the number of seconds is less than 0
Returns
-------
int
The number of frames
"""
if fps <= 0:
raise ValueError(
"The number of frames per second must be greater than 0")
if seconds < 0:
raise ValueError(
"The number of seconds must be greater than or equal to 0")
return int(seconds * fps)
from pydantic import BaseModel, Field
class Contact(BaseModel):
name: str = Field(..., description="Name of the contact person.")
email: str = Field(..., description="Email of the contact person.")
class Config:
schema_extra = {
"example": {
"name": "John Doe",
"email": "email@email.com"
}
}
class License(BaseModel):
name: str = Field(..., description="Name of the license.")
url: str = Field(..., description="URL of the license.")
class Config:
schema_extra = {
"example": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
}
}
class Info(BaseModel):
title: str = Field(..., description="The title of the API.")
description: str = Field(..., description="A short description of the API.")
version: str = Field(..., description="The version of the API.")
contact: Contact = Field(
..., description="Contact information for the owners of the API.")
license_info: License = Field(
..., description="License information for the API.")
class Config:
schema_extra = {
"example": {
"title": "API title",
"description": "A very nice API",
"version": "0.1.0",
"contact": {
"name": "John Doe",
"email": "example@example.com",
},
"license_info": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
}
}
}
import uuid
from dataclasses import dataclass
from enum import Enum
from mpai_cae_arp.audio.standards import EqualizationStandard, SpeedStandard
class IrregularityType(Enum):
BRANDS_ON_TAPE = "b"
SPLICE = "sp"
START_OF_TAPE = "sot"
ENDS_OF_TAPE = "eot"
DAMAGED_TAPE = "da"
DIRT = "di"
MARKS = "m"
SHADOWS = "s"
WOW_AND_FLUTTER = "wf"
PLAY_PAUSE_STOP = "pps"
SPEED = "ssv"
EQUALIZATION = "esv"
SPEED_AND_EQUALIZATION = "ssv"
BACKWARD = "sb"
class Source(Enum):
AUDIO = "a"
VIDEO = "v"
BOTH = "b"
@dataclass
class IrregularityProperties:
reading_speed: SpeedStandard
reading_equalisation: EqualizationStandard
writing_speed: SpeedStandard
writing_equalisation: EqualizationStandard
@staticmethod
def from_classification(classification_result: ClassificationResult):
return IrregularityProperties(
reading_speed=classification_result.reading_speed,
reading_equalisation=classification_result.reading_equalization,
writing_speed=classification_result.writing_speed,
writing_equalisation=classification_result.writing_equalization,
)
@staticmethod
def from_json(json_property: dict):
return IrregularityProperties(
reading_speed=SpeedStandard(json_property["ReadingSpeedStandard"]),
reading_equalisation=EqualizationStandard(
json_property["ReadingEqualisationStandard"]),
writing_speed=SpeedStandard(json_property["WritingSpeedStandard"]),
writing_equalisation=EqualizationStandard(
json_property["WritingEqualisationStandard"]))
def to_json(self):
return {
"ReadingSpeedStandard": self.reading_speed.value,
"ReadingEqualisationStandard": self.reading_equalisation.value,
"WritingSpeedStandard": self.writing_speed.value,
"WritingEqualisationStandard": self.writing_equalisation.value,
}
@dataclass
class Irregularity:
irregularity_ID: uuid.UUID
source: Source
time_label: str
irregularity_type: IrregularityType = None
irregularity_properties: IrregularityProperties | None = None
image_URI: str | None = None
audio_block_URI: str | None = None
@staticmethod
def from_json(json_irreg: dict):
properties = None
if json_irreg.get("IrregularityProperties") is not None:
properties = IrregularityProperties.from_json(
json_irreg["IrregularityProperties"])
raw_irreg_type = json_irreg.get("IrregularityType")
irregularity_type = None
if raw_irreg_type is not None:
if raw_irreg_type is not (
IrregularityType.SPEED.value
or IrregularityType.SPEED_AND_EQUALIZATION.value):
irregularity_type = IrregularityType(raw_irreg_type)
else:
if properties.reading_equalisation != properties.writing_equalisation:
irregularity_type = IrregularityType.SPEED_AND_EQUALIZATION
else:
irregularity_type = IrregularityType.SPEED
return Irregularity(irregularity_ID=uuid.UUID(
json_irreg["IrregularityID"]),
source=Source(json_irreg["Source"]),
time_label=json_irreg["TimeLabel"],
irregularity_type=irregularity_type,
irregularity_properties=properties,
image_URI=json_irreg.get("ImageURI"),
audio_block_URI=json_irreg.get("AudioBlockURI"))
def to_json(self):
dictionary = {
"IrregularityID": str(self.irregularity_ID),
"Source": self.source.value,
"TimeLabel": self.time_label,
}
if self.irregularity_type:
dictionary["IrregularityType"] = self.irregularity_type.value
if self.image_URI:
dictionary["ImageURI"] = self.image_URI
if self.audio_block_URI:
dictionary["AudioBlockURI"] = self.audio_block_URI
if self.irregularity_properties:
dictionary[
"IrregularityProperties"] = self.irregularity_properties.to_json(
)
return dictionary
class IrregularityFile:
# TODO: the offset calculation is not implemented yet, so it is set to None
irregularities: list[Irregularity]
offset: int | None
def __init__(self,
irregularities: list[Irregularity] = [],
offset: int | None = None):
self.irregularities = irregularities
self.offset = offset
def __eq__(self, __o: object) -> bool:
if not isinstance(__o, IrregularityFile):
return False
return self.irregularities == __o.irregularities and self.offset == __o.offset
@staticmethod
def from_json(json_irreg: dict):
irregularities = []
for irreg in json_irreg["Irregularities"]:
irregularities.append(Irregularity.from_json(irreg))
return IrregularityFile(irregularities=irregularities,
offset=int(json_irreg["Offset"]))
def to_json(self):
dictionary = {
"Irregularities":
[irregularity.to_json() for irregularity in self.irregularities],
}
if self.offset:
dictionary["Offset"] = self.offset
return dictionary
def add(self, irregularity: Irregularity):
"""Add an irregularity to the list of irregularities.
Parameters
----------
irregularity : Irregularity
the irregularity to add
Raises
------
TypeError
if the irregularity is not a py:class:`Irregularity` object
"""
if not isinstance(irregularity, Irregularity):
raise TypeError(
"IrregularityFile.add() expects an Irregularity object")
self.irregularities.append(irregularity)
def join(self, other):
"""Append the irregularities of other in current irregularity file.
Parameters
----------
other : IrregularityFile
the irregularity file you want to append at the current one
Raises
------
TypeError
if other is not an instance of IrregularityFile
"""
if not isinstance(other, IrregularityFile):
raise TypeError("other must be an instance of IrregularityFile")
self.irregularities += other.irregularities
This diff is collapsed.
[tool.poetry]
name = "mpai-cae-arp"
version = "0.1.0"
description = ""
authors = ["Matteo <spartacus990@gmail.com>"]
readme = "README.md"
packages = [{include = "mpai_cae_arp"}]
[tool.poetry.dependencies]
python = "^3.10"
numpy = "1.23.3"
pydantic = "^1.10.7"
pyyaml = "^6.0"
[tool.poetry.group.dev.dependencies]
pylint = "^2.17.2"
yapf = "^0.32.0"
pytest = "^7.2.2"
pytest-cov = "^4.0.0"
pytest-xdist = "^3.2.1"
toml = "^0.10.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
minversion = "6.0"
addopts = "-ra -q -n auto -W error::RuntimeWarning"
testpaths = ["tests"]
[tool.coverage.run]
relative_files = true
[tool.yapf]
blank_line_before_nested_class_or_def = true
column_limit = 80
[tool.pylint]
max-line-length = 80
disable = [
"C0103", # Invalid name
"C0114", # Missing module docstring
"C0115", # Missing class docstring
"C0116", # Missing function or method docstring
"C0301", # Line too long
"W0102", # Dangerous default value
"E1101", # Module has no member
]
import os
import json
import tempfile
from mpai_cae_arp import files
def test_test():
assert 0 == 0
import pytest
from mpai_cae_arp import time
def test_seconds_to_time():
assert time.seconds_to_string(0) == "00:00:00.000"
assert time.seconds_to_string(123.456) == "00:02:03.456"
assert time.seconds_to_string(123456.789) == "34:17:36.789"
assert time.seconds_to_string(123) == "00:02:03.000"
def test_time_to_seconds():
assert time.time_to_seconds("00:00:00.000") == 0
assert time.time_to_seconds("00:02:03.456") == 123.456
assert time.time_to_seconds("34:17:36.789") == 123456.789
with pytest.raises(ValueError):
time.time_to_seconds("00:02:03.456.789")
with pytest.raises(ValueError):
time.time_to_seconds("days 1, 00:02:05.123")
def test_frames_to_seconds():
assert time.frames_to_seconds(0, 44100) == 0
assert time.frames_to_seconds(44100, 44100) == 1
assert time.frames_to_seconds(88200, 44100) == 2
assert time.frames_to_seconds(44100, 48000) == 0.91875
with pytest.raises(ValueError):
time.frames_to_seconds(0, 0)
with pytest.raises(ValueError):
time.frames_to_seconds(0, -1)
with pytest.raises(ValueError):
time.frames_to_seconds(-1, 44100)
def test_seconds_to_frames():
assert time.seconds_to_frames(0, 44100) == 0
assert time.seconds_to_frames(1, 44100) == 44100
assert time.seconds_to_frames(2, 44100) == 88200
assert time.seconds_to_frames(0.91875, 48000) == 44100
with pytest.raises(ValueError):
time.seconds_to_frames(0, 0)
with pytest.raises(ValueError):
time.seconds_to_frames(0, -1)
with pytest.raises(ValueError):
time.seconds_to_frames(-1, 44100)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment