echoact 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- echoact-0.1.2/.vscode/mcp.json +17 -0
- {echoact-0.1.0 → echoact-0.1.2}/PKG-INFO +1 -1
- {echoact-0.1.0 → echoact-0.1.2}/docs/design.md +18 -11
- echoact-0.1.2/docs/icons/caret-echo.svg +6 -0
- echoact-0.1.2/docs/icons/horizon.svg +7 -0
- echoact-0.1.2/docs/icons/ieung.svg +4 -0
- echoact-0.1.2/docs/icons/origin.svg +6 -0
- echoact-0.1.2/docs/icons/ripple-candidates.html +333 -0
- echoact-0.1.2/echoact/__init__.py +16 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/__main__.py +17 -1
- {echoact-0.1.0 → echoact-0.1.2}/echoact/app.py +6 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/audio/player.py +657 -611
- {echoact-0.1.0 → echoact-0.1.2}/echoact/config/settings.py +1273 -1244
- {echoact-0.1.0 → echoact-0.1.2}/echoact/errors.py +19 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/mcp/__main__.py +66 -50
- {echoact-0.1.0 → echoact-0.1.2}/echoact/mcp/server.py +120 -11
- echoact-0.1.2/echoact/playrequests.py +140 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/policy.py +15 -1
- {echoact-0.1.0 → echoact-0.1.2}/echoact/service/__init__.py +20 -20
- {echoact-0.1.0 → echoact-0.1.2}/echoact/service/app.py +8 -7
- {echoact-0.1.0 → echoact-0.1.2}/echoact/service/deps.py +568 -563
- {echoact-0.1.0 → echoact-0.1.2}/echoact/service/errors.py +1 -1
- {echoact-0.1.0 → echoact-0.1.2}/echoact/service/routes.py +132 -9
- {echoact-0.1.0 → echoact-0.1.2}/echoact/service/schemas.py +56 -13
- echoact-0.1.2/echoact/ui/brand.py +127 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/bridge.py +75 -50
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/controls.py +201 -43
- echoact-0.1.2/echoact/ui/external_play.py +196 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/main_window.py +365 -73
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/mcp_setup.py +3 -3
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/settings_view.py +2343 -2258
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/theme.py +24 -20
- echoact-0.1.2/echoact/ui/tray.py +120 -0
- echoact-0.1.2/echoact/update.py +56 -0
- echoact-0.1.2/packaging/echoact.ico +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/pyproject.toml +1 -1
- echoact-0.1.2/scripts/make_app_icon.py +68 -0
- echoact-0.1.2/tests/test_brand.py +76 -0
- echoact-0.1.2/tests/test_main_window.py +404 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_mcp.py +105 -1
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_packaging.py +10 -1
- echoact-0.1.2/tests/test_playrequests.py +122 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_service.py +237 -8
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_settings.py +840 -835
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_settings_actions.py +47 -6
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_ui_settings.py +1383 -1307
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_ui_status.py +1 -1
- echoact-0.1.2/tests/test_ui_tray.py +219 -0
- echoact-0.1.2/tests/test_update.py +61 -0
- {echoact-0.1.0 → echoact-0.1.2}/uv.lock +1 -1
- echoact-0.1.0/echoact/__init__.py +0 -3
- echoact-0.1.0/tests/test_main_window.py +0 -193
- {echoact-0.1.0 → echoact-0.1.2}/.gitignore +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/.python-version +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/CLAUDE.md +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/LICENSE +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/README.md +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/audio/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/audio/devices.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/audio/wav.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/config/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/config/budget.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/db/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/db/backup.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/db/migrations.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/db/schema.sql +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/db/store.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/diagnostics.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/domain.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/engine/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/engine/container.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/engine/protocol.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/engine/runtime.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/engine/supervisor.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/engine/worker.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/instance.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/jobs/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/jobs/engine.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/jobs/request.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/mcp/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/mcp/client.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/mcp/config.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/models/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/models/catalog.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/models/manifest.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/models/registry.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/paths.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/security/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/security/credentials.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/security/ratelimit.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/service/server.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/text/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/text/language.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/text/loader.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/text/normalize.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/text/segment.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/text/sniff.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/credential_dialog.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/fonts.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/i18n.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/icons.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/library.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/licence.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/models_view.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/notifications.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/reading.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/ui/status_view.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/util/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/util/ids.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/echoact/util/logging.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/main.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/packaging/README.md +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/packaging/entry.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/scripts/fetch_fonts.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/spikes/engine_conformance.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/spikes/outline_emphasis.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/spikes/tts_feasibility.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/spikes/voice_review.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/__init__.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_backup.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_budget.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_container.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_credentials.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_diagnostics.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_end_to_end.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_instance.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_job_engine.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_language.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_loader.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_manifest.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_mcp_setup.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_migrations.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_navigation.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_normalize.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_notifications.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_player.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_ratelimit.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_reading_surface.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_registry.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_restore_gate.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_runtime.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_segment.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_service_codes.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_service_mcp.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_sniff.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_store.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_supervisor.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_ui_library.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_ui_models.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_wav.py +0 -0
- {echoact-0.1.0 → echoact-0.1.2}/tests/test_worker.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"servers": {
|
|
3
|
+
"echoact": {
|
|
4
|
+
"type": "stdio",
|
|
5
|
+
"command": "C:/Users/lhj56/ws/echo-act/.venv/Scripts/python.exe",
|
|
6
|
+
"args": [
|
|
7
|
+
"-m",
|
|
8
|
+
"echoact.mcp"
|
|
9
|
+
],
|
|
10
|
+
"env": {
|
|
11
|
+
"ECHOACT_URL": "http://127.0.0.1:8765",
|
|
12
|
+
"PYTHONPATH": "C:/Users/lhj56/ws/echo-act",
|
|
13
|
+
"ECHOACT_TOKEN": "eak_my-mcp-client-ea9056_oLrZDOpS2idR3xt3MaNKGGnNMc-nA_OZi7aLVfJbOBdM"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -140,12 +140,13 @@ Database support means local document and job history management usable without
|
|
|
140
140
|
|
|
141
141
|
| ID | Feature | Specification and verification criteria |
|
|
142
142
|
| --- | --- | --- |
|
|
143
|
-
| F-46 | Integration activation | The local REST service is on by default. It binds the loopback address only, always requires authentication, and can be turned off by the owner. MCP is
|
|
143
|
+
| F-46 | Integration activation | The local REST service is on by default. It binds the loopback address only, always requires authentication, and can be turned off by the owner. MCP is on by default and can be turned off by the owner as well. Because the MCP server is a REST client per F-58, turning REST off also makes MCP unavailable, and that consequence is stated at the point of the change. The activation state, bind address and port, connection method, and granted permissions can be reviewed. |
|
|
144
144
|
| F-47 | Shared features and budget | All entry paths use the same model list, input validation, job states, and resource budget. Across the GUI and all clients combined, concurrent generation is limited to one. A second request is refused with a busy error carrying a retry-after hint, and no automatic queue is provided. Contention is the expected case rather than an edge case, because the desktop user and every client share the one slot, so a client must honour the hint rather than retry immediately and must be able to give up. |
|
|
145
145
|
| F-48 | Asynchronous jobs | When a generation request is accepted, a job ID is returned, and progress, errors, and results are checked via separate queries. An accepted job is not canceled, nor is the same job regenerated, merely because the connection dropped. |
|
|
146
146
|
| F-49 | Re-requests and cancellation | Duplicate generation is prevented using a retry identifier supplied by the requester. The identifier is required on every generation request rather than optional, and a request without one is refused: with a single generation slot, a client that retries after a dropped or slow response would otherwise queue a second rendering of the same text behind the first, and automated clients retry as a matter of course. The same identifier with the same content returns the same job, while different content is refused as a conflict. Cancellation is provided as explicit job cancellation, and repeated cancellation adds no further side effects. |
|
|
147
147
|
| F-50 | Ownership and permissions | By default a client can query and cancel only its own jobs and results. Library access requires separate authorization. The GUI owner can review and cancel all jobs. External integrations cannot raise the global resource budget or halt other jobs. Requests carry no priority by path: a request from the GUI does not preempt one already running for a client, and the owner reclaims the single slot by cancelling, which F-69 makes reachable in one place. |
|
|
148
|
-
| F-51 | Playback of external requests | External requests perform speech generation only and
|
|
148
|
+
| F-51 | Playback of external requests | External requests perform speech generation only and never play by themselves. Audio reaches the host speakers only when the client asks for it under F-89 and the owner has left that setting on; otherwise the user selects and plays that job in the GUI. In either case external requesters do not overwrite UI input or take over the user's playback. |
|
|
149
|
+
| F-89 | External playback request | An external client may ask for a job it created to be played on the host output device. The owner turns this on and off and it is on by default; while it is off a play request is refused as a permanent error and nothing is heard. Playback begins at the first ready segment and follows generation as F-12 describes, so a short request is spoken without waiting for the whole job. It never takes over the person at the keyboard: the input surface is left exactly as it is, its highlight is reported unavailable for the duration rather than re-anchored to the external job's text, and a request arriving while the owner's own playback is in progress is refused as busy with a retry-after hint per F-57. The owner's own play and stop always win, and stopping external playback is reachable from the main screen per F-69. Volume, mute, and output device follow F-67. Where there is no output device, generation still succeeds and only playback is reported as failed. Asked for together with generation, a refusal to play is reported as part of the job rather than as the request's failure: the job exists, its audio is retrievable, and an error there would invite a retry that F-49 answers with the same job. |
|
|
149
150
|
| F-52 | Server lifetime | Integrations are available only while the app is running. The REST service starts with the app unless the owner turned it off; the MCP server is a separate short-lived process started by the MCP client. On exit, active jobs and connections are reported and exit is confirmed; on exit, the servers and model are also cleaned up. Turning off an integration blocks new requests, cancels that integration's in-progress jobs, and then shuts it down. An MCP server process started while the app is not running, while REST is off, or while MCP is disabled reports that condition to its client as a distinct, non-retrying error and never launches the app. GUI jobs are unaffected. |
|
|
150
151
|
|
|
151
152
|
### 2.10 REST API
|
|
@@ -153,7 +154,7 @@ Database support means local document and job history management usable without
|
|
|
153
154
|
| ID | Feature | Specification and verification criteria |
|
|
154
155
|
| --- | --- | --- |
|
|
155
156
|
| F-53 | Service and capability query | Uptime state, API version, supported models with their native sample rate and minimum resource budget, voices, languages, speaking styles, input formats, and the actual resource policy can be queried. Each voice carries a stable identifier, a display name, and a short description of its character and intended use, because a bare identifier gives a caller that is not a person no basis for choosing one. Descriptions come from the manifest in F-84 and are shown in the GUI as well. An undownloaded model is distinguished from a server fault. |
|
|
156
|
-
| F-54 | Generation request and status query | A job is created from text or a TXT/Markdown upload to be validated, voice settings, a retention flag, and the duplicate-prevention key required by F-49. The request may ask to wait for completion within the bound in F-88, which changes only when the response is sent and never what the job does. Every job request and every job representation carries an explicit job kind, and this release accepts exactly one value; an unrecognised kind is refused rather than defaulted, so that a client always states what it is asking for. Required values, ranges, and mutual contradictions are validated, and unknown models or options are not arbitrarily substituted. Upload is an alternative to the generation request body and cannot be specified together with inline text. |
|
|
157
|
+
| F-54 | Generation request and status query | A job is created from text or a TXT/Markdown upload to be validated, voice settings, a retention flag, and the duplicate-prevention key required by F-49. The request may ask to wait for completion within the bound in F-88, which changes only when the response is sent and never what the job does. Every job request and every job representation carries an explicit job kind, and this release accepts exactly one value; an unrecognised kind is refused rather than defaulted, so that a client always states what it is asking for. Required values, ranges, and mutual contradictions are validated, and unknown models or options are not arbitrarily substituted. Voice settings may be omitted in whole or in part, in which case the model manifest's documented default voice, style, and tempo apply and the job representation states the values actually used; those defaults come from the manifest and never from whatever the owner last selected in the GUI, so an automated caller's output does not depend on it. A request may also ask for the result to be played on the host output device, per F-89. Upload is an alternative to the generation request body and cannot be specified together with inline text. |
|
|
157
158
|
| F-55 | Segments and final result | For generated segments, the order, source-text range, start and end times, and retrieval identifier can be queried, and ready audio can be retrieved. After completion, the full WAV is provided in the format defined by F-82. Ungenerated segments and incomplete final files are never returned as if they were finished results. |
|
|
158
159
|
| F-56 | History query | Only when separately authorized may a client list and view details of retained jobs it owns. A summary query that excludes body text is the default, and the source-text snapshot of a retained job is retrieved through a separate, separately authorized request. External document editing, deletion, and full-database queries are not supported. |
|
|
159
160
|
| F-88 | Bounded wait and pre-flight estimate | Two facilities keep a caller from having to poll blindly. A generation request may ask to wait for the job to finish, for at most a bound the owner configures and the service caps; the service never waits longer than asked, never waits on model preparation or download, and returns the job ID unchanged when the bound passes, so waiting is an optimisation and never a different job lifecycle. Separately, a caller may submit text and settings for an estimate, receiving validation results, the segment count, the estimated audio duration and synthesis time, and whether the single generation slot is currently free, without creating a job, loading a model, or synthesising anything. Estimates are derived from the model's recorded throughput and are explicitly approximate. |
|
|
@@ -166,24 +167,25 @@ The external interface contract is as follows. Paths and responses are the publi
|
|
|
166
167
|
| GET /api/v1/status | Service, version, supported capabilities, resource policy | 200 |
|
|
167
168
|
| GET /api/v1/models | Models and voice/setting choices | 200 |
|
|
168
169
|
| POST /api/v1/estimate | Validate text and settings and estimate segments, duration, and synthesis time without creating a job | 200 |
|
|
169
|
-
| POST /api/v1/jobs | Create from text JSON or text file upload, with an explicit job kind and duplicate-prevention key; may request a bounded wait | New acceptance 202 + job ID; completed inside a requested wait 200 + terminal state; duplicate re-request 200 + existing ID |
|
|
170
|
+
| POST /api/v1/jobs | Create from text JSON or text file upload, with an explicit job kind and duplicate-prevention key; voice settings optional; may request a bounded wait and may ask for playback per F-89 | New acceptance 202 + job ID; completed inside a requested wait 200 + terminal state; duplicate re-request 200 + existing ID. A playback request made here is reported in the job representation, refusal included, because the job was created either way |
|
|
170
171
|
| GET /api/v1/jobs | Authorized retained history query | 200 + page info |
|
|
171
172
|
| GET /api/v1/jobs/{id} | State, progress, whether the result is ready | 200 |
|
|
172
173
|
| GET /api/v1/jobs/{id}/text | Source-text snapshot of a retained job, separately authorized | 200 |
|
|
173
174
|
| POST /api/v1/jobs/{id}/cancel | Cancel job | Cancellation request 202; already-terminated job 200 + final state |
|
|
175
|
+
| POST /api/v1/jobs/{id}/play | Ask for the job's audio to be played on the host output device, per F-89 | Accepted 202; playback turned off by the owner 403; the owner's own playback in progress 409 + retry-after |
|
|
174
176
|
| GET /api/v1/jobs/{id}/segments | List of ready segments | 200 + last sequence number |
|
|
175
177
|
| GET /api/v1/jobs/{id}/segments/{segment_id}/audio | Audio of a ready segment | 200 + audio/wav |
|
|
176
178
|
| GET /api/v1/jobs/{id}/audio | Completed full audio | 200 + audio/wav |
|
|
177
179
|
| GET /api/v1/jobs/{id}/result | Result metadata: format, sample rate, length, size, integrity value, expiry | 200 |
|
|
178
180
|
|
|
179
|
-
All paths are subject to authentication. Malformed requests are 400, unauthenticated 401, insufficient permission 403, nonexistent jobs or jobs owned by another owner 404, busy/result-not-ready/re-request conflict 409, size exceeded 413, unsupported format 415, invalid voice settings 422, request rate exceeded 429, and insufficient resources to start 503. Even for the same HTTP status, a detailed error code is provided. Responses of 409 for busy, 429, and 503 include a retry-after hint. The service listens on the loopback address only, and a request arriving with an unexpected Host or Origin is rejected before authentication is evaluated. Generation errors occurring after acceptance are surfaced as the job's failure state.
|
|
181
|
+
All paths are subject to authentication. Malformed requests are 400, unauthenticated 401, insufficient permission 403, nonexistent jobs or jobs owned by another owner 404, busy/result-not-ready/re-request conflict 409, size exceeded 413, unsupported format 415, invalid voice settings 422, request rate exceeded 429, and insufficient resources to start 503. A play request refused because the owner turned external playback off is 403 and carries no retry-after, because nothing about trying again would change it. Even for the same HTTP status, a detailed error code is provided. Responses of 409 for busy, 429, and 503 include a retry-after hint. The service listens on the loopback address only, and a request arriving with an unexpected Host or Origin is rejected before authentication is evaluated. Generation errors occurring after acceptance are surfaced as the job's failure state.
|
|
180
182
|
|
|
181
183
|
### 2.11 MCP Server
|
|
182
184
|
|
|
183
185
|
| ID | Feature | Specification and verification criteria |
|
|
184
186
|
| --- | --- | --- |
|
|
185
187
|
| F-58 | Local MCP connection | The connection method is local stdio, and the MCP server process is started by the MCP client. The MCP server holds no model, no database, and no job state of its own: it is a client of the local REST service in Section 2.10, reaching the app over loopback with a REST credential supplied in its client configuration. Client registration information, including that credential, is provided to the user by the app. Compatible protocol versions and the tool list are stated, and unsupported versions are clearly refused. The revision cited in this section is stateless and has no connection-establishing handshake, so the server implements the capability-discovery call that revision requires, carries its identity and protocol version in each result, and treats every request as self-describing. Streamable HTTP is excluded from scope. |
|
|
186
|
-
| F-59 | Tool provision | Querying supported models, requesting generation, querying jobs, canceling, querying segments,
|
|
188
|
+
| F-59 | Tool provision | Querying supported models, requesting generation, querying jobs, canceling, querying segments, retrieving results, and requesting playback are provided as tools. Each tool maps onto one REST operation and adds no capability REST does not already expose. Input and output schemas and side effects are described, and the same policies as F-46 through F-52 apply. |
|
|
187
189
|
| F-60 | Result delivery | A job ID is returned rather than blocking on model preparation, which can be arbitrarily long. A caller that expects a short job may instead ask to wait, bounded by F-88: if the job reaches a terminal state inside that bound the same call carries the outcome, and otherwise it returns the job ID and the job continues untouched. Waiting is never entered while a model is being prepared or downloaded. Audio is described by format, sample rate, size, length, and readiness state, and is delivered as an MCP resource whose URI the client reads over the same stdio session; the MCP server resolves that URI by retrieving the bytes from REST on the client's behalf. Large body text or audio is not embedded in the default tool response. Because the cited revision makes resource reads cacheable, every result reference carries a freshness hint no longer than the remaining result lifetime in Section 4.1 and is marked private, so that no shared intermediary may cache one owner's audio. Cross-call state travels as a server-minted job identifier passed as an ordinary tool argument, which is the pattern that revision prescribes. |
|
|
188
190
|
| F-61 | Permissions and consent | When registering a local client, generation, result reading, and retained-history reading are authorized separately and permissions can be revoked. An MCP client's effective permissions are exactly those of the REST credential it was registered with, so expiry and revocation take effect through one mechanism. No tools for arbitrary file read/write, command execution, direct database queries, or server configuration changes are provided. |
|
|
189
191
|
| F-62 | Client compatibility | Tool discovery, input errors, job progress queries, cancellation, and result retrieval are verified on the actual target clients. Support for an audio playback UI in the client is not assumed, and a method for retrieving results is provided. Behavior when the app is not running, when REST is off, and when the credential has expired is verified on the same clients. |
|
|
@@ -198,6 +200,8 @@ All paths are subject to authentication. Malformed requests are 400, unauthentic
|
|
|
198
200
|
| list_speech_segments | Query ready segments and their source-text/time mapping |
|
|
199
201
|
| get_speech_result | Retrieve a result reference for the completed WAV or a specific ready segment |
|
|
200
202
|
| list_speech_history | Query one's own retained history when separately authorized |
|
|
203
|
+
| speak | Generate and speak in one call: text with the voice settings optional, a short bounded wait, and playback requested per F-89. The tool a caller reaches for when a person said "speak" or "read this aloud" |
|
|
204
|
+
| play_speech | Ask for one's own job to be played on the host output device, per F-89 |
|
|
201
205
|
|
|
202
206
|
MCP result references are MCP resource URIs, readable by an authorized client over the same stdio session. They are opaque identifiers rather than addresses: arbitrary local file paths, and URLs carrying authentication tokens, are never returned to the client. The MCP server requires the REST service to be enabled and resolves every reference against it using its own credential. Access is refused after a reference expires, permissions are revoked, or the data is deleted. Diagnostic output from the MCP server goes to its own error stream rather than through the protocol's logging feature, which the cited revision deprecates. Cancellation of an MCP request itself is distinguished from cancellation of an already-accepted generation job; halting a job is done with cancel_speech_job.
|
|
203
207
|
|
|
@@ -291,7 +295,7 @@ The following defaults apply on first launch. Changeable values are managed in s
|
|
|
291
295
|
| Re-request identifiers | Scoped per client. Kept for at least 1 hour during job execution and after termination. Duplicate-prevention information is retained across a normal restart for that period, without retaining one-off body text or audio. It is disclosed that reusing the same key after expiry may become a new request. |
|
|
292
296
|
| Retention space | 5 GB by default for the combined app-managed documents, history, and audio. Model cache, user backups, and exported WAV files are shown separately. Retained data is kept until the user deletes it, and new retention is refused when the limit is reached. If the limit is reached at runtime, jobs requesting retention fail with a storage error. |
|
|
293
297
|
| Logs | Diagnostic logs without body text for 7 days, up to 100 MB. Old logs are cleaned up according to whichever limit is reached first. |
|
|
294
|
-
| External connections | REST on by default, loopback only, authentication always required. MCP
|
|
298
|
+
| External connections | REST on by default, loopback only, authentication always required. MCP on by default, stdio only, and implemented as a REST client, so it is unavailable while REST is off. No independent background service, and nothing listens once the app has exited. |
|
|
295
299
|
| Request rate | Per client, 10 generation requests per minute and 120 other requests per minute. Authentication failures are limited separately. The concurrent-generation limit always applies separately. |
|
|
296
300
|
| Bounded wait | A generation request may ask to wait for completion for up to 10 seconds, with an owner-configurable ceiling of 60 seconds. The service never waits longer than the caller asked, never waits while a model is being prepared or downloaded, and returns the job ID when the bound passes. Waiting does not extend the request-rate allowance or hold a second slot. |
|
|
297
301
|
| List queries | 20 items by default, up to 100 per response. |
|
|
@@ -302,6 +306,7 @@ The following additional defaults and change rules apply to operational settings
|
|
|
302
306
|
| --- | --- |
|
|
303
307
|
| Voice presets | Up to 100. On a duplicate name, overwrite is confirmed. |
|
|
304
308
|
| Playback volume | App volume 100% by default, mute off. Playback starts on the system default output device, and device changes during playback are handled per F-67. |
|
|
309
|
+
| External playback | Playing an external request on the host output device is allowed by default and can be turned off by the owner. It is refused while the owner's own playback is in progress, and it never changes the input text or its highlight. |
|
|
305
310
|
| OS notifications | Off by default. |
|
|
306
311
|
| External model downloads | Prohibited by default. Possible only where the owner has permitted it per model. |
|
|
307
312
|
| Credential expiry | The owner credential is created on first launch and follows the same policy as any other. REST credentials 90 days by default. The owner can set 1–365 days. A notice appears in the app 7 days before expiry. Expiry does not automatically cancel existing jobs but does block result access; access is possible again with reissued credentials for the same client. Explicit permission revocation also cancels in-progress jobs. |
|
|
@@ -344,7 +349,7 @@ Even if a result has already expired and no longer exists, calling again with a
|
|
|
344
349
|
### 5.2 Playback States
|
|
345
350
|
|
|
346
351
|
- Not ready for playback, Playing, Paused, Waiting for next segment, Stopped, and Playback ended are distinguished.
|
|
347
|
-
- GUI generation auto-plays when the first segment is ready unless auto-play is turned off per F-83. API and MCP generation never auto-play.
|
|
352
|
+
- GUI generation auto-plays when the first segment is ready unless auto-play is turned off per F-83. API and MCP generation never auto-plays by itself: its audio reaches the speakers only on an explicit play request under F-89, which the owner can turn off, and which leaves the reading surface bound to whatever the user has in it.
|
|
348
353
|
- Stopping or pausing playback does not change the generation state. Canceling generation also stops playback of that job.
|
|
349
354
|
- When input is replaced or a different history item is opened, existing playback is stopped and switched to the correct job so that existing audio is never played over different source text.
|
|
350
355
|
- Editing the input without replacing it does not stop playback. Once the input no longer matches the job's snapshot the highlight is dropped and reported as unavailable, per F-29, and it returns only if the text matches the snapshot again.
|
|
@@ -380,9 +385,9 @@ The scenarios below are mandatory acceptance items required for release approval
|
|
|
380
385
|
| A-07 | F-42–F-45, N-14–N-16 | Across one-off expiry, retention limits, deletion failure, missing files, corrupted backups, and errors during restore, there must be no loss of explicitly retained data and no false completion. |
|
|
381
386
|
| A-08 | F-45, F-48–F-49 | After a forced termination during generation, the interrupted state is shown. No automatic generation or playback. A valid re-request key must not create a duplicate job. |
|
|
382
387
|
| A-09 | F-17–F-23, F-47, N-03, N-04, N-21 | Even with simultaneous GUI, REST, and MCP requests, a single generation and the shared budget are maintained. The next request for the same model must not reload it, and on resource shortage the job must halt safely. |
|
|
383
|
-
| A-10 | F-46, F-50–F-52, N-17–N-19, N-31 | REST reachable on loopback only and never from another host, MCP
|
|
388
|
+
| A-10 | F-46, F-50–F-52, F-89, N-17–N-19, N-31 | REST reachable on loopback only and never from another host, MCP reachable by default and still useless without a credential, and blocking of unauthenticated requests, authentication failures, revoked permissions, access to another job ID, arbitrary paths, and invalid Host and Origin values. External generation must not auto-play on the speakers: with F-89's setting off a play request is refused permanently and nothing is heard, and with it on only the job that was asked for is played, while the input text, its highlight, and the owner's own playback are left as they were. A play request arriving while the owner is playing must be refused as busy with a retry-after hint, and the owner's own play or stop must always win. |
|
|
384
389
|
| A-11 | F-48–F-57, F-88 | Verify REST acceptance, status, ready segments, full WAV, cancellation, error codes, and paged queries. A duplicate key must return the same job, and different content must be a conflict. Retryable responses must carry a retry-after hint, and source-text and result-metadata requests must respect their separate authorization. A request with a missing or unrecognised job kind must be refused rather than defaulted, and every job representation must state its kind, and a request with no duplicate-prevention key must be refused. A bounded wait must return the terminal state when the job finishes inside it and the job ID when it does not, must never exceed the bound, and must leave the job unchanged either way. An estimate must validate, must not create a job or load a model, and must answer within N-22's figure. |
|
|
385
|
-
| A-12 | F-58–F-62, N-24 | On a real MCP client, tool discovery through generation, querying, result retrieval, and cancellation all succeed, with results delivered as MCP resources over stdio. With the app not running, with REST turned off, or with an expired credential, the MCP server must fail with a distinct reason and must not launch the app. Unsupported versions and invalid input are clearly refused, and MCP must expose no capability REST does not. Capability discovery must answer without a prior handshake, and result references must carry a freshness hint within the remaining result lifetime and be marked private. Repeating a create call with the same key must yield one job and not two, every voice must come back with a description, and the bounded wait and estimate tools must behave as A-11 requires of their REST equivalents. |
|
|
390
|
+
| A-12 | F-58–F-62, N-24 | On a real MCP client, tool discovery through generation, querying, result retrieval, and cancellation all succeed, with results delivered as MCP resources over stdio. With the app not running, with REST turned off, or with an expired credential, the MCP server must fail with a distinct reason and must not launch the app. Unsupported versions and invalid input are clearly refused, and MCP must expose no capability REST does not. Capability discovery must answer without a prior handshake, and result references must carry a freshness hint within the remaining result lifetime and be marked private. Repeating a create call with the same key must yield one job and not two, every voice must come back with a description, and the bounded wait and estimate tools must behave as A-11 requires of their REST equivalents. One speak call must generate and play in a single REST operation, must report the voice actually used when the settings were omitted, and must be refused exactly as A-10 requires when playback is off; play_speech must do the same for a job created earlier. |
|
|
386
391
|
| A-13 | N-20, N-25 | Default logs and backups must contain no credentials, and logs must contain no input source text. Verify whether one-off body text and audio remain after a normal exit with retention turned off. |
|
|
387
392
|
| A-14 | N-06, N-07, N-12, N-21–N-23 | In the reference environment of Section 8, measure highlight latency by the method in Section 8.2, along with API p95 response, cancellation time, time to first audio, and whole-app versus generation-job CPU, RAM, and disk separately. Shortfalls against targets must be recorded, not hidden. |
|
|
388
393
|
| A-15 | N-08, N-10, F-04 | On per-OS Windows and macOS install environments, verify launch, Korean and English generation, playback, database, REST, and MCP. Record the shipped model's minimum execution budget and any unsupported combinations, in the form Section 8.3 requires of every model. |
|
|
@@ -452,7 +457,7 @@ The minimum specification is an entry condition for running the app, not a guara
|
|
|
452
457
|
| --- | --- |
|
|
453
458
|
| First use | Review storage and privacy policy, including that the local REST service is on and listening on loopback and how to turn it off -> check models and download size -> prepare locally -> enter text -> generate and play. Retention, MCP, and issuing client credentials are the user's choice. |
|
|
454
459
|
| Everyday use | Enter text or select a retained document -> check the voice preset -> start reading -> check the bold position and control playback -> retain or export WAV as needed. |
|
|
455
|
-
| Registering an external integration | Owner confirms REST
|
|
460
|
+
| Registering an external integration | Owner confirms REST and MCP are on, which they are unless the owner turned them off -> issue a credential for that client with least privilege -> hand the client its connection details and credential -> authenticated requests -> retrieve status and results. If the client should be able to speak aloud, confirm F-89's playback setting is on. |
|
|
456
461
|
| Changing model or resources | Check active jobs -> choose apply-to-next-job or cancel-and-apply-immediately -> re-confirm the model and the actual budget. |
|
|
457
462
|
| Insufficient storage | Check usage -> clean up expired temporary data or back up and selectively delete retained data -> confirm free space and retry. |
|
|
458
463
|
| Generation failure | Check the failing stage and error code -> check model state, resource budget, and free space -> repair the model if needed -> retry as a new job. |
|
|
@@ -553,6 +558,8 @@ Recorded so implementation does not reopen them, and so a later reader can see w
|
|
|
553
558
|
| F-60, F-88, N-22 | A generation request may ask to wait a bounded time for completion instead of always returning an identifier to poll. | F-60 originally refused any blocking call because model preparation can take arbitrarily long. A.5 removed the premise for short work: warm load is 0.8 seconds and synthesis runs at about a fifth of real time, so a one-sentence request finishes well inside a second and forcing a second round trip to learn that is pure overhead for an automated caller. The bound is capped, never covers model preparation, and changes only when the response is sent, so the asynchronous lifecycle the rest of the document depends on is untouched. N-22 excludes the opt-in wait from its latency figure, because there the delay is the caller's own instruction rather than the service being slow. |
|
|
554
559
|
| F-49, F-54 | The duplicate-prevention key is required on every generation request rather than optional. | With one generation slot, a client that retries after a dropped or slow response would otherwise queue a second rendering of the same text behind the first, and automated clients retry as a matter of course. Making the key optional put the cost of that on whoever forgot it. There are no existing clients to break, so requiring it is free now and impossible later. |
|
|
555
560
|
| F-19 | The model is still released on cancellation, as originally written. | The concern was that a cancel, adjust, regenerate loop would pay a full reload each time. A.5 measured warm load at 0.8 seconds for the shipped model, so the cost is negligible and the simpler rule stands. Revisit only if a model heavy enough to be worth keeping resident ever ships. |
|
|
561
|
+
| F-51, F-89, Section 5.2 | An external request may be played on the host speakers, under an owner setting that is on by default. | F-51 originally reserved the speakers for the person sitting at the machine, and read as a privacy guarantee. It was also the reason an assistant asked to say something out loud could not: it generated a file and left the owner to go and press play, which is not what "speak" means. What is given up is the absolute form of that guarantee; what replaces it is narrower and checkable -- one owner setting, a permanent refusal when it is off, a busy refusal while the owner is playing, and no path by which external audio can touch the input text or its highlight. Requiring a separate credential permission was the alternative and was rejected because it would have made every credential already issued unable to speak until it was reissued, for a decision the owner had already made once in Settings. |
|
|
562
|
+
| F-46, Section 4.1 | MCP is on by default rather than off. | Off by default protected a setting, not an exposure: the MCP server is a short-lived stdio process that the client starts, it listens on nothing, and without a REST credential -- which only the owner can issue -- it can do nothing at all. REST, which is the thing that actually listens, has been on by default all along. The cost of the old default was that every new client failed once, silently enough that the remedy was a documentation question; that is now one switch the owner can still turn off. |
|
|
556
563
|
| F-47, F-50 | Requests carry no priority by path; the owner reclaims the slot by cancelling. | Preemption for GUI requests would require deciding what happens to a client's half-finished job, a policy F-49 and F-50 do not have. Cancelling is already reachable in one place per F-69. The cost is that a client running at the rate limit can hold the slot until the owner intervenes, which F-50 now states rather than implies. |
|
|
557
564
|
|
|
558
565
|
### A.4 Verification Backlog
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<rect x="7" y="28.5" width="15" height="7" rx="3.5" fill="#3f4bd6"/>
|
|
3
|
+
<rect x="3.5" y="24.5" width="22" height="15" rx="7.5" fill="none" stroke="#3f4bd6" stroke-width="3.2"/>
|
|
4
|
+
<rect x="29" y="24.5" width="22" height="15" rx="7.5" fill="none" stroke="#3f4bd6" stroke-width="3.2" opacity=".48"/>
|
|
5
|
+
<rect x="45.5" y="24.5" width="15" height="15" rx="7.5" fill="none" stroke="#3f4bd6" stroke-width="3.2" opacity=".24"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<circle cx="32" cy="39.5" r="4.8" fill="#3f4bd6"/>
|
|
3
|
+
<path d="M 23 39.5 A 9 9 0 0 1 41 39.5" fill="none" stroke="#3f4bd6" stroke-width="4" stroke-linecap="round" opacity=".8"/>
|
|
4
|
+
<path d="M 16 39.5 A 16 16 0 0 1 48 39.5" fill="none" stroke="#3f4bd6" stroke-width="3.7" stroke-linecap="round" opacity=".52"/>
|
|
5
|
+
<path d="M 9 39.5 A 23 23 0 0 1 55 39.5" fill="none" stroke="#3f4bd6" stroke-width="3.4" stroke-linecap="round" opacity=".28"/>
|
|
6
|
+
<path d="M 9 52 H 55" fill="none" stroke="#3f4bd6" stroke-width="4.4" stroke-linecap="round" opacity=".9"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<circle cx="32" cy="32" r="5.4" fill="#3f4bd6"/>
|
|
3
|
+
<circle cx="32" cy="32" r="14.5" fill="none" stroke="#3f4bd6" stroke-width="4.2" opacity=".8"/>
|
|
4
|
+
<circle cx="32" cy="32" r="23.5" fill="none" stroke="#3f4bd6" stroke-width="3.8" opacity=".5"/>
|
|
5
|
+
<circle cx="32" cy="32" r="29.6" fill="none" stroke="#3f4bd6" stroke-width="3.4" opacity=".26"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
<title>Ripple Candidates</title>
|
|
2
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@600;700;800&family=Instrument+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
|
3
|
+
<style>
|
|
4
|
+
:root{
|
|
5
|
+
--paper:#f6f6fa; --panel:#ffffff; --ink:#1c1e2b; --muted:#5c5f72;
|
|
6
|
+
--line:#e2e3ee; --accent:#3f4bd6; --accent-soft:#e8eaff;
|
|
7
|
+
}
|
|
8
|
+
@media (prefers-color-scheme: dark){
|
|
9
|
+
:root:not([data-theme="light"]){
|
|
10
|
+
--paper:#15161e; --panel:#1c1e28; --ink:#e7e8f2; --muted:#9a9db2;
|
|
11
|
+
--line:#2b2d3b; --accent:#7c86f5; --accent-soft:#26284a;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
:root[data-theme="dark"]{
|
|
15
|
+
--paper:#15161e; --panel:#1c1e28; --ink:#e7e8f2; --muted:#9a9db2;
|
|
16
|
+
--line:#2b2d3b; --accent:#7c86f5; --accent-soft:#26284a;
|
|
17
|
+
}
|
|
18
|
+
*{box-sizing:border-box}
|
|
19
|
+
body{
|
|
20
|
+
margin:0; background:var(--paper); color:var(--ink);
|
|
21
|
+
font:400 14.5px/1.55 "Instrument Sans","Segoe UI",system-ui,sans-serif;
|
|
22
|
+
padding-inline:clamp(16px,4vw,28px);
|
|
23
|
+
}
|
|
24
|
+
.wrap{max-width:1080px;margin:0 auto;padding-block:44px 72px}
|
|
25
|
+
.eyebrow{
|
|
26
|
+
font:500 11px/1 "IBM Plex Mono",ui-monospace,monospace;
|
|
27
|
+
letter-spacing:.16em;text-transform:uppercase;color:var(--muted);margin:0 0 14px;
|
|
28
|
+
}
|
|
29
|
+
h1{
|
|
30
|
+
font:800 clamp(32px,6vw,46px)/1.05 "Bricolage Grotesque","Instrument Sans",sans-serif;
|
|
31
|
+
letter-spacing:-0.022em;text-wrap:balance;margin:0 0 12px;
|
|
32
|
+
}
|
|
33
|
+
.lede{max-width:62ch;color:var(--muted);font-size:15.5px;margin:0}
|
|
34
|
+
h2{
|
|
35
|
+
font:700 21px/1.2 "Bricolage Grotesque","Instrument Sans",sans-serif;
|
|
36
|
+
letter-spacing:-0.01em;margin:56px 0 6px;
|
|
37
|
+
}
|
|
38
|
+
.h2note{color:var(--muted);margin:0 0 20px;max-width:62ch}
|
|
39
|
+
|
|
40
|
+
/* grammar strip */
|
|
41
|
+
.grammar{
|
|
42
|
+
display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));
|
|
43
|
+
gap:18px 26px;border-block:1px solid var(--line);padding-block:18px;margin-top:30px;
|
|
44
|
+
}
|
|
45
|
+
.g-item{display:flex;gap:12px;align-items:center}
|
|
46
|
+
.g-item svg{color:var(--accent);flex:none}
|
|
47
|
+
.g-item b{display:block;font-weight:600;font-size:13.5px}
|
|
48
|
+
.g-item span{display:block;color:var(--muted);font-size:12.5px;line-height:1.4}
|
|
49
|
+
|
|
50
|
+
/* candidate cards */
|
|
51
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(242px,1fr));gap:18px}
|
|
52
|
+
.card{
|
|
53
|
+
background:var(--panel);border:1px solid var(--line);border-radius:14px;
|
|
54
|
+
padding:20px 20px 16px;display:flex;flex-direction:column;gap:12px;
|
|
55
|
+
}
|
|
56
|
+
.stage{display:flex;align-items:center;justify-content:center;height:132px;color:var(--accent)}
|
|
57
|
+
.stage svg{transition:transform .25s ease,opacity .25s ease}
|
|
58
|
+
@media (prefers-reduced-motion:no-preference){
|
|
59
|
+
.card:hover .stage svg{transform:scale(1.045);animation:ripple .7s ease-out}
|
|
60
|
+
}
|
|
61
|
+
@keyframes ripple{from{transform:scale(.93);opacity:.55}}
|
|
62
|
+
.card h3{font:700 19px/1.2 "Bricolage Grotesque",sans-serif;margin:0;letter-spacing:-0.01em}
|
|
63
|
+
.role{
|
|
64
|
+
font:500 10.5px/1 "IBM Plex Mono",monospace;letter-spacing:.14em;
|
|
65
|
+
text-transform:uppercase;color:var(--accent);
|
|
66
|
+
}
|
|
67
|
+
.card p{margin:0;color:var(--muted);font-size:13.5px;line-height:1.55}
|
|
68
|
+
.ladder{
|
|
69
|
+
display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap;
|
|
70
|
+
border-top:1px solid var(--line);padding-top:14px;color:var(--accent);
|
|
71
|
+
}
|
|
72
|
+
.ladder figure{margin:0;display:flex;flex-direction:column;align-items:center;gap:6px}
|
|
73
|
+
.ladder figcaption{font:400 10px/1 "IBM Plex Mono",monospace;color:var(--muted)}
|
|
74
|
+
.on-dark{
|
|
75
|
+
background:#15161e;border-radius:10px;padding:10px 14px;margin-top:12px;
|
|
76
|
+
display:flex;align-items:center;gap:14px;
|
|
77
|
+
}
|
|
78
|
+
.on-dark .sw{color:#7c86f5;display:flex;align-items:center;gap:14px}
|
|
79
|
+
.on-dark .tag{
|
|
80
|
+
font:400 10px/1 "IBM Plex Mono",monospace;color:#8a8da8;margin-left:auto;
|
|
81
|
+
letter-spacing:.06em;white-space:nowrap;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* lockups */
|
|
85
|
+
.locks{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:18px}
|
|
86
|
+
.lock{
|
|
87
|
+
background:var(--panel);border:1px solid var(--line);border-radius:14px;
|
|
88
|
+
min-height:150px;display:flex;flex-direction:column;align-items:center;
|
|
89
|
+
justify-content:center;gap:6px;padding:26px 20px 14px;position:relative;
|
|
90
|
+
}
|
|
91
|
+
.lock .cap{
|
|
92
|
+
position:absolute;bottom:10px;left:0;right:0;text-align:center;
|
|
93
|
+
font:400 10px/1 "IBM Plex Mono",monospace;color:var(--muted);letter-spacing:.08em;
|
|
94
|
+
}
|
|
95
|
+
.wordmark{display:flex;align-items:center;gap:13px}
|
|
96
|
+
.wordmark .wm{
|
|
97
|
+
font:800 30px/1 "Bricolage Grotesque",sans-serif;letter-spacing:-0.02em;color:var(--ink);
|
|
98
|
+
}
|
|
99
|
+
.wordmark .wm i{font-style:normal;color:var(--accent)}
|
|
100
|
+
.hangul{
|
|
101
|
+
font:500 12px/1 "Malgun Gothic","Apple SD Gothic Neo",sans-serif;
|
|
102
|
+
letter-spacing:.42em;color:var(--muted);text-indent:.42em;
|
|
103
|
+
}
|
|
104
|
+
.stack{display:flex;flex-direction:column;align-items:center;gap:10px}
|
|
105
|
+
.tile{
|
|
106
|
+
width:88px;height:88px;border-radius:21%;display:flex;align-items:center;
|
|
107
|
+
justify-content:center;flex:none;
|
|
108
|
+
}
|
|
109
|
+
.t-accent{background:#3f4bd6;color:#ffffff;box-shadow:0 6px 18px rgba(63,75,214,.28)}
|
|
110
|
+
.t-light{background:#ffffff;color:#3f4bd6;border:1px solid #e2e3ee}
|
|
111
|
+
.t-dark{background:#1c1e28;color:#7c86f5;border:1px solid #2b2d3b}
|
|
112
|
+
.tiles{display:flex;gap:16px;align-items:center}
|
|
113
|
+
|
|
114
|
+
/* taskbars */
|
|
115
|
+
.taskbar{
|
|
116
|
+
height:52px;border-radius:12px;display:flex;align-items:center;
|
|
117
|
+
gap:6px;padding:0 12px;margin-top:14px;overflow-x:auto;
|
|
118
|
+
}
|
|
119
|
+
.tb-dark{background:#1a1b22;border:1px solid #2b2d3b}
|
|
120
|
+
.tb-light{background:#eceef6;border:1px solid var(--line)}
|
|
121
|
+
.tb-item{display:flex;align-items:center;gap:9px;padding:7px 12px;border-radius:8px;font-size:13px;white-space:nowrap}
|
|
122
|
+
.tb-dark .tb-item{color:#d6d8e6}
|
|
123
|
+
.tb-light .tb-item{color:#33364a}
|
|
124
|
+
.tb-item.active-d{background:#2b2d3e}
|
|
125
|
+
.tb-item.active-l{background:#ffffff;box-shadow:0 1px 3px rgba(20,22,40,.12)}
|
|
126
|
+
.tb-cap{font:400 10.5px/1 "IBM Plex Mono",monospace;color:var(--muted);letter-spacing:.06em;margin:8px 0 0}
|
|
127
|
+
|
|
128
|
+
/* recommendation */
|
|
129
|
+
.pick{
|
|
130
|
+
background:var(--accent-soft);border-radius:16px;padding:26px 26px 24px;margin-top:20px;
|
|
131
|
+
display:grid;grid-template-columns:auto 1fr;gap:26px;align-items:center;
|
|
132
|
+
}
|
|
133
|
+
.pick .vis{display:flex;align-items:center;gap:16px;color:var(--accent)}
|
|
134
|
+
.pick .vis .arrow{font:400 22px/1 "IBM Plex Mono",monospace;color:var(--muted)}
|
|
135
|
+
.pick h3{font:700 19px/1.25 "Bricolage Grotesque",sans-serif;margin:0 0 8px}
|
|
136
|
+
.pick p{margin:0 0 8px;max-width:60ch;font-size:14px;line-height:1.6}
|
|
137
|
+
.pick p:last-child{margin-bottom:0}
|
|
138
|
+
.pick strong{font-weight:600}
|
|
139
|
+
@media (max-width:640px){ .pick{grid-template-columns:1fr} }
|
|
140
|
+
</style>
|
|
141
|
+
|
|
142
|
+
<div class="wrap">
|
|
143
|
+
|
|
144
|
+
<p class="eyebrow">EchoAct · identity study · one concept, four drawings</p>
|
|
145
|
+
<h1>Ripple Candidates</h1>
|
|
146
|
+
<p class="lede">The Korean word for echo — <b>에코</b> — begins with <b>ㅇ</b>, a circle.
|
|
147
|
+
Four drawings of that idea, each shown at the sizes it has to survive and on both
|
|
148
|
+
grounds the app ships on. The accent is EchoAct's own <span style="font-family:'IBM Plex Mono',monospace;font-size:13px">#3f4bd6</span>.</p>
|
|
149
|
+
|
|
150
|
+
<div class="grammar">
|
|
151
|
+
<div class="g-item">
|
|
152
|
+
<svg width="34" height="34" viewBox="0 0 64 64"><circle cx="32" cy="32" r="7" fill="currentColor"/></svg>
|
|
153
|
+
<div><b>The filled dot</b><span>the source text — the only solid thing</span></div>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="g-item">
|
|
156
|
+
<svg width="34" height="34" viewBox="0 0 64 64"><circle cx="32" cy="32" r="11" fill="none" stroke="currentColor" stroke-width="4.4" opacity=".85"/><circle cx="32" cy="32" r="22" fill="none" stroke="currentColor" stroke-width="4" opacity=".45"/></svg>
|
|
157
|
+
<div><b>The outline rings</b><span>voice travelling outward, thinning as it goes</span></div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="g-item">
|
|
160
|
+
<svg width="34" height="34" viewBox="0 0 64 64"><rect x="9" y="9" width="46" height="46" rx="14" fill="none" stroke="currentColor" stroke-width="4.4"/><rect x="21" y="21" width="22" height="22" rx="7" fill="none" stroke="currentColor" stroke-width="4.4"/></svg>
|
|
161
|
+
<div><b>Outline, never weight</b><span>the app's own marking rule: nothing shifts when emphasised</span></div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
166
|
+
<defs>
|
|
167
|
+
<symbol id="a-full" viewBox="0 0 64 64">
|
|
168
|
+
<circle cx="32" cy="32" r="5.4" fill="currentColor"/>
|
|
169
|
+
<circle cx="32" cy="32" r="14.5" fill="none" stroke="currentColor" stroke-width="4.2" opacity=".8"/>
|
|
170
|
+
<circle cx="32" cy="32" r="23.5" fill="none" stroke="currentColor" stroke-width="3.8" opacity=".5"/>
|
|
171
|
+
<circle cx="32" cy="32" r="29.6" fill="none" stroke="currentColor" stroke-width="3.4" opacity=".26"/>
|
|
172
|
+
</symbol>
|
|
173
|
+
<symbol id="a-sm" viewBox="0 0 64 64">
|
|
174
|
+
<circle cx="32" cy="32" r="6.2" fill="currentColor"/>
|
|
175
|
+
<circle cx="32" cy="32" r="16.5" fill="none" stroke="currentColor" stroke-width="5" opacity=".85"/>
|
|
176
|
+
<circle cx="32" cy="32" r="26.5" fill="none" stroke="currentColor" stroke-width="4.4" opacity=".45"/>
|
|
177
|
+
</symbol>
|
|
178
|
+
<symbol id="b-mark" viewBox="0 0 64 64">
|
|
179
|
+
<circle cx="32" cy="32" r="20.5" fill="none" stroke="currentColor" stroke-width="7.5"/>
|
|
180
|
+
<circle cx="32" cy="32" r="5.2" fill="currentColor"/>
|
|
181
|
+
</symbol>
|
|
182
|
+
<symbol id="c-mark" viewBox="0 0 64 64">
|
|
183
|
+
<circle cx="32" cy="39.5" r="4.8" fill="currentColor"/>
|
|
184
|
+
<path d="M 23 39.5 A 9 9 0 0 1 41 39.5" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity=".8"/>
|
|
185
|
+
<path d="M 16 39.5 A 16 16 0 0 1 48 39.5" fill="none" stroke="currentColor" stroke-width="3.7" stroke-linecap="round" opacity=".52"/>
|
|
186
|
+
<path d="M 9 39.5 A 23 23 0 0 1 55 39.5" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round" opacity=".28"/>
|
|
187
|
+
<path d="M 9 52 H 55" fill="none" stroke="currentColor" stroke-width="4.4" stroke-linecap="round" opacity=".9"/>
|
|
188
|
+
</symbol>
|
|
189
|
+
<symbol id="d-mark" viewBox="0 0 64 64">
|
|
190
|
+
<rect x="7" y="28.5" width="15" height="7" rx="3.5" fill="currentColor"/>
|
|
191
|
+
<rect x="3.5" y="24.5" width="22" height="15" rx="7.5" fill="none" stroke="currentColor" stroke-width="3.2"/>
|
|
192
|
+
<rect x="29" y="24.5" width="22" height="15" rx="7.5" fill="none" stroke="currentColor" stroke-width="3.2" opacity=".48"/>
|
|
193
|
+
<rect x="45.5" y="24.5" width="15" height="15" rx="7.5" fill="none" stroke="currentColor" stroke-width="3.2" opacity=".24"/>
|
|
194
|
+
</symbol>
|
|
195
|
+
</defs>
|
|
196
|
+
</svg>
|
|
197
|
+
|
|
198
|
+
<h2>The four drawings</h2>
|
|
199
|
+
<p class="h2note">Below 32 px the ladder drops the faintest ring — that reduction is deliberate, not an accident of size.</p>
|
|
200
|
+
<div class="grid">
|
|
201
|
+
|
|
202
|
+
<div class="card">
|
|
203
|
+
<div class="stage"><svg width="118" height="118"><use href="#a-full"/></svg></div>
|
|
204
|
+
<p class="role">a · the plain statement</p>
|
|
205
|
+
<h3>Origin</h3>
|
|
206
|
+
<p>The literal idea: voice leaving a fixed point. Three rings at large sizes,
|
|
207
|
+
two under 32 px. Nothing to misread anywhere — and nothing wasted.</p>
|
|
208
|
+
<div class="ladder">
|
|
209
|
+
<figure><svg width="48" height="48"><use href="#a-full"/></svg><figcaption>48</figcaption></figure>
|
|
210
|
+
<figure><svg width="32" height="32"><use href="#a-sm"/></svg><figcaption>32</figcaption></figure>
|
|
211
|
+
<figure><svg width="20" height="20"><use href="#a-sm"/></svg><figcaption>20</figcaption></figure>
|
|
212
|
+
<figure><svg width="16" height="16"><use href="#a-sm"/></svg><figcaption>16</figcaption></figure>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="on-dark"><span class="sw"><svg width="44" height="44"><use href="#a-full"/></svg><svg width="24" height="24"><use href="#a-sm"/></svg><svg width="16" height="16"><use href="#a-sm"/></svg></span><span class="tag">dark taskbar</span></div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="card">
|
|
218
|
+
<div class="stage"><svg width="118" height="118"><use href="#b-mark"/></svg></div>
|
|
219
|
+
<p class="role">b · the letter is the picture</p>
|
|
220
|
+
<h3>Ieung</h3>
|
|
221
|
+
<p>A heavy ring and one dot: ㅇ itself, with the sound's origin inside it.
|
|
222
|
+
Two elements only, so it survives every size — the honest favicon.</p>
|
|
223
|
+
<div class="ladder">
|
|
224
|
+
<figure><svg width="48" height="48"><use href="#b-mark"/></svg><figcaption>48</figcaption></figure>
|
|
225
|
+
<figure><svg width="32" height="32"><use href="#b-mark"/></svg><figcaption>32</figcaption></figure>
|
|
226
|
+
<figure><svg width="20" height="20"><use href="#b-mark"/></svg><figcaption>20</figcaption></figure>
|
|
227
|
+
<figure><svg width="16" height="16"><use href="#b-mark"/></svg><figcaption>16</figcaption></figure>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="on-dark"><span class="sw"><svg width="44" height="44"><use href="#b-mark"/></svg><svg width="24" height="24"><use href="#b-mark"/></svg><svg width="16" height="16"><use href="#b-mark"/></svg></span><span class="tag">dark taskbar</span></div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="card">
|
|
233
|
+
<div class="stage"><svg width="118" height="118"><use href="#c-mark"/></svg></div>
|
|
234
|
+
<p class="role">c · text speaks</p>
|
|
235
|
+
<h3>Horizon</h3>
|
|
236
|
+
<p>A line of reading, and ripples rising from a point on it — what the app
|
|
237
|
+
actually does, in two strokes. The baseline is load-bearing: it turns echo into <em>read-aloud</em>.</p>
|
|
238
|
+
<div class="ladder">
|
|
239
|
+
<figure><svg width="48" height="48"><use href="#c-mark"/></svg><figcaption>48</figcaption></figure>
|
|
240
|
+
<figure><svg width="32" height="32"><use href="#c-mark"/></svg><figcaption>32</figcaption></figure>
|
|
241
|
+
<figure><svg width="20" height="20"><use href="#c-mark"/></svg><figcaption>20</figcaption></figure>
|
|
242
|
+
<figure><svg width="16" height="16"><use href="#c-mark"/></svg><figcaption>16</figcaption></figure>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="on-dark"><span class="sw"><svg width="44" height="44"><use href="#c-mark"/></svg><svg width="24" height="24"><use href="#c-mark"/></svg><svg width="16" height="16"><use href="#c-mark"/></svg></span><span class="tag">dark taskbar</span></div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div class="card">
|
|
248
|
+
<div class="stage"><svg width="118" height="118"><use href="#d-mark"/></svg></div>
|
|
249
|
+
<p class="role">d · the mark repeats</p>
|
|
250
|
+
<h3>Caret Echo</h3>
|
|
251
|
+
<p>The reading caret from the main screen — an outline, never a heavier
|
|
252
|
+
weight — reproduced three times and fading, so the icon's own signature is "echo."</p>
|
|
253
|
+
<div class="ladder">
|
|
254
|
+
<figure><svg width="48" height="48"><use href="#d-mark"/></svg><figcaption>48</figcaption></figure>
|
|
255
|
+
<figure><svg width="32" height="32"><use href="#d-mark"/></svg><figcaption>32</figcaption></figure>
|
|
256
|
+
<figure><svg width="20" height="20"><use href="#d-mark"/></svg><figcaption>20</figcaption></figure>
|
|
257
|
+
<figure><svg width="16" height="16"><use href="#d-mark"/></svg><figcaption>16</figcaption></figure>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="on-dark"><span class="sw"><svg width="44" height="44"><use href="#d-mark"/></svg><svg width="24" height="24"><use href="#d-mark"/></svg><svg width="16" height="16"><use href="#d-mark"/></svg></span><span class="tag">dark taskbar</span></div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<h2>Lockups and the tile</h2>
|
|
265
|
+
<p class="h2note">One accent, one ground colour per state. The wordmark sets “Act” in the accent so the name says what it does.</p>
|
|
266
|
+
<div class="locks">
|
|
267
|
+
|
|
268
|
+
<div class="lock">
|
|
269
|
+
<div class="wordmark">
|
|
270
|
+
<svg width="44" height="44" style="color:var(--accent)"><use href="#a-sm"/></svg>
|
|
271
|
+
<span class="wm">Echo<i>Act</i></span>
|
|
272
|
+
</div>
|
|
273
|
+
<span class="hangul">에코액트</span>
|
|
274
|
+
<span class="cap">horizontal · window title, README</span>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div class="lock">
|
|
278
|
+
<div class="stack">
|
|
279
|
+
<svg width="52" height="52" style="color:var(--accent)"><use href="#a-full"/></svg>
|
|
280
|
+
<span class="wm" style="font-size:22px">Echo<i style="font-style:normal;color:var(--accent)">Act</i></span>
|
|
281
|
+
</div>
|
|
282
|
+
<span class="cap">stacked · about screen</span>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<div class="lock">
|
|
286
|
+
<div class="tiles">
|
|
287
|
+
<div class="tile t-accent"><svg width="58" height="58"><use href="#a-full"/></svg></div>
|
|
288
|
+
<div class="tile t-light"><svg width="58" height="58"><use href="#a-full"/></svg></div>
|
|
289
|
+
<div class="tile t-dark"><svg width="52" height="52"><use href="#b-mark"/></svg></div>
|
|
290
|
+
</div>
|
|
291
|
+
<span class="cap">app tile · accent / light / dark</span>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<h2>On the taskbar</h2>
|
|
297
|
+
<p class="h2note">The real test at 26 px, next to strangers. The tile carries it; the bare mark does not need to.</p>
|
|
298
|
+
<div class="taskbar tb-dark">
|
|
299
|
+
<div class="tile" style="width:28px;height:28px;border-radius:7px;background:#3f4bd6;color:#fff;flex:none"><svg width="20" height="20"><use href="#a-sm"/></svg></div>
|
|
300
|
+
<span class="tb-item">EchoAct</span>
|
|
301
|
+
<span class="tb-item active-d">Explorer</span>
|
|
302
|
+
<span class="tb-item">Terminal</span>
|
|
303
|
+
<span class="tb-item">Mail</span>
|
|
304
|
+
</div>
|
|
305
|
+
<p class="tb-cap">windows 11 · dark</p>
|
|
306
|
+
<div class="taskbar tb-light">
|
|
307
|
+
<div class="tile" style="width:28px;height:28px;border-radius:7px;background:#3f4bd6;color:#fff;flex:none"><svg width="20" height="20"><use href="#a-sm"/></svg></div>
|
|
308
|
+
<span class="tb-item">EchoAct</span>
|
|
309
|
+
<span class="tb-item active-l">Explorer</span>
|
|
310
|
+
<span class="tb-item">Terminal</span>
|
|
311
|
+
<span class="tb-item">Mail</span>
|
|
312
|
+
</div>
|
|
313
|
+
<p class="tb-cap">windows 11 · light</p>
|
|
314
|
+
|
|
315
|
+
<h2>The pick</h2>
|
|
316
|
+
<div class="pick">
|
|
317
|
+
<div class="vis">
|
|
318
|
+
<svg width="72" height="72"><use href="#a-full"/></svg>
|
|
319
|
+
<span class="arrow">→</span>
|
|
320
|
+
<svg width="72" height="72"><use href="#b-mark"/></svg>
|
|
321
|
+
</div>
|
|
322
|
+
<div>
|
|
323
|
+
<h3>Origin as the icon; Ieung is already hiding inside it</h3>
|
|
324
|
+
<p>Take Origin's dot and its heaviest ring and discard the rest — that is Ieung.
|
|
325
|
+
So the small form is not a second design: <strong>it is the same drawing, told the truth about 16 px.</strong>
|
|
326
|
+
Ship Origin for the tile and lockups, and let the ladder fall to Ieung under 24 px.</p>
|
|
327
|
+
<p>Horizon earns the about screen and the installer, where the story is told in one
|
|
328
|
+
sentence — and Caret Echo belongs to the loading state: a caret ripple animating outward
|
|
329
|
+
while the first sentence renders. One concept, four jobs, zero extra invention.</p>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""EchoAct — local Korean/English speech generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError
|
|
6
|
+
from importlib.metadata import version as _pkg_version
|
|
7
|
+
|
|
8
|
+
# Single source of truth is pyproject.toml's ``version`` — read it from
|
|
9
|
+
# the installed package metadata so release bumps can't drift (0.5.3
|
|
10
|
+
# shipped with a stale hand-maintained string here). The fallback only
|
|
11
|
+
# fires when the package isn't installed at all (e.g. sources vendored
|
|
12
|
+
# without ``uv sync``).
|
|
13
|
+
try:
|
|
14
|
+
__version__ = _pkg_version("echoact")
|
|
15
|
+
except PackageNotFoundError: # pragma: no cover
|
|
16
|
+
__version__ = "0.0.0.dev0"
|
|
@@ -53,13 +53,29 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
53
53
|
from PySide6.QtWidgets import QApplication
|
|
54
54
|
|
|
55
55
|
from .app import Application
|
|
56
|
-
from .ui import theme
|
|
56
|
+
from .ui import brand, theme
|
|
57
57
|
from .ui.main_window import MainWindow
|
|
58
58
|
|
|
59
|
+
if sys.platform == "win32":
|
|
60
|
+
# The taskbar attributes its button to the *launcher process*, not
|
|
61
|
+
# to Qt: run through a script shim and the button wears Python's
|
|
62
|
+
# logo however good the window icon is (that only reaches the title
|
|
63
|
+
# bar). Declaring an application identity makes the button ours.
|
|
64
|
+
import ctypes
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("com.echoact.app")
|
|
68
|
+
except OSError: # pragma: no cover - a stripped shell must not stop the app
|
|
69
|
+
pass
|
|
70
|
+
|
|
59
71
|
QApplication.setAttribute(Qt.ApplicationAttribute.AA_DontCreateNativeWidgetSiblings, True)
|
|
60
72
|
qt = QApplication(argv)
|
|
61
73
|
qt.setApplicationName("EchoAct")
|
|
62
74
|
qt.setOrganizationName("EchoAct")
|
|
75
|
+
# The face in the taskbar and Alt-Tab: the brand on its tile, in the
|
|
76
|
+
# theme the window is about to resolve to. Set on the application so
|
|
77
|
+
# every dialog inherits it rather than wearing Qt's generic logo.
|
|
78
|
+
qt.setWindowIcon(brand.app_icon(theme.palette_for(theme.Mode.SYSTEM)))
|
|
63
79
|
|
|
64
80
|
# Before anything measures a glyph: a face registered later would not
|
|
65
81
|
# be the one the first layout used, and N-13's stability is about the
|