arona-agent 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/assets/blue-archive/arona/spine/arona_spr.atlas.txt +1168 -0
- package/assets/blue-archive/arona/spine/arona_spr.png +0 -0
- package/assets/blue-archive/arona/spine/arona_spr.skel +0 -0
- package/assets/blue-archive/arona/voice.mp3 +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.atlas.txt +174 -0
- package/assets/blue-archive/hoshino/hoshino_spr.json +15385 -0
- package/assets/blue-archive/hoshino/hoshino_spr.png +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.skel +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.atlas.txt +111 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.png +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.skel +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.atlas.txt +251 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.png +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.skel +0 -0
- package/assets/blue-archive/hoshino/voice.mp3 +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.atlas.txt +1266 -0
- package/assets/blue-archive/plana/spine/plana_spr.png +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.skel +0 -0
- package/assets/blue-archive/plana/voice.mp3 +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.atlas.txt +97 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.png +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.skel +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.atlas.txt +174 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.png +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.skel +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.atlas.txt +167 -0
- package/assets/blue-archive/shiroko/shiroko_spr.json +10702 -0
- package/assets/blue-archive/shiroko/shiroko_spr.png +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.skel +0 -0
- package/assets/blue-archive/shiroko/voice.mp3 +0 -0
- package/bin/arona.mjs +43 -0
- package/package.json +37 -0
- package/pet/agents.cjs +197 -0
- package/pet/main.cjs +462 -0
- package/pet/preload.cjs +21 -0
- package/pet/renderer/fx.html +30 -0
- package/pet/renderer/fx.js +41 -0
- package/pet/renderer/gallery.html +18 -0
- package/pet/renderer/gallery.js +31 -0
- package/pet/renderer/index.html +18 -0
- package/pet/renderer/renderer.js +382 -0
- package/pet/renderer/spine_layer.js +609 -0
- package/pet/renderer/spinetest.html +24 -0
- package/pet/renderer/spinetest.js +295 -0
- package/pet/renderer/style.css +63 -0
- package/pet/tools/gallery_capture.cjs +267 -0
- package/pet/tools/gen_sway.cjs +170 -0
- package/pet/tools/meshify_region.cjs +105 -0
- package/pet/tools/skel_inspect.cjs +112 -0
- package/pet/tools/skel_to_json.cjs +410 -0
- package/pet/tools/spine_node.cjs +69 -0
- package/pet/tools/visual_test.cjs +262 -0
- package/pet/vendor/spine/spine-canvas.js +8472 -0
- package/pet/vendor/spine/spine-webgl.js +10839 -0
- package/python/__pycache__/_i18n.cpython-313.pyc +0 -0
- package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
- package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-313.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
- package/python/__pycache__/stt.cpython-313.pyc +0 -0
- package/python/__pycache__/stt.cpython-314.pyc +0 -0
- package/python/__pycache__/tts.cpython-313.pyc +0 -0
- package/python/__pycache__/tts.cpython-314.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-313.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-314.pyc +0 -0
- package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
- package/python/_i18n.py +18 -0
- package/python/computer_use.py +190 -0
- package/python/hotkey.py +196 -0
- package/python/stt.py +240 -0
- package/python/tts_say.py +177 -0
- package/python/voice_clone.py +98 -0
- package/requirements.txt +6 -0
- package/src/agent.ts +791 -0
- package/src/agent_registry.ts +109 -0
- package/src/commands.ts +641 -0
- package/src/config.ts +245 -0
- package/src/in_memory_credentials.ts +61 -0
- package/src/index.ts +78 -0
- package/src/locale.ts +131 -0
- package/src/logo.ts +120 -0
- package/src/mcp.ts +192 -0
- package/src/memory.ts +303 -0
- package/src/pet.ts +346 -0
- package/src/renderer.ts +350 -0
- package/src/repl.ts +902 -0
- package/src/setup.ts +371 -0
- package/src/skills.ts +96 -0
- package/src/slash_menu.ts +371 -0
- package/src/slash_registry.ts +53 -0
- package/src/speaker_context.ts +41 -0
- package/src/text_split.ts +79 -0
- package/src/tools/computer_use.ts +142 -0
- package/src/tools/emotion_tool.ts +55 -0
- package/src/tools/keep_silent_tool.ts +26 -0
- package/src/tools/memory_tool.ts +24 -0
- package/src/tools/skill_tools.ts +100 -0
- package/src/tools/tavily_tools.ts +431 -0
- package/src/tools/voice_tools.ts +28 -0
- package/src/tts_stream.ts +260 -0
- package/src/tui_select.ts +146 -0
- package/src/undo.ts +451 -0
- package/src/utils/python.ts +211 -0
- package/src/utils/spawn.ts +56 -0
- package/src/voice.ts +83 -0
- package/src/voice_cli.ts +198 -0
- package/src/voices.ts +135 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ARONA 非流式 TTS:一句话合成并播放(一次性进程,非流式 HTTP)。
|
|
4
|
+
|
|
5
|
+
与 Node 侧(src/tts_stream.ts)通过 stdin/stdout JSON 行通信(每句一个进程):
|
|
6
|
+
stdin (单行): {"text": "...", "voice": "..."}
|
|
7
|
+
stdout 事件(协议通道,禁止混入日志):
|
|
8
|
+
{"event":"ready"} 进程就绪
|
|
9
|
+
{"event":"play_start"} 音频就绪、开始播放
|
|
10
|
+
{"event":"play_end"} 播放完毕
|
|
11
|
+
{"event":"error","message":"..."}
|
|
12
|
+
|
|
13
|
+
流程:HTTP POST 非流式 TTS 端点 → 解析 output.audio.url → 下载音频 → 播放。
|
|
14
|
+
整句一次合成,语气上下文完整,避免流式逐段截断(如"好好吃饭哦"被拆读)。
|
|
15
|
+
播放:pyaudio 播 wav;pyaudio 不可用降级系统播放器(afplay / powershell / aplay / ffplay / paplay)。
|
|
16
|
+
|
|
17
|
+
环境变量:
|
|
18
|
+
QWEN_TTS_API_KEY - 百炼 API Key
|
|
19
|
+
QWEN_TTS_MODEL - 模型名(默认 qwen-audio-3.0-tts-plus)
|
|
20
|
+
QWEN_WORKSPACE_ID - 百炼业务空间 ID(可选;留空走全局域名 dashscope.aliyuncs.com)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import io
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
import tempfile
|
|
30
|
+
import urllib.request
|
|
31
|
+
|
|
32
|
+
from _i18n import t
|
|
33
|
+
|
|
34
|
+
SAMPLE_RATE = 24000 # qwen-audio-3.0-tts 非流式 wav 标准采样率
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def emit(event, **kwargs):
|
|
38
|
+
"""向 stdout 输出一行事件 JSON(协议通道,禁止日志混入)。"""
|
|
39
|
+
payload = {"event": event, **kwargs}
|
|
40
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
41
|
+
sys.stdout.flush()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _opener():
|
|
45
|
+
# 显式禁用代理:DashScope / OSS 国内服务直连,避免 Clash 系统代理干扰
|
|
46
|
+
return urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_endpoint():
|
|
50
|
+
ws = os.environ.get("QWEN_WORKSPACE_ID", "")
|
|
51
|
+
if ws:
|
|
52
|
+
return f"https://{ws}.cn-beijing.maas.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer"
|
|
53
|
+
return "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def synthesize(text, voice):
|
|
57
|
+
"""非流式合成:返回完整 wav 音频字节。"""
|
|
58
|
+
api_key = os.environ.get("QWEN_TTS_API_KEY", "")
|
|
59
|
+
model = os.environ.get("QWEN_TTS_MODEL", "qwen-audio-3.0-tts-plus")
|
|
60
|
+
if not api_key:
|
|
61
|
+
raise RuntimeError(t("TTS: 未设置 QWEN_TTS_API_KEY", "TTS: QWEN_TTS_API_KEY not set"))
|
|
62
|
+
|
|
63
|
+
body = {
|
|
64
|
+
"model": model,
|
|
65
|
+
"input": {
|
|
66
|
+
"text": text,
|
|
67
|
+
"voice": voice,
|
|
68
|
+
"format": "wav",
|
|
69
|
+
"sample_rate": SAMPLE_RATE,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
req = urllib.request.Request(
|
|
73
|
+
build_endpoint(),
|
|
74
|
+
data=json.dumps(body).encode("utf-8"),
|
|
75
|
+
headers={
|
|
76
|
+
"Authorization": f"Bearer {api_key}",
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
with _opener().open(req, timeout=60) as resp:
|
|
81
|
+
raw = resp.read()
|
|
82
|
+
data = json.loads(raw)
|
|
83
|
+
url = (data.get("output", {}) or {}).get("audio", {}) or {}
|
|
84
|
+
url = url.get("url")
|
|
85
|
+
if not url:
|
|
86
|
+
raise RuntimeError(t(f"TTS 未返回音频 URL:{raw[:200]}", f"TTS no audio url: {raw[:200]}"))
|
|
87
|
+
with _opener().open(url, timeout=60) as resp:
|
|
88
|
+
return resp.read()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _play_command(wav_path):
|
|
92
|
+
"""按平台选择降级播放命令(pyaudio 不可用时的非实时兜底)。"""
|
|
93
|
+
if sys.platform == "darwin":
|
|
94
|
+
return ["afplay", wav_path]
|
|
95
|
+
if sys.platform == "win32":
|
|
96
|
+
return ["powershell", "-NoProfile", "-NonInteractive",
|
|
97
|
+
"-Command", f"(New-Object Media.SoundPlayer '{wav_path}').PlaySync()"]
|
|
98
|
+
for cmd in ("aplay", "ffplay", "paplay"):
|
|
99
|
+
if shutil.which(cmd):
|
|
100
|
+
return [cmd, wav_path]
|
|
101
|
+
return ["aplay", wav_path]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _play_pyaudio(data):
|
|
105
|
+
import pyaudio
|
|
106
|
+
import wave
|
|
107
|
+
wf = wave.open(io.BytesIO(data), "rb")
|
|
108
|
+
p = pyaudio.PyAudio()
|
|
109
|
+
stream = p.open(
|
|
110
|
+
format=p.get_format_from_width(wf.getsampwidth()),
|
|
111
|
+
channels=wf.getnchannels(),
|
|
112
|
+
rate=wf.getframerate(),
|
|
113
|
+
output=True,
|
|
114
|
+
)
|
|
115
|
+
try:
|
|
116
|
+
chunk = 1024
|
|
117
|
+
frame = wf.readframes(chunk)
|
|
118
|
+
while frame:
|
|
119
|
+
stream.write(frame)
|
|
120
|
+
frame = wf.readframes(chunk)
|
|
121
|
+
finally:
|
|
122
|
+
stream.stop_stream()
|
|
123
|
+
stream.close()
|
|
124
|
+
p.terminate()
|
|
125
|
+
wf.close()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _play_fallback(data):
|
|
129
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
|
130
|
+
try:
|
|
131
|
+
tmp.write(data)
|
|
132
|
+
tmp.close()
|
|
133
|
+
subprocess.run(_play_command(tmp.name), check=False)
|
|
134
|
+
finally:
|
|
135
|
+
try:
|
|
136
|
+
os.unlink(tmp.name)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def play_wav(data):
|
|
142
|
+
try:
|
|
143
|
+
_play_pyaudio(data)
|
|
144
|
+
except Exception:
|
|
145
|
+
_play_fallback(data)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main():
|
|
149
|
+
emit("ready")
|
|
150
|
+
line = sys.stdin.readline()
|
|
151
|
+
if not line:
|
|
152
|
+
return
|
|
153
|
+
try:
|
|
154
|
+
cmd = json.loads(line)
|
|
155
|
+
except Exception:
|
|
156
|
+
emit("error", message=t("TTS: 无效的 stdin JSON", "TTS: invalid stdin JSON"))
|
|
157
|
+
sys.stdout.flush()
|
|
158
|
+
return
|
|
159
|
+
text = (cmd.get("text") or "").strip()
|
|
160
|
+
voice = cmd.get("voice") or ""
|
|
161
|
+
if not text:
|
|
162
|
+
emit("error", message=t("TTS: 空文本", "TTS: empty text"))
|
|
163
|
+
sys.stdout.flush()
|
|
164
|
+
return
|
|
165
|
+
try:
|
|
166
|
+
audio = synthesize(text, voice)
|
|
167
|
+
emit("play_start")
|
|
168
|
+
play_wav(audio)
|
|
169
|
+
emit("play_end")
|
|
170
|
+
sys.stdout.flush()
|
|
171
|
+
except Exception as e:
|
|
172
|
+
emit("error", message=str(e))
|
|
173
|
+
sys.stdout.flush()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
main()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ARONA Voice Cloning — uploads voice.mp3 to DashScope, creates a custom voice,
|
|
4
|
+
and prints the voice_id as JSON on stdout. Deletes the uploaded file afterward.
|
|
5
|
+
|
|
6
|
+
Environment variables:
|
|
7
|
+
QWEN_TTS_API_KEY - DashScope API key
|
|
8
|
+
QWEN_TTS_MODEL - target TTS model (default qwen-audio-3.0-tts-plus)
|
|
9
|
+
ARONA_VOICE_AUDIO - path to the audio file (voice.mp3)
|
|
10
|
+
ARONA_VOICE_PREFIX - voice ID prefix, lowercase alnum, <=10 chars (default "arona")
|
|
11
|
+
|
|
12
|
+
Output (stdout): {"voice_id": "..."} on success, {"error": "..."} on failure.
|
|
13
|
+
All logging goes to stderr so stdout stays clean JSON.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fail(msg):
|
|
24
|
+
print(json.dumps({"error": msg}))
|
|
25
|
+
# 同步写 stderr:Node 侧 runPython 在非零退出码时只保留 stderr,stdout 会被丢弃,
|
|
26
|
+
# 不写 stderr 的话克隆失败的真实原因(如音频质量/网络)到不了调用方。
|
|
27
|
+
print(f"voice_clone error: {msg}", file=sys.stderr)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
api_key = os.environ.get("QWEN_TTS_API_KEY", "")
|
|
33
|
+
audio_file = os.environ.get("ARONA_VOICE_AUDIO", "")
|
|
34
|
+
prefix = os.environ.get("ARONA_VOICE_PREFIX", "arona")
|
|
35
|
+
target_model = os.environ.get("QWEN_TTS_MODEL", "qwen-audio-3.0-tts-plus")
|
|
36
|
+
|
|
37
|
+
if not api_key:
|
|
38
|
+
fail("QWEN_TTS_API_KEY not set")
|
|
39
|
+
if not audio_file or not os.path.isfile(audio_file):
|
|
40
|
+
fail(f"Audio file not found: {audio_file}")
|
|
41
|
+
if not re.fullmatch(r"[a-z0-9]{1,10}", prefix):
|
|
42
|
+
fail(f"PREFIX must be lowercase alnum, 1-10 chars (got: {prefix})")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
import dashscope
|
|
46
|
+
from dashscope import Files
|
|
47
|
+
from dashscope.audio.tts_v2 import VoiceEnrollmentService
|
|
48
|
+
except ImportError:
|
|
49
|
+
fail("dashscope package not installed. Run: pip install dashscope")
|
|
50
|
+
|
|
51
|
+
dashscope.api_key = api_key
|
|
52
|
+
|
|
53
|
+
# 1. Upload audio to DashScope hosted OSS
|
|
54
|
+
try:
|
|
55
|
+
resp = Files.upload(file_path=audio_file, purpose="voice_clone")
|
|
56
|
+
file_id = resp.output["uploaded_files"][0]["file_id"]
|
|
57
|
+
except Exception as e:
|
|
58
|
+
fail(f"Upload failed: {e}")
|
|
59
|
+
|
|
60
|
+
# 2. Get the Alibaba Cloud internal URL (must use internal address)
|
|
61
|
+
try:
|
|
62
|
+
oss_url = Files.get(file_id).output["url"]
|
|
63
|
+
except Exception as e:
|
|
64
|
+
fail(f"Failed to get OSS URL: {e}")
|
|
65
|
+
|
|
66
|
+
# 3. Submit voice cloning
|
|
67
|
+
try:
|
|
68
|
+
svc = VoiceEnrollmentService()
|
|
69
|
+
voice_id = svc.create_voice(target_model=target_model, prefix=prefix, url=oss_url)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
fail(f"create_voice failed: {e}")
|
|
72
|
+
|
|
73
|
+
# 4. Wait for voice to be ready (max 5 minutes)
|
|
74
|
+
try:
|
|
75
|
+
for _ in range(30):
|
|
76
|
+
info = svc.query_voice(voice_id=voice_id)
|
|
77
|
+
status = info.get("status")
|
|
78
|
+
if status == "OK":
|
|
79
|
+
break
|
|
80
|
+
if status == "UNDEPLOYED":
|
|
81
|
+
fail("Voice creation failed. Please check audio quality (10-20s clear voice recommended).")
|
|
82
|
+
time.sleep(10)
|
|
83
|
+
else:
|
|
84
|
+
fail("Timeout waiting for voice to be ready (5 minutes)")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
fail(f"query_voice failed: {e}")
|
|
87
|
+
|
|
88
|
+
# 5. Delete the uploaded file (best-effort cleanup)
|
|
89
|
+
try:
|
|
90
|
+
Files.delete(file_id)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass # Cleanup is best-effort
|
|
93
|
+
|
|
94
|
+
print(json.dumps({"voice_id": voice_id}))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main()
|