beqforge 0.2.2__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. beqforge-0.2.4/.gitattributes +1 -0
  2. {beqforge-0.2.2 → beqforge-0.2.4}/.github/workflows/build-executable.yml +8 -0
  3. {beqforge-0.2.2 → beqforge-0.2.4}/PKG-INFO +43 -4
  4. {beqforge-0.2.2 → beqforge-0.2.4}/README.md +42 -3
  5. {beqforge-0.2.2 → beqforge-0.2.4}/TODO.md +26 -23
  6. {beqforge-0.2.2 → beqforge-0.2.4}/beq_common/__init__.py +1 -1
  7. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/AGENTS.md +6 -2
  8. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/pipeline.py +26 -3
  9. beqforge-0.2.4/packaging/designer/beqforge-designer.cmd +33 -0
  10. {beqforge-0.2.2 → beqforge-0.2.4}/plans/done-design-and-pipeline.md +31 -0
  11. beqforge-0.2.4/plans/parameter-space-optimisation.md +75 -0
  12. beqforge-0.2.4/plans/texture-yardstick-sub-band.md +215 -0
  13. beqforge-0.2.4/plans/texture-yardstick-sub-band.patch +154 -0
  14. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_designer_server.py +61 -5
  15. beqforge-0.2.4/tests/test_design_ripple_band.py +56 -0
  16. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_designer_server_startup.py +19 -2
  17. {beqforge-0.2.2 → beqforge-0.2.4}/tools/designer_server.py +93 -14
  18. {beqforge-0.2.2 → beqforge-0.2.4}/tools/smoke_test_exe.py +21 -10
  19. {beqforge-0.2.2 → beqforge-0.2.4}/.dockerignore +0 -0
  20. {beqforge-0.2.2 → beqforge-0.2.4}/.github/dependabot.yml +0 -0
  21. {beqforge-0.2.2 → beqforge-0.2.4}/.github/workflows/build-designer-image.yml +0 -0
  22. {beqforge-0.2.2 → beqforge-0.2.4}/.github/workflows/build-device-check.yml +0 -0
  23. {beqforge-0.2.2 → beqforge-0.2.4}/.github/workflows/dependabot-auto-merge.yaml +0 -0
  24. {beqforge-0.2.2 → beqforge-0.2.4}/.github/workflows/tests.yml +0 -0
  25. {beqforge-0.2.2 → beqforge-0.2.4}/.gitignore +0 -0
  26. {beqforge-0.2.2 → beqforge-0.2.4}/.idea/.gitignore +0 -0
  27. {beqforge-0.2.2 → beqforge-0.2.4}/.idea/beqforge.iml +0 -0
  28. {beqforge-0.2.2 → beqforge-0.2.4}/.idea/editor.xml +0 -0
  29. {beqforge-0.2.2 → beqforge-0.2.4}/.idea/jsonSchemas.xml +0 -0
  30. {beqforge-0.2.2 → beqforge-0.2.4}/.idea/misc.xml +0 -0
  31. {beqforge-0.2.2 → beqforge-0.2.4}/.idea/modules.xml +0 -0
  32. {beqforge-0.2.2 → beqforge-0.2.4}/.idea/vcs.xml +0 -0
  33. {beqforge-0.2.2 → beqforge-0.2.4}/AGENTS.md +0 -0
  34. {beqforge-0.2.2 → beqforge-0.2.4}/CLAUDE.md +0 -0
  35. {beqforge-0.2.2 → beqforge-0.2.4}/LICENCE.md +0 -0
  36. {beqforge-0.2.2 → beqforge-0.2.4}/beq_common/AGENTS.md +0 -0
  37. {beqforge-0.2.2 → beqforge-0.2.4}/beq_common/biquad.py +0 -0
  38. {beqforge-0.2.2 → beqforge-0.2.4}/beq_common/provenance.py +0 -0
  39. {beqforge-0.2.2 → beqforge-0.2.4}/beq_common/publication.py +0 -0
  40. {beqforge-0.2.2 → beqforge-0.2.4}/beq_common/types.py +0 -0
  41. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/__init__.py +0 -0
  42. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/accept.py +0 -0
  43. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/beqd.py +0 -0
  44. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/biquad.py +0 -0
  45. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/cache.py +0 -0
  46. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/charts.py +0 -0
  47. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/cli.py +0 -0
  48. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/design.py +0 -0
  49. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/designer.py +0 -0
  50. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/diagnose.py +0 -0
  51. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/explain.py +0 -0
  52. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/extraction.py +0 -0
  53. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/filters.py +0 -0
  54. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/harness.py +0 -0
  55. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/identify.py +0 -0
  56. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/material.py +0 -0
  57. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/record.py +0 -0
  58. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/reference.py +0 -0
  59. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/rolloff.py +0 -0
  60. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge/verify.py +0 -0
  61. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge-device-check.spec +0 -0
  62. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge.spec +0 -0
  63. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/AGENTS.md +0 -0
  64. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/__init__.py +0 -0
  65. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/analyse.py +0 -0
  66. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/audio.py +0 -0
  67. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/catalogue.py +0 -0
  68. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/cli.py +0 -0
  69. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/coefficients.py +0 -0
  70. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/engines.py +0 -0
  71. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/evidence.py +0 -0
  72. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/manifest.py +0 -0
  73. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/measurement.py +0 -0
  74. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/profiles.py +0 -0
  75. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/qualification.py +0 -0
  76. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/report.py +0 -0
  77. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/transactions.py +0 -0
  78. {beqforge-0.2.2 → beqforge-0.2.4}/beqforge_device_check/verify.py +0 -0
  79. {beqforge-0.2.2 → beqforge-0.2.4}/beqoptimiser/AGENTS.md +0 -0
  80. {beqforge-0.2.2 → beqforge-0.2.4}/beqoptimiser/__init__.py +0 -0
  81. {beqforge-0.2.2 → beqforge-0.2.4}/beqoptimiser/cache.py +0 -0
  82. {beqforge-0.2.2 → beqforge-0.2.4}/beqoptimiser/cli.py +0 -0
  83. {beqforge-0.2.2 → beqforge-0.2.4}/beqoptimiser/core.py +0 -0
  84. {beqforge-0.2.2 → beqforge-0.2.4}/beqoptimiser/data/seed-manifest.json +0 -0
  85. {beqforge-0.2.2 → beqforge-0.2.4}/beqoptimiser/data/seed.json.gz +0 -0
  86. {beqforge-0.2.2 → beqforge-0.2.4}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
  87. {beqforge-0.2.2 → beqforge-0.2.4}/docs/device-check-examples/simulation.json +0 -0
  88. {beqforge-0.2.2 → beqforge-0.2.4}/docs/device-check.md +0 -0
  89. {beqforge-0.2.2 → beqforge-0.2.4}/docs/optimiser-report/README.md +0 -0
  90. {beqforge-0.2.2 → beqforge-0.2.4}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
  91. {beqforge-0.2.2 → beqforge-0.2.4}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
  92. {beqforge-0.2.2 → beqforge-0.2.4}/docs/optimiser-report/assets/example-1.png +0 -0
  93. {beqforge-0.2.2 → beqforge-0.2.4}/docs/optimiser-report/assets/example-2.png +0 -0
  94. {beqforge-0.2.2 → beqforge-0.2.4}/docs/optimiser-report/entry-results.json.gz +0 -0
  95. {beqforge-0.2.2 → beqforge-0.2.4}/docs/optimiser-report/statistics.json +0 -0
  96. {beqforge-0.2.2 → beqforge-0.2.4}/docs/optimiser.md +0 -0
  97. {beqforge-0.2.2 → beqforge-0.2.4}/evidence_validation.json +0 -0
  98. {beqforge-0.2.2 → beqforge-0.2.4}/negative_corpus.json +0 -0
  99. {beqforge-0.2.2 → beqforge-0.2.4}/packaging/designer/Dockerfile +0 -0
  100. {beqforge-0.2.2 → beqforge-0.2.4}/packaging/designer/smoke.py +0 -0
  101. {beqforge-0.2.2 → beqforge-0.2.4}/packaging/device-check/NOTICE.txt +0 -0
  102. {beqforge-0.2.2 → beqforge-0.2.4}/plans/F2-catalogue-filter-optimisation.md +0 -0
  103. {beqforge-0.2.2 → beqforge-0.2.4}/plans/F2-device-precision-validation.md +0 -0
  104. {beqforge-0.2.2 → beqforge-0.2.4}/plans/F2-package-consolidation.md +0 -0
  105. {beqforge-0.2.2 → beqforge-0.2.4}/plans/R2a-server-stage-cache.md +0 -0
  106. {beqforge-0.2.2 → beqforge-0.2.4}/plans/done-baseline-evidence.md +0 -0
  107. {beqforge-0.2.2 → beqforge-0.2.4}/plans/research-design-decisions.md +0 -0
  108. {beqforge-0.2.2 → beqforge-0.2.4}/plans/test-portability-and-runtime.md +0 -0
  109. {beqforge-0.2.2 → beqforge-0.2.4}/pyproject.toml +0 -0
  110. {beqforge-0.2.2 → beqforge-0.2.4}/tests/__init__.py +0 -0
  111. {beqforge-0.2.2 → beqforge-0.2.4}/tests/conftest.py +0 -0
  112. {beqforge-0.2.2 → beqforge-0.2.4}/tests/fixtures/optimiser_seed_entry.json +0 -0
  113. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_cli.py +0 -0
  114. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_accept.py +0 -0
  115. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_cache.py +0 -0
  116. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_cache_stores.py +0 -0
  117. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_channel_evidence.py +0 -0
  118. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_charts.py +0 -0
  119. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_corpus.py +0 -0
  120. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_design.py +0 -0
  121. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_designer.py +0 -0
  122. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_device_rate.py +0 -0
  123. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_diagnose.py +0 -0
  124. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_evidence.py +0 -0
  125. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_evidence_excess.py +0 -0
  126. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_exclusions.py +0 -0
  127. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_explain.py +0 -0
  128. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_extraction.py +0 -0
  129. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_filters.py +0 -0
  130. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_frozen_cache.py +0 -0
  131. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_guard.py +0 -0
  132. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_harness.py +0 -0
  133. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_headroom.py +0 -0
  134. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_hum.py +0 -0
  135. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_identify.py +0 -0
  136. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_jacobian.py +0 -0
  137. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_parallel.py +0 -0
  138. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_pipeline.py +0 -0
  139. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_playback.py +0 -0
  140. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_playback_headroom.py +0 -0
  141. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_publication.py +0 -0
  142. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_record.py +0 -0
  143. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_record_fingerprint.py +0 -0
  144. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_reference.py +0 -0
  145. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_references.py +0 -0
  146. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_rolloff.py +0 -0
  147. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_strategy_config.py +0 -0
  148. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_timings.py +0 -0
  149. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_design_verify.py +0 -0
  150. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_designer_image.py +0 -0
  151. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_analyse.py +0 -0
  152. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_catalogue.py +0 -0
  153. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_catalogue_predict.py +0 -0
  154. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_cli.py +0 -0
  155. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_engines.py +0 -0
  156. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_evidence.py +0 -0
  157. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_examples.py +0 -0
  158. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_manifest.py +0 -0
  159. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_measurement.py +0 -0
  160. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_qualification.py +0 -0
  161. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_transactions.py +0 -0
  162. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_device_check_verify.py +0 -0
  163. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_extract_layout.py +0 -0
  164. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_optimiser.py +0 -0
  165. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_optimiser_cache.py +0 -0
  166. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_optimiser_report.py +0 -0
  167. {beqforge-0.2.2 → beqforge-0.2.4}/tests/test_package_boundaries.py +0 -0
  168. {beqforge-0.2.2 → beqforge-0.2.4}/tools/__init__.py +0 -0
  169. {beqforge-0.2.2 → beqforge-0.2.4}/tools/build_optimiser_seed.py +0 -0
  170. {beqforge-0.2.2 → beqforge-0.2.4}/tools/design_beq.py +0 -0
  171. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/a1_sweep.py +0 -0
  172. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/compare_records.py +0 -0
  173. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/compare_verdicts.py +0 -0
  174. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/inject_variants.py +0 -0
  175. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p14_compare.py +0 -0
  176. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p14_jacobian.py +0 -0
  177. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p14_run_pipeline.py +0 -0
  178. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p14_surrogate.py +0 -0
  179. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p18_compare_greedy.py +0 -0
  180. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p18_compare_seeded.py +0 -0
  181. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p18_compare_structure.py +0 -0
  182. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p18_greedy.py +0 -0
  183. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p18_seeded.py +0 -0
  184. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p18_structure.py +0 -0
  185. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/p20_smooth_drift.py +0 -0
  186. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/probe.py +0 -0
  187. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/refit_probe.py +0 -0
  188. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/request_timing.py +0 -0
  189. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/score_injected.py +0 -0
  190. {beqforge-0.2.2 → beqforge-0.2.4}/tools/experiments/t8_floor_rules.py +0 -0
  191. {beqforge-0.2.2 → beqforge-0.2.4}/tools/extract.py +0 -0
  192. {beqforge-0.2.2 → beqforge-0.2.4}/tools/ledger_template.html +0 -0
  193. {beqforge-0.2.2 → beqforge-0.2.4}/tools/negative_corpus.py +0 -0
  194. {beqforge-0.2.2 → beqforge-0.2.4}/tools/optimiser_catalogue_report.py +0 -0
  195. {beqforge-0.2.2 → beqforge-0.2.4}/tools/prepare_device_check_helper.py +0 -0
  196. {beqforge-0.2.2 → beqforge-0.2.4}/tools/render_ledger.py +0 -0
  197. {beqforge-0.2.2 → beqforge-0.2.4}/tools/replay.py +0 -0
  198. {beqforge-0.2.2 → beqforge-0.2.4}/tools/seed_optimiser_cache.py +0 -0
  199. {beqforge-0.2.2 → beqforge-0.2.4}/tools/smoke_test_device_check.py +0 -0
  200. {beqforge-0.2.2 → beqforge-0.2.4}/tools/smoke_test_install.py +0 -0
  201. {beqforge-0.2.2 → beqforge-0.2.4}/tools/summarise.py +0 -0
  202. {beqforge-0.2.2 → beqforge-0.2.4}/tools/validate_evidence.py +0 -0
  203. {beqforge-0.2.2 → beqforge-0.2.4}/uv.lock +0 -0
@@ -0,0 +1 @@
1
+ *.cmd text eol=crlf
@@ -226,6 +226,14 @@ jobs:
226
226
  env:
227
227
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
228
228
 
229
+ - name: Attach the Windows designer launcher
230
+ if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/')
231
+ uses: softprops/action-gh-release@v3
232
+ with:
233
+ files: packaging/designer/beqforge-designer.cmd
234
+ env:
235
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
236
+
229
237
  publish:
230
238
  name: Publish Python package to PyPI
231
239
  needs: [validated_tests, package, build, create_release]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: beqforge
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -187,6 +187,12 @@ naming the array, rather than a design decline. Inline requests remain supported
187
187
  logs read, parse, decode, design and response timings; `tools/experiments/request_timing.py`
188
188
  measures cold and warm requests.
189
189
 
190
+ **Several designs at once.** `--workers N` (1-8, default 1) runs N designs at once, each in
191
+ its own worker process; further requests queue. `GET /health` reports it as `parallel`, so a
192
+ caller can send each server as many designs as it takes rather than be configured with that
193
+ number. Each design's fit pool gets an equal share of the machine's cores (`BEQ_FIT_WORKERS`,
194
+ if set, stays each design's own). The share changes how long a fit takes, never its answer.
195
+
190
196
  Every `serve-designer` configuration option can also be set through an environment variable.
191
197
  Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
192
198
  environment list. `--help` lists the variable beside each option. Other subcommands retain
@@ -207,6 +213,7 @@ their existing CLI configuration.
207
213
  | `--record-dir` | `BEQFORGE_RECORD_DIR` | path |
208
214
  | `--cache-dir` | `BEQFORGE_CACHE_DIR` | path |
209
215
  | `--shared-root` | `BEQFORGE_SHARED_ROOT` | path |
216
+ | `--workers` | `BEQFORGE_WORKERS` | integer, 1-8 |
210
217
  | `--quiet` | `BEQFORGE_QUIET` | boolean |
211
218
 
212
219
  Booleans accept `true`/`false`, `yes`/`no`, `on`/`off` or `1`/`0`, ignoring case.
@@ -270,9 +277,9 @@ The image's defaults, all overridable with the environment variables above:
270
277
 
271
278
  It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
272
279
  the shared root and the cache: the server checks both with a write at startup. Its
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.
280
+ `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in separate
281
+ worker processes, `BEQFORGE_WORKERS` (default 1) at a time, so a caller sending more at once
282
+ queues them; size the caller's timeout for that. `docker stop` stops a design in progress.
276
283
 
277
284
  Build and check it locally from the repository root:
278
285
 
@@ -284,6 +291,38 @@ python3 packaging/designer/smoke.py beqforge-designer
284
291
  The smoke script checks `/health` and one design inline and one by reference;
285
292
  `tests/test_designer_image.py` runs the same check against an in-process server.
286
293
 
294
+ ### Running the designer on Windows
295
+
296
+ The release's `beqforge-windows-x86_64.exe` runs the same server natively, for a beqdesigner
297
+ container (or install) that calls out to a Windows machine. Put `beqforge-designer.cmd`, which
298
+ is attached to the same release (source: `packaging/designer/beqforge-designer.cmd`), beside the
299
+ exe and double-click it, or run the exe directly:
300
+
301
+ ```bat
302
+ beqforge-windows-x86_64.exe serve-designer --host 0.0.0.0 --port 8420 ^
303
+ --shared-root D:\beq\work --cache-dir %LOCALAPPDATA%\beqforge\cache
304
+ ```
305
+
306
+ The launcher sets the image's defaults through the environment variables above, each only if
307
+ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
308
+ `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache`. Arguments pass through to `serve-designer`.
309
+
310
+ * **Listen on the network.** The CLI's default host is `127.0.0.1`, which a container cannot
311
+ reach; the launcher uses `0.0.0.0`. Windows Firewall asks the first time: allow private
312
+ networks.
313
+ * **Shared root.** For audio by reference, set `BEQFORGE_SHARED_ROOT` (or edit the commented
314
+ line in the launcher) to the Windows folder bind-mounted into beqdesigner's container as its
315
+ work directory. Without it, beqdesigner must send audio inline.
316
+ * **beqdesigner's side.** Under Docker Desktop on the same machine, register
317
+ `http://host.docker.internal:8420/design`; from another machine, use this one's LAN address.
318
+ Check `http://<address>:8420/health` from there first.
319
+ * **Several at once.** Set `BEQFORGE_WORKERS` (or pass `--workers 2`) for a machine that can
320
+ design more than one title at a time; `/health` reports it as `parallel`.
321
+ * **The rest.** The exe is unsigned, so SmartScreen warns on first run. `serve-designer` does not need `ffmpeg`. Close the window, or
322
+ press Ctrl+C, to stop.
323
+
324
+ ### Building and releasing
325
+
287
326
  For a machine without Python/`uv`, a standalone `beqforge` executable (every subcommand,
288
327
  including `serve-designer`) is built for Linux/macOS/Windows by
289
328
  `.github/workflows/build-executable.yml` on every `vX.Y.Z` tag, and attached to the matching
@@ -161,6 +161,12 @@ naming the array, rather than a design decline. Inline requests remain supported
161
161
  logs read, parse, decode, design and response timings; `tools/experiments/request_timing.py`
162
162
  measures cold and warm requests.
163
163
 
164
+ **Several designs at once.** `--workers N` (1-8, default 1) runs N designs at once, each in
165
+ its own worker process; further requests queue. `GET /health` reports it as `parallel`, so a
166
+ caller can send each server as many designs as it takes rather than be configured with that
167
+ number. Each design's fit pool gets an equal share of the machine's cores (`BEQ_FIT_WORKERS`,
168
+ if set, stays each design's own). The share changes how long a fit takes, never its answer.
169
+
164
170
  Every `serve-designer` configuration option can also be set through an environment variable.
165
171
  Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
166
172
  environment list. `--help` lists the variable beside each option. Other subcommands retain
@@ -181,6 +187,7 @@ their existing CLI configuration.
181
187
  | `--record-dir` | `BEQFORGE_RECORD_DIR` | path |
182
188
  | `--cache-dir` | `BEQFORGE_CACHE_DIR` | path |
183
189
  | `--shared-root` | `BEQFORGE_SHARED_ROOT` | path |
190
+ | `--workers` | `BEQFORGE_WORKERS` | integer, 1-8 |
184
191
  | `--quiet` | `BEQFORGE_QUIET` | boolean |
185
192
 
186
193
  Booleans accept `true`/`false`, `yes`/`no`, `on`/`off` or `1`/`0`, ignoring case.
@@ -244,9 +251,9 @@ The image's defaults, all overridable with the environment variables above:
244
251
 
245
252
  It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
246
253
  the shared root and the cache: the server checks both with a write at startup. Its
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.
254
+ `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in separate
255
+ worker processes, `BEQFORGE_WORKERS` (default 1) at a time, so a caller sending more at once
256
+ queues them; size the caller's timeout for that. `docker stop` stops a design in progress.
250
257
 
251
258
  Build and check it locally from the repository root:
252
259
 
@@ -258,6 +265,38 @@ python3 packaging/designer/smoke.py beqforge-designer
258
265
  The smoke script checks `/health` and one design inline and one by reference;
259
266
  `tests/test_designer_image.py` runs the same check against an in-process server.
260
267
 
268
+ ### Running the designer on Windows
269
+
270
+ The release's `beqforge-windows-x86_64.exe` runs the same server natively, for a beqdesigner
271
+ container (or install) that calls out to a Windows machine. Put `beqforge-designer.cmd`, which
272
+ is attached to the same release (source: `packaging/designer/beqforge-designer.cmd`), beside the
273
+ exe and double-click it, or run the exe directly:
274
+
275
+ ```bat
276
+ beqforge-windows-x86_64.exe serve-designer --host 0.0.0.0 --port 8420 ^
277
+ --shared-root D:\beq\work --cache-dir %LOCALAPPDATA%\beqforge\cache
278
+ ```
279
+
280
+ The launcher sets the image's defaults through the environment variables above, each only if
281
+ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
282
+ `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache`. Arguments pass through to `serve-designer`.
283
+
284
+ * **Listen on the network.** The CLI's default host is `127.0.0.1`, which a container cannot
285
+ reach; the launcher uses `0.0.0.0`. Windows Firewall asks the first time: allow private
286
+ networks.
287
+ * **Shared root.** For audio by reference, set `BEQFORGE_SHARED_ROOT` (or edit the commented
288
+ line in the launcher) to the Windows folder bind-mounted into beqdesigner's container as its
289
+ work directory. Without it, beqdesigner must send audio inline.
290
+ * **beqdesigner's side.** Under Docker Desktop on the same machine, register
291
+ `http://host.docker.internal:8420/design`; from another machine, use this one's LAN address.
292
+ Check `http://<address>:8420/health` from there first.
293
+ * **Several at once.** Set `BEQFORGE_WORKERS` (or pass `--workers 2`) for a machine that can
294
+ design more than one title at a time; `/health` reports it as `parallel`.
295
+ * **The rest.** The exe is unsigned, so SmartScreen warns on first run. `serve-designer` does not need `ffmpeg`. Close the window, or
296
+ press Ctrl+C, to stop.
297
+
298
+ ### Building and releasing
299
+
261
300
  For a machine without Python/`uv`, a standalone `beqforge` executable (every subcommand,
262
301
  including `serve-designer`) is built for Linux/macOS/Windows by
263
302
  `.github/workflows/build-executable.yml` on every `vX.Y.Z` tag, and attached to the matching
@@ -18,29 +18,32 @@ are retained so records and code comments remain traceable. Completed items are
18
18
  | Priority | ID | Status | Value, work and blocker or reason for parking |
19
19
  | --- | --- | --- | --- |
20
20
  | 1 | Playback contract | open | Reconcile the supplied bass-management model with the model actually used for targets, verification and clipping. Only `lpf_fs` is consumed today; check the ignored fields before claiming the listener's clipping cost. Caller propagation is an external subtask. |
21
- | 2 | E10 | open | Predeclare fresh content-goal validation before further tuning or a default change. Existing seeds and corpus have repeatedly informed development; writing the protocol needs no new audio. |
22
- | 3 | E1 | open | Add record-only scene-held-out verification: evidence independent of the material that generated the target. A gate needs a separate demonstrated benefit. |
23
- | 4 | Steep-filter scoring | open | Implement coloured/codec-like floors and recipe-specific truth bands under E10; inspect shape failures and published-ceiling excess. Existing titles can supply programme texture. |
24
- | 5 | Steep-filter default | blocked | Recovery improves substantially with `--content-edge`, but changing the default needs E10, the scoring above, listening judgements and more independent real steep titles. Safe House already works with the default; keep the option off meanwhile. |
25
- | 6 | F2 / device behaviour | in progress; first hardware pilot measured | Coefficient quantisation does not establish hardware accuracy. Limit changes need measured internal arithmetic/noise/limit cycles on a named device and a capture setup; the [measurement plan](plans/F2-device-precision-validation.md) specifies Python sweep/capture, optional REW cross-checks, software controls, single-entry/full-catalogue analysis and a standalone cross-platform executable. The `beqforge-device-check` preview includes frozen manifests, capture/recovery, CLI-only miniDSP controls (minidsp-rs 0.1.12, batched through `-f`) with float32 transport attribution, staged qualification/convergence, uncertainty-aware filter checks, entry/unique-cascade frequency distributions and matched-device deltas. First verified pilot on a miniDSP 2x4 HD (USB loopback, 96 kHz): the device plays its float32 coefficients as predicted (worst 0.0074 dB, 2-200 Hz), while float32 coefficients themselves put 18.8 dB of error into a 5 Hz Q 6 peak; see the plan, section 12. First catalogue sample (50 cascades): 49 play as predicted on every bin (worst 0.0082 dB); the 50th was clipped by the device and is to be remeasured (F2a, plan section 13). Next: the `grid` and `boundary` suites on hardware to map where float32 coefficients stop representing low, high-Q filters. No Q-limit change yet. |
26
- | 7 | Validation coverage (former TODO 10) | blocked | Broad real-programme generalisation needs the missing shapes and independently characterised paired releases listed below. Sparse titles exist; their absolute truth and wider sparse coverage remain missing. |
27
- | 8 | Identification | blocked | Generalising `N(f)` and its fit band needs independent, varied source material, especially paired releases. Sensitivity experiments can proceed; programme-only uncertainty cannot identify the cause of a droop. |
28
- | 9 | E4 | parked | No measured failure has been attributed to isolated ceiling holes. Reopen if unevenness or E9 excess is traced to such bins; smoothing may only reduce the raw licence. |
29
- | 10 | F4 follow-up | parked | Both fitter-objective selection spikes lost passes/true positives. Judge several fits only with a demonstrated usable alternative discarded before `_judge`; a smaller residual is insufficient evidence. |
30
- | 11 | F3 follow-up | parked | A fifth section moved no verdict when tried and costs more fitting. Reopen if an evidence-supported target has a demonstrated usable fit beyond four sections and no usable fit within four. |
31
- | 12 | T2 | parked | The steep-variant audit found no swept cap binding a weak winner. Derive caps only when a binding cap demonstrably prevents a better supported result. |
32
- | 13 | Residual (former TODO 5) | open | Correct the reported target error and its actual measurement band without changing fit selection. Primarily audit accuracy; any escalation/objective change is a separate, higher-impact decision. Non-parametric reporting is already allowed upstream. |
33
- | 14 | Authored features | parked | Manual `--exclude` works; no validated automatic detector exists. Reopen with real labelled features and negatives showing that detection preserves legitimate bass. |
34
- | 15 | Dynamic processing (former TODO 11) | parked | No suspected real case has scoped a useful tripwire. Reopen when compression/limiting demonstrably compromises this correction; non-LTI processing alone does not prove every BEQ is invalid. |
35
- | 16 | Tilted passband reference (goal/corpus follow-up) | parked | No real in-sub-band case needs a sloping reference yet; Dossier's abstention was decided. Reopen with one. Goal-aware corpus labels belong to the open E10 protocol work. Nearest so far: 13 Going on 30's mix tilts about −7 dB/oct above 50 Hz (bass is coherent across channels, ~200 Hz is not) and finds its plateau only through the one-sided fallback. That reference reads a 36 dB deficit below 25 Hz; each channel's own plateau reads 30-34 dB. A reference built from the channels would be 2-5 dB less generous there (see plans/done-design-and-pipeline.md, 11 October 2026). |
36
- | 17 | Mixed-channel policy | parked | `channel_scope` is currently an unpopulated optional response field, not a computed `mixed` finding awaiting a gate. Reopen with a case where existing per-channel restoration and common pricing produce an unsuitable mix correction. |
37
- | 18 | Fraction dial (former TODO 7) | parked | No user/title has needed partial licensed restoration as a preference; tilt and tolerance already ship. Reopen with a concrete requested outcome, then price and validate it like every target. |
38
- | 19 | Protective corner (former TODO 9) | parked | The corner is already derived from boost cap and implied order, floored by `lowest_frequency_hz`. No case requires an independent override or different fallback alignment; reopen with one. |
39
- | 20 | Device configuration on the wire | parked | The current contract deliberately omits device preference; server flags already configure realisation. Reopen if one server must serve different device formats/rates per request; then agree an upstream extension. Hardware validity remains F2. |
40
- | 21 | Catalogue comparison (former TODO 8) | parked | A disagreement detector has no demonstrated accuracy benefit. Reopen with an independent discrepancy to investigate; authored filters remain comparisons, never calibration targets. |
41
- | 22 | Performance | parked | Further reuse changes cost, not filter accuracy. Reopen after measuring repeated numerical identities or material remaining analysis cost; missed-fit search belongs to F3/F4 instead. |
42
- | 23 | Chart peak bias | parked | Deliberate catalogue-compatible per-bin maximum; it affects presentation only. Reopen if a reader needs an alternative percentile view; keep it distinct from the 95th-percentile evidence envelope. |
43
- | 24 | Documentation citations | open | Remove or repoint orphan references and stale descriptions of shipped behaviour. Low filter impact, but prevents future changes being based on obsolete assumptions. |
21
+ | 2 | Texture yardstick band | open (E10 only) | Implemented: the mix's ripple is measured about a cubic over the unchanged band, so the LFE knee is trend. 9 (2009), Beauty and the Beast, Crazy, Stupid, Love and Despicable Me 3 now accept; Carrie stays declined. Probe and protocol unchanged; corpus 0/45 gate pass. Neither width-normalised form separated `broadband/5`. The degree was chosen on corpus and library data, so it needs the fresh E10 validation before it counts as settled; see [the outcome](plans/texture-yardstick-sub-band.md#outcome-17-october-2026). |
22
+ | 3 | E10 | open | Predeclare fresh content-goal validation before further tuning or a default change. Existing seeds and corpus have repeatedly informed development; writing the protocol needs no new audio. |
23
+ | 4 | E1 | open | Add record-only scene-held-out verification: evidence independent of the material that generated the target. A gate needs a separate demonstrated benefit. |
24
+ | 5 | Steep-filter scoring | open | Implement coloured/codec-like floors and recipe-specific truth bands under E10; inspect shape failures and published-ceiling excess. Existing titles can supply programme texture. |
25
+ | 6 | Steep-filter default | blocked | Recovery improves substantially with `--content-edge`, but changing the default needs E10, the scoring above, listening judgements and more independent real steep titles. Safe House already works with the default; keep the option off meanwhile. |
26
+ | 7 | F2 / device behaviour | in progress; first hardware pilot measured | Coefficient quantisation does not establish hardware accuracy. Limit changes need measured internal arithmetic/noise/limit cycles on a named device and a capture setup; the [measurement plan](plans/F2-device-precision-validation.md) specifies Python sweep/capture, optional REW cross-checks, software controls, single-entry/full-catalogue analysis and a standalone cross-platform executable. The `beqforge-device-check` preview includes frozen manifests, capture/recovery, CLI-only miniDSP controls (minidsp-rs 0.1.12, batched through `-f`) with float32 transport attribution, staged qualification/convergence, uncertainty-aware filter checks, entry/unique-cascade frequency distributions and matched-device deltas. First verified pilot on a miniDSP 2x4 HD (USB loopback, 96 kHz): the device plays its float32 coefficients as predicted (worst 0.0074 dB, 2-200 Hz), while float32 coefficients themselves put 18.8 dB of error into a 5 Hz Q 6 peak; see the plan, section 12. First catalogue sample (50 cascades): 49 play as predicted on every bin (worst 0.0082 dB); the 50th was clipped by the device and is to be remeasured (F2a, plan section 13). Next: the `grid` and `boundary` suites on hardware to map where float32 coefficients stop representing low, high-Q filters. No Q-limit change yet. |
27
+ | 8 | Validation coverage (former TODO 10) | blocked | Broad real-programme generalisation needs the missing shapes and independently characterised paired releases listed below. Sparse titles exist; their absolute truth and wider sparse coverage remain missing. |
28
+ | 9 | Identification | blocked | Generalising `N(f)` and its fit band needs independent, varied source material, especially paired releases. Sensitivity experiments can proceed; programme-only uncertainty cannot identify the cause of a droop. |
29
+ | 10 | E4 | parked | No measured failure has been attributed to isolated ceiling holes. Reopen if unevenness or E9 excess is traced to such bins; smoothing may only reduce the raw licence. |
30
+ | 11 | F4 follow-up | parked | Both fitter-objective selection spikes lost passes/true positives. Judge several fits only with a demonstrated usable alternative discarded before `_judge`; a smaller residual is insufficient evidence. |
31
+ | 12 | F3 follow-up | parked | A fifth section moved no verdict when tried and costs more fitting. Reopen if an evidence-supported target has a demonstrated usable fit beyond four sections and no usable fit within four. |
32
+ | 13 | T2 | parked | The steep-variant audit found no swept cap binding a weak winner. Derive caps only when a binding cap demonstrably prevents a better supported result. |
33
+ | 14 | Residual (former TODO 5) | open | Correct the reported target error and its actual measurement band without changing fit selection. Primarily audit accuracy; any escalation/objective change is a separate, higher-impact decision. Non-parametric reporting is already allowed upstream. |
34
+ | 15 | Authored features | parked | Manual `--exclude` works; no validated automatic detector exists. Reopen with real labelled features and negatives showing that detection preserves legitimate bass. |
35
+ | 16 | Dynamic processing (former TODO 11) | parked | No suspected real case has scoped a useful tripwire. Reopen when compression/limiting demonstrably compromises this correction; non-LTI processing alone does not prove every BEQ is invalid. |
36
+ | 17 | Tilted passband reference (goal/corpus follow-up) | parked | No real in-sub-band case needs a sloping reference yet; Dossier's abstention was decided. Reopen with one. Goal-aware corpus labels belong to the open E10 protocol work. Nearest so far: 13 Going on 30's mix tilts about −7 dB/oct above 50 Hz (bass is coherent across channels, ~200 Hz is not) and finds its plateau only through the one-sided fallback. That reference reads a 36 dB deficit below 25 Hz; each channel's own plateau reads 30-34 dB. A reference built from the channels would be 2-5 dB less generous there (see plans/done-design-and-pipeline.md, 11 October 2026). |
37
+ | 18 | Mixed-channel policy | parked | `channel_scope` is currently an unpopulated optional response field, not a computed `mixed` finding awaiting a gate. Reopen with a case where existing per-channel restoration and common pricing produce an unsuitable mix correction. |
38
+ | 19 | Fraction dial (former TODO 7) | parked | No user/title has needed partial licensed restoration as a preference; tilt and tolerance already ship. Reopen with a concrete requested outcome, then price and validate it like every target. |
39
+ | 20 | Protective corner (former TODO 9) | parked | The corner is already derived from boost cap and implied order, floored by `lowest_frequency_hz`. No case requires an independent override or different fallback alignment; reopen with one. |
40
+ | 21 | Device configuration on the wire | parked | The current contract deliberately omits device preference; server flags already configure realisation. Reopen if one server must serve different device formats/rates per request; then agree an upstream extension. Hardware validity remains F2. |
41
+ | 22 | Catalogue comparison (former TODO 8) | parked | A disagreement detector has no demonstrated accuracy benefit. Reopen with an independent discrepancy to investigate; authored filters remain comparisons, never calibration targets. |
42
+ | 23 | Performance | parked | Further reuse changes cost, not filter accuracy. Reopen after measuring repeated numerical identities or material remaining analysis cost; missed-fit search belongs to F3/F4 instead. |
43
+ | 24 | Chart peak bias | parked | Deliberate catalogue-compatible per-bin maximum; it affects presentation only. Reopen if a reader needs an alternative percentile view; keep it distinct from the 95th-percentile evidence envelope. |
44
+ | 25 | Documentation citations | open | Remove or repoint orphan references and stale descriptions of shipped behaviour. Low filter impact, but prevents future changes being based on obsolete assumptions. |
45
+ | 26 | Texture yardstick retune (library review) | parked | The cubic (row 2) keeps the 200 Hz band only because the sub-band cap admitted one synthetic negative, `broadband/5`: chance unevenness in noise bursts, at a 2.6 dB boost. With 45 gated cases a single case moves the gate's upper bound from 0.079 to 0.118. Carrie is still declined (0.85x about a cubic, 1.33x with the cap). Reopen when the beqdesigner library review (~1,290 titles) is complete. Then retune against its declines: the band edge, the trend degree, and whether the gate weights a false acceptance by its boost. The library holds no known negative, so it can expose wrong declines but not measure false acceptances. Any retune still needs the corpus or a fresh E10 set for that, ideally with negatives built from real programme texture (row 5). See [the outcome](plans/texture-yardstick-sub-band.md#outcome-17-october-2026). |
46
+ | 27 | Parameter-space optimisation for several profiles | open | beqdesigner's main window will let a filter designer optimise a filter for chosen device profiles before publishing, in freq/Q/gain terms (a BEQ is published and loaded as parameters, so no catalogue schema change), minimising the worst error across all chosen profiles at once. The search belongs in `beqoptimiser`, beside `optimise()`; beqdesigner builds the interface. See [the request](plans/parameter-space-optimisation.md). |
44
47
 
45
48
  ## Review evidence
46
49
 
@@ -1,6 +1,6 @@
1
1
  """Shared filter primitives; no workflow package dependencies."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
4
4
 
5
5
  from .types import BiquadSpec as BiquadSpec
6
6
  from .types import BiquadType as BiquadType
@@ -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. 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`. |
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 a 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. A worker designs one title at a time; `--workers N` (1-8) runs N of them, each request taking the next idle one, and `/health` reports N as `parallel` so a caller can size its sends from the designer. With N > 1 each worker's fit pool is `filters.FIT_WORKERS // N` (`fit_share`) unless `BEQ_FIT_WORKERS` is set: pool size changes timing, never an answer. A worker that dies answers 500 and is replaced; the others carry on. 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`, `shared_root` and `parallel`. |
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. |
@@ -333,7 +333,11 @@ least `min_judge_octaves` above the floor — no fixed top (historical review T6
333
333
  (`pipeline.passband_ripple_db`) is left alone: it abstains with `within_programme_ripple`
334
334
  before any strategy runs. That ripple is the title's own yardstick, so there is no constant.
335
335
  Crest to trough rather than a one-sided dip, because the reference sits on the crests. This
336
- is what took the negative corpus from 4 false acceptances in 45 to 0.
336
+ is what took the negative corpus from 4 false acceptances in 45 to 0. The mix's ripple is
337
+ taken about a cubic in log frequency, so an LFE-led mix's knee above the crossover is trend,
338
+ not texture; each channel's is taken about a line. Don't shorten the band instead:
339
+ crest to trough shrinks with bin count, and the 1x boundary was calibrated on the full band
340
+ ([plans/texture-yardstick-sub-band.md](../plans/texture-yardstick-sub-band.md)).
337
341
  * **Steep filters: the default declines them; `judge_from_content_edge` is an opt-in that
338
342
  recovers them and lifts some noise.** Below a steep filter's edge the priced target already
339
343
  falls into the quiet floor with the programme — contrast bounds it — and the shape clauses
@@ -772,6 +772,9 @@ MAINS_HZ = (50.0, 60.0)
772
772
  # inside the 50 Hz spacing so it never reaches the next harmonic
773
773
  HUM_NOTCH_HALF_WIDTH_HZ = 2.5
774
774
  HUM_SURROUND_HALF_WIDTH_HZ = 10.0
775
+ # the mix's texture is measured about a cubic in log frequency, so an LFE-led mix's knee above
776
+ # the crossover is trend, not ripple (plans/texture-yardstick-sub-band.md)
777
+ MIX_TEXTURE_TREND_DEGREE = 3
775
778
 
776
779
 
777
780
  def mains_hum_notches(
@@ -815,7 +818,11 @@ def mains_hum_notches(
815
818
 
816
819
 
817
820
  def passband_ripple_db(
818
- material: Material, params: "PipelineParams", noise_floor_hz: float = math.nan
821
+ material: Material,
822
+ params: "PipelineParams",
823
+ noise_floor_hz: float = math.nan,
824
+ *,
825
+ trend_degree: int = 1,
819
826
  ) -> tuple[float, tuple[float, float]] | None:
820
827
  """The programme's own texture where nothing is missing: crest-to-trough, dB, and where.
821
828
 
@@ -823,6 +830,17 @@ def passband_ripple_db(
823
830
  correction — the top of the judged band to the top of the analysis band — as the spread of
824
831
  the curve about its own log-frequency trend, so a tilted passband is not read as ripple.
825
832
 
833
+ The trend is a polynomial of `trend_degree` in log frequency. The mix's texture blocker
834
+ asks for a cubic (`MIX_TEXTURE_TREND_DEGREE`). In an LFE-led mix the level falls away
835
+ where the LFE channel's content ends (60-100 Hz), a knee a straight line cannot remove, so
836
+ it read as ripple and withheld a real deficit: 9 (2009) measured 8.8 dB of "ripple" against
837
+ a 6.2 dB shortfall, 2.2 dB about a cubic. The band is not shortened instead. Crest to
838
+ trough shrinks with the number of bins even on noise, so ending it at the crossover lowered
839
+ every yardstick and admitted a broadband negative on the corpus. A cubic keeps the band the
840
+ 1x boundary was calibrated on and leaves the negatives where they were: at most 0.76x about
841
+ a cubic against 0.77x about a line, with every real and filtered title at 1.08x and up.
842
+ Each channel's own rule (`channels_missing_low_end`) keeps the line.
843
+
826
844
  Crest to trough, not a one-sided dip, because the reference sits near the 90th percentile
827
845
  of the spectrum, on the crests: a shortfall measured from it spans the whole swing. On the
828
846
  negative corpus (IMPROVEMENT_PLAN E2) every unfiltered title's low-end deficit came in
@@ -847,7 +865,7 @@ def passband_ripple_db(
847
865
  return None
848
866
  smooth = smooth_unexcluded(response, freqs, bands, 15, levels=True)[band]
849
867
  octaves = np.log2(freqs[band])
850
- about = smooth - np.polyval(np.polyfit(octaves, smooth, 1), octaves)
868
+ about = smooth - np.polyval(np.polyfit(octaves, smooth, trend_degree), octaves)
851
869
  return float(np.max(about) - np.min(about)), (low, high)
852
870
 
853
871
 
@@ -1550,7 +1568,12 @@ def analyse(
1550
1568
  f"programme (tracking stops at {diagnosis.noise_floor_hz:.1f} Hz), so there "
1551
1569
  "is no bass passband to restore towards; restoration withheld"
1552
1570
  )
1553
- texture = passband_ripple_db(material, params, diagnosis.noise_floor_hz)
1571
+ texture = passband_ripple_db(
1572
+ material,
1573
+ params,
1574
+ diagnosis.noise_floor_hz,
1575
+ trend_degree=MIX_TEXTURE_TREND_DEGREE,
1576
+ )
1554
1577
  low_end = low_end_deficit_db(material, params, diagnosis.noise_floor_hz)
1555
1578
  if texture is not None and low_end is not None:
1556
1579
  ripple, (edge_low, edge_high) = texture
@@ -0,0 +1,33 @@
1
+ @echo off
2
+ rem Runs the beqforge designer for beqdesigner on Windows: `beqforge serve-designer`, with the
3
+ rem container image's defaults (README, "Running the designer on Windows"). Keep this file
4
+ rem beside the release's beqforge-windows-x86_64.exe and double-click it, or run it from a
5
+ rem prompt with any extra serve-designer options, e.g. `beqforge-designer.cmd --record-dir D:\beq\records`.
6
+ rem
7
+ rem Every BEQFORGE_* variable already set in the environment wins over the defaults below.
8
+ setlocal
9
+
10
+ rem beqdesigner's work directory, as this machine sees it, so audio arrives by reference rather
11
+ rem than inline. Uncomment and point it at the folder mounted into beqdesigner's container.
12
+ rem if not defined BEQFORGE_SHARED_ROOT set "BEQFORGE_SHARED_ROOT=D:\beq\work"
13
+
14
+ rem How many titles to design at once, if this machine can take more than one (0.2.4 on).
15
+ rem if not defined BEQFORGE_WORKERS set "BEQFORGE_WORKERS=2"
16
+
17
+ if not defined BEQFORGE_HOST set "BEQFORGE_HOST=0.0.0.0"
18
+ if not defined BEQFORGE_PORT set "BEQFORGE_PORT=8420"
19
+ if not defined BEQFORGE_CACHE_DIR set "BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache"
20
+
21
+ set "EXE=%~dp0beqforge-windows-x86_64.exe"
22
+ if not exist "%EXE%" set "EXE=%~dp0beqforge.exe"
23
+ if not exist "%EXE%" (
24
+ echo beqforge-windows-x86_64.exe was not found beside %~nx0
25
+ pause
26
+ exit /b 1
27
+ )
28
+
29
+ "%EXE%" serve-designer %*
30
+ set "STATUS=%ERRORLEVEL%"
31
+ rem keep the window open on a startup error, so a double-click shows why
32
+ if not "%STATUS%"=="0" pause
33
+ exit /b %STATUS%
@@ -748,6 +748,37 @@ 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
+ ## Several designs at once, advertised on `/health` (18 October 2026)
752
+
753
+ A Windows machine that could design two titles at once was added beside the container as a
754
+ second endpoint, and the only way to use it was two servers on two ports: one server had a
755
+ single design worker. Saying how many each endpoint takes in beqdesigner's profile also put a
756
+ fact about the designer's host in the caller's configuration.
757
+
758
+ `serve-designer --workers N` (`BEQFORGE_WORKERS`, 1-8, the range beqdesigner accepts for an
759
+ endpoint's `parallel`) now runs N `_DesignWorker`s, each still one spawned, single-threaded
760
+ process. A request takes the next idle worker and waits for one when none is, so arrival order
761
+ is kept and a worker that dies fails only its own request. `/health` gains `"parallel": N` for
762
+ a caller to size its sends from; beqdesigner was sent the caller-side proposal. With N > 1 each
763
+ worker's fit pool is `filters.FIT_WORKERS // N` (`fit_share`), so N designs do not each claim
764
+ the whole machine; an explicit `BEQ_FIT_WORKERS` is left as each design's own. `_run_fits`
765
+ returns results in submission order from fixed seeds, so pool size moves timing and never an
766
+ answer. Nothing under `beqforge/` changed, so the decision regression probe does not apply.
767
+
768
+ * **Checks.** New tests: with two workers a quick request is answered while a real design is
769
+ still running on the other (the same test fails with one worker, so it distinguishes them),
770
+ `/health` reports `parallel`, a count outside 1-8 is refused by the server and the CLI, the
771
+ option comes from the environment, and the fit share. Server, startup, image and parallel-fit
772
+ tests: 61 passed. `tools/smoke_test_exe.py` now starts the executable with `--workers 2` and
773
+ requires `parallel: 2`. Its three requests alternate between the workers, so both design for
774
+ real inside the frozen build and the cache hit is one worker reusing the other's entries. A
775
+ Linux PyInstaller build passed it, with no process left after shutdown. Windows and macOS
776
+ are covered by the release workflow's smoke test, not run here.
777
+ * **Windows packaging.** `packaging/designer/beqforge-designer.cmd` starts the release's
778
+ Windows executable with the container's defaults and is attached to each release; the README
779
+ gains "Running the designer on Windows".
780
+
781
+
751
782
  ## The designer server answers `/health` during a design (17 October 2026)
752
783
 
753
784
  A deployed image went unhealthy while it designed correctly. `serve-designer` was a plain
@@ -0,0 +1,75 @@
1
+ # Optimising a filter's parameters for several device profiles at once
2
+
3
+ **Status: design, not built.** A request from beqdesigner (October 2026), for a feature in its main window: a filter
4
+ designer picks one or more device profiles, sees what each does to the filter, and asks for the filter to be optimised
5
+ before it is published. The person who owns both projects decided that the search belongs here, in `beqoptimiser`, and
6
+ beqdesigner only provides the interface. Parameter search is already listed as a follow-up in
7
+ [F2-catalogue-filter-optimisation.md](F2-catalogue-filter-optimisation.md).
8
+
9
+ ## What is different from `optimise()`
10
+
11
+ `optimise()` searches the coefficients of one cascade for one rate and precision, and returns custom SOS. This request
12
+ needs:
13
+
14
+ 1. **Parameters out, not coefficients.** The result is each section's `freq`, `q` and `gain` (and its type unchanged).
15
+ A BEQ is published and loaded as parameters: beqdesigner's projects and exports, the catalogue, and ezbeq (which
16
+ rebuilds RBJ coefficients from the parameters at each device's rate when it has none published for it) all carry
17
+ parameters. Optimised parameters therefore need no new catalogue schema and reach every consumer as they are.
18
+ 2. **Several profiles jointly.** The same parameters are realised on every device. The search minimises the worst
19
+ matching-band error across all the profiles the caller passes (a minimax), so a change that helps one device cannot
20
+ quietly make another worse. The result reports each profile's error before and after.
21
+ 3. **Representable parameters.** Only values beqdesigner can show and the exports keep: `freq` to 0.01 Hz, `q` (and a
22
+ shelf's `S`) to 0.0001, `gain` to 0.01 dB. Search on that grid, or round and re-validate, so the result is exactly
23
+ what is published.
24
+
25
+ ## Proposed interface
26
+
27
+ ```python
28
+ optimise_parameters(
29
+ sections, # the cascade as authored: beqoptimiser.core.Section (type, freq, q, gain), in order
30
+ *,
31
+ profiles, # [(rate, precision), ...]: 48000 or 96000, with Float32() or FixedPoint(i, f)
32
+ settings=None, # Settings: margin, band, plus the parameter bounds below
33
+ ) -> ParameterResult
34
+ ```
35
+
36
+ `ParameterResult` carries the outcome (with the same `replacement`/`improvement`/`no_replacement` rules as
37
+ `optimise()`), the new sections when there is one, and per profile: the original error, the candidate's error and its
38
+ guard (out-of-band) error, with the numerical version and settings for provenance.
39
+
40
+ * **Reference:** each section's ideal float64 RBJ response, from the authored parameters, as now.
41
+ * **Realised:** for each profile, the coefficients computed from the candidate parameters at that rate in float64,
42
+ then stored with that profile's precision (`quantise`), as a device that receives parameters (or coefficients
43
+ computed from them) would hold them.
44
+ * **Bounds:** the parameters may only move as far as keeps the filter the author's: proposed defaults of ±1% in
45
+ frequency, ±2% in Q and ±0.1 dB in gain per section, configurable. A result outside them is not a correction of
46
+ the same filter.
47
+ * **Sections it cannot change:** the request may contain other biquads (pass filters, a gain stage). They stay
48
+ fixed but count in the cascade's response; a cascade with nothing searchable returns `no_replacement`.
49
+
50
+ ## Open points for this repository to decide
51
+
52
+ * **Whose RBJ:** the coefficients must be computed as the consumers compute them. beqdesigner (`model/iir.py`),
53
+ `beq_common.biquad` and ezbeq each implement the cookbook; check that they agree, including the shelf Q/S conversion,
54
+ to the precision a profile can resolve. If they do not, name the one a profile models.
55
+ * **Search method:** a coordinate or pattern search on the parameter grid, a scalar minimiser per parameter as now, or
56
+ something else; deterministic, bounded and cacheable as `optimise()` is.
57
+ * **Whether it ever helps:** benchmark against direct-coefficient search on the catalogue's float32/96 kHz worst cases
58
+ (e.g. the 5 Hz Q 6 peak the 2x4 HD pilot measured). Parameter search may recover little where the coefficient grid is
59
+ coarse; the outcome rules already report that honestly as `no_replacement`.
60
+
61
+ ## What beqdesigner will build around it
62
+
63
+ * Device profiles: one per device it exports to (miniDSP HD-class: float32 at 96 kHz; earlier miniDSPs: Q5.23 fixed
64
+ point at 48 kHz; HTP-1 and JRiver compute in double precision, so nothing to show), plus profiles a user defines
65
+ (rate and storage). It uses `beqoptimiser`'s `Float32`/`FixedPoint` and `magnitude` for part (a).
66
+ * Part (a), now: the main chart shows the filter as each chosen profile realises it, and its error against the ideal.
67
+ * Part (b), after this ships: an *Optimise for devices* action on the filter, which calls `optimise_parameters` with the
68
+ ticked profiles, shows the before/after errors per profile and lets the designer accept the new parameters.
69
+
70
+ ## Completion criteria
71
+
72
+ * `optimise_parameters` in `beqoptimiser` with tests covering: both rates, both precisions, the joint (minimax)
73
+ objective protecting each profile, the parameter grid and bounds, fixed non-searchable sections, and cache identity.
74
+ * A short benchmark against `optimise()` on the catalogue report's worst float32 cases, recorded here.
75
+ * Released in a `beqforge` version beqdesigner can pin.