beqforge 0.2.0__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.
Files changed (198) hide show
  1. beqforge-0.2.2/.dockerignore +14 -0
  2. beqforge-0.2.2/.github/workflows/build-designer-image.yml +119 -0
  3. {beqforge-0.2.0 → beqforge-0.2.2}/.github/workflows/build-device-check.yml +16 -1
  4. {beqforge-0.2.0 → beqforge-0.2.2}/.github/workflows/build-executable.yml +5 -3
  5. beqforge-0.2.2/.github/workflows/tests.yml +140 -0
  6. {beqforge-0.2.0 → beqforge-0.2.2}/AGENTS.md +7 -2
  7. {beqforge-0.2.0 → beqforge-0.2.2}/PKG-INFO +37 -1
  8. {beqforge-0.2.0 → beqforge-0.2.2}/README.md +36 -0
  9. {beqforge-0.2.0 → beqforge-0.2.2}/TODO.md +1 -1
  10. {beqforge-0.2.0 → beqforge-0.2.2}/beq_common/__init__.py +1 -1
  11. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/AGENTS.md +1 -1
  12. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/diagnose.py +83 -23
  13. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/pipeline.py +54 -2
  14. {beqforge-0.2.0 → beqforge-0.2.2}/beqoptimiser/AGENTS.md +4 -2
  15. {beqforge-0.2.0 → beqforge-0.2.2}/beqoptimiser/cache.py +12 -6
  16. {beqforge-0.2.0 → beqforge-0.2.2}/beqoptimiser/core.py +7 -3
  17. {beqforge-0.2.0 → beqforge-0.2.2}/beqoptimiser/data/seed-manifest.json +17 -13
  18. {beqforge-0.2.0 → beqforge-0.2.2}/beqoptimiser/data/seed.json.gz +0 -0
  19. {beqforge-0.2.0 → beqforge-0.2.2}/docs/optimiser.md +9 -5
  20. beqforge-0.2.2/packaging/designer/Dockerfile +42 -0
  21. beqforge-0.2.2/packaging/designer/smoke.py +187 -0
  22. {beqforge-0.2.0 → beqforge-0.2.2}/plans/F2-catalogue-filter-optimisation.md +20 -0
  23. {beqforge-0.2.0 → beqforge-0.2.2}/plans/done-design-and-pipeline.md +151 -0
  24. {beqforge-0.2.0 → beqforge-0.2.2}/pyproject.toml +18 -1
  25. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_channel_evidence.py +3 -1
  26. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_designer_server.py +81 -48
  27. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_diagnose.py +30 -0
  28. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_exclusions.py +15 -0
  29. beqforge-0.2.2/tests/test_design_hum.py +63 -0
  30. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_references.py +35 -0
  31. beqforge-0.2.2/tests/test_designer_image.py +89 -0
  32. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_designer_server_startup.py +17 -18
  33. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_optimiser_cache.py +21 -0
  34. {beqforge-0.2.0 → beqforge-0.2.2}/tools/designer_server.py +268 -59
  35. {beqforge-0.2.0 → beqforge-0.2.2}/uv.lock +160 -76
  36. beqforge-0.2.0/.github/workflows/tests.yml +0 -101
  37. {beqforge-0.2.0 → beqforge-0.2.2}/.github/dependabot.yml +0 -0
  38. {beqforge-0.2.0 → beqforge-0.2.2}/.github/workflows/dependabot-auto-merge.yaml +0 -0
  39. {beqforge-0.2.0 → beqforge-0.2.2}/.gitignore +0 -0
  40. {beqforge-0.2.0 → beqforge-0.2.2}/.idea/.gitignore +0 -0
  41. {beqforge-0.2.0 → beqforge-0.2.2}/.idea/beqforge.iml +0 -0
  42. {beqforge-0.2.0 → beqforge-0.2.2}/.idea/editor.xml +0 -0
  43. {beqforge-0.2.0 → beqforge-0.2.2}/.idea/jsonSchemas.xml +0 -0
  44. {beqforge-0.2.0 → beqforge-0.2.2}/.idea/misc.xml +0 -0
  45. {beqforge-0.2.0 → beqforge-0.2.2}/.idea/modules.xml +0 -0
  46. {beqforge-0.2.0 → beqforge-0.2.2}/.idea/vcs.xml +0 -0
  47. {beqforge-0.2.0 → beqforge-0.2.2}/CLAUDE.md +0 -0
  48. {beqforge-0.2.0 → beqforge-0.2.2}/LICENCE.md +0 -0
  49. {beqforge-0.2.0 → beqforge-0.2.2}/beq_common/AGENTS.md +0 -0
  50. {beqforge-0.2.0 → beqforge-0.2.2}/beq_common/biquad.py +0 -0
  51. {beqforge-0.2.0 → beqforge-0.2.2}/beq_common/provenance.py +0 -0
  52. {beqforge-0.2.0 → beqforge-0.2.2}/beq_common/publication.py +0 -0
  53. {beqforge-0.2.0 → beqforge-0.2.2}/beq_common/types.py +0 -0
  54. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/__init__.py +0 -0
  55. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/accept.py +0 -0
  56. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/beqd.py +0 -0
  57. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/biquad.py +0 -0
  58. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/cache.py +0 -0
  59. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/charts.py +0 -0
  60. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/cli.py +0 -0
  61. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/design.py +0 -0
  62. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/designer.py +0 -0
  63. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/explain.py +0 -0
  64. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/extraction.py +0 -0
  65. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/filters.py +0 -0
  66. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/harness.py +0 -0
  67. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/identify.py +0 -0
  68. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/material.py +0 -0
  69. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/record.py +0 -0
  70. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/reference.py +0 -0
  71. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/rolloff.py +0 -0
  72. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge/verify.py +0 -0
  73. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge-device-check.spec +0 -0
  74. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge.spec +0 -0
  75. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/AGENTS.md +0 -0
  76. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/__init__.py +0 -0
  77. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/analyse.py +0 -0
  78. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/audio.py +0 -0
  79. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/catalogue.py +0 -0
  80. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/cli.py +0 -0
  81. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/coefficients.py +0 -0
  82. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/engines.py +0 -0
  83. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/evidence.py +0 -0
  84. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/manifest.py +0 -0
  85. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/measurement.py +0 -0
  86. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/profiles.py +0 -0
  87. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/qualification.py +0 -0
  88. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/report.py +0 -0
  89. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/transactions.py +0 -0
  90. {beqforge-0.2.0 → beqforge-0.2.2}/beqforge_device_check/verify.py +0 -0
  91. {beqforge-0.2.0 → beqforge-0.2.2}/beqoptimiser/__init__.py +0 -0
  92. {beqforge-0.2.0 → beqforge-0.2.2}/beqoptimiser/cli.py +0 -0
  93. {beqforge-0.2.0 → beqforge-0.2.2}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
  94. {beqforge-0.2.0 → beqforge-0.2.2}/docs/device-check-examples/simulation.json +0 -0
  95. {beqforge-0.2.0 → beqforge-0.2.2}/docs/device-check.md +0 -0
  96. {beqforge-0.2.0 → beqforge-0.2.2}/docs/optimiser-report/README.md +0 -0
  97. {beqforge-0.2.0 → beqforge-0.2.2}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
  98. {beqforge-0.2.0 → beqforge-0.2.2}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
  99. {beqforge-0.2.0 → beqforge-0.2.2}/docs/optimiser-report/assets/example-1.png +0 -0
  100. {beqforge-0.2.0 → beqforge-0.2.2}/docs/optimiser-report/assets/example-2.png +0 -0
  101. {beqforge-0.2.0 → beqforge-0.2.2}/docs/optimiser-report/entry-results.json.gz +0 -0
  102. {beqforge-0.2.0 → beqforge-0.2.2}/docs/optimiser-report/statistics.json +0 -0
  103. {beqforge-0.2.0 → beqforge-0.2.2}/evidence_validation.json +0 -0
  104. {beqforge-0.2.0 → beqforge-0.2.2}/negative_corpus.json +0 -0
  105. {beqforge-0.2.0 → beqforge-0.2.2}/packaging/device-check/NOTICE.txt +0 -0
  106. {beqforge-0.2.0 → beqforge-0.2.2}/plans/F2-device-precision-validation.md +0 -0
  107. {beqforge-0.2.0 → beqforge-0.2.2}/plans/F2-package-consolidation.md +0 -0
  108. {beqforge-0.2.0 → beqforge-0.2.2}/plans/R2a-server-stage-cache.md +0 -0
  109. {beqforge-0.2.0 → beqforge-0.2.2}/plans/done-baseline-evidence.md +0 -0
  110. {beqforge-0.2.0 → beqforge-0.2.2}/plans/research-design-decisions.md +0 -0
  111. {beqforge-0.2.0 → beqforge-0.2.2}/plans/test-portability-and-runtime.md +0 -0
  112. {beqforge-0.2.0 → beqforge-0.2.2}/tests/__init__.py +0 -0
  113. {beqforge-0.2.0 → beqforge-0.2.2}/tests/conftest.py +0 -0
  114. {beqforge-0.2.0 → beqforge-0.2.2}/tests/fixtures/optimiser_seed_entry.json +0 -0
  115. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_cli.py +0 -0
  116. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_accept.py +0 -0
  117. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_cache.py +0 -0
  118. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_cache_stores.py +0 -0
  119. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_charts.py +0 -0
  120. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_corpus.py +0 -0
  121. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_design.py +0 -0
  122. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_designer.py +0 -0
  123. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_device_rate.py +0 -0
  124. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_evidence.py +0 -0
  125. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_evidence_excess.py +0 -0
  126. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_explain.py +0 -0
  127. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_extraction.py +0 -0
  128. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_filters.py +0 -0
  129. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_frozen_cache.py +0 -0
  130. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_guard.py +0 -0
  131. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_harness.py +0 -0
  132. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_headroom.py +0 -0
  133. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_identify.py +0 -0
  134. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_jacobian.py +0 -0
  135. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_parallel.py +0 -0
  136. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_pipeline.py +0 -0
  137. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_playback.py +0 -0
  138. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_playback_headroom.py +0 -0
  139. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_publication.py +0 -0
  140. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_record.py +0 -0
  141. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_record_fingerprint.py +0 -0
  142. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_reference.py +0 -0
  143. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_rolloff.py +0 -0
  144. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_strategy_config.py +0 -0
  145. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_timings.py +0 -0
  146. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_design_verify.py +0 -0
  147. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_analyse.py +0 -0
  148. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_catalogue.py +0 -0
  149. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_catalogue_predict.py +0 -0
  150. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_cli.py +0 -0
  151. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_engines.py +0 -0
  152. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_evidence.py +0 -0
  153. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_examples.py +0 -0
  154. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_manifest.py +0 -0
  155. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_measurement.py +0 -0
  156. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_qualification.py +0 -0
  157. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_transactions.py +0 -0
  158. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_device_check_verify.py +0 -0
  159. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_extract_layout.py +0 -0
  160. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_optimiser.py +0 -0
  161. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_optimiser_report.py +0 -0
  162. {beqforge-0.2.0 → beqforge-0.2.2}/tests/test_package_boundaries.py +0 -0
  163. {beqforge-0.2.0 → beqforge-0.2.2}/tools/__init__.py +0 -0
  164. {beqforge-0.2.0 → beqforge-0.2.2}/tools/build_optimiser_seed.py +0 -0
  165. {beqforge-0.2.0 → beqforge-0.2.2}/tools/design_beq.py +0 -0
  166. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/a1_sweep.py +0 -0
  167. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/compare_records.py +0 -0
  168. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/compare_verdicts.py +0 -0
  169. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/inject_variants.py +0 -0
  170. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p14_compare.py +0 -0
  171. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p14_jacobian.py +0 -0
  172. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p14_run_pipeline.py +0 -0
  173. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p14_surrogate.py +0 -0
  174. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p18_compare_greedy.py +0 -0
  175. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p18_compare_seeded.py +0 -0
  176. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p18_compare_structure.py +0 -0
  177. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p18_greedy.py +0 -0
  178. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p18_seeded.py +0 -0
  179. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p18_structure.py +0 -0
  180. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/p20_smooth_drift.py +0 -0
  181. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/probe.py +0 -0
  182. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/refit_probe.py +0 -0
  183. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/request_timing.py +0 -0
  184. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/score_injected.py +0 -0
  185. {beqforge-0.2.0 → beqforge-0.2.2}/tools/experiments/t8_floor_rules.py +0 -0
  186. {beqforge-0.2.0 → beqforge-0.2.2}/tools/extract.py +0 -0
  187. {beqforge-0.2.0 → beqforge-0.2.2}/tools/ledger_template.html +0 -0
  188. {beqforge-0.2.0 → beqforge-0.2.2}/tools/negative_corpus.py +0 -0
  189. {beqforge-0.2.0 → beqforge-0.2.2}/tools/optimiser_catalogue_report.py +0 -0
  190. {beqforge-0.2.0 → beqforge-0.2.2}/tools/prepare_device_check_helper.py +0 -0
  191. {beqforge-0.2.0 → beqforge-0.2.2}/tools/render_ledger.py +0 -0
  192. {beqforge-0.2.0 → beqforge-0.2.2}/tools/replay.py +0 -0
  193. {beqforge-0.2.0 → beqforge-0.2.2}/tools/seed_optimiser_cache.py +0 -0
  194. {beqforge-0.2.0 → beqforge-0.2.2}/tools/smoke_test_device_check.py +0 -0
  195. {beqforge-0.2.0 → beqforge-0.2.2}/tools/smoke_test_exe.py +0 -0
  196. {beqforge-0.2.0 → beqforge-0.2.2}/tools/smoke_test_install.py +0 -0
  197. {beqforge-0.2.0 → beqforge-0.2.2}/tools/summarise.py +0 -0
  198. {beqforge-0.2.0 → beqforge-0.2.2}/tools/validate_evidence.py +0 -0
@@ -0,0 +1,14 @@
1
+ .git
2
+ .venv
3
+ **/__pycache__
4
+ build
5
+ dist
6
+ data
7
+ out
8
+ charts
9
+ plans
10
+ docs
11
+ tests
12
+ tools/experiments
13
+ *.run.json.gz
14
+ *.beq
@@ -0,0 +1,119 @@
1
+ name: Designer image
2
+
3
+ # The designer as a container, beside a beqdesigner pipeline service (README, "Running the
4
+ # designer in a container"). Every change to what goes into it is built and smoke-tested;
5
+ # a vX.Y.Z tag also publishes linux/amd64 and linux/arm64 to GHCR as ghcr.io/<owner>/beqforge-designer.
6
+ # A manual run with `publish` publishes the image of an existing release from a later commit,
7
+ # only if the designer's runtime code is identical to that release's tag (as when the image
8
+ # recipe arrived after the release).
9
+ on:
10
+ push:
11
+ branches: [main]
12
+ tags: ['v*']
13
+ paths:
14
+ - 'packaging/designer/**'
15
+ - '.dockerignore'
16
+ - '.github/workflows/build-designer-image.yml'
17
+ - 'pyproject.toml'
18
+ - 'uv.lock'
19
+ - 'beq_common/**'
20
+ - 'beqforge/**'
21
+ - 'tools/designer_server.py'
22
+ pull_request:
23
+ paths:
24
+ - 'packaging/designer/**'
25
+ - '.dockerignore'
26
+ - '.github/workflows/build-designer-image.yml'
27
+ workflow_dispatch:
28
+ inputs:
29
+ publish:
30
+ description: Publish the image as beq_common.__version__ (its vX.Y.Z tag must exist, with identical designer code)
31
+ type: boolean
32
+ default: false
33
+
34
+ permissions:
35
+ contents: read
36
+
37
+ concurrency:
38
+ group: designer-image-${{ github.ref }}
39
+ cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
40
+
41
+ jobs:
42
+ image:
43
+ runs-on: ubuntu-24.04
44
+ timeout-minutes: 60 # the arm64 image builds under emulation
45
+ permissions:
46
+ contents: read
47
+ packages: write
48
+ steps:
49
+ - uses: actions/checkout@v7
50
+ with:
51
+ fetch-depth: 0 # the release tags, for a manual publish's check
52
+ - name: Choose whether to publish
53
+ id: publish
54
+ shell: bash
55
+ run: |
56
+ if [[ "$GITHUB_REF" == refs/tags/* || "${{ inputs.publish }}" == true ]]; then
57
+ echo "publish=true" >> "$GITHUB_OUTPUT"
58
+ else
59
+ echo "publish=false" >> "$GITHUB_OUTPUT"
60
+ fi
61
+ - name: Set image version
62
+ id: version
63
+ shell: bash
64
+ run: |
65
+ version="$(python3 -c "import re, pathlib; print(re.search(r'__version__ = \"([^\"]+)\"', pathlib.Path('beq_common/__init__.py').read_text()).group(1))")"
66
+ if [[ "$GITHUB_REF" == refs/tags/* && "$GITHUB_REF_NAME" != "v$version" ]]; then
67
+ echo "tag $GITHUB_REF_NAME does not match beq_common.__version__ $version" >&2
68
+ exit 1
69
+ fi
70
+ if [[ "$GITHUB_REF" != refs/tags/* && "${{ inputs.publish }}" == true ]]; then
71
+ # the image says it is release $version: what it runs must be exactly that release's
72
+ if ! git diff --quiet "v$version" HEAD -- beqforge beq_common tools/designer_server.py; then
73
+ echo "the designer code differs from v$version: tag a release instead" >&2
74
+ git diff --stat "v$version" HEAD -- beqforge beq_common tools/designer_server.py >&2
75
+ exit 1
76
+ fi
77
+ fi
78
+ echo "version=$version" >> "$GITHUB_OUTPUT"
79
+ - name: Build image for smoke test
80
+ run: |
81
+ docker build -f packaging/designer/Dockerfile -t beqforge-designer:smoke \
82
+ --build-arg VERSION="${{ steps.version.outputs.version }}" --build-arg REVISION="$GITHUB_SHA" .
83
+ - name: Health, and a design inline and by reference
84
+ run: python3 packaging/designer/smoke.py beqforge-designer:smoke
85
+ - uses: docker/setup-qemu-action@v4
86
+ if: steps.publish.outputs.publish == 'true'
87
+ - uses: docker/setup-buildx-action@v4
88
+ if: steps.publish.outputs.publish == 'true'
89
+ - name: Log in to GHCR
90
+ if: steps.publish.outputs.publish == 'true'
91
+ uses: docker/login-action@v4
92
+ with:
93
+ registry: ghcr.io
94
+ username: ${{ github.actor }}
95
+ password: ${{ secrets.GITHUB_TOKEN }}
96
+ - name: Choose tags
97
+ id: tags
98
+ if: steps.publish.outputs.publish == 'true'
99
+ shell: bash
100
+ run: |
101
+ image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/beqforge-designer"
102
+ version="${{ steps.version.outputs.version }}"
103
+ tags="$image:$version"
104
+ if [[ ! "$version" =~ (a|b|rc|dev)[0-9]*$ ]]; then
105
+ tags="$tags,$image:latest"
106
+ fi
107
+ echo "tags=$tags" >> "$GITHUB_OUTPUT"
108
+ - name: Build and publish amd64 and arm64
109
+ if: steps.publish.outputs.publish == 'true'
110
+ uses: docker/build-push-action@v7
111
+ with:
112
+ context: .
113
+ file: packaging/designer/Dockerfile
114
+ platforms: linux/amd64,linux/arm64
115
+ push: true
116
+ tags: ${{ steps.tags.outputs.tags }}
117
+ build-args: |
118
+ VERSION=${{ steps.version.outputs.version }}
119
+ REVISION=${{ github.sha }}
@@ -2,9 +2,19 @@ name: Build device-check preview
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
+ # only when something that affects the packaged executable changes
5
6
  push:
6
7
  branches:
7
8
  - '**'
9
+ paths:
10
+ - 'beqforge_device_check/**'
11
+ - 'beqforge-device-check.spec'
12
+ - 'tools/prepare_device_check_helper.py'
13
+ - 'tools/smoke_test_device_check.py'
14
+ - 'packaging/device-check/**'
15
+ - '.github/workflows/build-device-check.yml'
16
+ - 'pyproject.toml'
17
+ - 'uv.lock'
8
18
  pull_request:
9
19
  paths:
10
20
  - 'beqforge_device_check/**'
@@ -16,6 +26,11 @@ on:
16
26
  - 'pyproject.toml'
17
27
  - 'uv.lock'
18
28
 
29
+ # a newer push to a branch supersedes its in-progress preview build
30
+ concurrency:
31
+ group: device-check-preview-${{ github.event_name }}-${{ github.ref }}
32
+ cancel-in-progress: true
33
+
19
34
  permissions:
20
35
  contents: read
21
36
 
@@ -46,7 +61,7 @@ jobs:
46
61
  executable: beqforge-device-check
47
62
  steps:
48
63
  - uses: actions/checkout@v7
49
- - uses: astral-sh/setup-uv@v10.2.0
64
+ - uses: astral-sh/setup-uv@v10.3.0
50
65
  with:
51
66
  enable-cache: true
52
67
  - name: Install Linux audio and USB libraries
@@ -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@v8
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.2.0
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.2.0
196
+ uses: astral-sh/setup-uv@v10.3.0
195
197
  with:
196
198
  enable-cache: true
197
199
 
@@ -0,0 +1,140 @@
1
+ name: Tests
2
+
3
+ # Fits already control their own process parallelism. Avoid multiplying each worker
4
+ # by a BLAS thread pool on the small hosted runners.
5
+ env:
6
+ OPENBLAS_NUM_THREADS: '1'
7
+ OMP_NUM_THREADS: '1'
8
+ MKL_NUM_THREADS: '1'
9
+ VECLIB_MAXIMUM_THREADS: '1'
10
+
11
+ on:
12
+ workflow_call:
13
+ push:
14
+ branches:
15
+ - '**'
16
+
17
+ # A newer push to a feature branch supersedes its in-progress run. main and tag (release) runs are
18
+ # grouped per commit so they are never cancelled or queued behind each other: a release reuses
19
+ # main's result for the tagged commit.
20
+ concurrency:
21
+ group: tests-${{ github.event_name }}-${{ github.ref }}-${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) && github.sha || 'latest' }}
22
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
23
+
24
+ jobs:
25
+ test:
26
+ name: Test (${{ matrix.os }})
27
+ runs-on: ${{ matrix.os }}
28
+ strategy:
29
+ fail-fast: false
30
+ matrix:
31
+ # feature branch pushes only test on Linux; main, tags (releases) and manual runs test all three
32
+ os: ${{ fromJSON(github.event_name == 'push' && startsWith(github.ref, 'refs/heads/') && github.ref != 'refs/heads/main' && '["ubuntu-latest"]' || '["ubuntu-latest", "macos-latest", "windows-latest"]') }}
33
+ steps:
34
+ - uses: actions/checkout@v7
35
+
36
+ # brew's and choco's default ffmpeg both lack the libsoxr resampler tools/extract.py
37
+ # prefers, unlike Ubuntu's apt package — but extract.py now detects that at runtime and
38
+ # falls back to ffmpeg's default resampler, so any ordinary ffmpeg install is fine here.
39
+ # apt mirrors have repeatedly taken from 8 to over 50 minutes to serve ffmpeg's dependencies on
40
+ # hosted runners, so use a static build from GitHub releases (which also includes libsoxr) and
41
+ # only fall back to apt if that download fails.
42
+ - name: Install ffmpeg (Linux)
43
+ if: runner.os == 'Linux'
44
+ timeout-minutes: 10
45
+ run: |
46
+ if command -v ffmpeg >/dev/null && command -v ffprobe >/dev/null; then
47
+ exit 0
48
+ fi
49
+ url=https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz
50
+ if curl -fsSL --retry 3 --max-time 300 "$url" -o "$RUNNER_TEMP/ffmpeg.tar.xz"; then
51
+ mkdir -p "$RUNNER_TEMP/ffmpeg"
52
+ tar -xJf "$RUNNER_TEMP/ffmpeg.tar.xz" -C "$RUNNER_TEMP/ffmpeg" --strip-components=1
53
+ echo "$RUNNER_TEMP/ffmpeg/bin" >> "$GITHUB_PATH"
54
+ else
55
+ # the Azure mirror is the slowest, use Ubuntu's archive mirror
56
+ sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|http://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt
57
+ sudo apt-get update
58
+ sudo apt-get install -y --no-install-recommends ffmpeg
59
+ fi
60
+
61
+ - name: Install ffmpeg (macOS)
62
+ if: runner.os == 'macOS'
63
+ run: brew install ffmpeg
64
+
65
+ - name: Install ffmpeg (Windows)
66
+ if: runner.os == 'Windows'
67
+ run: choco install ffmpeg -y
68
+
69
+ - name: Install uv
70
+ uses: astral-sh/setup-uv@v10.3.0
71
+ with:
72
+ enable-cache: true
73
+
74
+ - name: Install dependencies
75
+ run: uv sync --locked --all-groups --extra designer
76
+
77
+ # the device-check tests run in their own job below, in parallel, with the extra installed
78
+ - name: Run tests
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 }}
90
+
91
+ device-check:
92
+ name: Device check (${{ matrix.os }})
93
+ runs-on: ${{ matrix.os }}
94
+ strategy:
95
+ fail-fast: false
96
+ matrix:
97
+ # feature branch pushes only test on Linux; main, tags (releases) and manual runs test all three
98
+ os: ${{ fromJSON(github.event_name == 'push' && startsWith(github.ref, 'refs/heads/') && github.ref != 'refs/heads/main' && '["ubuntu-latest"]' || '["ubuntu-latest", "macos-latest", "windows-latest"]') }}
99
+ steps:
100
+ - uses: actions/checkout@v7
101
+
102
+ - name: Install uv
103
+ uses: astral-sh/setup-uv@v10.3.0
104
+ with:
105
+ enable-cache: true
106
+
107
+ # without the extra, most of these tests skip on importorskip('pyfar') rather than fail
108
+ - name: Install dependencies
109
+ run: uv sync --locked --all-groups --extra device-check
110
+
111
+ - name: Run device-check tests
112
+ shell: bash
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 }}
124
+
125
+ package-profiles:
126
+ name: Install profile (${{ matrix.profile }})
127
+ runs-on: ubuntu-latest
128
+ strategy:
129
+ fail-fast: false
130
+ matrix:
131
+ profile: [base, designer, optimiser, device-check, all]
132
+ steps:
133
+ - uses: actions/checkout@v7
134
+ - uses: astral-sh/setup-uv@v10.3.0
135
+ with:
136
+ enable-cache: true
137
+ - name: Build the single distribution
138
+ run: uv build --out-dir dist
139
+ - name: Verify isolated wheel installation and command
140
+ run: uv run --no-project --python 3.13 python tools/smoke_test_install.py --wheel-dir dist --profile '${{ matrix.profile }}'
@@ -55,11 +55,16 @@ importing them; use an isolated worktree for simultaneous experiments.
55
55
 
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
- profiles. `.github/workflows/build-executable.yml` builds release executables and distributions;
58
+ profiles and runs the suite in parallel (`pytest -n auto`, so tests must not share mutable state
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
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;
59
63
  tag pushes publish to PyPI after validation and successful executable builds. Tags must match
60
64
  the shared version exactly (`vX.Y.Z`). Manual runs build artifacts without publishing.
61
65
  PyPI uses the `pypi` environment and Trusted Publishing; setup details are in the README.
62
- Device-check preview executable packaging has its own workflow, not a separate Python release.
66
+ Device-check preview executable packaging has its own workflow, not a separate Python release;
67
+ it only runs when device-check code, packaging or dependencies change.
63
68
 
64
69
  ## Conventions
65
70
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: beqforge
3
- Version: 0.2.0
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
@@ -248,6 +248,42 @@ the same diagnosis, then the build in brackets. `beqforge/designer.py` is the
248
248
  pure `design(request) -> response` adapter, independently testable without a socket;
249
249
  `tools/designer_server.py` is the HTTP transport around it.
250
250
 
251
+ ### Running the designer in a container
252
+
253
+ `ghcr.io/3ll3d00d/beqforge-designer:<version>` runs `beqforge serve-designer` for a beqdesigner
254
+ pipeline service beside it (beqdesigner's `docker/compose.example.yaml` runs both). It is
255
+ published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` on
256
+ each `vX.Y.Z` tag, from the same commit as that release; `latest` follows the newest release.
257
+ A manual run of that workflow with `publish` set publishes the current version's image from a
258
+ later commit, and refuses unless the designer's code is identical to that version's tag (0.2.0's
259
+ image was published this way, the recipe having arrived after the release).
260
+ Pin a version: a catalogue designed over several days should come from one designer build.
261
+
262
+ The image's defaults, all overridable with the environment variables above:
263
+
264
+ | Setting | Default | Mount |
265
+ | --- | --- | --- |
266
+ | `BEQFORGE_HOST`, `BEQFORGE_PORT` | `0.0.0.0`, `8420` | |
267
+ | `BEQFORGE_SHARED_ROOT` | `/work` | the pipeline's work directory, so audio is sent by reference |
268
+ | `BEQFORGE_CACHE_DIR` | `/cache` | a volume, so a restart keeps the stage cache |
269
+ | `BEQFORGE_RECORD_DIR` | unset | set it (for example to `/records`, and mount that) to keep run records |
270
+
271
+ It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
272
+ 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.
276
+
277
+ Build and check it locally from the repository root:
278
+
279
+ ```bash
280
+ docker build -f packaging/designer/Dockerfile -t beqforge-designer .
281
+ python3 packaging/designer/smoke.py beqforge-designer
282
+ ```
283
+
284
+ The smoke script checks `/health` and one design inline and one by reference;
285
+ `tests/test_designer_image.py` runs the same check against an in-process server.
286
+
251
287
  For a machine without Python/`uv`, a standalone `beqforge` executable (every subcommand,
252
288
  including `serve-designer`) is built for Linux/macOS/Windows by
253
289
  `.github/workflows/build-executable.yml` on every `vX.Y.Z` tag, and attached to the matching
@@ -222,6 +222,42 @@ the same diagnosis, then the build in brackets. `beqforge/designer.py` is the
222
222
  pure `design(request) -> response` adapter, independently testable without a socket;
223
223
  `tools/designer_server.py` is the HTTP transport around it.
224
224
 
225
+ ### Running the designer in a container
226
+
227
+ `ghcr.io/3ll3d00d/beqforge-designer:<version>` runs `beqforge serve-designer` for a beqdesigner
228
+ pipeline service beside it (beqdesigner's `docker/compose.example.yaml` runs both). It is
229
+ published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` on
230
+ each `vX.Y.Z` tag, from the same commit as that release; `latest` follows the newest release.
231
+ A manual run of that workflow with `publish` set publishes the current version's image from a
232
+ later commit, and refuses unless the designer's code is identical to that version's tag (0.2.0's
233
+ image was published this way, the recipe having arrived after the release).
234
+ Pin a version: a catalogue designed over several days should come from one designer build.
235
+
236
+ The image's defaults, all overridable with the environment variables above:
237
+
238
+ | Setting | Default | Mount |
239
+ | --- | --- | --- |
240
+ | `BEQFORGE_HOST`, `BEQFORGE_PORT` | `0.0.0.0`, `8420` | |
241
+ | `BEQFORGE_SHARED_ROOT` | `/work` | the pipeline's work directory, so audio is sent by reference |
242
+ | `BEQFORGE_CACHE_DIR` | `/cache` | a volume, so a restart keeps the stage cache |
243
+ | `BEQFORGE_RECORD_DIR` | unset | set it (for example to `/records`, and mount that) to keep run records |
244
+
245
+ It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
246
+ 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.
250
+
251
+ Build and check it locally from the repository root:
252
+
253
+ ```bash
254
+ docker build -f packaging/designer/Dockerfile -t beqforge-designer .
255
+ python3 packaging/designer/smoke.py beqforge-designer
256
+ ```
257
+
258
+ The smoke script checks `/health` and one design inline and one by reference;
259
+ `tests/test_designer_image.py` runs the same check against an in-process server.
260
+
225
261
  For a machine without Python/`uv`, a standalone `beqforge` executable (every subcommand,
226
262
  including `serve-designer`) is built for Linux/macOS/Windows by
227
263
  `.github/workflows/build-executable.yml` on every `vX.Y.Z` tag, and attached to the matching
@@ -32,7 +32,7 @@ are retained so records and code comments remain traceable. Completed items are
32
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
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
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. |
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
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
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
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. |
@@ -1,6 +1,6 @@
1
1
  """Shared filter primitives; no workflow package dependencies."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
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. Single-threaded (`http.server.HTTPServer`, not `ThreadingHTTPServer`) on purpose: 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. Costs nothing real — the contract is one synchronous POST per `design()` call. 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); `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 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. |