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.
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/PKG-INFO +1 -1
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/gateway.json +35 -15
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/pyproject.toml +1 -1
- vis_agent-0.3.4.dev8058/release-tag +1 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/__init__.py +5 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_client.py +27 -0
- vis_agent-0.3.4.dev8058/src/blockether/vis/engine/_speech.py +289 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/compare.py +5 -4
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/extension.py +7 -1
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_api_guides.py +1 -1
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_contracts.py +8 -2
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_compare.py +28 -7
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_stats.py +4 -5
- vis_agent-0.3.4.dev8058/tests/test_speech.py +151 -0
- vis_agent-0.3.4.dev8055/release-tag +0 -1
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/.gitignore +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/LICENSE +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/README.md +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-arguments.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-cases.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-copy.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-execution.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-groups.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-read-session.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-reads.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-results.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-stream.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-tables.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/diff.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/live-primitives.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/session-health.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/fixtures/view.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/activity.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/agents.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/automations.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/common.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/config.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/content.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/council.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/diff.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/evals.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/provider.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/rooms.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/symbol.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/toggle.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/contracts/schema/view.json +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/examples/greeter/tests/test_greeter.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/hatch_build.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/__init__.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/_contracts.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/_outside.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/_wire.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/activity.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_agent.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_agents.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_council.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_extensions.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_host.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_install.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_local.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_models.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/__init__.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/_gateway.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/agent.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/bench.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/check.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/environment.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/grade.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/harbor.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/judge-rubric.toml +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/judge.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/labels.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/leaderboard.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/lic.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/longmemeval.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/mrcr.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/repeats.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/report.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/reports.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/rounds.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/stages.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/stats.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/trajectory.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/extension_package.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/py.typed +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/rooms.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/src/blockether/vis/views.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/conftest.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/evals_atif.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_activity.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_agent.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_agent_extension_lifecycle.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_agent_extensions.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_agents.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_attachments.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_authoring.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_catalog.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_catalog_recipe.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_ci_report_recipe.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_client.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_client_extensions_engine.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_code_quality_recipe.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_council.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_declarations.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_engine.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_engine_docstrings.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_agent.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_bench.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_check.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_environment.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_gateway.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_grade.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_guards.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_harbor.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_judge.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_labels.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_lic.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_longmemeval.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_mrcr.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_repeats.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_report.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_rounds.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_stages.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_tasks.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_evals_trajectory.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_extension_package.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_extension_reference.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_gateway_performance.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_goals.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_install.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_live_primitives.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_live_wait.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_local.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_method_typing.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_models.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_monitor_recipe.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_namespace.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_outside.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_providers.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_read_session.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_record_arguments.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_registration.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_repair_hooks.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_result_sequence.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_rooms.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_scoped_settings.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_sdk_bootstrap.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_sdk_guide.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_sdk_typing.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_shortcuts_live.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_structured_live.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_symbol_contract.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_testing.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_text_list_arguments.py +0 -0
- {vis_agent-0.3.4.dev8055 → vis_agent-0.3.4.dev8058}/tests/test_tools.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) ==
|
|
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) ==
|
|
59
|
+
assert len(operations) == 206
|
|
60
60
|
assert Counter(operation["request"] for operation in operations) == {
|
|
61
61
|
"none": 123,
|
|
62
|
-
"json":
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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():
|