danceflow-stemlab 1.4.1__tar.gz → 1.4.2__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.
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/AGENTS.md +6 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/PKG-INFO +13 -1
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/README.md +12 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/.codex-plugin/plugin.json +1 -1
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/README.md +6 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/plugin.json +1 -1
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/skills/stemlab/SKILL.md +9 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/PKG-INFO +13 -1
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/SOURCES.txt +3 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/__init__.py +1 -1
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/cli.py +2 -0
- danceflow_stemlab-1.4.2/src/stemlab/clip_cli.py +34 -0
- danceflow_stemlab-1.4.2/src/stemlab/clip_match.py +190 -0
- danceflow_stemlab-1.4.2/tests/test_clip_match.py +103 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/.agents/plugins/marketplace.json +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/.github/workflows/codex-plugin.yml +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/.github/workflows/release.yml +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/CONTRIBUTING.md +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/LICENSE +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/MANIFEST.in +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/docs/branding/README.md +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/docs/branding/logo-monochrome.svg +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/docs/branding/logo.png +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/docs/branding/logo.svg +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/docs/codex-plugin.md +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/docs/codex-publishing.md +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/docs/cue-alignment.md +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/docs/evidence-models.md +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/docs/midi.md +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/mcp-registry/server.json +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/.mcp.json +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/branding/logo-monochrome.svg +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/branding/logo.png +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/branding/logo.svg +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/mcp.json +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/skills/stemlab/references/tools.md +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/pyproject.toml +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/scripts/benchmark_evidence_corpus.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/scripts/build_codex_plugin.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/scripts/capture_loop_workflow.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/scripts/check_codex_timeline.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/scripts/loop_demo.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/scripts/prepare_codex_marketplace.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/scripts/publish_mcp_registry.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/scripts/smoke_midi.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/setup.cfg +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/dependency_links.txt +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/entry_points.txt +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/requires.txt +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/top_level.txt +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/__main__.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/__init__.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/evidence/__init__.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/evidence/adapters.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/evidence/registry.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/evidence/runner.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/evidence/worker.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/harmony.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/loops.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/lyrics.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/midi/__init__.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/midi/adapters.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/midi/artifacts.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/midi/registry.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/midi/runner.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/midi/worker.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/phrase_loops.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/registry.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/rhythm.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/rudiment_matches.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/runner.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/semantic_audio.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/semantic_text.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/song_map.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/sonic.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/structure.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/tempo_regimes.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/transcription.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/audio.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/beats/__init__.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/beats/base.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/beats/beatnet.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/beats/beatthis.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/beats/beattransformer.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/beats/common.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/bootstrap.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/branding.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/canonical.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/codex/__init__.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/codex/__main__.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/codex/bridge.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/codex/jobs.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/codex/paths.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/codex/server.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/codex/tasks.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/codex/timeline.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/codex/worker.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/cue_cli.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/cues.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/lyric_lrc.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/manifest.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/models.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/pipeline.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/separators/__init__.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/separators/base.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/separators/bsroformer.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/separators/common.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/separators/demucs.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/separators/openunmix.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/separators/scnet.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/server.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/sonic_visualiser.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/spectrogram.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/spectrogram_repair.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/speech.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/types.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/util.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/vamp.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/vamp_runtime.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/web/app.js +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/web/arcadians-cover.jpg +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/web/arcadians-reference.json +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/web/index.html +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/web/style.css +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/web/timeline.css +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/webui.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/word_alignment.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_analysis_registry.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_arcadians_fixture.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_arcadians_rudiments.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_audio_normalization.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_bar_phase.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_beat_consensus.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_beat_this_bootstrap.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_beatnet_bootstrap.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_beatnet_compat.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_branding.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_bsroformer_adapter.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_canonical.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_canonical_extended.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_codex_bridge.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_codex_jobs.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_codex_package.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_codex_protocol.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_codex_release.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_codex_timeline.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_cue_integration.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_cues.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_deep_harmony.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_deep_lyrics.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_deep_sonic_rhythm.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_distribution_identity.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_docker_contract.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_evidence_models.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_loop_integration.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_loops.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_lyric_lrc.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_manifest.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_midi_adapters.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_midi_extraction.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_midi_integration.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_midi_registry.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_models.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_phrase_loops.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_repository_hygiene.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_rudiment_matches.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_server.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_song_map.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_sonic_visualiser.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_sonic_visualiser_workspace.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_spectrogram.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_speech_repetition.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_vamp.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_vamp_runtime.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_web_frontend.py +0 -0
- {danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_word_alignment.py +0 -0
|
@@ -7,3 +7,9 @@
|
|
|
7
7
|
- Keep credentials, account data, private media and rights records out of source, fixtures, logs and packages. Use synthetic fixtures or explicitly authorised public examples and preserve their provenance/licences.
|
|
8
8
|
- Use [CONTRIBUTING.md](CONTRIBUTING.md) for review. Maintainers publish accepted, validated improvements as a fresh matching GitHub release and verify configured registry destinations. Preserve existing tags and release assets; publication and deployment remain subject to applicable authorisation.
|
|
9
9
|
- Website: https://kieransimkin.co.uk/danceflow/
|
|
10
|
+
|
|
11
|
+
## Keep README documentation current
|
|
12
|
+
|
|
13
|
+
- Whenever a feature is added or changed, update the relevant README.md in the same change to reflect the resulting behaviour. This applies to every feature addition or modification, including APIs, commands, configuration, defaults, integrations and removals.
|
|
14
|
+
- Update affected feature descriptions, usage instructions, options, examples and limitations. For tools or packages with their own README.md, update that README as well as the repository README when its overview or usage is affected.
|
|
15
|
+
- Check the README against the implemented and verified behaviour before committing, opening or merging a PR, publishing a release or handing off the work. A feature change is incomplete until its README documentation is current; a changelog or other documentation does not replace this requirement.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: danceflow-stemlab
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: Local music analysis for stems, timing, beats, harmony, lyrics, MIDI and loopable sections for Shorts. https://kieransimkin.co.uk/
|
|
5
5
|
Author: Kieran Simkin
|
|
6
6
|
Maintainer: Kieran Simkin
|
|
@@ -431,6 +431,18 @@ These filters suggest auditions and do not establish seamless or listening appro
|
|
|
431
431
|
|
|
432
432
|

|
|
433
433
|
|
|
434
|
+
## Recover a Short or video's source timing
|
|
435
|
+
|
|
436
|
+
`stemlab match-clip master.wav published-original.mp4 --output new-match.json`
|
|
437
|
+
locates a contiguous soundtrack excerpt without running models or downloading
|
|
438
|
+
anything. It retains both original hashes, timing precision, competing source
|
|
439
|
+
positions and independent window checks. Matched reports provide source offset,
|
|
440
|
+
soundtrack duration and player start for synchronised consumers. Short fades and
|
|
441
|
+
compression are tolerated; ambiguous repetition, edit jumps, different recordings
|
|
442
|
+
and rate changes remain unresolved. Use `--clip-start` / `--clip-end` for an
|
|
443
|
+
explicit known soundtrack interval. FFmpeg is required for video decoding.
|
|
444
|
+
See [clip matching](docs/clip-matching.md) for limitations and a synthetic example.
|
|
445
|
+
|
|
434
446
|
## Audio-to-MIDI model comparison
|
|
435
447
|
|
|
436
448
|
MIDI extraction is an **optional, independent model pass**. It adds Transkun V2
|
|
@@ -304,6 +304,18 @@ These filters suggest auditions and do not establish seamless or listening appro
|
|
|
304
304
|
|
|
305
305
|

|
|
306
306
|
|
|
307
|
+
## Recover a Short or video's source timing
|
|
308
|
+
|
|
309
|
+
`stemlab match-clip master.wav published-original.mp4 --output new-match.json`
|
|
310
|
+
locates a contiguous soundtrack excerpt without running models or downloading
|
|
311
|
+
anything. It retains both original hashes, timing precision, competing source
|
|
312
|
+
positions and independent window checks. Matched reports provide source offset,
|
|
313
|
+
soundtrack duration and player start for synchronised consumers. Short fades and
|
|
314
|
+
compression are tolerated; ambiguous repetition, edit jumps, different recordings
|
|
315
|
+
and rate changes remain unresolved. Use `--clip-start` / `--clip-end` for an
|
|
316
|
+
explicit known soundtrack interval. FFmpeg is required for video decoding.
|
|
317
|
+
See [clip matching](docs/clip-matching.md) for limitations and a synthetic example.
|
|
318
|
+
|
|
307
319
|
## Audio-to-MIDI model comparison
|
|
308
320
|
|
|
309
321
|
MIDI extraction is an **optional, independent model pass**. It adds Transkun V2
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/.codex-plugin/plugin.json
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stemlab",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Local music analysis for stems, timing, beats, harmony, lyrics, MIDI and loopable sections for Shorts. https://kieransimkin.co.uk/",
|
|
5
5
|
"skills": "./skills/",
|
|
6
6
|
"mcpServers": "./.mcp.json",
|
|
@@ -30,6 +30,12 @@ optional separation/speech/beat backends must already be installed for new infer
|
|
|
30
30
|
No model is downloaded at plugin installation. The existing `all` extra does not
|
|
31
31
|
include the opt-in MuQ or Basic Pitch routes.
|
|
32
32
|
|
|
33
|
+
The installed runtime also includes the model-free CLI
|
|
34
|
+
`stemlab match-clip SOURCE CLIP --output NEW.json` for hash-bound Short/video
|
|
35
|
+
soundtrack timing recovery. It runs locally and does not add a separate MCP tool
|
|
36
|
+
or change the timeline. Review competing matches and unresolved states before
|
|
37
|
+
using a result; see the repository's `docs/clip-matching.md`.
|
|
38
|
+
|
|
33
39
|
`configure` records only a workspace and resource limits in the platform's
|
|
34
40
|
user-configuration directory (`stemlab/codex.json`). It does not change Codex
|
|
35
41
|
settings or grant approvals. Use `--replace` only after reviewing an existing file.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "stemlab",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.2",
|
|
5
5
|
"description": "Local music analysis for stems, timing, beats, harmony, lyrics, MIDI and loopable sections for Shorts. https://kieransimkin.co.uk/",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Kieran Simkin",
|
|
@@ -141,4 +141,13 @@ readiness, and these outputs never rewrite canonical lyrics, sections or beat gr
|
|
|
141
141
|
|
|
142
142
|
## Improve StemLab
|
|
143
143
|
|
|
144
|
+
For existing video/Short soundtrack provenance, use the local CLI
|
|
145
|
+
`stemlab match-clip SOURCE CLIP --output NEW.json` before running learned models.
|
|
146
|
+
It preserves stereo phase, source/clip hashes and independent timing windows.
|
|
147
|
+
`--clip-start` / `--clip-end` select a known soundtrack interval. A matched report
|
|
148
|
+
provides offset, duration and player start; ambiguous repetition, edits or rate
|
|
149
|
+
changes remain unresolved. It does not grant track identity, artist approval or
|
|
150
|
+
publication rights. See `docs/clip-matching.md`; this CLI is separate from the
|
|
151
|
+
plugin's existing timeline and asynchronous analysis tools.
|
|
152
|
+
|
|
144
153
|
Inspect and use the available StemLab capabilities, and look for useful improvements wherever possible. Submit improvements with relevant tests, documentation and a reproducible example as PRs to https://github.com/kieransimkin/stemlab/pulls. Follow the repository's AGENTS.md and CONTRIBUTING.md.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: danceflow-stemlab
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: Local music analysis for stems, timing, beats, harmony, lyrics, MIDI and loopable sections for Shorts. https://kieransimkin.co.uk/
|
|
5
5
|
Author: Kieran Simkin
|
|
6
6
|
Maintainer: Kieran Simkin
|
|
@@ -431,6 +431,18 @@ These filters suggest auditions and do not establish seamless or listening appro
|
|
|
431
431
|
|
|
432
432
|

|
|
433
433
|
|
|
434
|
+
## Recover a Short or video's source timing
|
|
435
|
+
|
|
436
|
+
`stemlab match-clip master.wav published-original.mp4 --output new-match.json`
|
|
437
|
+
locates a contiguous soundtrack excerpt without running models or downloading
|
|
438
|
+
anything. It retains both original hashes, timing precision, competing source
|
|
439
|
+
positions and independent window checks. Matched reports provide source offset,
|
|
440
|
+
soundtrack duration and player start for synchronised consumers. Short fades and
|
|
441
|
+
compression are tolerated; ambiguous repetition, edit jumps, different recordings
|
|
442
|
+
and rate changes remain unresolved. Use `--clip-start` / `--clip-end` for an
|
|
443
|
+
explicit known soundtrack interval. FFmpeg is required for video decoding.
|
|
444
|
+
See [clip matching](docs/clip-matching.md) for limitations and a synthetic example.
|
|
445
|
+
|
|
434
446
|
## Audio-to-MIDI model comparison
|
|
435
447
|
|
|
436
448
|
MIDI extraction is an **optional, independent model pass**. It adds Transkun V2
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/SOURCES.txt
RENAMED
|
@@ -48,6 +48,8 @@ src/stemlab/bootstrap.py
|
|
|
48
48
|
src/stemlab/branding.py
|
|
49
49
|
src/stemlab/canonical.py
|
|
50
50
|
src/stemlab/cli.py
|
|
51
|
+
src/stemlab/clip_cli.py
|
|
52
|
+
src/stemlab/clip_match.py
|
|
51
53
|
src/stemlab/cue_cli.py
|
|
52
54
|
src/stemlab/cues.py
|
|
53
55
|
src/stemlab/lyric_lrc.py
|
|
@@ -133,6 +135,7 @@ tests/test_branding.py
|
|
|
133
135
|
tests/test_bsroformer_adapter.py
|
|
134
136
|
tests/test_canonical.py
|
|
135
137
|
tests/test_canonical_extended.py
|
|
138
|
+
tests/test_clip_match.py
|
|
136
139
|
tests/test_codex_bridge.py
|
|
137
140
|
tests/test_codex_jobs.py
|
|
138
141
|
tests/test_codex_package.py
|
|
@@ -14,6 +14,7 @@ from rich.table import Table
|
|
|
14
14
|
from .analysis.registry import ACTIONS
|
|
15
15
|
from .bootstrap import bootstrap as do_bootstrap
|
|
16
16
|
from .branding import AUTHOR_NAME, AUTHOR_SITE, MY_SONGS_URL, PROJECT_DESCRIPTION, REPOSITORY_URL
|
|
17
|
+
from .clip_cli import register_clip_commands
|
|
17
18
|
from .cue_cli import print_cue_review, register_cue_commands
|
|
18
19
|
from .models import MODEL_REGISTRY, profile as resolve_profile
|
|
19
20
|
from .pipeline import run_pipeline
|
|
@@ -23,6 +24,7 @@ from .util import device_string
|
|
|
23
24
|
app = typer.Typer(no_args_is_help=True, pretty_exceptions_show_locals=False, help=PROJECT_DESCRIPTION)
|
|
24
25
|
console = Console()
|
|
25
26
|
register_cue_commands(app, console)
|
|
27
|
+
register_clip_commands(app, console)
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
@app.command("repair-spectrograms")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_clip_commands(app, console):
|
|
10
|
+
@app.command("match-clip")
|
|
11
|
+
def match_clip_command(
|
|
12
|
+
source: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],
|
|
13
|
+
clip: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],
|
|
14
|
+
output: Annotated[Path, typer.Option("--output", "-o", help="New evidence JSON file")],
|
|
15
|
+
sample_rate: Annotated[int, typer.Option("--sample-rate", min=2000, max=24000)] = 8000,
|
|
16
|
+
clip_start: Annotated[float, typer.Option("--clip-start", min=0)] = 0.0,
|
|
17
|
+
clip_end: Annotated[float | None, typer.Option("--clip-end", min=0)] = None,
|
|
18
|
+
):
|
|
19
|
+
"""Locate a contiguous audio/video excerpt in its source; no models run."""
|
|
20
|
+
from .clip_match import match_clip
|
|
21
|
+
from .util import write_json
|
|
22
|
+
|
|
23
|
+
if output.exists() or output.resolve() in (source.resolve(), clip.resolve()):
|
|
24
|
+
raise typer.BadParameter("Output must be a new file, separate from the inputs")
|
|
25
|
+
try:
|
|
26
|
+
report = match_clip(source, clip, sample_rate=sample_rate,
|
|
27
|
+
clip_start=clip_start, clip_end=clip_end)
|
|
28
|
+
except (OSError, ValueError) as exc:
|
|
29
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
30
|
+
write_json(output, report)
|
|
31
|
+
console.print(f"{report['status']}: {report['reason']}")
|
|
32
|
+
console.print(str(output))
|
|
33
|
+
if report["status"] != "matched":
|
|
34
|
+
raise typer.Exit(2)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Find a contiguous recording excerpt on its source clock, without models.
|
|
2
|
+
|
|
3
|
+
Matching proves a measured waveform relationship, never public-use rights or
|
|
4
|
+
artist approval. AAC/MP3 encoding and fades are tolerated; ambiguous repetition,
|
|
5
|
+
edit jumps, silence, different performances and rate changes remain unresolved.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from scipy.signal import correlate, resample_poly
|
|
16
|
+
|
|
17
|
+
from .util import sha256_file
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _samples(value: np.ndarray) -> np.ndarray:
|
|
21
|
+
value = np.asarray(value, dtype=np.float64)
|
|
22
|
+
if value.ndim == 1:
|
|
23
|
+
value = value[:, None]
|
|
24
|
+
if value.ndim != 2 or not value.size or not np.all(np.isfinite(value)):
|
|
25
|
+
raise ValueError("Audio must contain finite frames with one or two channels")
|
|
26
|
+
if value.shape[1] not in (1, 2):
|
|
27
|
+
raise ValueError("Only mono and stereo inputs are supported")
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _correlation(source: np.ndarray, query: np.ndarray) -> np.ndarray:
|
|
32
|
+
"""Sliding, channel-preserving, zero-mean normalised correlation."""
|
|
33
|
+
if source.shape[1] != query.shape[1]:
|
|
34
|
+
if source.shape[1] == 1:
|
|
35
|
+
source = np.repeat(source, query.shape[1], axis=1)
|
|
36
|
+
else:
|
|
37
|
+
query = np.repeat(query, source.shape[1], axis=1)
|
|
38
|
+
n = len(query)
|
|
39
|
+
query = query - query.mean(axis=0)
|
|
40
|
+
energy = float(np.sum(query * query))
|
|
41
|
+
if energy <= 1e-12:
|
|
42
|
+
return np.zeros(len(source) - n + 1)
|
|
43
|
+
numerator = np.zeros(len(source) - n + 1)
|
|
44
|
+
variance = np.zeros_like(numerator)
|
|
45
|
+
for channel in range(source.shape[1]):
|
|
46
|
+
signal = source[:, channel]
|
|
47
|
+
numerator += correlate(signal, query[:, channel], mode="valid", method="fft")
|
|
48
|
+
total = np.concatenate(([0.0], np.cumsum(signal)))
|
|
49
|
+
square = np.concatenate(([0.0], np.cumsum(signal * signal)))
|
|
50
|
+
sums = total[n:] - total[:-n]
|
|
51
|
+
variance += square[n:] - square[:-n] - sums * sums / n
|
|
52
|
+
norm = np.sqrt(np.maximum(variance, 0) * energy)
|
|
53
|
+
return np.clip(np.divide(numerator, norm, out=np.zeros_like(norm), where=norm > 1e-12), -1, 1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def match_samples(source: np.ndarray, clip: np.ndarray, sample_rate: int, *,
|
|
57
|
+
min_correlation: float = 0.90, min_margin: float = 0.02,
|
|
58
|
+
tolerance_seconds: float = 0.002) -> dict:
|
|
59
|
+
"""Return conservative timing evidence for already decoded equal-rate PCM."""
|
|
60
|
+
source, clip = _samples(source), _samples(clip)
|
|
61
|
+
if not isinstance(sample_rate, int) or sample_rate < 1:
|
|
62
|
+
raise ValueError("sample_rate must be a positive integer")
|
|
63
|
+
if (not math.isfinite(min_correlation) or not 0 < min_correlation <= 1 or
|
|
64
|
+
not math.isfinite(min_margin) or not 0 <= min_margin <= 1 or
|
|
65
|
+
not math.isfinite(tolerance_seconds) or tolerance_seconds <= 0):
|
|
66
|
+
raise ValueError("Invalid matching thresholds")
|
|
67
|
+
report = {"status": "unresolved", "mapping": None, "candidates": [], "anchors": [],
|
|
68
|
+
"analysis_sample_rate": sample_rate, "precision_seconds": 1 / sample_rate,
|
|
69
|
+
"source_duration_seconds": len(source) / sample_rate,
|
|
70
|
+
"clip_duration_seconds": len(clip) / sample_rate,
|
|
71
|
+
"thresholds": {"min_correlation": min_correlation, "min_margin": min_margin,
|
|
72
|
+
"offset_tolerance_seconds": tolerance_seconds}}
|
|
73
|
+
if len(clip) < 2 * sample_rate or len(clip) > len(source):
|
|
74
|
+
return {**report, "reason": "Clip needs at least two seconds and must fit in the source"}
|
|
75
|
+
edge = min(round(0.25 * sample_rate), len(clip) // 10)
|
|
76
|
+
query = clip[edge:len(clip) - edge]
|
|
77
|
+
if np.std(query) < 1e-7:
|
|
78
|
+
return {**report, "reason": "Silent or constant clip has insufficient timing information"}
|
|
79
|
+
scores = _correlation(source, query)
|
|
80
|
+
remaining = scores.copy()
|
|
81
|
+
exclusion = max(1, round(0.5 * sample_rate))
|
|
82
|
+
for _ in range(min(5, len(scores))):
|
|
83
|
+
index = int(np.argmax(remaining))
|
|
84
|
+
if not np.isfinite(remaining[index]):
|
|
85
|
+
break
|
|
86
|
+
report["candidates"].append({"offset_samples": index - edge,
|
|
87
|
+
"offset_seconds": (index - edge) / sample_rate,
|
|
88
|
+
"correlation": float(scores[index])})
|
|
89
|
+
remaining[max(0, index - exclusion):index + exclusion + 1] = -np.inf
|
|
90
|
+
best = report["candidates"][0]
|
|
91
|
+
offset = best["offset_samples"]
|
|
92
|
+
second = report["candidates"][1]["correlation"] if len(report["candidates"]) > 1 else 0
|
|
93
|
+
report["correlation_margin"] = best["correlation"] - second
|
|
94
|
+
if best["correlation"] < min_correlation:
|
|
95
|
+
return {**report, "reason": "No strong contiguous waveform match"}
|
|
96
|
+
if offset < 0 or offset + len(clip) > len(source):
|
|
97
|
+
return {**report, "reason": "Matched interval extends outside the source; inspect clip range"}
|
|
98
|
+
anchor_frames = min(sample_rate, len(query) // 3)
|
|
99
|
+
starts = np.linspace(edge, len(clip) - edge - anchor_frames, 5).round().astype(int)
|
|
100
|
+
search_radius = max(1, round(0.05 * sample_rate))
|
|
101
|
+
for start in starts:
|
|
102
|
+
expected = offset + int(start)
|
|
103
|
+
left = max(0, expected - search_radius)
|
|
104
|
+
right = min(len(source), expected + anchor_frames + search_radius)
|
|
105
|
+
section = clip[start:start + anchor_frames]
|
|
106
|
+
if np.std(section) < 1e-7:
|
|
107
|
+
report["anchors"].append({"clip_time": start / sample_rate, "correlation": 0.0,
|
|
108
|
+
"informative": False, "offset_error_seconds": None})
|
|
109
|
+
continue
|
|
110
|
+
anchor_scores = _correlation(source[left:right], section)
|
|
111
|
+
index = int(np.argmax(anchor_scores))
|
|
112
|
+
actual_offset = left + index - int(start)
|
|
113
|
+
report["anchors"].append({"clip_time": start / sample_rate, "informative": True,
|
|
114
|
+
"correlation": float(anchor_scores[index]),
|
|
115
|
+
"offset_seconds": actual_offset / sample_rate,
|
|
116
|
+
"offset_error_seconds": (actual_offset - offset) / sample_rate})
|
|
117
|
+
reliable = [a for a in report["anchors"] if a["informative"]]
|
|
118
|
+
if len(reliable) < 3 or any(a["correlation"] < min_correlation or
|
|
119
|
+
abs(a["offset_error_seconds"]) > tolerance_seconds for a in reliable):
|
|
120
|
+
return {**report, "reason": "Independent windows disagree; possible edit, speed change or mismatch"}
|
|
121
|
+
if report["correlation_margin"] < min_margin:
|
|
122
|
+
return {**report, "reason": "Repeated audio gives ambiguous source positions"}
|
|
123
|
+
return {**report, "status": "matched", "reason": "Unique contiguous match with consistent independent windows",
|
|
124
|
+
"mapping": {"offset": offset / sample_rate, "duration": len(clip) / sample_rate,
|
|
125
|
+
"playerStart": 0.0}}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def decode_audio(path: Path, sample_rate: int = 8000, *, max_seconds: float = 1800) -> np.ndarray:
|
|
129
|
+
"""Decode a local audio/video file; retain stereo phase and bound memory/time."""
|
|
130
|
+
import soundfile as sf
|
|
131
|
+
|
|
132
|
+
path = Path(path)
|
|
133
|
+
if not path.is_file():
|
|
134
|
+
raise ValueError("Expected an existing local audio or video file")
|
|
135
|
+
if not isinstance(sample_rate, int) or not 2000 <= sample_rate <= 24000:
|
|
136
|
+
raise ValueError("Analysis rate must be an integer from 2000 to 24000 Hz")
|
|
137
|
+
if not math.isfinite(max_seconds) or not 2 <= max_seconds <= 7200:
|
|
138
|
+
raise ValueError("Decode limit must be between two seconds and two hours")
|
|
139
|
+
try:
|
|
140
|
+
info = sf.info(str(path))
|
|
141
|
+
except (sf.LibsndfileError, RuntimeError):
|
|
142
|
+
info = None
|
|
143
|
+
if info is not None:
|
|
144
|
+
if info.duration > max_seconds or info.channels not in (1, 2):
|
|
145
|
+
raise ValueError("Audio exceeds the decode limit or is not mono/stereo")
|
|
146
|
+
data, native_rate = sf.read(str(path), dtype="float32", always_2d=True)
|
|
147
|
+
if native_rate != sample_rate:
|
|
148
|
+
divisor = math.gcd(native_rate, sample_rate)
|
|
149
|
+
data = resample_poly(data, sample_rate // divisor, native_rate // divisor, axis=0)
|
|
150
|
+
return _samples(data)
|
|
151
|
+
ffmpeg = shutil.which("ffmpeg")
|
|
152
|
+
if ffmpeg is None:
|
|
153
|
+
raise ValueError("FFmpeg is required to decode this local video/audio format")
|
|
154
|
+
result = subprocess.run([ffmpeg, "-nostdin", "-v", "error", "-i", str(path),
|
|
155
|
+
"-map", "0:a:0", "-vn", "-t", str(max_seconds + 0.1),
|
|
156
|
+
"-ac", "2", "-ar", str(sample_rate), "-f", "f32le", "pipe:1"],
|
|
157
|
+
capture_output=True, timeout=120, check=True)
|
|
158
|
+
data = np.frombuffer(result.stdout, dtype="<f4")
|
|
159
|
+
if data.size % 2 or data.size > max_seconds * sample_rate * 2:
|
|
160
|
+
raise ValueError("Decoded audio is incomplete or exceeds the decode limit")
|
|
161
|
+
return _samples(data.reshape(-1, 2))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def match_clip(source_path: Path, clip_path: Path, *, sample_rate: int = 8000,
|
|
165
|
+
clip_start: float = 0.0, clip_end: float | None = None,
|
|
166
|
+
min_correlation: float = 0.90, min_margin: float = 0.02) -> dict:
|
|
167
|
+
"""Analyse immutable local files and return hash-bound source/player timing."""
|
|
168
|
+
source_path, clip_path = Path(source_path), Path(clip_path)
|
|
169
|
+
source_hash, clip_hash = sha256_file(source_path), sha256_file(clip_path)
|
|
170
|
+
source = decode_audio(source_path, sample_rate)
|
|
171
|
+
clip = decode_audio(clip_path, sample_rate)
|
|
172
|
+
if not math.isfinite(clip_start) or clip_start < 0:
|
|
173
|
+
raise ValueError("clip_start must be a finite non-negative time")
|
|
174
|
+
if clip_end is not None and (not math.isfinite(clip_end) or clip_end <= clip_start):
|
|
175
|
+
raise ValueError("clip_end must be finite and later than clip_start")
|
|
176
|
+
first = round(clip_start * sample_rate)
|
|
177
|
+
last = len(clip) if clip_end is None else round(clip_end * sample_rate)
|
|
178
|
+
if first >= len(clip) or last > len(clip):
|
|
179
|
+
raise ValueError("Selected clip interval must fit in the decoded audio")
|
|
180
|
+
report = match_samples(source, clip[first:last], sample_rate,
|
|
181
|
+
min_correlation=min_correlation, min_margin=min_margin)
|
|
182
|
+
if report["mapping"] is not None:
|
|
183
|
+
report["mapping"]["playerStart"] = first / sample_rate
|
|
184
|
+
from . import __version__
|
|
185
|
+
if source_hash != sha256_file(source_path) or clip_hash != sha256_file(clip_path):
|
|
186
|
+
raise ValueError("An input file changed during matching; discard this result")
|
|
187
|
+
return {"schema": "stemlab.clip-match.v1", "stemlab_version": __version__,
|
|
188
|
+
"source": {"path": str(source_path), "sha256": source_hash},
|
|
189
|
+
"clip": {"path": str(clip_path), "sha256": clip_hash},
|
|
190
|
+
"clip_range_seconds": [first / sample_rate, last / sample_rate], **report}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
import soundfile as sf
|
|
6
|
+
from scipy.signal import resample_poly
|
|
7
|
+
from typer.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
from stemlab.clip_match import decode_audio, match_clip, match_samples
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def recording(rate=4000, seconds=15):
|
|
13
|
+
rng = np.random.default_rng(4701)
|
|
14
|
+
return rng.normal(0, 0.1, (rate * seconds, 2))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_unique_gain_fade_noise_and_stereo_phase():
|
|
18
|
+
rate = 4000
|
|
19
|
+
source = recording(rate)
|
|
20
|
+
source[:, 1] = -source[:, 0]
|
|
21
|
+
offset = 21251
|
|
22
|
+
clip = source[offset:offset + 4 * rate].copy() * 0.5
|
|
23
|
+
clip[:rate // 4] *= np.linspace(0, 1, rate // 4)[:, None]
|
|
24
|
+
clip[-rate // 4:] *= np.linspace(1, 0, rate // 4)[:, None]
|
|
25
|
+
clip += np.random.default_rng(5).normal(0, 0.0001, clip.shape)
|
|
26
|
+
report = match_samples(source, clip, rate)
|
|
27
|
+
assert report['status'] == 'matched'
|
|
28
|
+
assert report['mapping']['offset'] == offset / rate
|
|
29
|
+
assert report['mapping']['duration'] == 4
|
|
30
|
+
assert len(report['anchors']) == 5
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_identical_repeated_passage_stays_ambiguous():
|
|
34
|
+
rate = 4000
|
|
35
|
+
source = recording(rate)
|
|
36
|
+
source[8 * rate:12 * rate] = source[2 * rate:6 * rate]
|
|
37
|
+
report = match_samples(source, source[2 * rate:6 * rate], rate)
|
|
38
|
+
assert report['status'] == 'unresolved'
|
|
39
|
+
assert report['mapping'] is None
|
|
40
|
+
assert 'ambiguous' in report['reason']
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize('kind', ['silence', 'constant', 'different', 'edit', 'rate'])
|
|
44
|
+
def test_invalid_clock_relationship_is_not_auto_mapped(kind):
|
|
45
|
+
rate = 4000
|
|
46
|
+
source = recording(rate)
|
|
47
|
+
clip = source[2 * rate:6 * rate].copy()
|
|
48
|
+
if kind == 'silence':
|
|
49
|
+
clip[:] = 0
|
|
50
|
+
elif kind == 'constant':
|
|
51
|
+
clip[:] = 0.1
|
|
52
|
+
elif kind == 'different':
|
|
53
|
+
clip = np.random.default_rng(3).normal(0, 0.1, clip.shape)
|
|
54
|
+
elif kind == 'edit':
|
|
55
|
+
clip[2 * rate:] = source[10 * rate:12 * rate]
|
|
56
|
+
else:
|
|
57
|
+
clip = resample_poly(clip, 101, 100, axis=0)
|
|
58
|
+
result = match_samples(source, clip, rate)
|
|
59
|
+
assert result['status'] == 'unresolved'
|
|
60
|
+
assert result['mapping'] is None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.parametrize('value', [float('nan'), float('inf'), -1, 0])
|
|
64
|
+
def test_invalid_thresholds(value):
|
|
65
|
+
with pytest.raises(ValueError):
|
|
66
|
+
match_samples(recording(), recording(seconds=3), 4000, min_correlation=value)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_local_hashes_range_and_rate_conversion(tmp_path):
|
|
70
|
+
rate = 8000
|
|
71
|
+
source = recording(rate)
|
|
72
|
+
source_path, clip_path = tmp_path / 'source.wav', tmp_path / 'clip.wav'
|
|
73
|
+
sf.write(source_path, source, rate, subtype='FLOAT')
|
|
74
|
+
# One second of unrelated material before the matching four-second excerpt.
|
|
75
|
+
clip = np.concatenate([recording(rate, 1), source[5 * rate:9 * rate]])
|
|
76
|
+
sf.write(clip_path, resample_poly(clip, 3, 2, axis=0), 12000, subtype='FLOAT')
|
|
77
|
+
result = match_clip(source_path, clip_path, clip_start=1, clip_end=5)
|
|
78
|
+
assert result['status'] == 'matched'
|
|
79
|
+
assert result['mapping'] == {'offset': 5.0, 'duration': 4.0, 'playerStart': 1.0}
|
|
80
|
+
assert len(result['source']['sha256']) == len(result['clip']['sha256']) == 64
|
|
81
|
+
assert result['schema'] == 'stemlab.clip-match.v1'
|
|
82
|
+
with pytest.raises(ValueError):
|
|
83
|
+
match_clip(source_path, clip_path, clip_end=7)
|
|
84
|
+
with pytest.raises(ValueError):
|
|
85
|
+
decode_audio(Path('https://example.invalid/source.mp4'))
|
|
86
|
+
with pytest.raises(ValueError):
|
|
87
|
+
decode_audio(source_path, max_seconds=2)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_cli_preserves_existing_inputs_and_reports_unresolved(tmp_path):
|
|
91
|
+
from stemlab.cli import app
|
|
92
|
+
|
|
93
|
+
source, clip, result = tmp_path / 'source.wav', tmp_path / 'clip.wav', tmp_path / 'match.json'
|
|
94
|
+
sf.write(source, recording(), 4000, subtype='FLOAT')
|
|
95
|
+
sf.write(clip, np.zeros((12000, 2)), 4000, subtype='FLOAT')
|
|
96
|
+
runner = CliRunner()
|
|
97
|
+
output = runner.invoke(app, ['match-clip', str(source), str(clip), '--output', str(result)])
|
|
98
|
+
assert output.exit_code == 2
|
|
99
|
+
assert result.is_file()
|
|
100
|
+
before = source.read_bytes()
|
|
101
|
+
output = runner.invoke(app, ['match-clip', str(source), str(clip), '--output', str(source)])
|
|
102
|
+
assert output.exit_code == 2
|
|
103
|
+
assert source.read_bytes() == before
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/plugins/stemlab/branding/logo-monochrome.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/requires.txt
RENAMED
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/danceflow_stemlab.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/evidence/__init__.py
RENAMED
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/evidence/adapters.py
RENAMED
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/evidence/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/analysis/rudiment_matches.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/src/stemlab/web/arcadians-reference.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{danceflow_stemlab-1.4.1 → danceflow_stemlab-1.4.2}/tests/test_sonic_visualiser_workspace.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|