beqforge 0.2.1__tar.gz → 0.2.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.
- {beqforge-0.2.1 → beqforge-0.2.2}/.github/workflows/build-device-check.yml +1 -1
- {beqforge-0.2.1 → beqforge-0.2.2}/.github/workflows/build-executable.yml +5 -3
- {beqforge-0.2.1 → beqforge-0.2.2}/.github/workflows/tests.yml +25 -5
- {beqforge-0.2.1 → beqforge-0.2.2}/AGENTS.md +3 -1
- {beqforge-0.2.1 → beqforge-0.2.2}/PKG-INFO +4 -3
- {beqforge-0.2.1 → beqforge-0.2.2}/README.md +3 -2
- {beqforge-0.2.1 → beqforge-0.2.2}/beq_common/__init__.py +1 -1
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/AGENTS.md +1 -1
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/diagnose.py +10 -4
- {beqforge-0.2.1 → beqforge-0.2.2}/plans/done-design-and-pipeline.md +70 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/pyproject.toml +18 -1
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_channel_evidence.py +3 -1
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_designer_server.py +81 -48
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_diagnose.py +30 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_designer_image.py +8 -11
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_designer_server_startup.py +17 -18
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/designer_server.py +268 -59
- {beqforge-0.2.1 → beqforge-0.2.2}/uv.lock +136 -76
- {beqforge-0.2.1 → beqforge-0.2.2}/.dockerignore +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.github/dependabot.yml +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.github/workflows/build-designer-image.yml +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.github/workflows/dependabot-auto-merge.yaml +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.gitignore +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.idea/.gitignore +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.idea/beqforge.iml +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.idea/editor.xml +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.idea/jsonSchemas.xml +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.idea/misc.xml +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.idea/modules.xml +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/.idea/vcs.xml +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/CLAUDE.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/LICENCE.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/TODO.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beq_common/AGENTS.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beq_common/biquad.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beq_common/provenance.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beq_common/publication.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beq_common/types.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/__init__.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/accept.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/beqd.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/biquad.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/cache.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/charts.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/cli.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/design.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/designer.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/explain.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/extraction.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/filters.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/harness.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/identify.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/material.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/pipeline.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/record.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/reference.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/rolloff.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge/verify.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge-device-check.spec +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge.spec +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/AGENTS.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/__init__.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/analyse.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/audio.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/catalogue.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/cli.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/coefficients.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/engines.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/evidence.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/manifest.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/measurement.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/profiles.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/qualification.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/report.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/transactions.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqforge_device_check/verify.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqoptimiser/AGENTS.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqoptimiser/__init__.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqoptimiser/cache.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqoptimiser/cli.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqoptimiser/core.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqoptimiser/data/seed-manifest.json +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/beqoptimiser/data/seed.json.gz +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/device-check-examples/simulation.json +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/device-check.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/optimiser-report/README.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/optimiser-report/assets/example-1.png +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/optimiser-report/assets/example-2.png +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/optimiser-report/entry-results.json.gz +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/optimiser-report/statistics.json +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/docs/optimiser.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/evidence_validation.json +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/negative_corpus.json +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/packaging/designer/Dockerfile +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/packaging/designer/smoke.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/packaging/device-check/NOTICE.txt +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/plans/F2-catalogue-filter-optimisation.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/plans/F2-device-precision-validation.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/plans/F2-package-consolidation.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/plans/R2a-server-stage-cache.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/plans/done-baseline-evidence.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/plans/research-design-decisions.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/plans/test-portability-and-runtime.md +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/__init__.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/conftest.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/fixtures/optimiser_seed_entry.json +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_cli.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_accept.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_cache.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_cache_stores.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_charts.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_corpus.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_design.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_designer.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_device_rate.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_evidence.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_evidence_excess.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_exclusions.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_explain.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_extraction.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_filters.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_frozen_cache.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_guard.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_harness.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_headroom.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_hum.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_identify.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_jacobian.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_parallel.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_pipeline.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_playback.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_playback_headroom.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_publication.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_record.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_record_fingerprint.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_reference.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_references.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_rolloff.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_strategy_config.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_timings.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_design_verify.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_analyse.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_catalogue.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_catalogue_predict.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_cli.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_engines.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_evidence.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_examples.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_manifest.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_measurement.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_qualification.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_transactions.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_device_check_verify.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_extract_layout.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_optimiser.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_optimiser_cache.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_optimiser_report.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tests/test_package_boundaries.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/__init__.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/build_optimiser_seed.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/design_beq.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/a1_sweep.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/compare_records.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/compare_verdicts.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/inject_variants.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p14_compare.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p14_jacobian.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p14_run_pipeline.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p14_surrogate.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p18_compare_greedy.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p18_compare_seeded.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p18_compare_structure.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p18_greedy.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p18_seeded.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p18_structure.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/p20_smooth_drift.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/probe.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/refit_probe.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/request_timing.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/score_injected.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/experiments/t8_floor_rules.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/extract.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/ledger_template.html +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/negative_corpus.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/optimiser_catalogue_report.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/prepare_device_check_helper.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/render_ledger.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/replay.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/seed_optimiser_cache.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/smoke_test_device_check.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/smoke_test_exe.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/smoke_test_install.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/summarise.py +0 -0
- {beqforge-0.2.1 → beqforge-0.2.2}/tools/validate_evidence.py +0 -0
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
steps:
|
|
26
26
|
- name: Find successful tests for the tagged commit
|
|
27
27
|
id: check
|
|
28
|
-
uses: actions/github-script@
|
|
28
|
+
uses: actions/github-script@v9
|
|
29
29
|
with:
|
|
30
30
|
script: |
|
|
31
31
|
core.setOutput('reusable', 'false');
|
|
@@ -72,6 +72,8 @@ jobs:
|
|
|
72
72
|
needs: previous_tests
|
|
73
73
|
if: needs.previous_tests.outputs.reusable != 'true'
|
|
74
74
|
uses: ./.github/workflows/tests.yml
|
|
75
|
+
# CODECOV_TOKEN, for the coverage upload
|
|
76
|
+
secrets: inherit
|
|
75
77
|
permissions:
|
|
76
78
|
contents: read
|
|
77
79
|
|
|
@@ -102,7 +104,7 @@ jobs:
|
|
|
102
104
|
contents: read
|
|
103
105
|
steps:
|
|
104
106
|
- uses: actions/checkout@v7
|
|
105
|
-
- uses: astral-sh/setup-uv@v10.
|
|
107
|
+
- uses: astral-sh/setup-uv@v10.3.0
|
|
106
108
|
with:
|
|
107
109
|
enable-cache: true
|
|
108
110
|
- name: Build wheel and source distribution
|
|
@@ -191,7 +193,7 @@ jobs:
|
|
|
191
193
|
- uses: actions/checkout@v7
|
|
192
194
|
|
|
193
195
|
- name: Install uv
|
|
194
|
-
uses: astral-sh/setup-uv@v10.
|
|
196
|
+
uses: astral-sh/setup-uv@v10.3.0
|
|
195
197
|
with:
|
|
196
198
|
enable-cache: true
|
|
197
199
|
|
|
@@ -67,7 +67,7 @@ jobs:
|
|
|
67
67
|
run: choco install ffmpeg -y
|
|
68
68
|
|
|
69
69
|
- name: Install uv
|
|
70
|
-
uses: astral-sh/setup-uv@v10.
|
|
70
|
+
uses: astral-sh/setup-uv@v10.3.0
|
|
71
71
|
with:
|
|
72
72
|
enable-cache: true
|
|
73
73
|
|
|
@@ -76,7 +76,17 @@ jobs:
|
|
|
76
76
|
|
|
77
77
|
# the device-check tests run in their own job below, in parallel, with the extra installed
|
|
78
78
|
- name: Run tests
|
|
79
|
-
run: uv run --extra designer pytest -n auto --durations=25 --ignore-glob='tests/test_device_check_*.py'
|
|
79
|
+
run: uv run --extra designer pytest -n auto --durations=25 --ignore-glob='tests/test_device_check_*.py' --cov --cov-report=xml
|
|
80
|
+
|
|
81
|
+
- name: Upload coverage
|
|
82
|
+
if: ${{ !cancelled() }}
|
|
83
|
+
uses: codecov/codecov-action@v7.1.1
|
|
84
|
+
with:
|
|
85
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
86
|
+
files: coverage.xml
|
|
87
|
+
disable_search: true
|
|
88
|
+
flags: designer,${{ runner.os }}
|
|
89
|
+
name: designer-${{ runner.os }}
|
|
80
90
|
|
|
81
91
|
device-check:
|
|
82
92
|
name: Device check (${{ matrix.os }})
|
|
@@ -90,7 +100,7 @@ jobs:
|
|
|
90
100
|
- uses: actions/checkout@v7
|
|
91
101
|
|
|
92
102
|
- name: Install uv
|
|
93
|
-
uses: astral-sh/setup-uv@v10.
|
|
103
|
+
uses: astral-sh/setup-uv@v10.3.0
|
|
94
104
|
with:
|
|
95
105
|
enable-cache: true
|
|
96
106
|
|
|
@@ -100,7 +110,17 @@ jobs:
|
|
|
100
110
|
|
|
101
111
|
- name: Run device-check tests
|
|
102
112
|
shell: bash
|
|
103
|
-
run: uv run --extra device-check pytest -n auto --durations=25 tests/test_device_check_*.py
|
|
113
|
+
run: uv run --extra device-check pytest -n auto --durations=25 tests/test_device_check_*.py --cov --cov-report=xml
|
|
114
|
+
|
|
115
|
+
- name: Upload coverage
|
|
116
|
+
if: ${{ !cancelled() }}
|
|
117
|
+
uses: codecov/codecov-action@v7.1.1
|
|
118
|
+
with:
|
|
119
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
120
|
+
files: coverage.xml
|
|
121
|
+
disable_search: true
|
|
122
|
+
flags: device-check,${{ runner.os }}
|
|
123
|
+
name: device-check-${{ runner.os }}
|
|
104
124
|
|
|
105
125
|
package-profiles:
|
|
106
126
|
name: Install profile (${{ matrix.profile }})
|
|
@@ -111,7 +131,7 @@ jobs:
|
|
|
111
131
|
profile: [base, designer, optimiser, device-check, all]
|
|
112
132
|
steps:
|
|
113
133
|
- uses: actions/checkout@v7
|
|
114
|
-
- uses: astral-sh/setup-uv@v10.
|
|
134
|
+
- uses: astral-sh/setup-uv@v10.3.0
|
|
115
135
|
with:
|
|
116
136
|
enable-cache: true
|
|
117
137
|
- name: Build the single distribution
|
|
@@ -56,7 +56,9 @@ importing them; use an isolated worktree for simultaneous experiments.
|
|
|
56
56
|
The root build configuration includes every workflow and the optimiser's bundled seed in one
|
|
57
57
|
wheel and source distribution. `.github/workflows/tests.yml` checks isolated dependency
|
|
58
58
|
profiles and runs the suite in parallel (`pytest -n auto`, so tests must not share mutable state
|
|
59
|
-
or fixed ports)
|
|
59
|
+
or fixed ports), with coverage (`--cov`, configured in `pyproject.toml`, `multiprocessing`
|
|
60
|
+
children included) uploaded to Codecov by each test job using the `CODECOV_TOKEN` secret;
|
|
61
|
+
release runs inherit it. Feature branch pushes test on Linux only and a newer push cancels the branch's
|
|
60
62
|
previous run; `main`, tags and manual runs test Linux, macOS and Windows and are never cancelled. `.github/workflows/build-executable.yml` builds release executables and distributions;
|
|
61
63
|
tag pushes publish to PyPI after validation and successful executable builds. Tags must match
|
|
62
64
|
the shared version exactly (`vX.Y.Z`). Manual runs build artifacts without publishing.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: beqforge
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Audio-derived BEQ design, published filter optimisation and DSP validation
|
|
5
5
|
License-File: LICENCE.md
|
|
6
6
|
Requires-Python: <3.15,>=3.13
|
|
@@ -270,8 +270,9 @@ The image's defaults, all overridable with the environment variables above:
|
|
|
270
270
|
|
|
271
271
|
It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
|
|
272
272
|
the shared root and the cache: the server checks both with a write at startup. Its
|
|
273
|
-
`HEALTHCHECK` asks `/health
|
|
274
|
-
caller sending several at once queues them; size the
|
|
273
|
+
`HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in a separate
|
|
274
|
+
worker process, one at a time, so a caller sending several at once queues them; size the
|
|
275
|
+
caller's timeout for that. `docker stop` stops a design in progress.
|
|
275
276
|
|
|
276
277
|
Build and check it locally from the repository root:
|
|
277
278
|
|
|
@@ -244,8 +244,9 @@ The image's defaults, all overridable with the environment variables above:
|
|
|
244
244
|
|
|
245
245
|
It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
|
|
246
246
|
the shared root and the cache: the server checks both with a write at startup. Its
|
|
247
|
-
`HEALTHCHECK` asks `/health
|
|
248
|
-
caller sending several at once queues them; size the
|
|
247
|
+
`HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in a separate
|
|
248
|
+
worker process, one at a time, so a caller sending several at once queues them; size the
|
|
249
|
+
caller's timeout for that. `docker stop` stops a design in progress.
|
|
249
250
|
|
|
250
251
|
Build and check it locally from the repository root:
|
|
251
252
|
|
|
@@ -43,7 +43,7 @@ still holds: no other API, no other service, and the server is a thin transport
|
|
|
43
43
|
| `beqforge/explain.py` | The plain-language account a designer response leads with — `found` (reference plateau, which channels carry it and which are silent, down to where there is real bass content, judged band, goal), `correction` (the cascade, then frequency by frequency what was wanted, what the content supports and before → after), `clipping` (the sub-feed peak and how far to turn the sub down, on the request's or the assumed bass management) and `alternatives` (why each other candidate lost). Reads the `Report`; measures and decides nothing. |
|
|
44
44
|
| `beqforge/designer.py` | The `design(request) -> response` adapter between beqdesigner's `designer-interface.md` v1.1 and `beqforge.pipeline.run` — request/response dataclasses (1.1: every candidate the judge failed goes back in `rejected` with its failures, review only, beside the answer or the decline), wire-format (de)serialisation and the field-by-field mapping onto `Report`/`Candidate`/`Verdict`. Also `validate_response`, an independent re-implementation of beqdesigner's own §3-§5 rules (that repo isn't importable — PyQt6), called by the server before a response ever goes on the wire. Pure and independently testable; no socket. |
|
|
45
45
|
| `tools/design_beq.py` | **The entry point.** One command, a filter and its reasoning. `--charts DIR` for the pictures; writes a `.run.json.gz` record beside the material unless `--no-record`. |
|
|
46
|
-
| `tools/designer_server.py` | `beqforge serve-designer` — the HTTP transport (`designer-interface.md` §7.1) around `beqforge/designer.py`. Server-wide flags for device realisation/strategies/exclusions; everything per-title comes from the request.
|
|
46
|
+
| `tools/designer_server.py` | `beqforge serve-designer` — the HTTP transport (`designer-interface.md` §7.1) around `beqforge/designer.py`. Server-wide flags for device realisation/strategies/exclusions; everything per-title comes from the request. A `ThreadingHTTPServer`, so `/health` answers during a design, with every design run in one long-lived worker process started with "spawn" (`_DesignWorker`): the fitter forks worker processes (`beqforge.filters.PARALLEL_FITS`) when a fit escalates past one section count, and forking a multi-threaded process risks a deadlock, so the process that forks must stay single-threaded — the worker's log records come back over a `SimpleQueue`, not a `Queue`, for that reason. One worker keeps designs serial (the contract is one synchronous POST per `design()` call); a worker that dies answers 500 and is replaced. SIGTERM shuts down like Ctrl-C, stopping the worker's process group, fit pool included. Every response names its build (`beqforge_revision`); `--record-dir DIR` also writes each request's run record, named by a digest of the request audio, so a review-queue entry can be replayed. `--cache-dir DIR` keeps the stage cache there (`cache.DirStore`, one file per entry, atomic): a repeat request for a title skips the analysis — 110 s cold, 50 s warm on a 2-hour 8-channel title. Each request logs a timing line (read, parse, decode, design, respond, queued); `tools/experiments/request_timing.py` reports them. `--shared-root DIR` accepts audio by reference (contract 1.2, beqdesigner's `design/designer-by-reference.md` §3): an array names a WAV relative to DIR plus the SHA-256 of its decoded column, and `beqforge/reference.py` resolves, decodes and checks it; one that cannot be honoured is a 422 naming the array, never a decline. `GET /health` reports `contract_version` and `shared_root`. |
|
|
47
47
|
| `tools/replay.py` | Redraw charts and export to beqdesigner from a record — no rerun, no extraction. Refuses on a stale record unless `--force`. |
|
|
48
48
|
| `tools/extract.py` | ffmpeg → 1 kHz per-channel `.npz`. Requires an explicit supported channel layout; preserves layout provenance. Needs no beqdesigner. |
|
|
49
49
|
| `tools/summarise.py` | Sanity-check an extraction before using it. |
|
|
@@ -618,15 +618,21 @@ def diagnose(
|
|
|
618
618
|
constrain channel restoration and the band over which a filter is judged.
|
|
619
619
|
"""
|
|
620
620
|
params = params or DiagnoseParams()
|
|
621
|
-
|
|
622
|
-
band = (
|
|
623
|
-
freqs, mix_db =
|
|
621
|
+
spectrum_freqs, mix_spectrum = mean_spectrum(material.mono_mix, material.fs)
|
|
622
|
+
band = (spectrum_freqs >= params.band_hz[0]) & (spectrum_freqs <= params.band_hz[1])
|
|
623
|
+
freqs, mix_db = spectrum_freqs[band], mix_spectrum[band]
|
|
624
624
|
spectra = {
|
|
625
625
|
name: mean_spectrum(samples, material.fs)[1]
|
|
626
626
|
for name, samples in material.channels.items()
|
|
627
627
|
}
|
|
628
628
|
shares, share_errors = coherent_shares(material)
|
|
629
|
-
|
|
629
|
+
# The mix plateau is found on the whole spectrum, exactly as `pipeline` finds it, so tracking
|
|
630
|
+
# is measured from the plateau the run judges against. The reference grid ends exactly at the
|
|
631
|
+
# band top: cut off at the last bin below it, a plateau reaching that edge loses its top point
|
|
632
|
+
# and can vanish. Diagnosis then lost the plateau `pipeline` had, measured no tracking at all
|
|
633
|
+
# and reported the band top as where tracking stopped (A Few Good Men: 150.5-200 Hz found by
|
|
634
|
+
# one, none by the other).
|
|
635
|
+
_, mix_plateau = plateau_reference(mix_spectrum, spectrum_freqs, params)
|
|
630
636
|
from beqforge.extraction import ExtractionParams, extract
|
|
631
637
|
|
|
632
638
|
extraction_params = extraction_params or ExtractionParams()
|
|
@@ -748,6 +748,76 @@ beside the answer or the decline (probe unchanged; no decision touched).
|
|
|
748
748
|
caller's; `beqforge_revision` is already in every response for it.
|
|
749
749
|
|
|
750
750
|
|
|
751
|
+
## The designer server answers `/health` during a design (17 October 2026)
|
|
752
|
+
|
|
753
|
+
A deployed image went unhealthy while it designed correctly. `serve-designer` was a plain
|
|
754
|
+
`HTTPServer` running `design()` on its only thread, so a `GET /health` waited behind a 50-110 s
|
|
755
|
+
design; the image's `HEALTHCHECK` gives up after 3 s and three failures mark the container
|
|
756
|
+
unhealthy. It had been single-threaded on purpose: the fitter forks its pool, and forking a
|
|
757
|
+
multi-threaded process risks a deadlock.
|
|
758
|
+
|
|
759
|
+
The server is now a `ThreadingHTTPServer`, and every design runs in one long-lived worker
|
|
760
|
+
process started with "spawn" (`_DesignWorker` in `tools/designer_server.py`). The worker's
|
|
761
|
+
only thread runs the design, so the fitter forks from a single-threaded process as before. Its
|
|
762
|
+
log records come back over a `SimpleQueue` (a `Queue` would start a feeder thread in it) and
|
|
763
|
+
go through the parent's loggers, and the parent waits for a request's records before
|
|
764
|
+
answering. One worker keeps designs serial. A worker that dies answers 500 and is replaced.
|
|
765
|
+
SIGTERM now shuts down as Ctrl-C does (as a container's PID 1 the server used to ignore it),
|
|
766
|
+
and closing stops the worker's process group, fit pool included, rather than wait for a
|
|
767
|
+
design in progress. The timing line gains `queued`. Nothing under `beqforge/` changed, so the
|
|
768
|
+
decision regression probe does not apply.
|
|
769
|
+
|
|
770
|
+
* **Checks.** New tests: `/health` answers within 3 s while a real design runs, and a killed
|
|
771
|
+
worker gives a 500 and then a working replacement. The existing server, image and startup
|
|
772
|
+
tests now build `DesignServer` with `DesignSettings` (51 passed). Against the installed
|
|
773
|
+
`beqforge serve-designer`: `/health` took at most 16 ms during a design, and SIGTERM in the
|
|
774
|
+
middle of one exited in 0.3 s with no worker or fit-pool process left. Before the
|
|
775
|
+
process-group fix, the fit pool was orphaned and kept computing. The image built and
|
|
776
|
+
`packaging/designer/smoke.py` passed, inline and by reference. Full designer suite with
|
|
777
|
+
coverage: 741 passed, 1 skipped. The fork-from-a-threaded-process warnings in it come from
|
|
778
|
+
xdist's own worker threads and appear without these changes.
|
|
779
|
+
* **Coverage.** CI now measures coverage (`pytest-cov`; `[tool.coverage.run]` in
|
|
780
|
+
`pyproject.toml`, with `concurrency = multiprocessing` so the design worker and fit pools
|
|
781
|
+
count on every platform; `patch = fork` is unsupported on Windows). Both test jobs upload
|
|
782
|
+
`coverage.xml` to Codecov with `CODECOV_TOKEN`, and the release workflow passes its secrets
|
|
783
|
+
to the tests it calls.
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
## The mix plateau diagnosis tracks from is the pipeline's (17 October 2026)
|
|
787
|
+
|
|
788
|
+
`diagnose` found the mix plateau on the spectrum cut off at the analysis band's top, while
|
|
789
|
+
`pipeline` finds it on the whole spectrum. `plateau_reference`'s grid ends exactly at the band
|
|
790
|
+
top with `right=nan`, so the cut-off copy always lost the top grid point, and a plateau reaching
|
|
791
|
+
that edge could vanish. Diagnosis then took `_temporal_evidence`'s early return (no strata, the
|
|
792
|
+
tracking floor set to the band top), and the pipeline's plateau blocker read that as "nothing
|
|
793
|
+
below it tracks the programme (tracking stops at 200.0 Hz)". The mix plateau is now found on
|
|
794
|
+
the whole spectrum, as the pipeline finds it. Channel plateaus are unchanged.
|
|
795
|
+
|
|
796
|
+
* **Found in the full-library run (beqdesigner).** A Few Good Men (5.1) declined
|
|
797
|
+
`no_usable_plateau`: the pipeline's plateau was 150.5-200 Hz, diagnosis had none, and its
|
|
798
|
+
record showed no strata and a tracking floor of exactly 200.0. `band_tracking` against the
|
|
799
|
+
pipeline's plateau passes every octave band to ~13 Hz (0.53-0.62). Patched, it accepts two
|
|
800
|
+
low shelves (16.8 Hz +5.8 dB, 26.8 Hz +6.5 dB, confidence 0.91). Before the Devil Knows
|
|
801
|
+
You're Dead and Blink Twice show the same record signature.
|
|
802
|
+
* **First version: every plateau on the whole spectrum, rejected.** Handing channels the whole
|
|
803
|
+
spectrum too moved channel plateaus on eight baseline titles (an extra finite top point moves
|
|
804
|
+
the 90th-percentile threshold), changed counterfactual targets on four, and in real runs
|
|
805
|
+
against HEAD flipped counterfactual verdicts on Alto Knights and Obsession. As with the
|
|
806
|
+
one-sided fallback above, the channel calls were left alone.
|
|
807
|
+
* **Validation of the mix-only version**, against 7a1702f. Probe: 28 Years Later's tracking
|
|
808
|
+
floor 20.50 -> 20.30 Hz (judged band and targets by up to 0.17 dB); Dossier 137's filter
|
|
809
|
+
floor 59.3 -> 79.8 Hz and Send Help's 20.0 -> 20.8 Hz, nothing else; the other eight titles
|
|
810
|
+
unchanged. 28 Years Later's real run against a HEAD run: the winner changes `parametric` ->
|
|
811
|
+
`flatten`, both passing either way; the accepted corrections agree within 0.6 dB in every band
|
|
812
|
+
from 5 to 80 Hz (+5.8/+6.0/+6.6/+3.3/+0.6 -> +5.9/+6.6/+6.4/+3.1/+0.7 dB), the same answer.
|
|
813
|
+
Protocol, seeds 101 and 947: identical false acceptances, abstentions, selections and recovery.
|
|
814
|
+
Corpus, seeds 1-9: gated 0/45, gate pass 0.079; positives 7/9 (median recovery 1.27 dB RMS);
|
|
815
|
+
`natural_droop` 4/9 against the committed file's 5/9 (an older revision; reported, not gated).
|
|
816
|
+
Full suite: 804 passed, 2 skipped. New test in `tests/test_design_diagnose.py`: a mix
|
|
817
|
+
high-passed at 144-146 Hz, whose only plateau reaches the band top, is tracked from that
|
|
818
|
+
plateau (fails on the old code: no strata, floor at 200 Hz).
|
|
819
|
+
|
|
820
|
+
|
|
751
821
|
## Levels beside an omission, and mains-hum notches as texture (11 October 2026)
|
|
752
822
|
|
|
753
823
|
Three titles from a beqdesigner run against `/media/beq` were declined in ways that should not
|
|
@@ -30,12 +30,29 @@ all = [
|
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[dependency-groups]
|
|
33
|
-
dev = [
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=9.0.2",
|
|
35
|
+
"pytest-cov>=7.1.0",
|
|
36
|
+
"pytest-xdist>=3.8",
|
|
37
|
+
"ruff>=0.14.10",
|
|
38
|
+
"scipy-stubs>=1.17.1,<1.19.0",
|
|
39
|
+
]
|
|
34
40
|
|
|
35
41
|
[tool.pytest.ini_options]
|
|
36
42
|
pythonpath = ["."]
|
|
37
43
|
testpaths = ["tests"]
|
|
38
44
|
|
|
45
|
+
[tool.coverage.run]
|
|
46
|
+
# the repository root, so every report path is the file's path in the repository
|
|
47
|
+
source = ["."]
|
|
48
|
+
omit = ["tests/*", ".venv/*", "build/*", "packaging/*"]
|
|
49
|
+
# the designer server's design worker and the fitter's pool are `multiprocessing` processes,
|
|
50
|
+
# measured too (and on every platform, unlike `patch = ["fork"]`); every process writes its
|
|
51
|
+
# own data file and they are combined at the end of the run
|
|
52
|
+
concurrency = ["multiprocessing", "thread"]
|
|
53
|
+
patch = ["subprocess"]
|
|
54
|
+
relative_files = true
|
|
55
|
+
|
|
39
56
|
[build-system]
|
|
40
57
|
requires = ["hatchling"]
|
|
41
58
|
build-backend = "hatchling.build"
|
|
@@ -52,7 +52,9 @@ def test_mix_floor_is_measured_on_mix_not_walled_lfe():
|
|
|
52
52
|
material = material_from({"L": source, "LFE": high_passed(source, 25, 10)})
|
|
53
53
|
result = diagnose(material)
|
|
54
54
|
params = DiagnoseParams()
|
|
55
|
-
|
|
55
|
+
# found on the whole mix spectrum, as diagnosis and the pipeline both find it
|
|
56
|
+
freqs, mix_db = mean_spectrum(material.mono_mix, 1000)
|
|
57
|
+
_, plateau = plateau_reference(mix_db, freqs, params)
|
|
56
58
|
axis, strata = stratified_response(material.mono_mix, 1000, params, plateau)
|
|
57
59
|
from beqforge.diagnose import smooth_unexcluded
|
|
58
60
|
|
|
@@ -6,7 +6,8 @@ Everything else about the designer binding is tested in isolation in
|
|
|
6
6
|
`test_design_designer.py`, on purpose (see that file's docstring) — this file exists because
|
|
7
7
|
none of those tests ever go through an actual HTTP request, so a bug in `_Handler` itself (a
|
|
8
8
|
wrong status code, a header the client can't parse, `do_POST` never reaching `design()`) would
|
|
9
|
-
be invisible to them.
|
|
9
|
+
be invisible to them. Every server here runs its designs in its own spawned worker process,
|
|
10
|
+
as production does.
|
|
10
11
|
|
|
11
12
|
The accepted-candidate case costs a real, bounded fit — `strategies=("flatten",)` measured
|
|
12
13
|
directly at ~4-8s (AGENTS.md's "40-80s a title" is for a full multi-strategy run against real,
|
|
@@ -35,17 +36,15 @@ from beqforge.designer import (
|
|
|
35
36
|
from beqforge.harness import apply_high_pass
|
|
36
37
|
from beqforge.material import LFE_GAIN
|
|
37
38
|
from beqforge.pipeline import PipelineParams
|
|
38
|
-
from tools.designer_server import DESIGN_PATH,
|
|
39
|
+
from tools.designer_server import DESIGN_PATH, DesignServer, DesignSettings
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
@pytest.fixture(scope="module")
|
|
42
43
|
def server():
|
|
43
|
-
"""`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
_Handler.params = PipelineParams(strategies=("flatten",))
|
|
48
|
-
httpd = HTTPServer(("127.0.0.1", 0), _Handler)
|
|
44
|
+
"""`DesignServer`, same as production, its designs in a spawned worker process."""
|
|
45
|
+
httpd = DesignServer(
|
|
46
|
+
("127.0.0.1", 0), DesignSettings(PipelineParams(strategies=("flatten",)))
|
|
47
|
+
)
|
|
49
48
|
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
50
49
|
thread.start()
|
|
51
50
|
try:
|
|
@@ -53,6 +52,7 @@ def server():
|
|
|
53
52
|
finally:
|
|
54
53
|
httpd.shutdown()
|
|
55
54
|
thread.join(timeout=5)
|
|
55
|
+
httpd.server_close()
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
DESIGN_TIMEOUT_S = 900.0
|
|
@@ -223,15 +223,61 @@ def test_accepts_a_real_injected_rolloff_over_http(server) -> None:
|
|
|
223
223
|
validate_response(_response_from_json(body)) # must not raise
|
|
224
224
|
|
|
225
225
|
|
|
226
|
-
def
|
|
227
|
-
"""
|
|
228
|
-
|
|
226
|
+
def test_health_answers_while_a_design_runs(server) -> None:
|
|
227
|
+
"""The designs run in the worker process, so a health check never waits behind one."""
|
|
228
|
+
answered = []
|
|
229
|
+
designing = threading.Thread(
|
|
230
|
+
target=lambda: answered.append(
|
|
231
|
+
_request(
|
|
232
|
+
server,
|
|
233
|
+
"POST",
|
|
234
|
+
DESIGN_PATH,
|
|
235
|
+
body=_known_filter_request(),
|
|
236
|
+
timeout=DESIGN_TIMEOUT_S,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
designing.start()
|
|
241
|
+
try:
|
|
242
|
+
status, _ = _request(server, "GET", "/health", timeout=3.0)
|
|
243
|
+
assert status == 200
|
|
244
|
+
assert designing.is_alive()
|
|
245
|
+
finally:
|
|
246
|
+
designing.join(timeout=DESIGN_TIMEOUT_S)
|
|
247
|
+
assert answered[0][0] == 200
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_a_dead_worker_is_a_500_and_the_next_request_gets_a_new_one() -> None:
|
|
251
|
+
httpd, thread = _serve(PipelineParams(strategies=("flatten",)), None)
|
|
252
|
+
try:
|
|
253
|
+
for process in list(httpd.worker._pool._processes.values()):
|
|
254
|
+
process.kill()
|
|
255
|
+
process.join(timeout=10)
|
|
256
|
+
status, body = _request(
|
|
257
|
+
httpd.server_address, "POST", DESIGN_PATH, _no_evidence_request()
|
|
258
|
+
)
|
|
259
|
+
assert status == 500
|
|
260
|
+
assert "worker died" in body["error"]
|
|
261
|
+
status, body = _request(
|
|
262
|
+
httpd.server_address, "POST", DESIGN_PATH, _no_evidence_request()
|
|
263
|
+
)
|
|
264
|
+
assert status == 200
|
|
265
|
+
assert body["decline_reason"] == "channel_evidence_unavailable"
|
|
266
|
+
finally:
|
|
267
|
+
_stop(httpd, thread)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _serve(params: PipelineParams, cache, shared_root=None):
|
|
271
|
+
"""A server of its own, with its own settings and its own worker process."""
|
|
229
272
|
# These tests exercise transport and cache identity/reuse, not section-budget
|
|
230
273
|
# escalation. Still run the real pipeline with a bounded numerical search;
|
|
231
274
|
# test_accepts_a_real_injected_rolloff_over_http retains the full budget.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
275
|
+
httpd = DesignServer(
|
|
276
|
+
("127.0.0.1", 0),
|
|
277
|
+
DesignSettings(
|
|
278
|
+
replace(params, max_sections=1), cache=cache, shared_root=shared_root
|
|
279
|
+
),
|
|
280
|
+
)
|
|
235
281
|
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
236
282
|
thread.start()
|
|
237
283
|
return httpd, thread
|
|
@@ -255,14 +301,12 @@ BOTH = ("flatten", "parametric")
|
|
|
255
301
|
|
|
256
302
|
|
|
257
303
|
def test_a_repeat_request_reuses_every_stage_and_answers_the_same(
|
|
258
|
-
|
|
304
|
+
tmp_path, caplog
|
|
259
305
|
) -> None:
|
|
260
306
|
"""R2a: `--cache-dir` — the second request for a title skips its analysis."""
|
|
261
307
|
from beqforge.cache import DirStore
|
|
262
308
|
|
|
263
|
-
httpd, thread = _serve(
|
|
264
|
-
monkeypatch, PipelineParams(strategies=BOTH), DirStore(tmp_path)
|
|
265
|
-
)
|
|
309
|
+
httpd, thread = _serve(PipelineParams(strategies=BOTH), DirStore(tmp_path))
|
|
266
310
|
try:
|
|
267
311
|
request = _known_filter_request()
|
|
268
312
|
first = _request(
|
|
@@ -280,16 +324,12 @@ def test_a_repeat_request_reuses_every_stage_and_answers_the_same(
|
|
|
280
324
|
assert list((tmp_path / "analysis").iterdir())
|
|
281
325
|
|
|
282
326
|
|
|
283
|
-
def test_another_server_setting_reuses_the_analysis_only(
|
|
284
|
-
monkeypatch, tmp_path, caplog
|
|
285
|
-
) -> None:
|
|
327
|
+
def test_another_server_setting_reuses_the_analysis_only(tmp_path, caplog) -> None:
|
|
286
328
|
from beqforge.accept import AcceptParams
|
|
287
329
|
from beqforge.cache import DirStore
|
|
288
330
|
|
|
289
331
|
request = _known_filter_request()
|
|
290
|
-
httpd, thread = _serve(
|
|
291
|
-
monkeypatch, PipelineParams(strategies=BOTH), DirStore(tmp_path)
|
|
292
|
-
)
|
|
332
|
+
httpd, thread = _serve(PipelineParams(strategies=BOTH), DirStore(tmp_path))
|
|
293
333
|
try:
|
|
294
334
|
_request(httpd.server_address, "POST", DESIGN_PATH, request, DESIGN_TIMEOUT_S)
|
|
295
335
|
finally:
|
|
@@ -297,7 +337,7 @@ def test_another_server_setting_reuses_the_analysis_only(
|
|
|
297
337
|
tilted = PipelineParams(
|
|
298
338
|
strategies=BOTH, accept=AcceptParams(target_tilt_db_per_octave=1.0)
|
|
299
339
|
)
|
|
300
|
-
httpd, thread = _serve(
|
|
340
|
+
httpd, thread = _serve(tilted, DirStore(tmp_path))
|
|
301
341
|
caplog.clear()
|
|
302
342
|
try:
|
|
303
343
|
with caplog.at_level(logging.INFO, logger="beqforge.cache"):
|
|
@@ -311,15 +351,11 @@ def test_another_server_setting_reuses_the_analysis_only(
|
|
|
311
351
|
assert len(list((tmp_path / "parametric").iterdir())) == 2
|
|
312
352
|
|
|
313
353
|
|
|
314
|
-
def test_bass_management_alone_reuses_the_analysis(
|
|
315
|
-
monkeypatch, tmp_path, caplog
|
|
316
|
-
) -> None:
|
|
354
|
+
def test_bass_management_alone_reuses_the_analysis(tmp_path, caplog) -> None:
|
|
317
355
|
from beqforge.cache import DirStore
|
|
318
356
|
|
|
319
357
|
request = _known_filter_request()
|
|
320
|
-
httpd, thread = _serve(
|
|
321
|
-
monkeypatch, PipelineParams(strategies=("flatten",)), DirStore(tmp_path)
|
|
322
|
-
)
|
|
358
|
+
httpd, thread = _serve(PipelineParams(strategies=("flatten",)), DirStore(tmp_path))
|
|
323
359
|
try:
|
|
324
360
|
_request(httpd.server_address, "POST", DESIGN_PATH, request, DESIGN_TIMEOUT_S)
|
|
325
361
|
managed = {
|
|
@@ -343,8 +379,8 @@ def test_bass_management_alone_reuses_the_analysis(
|
|
|
343
379
|
assert "analysis" in _reused(caplog)
|
|
344
380
|
|
|
345
381
|
|
|
346
|
-
def test_without_a_cache_dir_nothing_is_written(
|
|
347
|
-
httpd, thread = _serve(
|
|
382
|
+
def test_without_a_cache_dir_nothing_is_written(tmp_path) -> None:
|
|
383
|
+
httpd, thread = _serve(PipelineParams(strategies=("flatten",)), None)
|
|
348
384
|
try:
|
|
349
385
|
status, _ = _request(
|
|
350
386
|
httpd.server_address, "POST", DESIGN_PATH, _no_evidence_request()
|
|
@@ -393,9 +429,8 @@ def _by_reference(request: dict, root) -> dict:
|
|
|
393
429
|
}
|
|
394
430
|
|
|
395
431
|
|
|
396
|
-
def test_health_says_whether_references_are_accepted(
|
|
397
|
-
httpd, thread = _serve(
|
|
398
|
-
monkeypatch.setattr(_Handler, "shared_root", tmp_path)
|
|
432
|
+
def test_health_says_whether_references_are_accepted(tmp_path) -> None:
|
|
433
|
+
httpd, thread = _serve(PipelineParams(strategies=("flatten",)), None, tmp_path)
|
|
399
434
|
try:
|
|
400
435
|
status, body = _request(httpd.server_address, "GET", "/health")
|
|
401
436
|
finally:
|
|
@@ -404,14 +439,13 @@ def test_health_says_whether_references_are_accepted(monkeypatch, tmp_path) -> N
|
|
|
404
439
|
assert body == {"status": "ok", "contract_version": "1.2", "shared_root": True}
|
|
405
440
|
|
|
406
441
|
|
|
407
|
-
def test_a_request_by_reference_answers_as_the_same_request_inline(
|
|
408
|
-
monkeypatch, tmp_path
|
|
409
|
-
) -> None:
|
|
442
|
+
def test_a_request_by_reference_answers_as_the_same_request_inline(tmp_path) -> None:
|
|
410
443
|
"""R2b over a real socket: same audio by path, same response."""
|
|
411
444
|
inline = {**_known_filter_request(), "contract_version": "1.2"}
|
|
412
445
|
by_reference = _by_reference(inline, tmp_path)
|
|
413
|
-
httpd, thread = _serve(
|
|
414
|
-
|
|
446
|
+
httpd, thread = _serve(
|
|
447
|
+
PipelineParams(strategies=("flatten",)), None, tmp_path.resolve()
|
|
448
|
+
)
|
|
415
449
|
try:
|
|
416
450
|
first = _request(
|
|
417
451
|
httpd.server_address, "POST", DESIGN_PATH, inline, DESIGN_TIMEOUT_S
|
|
@@ -425,11 +459,9 @@ def test_a_request_by_reference_answers_as_the_same_request_inline(
|
|
|
425
459
|
assert first[1] == second[1]
|
|
426
460
|
|
|
427
461
|
|
|
428
|
-
def test_a_reference_without_a_shared_root_is_422_naming_the_array(
|
|
429
|
-
monkeypatch, tmp_path
|
|
430
|
-
) -> None:
|
|
462
|
+
def test_a_reference_without_a_shared_root_is_422_naming_the_array(tmp_path) -> None:
|
|
431
463
|
by_reference = _by_reference(_known_filter_request(duration_s=20.0), tmp_path)
|
|
432
|
-
httpd, thread = _serve(
|
|
464
|
+
httpd, thread = _serve(PipelineParams(strategies=("flatten",)), None)
|
|
433
465
|
try:
|
|
434
466
|
status, body = _request(httpd.server_address, "POST", DESIGN_PATH, by_reference)
|
|
435
467
|
finally:
|
|
@@ -439,7 +471,7 @@ def test_a_reference_without_a_shared_root_is_422_naming_the_array(
|
|
|
439
471
|
assert "no shared root" in body["reason"]
|
|
440
472
|
|
|
441
473
|
|
|
442
|
-
def test_an_escaping_path_is_422_and_both_forms_is_400(
|
|
474
|
+
def test_an_escaping_path_is_422_and_both_forms_is_400(tmp_path) -> None:
|
|
443
475
|
root = tmp_path / "root"
|
|
444
476
|
root.mkdir()
|
|
445
477
|
by_reference = _by_reference(_known_filter_request(duration_s=20.0), root)
|
|
@@ -456,8 +488,9 @@ def test_an_escaping_path_is_422_and_both_forms_is_400(monkeypatch, tmp_path) ->
|
|
|
456
488
|
**by_reference,
|
|
457
489
|
"mono_mix": {**by_reference["mono_mix"], "data_base64": "AAAA"},
|
|
458
490
|
}
|
|
459
|
-
httpd, thread = _serve(
|
|
460
|
-
|
|
491
|
+
httpd, thread = _serve(
|
|
492
|
+
PipelineParams(strategies=("flatten",)), None, root.resolve()
|
|
493
|
+
)
|
|
461
494
|
try:
|
|
462
495
|
escaped = _request(httpd.server_address, "POST", DESIGN_PATH, escaping)
|
|
463
496
|
doubled = _request(httpd.server_address, "POST", DESIGN_PATH, both)
|
|
@@ -9,7 +9,9 @@ from beqforge.diagnose import (
|
|
|
9
9
|
band_slope,
|
|
10
10
|
band_tracking,
|
|
11
11
|
diagnose,
|
|
12
|
+
mean_spectrum,
|
|
12
13
|
mix_shares,
|
|
14
|
+
plateau_reference,
|
|
13
15
|
steepest_slope,
|
|
14
16
|
stratified_response,
|
|
15
17
|
)
|
|
@@ -192,6 +194,34 @@ def test_the_plateau_reference_follows_the_channel_not_a_constant() -> None:
|
|
|
192
194
|
)
|
|
193
195
|
|
|
194
196
|
|
|
197
|
+
@pytest.mark.parametrize("corner_hz", [144.0, 145.0, 146.0])
|
|
198
|
+
def test_a_plateau_reaching_the_band_top_is_the_one_the_pipeline_finds(
|
|
199
|
+
corner_hz,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Diagnosis measures tracking from the mix plateau the pipeline judges against.
|
|
202
|
+
|
|
203
|
+
A plateau that runs into the band's top edge was lost when the spectrum was cut off at the
|
|
204
|
+
last bin below it: diagnosis then measured no strata or tracking and reported the band top as
|
|
205
|
+
where tracking stopped, which the pipeline read as "nothing below it tracks" (A Few Good Men,
|
|
206
|
+
150.5-200 Hz).
|
|
207
|
+
"""
|
|
208
|
+
rng = np.random.default_rng(0)
|
|
209
|
+
mix = signal.sosfilt(
|
|
210
|
+
signal.butter(8, corner_hz, btype="high", fs=FS, output="sos"),
|
|
211
|
+
rng.standard_normal(int(FS * DURATION_S)),
|
|
212
|
+
)
|
|
213
|
+
mix = mix + 1e-3 * rng.standard_normal(mix.size)
|
|
214
|
+
freqs, spectrum = mean_spectrum(mix, FS)
|
|
215
|
+
level, plateau = plateau_reference(spectrum, freqs, PARAMS)
|
|
216
|
+
assert np.isfinite(level) and plateau[1] == PARAMS.band_hz[1]
|
|
217
|
+
|
|
218
|
+
result = diagnose(Material("edge", FS, mix, {}, "complete_programme"), PARAMS)
|
|
219
|
+
|
|
220
|
+
assert result.stratified
|
|
221
|
+
# nothing below the high-pass tracks, so tracking stops at the plateau's lower edge
|
|
222
|
+
assert result.noise_floor_hz == pytest.approx(plateau[0])
|
|
223
|
+
|
|
224
|
+
|
|
195
225
|
def test_the_plateau_reference_clears_the_knee() -> None:
|
|
196
226
|
"""The reference must sit above the corner, or attenuation is measured against itself."""
|
|
197
227
|
material = material_from(
|