vis-agent 0.3.4.dev8048__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.dev8048 → vis_agent-0.3.4.dev8058}/PKG-INFO +1 -1
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/evals.json +24 -2
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/gateway.json +35 -15
- {vis_agent-0.3.4.dev8048 → 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.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/__init__.py +5 -0
- {vis_agent-0.3.4.dev8048 → 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.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/_gateway.py +10 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/compare.py +100 -25
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/stats.py +7 -5
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/extension.py +7 -1
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_api_guides.py +1 -1
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_contracts.py +8 -2
- vis_agent-0.3.4.dev8058/tests/test_evals_compare.py +217 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_gateway.py +5 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_stats.py +120 -1
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_tasks.py +2 -1
- vis_agent-0.3.4.dev8058/tests/test_speech.py +151 -0
- vis_agent-0.3.4.dev8048/release-tag +0 -1
- vis_agent-0.3.4.dev8048/tests/test_evals_compare.py +0 -53
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/.gitignore +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/LICENSE +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/README.md +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-arguments.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-cases.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-copy.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-execution.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-groups.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-read-session.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-reads.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-results.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-stream.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity-tables.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/activity.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/diff.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/live-primitives.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/session-health.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/fixtures/view.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/activity.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/agents.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/automations.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/common.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/config.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/content.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/council.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/diff.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/provider.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/rooms.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/symbol.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/toggle.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/contracts/schema/view.json +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/examples/greeter/tests/test_greeter.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/hatch_build.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/__init__.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/_contracts.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/_outside.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/_wire.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/activity.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_agent.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_agents.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_council.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_extensions.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_host.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_install.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_local.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/engine/_models.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/__init__.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/agent.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/bench.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/check.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/environment.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/grade.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/harbor.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/judge-rubric.toml +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/judge.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/labels.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/leaderboard.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/lic.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/longmemeval.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/mrcr.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/repeats.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/report.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/reports.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/rounds.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/stages.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/evals/trajectory.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/extension_package.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/py.typed +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/rooms.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/src/blockether/vis/views.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/conftest.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/evals_atif.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_activity.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_agent.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_agent_extension_lifecycle.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_agent_extensions.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_agents.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_attachments.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_authoring.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_catalog.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_catalog_recipe.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_ci_report_recipe.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_client.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_client_extensions_engine.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_code_quality_recipe.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_council.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_declarations.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_engine.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_engine_docstrings.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_agent.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_bench.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_check.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_environment.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_grade.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_guards.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_harbor.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_judge.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_labels.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_lic.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_longmemeval.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_mrcr.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_repeats.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_report.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_rounds.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_stages.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_evals_trajectory.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_extension_package.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_extension_reference.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_gateway_performance.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_goals.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_install.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_live_primitives.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_live_wait.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_local.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_method_typing.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_models.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_monitor_recipe.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_namespace.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_outside.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_providers.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_read_session.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_record_arguments.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_registration.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_repair_hooks.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_result_sequence.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_rooms.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_scoped_settings.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_sdk_bootstrap.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_sdk_guide.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_sdk_typing.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_shortcuts_live.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_structured_live.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_symbol_contract.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_testing.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_text_list_arguments.py +0 -0
- {vis_agent-0.3.4.dev8048 → vis_agent-0.3.4.dev8058}/tests/test_tools.py +0 -0
- {vis_agent-0.3.4.dev8048 → 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
|
|
@@ -149,6 +149,12 @@
|
|
|
149
149
|
},
|
|
150
150
|
"candidate_name": {
|
|
151
151
|
"type": "string"
|
|
152
|
+
},
|
|
153
|
+
"margin_points": {
|
|
154
|
+
"description": "The smallest change in points that matters. It splits a result without a clear change into equivalent, not worse, not better or inconclusive.",
|
|
155
|
+
"type": "number",
|
|
156
|
+
"exclusiveMinimum": 0,
|
|
157
|
+
"maximum": 100
|
|
152
158
|
}
|
|
153
159
|
},
|
|
154
160
|
"required": [
|
|
@@ -649,7 +655,16 @@
|
|
|
649
655
|
"maxItems": 2
|
|
650
656
|
},
|
|
651
657
|
"verdict": {
|
|
652
|
-
"type": "string"
|
|
658
|
+
"type": "string",
|
|
659
|
+
"enum": [
|
|
660
|
+
"better",
|
|
661
|
+
"worse",
|
|
662
|
+
"no measurable difference",
|
|
663
|
+
"equivalent",
|
|
664
|
+
"not worse",
|
|
665
|
+
"not better",
|
|
666
|
+
"inconclusive"
|
|
667
|
+
]
|
|
653
668
|
},
|
|
654
669
|
"detectable_change_points": {
|
|
655
670
|
"type": "number"
|
|
@@ -683,6 +698,12 @@
|
|
|
683
698
|
"items": {
|
|
684
699
|
"type": "string"
|
|
685
700
|
}
|
|
701
|
+
},
|
|
702
|
+
"margin_points": {
|
|
703
|
+
"type": [
|
|
704
|
+
"number",
|
|
705
|
+
"null"
|
|
706
|
+
]
|
|
686
707
|
}
|
|
687
708
|
},
|
|
688
709
|
"required": [
|
|
@@ -698,7 +719,8 @@
|
|
|
698
719
|
"worse",
|
|
699
720
|
"better",
|
|
700
721
|
"unpaired",
|
|
701
|
-
"caveats"
|
|
722
|
+
"caveats",
|
|
723
|
+
"margin_points"
|
|
702
724
|
],
|
|
703
725
|
"additionalProperties": false
|
|
704
726
|
},
|
|
@@ -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
|
|
@@ -68,6 +68,15 @@ def _required_text(body: Mapping, key: str) -> str:
|
|
|
68
68
|
return value
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def _number(body: Mapping, key: str) -> float | None:
|
|
72
|
+
value = body.get(key)
|
|
73
|
+
if value is None:
|
|
74
|
+
return None
|
|
75
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
76
|
+
raise ValueError(f"`{key}` must be a number.")
|
|
77
|
+
return float(value)
|
|
78
|
+
|
|
79
|
+
|
|
71
80
|
def _cases(body: Mapping, key: str) -> dict[tuple[str, ...], tuple[int, int]]:
|
|
72
81
|
rows = body.get(key)
|
|
73
82
|
if not isinstance(rows, list):
|
|
@@ -114,6 +123,7 @@ def _compare(body: Mapping):
|
|
|
114
123
|
_cases(body, "candidate"),
|
|
115
124
|
baseline_name=_word(body, "baseline_name", "baseline"),
|
|
116
125
|
candidate_name=_word(body, "candidate_name", "candidate"),
|
|
126
|
+
margin_points=_number(body, "margin_points"),
|
|
117
127
|
)
|
|
118
128
|
|
|
119
129
|
|