audio-as-code 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. audio_as_code-0.1.0/.github/FUNDING.yml +1 -0
  2. audio_as_code-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +42 -0
  3. audio_as_code-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. audio_as_code-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  5. audio_as_code-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  6. audio_as_code-0.1.0/.github/dependabot.yml +29 -0
  7. audio_as_code-0.1.0/.github/scripts/check_distribution.py +272 -0
  8. audio_as_code-0.1.0/.github/scripts/check_schema.py +11 -0
  9. audio_as_code-0.1.0/.github/scripts/smoke_distribution.py +80 -0
  10. audio_as_code-0.1.0/.github/scripts/smoke_installed.py +112 -0
  11. audio_as_code-0.1.0/.github/workflows/ci.yml +71 -0
  12. audio_as_code-0.1.0/.github/workflows/publish.yml +141 -0
  13. audio_as_code-0.1.0/.gitignore +56 -0
  14. audio_as_code-0.1.0/AGENTS.md +8 -0
  15. audio_as_code-0.1.0/CHANGELOG.md +107 -0
  16. audio_as_code-0.1.0/CONTRIBUTING.md +135 -0
  17. audio_as_code-0.1.0/LICENSE +21 -0
  18. audio_as_code-0.1.0/PKG-INFO +340 -0
  19. audio_as_code-0.1.0/README.md +312 -0
  20. audio_as_code-0.1.0/SECURITY.md +36 -0
  21. audio_as_code-0.1.0/docs/agents.md +192 -0
  22. audio_as_code-0.1.0/docs/analytics.md +153 -0
  23. audio_as_code-0.1.0/docs/architecture.md +124 -0
  24. audio_as_code-0.1.0/docs/classic-reimaginations.md +91 -0
  25. audio_as_code-0.1.0/docs/classic-showcase.md +80 -0
  26. audio_as_code-0.1.0/docs/cloudflare-hosting.md +162 -0
  27. audio_as_code-0.1.0/docs/composition-tools.md +83 -0
  28. audio_as_code-0.1.0/docs/creative-workflows.md +119 -0
  29. audio_as_code-0.1.0/docs/expressive-engine.md +171 -0
  30. audio_as_code-0.1.0/docs/extended-instruments.md +181 -0
  31. audio_as_code-0.1.0/docs/full-compositions.md +75 -0
  32. audio_as_code-0.1.0/docs/instrument-foundation.md +95 -0
  33. audio_as_code-0.1.0/docs/music-demos.md +83 -0
  34. audio_as_code-0.1.0/docs/orchestra.md +105 -0
  35. audio_as_code-0.1.0/docs/piano-sustain.md +74 -0
  36. audio_as_code-0.1.0/docs/releasing.md +154 -0
  37. audio_as_code-0.1.0/docs/render-performance.md +87 -0
  38. audio_as_code-0.1.0/docs/score-inspection.md +122 -0
  39. audio_as_code-0.1.0/docs/site/agents.md +240 -0
  40. audio_as_code-0.1.0/docs/site/composition.md +359 -0
  41. audio_as_code-0.1.0/docs/site/examples/01_first_score.py +65 -0
  42. audio_as_code-0.1.0/docs/site/examples/02_motif_and_progression.py +104 -0
  43. audio_as_code-0.1.0/docs/site/examples/03_song_form.py +170 -0
  44. audio_as_code-0.1.0/docs/site/examples/04_expressive_controls.py +126 -0
  45. audio_as_code-0.1.0/docs/site/examples/05_agent_loop.py +117 -0
  46. audio_as_code-0.1.0/docs/site/examples/06_agent_handoff.py +94 -0
  47. audio_as_code-0.1.0/docs/site/examples/agent-score.json +86 -0
  48. audio_as_code-0.1.0/docs/site/integrations.md +189 -0
  49. audio_as_code-0.1.0/docs/site/llms.txt +80 -0
  50. audio_as_code-0.1.0/docs/site/quickstart.md +217 -0
  51. audio_as_code-0.1.0/docs/site/reference.md +240 -0
  52. audio_as_code-0.1.0/docs/synthesis.md +142 -0
  53. audio_as_code-0.1.0/docs/website-discovery.md +38 -0
  54. audio_as_code-0.1.0/docs/website.md +192 -0
  55. audio_as_code-0.1.0/examples/_site_discovery.py +138 -0
  56. audio_as_code-0.1.0/examples/benchmark_render.py +203 -0
  57. audio_as_code-0.1.0/examples/build_site.py +1500 -0
  58. audio_as_code-0.1.0/examples/classic_reimaginations.py +522 -0
  59. audio_as_code-0.1.0/examples/classic_showcase.py +305 -0
  60. audio_as_code-0.1.0/examples/composition_tools.py +44 -0
  61. audio_as_code-0.1.0/examples/creative_workflows.py +393 -0
  62. audio_as_code-0.1.0/examples/expressive-score.json +303 -0
  63. audio_as_code-0.1.0/examples/expressive_engine.py +130 -0
  64. audio_as_code-0.1.0/examples/famous_music.py +309 -0
  65. audio_as_code-0.1.0/examples/first_light.py +17 -0
  66. audio_as_code-0.1.0/examples/full_compositions.py +616 -0
  67. audio_as_code-0.1.0/examples/instrument_browser.py +347 -0
  68. audio_as_code-0.1.0/examples/music/README.md +30 -0
  69. audio_as_code-0.1.0/examples/music/build_classics_full.py +374 -0
  70. audio_as_code-0.1.0/examples/music/build_excerpts.py +292 -0
  71. audio_as_code-0.1.0/examples/music/classics-full.json +29757 -0
  72. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007-viola.ly +64 -0
  73. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007.ly +61 -0
  74. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_allemande.ly +52 -0
  75. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_courante.ly +63 -0
  76. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_gigue.ly +50 -0
  77. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_menuet_i.ly +20 -0
  78. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_menuet_ii.ly +20 -0
  79. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_prelude.ly +60 -0
  80. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-bwv1007_sarabande.ly +29 -0
  81. audio_as_code-0.1.0/examples/music/classics-sources/cello_prelude-header.ily +25 -0
  82. audio_as_code-0.1.0/examples/music/classics-sources/fur_elise-fur_elise.ly +203 -0
  83. audio_as_code-0.1.0/examples/music/classics-sources/greensleeves-greensleeves.ly +73 -0
  84. audio_as_code-0.1.0/examples/music/classics-sources/ode_to_joy-ode_to_joy.ly +98 -0
  85. audio_as_code-0.1.0/examples/music/classics-sources/turkish_march-KV331_3_RondoAllaTurca.ly +43 -0
  86. audio_as_code-0.1.0/examples/music/classics-sources/turkish_march-KV331_3_RondoAllaTurcaNotes.ly +321 -0
  87. audio_as_code-0.1.0/examples/music/excerpts.json +2637 -0
  88. audio_as_code-0.1.0/examples/physical_instruments.py +212 -0
  89. audio_as_code-0.1.0/examples/piano_sustain.py +87 -0
  90. audio_as_code-0.1.0/examples/prepare_cloudflare.py +147 -0
  91. audio_as_code-0.1.0/examples/serve_site.py +173 -0
  92. audio_as_code-0.1.0/examples/showcase.py +438 -0
  93. audio_as_code-0.1.0/pyproject.toml +79 -0
  94. audio_as_code-0.1.0/schemas/song-v1.schema.json +607 -0
  95. audio_as_code-0.1.0/skills/audio-as-code/SKILL.md +195 -0
  96. audio_as_code-0.1.0/src/audio_as_code/__init__.py +44 -0
  97. audio_as_code-0.1.0/src/audio_as_code/__main__.py +3 -0
  98. audio_as_code-0.1.0/src/audio_as_code/_audio.py +87 -0
  99. audio_as_code-0.1.0/src/audio_as_code/_export_rules.py +32 -0
  100. audio_as_code-0.1.0/src/audio_as_code/_orchestra_profiles.py +188 -0
  101. audio_as_code-0.1.0/src/audio_as_code/_paths.py +36 -0
  102. audio_as_code-0.1.0/src/audio_as_code/_voices.py +173 -0
  103. audio_as_code-0.1.0/src/audio_as_code/acoustics.py +126 -0
  104. audio_as_code-0.1.0/src/audio_as_code/automation.py +40 -0
  105. audio_as_code-0.1.0/src/audio_as_code/cli.py +171 -0
  106. audio_as_code-0.1.0/src/audio_as_code/demo.py +106 -0
  107. audio_as_code-0.1.0/src/audio_as_code/effects.py +85 -0
  108. audio_as_code-0.1.0/src/audio_as_code/extended.py +283 -0
  109. audio_as_code-0.1.0/src/audio_as_code/inspection.py +351 -0
  110. audio_as_code-0.1.0/src/audio_as_code/instruments.py +923 -0
  111. audio_as_code-0.1.0/src/audio_as_code/midi.py +192 -0
  112. audio_as_code-0.1.0/src/audio_as_code/model.py +292 -0
  113. audio_as_code-0.1.0/src/audio_as_code/orchestra.py +481 -0
  114. audio_as_code-0.1.0/src/audio_as_code/pattern.py +151 -0
  115. audio_as_code-0.1.0/src/audio_as_code/physical.py +189 -0
  116. audio_as_code-0.1.0/src/audio_as_code/py.typed +0 -0
  117. audio_as_code-0.1.0/src/audio_as_code/render.py +230 -0
  118. audio_as_code-0.1.0/tests/test_acoustics.py +68 -0
  119. audio_as_code-0.1.0/tests/test_agent_handoff.py +55 -0
  120. audio_as_code-0.1.0/tests/test_agent_loop.py +57 -0
  121. audio_as_code-0.1.0/tests/test_agent_readiness.py +154 -0
  122. audio_as_code-0.1.0/tests/test_classic_showcase.py +63 -0
  123. audio_as_code-0.1.0/tests/test_cli_dx.py +72 -0
  124. audio_as_code-0.1.0/tests/test_cloudflare_staging.py +99 -0
  125. audio_as_code-0.1.0/tests/test_core_export_regressions.py +96 -0
  126. audio_as_code-0.1.0/tests/test_creative_workflows.py +117 -0
  127. audio_as_code-0.1.0/tests/test_demo_performance.py +144 -0
  128. audio_as_code-0.1.0/tests/test_distribution_contract.py +254 -0
  129. audio_as_code-0.1.0/tests/test_expressive_engine.py +331 -0
  130. audio_as_code-0.1.0/tests/test_extended_instruments.py +249 -0
  131. audio_as_code-0.1.0/tests/test_finite_strikes.py +66 -0
  132. audio_as_code-0.1.0/tests/test_full_compositions.py +120 -0
  133. audio_as_code-0.1.0/tests/test_instrument_refinement.py +132 -0
  134. audio_as_code-0.1.0/tests/test_instruments.py +156 -0
  135. audio_as_code-0.1.0/tests/test_midi_cli.py +106 -0
  136. audio_as_code-0.1.0/tests/test_midi_warnings.py +84 -0
  137. audio_as_code-0.1.0/tests/test_music_demos.py +54 -0
  138. audio_as_code-0.1.0/tests/test_orchestra.py +239 -0
  139. audio_as_code-0.1.0/tests/test_output_preflight.py +42 -0
  140. audio_as_code-0.1.0/tests/test_pattern_composition.py +147 -0
  141. audio_as_code-0.1.0/tests/test_physical.py +146 -0
  142. audio_as_code-0.1.0/tests/test_piano_sustain.py +243 -0
  143. audio_as_code-0.1.0/tests/test_render.py +104 -0
  144. audio_as_code-0.1.0/tests/test_render_performance.py +31 -0
  145. audio_as_code-0.1.0/tests/test_score.py +105 -0
  146. audio_as_code-0.1.0/tests/test_score_inspection.py +255 -0
  147. audio_as_code-0.1.0/tests/test_security_regressions.py +60 -0
  148. audio_as_code-0.1.0/tests/test_serve_site.py +107 -0
  149. audio_as_code-0.1.0/tests/test_site_analytics.py +241 -0
  150. audio_as_code-0.1.0/tests/test_site_discovery.py +66 -0
  151. audio_as_code-0.1.0/tests/test_site_docs.py +68 -0
  152. audio_as_code-0.1.0/uv.lock +895 -0
  153. audio_as_code-0.1.0/web/cloudflare/media-handler.js +64 -0
  154. audio_as_code-0.1.0/web/cloudflare/media-handler.test.js +85 -0
  155. audio_as_code-0.1.0/web/cloudflare/package-lock.json +1590 -0
  156. audio_as_code-0.1.0/web/cloudflare/package.json +12 -0
  157. audio_as_code-0.1.0/web/cloudflare/worker.js +12 -0
  158. audio_as_code-0.1.0/web/instrument-browser.html +586 -0
  159. audio_as_code-0.1.0/web/site/404.html +11 -0
  160. audio_as_code-0.1.0/web/site/analytics-config.json +9 -0
  161. audio_as_code-0.1.0/web/site/assets/agent-handoff.js +89 -0
  162. audio_as_code-0.1.0/web/site/assets/analytics.js +169 -0
  163. audio_as_code-0.1.0/web/site/assets/fonts/Anybody-OFL.txt +93 -0
  164. audio_as_code-0.1.0/web/site/assets/fonts/Archivo-OFL.txt +93 -0
  165. audio_as_code-0.1.0/web/site/assets/fonts/FONTS.txt +5 -0
  166. audio_as_code-0.1.0/web/site/assets/fonts/JetBrainsMono-OFL.txt +93 -0
  167. audio_as_code-0.1.0/web/site/assets/fonts/anybody-latin.woff2 +0 -0
  168. audio_as_code-0.1.0/web/site/assets/fonts/archivo-latin.woff2 +0 -0
  169. audio_as_code-0.1.0/web/site/assets/fonts/jetbrains-mono-latin.woff2 +0 -0
  170. audio_as_code-0.1.0/web/site/assets/icon.svg +1 -0
  171. audio_as_code-0.1.0/web/site/assets/ring.svg +1 -0
  172. audio_as_code-0.1.0/web/site/assets/site.css +719 -0
  173. audio_as_code-0.1.0/web/site/assets/site.js +938 -0
  174. audio_as_code-0.1.0/web/site/assets/social.png +0 -0
  175. audio_as_code-0.1.0/web/site/assets/social.svg +18 -0
  176. audio_as_code-0.1.0/web/site/assets/torn.svg +1 -0
  177. audio_as_code-0.1.0/web/site/identity.json +8 -0
  178. audio_as_code-0.1.0/web/site/index.html +230 -0
  179. audio_as_code-0.1.0/web/site/layout.html +62 -0
  180. audio_as_code-0.1.0/web/site/snippets/loop.py +20 -0
  181. audio_as_code-0.1.0/web/site/source.html +30 -0
  182. audio_as_code-0.1.0/web/site/tools/verify_site.py +345 -0
@@ -0,0 +1 @@
1
+ ko_fi: joaothecarvalho
@@ -0,0 +1,42 @@
1
+ name: Bug report
2
+ description: Report a reproducible score, rendering, export, or CLI problem.
3
+ labels: [bug]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ This is an early procedural-audio prototype. Please use a minimal original
9
+ or public-domain score. Do not include credentials or private project files.
10
+ For a suspected vulnerability, read [SECURITY.md](https://github.com/joaoCarvalho1000/audio-as-code/blob/main/SECURITY.md) before sharing details.
11
+ - type: textarea
12
+ id: expected
13
+ attributes:
14
+ label: Expected and actual behavior
15
+ description: What did you expect, and what happened instead?
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: reproduction
20
+ attributes:
21
+ label: Smallest reproduction
22
+ description: Paste the command and a small JSON score or Python example.
23
+ render: text
24
+ validations:
25
+ required: true
26
+ - type: textarea
27
+ id: environment
28
+ attributes:
29
+ label: Environment
30
+ description: Include your OS, Python version, aac --version, and installation method.
31
+ placeholder: Source checkout or source ZIP; Python version; OS; package version.
32
+ validations:
33
+ required: true
34
+ - type: textarea
35
+ id: diagnostics
36
+ attributes:
37
+ label: Diagnostics and audio observations
38
+ description: |
39
+ Include the complete JSON error or relevant render/analyze report. For sound
40
+ problems, identify the instrument, pitch, tempo, and what you heard. State
41
+ whether you listened; signal measurements alone do not establish sound quality.
42
+ render: text
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Report a security vulnerability privately
4
+ url: https://github.com/joaoCarvalho1000/audio-as-code/security/advisories/new
5
+ about: Use the private reporting route instead of sharing exploit details in a public issue.
@@ -0,0 +1,28 @@
1
+ name: Feature request
2
+ description: Describe a composition workflow or API capability you need.
3
+ labels: [enhancement]
4
+ body:
5
+ - type: textarea
6
+ id: task
7
+ attributes:
8
+ label: What are you trying to make?
9
+ description: Describe the musical or integration task and what currently gets in the way.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: proposal
14
+ attributes:
15
+ label: Proposed behavior
16
+ description: A small score, command, or Python example helps make the request concrete.
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: alternatives
21
+ attributes:
22
+ label: Current workaround and constraints
23
+ description: Include compatibility needs, timing, instrument controls, or export requirements.
24
+ - type: markdown
25
+ attributes:
26
+ value: |
27
+ Instrument sources must be generated from code. Recorded samples, SoundFonts,
28
+ and measured impulse responses are outside this project's synthesis direction.
@@ -0,0 +1,18 @@
1
+ ## What changes?
2
+
3
+ Describe the concrete problem and resulting behavior. Include a small before/after
4
+ example when it helps someone review the change.
5
+
6
+ ## Validation
7
+
8
+ List the commands you ran and their results. Mention checks you could not run.
9
+ For audio changes, state what you listened to and keep numerical checks separate
10
+ from perceptual observations.
11
+
12
+ - [ ] Relevant tests, Ruff lint, and formatting pass.
13
+ - [ ] User-facing behavior and compatibility changes are documented.
14
+ - [ ] Score schema regenerated if the models changed.
15
+ - [ ] New instrument controls affect implemented synthesis and are tested.
16
+ - [ ] No generated output, credentials, or workstation state is included.
17
+
18
+ Mark checks that do not apply as such; see CONTRIBUTING.md for the full workflow.
@@ -0,0 +1,29 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: uv
4
+ directory: /
5
+ schedule:
6
+ interval: monthly
7
+ open-pull-requests-limit: 2
8
+ groups:
9
+ python-minor-patch:
10
+ patterns: ['*']
11
+ update-types: [minor, patch]
12
+ - package-ecosystem: npm
13
+ directory: /web/cloudflare
14
+ schedule:
15
+ interval: monthly
16
+ open-pull-requests-limit: 2
17
+ groups:
18
+ hosting-minor-patch:
19
+ patterns: ['*']
20
+ update-types: [minor, patch]
21
+ - package-ecosystem: github-actions
22
+ directory: /
23
+ schedule:
24
+ interval: monthly
25
+ open-pull-requests-limit: 2
26
+ groups:
27
+ actions-minor-patch:
28
+ patterns: ['*']
29
+ update-types: [minor, patch]
@@ -0,0 +1,272 @@
1
+ """Inspect release artifacts without importing or extracting their contents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import ast
7
+ import json
8
+ import re
9
+ import stat
10
+ import tarfile
11
+ import zipfile
12
+ from email.parser import BytesParser
13
+ from pathlib import Path, PurePosixPath
14
+
15
+ ROOT = Path(__file__).resolve().parents[2]
16
+ PRIVATE_PARTS = {
17
+ ".git",
18
+ ".env",
19
+ ".agents",
20
+ ".codex",
21
+ ".impeccable",
22
+ ".wrangler",
23
+ ".claude",
24
+ ".orca",
25
+ ".cursor",
26
+ ".idea",
27
+ ".vscode",
28
+ ".internal",
29
+ ".venv",
30
+ "node_modules",
31
+ "__pycache__",
32
+ ".pytest_cache",
33
+ ".ruff_cache",
34
+ ".mypy_cache",
35
+ ".hypothesis",
36
+ ".tox",
37
+ ".nox",
38
+ "playwright-report",
39
+ "test-results",
40
+ "htmlcov",
41
+ "%systemdrive%",
42
+ "output",
43
+ "dist",
44
+ "build",
45
+ }
46
+ PRIVATE_SUFFIXES = {
47
+ ".pyc",
48
+ ".pyo",
49
+ ".log",
50
+ ".key",
51
+ ".pem",
52
+ ".p12",
53
+ ".pfx",
54
+ ".prof",
55
+ ".wav",
56
+ ".mp3",
57
+ ".mid",
58
+ ".midi",
59
+ }
60
+ INTERNAL_DOCS = {
61
+ "design.md",
62
+ "product.md",
63
+ "plan.md",
64
+ "handoff.md",
65
+ "session.md",
66
+ "notes.md",
67
+ "progress.md",
68
+ "checkpoint.md",
69
+ "tasks.md",
70
+ "instrument-browser-brief.md",
71
+ "instrument-refinement.md",
72
+ }
73
+ SDIST_ROOTS = {
74
+ "src",
75
+ "tests",
76
+ "examples",
77
+ "docs",
78
+ "schemas",
79
+ "skills",
80
+ "web",
81
+ ".github",
82
+ "README.md",
83
+ "LICENSE",
84
+ "CONTRIBUTING.md",
85
+ "AGENTS.md",
86
+ "CHANGELOG.md",
87
+ "SECURITY.md",
88
+ ".gitignore",
89
+ "pyproject.toml",
90
+ "uv.lock",
91
+ "PKG-INFO",
92
+ }
93
+ REQUIRED_SOURCE = {
94
+ "src/audio_as_code/__init__.py",
95
+ "src/audio_as_code/py.typed",
96
+ "src/audio_as_code/extended.py",
97
+ "src/audio_as_code/_audio.py",
98
+ "src/audio_as_code/_voices.py",
99
+ "src/audio_as_code/_export_rules.py",
100
+ "src/audio_as_code/_orchestra_profiles.py",
101
+ "pyproject.toml",
102
+ "uv.lock",
103
+ "README.md",
104
+ "LICENSE",
105
+ "CONTRIBUTING.md",
106
+ "CHANGELOG.md",
107
+ "SECURITY.md",
108
+ "docs/architecture.md",
109
+ "docs/extended-instruments.md",
110
+ "docs/synthesis.md",
111
+ "docs/piano-sustain.md",
112
+ "docs/creative-workflows.md",
113
+ "docs/render-performance.md",
114
+ "docs/cloudflare-hosting.md",
115
+ "docs/releasing.md",
116
+ "schemas/song-v1.schema.json",
117
+ "examples/first_light.py",
118
+ "examples/classic_showcase.py",
119
+ "examples/piano_sustain.py",
120
+ "examples/creative_workflows.py",
121
+ "examples/benchmark_render.py",
122
+ "examples/prepare_cloudflare.py",
123
+ "examples/music/classics-full.json",
124
+ "skills/audio-as-code/SKILL.md",
125
+ ".github/workflows/ci.yml",
126
+ ".github/scripts/check_distribution.py",
127
+ ".github/scripts/smoke_installed.py",
128
+ "web/cloudflare/worker.js",
129
+ "web/cloudflare/media-handler.js",
130
+ "web/cloudflare/package.json",
131
+ "web/cloudflare/package-lock.json",
132
+ }
133
+
134
+
135
+ def private_path(name: str) -> bool:
136
+ """Reject private/generated paths at any nesting depth, including Windows spelling."""
137
+ normalized = name.replace("\\", "/")
138
+ parts = normalized.split("/")
139
+ if normalized.startswith("/") or ".." in parts or any(":" in part for part in parts):
140
+ return True
141
+ if normalized.lower().startswith("docs/internal/"):
142
+ return True
143
+ for part in parts:
144
+ lower = part.lower()
145
+ if lower in PRIVATE_PARTS or lower in INTERNAL_DOCS:
146
+ return True
147
+ if lower.startswith((".env", ".dev.vars", ".coverage", "credentials")):
148
+ return True
149
+ if lower.endswith(".egg-info") or PurePosixPath(lower).suffix in PRIVATE_SUFFIXES:
150
+ return True
151
+ return False
152
+
153
+
154
+ def source_version(root: Path = ROOT) -> str:
155
+ tree = ast.parse((root / "src/audio_as_code/__init__.py").read_text(encoding="utf-8"))
156
+ for statement in tree.body:
157
+ if isinstance(statement, ast.Assign) and any(
158
+ isinstance(target, ast.Name) and target.id == "__version__"
159
+ for target in statement.targets
160
+ ):
161
+ value = ast.literal_eval(statement.value)
162
+ if isinstance(value, str):
163
+ return value
164
+ raise ValueError("No literal __version__ in the package entry point")
165
+
166
+
167
+ def check_metadata(data: bytes, expected_version: str) -> None:
168
+ metadata = BytesParser().parsebytes(data)
169
+ if metadata["Name"] != "audio-as-code" or metadata["Version"] != expected_version:
170
+ raise ValueError("Distribution name/version disagree with the package's __version__")
171
+ if metadata["Requires-Python"] != ">=3.10":
172
+ raise ValueError("Unexpected Python requirement")
173
+ if (metadata["License-Expression"] or metadata["License"]) != "MIT":
174
+ raise ValueError("MIT license metadata is missing")
175
+ requirements = metadata.get_all("Requires-Dist", [])
176
+ names = {re.split(r"[<>=!~;\s\[]", item, maxsplit=1)[0].lower() for item in requirements}
177
+ if names != {"numpy", "pydantic", "mido"} or any("@" in item for item in requirements):
178
+ raise ValueError("Runtime dependencies changed or contain nonportable direct URLs")
179
+
180
+
181
+ def check_wheel(path: Path, expected_version: str) -> dict:
182
+ with zipfile.ZipFile(path) as archive:
183
+ names = archive.namelist()
184
+ if len(names) != len(set(names)):
185
+ raise ValueError("Duplicate wheel members")
186
+ info_root = f"audio_as_code-{expected_version}.dist-info"
187
+ for info in archive.infolist():
188
+ if private_path(info.filename) or PurePosixPath(info.filename).parts[0] not in {
189
+ "audio_as_code",
190
+ info_root,
191
+ }:
192
+ raise ValueError(f"Unexpected wheel member: {info.filename}")
193
+ if stat.S_ISLNK(info.external_attr >> 16):
194
+ raise ValueError(f"Wheel may not contain symlinks: {info.filename}")
195
+ required = {
196
+ f"audio_as_code/{name}"
197
+ for name in (
198
+ "__init__.py",
199
+ "__main__.py",
200
+ "cli.py",
201
+ "model.py",
202
+ "pattern.py",
203
+ "render.py",
204
+ "extended.py",
205
+ "_audio.py",
206
+ "_voices.py",
207
+ "_export_rules.py",
208
+ "_orchestra_profiles.py",
209
+ "py.typed",
210
+ )
211
+ }
212
+ required |= {f"{info_root}/METADATA", f"{info_root}/WHEEL", f"{info_root}/entry_points.txt"}
213
+ if missing := required - set(names):
214
+ raise ValueError(f"Missing wheel files: {sorted(missing)}")
215
+ if not any(
216
+ name.startswith(info_root + "/") and name.endswith("/LICENSE") for name in names
217
+ ):
218
+ raise ValueError("Wheel is missing the license file")
219
+ check_metadata(archive.read(f"{info_root}/METADATA"), expected_version)
220
+ entry_points = archive.read(f"{info_root}/entry_points.txt").decode()
221
+ if "aac = audio_as_code.cli:main" not in entry_points:
222
+ raise ValueError("Wheel is missing the aac console entry point")
223
+ return {"file": path.name, "files": len(names), "typed": True}
224
+
225
+
226
+ def check_sdist(path: Path, expected_version: str) -> dict:
227
+ with tarfile.open(path, "r:gz") as archive:
228
+ members = archive.getmembers()
229
+ prefix = f"audio_as_code-{expected_version}/"
230
+ names = set()
231
+ for member in members:
232
+ if member.isdir():
233
+ continue
234
+ if not member.isfile() or not member.name.startswith(prefix):
235
+ raise ValueError(f"Unexpected sdist member: {member.name}")
236
+ name = member.name[len(prefix) :]
237
+ if private_path(name) or PurePosixPath(name).parts[0] not in SDIST_ROOTS:
238
+ raise ValueError(f"Private/generated or unlisted sdist member: {name}")
239
+ if name in names:
240
+ raise ValueError(f"Duplicate sdist member: {name}")
241
+ names.add(name)
242
+ if missing := REQUIRED_SOURCE - names:
243
+ raise ValueError(f"Missing sdist files: {sorted(missing)}")
244
+ metadata = archive.extractfile(prefix + "PKG-INFO")
245
+ if metadata is None:
246
+ raise ValueError("Missing sdist metadata")
247
+ check_metadata(metadata.read(), expected_version)
248
+ return {"file": path.name, "files": len(names), "portable_source": True}
249
+
250
+
251
+ def main() -> None:
252
+ parser = argparse.ArgumentParser(description=__doc__)
253
+ parser.add_argument("directory", nargs="?", type=Path, default=Path("dist"))
254
+ args = parser.parse_args()
255
+ wheels = sorted(args.directory.glob("*.whl"))
256
+ sdists = sorted(args.directory.glob("*.tar.gz"))
257
+ if len(wheels) != 1 or len(sdists) != 1:
258
+ parser.error("Expected exactly one wheel and one sdist; use a fresh output directory")
259
+ version = source_version()
260
+ try:
261
+ result = {
262
+ "version": version,
263
+ "wheel": check_wheel(wheels[0], version),
264
+ "sdist": check_sdist(sdists[0], version),
265
+ }
266
+ except (ValueError, KeyError) as error:
267
+ parser.error(str(error))
268
+ print(json.dumps(result, indent=2))
269
+
270
+
271
+ if __name__ == "__main__":
272
+ main()
@@ -0,0 +1,11 @@
1
+ """Fail when the checked-in schema differs from the installed score model."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from audio_as_code import Song
7
+
8
+ path = Path(__file__).resolve().parents[2] / "schemas/song-v1.schema.json"
9
+ if json.loads(path.read_text(encoding="utf-8")) != Song.model_json_schema():
10
+ raise SystemExit("Schema drift: run uv run aac schema -o schemas/song-v1.schema.json")
11
+ print("The checked-in score schema matches Song.model_json_schema().")
@@ -0,0 +1,80 @@
1
+ """Rebuild the sdist and smoke-test both wheels in isolated installed environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import zipfile
12
+ from pathlib import Path
13
+
14
+ from check_distribution import check_wheel, source_version
15
+
16
+
17
+ def payload(path: Path) -> dict[str, bytes]:
18
+ with zipfile.ZipFile(path) as archive:
19
+ return {
20
+ name: archive.read(name)
21
+ for name in archive.namelist()
22
+ if name.startswith("audio_as_code/")
23
+ }
24
+
25
+
26
+ def main() -> None:
27
+ parser = argparse.ArgumentParser(description=__doc__)
28
+ parser.add_argument("directory", nargs="?", type=Path, default=Path("dist"))
29
+ args = parser.parse_args()
30
+ wheels = list(args.directory.glob("*.whl"))
31
+ sdists = list(args.directory.glob("*.tar.gz"))
32
+ if len(wheels) != 1 or len(sdists) != 1:
33
+ parser.error("Expected exactly one wheel and one sdist")
34
+ uv = shutil.which("uv")
35
+ if uv is None:
36
+ parser.error("Install uv to run the isolated distribution checks")
37
+ script = Path(__file__).with_name("smoke_installed.py").resolve()
38
+ with tempfile.TemporaryDirectory(prefix="aac-distribution-") as folder:
39
+ work = Path(folder)
40
+ subprocess.run(
41
+ [
42
+ uv,
43
+ "build",
44
+ str(sdists[0].resolve()),
45
+ "--wheel",
46
+ "--no-sources",
47
+ "--out-dir",
48
+ str(work / "rebuilt"),
49
+ "--python",
50
+ sys.executable,
51
+ ],
52
+ check=True,
53
+ cwd=work,
54
+ )
55
+ rebuilt = next((work / "rebuilt").glob("*.whl"))
56
+ check_wheel(rebuilt, source_version())
57
+ assert payload(wheels[0]) == payload(rebuilt), "Sdist rebuild changed the installed package"
58
+ for wheel in (wheels[0].resolve(), rebuilt):
59
+ subprocess.run(
60
+ [
61
+ uv,
62
+ "run",
63
+ "--isolated",
64
+ "--no-project",
65
+ "--python",
66
+ sys.executable,
67
+ "--with",
68
+ str(wheel),
69
+ "python",
70
+ "-I",
71
+ str(script),
72
+ ],
73
+ check=True,
74
+ cwd=work,
75
+ )
76
+ print(json.dumps({"sdist_rebuild": "passed", "isolated_wheel_installs": 2}))
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
@@ -0,0 +1,112 @@
1
+ """Exercise an installed artifact from a temporary working directory, never the checkout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ from importlib.metadata import distribution
10
+ from pathlib import Path
11
+
12
+ import audio_as_code
13
+ from audio_as_code import (
14
+ Note,
15
+ Pattern,
16
+ Song,
17
+ Track,
18
+ analyze_wav,
19
+ export_midi,
20
+ get_instrument,
21
+ inspect_score,
22
+ render,
23
+ )
24
+
25
+
26
+ def main() -> None:
27
+ repo = Path(__file__).resolve().parents[2]
28
+ package = Path(audio_as_code.__file__).resolve()
29
+ if package.is_relative_to(repo / "src"):
30
+ raise SystemExit("Smoke test imported the checkout; install the built wheel in isolation")
31
+ metadata = distribution("audio-as-code")
32
+ assert metadata.version == audio_as_code.__version__
33
+ assert (package.parent / "py.typed").is_file()
34
+ console = Path(sys.executable).with_name("aac.exe" if sys.platform == "win32" else "aac")
35
+ with tempfile.TemporaryDirectory(prefix="aac-installed-") as folder:
36
+ work = Path(folder)
37
+ for command in ([str(console)], [sys.executable, "-m", "audio_as_code"]):
38
+ version = subprocess.run(
39
+ [*command, "--version"], cwd=work, capture_output=True, text=True, check=True
40
+ )
41
+ assert json.loads(version.stdout) == {"version": metadata.version}
42
+ subprocess.run([*command, "--help"], cwd=work, capture_output=True, check=True)
43
+ phrase = Pattern.sequence(["C4"], step=0.25)
44
+ phrase = phrase.overlay(phrase.transpose(12), offset=0.25).stretch(2).scale_velocity(0.5)
45
+ song = Song(
46
+ title="Installed artifact smoke test",
47
+ bpm=120,
48
+ beats=1,
49
+ sample_rate=22050,
50
+ seed=7,
51
+ tracks=[Track(name="Lead", instrument="sine", notes=phrase.notes)],
52
+ )
53
+ score = work / "score.json"
54
+ song.save(score)
55
+ assert Song.load(score) == song
56
+ inspection = inspect_score(song)
57
+ assert inspection["summary"]["note_count"] == 2
58
+ assert inspection["readiness"]["render"]["ready"]
59
+ assert inspection["readiness"]["midi"]["ready"]
60
+ inspected = subprocess.run(
61
+ [str(console), "inspect", str(score)],
62
+ cwd=work,
63
+ capture_output=True,
64
+ text=True,
65
+ check=True,
66
+ )
67
+ assert json.loads(inspected.stdout) == inspection
68
+ report = render(song, work / "score.wav")
69
+ export_midi(song, work / "score.mid")
70
+ assert report["wav"] == analyze_wav(work / "score.wav")
71
+ assert not report["wav"]["silent"] and not report["wav"]["full_scale_samples"]
72
+ validation = subprocess.run(
73
+ [str(console), "validate", str(score)],
74
+ cwd=work,
75
+ capture_output=True,
76
+ text=True,
77
+ check=True,
78
+ )
79
+ assert json.loads(validation.stdout)["valid"]
80
+ for voice in ("mandolin", "kalimba", "celesta", "recorder"):
81
+ info = get_instrument(voice)
82
+ probe = Song(
83
+ bpm=120,
84
+ beats=1,
85
+ sample_rate=22050,
86
+ tracks=[
87
+ Track(
88
+ name=voice,
89
+ instrument=voice,
90
+ notes=[Note(pitch=info.preview_pitch, duration=1)],
91
+ )
92
+ ],
93
+ )
94
+ voice_report = render(probe, work / f"{voice}.wav")
95
+ assert not voice_report["wav"]["silent"]
96
+ assert not voice_report["wav"]["full_scale_samples"]
97
+ export_midi(probe, work / f"{voice}.mid")
98
+ invalid = work / "invalid.json"
99
+ invalid.write_text('{"tracks":[]}', encoding="utf-8")
100
+ failure = subprocess.run(
101
+ [str(console), "validate", str(invalid)],
102
+ cwd=work,
103
+ capture_output=True,
104
+ text=True,
105
+ )
106
+ assert failure.returncode == 2 and not failure.stdout
107
+ assert json.loads(failure.stderr)["error"] == "invalid_score"
108
+ print(json.dumps({"version": metadata.version, "installed_artifact_smoke": "passed"}))
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,71 @@
1
+ name: CI
2
+ on: [push, pull_request, workflow_dispatch]
3
+
4
+ permissions:
5
+ contents: read
6
+
7
+ concurrency:
8
+ group: ci-${{ github.workflow }}-${{ github.ref }}
9
+ cancel-in-progress: true
10
+
11
+ jobs:
12
+ hosting:
13
+ runs-on: ubuntu-latest
14
+ timeout-minutes: 5
15
+ steps:
16
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
17
+ with:
18
+ persist-credentials: false
19
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
20
+ with:
21
+ node-version: '24'
22
+ - run: npm ci
23
+ working-directory: web/cloudflare
24
+ - run: npm exec --no -- wrangler --version
25
+ working-directory: web/cloudflare
26
+ - run: node --test web/cloudflare/media-handler.test.js
27
+
28
+ test:
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ os: [ubuntu-latest, windows-latest]
33
+ python: ['3.10', '3.13']
34
+ include:
35
+ - os: macos-latest
36
+ python: '3.13'
37
+ runs-on: ${{ matrix.os }}
38
+ timeout-minutes: 20
39
+ steps:
40
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
41
+ with:
42
+ persist-credentials: false
43
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
44
+ with:
45
+ python-version: ${{ matrix.python }}
46
+ version: '0.12.15'
47
+ - run: uv sync --locked
48
+ - name: Schema stays in sync with the score model
49
+ run: uv run --locked python .github/scripts/check_schema.py
50
+ - run: uv run --locked pytest
51
+
52
+ distribution:
53
+ runs-on: ubuntu-latest
54
+ timeout-minutes: 15
55
+ steps:
56
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
57
+ with:
58
+ persist-credentials: false
59
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
60
+ with:
61
+ python-version: '3.13'
62
+ version: '0.12.15'
63
+ - run: uv sync --locked
64
+ - run: uv run --locked ruff check .
65
+ - run: uv run --locked ruff format --check .
66
+ - name: Build without local dependency source overrides
67
+ run: uv build --no-sources
68
+ - name: Inspect archive contents and metadata
69
+ run: uv run --locked python .github/scripts/check_distribution.py dist
70
+ - name: Install wheel and rebuilt sdist outside the checkout
71
+ run: uv run --locked python .github/scripts/smoke_distribution.py dist