Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f4cb73e
fix: zip mac
azfoo Mar 20, 2026
1f67f5a
ci: check release count
azfoo Mar 21, 2026
268d996
feat: one step less
azfoo Mar 23, 2026
3bcab2d
fix: version warning
azfoo Mar 23, 2026
f8978a8
fix: skip hanging tests
azfoo Mar 23, 2026
fc0c47c
fix: finding package
azfoo Mar 24, 2026
761e21d
fix: finding package
azfoo Mar 25, 2026
3748223
fix: failing tests
azfoo Mar 30, 2026
60e1629
refactor: typing
azfoo Mar 31, 2026
36f4fbb
chore: generalise
azfoo Mar 31, 2026
b0f8eac
chore: sort alphabetically
azfoo Mar 31, 2026
2069cf4
refactor: rename command to be more similar with the other commands
azfoo Mar 28, 2026
1f9a39e
feat: use default icons where available
azfoo Mar 26, 2026
4b16705
chore: remove unused icons
azfoo Mar 26, 2026
8557a47
refactor: use fewer fonts
azfoo Mar 27, 2026
d005c48
feat: dark mode icons
azfoo Mar 27, 2026
29a515c
fix: use white score background
azfoo Mar 27, 2026
10bf794
chore: more accurate naming
azfoo Mar 27, 2026
35396b1
refactor: library for theme independent icons
azfoo Mar 28, 2026
b6a474c
refactor: move existing icons into icon directory
azfoo Mar 28, 2026
c401c88
refactor: compress svgs
azfoo Mar 28, 2026
1dc1b21
ci: icons folder
azfoo Mar 28, 2026
8a64796
feat: vector logo
azfoo Mar 28, 2026
0cfd256
refactor: completely remove image dir
azfoo Mar 28, 2026
ba4a179
docs: icons
azfoo Mar 28, 2026
dc473fb
fix: catch when attributes are not filled in
azfoo Mar 31, 2026
3b5e74f
feat: use music21's converter to import and convert other score formats
azfoo Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ jobs:
echo "\n\nStarting CLI..."
echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }} --user-interface=cli" | bash || true

zip -r9 ${{ steps.build-exe.outputs.zip-filepath }} ${{ steps.build-exe.outputs.out-filepath }} || true
rm -rf ${{ steps.build-exe.outputs.out-filepath }}

- name: Test executable [Windows]
if: runner.os == 'Windows'
shell: bash
Expand Down Expand Up @@ -120,7 +123,6 @@ jobs:

deploy:
name: Create release
continue-on-error: true
needs: build
runs-on: "ubuntu-latest"
permissions:
Expand Down Expand Up @@ -163,9 +165,22 @@ jobs:

- name: Upload
uses: "softprops/action-gh-release@v2"
id: release
with:
tag_name: ${{github.ref_name}}
make_latest: ${{github.event_name == 'push'}}
generate_release_notes: true
files: |
build/*/exe/TiLiA-v*

- name: Check output
run: |
echo "Counting assets released..."
if [ -n "${{ fromJSON(steps.release.outputs.assets)[3].id }}" ] && [ -z "${{ fromJSON(steps.release.outputs.assets)[4].id }}" ];
then
echo "4 assets found!"
else
echo "::error::Incorrect number of assets! Potential errors in build."
echo assets: ${{ steps.release.outputs.assets }}
exit 1;
fi;
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ You need Python >=3.10 to build and test TiLiA. You will also need to have ffmpe
# Developing for TiLiA
For a better development experience, we recommend the installation of a few more packages:
```
pip install --group dev --group testing
pip install --group dev
pre-commit install
```
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://tilia-app.com/">
<img src="docs/img/logo.png" alt="drawing" width="100" >
<img src="tilia/ui/icons/hicolor/apps/256/tilia.svg" alt="drawing" width="100" >
</a>
</p>
TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured, easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PySide library for its GUI.
Expand All @@ -23,7 +23,7 @@ Here are some examples of TiLiA visualizations:
- 7 kinds of timelines
- AudioWave: visualize audio files through bars that represent changes in amplitude
- Beat: beat and measure markers with support for numbering
- Harmony: Roman numeral and chord symbol labels using a specialized font, including proper display of inversion numerals, quality symbols and applied chords
- Harmony: Roman numeral and letter symbol labels using a specialized font, including proper display of inversion numerals, quality symbols and applied chords
- Hierarchy: nested and levelled units organized in arbitrarily complex hierarchical structures
- Marker: simple, labelled markers to indicate discrete events
- PDF: visualize PDF files synced to playback
Expand Down
Binary file removed docs/img/logo.png
Binary file not shown.
File renamed without changes.
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ description = "A GUI for creating and visualizing annotations over audio and vid
readme = "README.md"
requires-python = ">=3.10,<3.14"
dependencies = [
"chardet<6.0.0",
"colorama~=0.4.6",
"httpx~=0.28.1",
"isodate~=0.7.2",
Expand Down Expand Up @@ -58,10 +59,12 @@ ci-tests = [
"coverage-badge>=1.0.0",
"ruff>=0.15.0",
"pytest-cov>=6.0.0",
"pytest-timeout",
]
dev = [
"icecream>=2.1.0",
"pre-commit",
{include-group = "testing"},
]
testing = [
"pytest>=8.0.0",
Expand Down Expand Up @@ -90,6 +93,7 @@ env = [
"ENVIRONMENT=test",
"QT_QPA_PLATFORM=offscreen",
]
timeout = 10

[tool.ruff]
exclude = ["tilia/dev"]
Expand All @@ -115,10 +119,12 @@ tilia = [
"../.tilia.env",
"../tilia.nuitka-package.config.yml", # because we build from sdist
"../LICENSE",
"ui/img/*",
"ui/fonts/*",
"media/player/youtube.html",
"media/player/youtube.css",
"parsers/score/svg_maker.html",
"parsers/score/timewise_to_partwise.xsl",
"ui/fonts/*",
"ui/icons/*/index.theme",
"ui/icons/*/actions/256/*",
"ui/icons/*/apps/256/*",
]
38 changes: 28 additions & 10 deletions scripts/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,21 @@ def _get_exe_cmd() -> list[str]:
name = options.get("project", {}).get("name", "TiLiA")
version = options.get("project", {}).get("version", "0")
_set_out_filename(name, version)
icon_path = Path(__file__).parents[1] / "tilia" / "ui" / "img" / "main_icon.ico"
icon_path = Path(__file__).parents[1] / "docs" / "img" / "main_icon.ico"
exe_args = [
sys.executable,
"-m",
"nuitka",
f"--output-dir={outdir}/exe",
f"--output-dir={(outdir / 'exe').as_posix()}",
f"--product-name={name}",
f"--file-version={version}",
f"--output-filename={out_filename}",
f"--macos-app-icon={icon_path}",
f"--macos-app-icon={icon_path.as_posix()}",
"--macos-app-mode=gui",
f"--macos-app-version={version}",
"--windows-console-mode=attach",
f"--windows-icon-from-ico={icon_path}",
f"--linux-icon={icon_path}",
f"--windows-icon-from-ico={icon_path.as_posix()}",
f"--linux-icon={icon_path.as_posix()}",
]

return exe_args
Expand Down Expand Up @@ -166,6 +166,23 @@ def _create_lib() -> Path:
return lib / tilia


def _get_implicit_imports():
from tilia.utils import get_sibling_packages

tls = [
tl + ".timeline"
for tl in get_sibling_packages(
"tilia.timelines.base.timeline",
(Path(__file__).parent.parent / "tilia/timelines/base/timeline").as_posix(),
)
]
tluis = get_sibling_packages(
"tilia.ui.timelines.base.timeline",
(Path(__file__).parent.parent / "tilia/ui/timelines/base/timeline").as_posix(),
)
return tls + tluis


def _update_yml():
if not Path(pkg_cfg).exists():
return
Expand All @@ -191,6 +208,7 @@ def _update_yml():
},
{"include-metadata": ["TiLiA"]},
],
"implicit-imports": [{"depends": _get_implicit_imports()}],
}
)

Expand Down Expand Up @@ -255,16 +273,16 @@ def build():
_build_exe()
if os.environ.get("GITHUB_OUTPUT"):
if "mac" in build_os:
os.rename(
outdir / "exe" / "tilia.app",
outdir / "exe" / (out_filename + ".app"),
)
out_filepath = outdir / "exe" / (out_filename + ".app")
out_filepath = outdir / "exe" / "tilia.app"
else:
out_filepath = outdir / "exe" / out_filename
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"out-filepath={out_filepath.as_posix()}\n")
f.write(f"out-filename={out_filename}\n")
if "mac" in build_os:
f.write(
f"zip-filepath={outdir.as_posix()}/exe/{out_filename}.zip\n"
)
os.chdir(old_dir)
dotenv.set_key(".tilia.env", "ENVIRONMENT", old_env_var)
except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion tests/parsers/csv/test_harmony_from_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def test_row_does_not_fail_if_missing_non_required_value(self, harmony_tl):
[
"time,harmony_or_key,symbol,display_mode",
"0,harmony,C",
"10,harmony,D,chord",
"10,harmony,D,letter",
]
)
success, errors = call_patched_import_by_time_func(harmony_tl, data)
Expand Down
4 changes: 2 additions & 2 deletions tests/parsers/score/test_score_from_musicxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from PySide6.QtWidgets import QMessageBox

from tilia.parsers.score.musicxml import notes_from_musicXML
from tilia.parsers.score.musicxml import score
from tilia.timelines.component_kinds import ComponentKind
from tilia.timelines.score.components import Clef
from tilia.timelines.score.timeline import ScoreTimeline
Expand All @@ -11,7 +11,7 @@
def _import_with_patch(score_tl, beat_tl, data, tmp_path):
tmp_file = tmp_path / "test.musicxml"
tmp_file.write_text(data)
errors = notes_from_musicXML(score_tl, beat_tl, str(tmp_file.resolve()))
errors = score(score_tl, beat_tl, str(tmp_file.resolve()))
return errors


Expand Down
4 changes: 2 additions & 2 deletions tests/ui/test_qtui.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_raises_error_if_invalid_csv(
post(Post.IMPORT_CSV, TimelineKind.MARKER_TIMELINE)

tilia_errors.assert_error()
tilia_errors.assert_in_error_title("import")
tilia_errors.assert_in_error_title("Import")
tilia_errors.assert_in_error_message("CSV")

def test_raises_error_if_invalid_musicXML(
Expand All @@ -70,7 +70,7 @@ def test_raises_error_if_invalid_musicXML(
post(Post.IMPORT_MUSICXML)

tilia_errors.assert_error()
tilia_errors.assert_in_error_title("import")
tilia_errors.assert_in_error_title("Import")
tilia_errors.assert_in_error_message("musicXML")


Expand Down
4 changes: 2 additions & 2 deletions tests/ui/timelines/harmony/test_harmony_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_paste_single_into_element(self, tlui):
"applied_to": 4,
"inversion": 1,
"comments": "some comments",
"display_mode": "chord",
"display_mode": "letter",
"custom_text": "some custom text",
}
_, copied_hui = tlui.create_harmony(0, **attributes_to_copy)
Expand Down Expand Up @@ -90,7 +90,7 @@ def test_paste_multiple_into_element(self, tlui):
"applied_to": 4,
"inversion": 1,
"comments": "some comments",
"display_mode": "chord",
"display_mode": "letter",
"custom_text": "some custom text",
}
for i in range(3):
Expand Down
10 changes: 5 additions & 5 deletions tests/ui/timelines/score/test_musicxml.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from tests.mock import patch_yes_or_no_dialog
from tests.constants import EXAMPLE_MUSICXML_PATH
from tilia.parsers.score.musicxml import notes_from_musicXML
from tilia.parsers.score.musicxml import score
from tilia.timelines.component_kinds import ComponentKind


Expand All @@ -17,7 +17,7 @@ def test_user_accepts(self, qtui, score_tlui, beat_tl, tmp_path, tilia_state):
self.setup_valid_beats(beat_tl)

with patch_yes_or_no_dialog(True):
notes_from_musicXML(score_tlui.timeline, beat_tl, EXAMPLE_MUSICXML_PATH)
score(score_tlui.timeline, beat_tl, EXAMPLE_MUSICXML_PATH)

clefs = score_tlui.timeline.get_components_by_attr("KIND", ComponentKind.CLEF)
assert len(clefs) == 1
Expand All @@ -39,7 +39,7 @@ def test_user_accepts_but_no_space_for_measure(
beat_tl.fill_with_beats(beat_tl.FillMethod.BY_AMOUNT, 10)

with patch_yes_or_no_dialog(True):
notes_from_musicXML(score_tlui.timeline, beat_tl, EXAMPLE_MUSICXML_PATH)
score(score_tlui.timeline, beat_tl, EXAMPLE_MUSICXML_PATH)

assert len(score_tlui) == 0

Expand All @@ -52,7 +52,7 @@ def test_user_accepts_but_less_than_two_measure_in_beat_timeline(
beat_tl.recalculate_measures()

with patch_yes_or_no_dialog(True):
notes_from_musicXML(score_tlui.timeline, beat_tl, EXAMPLE_MUSICXML_PATH)
score(score_tlui.timeline, beat_tl, EXAMPLE_MUSICXML_PATH)

assert len(score_tlui) == 0

Expand All @@ -61,7 +61,7 @@ def test_no_beat_1(self, qtui, score_tlui, beat_tl, tmp_path, tilia_state):

beat_tl.set_measure_number(0, 2)

notes_from_musicXML(score_tlui.timeline, beat_tl, EXAMPLE_MUSICXML_PATH)
score(score_tlui.timeline, beat_tl, EXAMPLE_MUSICXML_PATH)

# no prompt for measure 0 as there is no measure 1
# and measure 2 should have been imported
Expand Down
12 changes: 6 additions & 6 deletions tests/ui/timelines/score/test_score_timeline_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from tests.mock import Serve, patch_file_dialog, patch_yes_or_no_dialog
from tests.utils import reloadable, get_blank_file_data
from tilia.errors import SCORE_STAFF_ID_ERROR
from tilia.parsers.score.musicxml import notes_from_musicXML
from tilia.parsers.score.musicxml import score
from tilia.requests import Get, get
from tilia.timelines.component_kinds import ComponentKind
from tilia.timelines.score.components import Clef
Expand Down Expand Up @@ -84,7 +84,7 @@ def test_attribute_positions(qtui, score_tl, beat_tl, tmp_path):
beat_tl.measure_numbers = [0, 1, 2]
beat_tl.recalculate_measures()

notes_from_musicXML(score_tl, beat_tl, EXAMPLE_MULTISTAFF_MUSICXML_PATH)
score(score_tl, beat_tl, EXAMPLE_MULTISTAFF_MUSICXML_PATH)

_check_attrs(tmp_path, items_per_attr=3)

Expand All @@ -97,7 +97,7 @@ def test_attribute_positions_without_measure_zero(qtui, score_tl, beat_tl, tmp_p
beat_tl.recalculate_measures()

with patch_yes_or_no_dialog(False):
notes_from_musicXML(score_tl, beat_tl, EXAMPLE_MULTISTAFF_MUSICXML_PATH)
score(score_tl, beat_tl, EXAMPLE_MULTISTAFF_MUSICXML_PATH)

_check_attrs(tmp_path, items_per_attr=3)

Expand All @@ -110,7 +110,7 @@ def test_correct_clef_to_staff(qtui, score_tl, beat_tl):
beat_tl.recalculate_measures()

with patch_yes_or_no_dialog(False):
notes_from_musicXML(score_tl, beat_tl, EXAMPLE_MULTISTAFF_MUSICXML_PATH)
score(score_tl, beat_tl, EXAMPLE_MULTISTAFF_MUSICXML_PATH)

clefs = score_tl.get_components_by_attr("KIND", ComponentKind.CLEF)
staff_no_to_clef = {clef.staff_index: clef.icon for clef in clefs}
Expand Down Expand Up @@ -138,7 +138,7 @@ def test_missing_staff_deletes_timeline(qtui, tls, tilia_errors, tmp_path):
"line_number": -1,
"step": 4,
"octave": 4,
"icon": "clef-treble.svg",
"icon": "clef-treble",
"kind": "CLEF",
"hash": "",
},
Expand Down Expand Up @@ -221,7 +221,7 @@ def test_symbol_staff_collision(qtui, tmp_path):
"line_number": -1,
"step": 4,
"octave": 4,
"icon": "clef-treble.svg",
"icon": "clef-treble",
"kind": "CLEF",
"hash": "",
},
Expand Down
2 changes: 1 addition & 1 deletion tilia/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def _setup_commands(self):
)

commands.register(
"open_autosaves_folder",
"folder.open.autosaves",
tilia.dirs.open_autosaves_dir,
"Open autosa&ves folder...",
),
Expand Down
1 change: 0 additions & 1 deletion tilia/dirs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
_USER_DATA_DIR = Path(
platformdirs.user_data_dir(tilia.constants.APP_NAME, roaming=True)
)
IMG_DIR = Path(__file__).parent / "ui" / "img"


def setup_data_dir() -> Path:
Expand Down
8 changes: 4 additions & 4 deletions tilia/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ class Error(NamedTuple):
"Invalid metadata field",
"The following fields contain non-alphanumeric characters and will not be added:\n{}",
)
CSV_IMPORT_FAILED = Error("CSV import failed", "Import failed:\n{}")
CSV_IMPORT_SUCCESS_ERRORS = Error(
"CSV import",
IMPORT_FAILED = Error("Import failed", "Import failed:\n{}")
IMPORT_SUCCESS_ERRORS = Error(
"Import",
"Import was successful, but some components may not have been imported.\nThe following errors occurred:\n{}",
)
CREATE_TIMELINE_WITHOUT_MEDIA = Error(
Expand All @@ -47,7 +47,7 @@ class Error(NamedTuple):
EXPORT_AUDIO_FAILED = Error("Export Audio", "{}")
INVALID_HARMONY_INVERSION = Error(
"Invalid harmony inversion",
"Can't set inversion '{}' on a chord of type '{}'. Please select a valid inversion for this chord type.",
"Can't set inversion '{}' on a letter of type '{}'. Please select a valid inversion for this letter type.",
)
ADD_MODE_FAILED = Error("Add key failed", "Adding key failed: {}.")
ADD_HARMONY_FAILED = Error("Add harmony failed", "{}")
Expand Down
Loading