vis-agent 0.3.4.dev8055__tar.gz → 0.3.4.dev8058__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 (157) hide show
  1. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/PKG-INFO +1 -1
  2. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/gateway.json +35 -15
  3. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/pyproject.toml +1 -1
  4. vis_agent-0.3.4.dev8058/release-tag +1 -0
  5. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/__init__.py +5 -0
  6. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_client.py +27 -0
  7. vis_agent-0.3.4.dev8058/src/blockether/vis/engine/_speech.py +289 -0
  8. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/compare.py +5 -4
  9. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/extension.py +7 -1
  10. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_api_guides.py +1 -1
  11. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_contracts.py +8 -2
  12. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_compare.py +28 -7
  13. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_stats.py +4 -5
  14. vis_agent-0.3.4.dev8058/tests/test_speech.py +151 -0
  15. vis_agent-0.3.4.dev8055/release-tag +0 -1
  16. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/.gitignore +0 -0
  17. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/LICENSE +0 -0
  18. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/README.md +0 -0
  19. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-arguments.json +0 -0
  20. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-cases.json +0 -0
  21. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-copy.json +0 -0
  22. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-execution.json +0 -0
  23. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-groups.json +0 -0
  24. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-read-session.json +0 -0
  25. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-reads.json +0 -0
  26. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-results.json +0 -0
  27. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-stream.json +0 -0
  28. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-tables.json +0 -0
  29. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity.json +0 -0
  30. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/diff.json +0 -0
  31. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/live-primitives.json +0 -0
  32. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/session-health.json +0 -0
  33. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/view.json +0 -0
  34. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/activity.json +0 -0
  35. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/agents.json +0 -0
  36. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/automations.json +0 -0
  37. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/common.json +0 -0
  38. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/config.json +0 -0
  39. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/content.json +0 -0
  40. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/council.json +0 -0
  41. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/diff.json +0 -0
  42. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/evals.json +0 -0
  43. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/provider.json +0 -0
  44. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/rooms.json +0 -0
  45. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/symbol.json +0 -0
  46. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/toggle.json +0 -0
  47. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/view.json +0 -0
  48. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/examples/greeter/tests/test_greeter.py +0 -0
  49. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/hatch_build.py +0 -0
  50. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/__init__.py +0 -0
  51. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/_contracts.py +0 -0
  52. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/_outside.py +0 -0
  53. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/_wire.py +0 -0
  54. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/activity.py +0 -0
  55. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_agent.py +0 -0
  56. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_agents.py +0 -0
  57. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_council.py +0 -0
  58. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_extensions.py +0 -0
  59. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_host.py +0 -0
  60. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_install.py +0 -0
  61. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_local.py +0 -0
  62. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_models.py +0 -0
  63. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/__init__.py +0 -0
  64. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/_gateway.py +0 -0
  65. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/agent.py +0 -0
  66. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/bench.py +0 -0
  67. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/check.py +0 -0
  68. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/environment.py +0 -0
  69. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/grade.py +0 -0
  70. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/harbor.py +0 -0
  71. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/judge-rubric.toml +0 -0
  72. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/judge.py +0 -0
  73. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/labels.py +0 -0
  74. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/leaderboard.py +0 -0
  75. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/lic.py +0 -0
  76. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/longmemeval.py +0 -0
  77. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/mrcr.py +0 -0
  78. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/repeats.py +0 -0
  79. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/report.py +0 -0
  80. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/reports.py +0 -0
  81. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/rounds.py +0 -0
  82. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/stages.py +0 -0
  83. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/stats.py +0 -0
  84. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/trajectory.py +0 -0
  85. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/extension_package.py +0 -0
  86. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/py.typed +0 -0
  87. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/rooms.py +0 -0
  88. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/views.py +0 -0
  89. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/conftest.py +0 -0
  90. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/evals_atif.py +0 -0
  91. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_activity.py +0 -0
  92. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_agent.py +0 -0
  93. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_agent_extension_lifecycle.py +0 -0
  94. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_agent_extensions.py +0 -0
  95. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_agents.py +0 -0
  96. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_attachments.py +0 -0
  97. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_authoring.py +0 -0
  98. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_catalog.py +0 -0
  99. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_catalog_recipe.py +0 -0
  100. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_ci_report_recipe.py +0 -0
  101. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_client.py +0 -0
  102. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_client_extensions_engine.py +0 -0
  103. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_code_quality_recipe.py +0 -0
  104. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_council.py +0 -0
  105. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_declarations.py +0 -0
  106. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_engine.py +0 -0
  107. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_engine_docstrings.py +0 -0
  108. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_agent.py +0 -0
  109. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_bench.py +0 -0
  110. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_check.py +0 -0
  111. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_environment.py +0 -0
  112. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_gateway.py +0 -0
  113. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_grade.py +0 -0
  114. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_guards.py +0 -0
  115. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_harbor.py +0 -0
  116. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_judge.py +0 -0
  117. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_labels.py +0 -0
  118. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_lic.py +0 -0
  119. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_longmemeval.py +0 -0
  120. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_mrcr.py +0 -0
  121. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_repeats.py +0 -0
  122. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_report.py +0 -0
  123. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_rounds.py +0 -0
  124. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_stages.py +0 -0
  125. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_tasks.py +0 -0
  126. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_trajectory.py +0 -0
  127. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_extension_package.py +0 -0
  128. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_extension_reference.py +0 -0
  129. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_gateway_performance.py +0 -0
  130. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_goals.py +0 -0
  131. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_install.py +0 -0
  132. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_live_primitives.py +0 -0
  133. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_live_wait.py +0 -0
  134. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_local.py +0 -0
  135. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_method_typing.py +0 -0
  136. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_models.py +0 -0
  137. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_monitor_recipe.py +0 -0
  138. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_namespace.py +0 -0
  139. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_outside.py +0 -0
  140. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_providers.py +0 -0
  141. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_read_session.py +0 -0
  142. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_record_arguments.py +0 -0
  143. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_registration.py +0 -0
  144. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_repair_hooks.py +0 -0
  145. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_result_sequence.py +0 -0
  146. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_rooms.py +0 -0
  147. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_scoped_settings.py +0 -0
  148. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_sdk_bootstrap.py +0 -0
  149. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_sdk_guide.py +0 -0
  150. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_sdk_typing.py +0 -0
  151. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_shortcuts_live.py +0 -0
  152. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_structured_live.py +0 -0
  153. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_symbol_contract.py +0 -0
  154. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_testing.py +0 -0
  155. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_text_list_arguments.py +0 -0
  156. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_tools.py +0 -0
  157. {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vis-agent
3
- Version: 0.3.4.dev8055
3
+ Version: 0.3.4.dev8058
4
4
  Summary: The Vis Python SDK — author extensions and control local or remote engines.
5
5
  Project-URL: Homepage, https://vis.blockether.com
6
6
  Project-URL: Documentation, https://vis.blockether.com
@@ -1437,7 +1437,8 @@
1437
1437
  "operations": {
1438
1438
  "post": {
1439
1439
  "request": "json",
1440
- "response": "json"
1440
+ "response": "json",
1441
+ "extension": "background"
1441
1442
  }
1442
1443
  },
1443
1444
  "path": "/v1/speech"
@@ -1447,11 +1448,13 @@
1447
1448
  "operations": {
1448
1449
  "delete": {
1449
1450
  "request": "none",
1450
- "response": "json"
1451
+ "response": "json",
1452
+ "extension": "background"
1451
1453
  },
1452
1454
  "get": {
1453
1455
  "request": "none",
1454
- "response": "json"
1456
+ "response": "json",
1457
+ "extension": "background"
1455
1458
  }
1456
1459
  },
1457
1460
  "path": "/v1/speech/jobs/:job-id"
@@ -1461,7 +1464,8 @@
1461
1464
  "operations": {
1462
1465
  "get": {
1463
1466
  "request": "none",
1464
- "response": "binary"
1467
+ "response": "binary",
1468
+ "extension": "background"
1465
1469
  }
1466
1470
  },
1467
1471
  "path": "/v1/speech/jobs/:job-id/audio"
@@ -1481,11 +1485,13 @@
1481
1485
  "operations": {
1482
1486
  "get": {
1483
1487
  "request": "none",
1484
- "response": "json"
1488
+ "response": "json",
1489
+ "extension": "background"
1485
1490
  },
1486
1491
  "post": {
1487
1492
  "request": "none",
1488
- "response": "json"
1493
+ "response": "json",
1494
+ "extension": "callback"
1489
1495
  }
1490
1496
  },
1491
1497
  "path": "/v1/speech/model"
@@ -1495,11 +1501,13 @@
1495
1501
  "operations": {
1496
1502
  "get": {
1497
1503
  "request": "none",
1498
- "response": "json"
1504
+ "response": "json",
1505
+ "extension": "background"
1499
1506
  },
1500
1507
  "post": {
1501
1508
  "request": "binary",
1502
- "response": "json"
1509
+ "response": "json",
1510
+ "extension": "callback"
1503
1511
  }
1504
1512
  },
1505
1513
  "path": "/v1/speech/voices"
@@ -1509,7 +1517,13 @@
1509
1517
  "operations": {
1510
1518
  "delete": {
1511
1519
  "request": "none",
1512
- "response": "json"
1520
+ "response": "json",
1521
+ "extension": "callback"
1522
+ },
1523
+ "patch": {
1524
+ "request": "json",
1525
+ "response": "json",
1526
+ "extension": "callback"
1513
1527
  }
1514
1528
  },
1515
1529
  "path": "/v1/speech/voices/:voice-id"
@@ -1519,7 +1533,8 @@
1519
1533
  "operations": {
1520
1534
  "get": {
1521
1535
  "request": "none",
1522
- "response": "binary"
1536
+ "response": "binary",
1537
+ "extension": "background"
1523
1538
  }
1524
1539
  },
1525
1540
  "path": "/v1/speech/voices/:voice-id/sample"
@@ -1601,7 +1616,8 @@
1601
1616
  "operations": {
1602
1617
  "post": {
1603
1618
  "request": "binary",
1604
- "response": "json"
1619
+ "response": "json",
1620
+ "extension": "background"
1605
1621
  }
1606
1622
  },
1607
1623
  "path": "/v1/voice"
@@ -1611,11 +1627,13 @@
1611
1627
  "operations": {
1612
1628
  "delete": {
1613
1629
  "request": "none",
1614
- "response": "json"
1630
+ "response": "json",
1631
+ "extension": "background"
1615
1632
  },
1616
1633
  "get": {
1617
1634
  "request": "none",
1618
- "response": "json"
1635
+ "response": "json",
1636
+ "extension": "background"
1619
1637
  }
1620
1638
  },
1621
1639
  "path": "/v1/voice/jobs/:job-id"
@@ -1635,11 +1653,13 @@
1635
1653
  "operations": {
1636
1654
  "get": {
1637
1655
  "request": "none",
1638
- "response": "json"
1656
+ "response": "json",
1657
+ "extension": "background"
1639
1658
  },
1640
1659
  "post": {
1641
1660
  "request": "none",
1642
- "response": "json"
1661
+ "response": "json",
1662
+ "extension": "callback"
1643
1663
  }
1644
1664
  },
1645
1665
  "path": "/v1/voice/model"
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
 
9
9
  [project]
10
10
  name = "vis-agent"
11
- version = "0.3.4.dev8055"
11
+ version = "0.3.4.dev8058"
12
12
  description = "The Vis Python SDK — author extensions and control local or remote engines."
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.12"
@@ -0,0 +1 @@
1
+ beta-e21d3544abdc993020fd27956ece8df3a4736c1e
@@ -36,6 +36,7 @@ from ._council import (
36
36
  from ._install import InstallError, ensure_installed, install
37
37
  from ._local import LocalEngine
38
38
  from ._models import Model, SessionModel
39
+ from ._speech import Speech, SpeechAudio, Transcript, Voice
39
40
 
40
41
  __all__ = [
41
42
  "Agent",
@@ -65,10 +66,14 @@ __all__ = [
65
66
  "Response",
66
67
  "Session",
67
68
  "SessionModel",
69
+ "Speech",
70
+ "SpeechAudio",
68
71
  "StructuredOutputError",
72
+ "Transcript",
69
73
  "TransportError",
70
74
  "Turn",
71
75
  "VisTimeout",
76
+ "Voice",
72
77
  "ensure_installed",
73
78
  "final_answer",
74
79
  "install",
@@ -29,6 +29,7 @@ from blockether.vis.views import InputView, LiveView, ViewEvent
29
29
 
30
30
  if TYPE_CHECKING:
31
31
  from ._agent import ResponseModel
32
+ from ._speech import Speech
32
33
 
33
34
  JSONValue: TypeAlias = (
34
35
  str | int | float | bool | None | list["JSONValue"] | dict[str, "JSONValue"]
@@ -401,6 +402,13 @@ class ExecutionLayer(ABC):
401
402
  except OSError:
402
403
  raise TransportError("gateway response interrupted") from None
403
404
 
405
+ @property
406
+ def speech(self) -> Speech:
407
+ """Return the `Speech` API: voices, synthesis and transcription."""
408
+ from ._speech import Speech
409
+
410
+ return Speech(self)
411
+
404
412
  def session(self, sid: str) -> Session:
405
413
  """Return a lightweight handle for an existing session ID.
406
414
 
@@ -2857,6 +2865,25 @@ class ExecutionLayer(ABC):
2857
2865
  )
2858
2866
  return response.json()
2859
2867
 
2868
+ def patch_speech_voice(
2869
+ self,
2870
+ voice_id: str,
2871
+ *,
2872
+ query: Query | None = None,
2873
+ timeout: float | None = None,
2874
+ body: JSONValue = None,
2875
+ ) -> JSONValue:
2876
+ """PATCH /v1/speech/voices/:voice-id — json response."""
2877
+ response = self._request(
2878
+ "PATCH",
2879
+ "/v1/speech/voices/:voice-id",
2880
+ path={"voice-id": voice_id},
2881
+ query=query,
2882
+ timeout=timeout,
2883
+ body=body,
2884
+ )
2885
+ return response.json()
2886
+
2860
2887
  def get_speech_voice_sample(
2861
2888
  self, voice_id: str, *, query: Query | None = None, timeout: float | None = None
2862
2889
  ) -> Response:
@@ -0,0 +1,289 @@
1
+ """Speech API: voices, synthesis and transcription through the gateway engines.
2
+
3
+ Use `ExecutionLayer.speech` from the SDK, or `vis.speech` inside an extension.
4
+ Both send the same canonical gateway requests.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from ._client import GatewayError, ProtocolError, VisTimeout
15
+
16
+ if TYPE_CHECKING:
17
+ from ._client import ExecutionLayer
18
+
19
+ CLONING_ENGINE = "pocket-tts-local"
20
+ """The engine that learns a voice from a recording."""
21
+
22
+ _UNSET: Any = object()
23
+ _POLL_SECONDS = 0.25
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class Voice:
28
+ """One voice that a speaking engine can use."""
29
+
30
+ id: str
31
+ label: str
32
+ engine: str
33
+ language: str | None = None
34
+ is_imported: bool = False
35
+ clip_text: str | None = None
36
+ license: str | None = None
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class SpeechAudio:
41
+ """Audio that an engine spoke, as the bytes of one file."""
42
+
43
+ content: bytes
44
+ media_type: str
45
+ engine: str | None
46
+ voice: str | None
47
+ seed: int | None
48
+
49
+ def save(self, path: str | Path) -> Path:
50
+ """Write the audio to `path`, create missing parents and return the path."""
51
+ target = Path(path)
52
+ target.parent.mkdir(parents=True, exist_ok=True)
53
+ target.write_bytes(self.content)
54
+ return target
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class Transcript:
59
+ """The text that a transcription engine heard in a recording."""
60
+
61
+ text: str
62
+ engine: str | None
63
+ segments: tuple[dict[str, Any], ...] = ()
64
+
65
+
66
+ def _voice(entry: Any, engine: str) -> Voice:
67
+ if not isinstance(entry, dict) or not isinstance(entry.get("id"), str):
68
+ raise ProtocolError("invalid voice entry")
69
+ return Voice(
70
+ id=entry["id"],
71
+ label=str(entry.get("label") or entry["id"]),
72
+ engine=engine,
73
+ language=entry.get("language"),
74
+ is_imported=bool(entry.get("is_imported")),
75
+ clip_text=entry.get("clip_text"),
76
+ license=entry.get("license"),
77
+ )
78
+
79
+
80
+ def _object(reply: Any, what: str) -> dict[str, Any]:
81
+ """The JSON object of a gateway answer, or ProtocolError for another shape."""
82
+ if not isinstance(reply, dict):
83
+ raise ProtocolError(f"invalid {what} answer")
84
+ return reply
85
+
86
+
87
+ def _audio_bytes(audio: str | Path | bytes) -> bytes:
88
+ if isinstance(audio, bytes):
89
+ return audio
90
+ return Path(audio).expanduser().read_bytes()
91
+
92
+
93
+ def _media_type(headers: dict[str, str]) -> str:
94
+ for key, value in headers.items():
95
+ if key.lower() == "content-type" and value:
96
+ return value.split(";")[0].strip()
97
+ return "audio/wav"
98
+
99
+
100
+ def _engine_query(engine: str | None, **extra: str | None) -> dict[str, str] | None:
101
+ query = {k: v for k, v in {"engine": engine, **extra}.items() if v is not None}
102
+ return query or None
103
+
104
+
105
+ class Speech:
106
+ """Manage voices, speak text and transcribe audio with the gateway engines.
107
+
108
+ Without `engine`, a call uses the engine that the gateway selected. Voice changes
109
+ use the cloning engine `pocket-tts-local` by default. Long work runs as a gateway
110
+ job; these methods wait for it and return the result.
111
+ """
112
+
113
+ def __init__(self, layer: ExecutionLayer):
114
+ self._layer = layer
115
+
116
+ def voices(self, engine: str | None = None) -> list[Voice]:
117
+ """List the voices of `engine`, imported voices included."""
118
+ reply = _object(
119
+ self._layer.get_speech_voices(query=_engine_query(engine)), "voices"
120
+ )
121
+ name = str((reply.get("engine") or {}).get("id") or engine or "")
122
+ return [_voice(entry, name) for entry in reply.get("voices") or []]
123
+
124
+ def add_voice(
125
+ self,
126
+ recording: str | Path | bytes,
127
+ name: str,
128
+ *,
129
+ language: str | None = None,
130
+ transcript: str | None = None,
131
+ engine: str = CLONING_ENGINE,
132
+ ) -> Voice:
133
+ """Import a recording of one speaker as a voice and return it.
134
+
135
+ Use 3 to 30 seconds of clean speech. WAV works everywhere; the gateway converts
136
+ other containers when it can. `transcript` is what the clip says and improves
137
+ the clone. A voice with the same id is replaced, so this also replaces the audio
138
+ of an existing voice. Raises `GatewayError` 400 for an unusable recording and
139
+ 409 for an engine that cannot learn voices.
140
+ """
141
+ if not name or not name.strip():
142
+ raise ValueError("name must not be blank")
143
+ reply = _object(
144
+ self._layer.post_speech_voices(
145
+ content=_audio_bytes(recording),
146
+ query=_engine_query(engine, name=name, lang=language, text=transcript),
147
+ ),
148
+ "voice",
149
+ )
150
+ return _voice(reply.get("voice"), engine)
151
+
152
+ def update_voice(
153
+ self,
154
+ voice_id: str,
155
+ *,
156
+ name: str | None = _UNSET,
157
+ language: str | None = _UNSET,
158
+ transcript: str | None = _UNSET,
159
+ engine: str = CLONING_ENGINE,
160
+ ) -> Voice:
161
+ """Change the label, language or clip transcript of an imported voice.
162
+
163
+ An omitted argument keeps its value; `None` or "" removes it. The id stays the
164
+ same. Raises `GatewayError` 404 when the engine has no such imported voice.
165
+ """
166
+ body: dict[str, Any] = {
167
+ key: value
168
+ for key, value in (
169
+ ("name", name),
170
+ ("language", language),
171
+ ("text", transcript),
172
+ )
173
+ if value is not _UNSET
174
+ }
175
+ if not body:
176
+ raise ValueError("give name, language or transcript")
177
+ reply = _object(
178
+ self._layer.patch_speech_voice(
179
+ voice_id, body=body, query=_engine_query(engine)
180
+ ),
181
+ "voice",
182
+ )
183
+ return _voice(reply.get("voice"), engine)
184
+
185
+ def remove_voice(self, voice_id: str, *, engine: str = CLONING_ENGINE) -> bool:
186
+ """Delete an imported voice. Return False when the engine has no such voice."""
187
+ try:
188
+ self._layer.delete_speech_voice(voice_id, query=_engine_query(engine))
189
+ except GatewayError as error:
190
+ if error.status == 404:
191
+ return False
192
+ raise
193
+ return True
194
+
195
+ def voice_sample(self, voice_id: str, *, engine: str | None = None) -> bytes:
196
+ """Return a short audio sample of a voice. Raises `GatewayError` 404 without one."""
197
+ return self._layer.get_speech_voice_sample(
198
+ voice_id, query=_engine_query(engine)
199
+ ).content
200
+
201
+ def model(self, engine: str | None = None, *, prepare: bool = False) -> dict:
202
+ """Return the readiness of a speaking engine; `prepare=True` starts its download."""
203
+ query = _engine_query(engine)
204
+ if prepare:
205
+ return _object(self._layer.post_speech_model(query=query), "model")
206
+ return _object(self._layer.get_speech_model(query=query), "model")
207
+
208
+ def speak(
209
+ self,
210
+ text: str,
211
+ *,
212
+ voice: str | None = None,
213
+ engine: str | None = None,
214
+ seed: int | None = None,
215
+ timeout: float = 600,
216
+ ) -> SpeechAudio:
217
+ """Speak `text` and return the audio, usually 24 kHz mono WAV.
218
+
219
+ `seed` selects another take of the same text on engines that sample noise,
220
+ such as `vibevoice-local`. The gateway speaks at most 20000 characters at once.
221
+ Raises `GatewayError` 425 while the engine model is not ready and `VisTimeout`
222
+ when the job takes longer than `timeout` seconds.
223
+ """
224
+ if not text or not text.strip():
225
+ raise ValueError("text must not be blank")
226
+ if seed is not None and not (type(seed) is int and 0 <= seed <= 2147483647):
227
+ raise ValueError("seed must be an integer from 0 to 2147483647")
228
+ body: dict[str, Any] = {"text": text}
229
+ if voice is not None:
230
+ body["voice"] = voice
231
+ if seed is not None:
232
+ body["seed"] = seed
233
+ response = self._layer._request(
234
+ "POST", "/v1/speech", body=body, query=_engine_query(engine)
235
+ )
236
+ if response.status == 200:
237
+ return SpeechAudio(
238
+ response.content, _media_type(response.headers), engine, voice, seed
239
+ )
240
+ job = self._wait("speech", response.json(), timeout)
241
+ audio = self._layer.get_speech_job_audio(job["id"])
242
+ return SpeechAudio(
243
+ audio.content,
244
+ _media_type(audio.headers),
245
+ job.get("engine") or engine,
246
+ voice,
247
+ seed,
248
+ )
249
+
250
+ def transcribe(
251
+ self,
252
+ audio: str | Path | bytes,
253
+ *,
254
+ engine: str | None = None,
255
+ timeout: float = 600,
256
+ ) -> Transcript:
257
+ """Transcribe a 16-bit PCM WAV recording and return its text.
258
+
259
+ Engines include `parakeet-local` and `vibevoice-asr-local`. Raises `GatewayError`
260
+ 400 for audio that is not WAV and `VisTimeout` after `timeout` seconds.
261
+ """
262
+ job = self._layer.post_voice(
263
+ content=_audio_bytes(audio), query=_engine_query(engine)
264
+ )
265
+ job = self._wait("voice", job, timeout)
266
+ segments = job.get("segments") or ()
267
+ return Transcript(
268
+ str(job.get("text") or ""),
269
+ job.get("engine") or engine,
270
+ tuple(s for s in segments if isinstance(s, dict)),
271
+ )
272
+
273
+ def _wait(self, direction: str, job: Any, timeout: float) -> dict:
274
+ if not isinstance(job, dict) or not isinstance(job.get("id"), str):
275
+ raise ProtocolError(f"invalid {direction} job")
276
+ get = (
277
+ self._layer.get_speech_job
278
+ if direction == "speech"
279
+ else self._layer.get_voice_job
280
+ )
281
+ deadline = time.monotonic() + timeout
282
+ while not job.get("is_done"):
283
+ if time.monotonic() > deadline:
284
+ raise VisTimeout(f"{direction} job {job['id']} is still running")
285
+ time.sleep(_POLL_SECONDS)
286
+ job = get(job["id"])
287
+ if job.get("phase") == "failed":
288
+ raise RuntimeError(str(job.get("error") or f"{direction} job failed"))
289
+ return job
@@ -122,18 +122,19 @@ def _crossed_caveat(base: tuple[str, ...], other: tuple[str, ...]) -> str:
122
122
  def _margin_caveat(
123
123
  verdict: str, interval: tuple[float, float], margin: float | None
124
124
  ) -> str | None:
125
- if not margin or verdict not in ("better", "worse"):
125
+ if margin is None or verdict not in ("better", "worse"):
126
126
  return None
127
127
  low, high = interval if verdict == "better" else (-interval[1], -interval[0])
128
128
  change = "gain" if verdict == "better" else "loss"
129
129
  if high < margin:
130
130
  return (
131
- f"The {change} is real, but smaller than the margin of {margin:g} points."
131
+ f"The {change} is real, but smaller than the margin of {margin:.12g} "
132
+ "points."
132
133
  )
133
134
  if low < margin:
134
135
  return (
135
136
  f"The {change} is real, but it can be smaller than the margin of "
136
- f"{margin:g} points."
137
+ f"{margin:.12g} points."
137
138
  )
138
139
  return None
139
140
 
@@ -144,7 +145,7 @@ def _verdict(interval: tuple[float, float], margin: float | None) -> str:
144
145
  return "better"
145
146
  if high < 0:
146
147
  return "worse"
147
- if not margin:
148
+ if margin is None:
148
149
  return "no measurable difference"
149
150
  if -margin < low and high < margin:
150
151
  return "equivalent"
@@ -308,8 +308,14 @@ def __getattr__(name: str) -> Any:
308
308
  """Create the API of the bound session through the host on first use.
309
309
 
310
310
  `session` is the `Session` of the bound Vis session, and `council` is its
311
- `Council`. Without a bound session, both raise `RuntimeError`.
311
+ `Council`. Without a bound session, both raise `RuntimeError`. `speech` is the
312
+ `blockether.vis.engine.Speech` API of the gateway: voices, synthesis and
313
+ transcription. A voice change needs an extension callback.
312
314
  """
315
+ if name == "speech":
316
+ from blockether.vis.engine._speech import Speech
317
+
318
+ return Speech(_host_layer())
313
319
  if name in ("session", "council"):
314
320
  sid = _host.bound_session()
315
321
  if not sid:
@@ -93,7 +93,7 @@ _CONCEPTS = sorted(
93
93
 
94
94
  def test_pages_name_gateway_routes_only_on_http_pages():
95
95
  # An empty topic list once turned the parametrized cases below into zero cases.
96
- assert len(_CONCEPTS) == 8, f"merged API pages found: {_CONCEPTS}"
96
+ assert len(_CONCEPTS) == 9, f"merged API pages found: {_CONCEPTS}"
97
97
  for page in _PAGES:
98
98
  if page.name == "http-api.md":
99
99
  continue
@@ -56,10 +56,10 @@ def test_gateway_routes_and_lease_policy_are_shared():
56
56
  assert 0 < lease["touch_ms"] < lease["keepalive_ms"] < lease["ttl_ms"]
57
57
  assert 0 < lease["keepalive_timeout_ms"] < lease["keepalive_ms"]
58
58
  assert len(routes) == 169
59
- assert len(operations) == 205
59
+ assert len(operations) == 206
60
60
  assert Counter(operation["request"] for operation in operations) == {
61
61
  "none": 123,
62
- "json": 76,
62
+ "json": 77,
63
63
  "binary": 6,
64
64
  }
65
65
  assert by_path["/v1/decisions/models"]["operations"]["get"] == {
@@ -93,6 +93,12 @@ def test_gateway_routes_and_lease_policy_are_shared():
93
93
  assert by_path["/v1/speech/voices"]["operations"]["post"] == {
94
94
  "request": "binary",
95
95
  "response": "json",
96
+ "extension": "callback",
97
+ }
98
+ assert by_path["/v1/speech/voices/:voice-id"]["operations"]["patch"] == {
99
+ "request": "json",
100
+ "response": "json",
101
+ "extension": "callback",
96
102
  }
97
103
  assert by_path["/v1/settings"]["operations"]["patch"] == {
98
104
  "request": "json",
@@ -4,6 +4,7 @@ The calibration tests run seeded simulations. They prove the claims of the
4
4
  evaluation guide on false verdicts and power.
5
5
  """
6
6
 
7
+ import math
7
8
  import random
8
9
 
9
10
  import pytest
@@ -161,17 +162,37 @@ def _simulate(rng, tasks, change, margin=None, runs=1):
161
162
  return compare_cases(side(0.0), side(change), margin_points=margin)
162
163
 
163
164
 
164
- @pytest.mark.parametrize(
165
- ("tasks", "runs"), [(12, 1), (16, 1), (30, 1), (30, 3), (80, 1)]
166
- )
167
- def test_without_a_change_a_verdict_is_rare(tasks, runs):
168
- rng = random.Random(tasks * runs)
165
+ def _verdict_chance(tasks, chance):
166
+ # The exact chance of a verdict without a true change. Each case becomes better or
167
+ # worse with `chance`, so the changed cases follow a trinomial distribution.
168
+ total = 0.0
169
+ for up in range(tasks + 1):
170
+ for down in range(tasks - up + 1):
171
+ same = tasks - up - down
172
+ changes = [1] * up + [-1] * down + [0] * same
173
+ if compare_cases(*_changes(changes)).verdict != "no measurable difference":
174
+ ways = math.factorial(tasks) // (
175
+ math.factorial(up) * math.factorial(down) * math.factorial(same)
176
+ )
177
+ total += ways * chance ** (up + down) * (1 - 2 * chance) ** same
178
+ return total
179
+
180
+
181
+ @pytest.mark.parametrize("tasks", [12, 16, 20, 30, 80])
182
+ def test_without_a_change_a_verdict_is_rare(tasks):
183
+ # Pass rates from 30% to 70%: a case changes in each direction with chance
184
+ # E[p(1 - p)] = 0.25 - 0.4**2 / 12.
185
+ assert 0.05 <= _verdict_chance(tasks, 0.25 - 0.4**2 / 12) <= 0.07
186
+
187
+
188
+ def test_without_a_change_a_verdict_is_rarer_with_repeats():
189
+ rng = random.Random(90)
169
190
  trials = 1000
170
191
  verdicts = sum(
171
- _simulate(rng, tasks, 0.0, runs=runs).verdict != "no measurable difference"
192
+ _simulate(rng, 30, 0.0, runs=3).verdict != "no measurable difference"
172
193
  for _ in range(trials)
173
194
  )
174
- assert verdicts / trials <= 0.06
195
+ assert verdicts / trials <= 0.04
175
196
 
176
197
 
177
198
  def test_the_detectable_change_shows_with_80_percent_power():
@@ -211,15 +211,14 @@ def _wilson_coverage(runs, rate):
211
211
  def test_wilson_interval_covers_the_true_rate_95_percent_of_the_time(runs):
212
212
  coverages = [_wilson_coverage(runs, percent / 100) for percent in range(5, 96, 5)]
213
213
  assert 0.94 <= sum(coverages) / len(coverages) <= 0.96
214
- assert min(coverages) >= 0.91
214
+ dense = [_wilson_coverage(runs, permille / 1000) for permille in range(50, 951)]
215
+ assert min(dense) >= 0.89
215
216
 
216
217
 
217
218
  @pytest.mark.parametrize("runs", [10, 30, 100])
218
219
  def test_wilson_interval_covers_less_near_zero(runs):
219
- coverages = [
220
- _wilson_coverage(runs, permille / 1000) for permille in range(5, 50, 5)
221
- ]
222
- assert 0.85 <= min(coverages) < 0.92
220
+ coverages = [_wilson_coverage(runs, rate / 10000) for rate in range(10, 500)]
221
+ assert 0.83 <= min(coverages) < 0.85
223
222
 
224
223
 
225
224
  def test_mcnemar_exact_keeps_false_alarms_at_most_5_percent():