echoact 0.1.0__py3-none-any.whl
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.
- echoact/__init__.py +3 -0
- echoact/__main__.py +117 -0
- echoact/app.py +315 -0
- echoact/audio/__init__.py +0 -0
- echoact/audio/devices.py +192 -0
- echoact/audio/player.py +611 -0
- echoact/audio/wav.py +854 -0
- echoact/config/__init__.py +0 -0
- echoact/config/budget.py +370 -0
- echoact/config/settings.py +1244 -0
- echoact/db/__init__.py +0 -0
- echoact/db/backup.py +2429 -0
- echoact/db/migrations.py +434 -0
- echoact/db/schema.sql +214 -0
- echoact/db/store.py +2062 -0
- echoact/diagnostics.py +902 -0
- echoact/domain.py +487 -0
- echoact/engine/__init__.py +0 -0
- echoact/engine/container.py +843 -0
- echoact/engine/protocol.py +241 -0
- echoact/engine/runtime.py +324 -0
- echoact/engine/supervisor.py +961 -0
- echoact/engine/worker.py +659 -0
- echoact/errors.py +281 -0
- echoact/instance.py +172 -0
- echoact/jobs/__init__.py +0 -0
- echoact/jobs/engine.py +776 -0
- echoact/jobs/request.py +300 -0
- echoact/mcp/__init__.py +0 -0
- echoact/mcp/__main__.py +50 -0
- echoact/mcp/client.py +202 -0
- echoact/mcp/config.py +112 -0
- echoact/mcp/server.py +340 -0
- echoact/models/__init__.py +0 -0
- echoact/models/catalog.py +273 -0
- echoact/models/manifest.py +278 -0
- echoact/models/registry.py +1551 -0
- echoact/paths.py +93 -0
- echoact/policy.py +189 -0
- echoact/security/__init__.py +0 -0
- echoact/security/credentials.py +930 -0
- echoact/security/ratelimit.py +534 -0
- echoact/service/__init__.py +20 -0
- echoact/service/app.py +182 -0
- echoact/service/deps.py +563 -0
- echoact/service/errors.py +241 -0
- echoact/service/routes.py +1125 -0
- echoact/service/schemas.py +509 -0
- echoact/service/server.py +270 -0
- echoact/text/__init__.py +0 -0
- echoact/text/language.py +44 -0
- echoact/text/loader.py +577 -0
- echoact/text/normalize.py +924 -0
- echoact/text/segment.py +499 -0
- echoact/text/sniff.py +1202 -0
- echoact/ui/__init__.py +0 -0
- echoact/ui/bridge.py +50 -0
- echoact/ui/controls.py +360 -0
- echoact/ui/credential_dialog.py +131 -0
- echoact/ui/fonts.py +94 -0
- echoact/ui/i18n.py +260 -0
- echoact/ui/icons.py +440 -0
- echoact/ui/library.py +1642 -0
- echoact/ui/licence.py +162 -0
- echoact/ui/main_window.py +1202 -0
- echoact/ui/mcp_setup.py +494 -0
- echoact/ui/models_view.py +1142 -0
- echoact/ui/notifications.py +202 -0
- echoact/ui/reading.py +494 -0
- echoact/ui/settings_view.py +2258 -0
- echoact/ui/status_view.py +1193 -0
- echoact/ui/theme.py +579 -0
- echoact/util/__init__.py +0 -0
- echoact/util/ids.py +62 -0
- echoact/util/logging.py +127 -0
- echoact-0.1.0.dist-info/METADATA +162 -0
- echoact-0.1.0.dist-info/RECORD +80 -0
- echoact-0.1.0.dist-info/WHEEL +4 -0
- echoact-0.1.0.dist-info/entry_points.txt +3 -0
- echoact-0.1.0.dist-info/licenses/LICENSE +21 -0
echoact/service/app.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Assembling the ASGI application, and the specification F-57 requires.
|
|
2
|
+
|
|
3
|
+
Three deliberate absences are worth naming, because each is a default that
|
|
4
|
+
had to be turned off rather than something forgotten:
|
|
5
|
+
|
|
6
|
+
* **No CORS middleware.** N-17 disallows CORS by default, and the way to
|
|
7
|
+
disallow it is to emit no ``Access-Control-Allow-*`` header at all. The
|
|
8
|
+
Origin check in :mod:`echoact.service.deps` refuses a cross-origin request
|
|
9
|
+
outright; nothing here would have let a browser read a reply anyway.
|
|
10
|
+
* **No interactive docs.** Swagger UI and ReDoc load their JavaScript from a
|
|
11
|
+
public CDN. N-01 says the app makes no automatic external communication
|
|
12
|
+
beyond model downloads and update checks the user asked for, and a docs
|
|
13
|
+
page that fetches from a CDN the moment it is opened would be exactly that.
|
|
14
|
+
The machine-readable document remains, which is what F-57 actually asks for.
|
|
15
|
+
* **No lifespan hooks.** The parts already exist: ``Application`` builds the
|
|
16
|
+
store, the engine, and the credentials before the service is started, and
|
|
17
|
+
F-79 requires the GUI to survive a service that never starts. Wiring
|
|
18
|
+
construction into the ASGI lifespan would put the app's lifetime inside the
|
|
19
|
+
server's, which is the inversion A.2 rules out.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from fastapi import FastAPI
|
|
27
|
+
from fastapi.openapi.utils import get_openapi
|
|
28
|
+
|
|
29
|
+
from .. import __version__
|
|
30
|
+
from ..errors import Code
|
|
31
|
+
from ..policy import API_PREFIX, API_VERSION
|
|
32
|
+
from .deps import ServiceContext, ServiceGate
|
|
33
|
+
from .errors import error_document, install_error_handlers
|
|
34
|
+
from .routes import build_router
|
|
35
|
+
from .schemas import CreateJobRequest, ErrorBody
|
|
36
|
+
|
|
37
|
+
_METHODS = frozenset({"get", "put", "post", "delete", "options", "head", "patch", "trace"})
|
|
38
|
+
|
|
39
|
+
#: The refusals a caller most needs to have seen before it meets one, rendered
|
|
40
|
+
#: as F-57's "request/response examples". Busy and rate-limited are here
|
|
41
|
+
#: because F-47 calls contention the expected case rather than an edge case,
|
|
42
|
+
#: and a client that has never seen the hint will not honour it.
|
|
43
|
+
_ERROR_EXAMPLES: tuple[Code, ...] = (
|
|
44
|
+
Code.UNAUTHENTICATED,
|
|
45
|
+
Code.FORBIDDEN,
|
|
46
|
+
Code.NOT_FOUND,
|
|
47
|
+
Code.BUSY,
|
|
48
|
+
Code.IDEMPOTENCY_KEY_CONFLICT,
|
|
49
|
+
Code.RATE_LIMITED,
|
|
50
|
+
Code.INSUFFICIENT_RESOURCES,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
#: The whole external contract, exactly as Section 2.10's table lists it, as
|
|
54
|
+
#: (method, path) pairs. Written down here rather than derived from the
|
|
55
|
+
#: router so that the test asserting "these twelve and no others" checks the
|
|
56
|
+
#: code against the document instead of against itself.
|
|
57
|
+
#:
|
|
58
|
+
#: Twelve operations over eleven paths: creating a job and listing history are
|
|
59
|
+
#: two methods on ``/api/v1/jobs``, which is one path to OpenAPI and two rows
|
|
60
|
+
#: in the contract table.
|
|
61
|
+
CONTRACT_OPERATIONS: tuple[tuple[str, str], ...] = (
|
|
62
|
+
("GET", f"{API_PREFIX}/status"),
|
|
63
|
+
("GET", f"{API_PREFIX}/models"),
|
|
64
|
+
("POST", f"{API_PREFIX}/estimate"),
|
|
65
|
+
("POST", f"{API_PREFIX}/jobs"),
|
|
66
|
+
("GET", f"{API_PREFIX}/jobs"),
|
|
67
|
+
("GET", f"{API_PREFIX}/jobs/{{job_id}}"),
|
|
68
|
+
("GET", f"{API_PREFIX}/jobs/{{job_id}}/text"),
|
|
69
|
+
("POST", f"{API_PREFIX}/jobs/{{job_id}}/cancel"),
|
|
70
|
+
("GET", f"{API_PREFIX}/jobs/{{job_id}}/segments"),
|
|
71
|
+
("GET", f"{API_PREFIX}/jobs/{{job_id}}/segments/{{segment_id}}/audio"),
|
|
72
|
+
("GET", f"{API_PREFIX}/jobs/{{job_id}}/audio"),
|
|
73
|
+
("GET", f"{API_PREFIX}/jobs/{{job_id}}/result"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
CONTRACT_PATHS: tuple[str, ...] = tuple(dict.fromkeys(path for _, path in CONTRACT_OPERATIONS))
|
|
77
|
+
|
|
78
|
+
_DESCRIPTION = """\
|
|
79
|
+
EchoAct's local generation service. It listens on the loopback address only,
|
|
80
|
+
requires authentication on every path including the specification itself, and
|
|
81
|
+
exists only while the desktop application is running.
|
|
82
|
+
|
|
83
|
+
Authenticate with `Authorization: Bearer <token>`, using a credential issued
|
|
84
|
+
in the application. Tokens never travel in a URL.
|
|
85
|
+
|
|
86
|
+
Errors carry a stable `code`, a `message`, `retryable`, and a `request_id`.
|
|
87
|
+
The three refusals that can succeed later -- busy, rate limited, and
|
|
88
|
+
insufficient resources -- also carry `retry_after_s` and a `Retry-After`
|
|
89
|
+
header; a permanent refusal carries neither.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create_app(context: ServiceContext) -> FastAPI:
|
|
94
|
+
"""Build the ASGI application for one service context."""
|
|
95
|
+
app = FastAPI(
|
|
96
|
+
title="EchoAct local service",
|
|
97
|
+
version=__version__,
|
|
98
|
+
description=_DESCRIPTION,
|
|
99
|
+
openapi_url="/openapi.json",
|
|
100
|
+
docs_url=None,
|
|
101
|
+
redoc_url=None,
|
|
102
|
+
)
|
|
103
|
+
install_error_handlers(app)
|
|
104
|
+
app.include_router(build_router(context), prefix=API_PREFIX)
|
|
105
|
+
# Added last so it wraps everything, routing and the specification route
|
|
106
|
+
# included: N-31's "no request succeeds without a valid credential" has to
|
|
107
|
+
# cover the paths that are not operations as well as the ones that are.
|
|
108
|
+
app.add_middleware(ServiceGate, context=context)
|
|
109
|
+
app.openapi = lambda: _specification(app) # type: ignore[method-assign]
|
|
110
|
+
app.state.echoact = context
|
|
111
|
+
return app
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _specification(app: FastAPI) -> dict[str, Any]:
|
|
115
|
+
"""F-57's machine-readable contract, cached on the app as FastAPI does.
|
|
116
|
+
|
|
117
|
+
``CreateJobRequest`` is merged in by hand. POST /api/v1/jobs declares two
|
|
118
|
+
alternative bodies -- inline JSON and a multipart upload -- which FastAPI
|
|
119
|
+
cannot express from a signature, so the operation supplies its own
|
|
120
|
+
``requestBody`` and the model it refers to would otherwise never reach
|
|
121
|
+
``components.schemas``. A specification that references a schema it does
|
|
122
|
+
not define is not machine-readable, which is the whole point of F-57.
|
|
123
|
+
"""
|
|
124
|
+
if app.openapi_schema:
|
|
125
|
+
return app.openapi_schema
|
|
126
|
+
schema = get_openapi(
|
|
127
|
+
title=app.title,
|
|
128
|
+
version=app.version,
|
|
129
|
+
description=app.description,
|
|
130
|
+
routes=app.routes,
|
|
131
|
+
)
|
|
132
|
+
schema["info"]["x-api-version"] = API_VERSION
|
|
133
|
+
components = schema.setdefault("components", {}).setdefault("schemas", {})
|
|
134
|
+
body = CreateJobRequest.model_json_schema(ref_template="#/components/schemas/{model}")
|
|
135
|
+
for name, definition in body.pop("$defs", {}).items():
|
|
136
|
+
components.setdefault(name, definition)
|
|
137
|
+
components.setdefault("CreateJobRequest", body)
|
|
138
|
+
components.setdefault("ErrorBody", ErrorBody.model_json_schema())
|
|
139
|
+
_attach_error_responses(schema)
|
|
140
|
+
for unused in ("HTTPValidationError", "ValidationError"):
|
|
141
|
+
components.pop(unused, None)
|
|
142
|
+
app.openapi_schema = schema
|
|
143
|
+
return schema
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _attach_error_responses(schema: dict[str, Any]) -> None:
|
|
147
|
+
"""Give every operation F-57's error envelope, with examples.
|
|
148
|
+
|
|
149
|
+
Attached here rather than declared on each route because the envelope is
|
|
150
|
+
the same for all twelve and Section 2.10 fixes the statuses centrally: a
|
|
151
|
+
per-route list would be twelve chances for the document and
|
|
152
|
+
``echoact.errors`` to disagree about what a refusal looks like.
|
|
153
|
+
"""
|
|
154
|
+
examples = {
|
|
155
|
+
code.value: {"summary": code.value, "value": error_document(code)}
|
|
156
|
+
for code in _ERROR_EXAMPLES
|
|
157
|
+
}
|
|
158
|
+
for path_item in schema.get("paths", {}).values():
|
|
159
|
+
for method, operation in path_item.items():
|
|
160
|
+
if method.lower() not in _METHODS:
|
|
161
|
+
continue
|
|
162
|
+
responses = operation.setdefault("responses", {})
|
|
163
|
+
# FastAPI adds a 422 of its own shape to every operation with a
|
|
164
|
+
# parameter. This service never answers one: a malformed request
|
|
165
|
+
# is 400 with a code, and the only 422 it produces is invalid
|
|
166
|
+
# voice settings, in the same envelope as every other refusal.
|
|
167
|
+
# Leaving FastAPI's in would document a response that cannot
|
|
168
|
+
# occur, in a schema nothing here returns.
|
|
169
|
+
responses.pop("422", None)
|
|
170
|
+
responses["default"] = {
|
|
171
|
+
"description": (
|
|
172
|
+
"A refusal. Carries a stable code, a message, whether a retry can "
|
|
173
|
+
"succeed, and a request identifier; the retryable ones also carry "
|
|
174
|
+
"retry_after_s and a Retry-After header."
|
|
175
|
+
),
|
|
176
|
+
"content": {
|
|
177
|
+
"application/json": {
|
|
178
|
+
"schema": {"$ref": "#/components/schemas/ErrorBody"},
|
|
179
|
+
"examples": examples,
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
}
|