tagword-codeagent 1.1.6__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.
- codeagent/__init__.py +10 -0
- codeagent/__main__.py +13 -0
- codeagent/cli/__init__.py +13 -0
- codeagent/cli/entrypoint.py +161 -0
- codeagent/cli/main.py +979 -0
- codeagent/core/__init__.py +2 -0
- codeagent/core/attachments.py +342 -0
- codeagent/core/audio_models.py +50 -0
- codeagent/core/bootstrap.py +178 -0
- codeagent/core/default_presets.py +159 -0
- codeagent/core/env.py +106 -0
- codeagent/core/folder_picker.py +151 -0
- codeagent/core/image_understanding.py +90 -0
- codeagent/core/models.py +6 -0
- codeagent/core/paths.py +238 -0
- codeagent/core/pricing.py +107 -0
- codeagent/core/process_ports.py +258 -0
- codeagent/core/seed_bridge.py +21 -0
- codeagent/core/settings.py +98 -0
- codeagent/core/speech_synth.py +330 -0
- codeagent/core/team_config.py +247 -0
- codeagent/core/team_manager.py +245 -0
- codeagent/core/token_counter.py +41 -0
- codeagent/core/tts_voices.py +103 -0
- codeagent/core/usage_billing.py +72 -0
- codeagent/core/vision_models.py +93 -0
- codeagent/memory/diary.py +144 -0
- codeagent/persona_defaults/README.md +12 -0
- codeagent/persona_defaults/agent.md +352 -0
- codeagent/persona_defaults/identity.md +29 -0
- codeagent/persona_defaults/memory.md +4 -0
- codeagent/persona_defaults/skills.md +325 -0
- codeagent/persona_defaults/soul.md +38 -0
- codeagent/persona_defaults/tools.md +41 -0
- codeagent/persona_defaults/user.md +18 -0
- codeagent/runtime/__init__.py +11 -0
- codeagent/runtime/agent_context.py +6 -0
- codeagent/runtime/agent_runtime.py +21 -0
- codeagent/runtime/compact_prompt.py +9 -0
- codeagent/runtime/compact_state.py +40 -0
- codeagent/runtime/llm_worker.py +132 -0
- codeagent/runtime/orchestrator.py +161 -0
- codeagent/runtime/prompt_enrichment.py +324 -0
- codeagent/runtime/session_manager.py +30 -0
- codeagent/runtime/task_split.py +38 -0
- codeagent/runtime/turn_loop.py +13 -0
- codeagent/runtime/worker.py +48 -0
- codeagent/server/__init__.py +505 -0
- codeagent/server/app_factory.py +1292 -0
- codeagent/server/attachment_api.py +261 -0
- codeagent/server/hub.py +175 -0
- codeagent/server/icon.png +0 -0
- codeagent/server/self_healing.py +143 -0
- codeagent/server/team_engine.py +243 -0
- codeagent/server/tts_api.py +51 -0
- codeagent/server/webui_api_app.py +3180 -0
- codeagent/skills/__init__.py +2 -0
- codeagent/skills/select.py +144 -0
- codeagent/tools/__init__.py +6 -0
- codeagent/tools/agent_tools.py +10 -0
- codeagent/web/__init__.py +1 -0
- codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
- codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
- codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
- codeagent/web/auth_impl.py +326 -0
- codeagent/web/static/00-marked.min.js +6 -0
- codeagent/web/static/00-mermaid.min.js +2314 -0
- codeagent/web/static/00-purify.min.js +3 -0
- codeagent/web/static/00-storage.js +83 -0
- codeagent/web/static/00-utils.js +51 -0
- codeagent/web/static/01-theme.css +96 -0
- codeagent/web/static/01a-scroll.js +86 -0
- codeagent/web/static/01b-think.js +62 -0
- codeagent/web/static/01c-session-identity.js +81 -0
- codeagent/web/static/01c-session-thinking.js +61 -0
- codeagent/web/static/01c-session-tree-menus.js +349 -0
- codeagent/web/static/01c-session-tree.js +531 -0
- codeagent/web/static/01c-session-utilities.js +83 -0
- codeagent/web/static/01c-vision-model.js +132 -0
- codeagent/web/static/01d-image-gen-model.js +84 -0
- codeagent/web/static/01e-audio-model.js +97 -0
- codeagent/web/static/01f-compose-ui.js +88 -0
- codeagent/web/static/01g-tts-settings.js +128 -0
- codeagent/web/static/01h-music-model.js +85 -0
- codeagent/web/static/01i-model-stack.js +291 -0
- codeagent/web/static/01j-video-gen-model.js +85 -0
- codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
- codeagent/web/static/01k-model-stack-state.js +209 -0
- codeagent/web/static/01q-inflight.js +195 -0
- codeagent/web/static/02-reset.css +22 -0
- codeagent/web/static/02-session-ui.js +201 -0
- codeagent/web/static/03-layout-p1.css +2 -0
- codeagent/web/static/03-layout-p2.css +2 -0
- codeagent/web/static/03a-links-stream.js +88 -0
- codeagent/web/static/03b-stream-text.js +119 -0
- codeagent/web/static/03c-render-bubble.js +520 -0
- codeagent/web/static/03d-agent-bubble.js +126 -0
- codeagent/web/static/03e-tool-trace.js +182 -0
- codeagent/web/static/03f-timeline.js +54 -0
- codeagent/web/static/03g-bubble-tts.js +292 -0
- codeagent/web/static/04-components-p1.css +1 -0
- codeagent/web/static/04-components-p2b.css +1 -0
- codeagent/web/static/04-components-p3.css +2 -0
- codeagent/web/static/04-components-p4.css +1 -0
- codeagent/web/static/04-components-p5.css +1 -0
- codeagent/web/static/04-components-p6.css +58 -0
- codeagent/web/static/04-components-p7.css +4 -0
- codeagent/web/static/04-components-p8.css +1 -0
- codeagent/web/static/04-components-p9.css +1 -0
- codeagent/web/static/04-ws-connect.js +351 -0
- codeagent/web/static/04-ws-helpers.js +116 -0
- codeagent/web/static/05-chat-p1.css +1058 -0
- codeagent/web/static/05-chat-p2.css +1 -0
- codeagent/web/static/05-chat-p3.css +295 -0
- codeagent/web/static/05a-session-history.js +176 -0
- codeagent/web/static/05b-session-list.js +114 -0
- codeagent/web/static/05c-dialog.js +123 -0
- codeagent/web/static/05d-project-create.js +172 -0
- codeagent/web/static/05e-project-remote.js +143 -0
- codeagent/web/static/06-chat.js +552 -0
- codeagent/web/static/06-utilities.css +181 -0
- codeagent/web/static/06b-attachments.js +304 -0
- codeagent/web/static/06c-camera.js +488 -0
- codeagent/web/static/07-activity-bar.css +127 -0
- codeagent/web/static/09-git-p1.css +1 -0
- codeagent/web/static/09-git-p2.css +1 -0
- codeagent/web/static/09a-init-bind.js +110 -0
- codeagent/web/static/09b-panel-toggle.js +80 -0
- codeagent/web/static/09c-api-refresh.js +100 -0
- codeagent/web/static/09d-status-log.js +80 -0
- codeagent/web/static/09e-commit.js +80 -0
- codeagent/web/static/09f-remote.js +93 -0
- codeagent/web/static/11a-cron-parse.js +117 -0
- codeagent/web/static/11b-cron-panel.js +100 -0
- codeagent/web/static/11c-cron-card.js +133 -0
- codeagent/web/static/11d-cron-form.js +105 -0
- codeagent/web/static/11e-env-config.js +168 -0
- codeagent/web/static/11f-env-llm-a-state.js +136 -0
- codeagent/web/static/11f-env-llm-b-form.js +409 -0
- codeagent/web/static/11f-env-llm-c-board.js +322 -0
- codeagent/web/static/11g-env-paths-git.js +973 -0
- codeagent/web/static/11h-env-mcp-a-state.js +161 -0
- codeagent/web/static/11h-env-mcp-b-form.js +229 -0
- codeagent/web/static/11h-env-mcp-c-board.js +565 -0
- codeagent/web/static/12-plugins.js +127 -0
- codeagent/web/static/13a-skills.js +156 -0
- codeagent/web/static/13a-todo-utils.js +140 -0
- codeagent/web/static/13b-todo-fetch.js +106 -0
- codeagent/web/static/13b-tools.js +127 -0
- codeagent/web/static/13c-todo-crud.js +92 -0
- codeagent/web/static/14-activity-bar.js +177 -0
- codeagent/web/static/15-plans.css +194 -0
- codeagent/web/static/15-plans.js +219 -0
- codeagent/web/static/16-mobile.css +612 -0
- codeagent/web/static/16-mobile.js +178 -0
- codeagent/web/static/17-preset-ux.css +287 -0
- codeagent/web/static/18-agent-mgr.css +616 -0
- codeagent/web/static/18-agent-mgr.js +204 -0
- codeagent/web/static/24-health-dashboard.js +68 -0
- codeagent/web/static/99-brand-polish.css +152 -0
- codeagent/web/static/body.html +824 -0
- codeagent/web/static/setup/setup_form.html +171 -0
- codeagent/web/static/setup/setup_head.html +100 -0
- codeagent/web/static/setup/setup_tail.html +264 -0
- codeagent/web/static/setup/setup_test.html +38 -0
- codeagent/web/web_login.html +265 -0
- codeagent/web/webui.html +35 -0
- tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
- tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
- tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
- tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
- tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
- tagword_codeagent-1.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Team workflow engine — executes multi-agent team runs.
|
|
2
|
+
|
|
3
|
+
Three modes:
|
|
4
|
+
- sequential: members run one after another, passing context
|
|
5
|
+
- parallel: all members run simultaneously
|
|
6
|
+
- manager: first member is PM who splits and dispatches to others
|
|
7
|
+
|
|
8
|
+
Each run is persisted as a JSON file in ~/.codeagent/runs/
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from codeagent.core.paths import codeagent_home
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
RUN_STATUS_PENDING = "pending"
|
|
25
|
+
RUN_STATUS_RUNNING = "running"
|
|
26
|
+
RUN_STATUS_DONE = "done"
|
|
27
|
+
RUN_STATUS_FAILED = "failed"
|
|
28
|
+
RUN_STATUS_TIMEOUT = "timeout"
|
|
29
|
+
|
|
30
|
+
STEP_STATUS_PENDING = "pending"
|
|
31
|
+
STEP_STATUS_RUNNING = "running"
|
|
32
|
+
STEP_STATUS_DONE = "done"
|
|
33
|
+
STEP_STATUS_FAILED = "failed"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _runs_dir() -> Path:
|
|
37
|
+
p = codeagent_home() / "runs"
|
|
38
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return p
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _teams_dir() -> Path:
|
|
43
|
+
p = codeagent_home() / "teams"
|
|
44
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
return p
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def list_runs(team_id: str | None = None, limit: int = 50) -> list[dict]:
|
|
49
|
+
"""List runs, optionally filtered by team_id."""
|
|
50
|
+
rd = _runs_dir()
|
|
51
|
+
runs = []
|
|
52
|
+
for f in sorted(rd.glob("*.json"), reverse=True):
|
|
53
|
+
if len(runs) >= limit:
|
|
54
|
+
break
|
|
55
|
+
try:
|
|
56
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
57
|
+
data["id"] = f.stem
|
|
58
|
+
if team_id and data.get("team_id") != team_id:
|
|
59
|
+
continue
|
|
60
|
+
runs.append(data)
|
|
61
|
+
except (json.JSONDecodeError, OSError):
|
|
62
|
+
pass
|
|
63
|
+
return runs
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def create_run(team_id: str, user_input: str) -> dict:
|
|
67
|
+
"""Create a new run record and return it."""
|
|
68
|
+
run_id = uuid.uuid4().hex[:12]
|
|
69
|
+
run = {
|
|
70
|
+
"team_id": team_id,
|
|
71
|
+
"user_input": user_input,
|
|
72
|
+
"status": RUN_STATUS_PENDING,
|
|
73
|
+
"created_at": time.time(),
|
|
74
|
+
"updated_at": time.time(),
|
|
75
|
+
"steps": [],
|
|
76
|
+
}
|
|
77
|
+
f = _runs_dir() / f"{run_id}.json"
|
|
78
|
+
f.write_text(json.dumps(run, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
79
|
+
run["id"] = run_id
|
|
80
|
+
return run
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_run(run_id: str) -> dict | None:
|
|
84
|
+
"""Get run by id."""
|
|
85
|
+
f = _runs_dir() / f"{run_id}.json"
|
|
86
|
+
if not f.is_file():
|
|
87
|
+
return None
|
|
88
|
+
try:
|
|
89
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
90
|
+
data["id"] = run_id
|
|
91
|
+
return data
|
|
92
|
+
except (json.JSONDecodeError, OSError):
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def update_run(run_id: str, updates: dict) -> dict | None:
|
|
97
|
+
"""Update run fields."""
|
|
98
|
+
f = _runs_dir() / f"{run_id}.json"
|
|
99
|
+
if not f.is_file():
|
|
100
|
+
return None
|
|
101
|
+
data = get_run(run_id)
|
|
102
|
+
if data is None:
|
|
103
|
+
return None
|
|
104
|
+
data.update(updates)
|
|
105
|
+
data["updated_at"] = time.time()
|
|
106
|
+
f.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def add_step(run_id: str, agent_id: str, step_type: str) -> dict | None:
|
|
111
|
+
"""Add a step to a run, return the step."""
|
|
112
|
+
run = get_run(run_id)
|
|
113
|
+
if run is None:
|
|
114
|
+
return None
|
|
115
|
+
step = {
|
|
116
|
+
"step_id": uuid.uuid4().hex[:8],
|
|
117
|
+
"agent_id": agent_id,
|
|
118
|
+
"type": step_type,
|
|
119
|
+
"status": STEP_STATUS_PENDING,
|
|
120
|
+
"input": "",
|
|
121
|
+
"output": "",
|
|
122
|
+
"created_at": time.time(),
|
|
123
|
+
}
|
|
124
|
+
run["steps"].append(step)
|
|
125
|
+
update_run(run_id, {"steps": run["steps"]})
|
|
126
|
+
return step
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def update_step(run_id: str, step_id: str, updates: dict) -> dict | None:
|
|
130
|
+
"""Update a step within a run."""
|
|
131
|
+
run = get_run(run_id)
|
|
132
|
+
if run is None:
|
|
133
|
+
return None
|
|
134
|
+
for step in run["steps"]:
|
|
135
|
+
if step.get("step_id") == step_id:
|
|
136
|
+
step.update(updates)
|
|
137
|
+
break
|
|
138
|
+
update_run(run_id, {"steps": run["steps"]})
|
|
139
|
+
return run
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run_team(team_id: str, user_input: str) -> dict:
|
|
143
|
+
"""Execute a team workflow. Returns immediately with run_id; actual execution is simulated.
|
|
144
|
+
|
|
145
|
+
For a real implementation, this would spin up agents via the chat API.
|
|
146
|
+
"""
|
|
147
|
+
# Load team
|
|
148
|
+
tf = _teams_dir() / f"{team_id}.json"
|
|
149
|
+
if not tf.is_file():
|
|
150
|
+
raise ValueError(f"Team '{team_id}' not found")
|
|
151
|
+
team = json.loads(tf.read_text(encoding="utf-8"))
|
|
152
|
+
team["id"] = team_id
|
|
153
|
+
|
|
154
|
+
# Create run
|
|
155
|
+
run = create_run(team_id, user_input)
|
|
156
|
+
run_id = run["id"]
|
|
157
|
+
update_run(run_id, {"status": RUN_STATUS_RUNNING})
|
|
158
|
+
|
|
159
|
+
mode = team.get("mode", "sequential")
|
|
160
|
+
members = team.get("members", [])
|
|
161
|
+
error_policy = team.get("error_policy", "stop")
|
|
162
|
+
timeout = team.get("timeout_seconds", 300)
|
|
163
|
+
|
|
164
|
+
if mode == "sequential":
|
|
165
|
+
_run_sequential(run_id, members, user_input, error_policy, timeout)
|
|
166
|
+
elif mode == "parallel":
|
|
167
|
+
_run_parallel(run_id, members, user_input, error_policy, timeout)
|
|
168
|
+
elif mode == "manager":
|
|
169
|
+
_run_manager(run_id, members, user_input, error_policy, timeout, manager_id=team.get("manager_id", ""))
|
|
170
|
+
else:
|
|
171
|
+
update_run(run_id, {"status": RUN_STATUS_FAILED, "error": f"Unknown mode: {mode}"})
|
|
172
|
+
|
|
173
|
+
update_run(run_id, {"status": RUN_STATUS_DONE})
|
|
174
|
+
return get_run(run_id) or run
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _run_sequential(run_id: str, members: list, user_input: str, error_policy: str, timeout: int) -> None:
|
|
178
|
+
"""Run members one after another, passing context."""
|
|
179
|
+
context = user_input
|
|
180
|
+
for member in members:
|
|
181
|
+
agent_id = member if isinstance(member, str) else member.get("id", "")
|
|
182
|
+
step = add_step(run_id, agent_id, "sequential")
|
|
183
|
+
if step is None:
|
|
184
|
+
return
|
|
185
|
+
update_step(run_id, step["step_id"], {"status": STEP_STATUS_RUNNING, "input": context})
|
|
186
|
+
# Simulate execution (in real impl, this calls the agent)
|
|
187
|
+
time.sleep(0.5)
|
|
188
|
+
output = f"[模拟] Agent '{agent_id}' 处理完成:\n收到: {context[:100]}..."
|
|
189
|
+
update_step(run_id, step["step_id"], {"status": STEP_STATUS_DONE, "output": output})
|
|
190
|
+
context = output
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _run_parallel(run_id: str, members: list, user_input: str, error_policy: str, timeout: int) -> None:
|
|
194
|
+
"""Run all members simultaneously."""
|
|
195
|
+
import concurrent.futures
|
|
196
|
+
|
|
197
|
+
def _run_one(agent_id: str) -> tuple[str, str, str]:
|
|
198
|
+
step = add_step(run_id, agent_id, "parallel")
|
|
199
|
+
sid = step["step_id"] if step else ""
|
|
200
|
+
update_step(run_id, sid, {"status": STEP_STATUS_RUNNING, "input": user_input})
|
|
201
|
+
time.sleep(0.3)
|
|
202
|
+
output = f"[模拟] Agent '{agent_id}' 并行处理完成"
|
|
203
|
+
update_step(run_id, sid, {"status": STEP_STATUS_DONE, "output": output})
|
|
204
|
+
return agent_id, sid, output
|
|
205
|
+
|
|
206
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(members)) as pool:
|
|
207
|
+
futures = []
|
|
208
|
+
for member in members:
|
|
209
|
+
agent_id = member if isinstance(member, str) else member.get("id", "")
|
|
210
|
+
futures.append(pool.submit(_run_one, agent_id))
|
|
211
|
+
concurrent.futures.wait(futures)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _run_manager(run_id: str, members: list, user_input: str, error_policy: str, timeout: int, manager_id: str = "") -> None:
|
|
215
|
+
"""First member is PM who dispatches to others. If manager_id is set, that member is PM."""
|
|
216
|
+
if not members:
|
|
217
|
+
return
|
|
218
|
+
# 显式指定的管家优先,否则取第一个成员
|
|
219
|
+
pm_id = manager_id if manager_id and manager_id in [m if isinstance(m, str) else m.get("id", "") for m in members] else (members[0] if isinstance(members[0], str) else members[0].get("id", ""))
|
|
220
|
+
workers = members[1:] if len(members) > 1 else []
|
|
221
|
+
|
|
222
|
+
# PM step: analyze and split
|
|
223
|
+
pm_step = add_step(run_id, pm_id, "manager-pm")
|
|
224
|
+
if pm_step is None:
|
|
225
|
+
return
|
|
226
|
+
update_step(run_id, pm_step["step_id"], {"status": STEP_STATUS_RUNNING, "input": user_input})
|
|
227
|
+
time.sleep(0.5)
|
|
228
|
+
pm_output = f"[模拟] PM '{pm_id}' 将任务拆分为 {max(len(workers), 1)} 个子任务"
|
|
229
|
+
update_step(run_id, pm_step["step_id"], {"status": STEP_STATUS_DONE, "output": pm_output})
|
|
230
|
+
|
|
231
|
+
# Worker steps
|
|
232
|
+
for i, worker in enumerate(workers):
|
|
233
|
+
wid = worker if isinstance(worker, str) else worker.get("id", "")
|
|
234
|
+
w_step = add_step(run_id, wid, "manager-worker")
|
|
235
|
+
if w_step is None:
|
|
236
|
+
return
|
|
237
|
+
update_step(run_id, w_step["step_id"], {
|
|
238
|
+
"status": STEP_STATUS_RUNNING,
|
|
239
|
+
"input": f"子任务 {i+1}: {user_input[:80]}..."
|
|
240
|
+
})
|
|
241
|
+
time.sleep(0.3)
|
|
242
|
+
w_output = f"[模拟] Worker '{wid}' 完成子任务 {i+1}"
|
|
243
|
+
update_step(run_id, w_step["step_id"], {"status": STEP_STATUS_DONE, "output": w_output})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""POST /api/tts — synthesize chat bubble text for playback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from starlette.requests import Request
|
|
6
|
+
from starlette.responses import JSONResponse, Response
|
|
7
|
+
|
|
8
|
+
from codeagent.core.speech_synth import (
|
|
9
|
+
minimax_tts_configured,
|
|
10
|
+
synthesize_minimax_speech_with_detail,
|
|
11
|
+
tts_http_status_for_outcome,
|
|
12
|
+
)
|
|
13
|
+
from codeagent.core.tts_voices import normalize_tts_model, normalize_tts_voice_id
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def api_tts(request: Request):
|
|
17
|
+
try:
|
|
18
|
+
body = await request.json()
|
|
19
|
+
except Exception:
|
|
20
|
+
body = {}
|
|
21
|
+
if not isinstance(body, dict):
|
|
22
|
+
body = {}
|
|
23
|
+
|
|
24
|
+
text = str(body.get("text") or "").strip()
|
|
25
|
+
if not text:
|
|
26
|
+
return JSONResponse({"detail": "text required"}, status_code=422)
|
|
27
|
+
if len(text) > 10000:
|
|
28
|
+
return JSONResponse({"detail": "text too long (max 10000)"}, status_code=422)
|
|
29
|
+
|
|
30
|
+
voice_id = normalize_tts_voice_id(body.get("voice_id"))
|
|
31
|
+
model = normalize_tts_model(body.get("model"))
|
|
32
|
+
|
|
33
|
+
if not minimax_tts_configured():
|
|
34
|
+
return JSONResponse(
|
|
35
|
+
{"detail": "MiniMax TTS not configured", "fallback": True},
|
|
36
|
+
status_code=503,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
out = synthesize_minimax_speech_with_detail(text, voice_id=voice_id, model=model)
|
|
40
|
+
if not out.ok or out.audio is None:
|
|
41
|
+
detail = out.error or "TTS synthesis failed"
|
|
42
|
+
return JSONResponse(
|
|
43
|
+
{
|
|
44
|
+
"detail": detail,
|
|
45
|
+
"fallback": True,
|
|
46
|
+
"status_code": out.status_code,
|
|
47
|
+
},
|
|
48
|
+
status_code=tts_http_status_for_outcome(out),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return Response(content=out.audio, media_type=out.mime)
|