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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/assets/blue-archive/arona/spine/arona_spr.atlas.txt +1168 -0
  4. package/assets/blue-archive/arona/spine/arona_spr.png +0 -0
  5. package/assets/blue-archive/arona/spine/arona_spr.skel +0 -0
  6. package/assets/blue-archive/arona/voice.mp3 +0 -0
  7. package/assets/blue-archive/hoshino/hoshino_spr.atlas.txt +174 -0
  8. package/assets/blue-archive/hoshino/hoshino_spr.json +15385 -0
  9. package/assets/blue-archive/hoshino/hoshino_spr.png +0 -0
  10. package/assets/blue-archive/hoshino/hoshino_spr.skel +0 -0
  11. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.atlas.txt +111 -0
  12. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.png +0 -0
  13. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.skel +0 -0
  14. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.atlas.txt +251 -0
  15. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.png +0 -0
  16. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.skel +0 -0
  17. package/assets/blue-archive/hoshino/voice.mp3 +0 -0
  18. package/assets/blue-archive/plana/spine/plana_spr.atlas.txt +1266 -0
  19. package/assets/blue-archive/plana/spine/plana_spr.png +0 -0
  20. package/assets/blue-archive/plana/spine/plana_spr.skel +0 -0
  21. package/assets/blue-archive/plana/voice.mp3 +0 -0
  22. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.atlas.txt +97 -0
  23. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.png +0 -0
  24. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.skel +0 -0
  25. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.atlas.txt +174 -0
  26. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.png +0 -0
  27. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.skel +0 -0
  28. package/assets/blue-archive/shiroko/shiroko_spr.atlas.txt +167 -0
  29. package/assets/blue-archive/shiroko/shiroko_spr.json +10702 -0
  30. package/assets/blue-archive/shiroko/shiroko_spr.png +0 -0
  31. package/assets/blue-archive/shiroko/shiroko_spr.skel +0 -0
  32. package/assets/blue-archive/shiroko/voice.mp3 +0 -0
  33. package/bin/arona.mjs +43 -0
  34. package/package.json +37 -0
  35. package/pet/agents.cjs +197 -0
  36. package/pet/main.cjs +462 -0
  37. package/pet/preload.cjs +21 -0
  38. package/pet/renderer/fx.html +30 -0
  39. package/pet/renderer/fx.js +41 -0
  40. package/pet/renderer/gallery.html +18 -0
  41. package/pet/renderer/gallery.js +31 -0
  42. package/pet/renderer/index.html +18 -0
  43. package/pet/renderer/renderer.js +382 -0
  44. package/pet/renderer/spine_layer.js +609 -0
  45. package/pet/renderer/spinetest.html +24 -0
  46. package/pet/renderer/spinetest.js +295 -0
  47. package/pet/renderer/style.css +63 -0
  48. package/pet/tools/gallery_capture.cjs +267 -0
  49. package/pet/tools/gen_sway.cjs +170 -0
  50. package/pet/tools/meshify_region.cjs +105 -0
  51. package/pet/tools/skel_inspect.cjs +112 -0
  52. package/pet/tools/skel_to_json.cjs +410 -0
  53. package/pet/tools/spine_node.cjs +69 -0
  54. package/pet/tools/visual_test.cjs +262 -0
  55. package/pet/vendor/spine/spine-canvas.js +8472 -0
  56. package/pet/vendor/spine/spine-webgl.js +10839 -0
  57. package/python/__pycache__/_i18n.cpython-313.pyc +0 -0
  58. package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
  59. package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
  60. package/python/__pycache__/hotkey.cpython-313.pyc +0 -0
  61. package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
  62. package/python/__pycache__/stt.cpython-313.pyc +0 -0
  63. package/python/__pycache__/stt.cpython-314.pyc +0 -0
  64. package/python/__pycache__/tts.cpython-313.pyc +0 -0
  65. package/python/__pycache__/tts.cpython-314.pyc +0 -0
  66. package/python/__pycache__/tts_stream.cpython-313.pyc +0 -0
  67. package/python/__pycache__/tts_stream.cpython-314.pyc +0 -0
  68. package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
  69. package/python/_i18n.py +18 -0
  70. package/python/computer_use.py +190 -0
  71. package/python/hotkey.py +196 -0
  72. package/python/stt.py +240 -0
  73. package/python/tts_say.py +177 -0
  74. package/python/voice_clone.py +98 -0
  75. package/requirements.txt +6 -0
  76. package/src/agent.ts +791 -0
  77. package/src/agent_registry.ts +109 -0
  78. package/src/commands.ts +641 -0
  79. package/src/config.ts +245 -0
  80. package/src/in_memory_credentials.ts +61 -0
  81. package/src/index.ts +78 -0
  82. package/src/locale.ts +131 -0
  83. package/src/logo.ts +120 -0
  84. package/src/mcp.ts +192 -0
  85. package/src/memory.ts +303 -0
  86. package/src/pet.ts +346 -0
  87. package/src/renderer.ts +350 -0
  88. package/src/repl.ts +902 -0
  89. package/src/setup.ts +371 -0
  90. package/src/skills.ts +96 -0
  91. package/src/slash_menu.ts +371 -0
  92. package/src/slash_registry.ts +53 -0
  93. package/src/speaker_context.ts +41 -0
  94. package/src/text_split.ts +79 -0
  95. package/src/tools/computer_use.ts +142 -0
  96. package/src/tools/emotion_tool.ts +55 -0
  97. package/src/tools/keep_silent_tool.ts +26 -0
  98. package/src/tools/memory_tool.ts +24 -0
  99. package/src/tools/skill_tools.ts +100 -0
  100. package/src/tools/tavily_tools.ts +431 -0
  101. package/src/tools/voice_tools.ts +28 -0
  102. package/src/tts_stream.ts +260 -0
  103. package/src/tui_select.ts +146 -0
  104. package/src/undo.ts +451 -0
  105. package/src/utils/python.ts +211 -0
  106. package/src/utils/spawn.ts +56 -0
  107. package/src/voice.ts +83 -0
  108. package/src/voice_cli.ts +198 -0
  109. 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()
@@ -0,0 +1,6 @@
1
+ cua>=0.1.1,<0.2.0
2
+ websockets>=12.0
3
+ pyaudio>=0.2.14
4
+ numpy>=1.26.0
5
+ pynput>=1.8.0
6
+ dashscope>=1.20.0