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/paths.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Where EchoAct keeps things on disk.
|
|
2
|
+
|
|
3
|
+
One module so that F-72's diagnostic export can state every location, F-73
|
|
4
|
+
can size each of them separately, and N-20 can keep the user's home path out
|
|
5
|
+
of logs by rendering paths relative to these roots.
|
|
6
|
+
|
|
7
|
+
An ``ECHOACT_DATA_DIR`` override exists for tests and for running two builds
|
|
8
|
+
side by side; nothing in the product writes outside the returned tree except
|
|
9
|
+
files the user explicitly chooses (exported WAV, backups).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
APP_NAME = "EchoAct"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@lru_cache(maxsize=1)
|
|
23
|
+
def data_dir() -> Path:
|
|
24
|
+
"""Per-user application data: database, audio, settings, logs."""
|
|
25
|
+
override = os.environ.get("ECHOACT_DATA_DIR")
|
|
26
|
+
if override:
|
|
27
|
+
return Path(override).expanduser()
|
|
28
|
+
if sys.platform == "win32":
|
|
29
|
+
base = os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")
|
|
30
|
+
return Path(base) / APP_NAME
|
|
31
|
+
if sys.platform == "darwin":
|
|
32
|
+
return Path.home() / "Library" / "Application Support" / APP_NAME
|
|
33
|
+
return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / APP_NAME
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def db_path() -> Path:
|
|
37
|
+
return data_dir() / "echoact.sqlite3"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def audio_dir() -> Path:
|
|
41
|
+
"""Retained job audio and full-result WAV files."""
|
|
42
|
+
return data_dir() / "audio"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def temp_dir() -> Path:
|
|
46
|
+
"""Segment audio for in-flight jobs and one-off results.
|
|
47
|
+
|
|
48
|
+
N-02 requires whatever a forced termination leaves here to be cleaned up
|
|
49
|
+
on relaunch, so nothing durable may live in this tree.
|
|
50
|
+
"""
|
|
51
|
+
return data_dir() / "temp"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def log_dir() -> Path:
|
|
55
|
+
return data_dir() / "logs"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def model_cache_dir() -> Path:
|
|
59
|
+
"""Model weights. Kept apart from user data so F-73 can size it, F-65
|
|
60
|
+
can delete it, and F-76 can offer it as a separate deletion scope."""
|
|
61
|
+
override = os.environ.get("ECHOACT_MODEL_DIR")
|
|
62
|
+
if override:
|
|
63
|
+
return Path(override).expanduser()
|
|
64
|
+
return data_dir() / "models"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def settings_path() -> Path:
|
|
68
|
+
return data_dir() / "settings.json"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def lock_path() -> Path:
|
|
72
|
+
"""F-85's single-instance marker."""
|
|
73
|
+
return data_dir() / "instance.lock"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def ensure_tree() -> None:
|
|
77
|
+
"""Create every directory the app writes to. Safe to call repeatedly."""
|
|
78
|
+
for p in (data_dir(), audio_dir(), temp_dir(), log_dir(), model_cache_dir()):
|
|
79
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def redact(path: str | Path) -> str:
|
|
83
|
+
"""Render a path for a log or a diagnostic export.
|
|
84
|
+
|
|
85
|
+
N-20 and F-72 exclude the user's home path. Anything under the data
|
|
86
|
+
directory becomes ``<data>/...``; anything else becomes its basename, so
|
|
87
|
+
a filename the user chose never leaks its directory.
|
|
88
|
+
"""
|
|
89
|
+
p = Path(path)
|
|
90
|
+
try:
|
|
91
|
+
return "<data>/" + p.resolve().relative_to(data_dir().resolve()).as_posix()
|
|
92
|
+
except (ValueError, OSError):
|
|
93
|
+
return "<path>/" + p.name
|
echoact/policy.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Section 4's operating policy, as constants.
|
|
2
|
+
|
|
3
|
+
Every number the requirements fix lives here and nowhere else, so that a
|
|
4
|
+
policy change is one edit and a reviewer can check the document against one
|
|
5
|
+
file. Nothing here is read from disk: these are the *defaults* that
|
|
6
|
+
``echoact.config.settings`` starts from and that safety limits clamp against.
|
|
7
|
+
|
|
8
|
+
Units follow Section 4.1's closing rule: memory in GiB (2**30), storage in
|
|
9
|
+
GB (10**9), transfer in bytes, CPU as a percentage of total logical capacity.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Final
|
|
15
|
+
|
|
16
|
+
GIB: Final = 1 << 30
|
|
17
|
+
GB: Final = 1_000_000_000
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------- input ---
|
|
20
|
+
MAX_INPUT_CODEPOINTS: Final = 50_000 # F-03
|
|
21
|
+
MAX_IMPORT_FILE_BYTES: Final = 2_000_000 # F-03
|
|
22
|
+
MAX_REQUEST_BODY_BYTES: Final = 2_000_000 # 4.1, includes upload metadata
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------ segmenting ---
|
|
25
|
+
# F-81. A segment is capped by code points and by estimated audio seconds;
|
|
26
|
+
# the first segment of a job is capped harder so playback can start sooner.
|
|
27
|
+
SEGMENT_MAX_CODEPOINTS: Final = 200
|
|
28
|
+
SEGMENT_MAX_SECONDS: Final = 20.0
|
|
29
|
+
FIRST_SEGMENT_MAX_SECONDS: Final = 8.0
|
|
30
|
+
SEGMENT_MIN_CODEPOINTS: Final = 20
|
|
31
|
+
|
|
32
|
+
# Estimating audio length from source length.
|
|
33
|
+
#
|
|
34
|
+
# F-81 splits on an estimated duration and F-88 quotes one to a caller, so
|
|
35
|
+
# the numbers have to come from the engine rather than from intuition. These
|
|
36
|
+
# are measured against Supertonic 3 at tempo 1.00, two threads, voice F1:
|
|
37
|
+
# Korean 6.0 code points per second of audio (range 3.7 to 6.4), English 13.3
|
|
38
|
+
# (range 7.2 to 14.0). Short sentences sit at the low end because onset and
|
|
39
|
+
# trailing silence are a larger fraction of them, so using the median
|
|
40
|
+
# over-estimates a short segment's length -- which is the safe direction for
|
|
41
|
+
# a cap. Tempo divides duration almost exactly: 0.70x measured 1.43x the
|
|
42
|
+
# 1.00x length and 1.50x measured 0.67x, so ``seconds / tempo`` is the model.
|
|
43
|
+
ESTIMATE_CODEPOINTS_PER_SECOND_KO: Final = 6.0
|
|
44
|
+
ESTIMATE_CODEPOINTS_PER_SECOND_EN: Final = 13.3
|
|
45
|
+
#: Seconds of computation per second of audio, at the Section 8.2 baseline.
|
|
46
|
+
#: Median 0.24, worst case 0.34; the worst case is quoted so an estimate is
|
|
47
|
+
#: not optimistic about a machine that is busier than the one measured.
|
|
48
|
+
ESTIMATE_REAL_TIME_FACTOR: Final = 0.35
|
|
49
|
+
|
|
50
|
+
# ----------------------------------------------------------- engine call ---
|
|
51
|
+
# Fixed arguments for every synthesis call. Both exist to keep a segment's
|
|
52
|
+
# duration exactly known rather than inferred:
|
|
53
|
+
#
|
|
54
|
+
# * The engine re-chunks long text on its own (120 code points for Korean)
|
|
55
|
+
# and would then decide its own internal boundaries. A large limit makes
|
|
56
|
+
# one of our segments exactly one engine call.
|
|
57
|
+
# * The engine's own inter-chunk silence is inert once we chunk first, which
|
|
58
|
+
# A.5 measured, so F-82 makes inter-segment silence the app's to insert.
|
|
59
|
+
# Asking for zero states that intent rather than relying on the accident.
|
|
60
|
+
ENGINE_MAX_CHUNK_CODEPOINTS: Final = 100_000
|
|
61
|
+
ENGINE_SILENCE_DURATION_S: Final = 0.0
|
|
62
|
+
#: Diffusion steps. A.5 measured RTF 0.067 at the fastest setting and 0.207
|
|
63
|
+
#: at this one; the margin over playback is fivefold either way, so quality
|
|
64
|
+
#: wins.
|
|
65
|
+
ENGINE_TOTAL_STEPS: Final = 8
|
|
66
|
+
|
|
67
|
+
# F-08's speaking styles, as the preset the requirement describes: a tempo
|
|
68
|
+
# multiplier applied on top of the user's own tempo, and the inter-segment
|
|
69
|
+
# pause the app inserts per F-82. A style never changes which characters are
|
|
70
|
+
# spoken. The product of the two tempi is clamped to F-07's range, so a
|
|
71
|
+
# style can never take tempo outside what the user is told is possible.
|
|
72
|
+
STYLE_TEMPO_MULTIPLIER: Final = {
|
|
73
|
+
"natural": 1.00,
|
|
74
|
+
"calm": 0.92,
|
|
75
|
+
"bright": 1.08,
|
|
76
|
+
"narration": 0.97,
|
|
77
|
+
}
|
|
78
|
+
STYLE_SEGMENT_GAP_MS: Final = {
|
|
79
|
+
"natural": 250,
|
|
80
|
+
"calm": 400,
|
|
81
|
+
"bright": 180,
|
|
82
|
+
"narration": 320,
|
|
83
|
+
}
|
|
84
|
+
#: Extra pause where the source text itself had a paragraph break.
|
|
85
|
+
PARAGRAPH_EXTRA_GAP_MS: Final = 350
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------- voice ---
|
|
88
|
+
TEMPO_MIN: Final = 0.70 # F-07
|
|
89
|
+
TEMPO_MAX: Final = 1.50
|
|
90
|
+
TEMPO_DEFAULT: Final = 1.00
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------- resources ---
|
|
93
|
+
CPU_PERCENT_MIN: Final = 10 # F-20
|
|
94
|
+
CPU_PERCENT_MAX: Final = 70
|
|
95
|
+
CPU_PERCENT_DEFAULT: Final = 20
|
|
96
|
+
|
|
97
|
+
MEMORY_DEFAULT_FRACTION: Final = 0.25 # F-21
|
|
98
|
+
MEMORY_DEFAULT_MIN_BYTES: Final = 2 * GIB
|
|
99
|
+
MEMORY_DEFAULT_MAX_BYTES: Final = 6 * GIB
|
|
100
|
+
MEMORY_CEILING_BYTES: Final = 32 * GIB
|
|
101
|
+
MEMORY_CEILING_FRACTION_OF_TOTAL: Final = 0.5
|
|
102
|
+
MEMORY_FLOOR_BYTES: Final = 2 * GIB # F-23: below this, do not load
|
|
103
|
+
|
|
104
|
+
HEADROOM_FRACTION: Final = 0.10 # N-04: greater of 10% of RAM or 1 GiB
|
|
105
|
+
HEADROOM_MIN_BYTES: Final = 1 * GIB
|
|
106
|
+
RESOURCE_SAMPLE_INTERVAL_S: Final = 0.5
|
|
107
|
+
|
|
108
|
+
# --------------------------------------------------------------- jobs -----
|
|
109
|
+
MAX_CONCURRENT_GENERATION: Final = 1 # F-47, whole app
|
|
110
|
+
WORKER_RELEASE_DEADLINE_S: Final = 5.0 # N-22
|
|
111
|
+
WORKER_TERMINATE_GRACE_S: Final = 1.0 # ask, then kill
|
|
112
|
+
|
|
113
|
+
# One-off results. 4.1: external jobs stay retrievable for one hour after a
|
|
114
|
+
# terminal state, or until the app exits, whichever comes first.
|
|
115
|
+
ONEOFF_RESULT_TTL_S: Final = 3600.0
|
|
116
|
+
IDEMPOTENCY_TTL_S: Final = 3600.0 # 4.1, survives a normal restart
|
|
117
|
+
|
|
118
|
+
# F-88 / 4.1 bounded wait.
|
|
119
|
+
BOUNDED_WAIT_DEFAULT_S: Final = 10.0
|
|
120
|
+
BOUNDED_WAIT_CEILING_S: Final = 60.0
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------- limits ---
|
|
123
|
+
#: What a caller is told to wait for a result that is not finished yet.
|
|
124
|
+
#: Section 4 fixes no figure, so this borrows the busy hint rather than
|
|
125
|
+
#: introducing a second unexplained number.
|
|
126
|
+
NOT_READY_RETRY_AFTER_S: Final = 3.0
|
|
127
|
+
|
|
128
|
+
# F-74's scheduled backup: once daily, made up only once when missed.
|
|
129
|
+
SCHEDULED_BACKUP_INTERVAL_S: Final = 86_400.0
|
|
130
|
+
#: How long a failed scheduled backup waits before trying again. Not a
|
|
131
|
+
#: requirement; without it a frequent tick would retry a failing backup
|
|
132
|
+
#: continuously, which N-28 would not forgive.
|
|
133
|
+
SCHEDULED_BACKUP_RETRY_S: Final = 3_600.0
|
|
134
|
+
|
|
135
|
+
RATE_GENERATION_PER_MIN: Final = 10 # 4.1, per client
|
|
136
|
+
RATE_OTHER_PER_MIN: Final = 120
|
|
137
|
+
AUTH_FAILURES_PER_MIN: Final = 10
|
|
138
|
+
AUTH_LOCKOUT_S: Final = 60.0
|
|
139
|
+
|
|
140
|
+
LIST_PAGE_DEFAULT: Final = 20
|
|
141
|
+
LIST_PAGE_MAX: Final = 100
|
|
142
|
+
|
|
143
|
+
# -------------------------------------------------------------- storage ---
|
|
144
|
+
RETENTION_DEFAULT_BYTES: Final = 5 * GB
|
|
145
|
+
RETENTION_MIN_BYTES: Final = 1 * GB
|
|
146
|
+
RETENTION_MAX_BYTES: Final = 100 * GB
|
|
147
|
+
LOW_SPACE_WARNING_BYTES: Final = 1 * GB
|
|
148
|
+
|
|
149
|
+
LOG_RETENTION_DAYS: Final = 7
|
|
150
|
+
LOG_MAX_BYTES: Final = 100 * 1_000_000
|
|
151
|
+
|
|
152
|
+
BACKUP_KEEP_SCHEDULED: Final = 7
|
|
153
|
+
BACKUP_RESTORE_MAX_BYTES: Final = 100 * GB
|
|
154
|
+
BACKUP_RESTORE_MAX_ITEMS: Final = 100_000
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------- service ---
|
|
157
|
+
REST_HOST: Final = "127.0.0.1" # N-17: loopback only, not configurable
|
|
158
|
+
REST_PORT_DEFAULT: Final = 8765
|
|
159
|
+
REST_ENABLED_DEFAULT: Final = True # F-46
|
|
160
|
+
MCP_ENABLED_DEFAULT: Final = False # F-46
|
|
161
|
+
|
|
162
|
+
CREDENTIAL_DAYS_DEFAULT: Final = 90 # 4.1
|
|
163
|
+
CREDENTIAL_DAYS_MIN: Final = 1
|
|
164
|
+
CREDENTIAL_DAYS_MAX: Final = 365
|
|
165
|
+
CREDENTIAL_EXPIRY_WARNING_DAYS: Final = 7
|
|
166
|
+
|
|
167
|
+
# Hosts a request may claim. N-17 evaluates this before authentication.
|
|
168
|
+
ALLOWED_HOSTS: Final = frozenset(
|
|
169
|
+
{"127.0.0.1", "localhost", "[::1]", "::1"}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------ interface ---
|
|
173
|
+
API_VERSION: Final = "v1"
|
|
174
|
+
API_PREFIX: Final = "/api/v1"
|
|
175
|
+
# The MCP revision this build implements, per F-58.
|
|
176
|
+
MCP_PROTOCOL_REVISION: Final = "2026-07-28"
|
|
177
|
+
|
|
178
|
+
# ----------------------------------------------------------------- misc ---
|
|
179
|
+
VOICE_PRESET_MAX: Final = 100 # 4.1
|
|
180
|
+
PLAYBACK_VOLUME_DEFAULT: Final = 1.0
|
|
181
|
+
OS_NOTIFICATIONS_DEFAULT: Final = False
|
|
182
|
+
AUTOPLAY_DEFAULT: Final = True # F-83
|
|
183
|
+
FOLLOW_DEFAULT: Final = True # F-30
|
|
184
|
+
AUTOSAVE_DOCUMENTS_DEFAULT: Final = False # 4.1
|
|
185
|
+
RETAIN_HISTORY_DEFAULT: Final = False # 4.1
|
|
186
|
+
SCHEDULED_BACKUP_DEFAULT: Final = False # 4.1
|
|
187
|
+
|
|
188
|
+
# N-12: the on-screen highlight must track the audio clock this closely.
|
|
189
|
+
HIGHLIGHT_SYNC_BUDGET_MS: Final = 300
|
|
File without changes
|