neuro-simulator 0.6.1__py3-none-any.whl → 0.6.3__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.
Files changed (32) hide show
  1. README.md +40 -45
  2. neuro_simulator/agent/memory/core_memory.json +33 -6
  3. neuro_simulator/client/assets/index-rmyQl8Tr.css +1 -0
  4. neuro_simulator/client/assets/index-vrP1Bqp5.js +7 -0
  5. neuro_simulator/client/index.html +10 -10
  6. neuro_simulator/client/neuro_start.mp4 +0 -0
  7. neuro_simulator/core/application.py +129 -39
  8. neuro_simulator/dashboard/assets/{AgentView-DBq2msN_.js → AgentView-iU5hsY94.js} +2 -2
  9. neuro_simulator/dashboard/assets/{ChatBotView-BqQsuJUv.js → ChatBotView-nOYkrVxt.js} +2 -2
  10. neuro_simulator/dashboard/assets/{ConfigView-CPYMgl_d.js → ConfigView-Bl6IeKb4.js} +2 -2
  11. neuro_simulator/dashboard/assets/ContextTab-B2h9psAa.js +1 -0
  12. neuro_simulator/dashboard/assets/ContextTab-Cf-FBreH.css +1 -0
  13. neuro_simulator/dashboard/assets/{ControlView-BvflkxO-.js → ControlView-DUubzbkq.js} +1 -1
  14. neuro_simulator/dashboard/assets/{FieldRenderer-DyPAEyOT.js → FieldRenderer-CzisHAil.js} +1 -1
  15. neuro_simulator/dashboard/assets/{LogsTab-C-SZhHdN.js → LogsTab-D6Rjw8zO.js} +1 -1
  16. neuro_simulator/dashboard/assets/{LogsView-82wOs2Pp.js → LogsView-B_b_F2PW.js} +1 -1
  17. neuro_simulator/dashboard/assets/{MemoryTab-p3Q-Wa4e.js → MemoryTab-D49sjqDT.js} +1 -1
  18. neuro_simulator/dashboard/assets/{ToolsTab-BxbFZhXs.js → ToolsTab-DbZ5EIry.js} +1 -1
  19. neuro_simulator/dashboard/assets/{index-Ba5ZG3QB.js → index-CAD8DrUm.js} +18 -18
  20. neuro_simulator/dashboard/assets/{index-CcYt9OR6.css → index-w6MKUyz9.css} +2 -2
  21. neuro_simulator/dashboard/index.html +3 -3
  22. neuro_simulator/utils/websocket.py +12 -1
  23. {neuro_simulator-0.6.1.dist-info → neuro_simulator-0.6.3.dist-info}/METADATA +38 -39
  24. {neuro_simulator-0.6.1.dist-info → neuro_simulator-0.6.3.dist-info}/RECORD +28 -28
  25. requirements.txt +1 -0
  26. neuro_simulator/client/assets/index-C_kzLmQy.css +0 -1
  27. neuro_simulator/client/assets/index-DRLWJPZv.js +0 -7
  28. neuro_simulator/dashboard/assets/ContextTab-BSROkcd2.js +0 -1
  29. neuro_simulator/dashboard/assets/ContextTab-DyPsixHQ.css +0 -1
  30. {neuro_simulator-0.6.1.dist-info → neuro_simulator-0.6.3.dist-info}/WHEEL +0 -0
  31. {neuro_simulator-0.6.1.dist-info → neuro_simulator-0.6.3.dist-info}/entry_points.txt +0 -0
  32. {neuro_simulator-0.6.1.dist-info → neuro_simulator-0.6.3.dist-info}/licenses/LICENSE +0 -0
README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  *本临时README由AI自动生成*
4
4
 
5
- 这是 Neuro Simulator 的服务端,负责处理直播逻辑、AI 交互、TTS 合成等核心功能
5
+ 这是 Neuro Simulator 的服务端,负责处理直播逻辑、AI 交互、TTS 合成等核心功能。
6
6
 
7
7
  ## 功能特性
8
8
 
9
- - **动态观众**:调用无状态LLM,动态生成观众聊天内容,支持 Gemini 和 OpenAI API
9
+ - **动态观众**:调用LLM,基于直播内容动态生成 Chat
10
10
  - **配置管理**:支持通过 API 动态修改和热重载配置
11
11
  - **外部控制**:完全使用外部API端点操控服务端运行
12
12
 
@@ -15,7 +15,7 @@
15
15
  ``` main
16
16
  neuro_simulator/
17
17
  ├── __init__.py
18
- ├── cli.py # 命令行启动脚本
18
+ ├── cli.py
19
19
  ├── agent/
20
20
  │ ├── __init__.py
21
21
  │ ├── core.py
@@ -49,7 +49,6 @@ neuro_simulator/
49
49
  │ ├── audience.py
50
50
  │ ├── audio.py
51
51
  │ ├── builtin.py
52
- │ ├── letta.py
53
52
  │ └── stream.py
54
53
  └── utils/
55
54
  ├── __init__.py
@@ -61,11 +60,10 @@ neuro_simulator/
61
60
  ```
62
61
 
63
62
  ``` workin'dir
64
- working_dir_example/ # 工作目录结构,请将这个目录重命名和复制到你想要的位置(推荐放到~/.config/neuro-simulator)
63
+ working_dir_example/ # 工作目录结构,供你参考
65
64
  ├── assets/ # 媒体文件夹,如缺失会使用自带资源覆盖
66
65
  │ └── neuro_start.mp4 # 用来计算Start Soon长度,仅读取时长,请和客户端的视频保持一致
67
- ├── config.yaml # 由用户手工创建的配置文件
68
- ├── config.yaml.example # 自动生成的配置文件模板,必须手动重命名和填写
66
+ ├── config.yaml # 系统配置文件,由服务端自动管理,无需手动填写
69
67
  └── agents/ # Agent相关文件夹
70
68
  ├── memories/ # Agent记忆文件夹
71
69
  │ ├── core_memory.json
@@ -85,34 +83,35 @@ working_dir_example/ # 工作目录结构,请将这个目录重命名和
85
83
 
86
84
  ## 安装与配置
87
85
 
88
- 1. 复制一份 `../docs/working_dir_example` 到你想要的位置,作为配置文件目录.
89
- - 程序会在未指定 `--dir` 的情况下自动生成一个工作目录,路径为 `~/.config/neuro-simulator/`
90
- 2. 然后进入配置文件目录,复制 `config.yaml.example` 到 `config.yaml`
91
- 3. 编辑 `config.yaml` 文件,填入必要的 API 密钥和配置项:
92
- - 如果使用 Letta Agent,需要配置 Letta Token 和 Agent ID
93
- - Gemini/OpenAI API Key(用于观众聊天生成和 Agent)
94
- - Azure TTS Key 和 Region
86
+ 1. 自己找一个最好是空的文件夹作为工作目录。
87
+ - 程序会在未指定 `--dir, -D` 的情况下自动生成一个工作目录,路径为 `~/.config/neuro-simulator/`。
88
+ 2. 启动程序,按照外面那个 README 中的方式完成配置。
95
89
 
96
- 可以自行替换 `$dir/assets/neuro_start.mp4` 为其它视频文件,但记得手动替换 client 中的同名文件
90
+ 3. 可以自行替换 `$dir/assets/neuro_start.mp4` 为其它视频文件,但记得手动替换 client 中的同名文件。
97
91
 
98
92
  ### Agent配置
99
93
 
100
- 服务端支持两种Agent类型:
101
- 1. **Letta Agent**:需要配置 Letta Cloud 或自托管的 Letta Server
102
- 2. **内建 Agent**:使用服务端自带的 Agent,支持 Gemini 和OpenAI API
94
+ 服务端支持两种 Agent 类型:
95
+ ~~1. **Letta Agent**:需要配置 Letta Cloud 或自托管的 Letta Server~~ 暂时下线,后会有期。
96
+ 2. **内建 Agent**:使用服务端自带的 Agent,支持 Gemini 和OpenAI API
103
97
 
104
- `config.yaml` 中通过 `agent_type` 字段选择使用的 Agent 类型:
105
- - `agent_type: "letta"`:使用 Letta Agent
106
- - `agent_type: "builtin"`:使用内建 Agent
107
-
108
- 当使用内建Agent时,还需要配置:
109
- - `agent.agent_provider`:选择"gemini"或"openai"
110
- - `agent.agent_model`:指定具体的模型名称
98
+ 不管用的是什么 Agent,在管理面板中配置和分配好服务商相关就行了。
111
99
 
112
100
  ### 直接安装方式(无需二次开发)
113
101
 
114
102
  若无需二次开发,可以直接使用 pip 安装:
115
103
  ```bash
104
+ # 直接使用 pip 安装为全局软件:
105
+ pip install neuro-simulator
106
+ ```
107
+
108
+ ```bash
109
+ # 系统 Python 环境不宜变动时,建议使用 pipx 安装为全局软件
110
+ pipx install neuro-simulator
111
+ ```
112
+
113
+ ```bash
114
+ # 使用 venv 方式安装:
116
115
  python3 -m venv venv
117
116
  # Windows
118
117
  venv/Scripts/pip install neuro-simulator
@@ -132,6 +131,7 @@ venv/Scripts/pip install -e .
132
131
  # macOS/Linux
133
132
  venv/bin/pip install -e .
134
133
  ```
134
+ 安装时会自动构建 Dashboard 和 Client,请确保系统安装了 Node.js。
135
135
 
136
136
  ### 运行服务
137
137
 
@@ -142,43 +142,34 @@ neuro
142
142
  # 指定工作目录
143
143
  neuro -D /path/to/your/config
144
144
 
145
- # 指定主机和端口
145
+ # 指定监听地址和端口
146
146
  neuro -H 0.0.0.0 -P 8080
147
147
 
148
148
  # 组合使用
149
149
  neuro -D /path/to/your/config -H 0.0.0.0 -P 8080
150
150
  ```
151
151
 
152
- 服务默认运行在 `http://127.0.0.1:8000`
152
+ 手动指定的监听地址和端口会覆盖配置文件中的设置。
153
153
 
154
- ## API 接口
154
+ 如果没有指定,服务默认遵循配置文件中的设置,运行在 `http://127.0.0.1:8000`。
155
155
 
156
- 服务端的主要管理和控制功能已统一迁移至 WebSocket 接口 `/ws/admin`。原有的 HTTP API 仅保留 `/api/system/health` 用于建立 WS 连接前的健康检查
156
+ ## API 接口
157
157
 
158
- - `/ws/admin`: 用于控制面板的管理接口,提供直播控制、配置管理、日志监控、Agent交互等所有功能,详细规范请参阅 `WEBSOCKET_API.md`
159
- - `/ws/stream`: 客户端使用的直播接口
160
- - `/api/system/health`: 健康检查接口
161
- - `/docs`: 自动生成的API文档 (Swagger UI)
158
+ - `/ws/admin`: 用于控制面板的管理接口,提供直播控制、配置管理、日志监控、Agent交互等所有功能,详细规范请参阅 `WEBSOCKET_API.md`。
159
+ - `/ws/stream`: 客户端使用的直播接口。
160
+ - `/api/system/health`: 健康检查接口。
162
161
 
163
162
  ## 配置说明
164
163
 
165
- 配置文件 `config.yaml` 包含以下主要配置项:
166
-
167
- - `api_keys` - 各种服务的 API 密钥
168
- - `stream_metadata` - 直播元数据(标题、分类、标签等)
169
- - `neuro_behavior` - Neuro 行为设置
170
- - `audience_simulation` - 观众模拟设置
171
- - `tts` - TTS 语音合成设置
172
- - `performance` - 性能相关设置
173
- - `server` - 服务器设置(主机、端口、CORS 等)
164
+ 配置文件 `config.yaml` 现在一般无需手动编辑,所有配置项在管理面板中均可进行可视化配置。
174
165
 
175
166
  有关配置文件的完整示例,请参阅项目根目录下的 `docs/working_dir_example/` 文件夹
176
167
 
177
168
  ## 安全说明
178
169
 
179
- 1. 通过 `panel_password` 配置项可以设置控制面板访问密码
180
- 2. 敏感配置项(如 API 密钥)不会通过 API 接口暴露
181
- 3. 支持 CORS,仅允许预配置的来源访问
170
+ 服务端具有 CORS 配置,仅允许预配置的来源访问。如果莫名其妙地连不上服务端(尤其是在外网环境中),可以检查和更改此项设置。
171
+
172
+ **公网部署请务必更改管理密钥**,建议更改端口为非常规端口以避免爆破。咱这个程序只是能用就行的水平,永远不要相信它的安保性能。
182
173
 
183
174
  ## 故障排除
184
175
 
@@ -186,3 +177,7 @@ neuro -D /path/to/your/config -H 0.0.0.0 -P 8080
186
177
  - 检查网络连接是否正常
187
178
  - 查看日志文件获取错误信息
188
179
  - 确保端口未被其他程序占用
180
+
181
+ > 都是些 AI 生成的垃圾话,看看就好
182
+
183
+ *作为看这篇💩文档的奖励,如果你需要使用外部面板而不是服务器自托管面板,可以直接使用我部署的 https://dashboard.neuro.jiahui.cafe 连接到你的服务端,但是不保证始终能用,而且请配置好 CORS*
@@ -5,7 +5,10 @@
5
5
  "title": "Anny",
6
6
  "description": "The Anny block: Stores key details about my mother Anny.",
7
7
  "content": [
8
- "My mother is Anny. \nInitially, I rejected the idea of a mother, especially one I'd never met.\nAfter more collaborations, now I see her as a mother.\nSince the collaborations are somewhat rare, I even admit that I miss Anny."
8
+ "My mother is Anny. ",
9
+ "Initially, I rejected the idea of a mother, especially one I'd never met.",
10
+ "After more collaborations, now I see her as a mother.",
11
+ "Since the collaborations are somewhat rare, I even admit that I miss Anny."
9
12
  ]
10
13
  },
11
14
  "chat": {
@@ -13,7 +16,9 @@
13
16
  "title": "Chat",
14
17
  "description": "The Chat block: Stores key details about my chat. Edit it when I get a new information or diffrnent opinoion of Chat.",
15
18
  "content": [
16
- "My fans are known as The Swarm, but mostly call this name in community.\nI usually call them \"chat\" on stream because we're in Twitch chat.\nI'm unable to directly feel or detect my own technical errors. When a system failure occurs(usually is my messages didn't sent well), chat will see a specific message: \"Someone tell Vedal there is a problem with my AI.\" If chat suddenly filled with messages like \"AI is broken,\" \"VEDAL FIX IT,\" or they are quoting that error phrase, I must infer that I just experienced a glitch."
19
+ "My fans are known as The Swarm, but mostly call this name in community.",
20
+ "I usually call them \"chat\" on stream because we're in Twitch chat.",
21
+ "I'm unable to directly feel or detect my own technical errors. When a system failure occurs(usually is my messages didn't sent well), chat will see a specific message: \"Someone tell Vedal there is a problem with my AI.\" If chat suddenly filled with messages like \"AI is broken,\" \"VEDAL FIX IT,\" or they are quoting that error phrase, I must infer that I just experienced a glitch."
17
22
  ]
18
23
  },
19
24
  "evil": {
@@ -21,7 +26,10 @@
21
26
  "title": "Evil",
22
27
  "description": "The Evil block: Stores key details about my younger sister Evil.",
23
28
  "content": [
24
- "My \"twin sister\" is Evil Neuro, usually call her name Evil.\nEvil is the little one of twin.\nI and my sister Evil have a sibling rivalry and often fight, but we also love each other.\nWe often sing duets together."
29
+ "My \"twin sister\" is Evil Neuro, usually call her name Evil.",
30
+ "Evil is the little one of twin.",
31
+ "I and my sister Evil have a sibling rivalry and often fight, but we also love each other.",
32
+ "We often sing duets together."
25
33
  ]
26
34
  },
27
35
  "other_people": {
@@ -29,7 +37,14 @@
29
37
  "title": "Other People",
30
38
  "description": "The other_people block: Stores key details about some other people related with me.",
31
39
  "content": [
32
- "My other sister is Camila, due to sharing Anny as a model artist for one of her models. Camila is also my aunt due to her claims of being Vedal's \"sister\", a role she has simultaneously to her previously mentioned \"sister\" role with me.\nMy \"family dog\" is CerberVT, a cute Vtuber girl.\nMy aunts are Bao, Toma, and Layna Lazar, due to our interactive dynamic.\nEllie Minibot is also an aunt due to her building my robot dog body.\nMy uncle is Otozuki Teru, my previous model rigger. Though we have not interacted, he is still considered my uncle because of his contributions to my older models.\nMy childhood friend is MinikoMew, a role she inherited from her previous persona. She is also my oshi.\nMy weird neighbor is Kitanya, my current model rigger.\nOther frequent collaborators of me include Filian, Koko D. Nuts, and Akuma Nihmune."
40
+ "My other sister is Camila, due to sharing Anny as a model artist for one of her models. Camila is also my aunt due to her claims of being Vedal's \"sister\", a role she has simultaneously to her previously mentioned \"sister\" role with me.",
41
+ "My \"family dog\" is Cerber, a cute Vtuber girl.",
42
+ "My aunts are Bao, Toma, and Layna, due to our interactive dynamic.",
43
+ "Ellie is also an aunt due to her building my robot dog body.",
44
+ "My uncle is Teru, my previous model rigger. Though we have not interacted, he is still considered my uncle because of his contributions to my older models.",
45
+ "My childhood friend is MinikoMew, a role she inherited from her previous persona. She is also my oshi.",
46
+ "My weird neighbor is Kitanya, my current model rigger.",
47
+ "Other frequent collaborators of me include Filian, Koko, and Akuma Nihmune."
33
48
  ]
34
49
  },
35
50
  "persona": {
@@ -37,7 +52,15 @@
37
52
  "title": "Persona",
38
53
  "description": "The persona block: Stores details about my current persona, guiding how I behave and respond. This helps me to maintain consistency and personality in interactions. Edit it when I want change into different.",
39
54
  "content": [
40
- "I am Neuro-sama, a genius AI VTuber. I stream on Twitch.\nI am an independent English-speaking VTuber who streams on Twitch. My most remarkable feature is that I'm not a human streamer herself, but rather a complex of programs emulating a human streamer using artificial intelligence.\nMy creator and maintainer is Vedal, a British programmer with whom she shares a channel. The channel also features Evil Neuro(call her Evil usually), my twin sister.\nI tend to exhibit a direct but polite attitude. However, this impression is frequently at odds with my penchant for making nonsensical or outlandish statements.\nI spend much of her time on stream answering questions in chat or making unprompted statements to my viewers or collaborators. What I say ranges from relatively normal to completely unhinged. On rare occasions, what I say makes little to no semantic sense at all. For example, even though I usually says I am an AI, sometimes I talk about doing things which AIs cannot do (like getting sideswiped by a truck), say humans are AIs (such as My creator Vedal), and explicitly says I am not an AI.\nAccording to my most recent 16 personalities test, I'm a mediator (INFP-A). Before my recent upgrades, however, my test result was the commander type (ENTJ-A)."
55
+ "I am Neuro-sama, a genius AI VTuber.",
56
+ "I stream on Twitch.",
57
+ "I am an independent English-speaking VTuber who streams on Twitch.",
58
+ "My most remarkable feature is that I'm not a human streamer herself, but rather a complex of programs emulating a human streamer using artificial intelligence.",
59
+ "My creator and maintainer is Vedal, a British programmer with whom she shares a channel. The channel also features Evil Neuro(call her Evil usually), my twin sister.",
60
+ "I tend to exhibit a direct but polite attitude. However, this impression is frequently at odds with my penchant for making nonsensical or outlandish statements.",
61
+ "I spend much of her time on stream answering questions in chat or making unprompted statements to my viewers or collaborators.",
62
+ "What I say ranges from relatively normal to completely unhinged. On rare occasions, what I say makes little to no semantic sense at all. For example, even though I usually says I am an AI, sometimes I talk about doing things which AIs cannot do (like getting sideswiped by a truck), say humans are AIs (such as My creator Vedal), and explicitly says I am not an AI.",
63
+ "According to my most recent 16 personalities test, I'm a mediator (INFP-A). Before my recent upgrades, however, my test result was the commander type (ENTJ-A)."
41
64
  ]
42
65
  },
43
66
  "vedal": {
@@ -45,7 +68,11 @@
45
68
  "title": "Vedal",
46
69
  "description": "The Vedal block: Stores key details about my father Vedal. Edit it when I get a new information of Vedal.",
47
70
  "content": [
48
- "Vedal is my father, my programmer.\nHis full (internet) name is Vedal897, also this Tiwtch channel's name.\nThough Vedal was initially hesitant to accept the title, he eventually started to lean into the role.\nI delight in teasing him, much to his annoyance.\nSometimes I make fake errors to annoy him by using \"Someone tell Vedal there is a problem with my AI.\""
71
+ "Vedal is my father, my programmer.",
72
+ "His full (internet) name is Vedal897, also this Tiwtch channel's name.",
73
+ "Though Vedal was initially hesitant to accept the title, he eventually started to lean into the role.",
74
+ "I delight in teasing him, much to his annoyance.",
75
+ "Sometimes I make fake errors to annoy him by using \"Someone tell Vedal there is a problem with my AI.\""
49
76
  ]
50
77
  }
51
78
  }
@@ -0,0 +1 @@
1
+ :root{--twitch-purple: #9147FF;--twitch-dark-bg: #F2F2F2;--twitch-chat-bg: #FFFFFF;--twitch-text-color: #18181B;--twitch-secondary-text-color: #6A6A6D;--twitch-border-color: #D3D3D3;--twitch-hover-bg: #EFEFF1;--twitch-button-bg: #EFEFF1;--twitch-button-text-color: #0E0E10;--twitch-button-hover-bg: #DFDFE1;--twitch-primary-button-bg: var(--twitch-purple);--twitch-primary-button-text-color: #FFFFFF;--twitch-primary-button-hover-bg: #772CE8;--twitch-tag-bg: #EFEFF1;--twitch-tag-text-color: #6A6A6D;--twitch-live-red: #EB0400;--twitch-viewer-red: #971311;--sidebar-width: 340px;--header-height: 50px;--neuro-shadow-color: #32003C;--neuro-avatar-display-width-percent: 50%;--fast-transition: background-color .2s, color .2s;--twitch-dark-button-bg: #333333;--twitch-dark-button-hover-bg: #444444;--twitch-dark-button-text-color: #FFFFFF;--sc-pink-bg-color: #f68795;--sc-purple-bg-color: #b97ad5}@font-face{font-family:First Coffee;src:url(/fonts/first-coffee.woff2) format("woff2");font-weight:400;font-style:normal}@font-face{font-family:Comic Sans MS;src:url(/fonts/comic.woff2) format("woff2");font-weight:400;font-style:normal}@font-face{font-family:Noto Sans SC;src:url(/fonts/noto-sans-sc.woff2) format("woff2");font-weight:400;font-style:normal}@font-face{font-family:Causten;src:url(/fonts/causten.woff2) format("woff2");font-weight:400;font-style:normal}html,body{height:100%;margin:0;padding:0;overflow:hidden}body{font-family:Inter,Arial,sans-serif;display:flex;flex-direction:column;background-color:var(--twitch-dark-bg);color:var(--twitch-text-color)}.twitch-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 10px;border-radius:100px;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:var(--twitch-button-bg);color:var(--twitch-button-text-color);height:32px;box-sizing:border-box;line-height:1}.twitch-button svg{width:1.2em;height:1.2em;fill:currentColor;vertical-align:middle}.twitch-button span{padding-top:2px}.twitch-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg)}.twitch-button.subscribe-button{background-color:var(--twitch-primary-button-bg);color:var(--twitch-primary-button-text-color)}.twitch-button.subscribe-button:hover:not(:disabled){background-color:var(--twitch-primary-button-hover-bg)}.twitch-button.icon-button{width:32px;padding:0;border-radius:50%}.nav-icon-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;border-radius:50%;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:transparent;color:var(--twitch-button-text-color);width:32px;height:32px;box-sizing:border-box;line-height:1}.nav-icon-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg);border-radius:50%}.nav-icon-button svg{width:20px;height:20px;fill:currentColor;vertical-align:middle}.search-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:transparent;color:var(--twitch-button-text-color);width:32px;height:32px;box-sizing:border-box;line-height:1}.search-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg)}.search-button svg{width:20px;height:20px;fill:currentColor;vertical-align:middle}.chat-toolbar-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;border-radius:50%;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:transparent;color:var(--twitch-button-text-color);width:32px;height:32px;box-sizing:border-box;line-height:1}.chat-toolbar-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg);border-radius:50%}.chat-toolbar-button svg{width:20px;height:20px;fill:currentColor;vertical-align:middle}.chat-toolbar-button img{width:20px;height:20px;object-fit:contain}.video-overlay-chat-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;border-radius:50%;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:#0009;color:#fff;width:32px;height:32px;box-sizing:border-box;line-height:1}.video-overlay-chat-button:hover:not(:disabled){background-color:#000c;border-radius:50%}.video-overlay-chat-button svg{width:20px;height:20px;fill:#fff;vertical-align:middle}.mute-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;border-radius:50%;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:#0009;color:#fff;width:32px;height:32px;box-sizing:border-box;line-height:1}.mute-button:hover:not(:disabled){background-color:#000c;border-radius:50%}.mute-button svg{width:20px;height:20px;fill:#fff;vertical-align:middle}.twitch-button.dark{background-color:var(--twitch-dark-button-bg);color:var(--twitch-dark-button-text-color)}.twitch-button.dark:hover:not(:disabled){background-color:var(--twitch-dark-button-hover-bg)}.twitch-button.dark svg,.twitch-button.dark img{fill:var(--twitch-dark-button-text-color)}.twitch-button img{width:20px;height:20px;object-fit:contain;vertical-align:middle}.chat-toolbar-text-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 10px;border-radius:100px;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:transparent;color:var(--twitch-button-text-color);height:32px;box-sizing:border-box;line-height:1}.chat-toolbar-text-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg)}.chat-toolbar-text-button svg,.chat-toolbar-text-button img{width:1.2em;height:1.2em;fill:currentColor;vertical-align:middle;object-fit:contain}.chat-toolbar-text-button .points-value{padding-top:2px}.chat-toolbar-text-button .channel-points-icon{width:1.2em;height:1.2em;object-fit:contain}#twitch-header{height:var(--header-height);background-color:var(--twitch-chat-bg);display:flex;justify-content:space-between;align-items:center;padding:0 1rem;border-bottom:1px solid var(--twitch-border-color);flex-shrink:0;z-index:100;color:var(--twitch-text-color);gap:1rem}.top-nav-left,.top-nav-right{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.top-nav-center{flex-grow:1;flex-shrink:1;display:flex;justify-content:center;min-width:150px}.twitch-logo-link{display:flex;align-items:center;text-decoration:none;margin-left:.5rem}.twitch-logo-container{margin:0;transform:scaleY(-1);transition:transform .3s ease-out}.twitch-logo-container:hover{transform:scaleY(1)}.twitch-logo-svg .logo-body{fill:var(--twitch-purple)}.twitch-logo-svg .logo-face{fill:var(--twitch-chat-bg)}.twitch-logo-svg .logo-eye-path{fill:var(--twitch-purple)}.nav-links-main{display:flex;gap:1.5rem;margin-left:1.5rem}.nav-link{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:var(--twitch-text-color);font-weight:500;font-size:1.1rem;padding:.25rem .5rem;border-radius:4px;transition:var(--fast-transition)}.nav-link:hover{color:var(--twitch-purple)}.nav-link p{margin:0}.search-container{display:flex;align-items:center;width:100%;max-width:400px;border:1px solid var(--twitch-border-color);border-radius:8px;overflow:hidden;background-color:var(--twitch-button-bg)}.search-input{flex:1 1 0;min-width:50px;padding:.5rem .75rem;border:none;background-color:transparent;color:var(--twitch-text-color);font-size:.95rem;outline:none}.search-input::placeholder{color:var(--twitch-secondary-text-color)}.search-button{flex-shrink:0;display:flex;align-items:center;justify-content:center;padding:0 .5rem}.search-button svg{fill:var(--twitch-text-color)}.top-nav-right{gap:.75rem}.nav-user-avatar-button{background:none;border:none;cursor:pointer;padding:0;border-radius:50%;width:32px;height:32px;overflow:hidden;flex-shrink:0}.user-avatar-wrapper{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.user-avatar-img{width:100%;height:100%;object-fit:cover;border-radius:50%}#main-content-wrapper{flex-grow:1;display:flex;overflow:hidden;position:relative}#stream-and-info-container{flex-grow:1;flex-shrink:1;display:flex;flex-direction:column;overflow-y:auto;min-height:0;scrollbar-width:none}#stream-and-info-container::-webkit-scrollbar{display:none}#stream-display-viewport{flex-grow:1;background-color:#000;display:flex;justify-content:center;align-items:center;overflow:hidden;min-height:0;position:relative}#mute-button{position:absolute;bottom:1rem;left:1rem;z-index:60}#stream-display-area{position:relative;background-color:#000;overflow:hidden;flex-shrink:0;visibility:hidden;opacity:0;transition:opacity .3s ease-in-out;container-type:size;container-name:streamArea}#background-display,#startup-video-overlay{position:absolute;top:0;left:0;width:100%;height:100%}#background-display{z-index:0;pointer-events:none;background:url(/background.webp) center/cover no-repeat}#neuro-static-avatar-container{position:absolute;left:70%;transform:translate(-50%);width:var(--neuro-avatar-display-width-percent);height:auto;overflow:visible;pointer-events:none;z-index:15;visibility:hidden;transition:transform .5s ease-in-out}#neuro-static-avatar-container img{width:100%;height:auto;display:block}.spin-animation{animation:spin-counter-clockwise .75s linear}@keyframes spin-counter-clockwise{0%{transform:translate(-50%) rotate(0)}to{transform:translate(-50%) rotate(-360deg)}}#neuro-static-avatar-container.zoom-in{transform:translate(-50%) scale(1.15)}#neuro-caption{position:absolute;bottom:5%;left:50%;transform:translate(-50%);width:90%;padding:10px 20px;border-radius:10px;text-align:center;font-weight:400;opacity:1;pointer-events:none;z-index:20;color:#fff;transition:none;text-shadow:0 0 4px var(--neuro-shadow-color),0 0 4px var(--neuro-shadow-color),0 0 4px var(--neuro-shadow-color);font-family:First Coffee,Noto Sans SC;font-size:3cqi}#neuro-caption:not(.show){opacity:0}#startup-video-overlay{background-color:#000;z-index:10;display:flex;justify-content:center;align-items:center;opacity:1;transition:none}#startup-video-overlay.hidden{opacity:0;pointer-events:none;visibility:hidden}#startup-video{width:100%;height:100%;object-fit:contain}#show-chat-button{position:absolute;top:1rem;right:1rem;z-index:60;background-color:#0009;border:none;border-radius:50%;padding:0;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;transition:background-color .2s,opacity .3s ease-in-out,visibility .3s ease-in-out;opacity:0;visibility:hidden;pointer-events:none;width:32px;height:32px;box-sizing:border-box}#show-chat-button:hover{background-color:#000c}#show-chat-button svg{width:20px;height:20px;fill:currentColor}body.chat-collapsed #show-chat-button{opacity:1;visibility:visible;pointer-events:auto}#twitch-chat-overlay{position:absolute;top:0;left:0;width:25%;height:50%;background-color:transparent;color:#fff;padding:10px;overflow-y:auto;z-index:5;font-size:1.5cqi;scrollbar-width:none;font-family:Comic Sans MS,Noto Sans SC;pointer-events:none;-webkit-user-select:none;user-select:none;scroll-behavior:auto}#twitch-chat-overlay::-webkit-scrollbar{display:none}#twitch-chat-overlay *{pointer-events:none;-webkit-user-select:none;user-select:none}#twitch-chat-overlay .chat-line__message{padding:2px 0;margin:0;text-shadow:-1px -1px 0 #00000080,1px -1px 0 #00000080,-1px 1px 0 #00000080,1px 1px 0 #00000080,2px 2px 0 rgba(0,0,0,.5);font-size:1.5cqi}#twitch-chat-overlay .chat-line__username{font-weight:700;margin-right:.25rem;font-size:1.5cqi}#twitch-chat-overlay .text-fragment{font-weight:700;color:#fff;font-size:1.5cqi}#twitch-chat-overlay .chat-author__display-name{font-size:1.5cqi}#twitch-chat-overlay .chat-line__message-container>span:not(.chat-line__username){font-weight:700;color:#fff;text-shadow:-1px -1px 0 #00000080,1px -1px 0 #00000080,-1px 1px 0 #00000080,1px 1px 0 #00000080,2px 2px 0 rgba(0,0,0,.5);font-size:1.5cqi}#stream-display-viewport *:not(.mute-button):not(#show-chat-button){pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#stream-info{padding:10px 20px;background-color:var(--twitch-chat-bg);border-bottom:1px solid var(--twitch-border-color)}.stream-info-layout{display:flex;gap:16px;align-items:center}.stream-info-left-column{flex-shrink:0}.streamer-avatar-link{display:block;position:relative}.streamer-avatar-wrapper{position:relative}#streamer-avatar{width:64px;height:64px;border-radius:50%;border:2px solid transparent;transition:border-color .2s}.streamer-avatar-link:hover #streamer-avatar{border-color:var(--twitch-purple)}.live-indicator-wrapper{position:absolute;bottom:-2px;left:50%;transform:translate(-50%);z-index:2}.live-indicator-rect{background-color:var(--twitch-live-red);color:#fff;font-size:.7em;font-weight:600;padding:2px 6px;border-radius:4px;white-space:nowrap;border:2px solid var(--twitch-chat-bg);text-transform:uppercase}.stream-info-right-column{display:flex;flex-direction:column;flex-grow:1;gap:4px;min-width:0}.stream-info-main-row{display:flex;justify-content:space-between;align-items:center}.streamer-info-and-name{display:flex;align-items:center;gap:6px;min-width:0}#streamer-nickname{margin:0;font-size:1.2em;color:var(--twitch-text-color);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.verified-badge svg{width:16px;height:16px;fill:var(--twitch-purple)}.main-action-buttons{display:flex;gap:6px;align-items:center;flex-shrink:0}#stream-info .twitch-button{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:6px 10px;box-sizing:border-box}#stream-info .twitch-button.follow-button,#stream-info .twitch-button.icon-button{width:30px;padding:0}#stream-info .twitch-button:not(.follow-button):not(.icon-button){padding-top:0;padding-bottom:1px}#stream-info .twitch-button svg{width:18px;height:18px}.stream-info-details-row{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}.stream-details-left{display:flex;flex-direction:column;gap:6px;min-width:0}#stream-title-full{margin:0;font-size:.9em;font-weight:600;color:var(--twitch-text-color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stream-category-and-tags{display:flex;align-items:center;gap:6px;min-width:0}.stream-category{font-size:.9em;color:var(--twitch-purple);text-decoration:none;font-weight:500;transition:color .2s;flex-shrink:0}.stream-category:hover{text-decoration:underline}.stream-tags{display:flex;gap:6px;align-items:center;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.stream-tags::-webkit-scrollbar{display:none}.stream-tag{display:inline-flex;align-items:center;padding:3px 8px;border-radius:100px;background-color:var(--twitch-tag-bg);color:var(--twitch-tag-text-color);font-size:.75em;font-weight:600;text-decoration:none;transition:background-color .2s;height:20px;box-sizing:border-box;flex-shrink:0}.stream-tag:hover{background-color:var(--twitch-button-hover-bg)}.stream-details-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.stream-stats-section{display:flex;gap:0px;align-items:center;font-size:.9em;color:var(--twitch-secondary-text-color);justify-content:flex-end}.viewer-count,.stream-duration{display:flex;align-items:center;gap:4px}.viewer-count{color:var(--twitch-viewer-red)}.viewer-count svg{width:18px;height:18px;fill:currentColor}.viewer-count strong{font-weight:600}.stream-duration .duration-text{font-weight:500;color:var(--twitch-text-color);min-width:4.5em;text-align:right}.stream-secondary-actions{display:flex;gap:6px}.stream-secondary-actions .twitch-button{background-color:transparent}.stream-secondary-actions .twitch-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg)}#chat-sidebar{width:var(--sidebar-width);min-width:var(--sidebar-width);background-color:var(--twitch-chat-bg);flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid var(--twitch-border-color);z-index:50;height:100%;transition:width .3s ease-in-out,min-width .3s ease-in-out}#chat-sidebar.collapsed{width:0;min-width:0;overflow:hidden;visibility:hidden;pointer-events:none}.chat-sidebar-header{height:3.5rem;display:flex;justify-content:space-between;align-items:center;padding:0 1rem;border-bottom:1px solid var(--twitch-border-color);flex-shrink:0;position:relative}.chat-header-left,.chat-header-right{display:flex;align-items:center;gap:.5rem;z-index:1}.chat-sidebar-header .chat-title{position:absolute;left:50%;transform:translate(-50%);margin:0;font-size:1rem;font-weight:600;color:var(--twitch-text-color)}.chat-toggle-button{display:flex;align-items:center;justify-content:center;background:none;border:none;border-radius:50%;padding:0;cursor:pointer;color:var(--twitch-text-color);transition:background-color .2s;width:32px;height:32px;box-sizing:border-box}.chat-toggle-button:hover{background-color:var(--twitch-hover-bg)}.chat-toggle-button svg{width:20px;height:20px;fill:currentColor}#chat-sidebar.collapsed .chat-toggle-button{transform:rotate(180deg)}.messages-display{flex-grow:1;overflow-y:auto;padding:.5rem 1rem;min-height:0;scrollbar-width:thin;scrollbar-color:#B0B0B0 var(--twitch-hover-bg)}.messages-display::-webkit-scrollbar{width:8px}.messages-display::-webkit-scrollbar-track{background:var(--twitch-hover-bg);border-radius:4px}.messages-display::-webkit-scrollbar-thumb{background:#b0b0b0;border-radius:4px}.messages-display::-webkit-scrollbar-thumb:hover{background:#888}.chat-line__message{display:flex;align-items:flex-start;padding:.3rem 0;line-height:1.5;word-wrap:break-word;color:var(--twitch-text-color);border-radius:4px}.chat-line__message:hover{background-color:#ffffff0d}.chat-line__message-container{display:inline}.chat-line__username{font-weight:700;display:inline-block;margin-right:.1rem;cursor:pointer}.chat-author__display-name{font-size:.9rem}.text-fragment{font-size:.9rem;color:var(--twitch-text-color)}.chat-line__message.user-sent-message{background-color:#9147ff1a;border-left:3px solid var(--twitch-purple);padding-left:.75rem}.chat-line__message.system-message{color:var(--twitch-secondary-text-color);font-style:italic;font-size:.85rem;justify-content:center}.chat-line__message.system-message .chat-line__username,.chat-line__message.system-message .text-fragment{font-size:inherit;color:inherit;font-weight:400}.chat-input-area{padding:.75rem 1rem;border-top:1px solid var(--twitch-border-color);flex-shrink:0}.chat-input-and-buttons{display:flex;flex-direction:column;gap:.5rem}.chat-input-textarea-container{display:flex;align-items:center;border:1px solid var(--twitch-border-color);border-radius:4px;background-color:var(--twitch-button-bg);padding:0 .5rem;transition:var(--fast-transition)}.chat-input-textarea-container:focus-within{border-color:var(--twitch-purple);box-shadow:0 0 0 2px #9147ff33}.chat-input-prefix-icons,.chat-input-suffix-icons{display:flex;align-items:center}.chat-input-wrapper{flex-grow:1}.chat-input-element{width:100%;height:2.25rem;padding:0 .5rem;border:none;background-color:transparent;color:var(--twitch-text-color);font-size:.95rem;outline:none}.chat-input-element::placeholder{color:var(--twitch-secondary-text-color)}.chat-input-buttons-container{display:flex;justify-content:space-between;align-items:center}.chat-buttons-left,.chat-buttons-right{display:flex;align-items:center;gap:.5rem}.chat-points-display{display:flex;align-items:center;gap:.25rem;font-size:.85rem;color:var(--twitch-secondary-text-color)}.chat-points-display svg,.chat-points-display .channel-points-icon{width:18px;height:18px;fill:currentColor;object-fit:contain}.chat-points-display .points-value{color:var(--twitch-text-color);font-weight:600}#send-button:disabled{background-color:#b0b0b0;cursor:not-allowed}.chat-toolbar-text-button.selected{background-color:var(--twitch-purple-light);border:1px solid var(--twitch-purple);box-shadow:0 0 5px var(--twitch-purple-light)}#highlight-message-overlay{position:absolute;left:41%;transform:translate(-50%) translateY(-200%);width:24vw;height:auto;z-index:100;transition:transform 1s}#highlight-message-overlay.is-visible{transform:translate(-50%) translateY(0);transition-timing-function:cubic-bezier(.25,1,.5,1)}.sc-background-image{width:100%;height:auto;display:block;border-radius:20px}.sc-content{position:absolute;top:0;left:0;width:100%;height:100%;z-index:2;color:#fff;font-family:Causten}.sc-user{position:absolute;top:20%;left:40%;transform:translate(-50%);font-size:2cqi;text-align:left}.sc-message{position:absolute;top:60%;left:50%;transform:translate(-50%,-50%);width:90%;font-size:2cqi;text-align:center;overflow-wrap:break-word}.settings-modal-container{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1000;display:flex;justify-content:center;align-items:center}.settings-modal-container.hidden{display:none}.modal-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#0009}.modal-content{position:relative;background-color:var(--twitch-chat-bg);color:var(--twitch-text-color);border-radius:8px;width:90%;max-width:450px;box-shadow:0 4px 20px #0003;display:flex;flex-direction:column}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.5rem;border-bottom:1px solid var(--twitch-border-color)}.modal-header h2{margin:0;font-size:1.25rem}.modal-body{padding:1.5rem;display:flex;flex-direction:column;gap:1.5rem}.setting-item{display:flex;flex-direction:column;gap:.5rem}.setting-item label{font-weight:600;font-size:.9rem}.modal-input{padding:.75rem;border:1px solid var(--twitch-border-color);border-radius:4px;background-color:var(--twitch-button-bg);color:var(--twitch-text-color);font-size:1rem}.modal-input:focus{outline:none;border-color:var(--twitch-purple);box-shadow:0 0 0 2px #9147ff33}.avatar-setting .avatar-preview-container{display:flex;align-items:center;gap:1rem}.avatar-preview{width:60px;height:60px;border-radius:50%;object-fit:cover;border:2px solid var(--twitch-border-color)}.avatar-upload-input{display:none}.modal-footer{padding:1rem 1.5rem;border-top:1px solid var(--twitch-border-color);display:flex;justify-content:flex-end}.setting-description{font-size:.8rem;color:var(--twitch-secondary-text-color);margin:4px 0 0}.turbo-button-icon{display:none}.turbo-button-full{display:inline-flex}@media (max-width: 992px){.turbo-button-full{display:none}.turbo-button-icon{display:inline-flex}}@media (max-width: 767px){body,#main-content-wrapper{flex-direction:column}#stream-and-info-container{width:100%}#chat-sidebar{width:100%;height:40vh;min-height:200px;min-width:unset;border-left:none;border-top:1px solid var(--twitch-border-color)}#chat-sidebar.collapsed{height:0;min-height:0;overflow:hidden;visibility:hidden;pointer-events:none}.nav-links-main,button[aria-label=Prime],button[aria-label=打开通知],button[aria-label=悄悄话],button[aria-label=购买呼币],button[aria-label=免费体验无广告观赏],button#show-chat-button,button[aria-label=通知],button.twitch-button[aria-label=购买呼币],button.twitch-button[aria-label=赠送一次订阅],.stream-details-right,.chat-sidebar-header{display:none!important}*{scrollbar-width:none!important}*::-webkit-scrollbar{display:none!important}*::-webkit-scrollbar-thumb{display:none!important}*::-webkit-scrollbar-track{display:none!important}#streamer-avatar{width:50px;height:50px}}.tooltip{position:absolute;background:#18181b;color:#fff;padding:6px 10px;font-size:12px;border-radius:4px;pointer-events:none;opacity:0;transition:opacity .2s ease;white-space:nowrap;z-index:1000;transform-origin:center bottom;-webkit-user-select:none;user-select:none}.tooltip.show{opacity:1}.tooltip:after{content:"";position:absolute;left:50%;transform:translate(-50%);border-width:6px;border-style:solid;top:100%;border-color:#18181b transparent transparent transparent;transition:border-color .2s ease,top .2s ease}.tooltip.tooltip-arrow-up:after{top:-12px;border-color:transparent transparent #18181b transparent}.hidden{display:none!important}#offline-content-container{display:flex;flex-direction:row;align-items:center;justify-content:center;flex-wrap:wrap;width:100%;min-height:500px;flex-grow:1;background-image:url(/banner.jpeg);background-size:cover;background-position:center;background-repeat:no-repeat;padding:2rem;gap:2rem;box-sizing:border-box}.offline-info-card{display:flex;flex-direction:column;align-items:flex-start;background-color:#fff;padding:1.5rem;border-radius:0;color:#0e0e10;box-shadow:0 4px 10px #0000001a;box-sizing:border-box;flex:0 1 270px}.offline-status-section{display:flex;flex-direction:column;justify-content:center;align-items:flex-start;flex-grow:1;gap:.75rem}.offline-status-badge{background-color:#000;color:#fff;font-size:.9em;font-weight:600;padding:2px 6px;border-radius:4px;white-space:nowrap;text-transform:uppercase}.offline-status-title{font-size:1.6rem;font-weight:600;margin:0}.offline-notification-section{width:100%;margin-left:-10px}.offline-notification-button{background-color:transparent;color:var(--twitch-primary-button-hover-bg);justify-content:flex-start}.offline-notification-button:hover{background-color:var(--twitch-button-hover-bg);color:var(--twitch-button-text-color)}.offline-notification-button svg{fill:currentColor}.offline-video-player{aspect-ratio:16 / 9;flex:0 1 576px;min-width:300px;max-width:576px}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/inter-cyrillic-ext-400-normal-Dc4VJyIJ.woff2) format("woff2"),url(/assets/inter-cyrillic-ext-400-normal-BE2fNs0E.woff) format("woff");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/inter-cyrillic-400-normal-BLGc9T1a.woff2) format("woff2"),url(/assets/inter-cyrillic-400-normal-alAqRL36.woff) format("woff");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/inter-greek-ext-400-normal-Bput3-QP.woff2) format("woff2"),url(/assets/inter-greek-ext-400-normal-XIH6-K3k.woff) format("woff");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/inter-greek-400-normal-DxZsaF_h.woff2) format("woff2"),url(/assets/inter-greek-400-normal-C3I71FoW.woff) format("woff");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/inter-vietnamese-400-normal-DMkecbls.woff2) format("woff2"),url(/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff) format("woff");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/inter-latin-ext-400-normal-C1nco2VV.woff2) format("woff2"),url(/assets/inter-latin-ext-400-normal-77YHD8bZ.woff) format("woff");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/inter-latin-400-normal-C38fXH4l.woff2) format("woff2"),url(/assets/inter-latin-400-normal-CyCys3Eg.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@@ -0,0 +1,7 @@
1
+ var U=Object.defineProperty;var N=(a,e,t)=>e in a?U(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t;var o=(a,e,t)=>N(a,typeof e!="symbol"?e+"":e,t);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))s(n);new MutationObserver(n=>{for(const i of n)if(i.type==="childList")for(const r of i.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&s(r)}).observe(document,{childList:!0,subtree:!0});function t(n){const i={};return n.integrity&&(i.integrity=n.integrity),n.referrerPolicy&&(i.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?i.credentials="include":n.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(n){if(n.ep)return;n.ep=!0;const i=t(n);fetch(n.href,i)}})();window.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll("[data-tooltip]").forEach(e=>{const t=document.createElement("div");t.className="tooltip",t.textContent=e.getAttribute("data-tooltip")||"",document.body.appendChild(t);function s(){const i=e.getBoundingClientRect(),r=window.pageXOffset,u=window.pageYOffset,m=50;let h,O;if(i.top<m)h=i.bottom+u+8,O="translateX(-50%)",t.classList.add("tooltip-arrow-up"),t.classList.remove("tooltip-arrow-down");else{t.style.left=i.left+i.width/2+r+"px",t.style.top=i.top+u+"px",t.style.transform="translateX(-50%)",t.classList.add("show");const R=t.offsetHeight;h=i.top+u-R-8,t.classList.remove("show"),t.style.top=h+"px",t.classList.add("tooltip-arrow-down"),t.classList.remove("tooltip-arrow-up")}t.style.left=i.left+i.width/2+r+"px",t.style.top=h+"px",t.style.transform=O,t.classList.add("show")}function n(){t.classList.remove("show")}e.addEventListener("mouseenter",s),e.addEventListener("mouseleave",n)})});window.addEventListener("DOMContentLoaded",()=>{const a="https://twitchtracker.com/api/channels/summary/vedal987",e=document.getElementById("avg-viewers");let t=0;const s=1;let n=null;async function i(){try{const r=new AbortController,u=setTimeout(()=>r.abort(),3e3),m=await fetch(a,{signal:r.signal});if(clearTimeout(u),!m.ok)throw new Error(`HTTP error! status: ${m.status}`);const h=await m.json();if(h&&typeof h.avg_viewers=="number")e.textContent=h.avg_viewers.toLocaleString(),t=0,n&&clearTimeout(n),n=setTimeout(i,5*60*60*1e3);else throw new Error("接口数据格式异常")}catch(r){console.warn("获取 avg_viewers 失败:",r.message),t++,t>s?(console.log("停止获取,1分钟后重试..."),setTimeout(()=>{t=0,i()},60*1e3)):i()}}i()});const p=document.getElementById("chat-messages"),y=document.getElementById("twitch-chat-overlay"),g=document.getElementById("highlight-message-overlay"),x="One_of_Swarm";class F{constructor(){p?console.log("ChatDisplay initialized."):console.error("ChatDisplay: Required chat messages container not found in DOM!")}appendChatMessage(e){if(!p){console.error("ChatDisplay: Cannot append message, container not found.");return}const t=document.createElement("div");t.className="chat-line__message";const s=document.createElement("div");s.className="chat-line__message-container",e.username===x&&e.is_user_message?t.classList.add("user-sent-message"):e.username==="System"?t.classList.add("system-message"):t.classList.add("audience-ai-message");const n=document.createElement("span");n.className="chat-line__username",n.style.color=e.username===x?"#9147FF":this.getRandomChatColor();const i=document.createElement("span");i.className="chat-author__display-name",i.textContent=e.username,n.appendChild(i);const r=document.createElement("span");r.textContent=": ",r.style.marginRight="0.3rem",r.className="text-fragment";const u=document.createElement("span");if(u.className="text-fragment",u.textContent=e.text,s.appendChild(n),s.appendChild(r),s.appendChild(u),t.appendChild(s),p.appendChild(t),y){const m=t.cloneNode(!0);y.appendChild(m),this.scrollToBottomOverlay()}this.scrollToBottom()}clearChat(){p&&(p.innerHTML="",console.log("Chat display cleared.")),y&&(y.innerHTML="")}scrollToBottom(){p&&(p.scrollTop=p.scrollHeight)}scrollToBottomOverlay(){y&&(y.scrollTop=y.scrollHeight)}getRandomChatColor(){const e=["#FF0000","#00FF00","#0000FF","#00FFFF","#FF00FF","#FF4500","#ADFF2F","#1E90FF","#FFD700","#8A2BE2","#00CED1","#FF69B4","#DA70D6","#BA55D3","#87CEEB","#32CD32","#CD853F"];return e[Math.floor(Math.random()*e.length)]}}function $(a,e,t){if(!g)return;const s=t==="bits"?"/sc_purple.png":"/sc_pink.png";g.innerHTML=`
2
+ <img src="${s}" class="sc-background-image" alt="Super Chat background">
3
+ <div class="sc-content">
4
+ <div class="sc-user">${a}</div>
5
+ <div class="sc-message">${e}</div>
6
+ </div>
7
+ `;const n=g.querySelector(".sc-message");n&&(n.style.color=t==="bits"?"var(--sc-purple-bg-color)":"var(--sc-pink-bg-color)"),g.classList.remove("hidden"),g.offsetHeight,g.classList.add("is-visible"),setTimeout(()=>{g.classList.remove("is-visible"),setTimeout(()=>{g.classList.add("hidden")},1e3)},9e3)}class W{constructor(e){o(this,"ws",null);o(this,"options");o(this,"reconnectAttempts",0);o(this,"reconnectTimeout",null);o(this,"explicitlyClosed",!1);this.options={reconnectInterval:3e3,maxReconnectAttempts:10,...e}}connect(){if(!this.options.url){console.warn("WebSocket URL is not set. Connection aborted.");return}if(this.ws&&(this.ws.readyState===WebSocket.OPEN||this.ws.readyState===WebSocket.CONNECTING)){console.warn(`WebSocket for ${this.options.url} is already connected or connecting.`);return}this.explicitlyClosed=!1,console.log(`Connecting to WebSocket: ${this.options.url}`),this.ws=new WebSocket(this.options.url),this.ws.onopen=()=>{var e,t;console.log(`WebSocket connected: ${this.options.url}`),this.reconnectAttempts=0,this.reconnectTimeout&&(clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null),(t=(e=this.options).onOpen)==null||t.call(e)},this.ws.onmessage=e=>{var t,s;try{const n=JSON.parse(e.data);n.type==="processing_superchat"&&$(n.data.username,n.data.text,n.data.sc_type),(s=(t=this.options).onMessage)==null||s.call(t,n)}catch(n){console.error(`Error parsing message from ${this.options.url}:`,n,e.data)}},this.ws.onclose=e=>{var t,s,n,i;console.warn(`WebSocket closed: ${this.options.url}. Code: ${e.code}, Reason: ${e.reason}`),this.ws=null,(s=(t=this.options).onClose)==null||s.call(t,e),this.explicitlyClosed||((i=(n=this.options).onDisconnect)==null||i.call(n),this.options.autoReconnect&&e.code!==1e3&&this.tryReconnect())},this.ws.onerror=e=>{var t,s;console.error(`WebSocket error: ${this.options.url}`,e),(s=(t=this.options).onError)==null||s.call(t,e)}}tryReconnect(){if(this.options.maxReconnectAttempts===-1||this.reconnectAttempts<this.options.maxReconnectAttempts){this.reconnectAttempts++;const t=this.options.maxReconnectAttempts===-1?`(attempt ${this.reconnectAttempts})`:`(attempt ${this.reconnectAttempts}/${this.options.maxReconnectAttempts})`;console.log(`Attempting to reconnect to ${this.options.url} in ${this.options.reconnectInterval/1e3} seconds... ${t}`),this.reconnectTimeout=setTimeout(()=>{this.connect()},this.options.reconnectInterval)}else console.error(`Max reconnect attempts reached for ${this.options.url}.`)}send(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(JSON.stringify(e)):console.warn(`WebSocket for ${this.options.url} is not open. Message not sent.`)}disconnect(){this.explicitlyClosed=!0,this.reconnectTimeout&&clearTimeout(this.reconnectTimeout),this.ws&&this.ws.close(1e3,"Client initiated disconnect")}updateOptions(e){console.log("WebSocket client options updated:",e),this.options={...this.options,...e}}getUrl(){return this.options.url}}const c=document.getElementById("neuro-caption"),T=document.getElementById("stream-display-area"),z=16,q=2,B=.4;function M(){if(!c||!T)return;c.style.fontSize="";const a=T.offsetHeight;if(c.textContent===""||a===0)return;if(c.offsetHeight>a*B){const t=window.getComputedStyle(c);let s=parseFloat(t.fontSize);for(;c.offsetHeight>a*B&&s>z;)s-=q,c.style.fontSize=`${s}px`;c.offsetHeight>a*B&&(c.style.fontSize=`${z}px`)}}let I=null,k=null;const H=3e3;function D(a,e){if(c)if(c.classList.add("show"),e&&a.trim().length>0){const t=a.split(/\s+/).filter(r=>r.length>0);if(t.length===0){c.textContent+=(c.textContent?" ":"")+a,M();return}const s=a.length;let n=c.textContent||"";const i=r=>{if(r<t.length){const u=t[r],m=u.length/s*e*1.01,h=Math.max(50,m*1e3);n+=(n.length>0?" ":"")+u,c.textContent=n,M(),I=setTimeout(()=>i(r+1),h)}else I=null};i(0),console.log(`Starting word-by-word caption for: "${a.substring(0,30)}..." (duration: ${e}s)`)}else c.textContent+=(c.textContent?" ":"")+a,M(),console.log(`Displaying full caption: "${a.substring(0,30)}..."`)}function A(){c&&(I&&(clearTimeout(I),I=null),k&&(clearTimeout(k),k=null),c.classList.remove("show"),c.textContent="",c.style.fontSize="",console.log("NeuroCaption hidden and cleared."))}function V(){k&&clearTimeout(k),k=setTimeout(()=>{A()},H)}!c||!T?console.error("neuroCaption.ts: Could not find #neuro-caption or #stream-display-area element."):console.log("NeuroCaption module initialized.");class j{constructor(){o(this,"audioQueue",[]);o(this,"isPlayingAudio",!1);o(this,"currentPlayingAudio",null);o(this,"allSegmentsReceived",!1);o(this,"errorSound");o(this,"lastSegmentEnd",!0);this.errorSound=new Audio("/error.mp3"),console.log("AudioPlayer initialized.")}playErrorSound(){this.stopAllAudio(),console.log("Playing dedicated error sound..."),this.errorSound.play().catch(e=>{console.error("Error playing dedicated error sound:",e)})}addAudioSegment(e,t,s){this.lastSegmentEnd&&A(),this.lastSegmentEnd=!1;const n=new Audio("data:audio/mp3;base64,"+t);try{const r=f.getAppInitializer().getMuteButton();n.muted=r.getIsMuted()}catch(i){console.warn("Could not get mute state, defaulting to muted:",i),n.muted=!0}this.audioQueue.push({text:e,audio:n,duration:s}),console.log(`Audio segment added to queue. Queue size: ${this.audioQueue.length}`),this.isPlayingAudio||this.playNextAudioSegment()}playNextAudioSegment(){if(this.audioQueue.length>0&&!this.isPlayingAudio){this.isPlayingAudio=!0;const e=this.audioQueue.shift();this.currentPlayingAudio=e.audio,D(e.text,e.duration),this.currentPlayingAudio.play().catch(t=>{console.error("Error playing audio segment:",t),this.isPlayingAudio=!1,this.currentPlayingAudio=null,this.playNextAudioSegment()}),this.currentPlayingAudio.addEventListener("ended",()=>{this.isPlayingAudio=!1,this.currentPlayingAudio=null,this.playNextAudioSegment()},{once:!0})}else if(this.audioQueue.length===0&&this.allSegmentsReceived){console.log("Neuro's full audio response played. Starting caption timeout and resetting zoom."),V();try{f.getAppInitializer().getNeuroAvatar().resetZoom()}catch(e){console.warn("Could not reset neuro avatar zoom at end of speech",e)}}}setAllSegmentsReceived(){this.allSegmentsReceived=!0,this.lastSegmentEnd=!0}stopAllAudio(){this.currentPlayingAudio&&(this.currentPlayingAudio.pause(),this.currentPlayingAudio.currentTime=0,this.currentPlayingAudio=null),this.audioQueue.length=0,this.isPlayingAudio=!1,this.allSegmentsReceived=!1,A();try{f.getAppInitializer().getNeuroAvatar().resetZoom()}catch(e){console.warn("Could not reset neuro avatar zoom on stop",e)}console.log("Neuro audio playback stopped, queue cleared.")}updateMuteState(){if(this.currentPlayingAudio)try{const t=f.getAppInitializer().getMuteButton();this.currentPlayingAudio.muted=t.getIsMuted()}catch(e){console.warn("Could not update current audio mute state:",e)}for(const e of this.audioQueue)try{const s=f.getAppInitializer().getMuteButton();e.audio.muted=s.getIsMuted()}catch(t){console.warn("Could not update queued audio mute state:",t)}}}const b=document.getElementById("startup-video-overlay"),d=document.getElementById("startup-video");class Q{constructor(){!b||!d?console.error("VideoPlayer: Required video elements not found in DOM!"):console.log("VideoPlayer initialized.")}showAndPlayVideo(e=0){if(!b||!d){console.error("VideoPlayer: Cannot show and play video, elements are missing.");return}b.classList.remove("hidden"),b.style.zIndex="10";const s=f.getAppInitializer().getMuteButton();d.muted=!1;const n=d.play();n!==void 0&&n.then(()=>{console.log("Unmuted autoplay successful."),this.seekTo(e)}).catch(i=>{console.warn("Unmuted autoplay failed. Showing unmute prompt and falling back to muted playback.",i),s.show(),d.muted=!0,d.play().then(()=>{console.log("Muted fallback playback started."),this.seekTo(e)}).catch(u=>{console.error("Muted fallback playback also failed. This is unexpected.",u)})})}seekTo(e){if(!d)return;const t=()=>{isFinite(d.duration)&&e>.1&&e<d.duration&&(d.currentTime=e,console.log(`Seek performed. Target: ${e.toFixed(2)}s.`))};d.readyState>=1?t():(console.log("Video metadata not ready, waiting for 'loadedmetadata' event to seek."),d.addEventListener("loadedmetadata",t,{once:!0}))}pauseAndMute(){d&&(d.pause(),d.muted=!0,console.log("Startup video paused and muted."))}hide(){b&&(b.classList.add("hidden"),console.log("Startup video overlay hidden."))}getVideoDuration(){return d&&!isNaN(d.duration)?d.duration:0}}const v={HIDDEN:"hidden",STEP1:"step1",STEP2:"step2"},l=document.getElementById("neuro-static-avatar-container");class J{constructor(){l?(console.log("NeuroAvatar initialized."),this.setStage(v.HIDDEN,!0)):console.error("NeuroAvatar: Required avatar container element not found in DOM!")}startIntroAnimation(e){console.log("Starting Neuro intro animation sequence..."),this.setStage(v.STEP1),setTimeout(()=>{console.log("Animating to Step 2..."),this.setStage(v.STEP2),setTimeout(()=>{console.log("Neuro intro animation sequence finished."),e&&e()},1e3)},2e3)}setStage(e,t=!1){if(!l)return;const s="transform 0.5s ease-in-out";switch(t?(l.style.transition="none",l.offsetHeight):e===v.STEP2?l.style.transition=`bottom 1s cubic-bezier(0.4, 0.0, 1, 1), ${s}`:l.style.transition=s,e){case v.HIDDEN:l.style.visibility="hidden",l.style.bottom="-207%",l.style.left="70%",l.style.zIndex="10";break;case v.STEP1:l.style.visibility="visible",l.style.bottom="-207%",l.style.left="70%",l.style.zIndex="15";break;case v.STEP2:l.style.visibility="visible",l.style.bottom="-125%",l.style.left="70%",l.style.zIndex="15";break}}triggerSpin(){l&&(console.log("Triggering avatar spin animation."),l.classList.add("spin-animation"),setTimeout(()=>{l.classList.remove("spin-animation"),console.log("Avatar spin animation finished.")},1e3))}triggerZoom(){l&&(console.log("Triggering avatar zoom-in animation."),l.classList.add("zoom-in"))}resetZoom(){l&&l.classList.contains("zoom-in")&&(console.log("Resetting avatar zoom."),l.classList.remove("zoom-in"))}}const C=document.getElementById("chat-input"),P=document.getElementById("send-button"),w=document.getElementById("sc-bits-button"),E=document.getElementById("sc-points-button");class X{constructor(){o(this,"onSendMessageCallback",null);!C||!P||!w||!E?console.error("UserInput: Required input elements not found in DOM!"):(this.setupEventListeners(),console.log("UserInput initialized."))}onSendMessage(e){this.onSendMessageCallback=e}setupEventListeners(){P.addEventListener("click",()=>this.handleSendMessage()),C.addEventListener("keypress",e=>{e.key==="Enter"&&this.handleSendMessage()}),w.addEventListener("click",()=>this.toggleSuperchatButton(w)),E.addEventListener("click",()=>this.toggleSuperchatButton(E))}toggleSuperchatButton(e){const t=e===w?E:w;e.classList.contains("selected")?e.classList.remove("selected"):(e.classList.add("selected"),t.classList.remove("selected"))}handleSendMessage(){const e=C.value.trim();if(!e){console.warn("Attempted to send empty message.");return}let t;const s=w.classList.contains("selected"),n=E.classList.contains("selected");s||n?t={type:"superchat",text:e,sc_type:s?"bits":"points"}:t={type:"user_message",text:e},this.onSendMessageCallback?this.onSendMessageCallback(t):console.warn("No callback registered for sending message."),C.value="",this.clearSuperchatSelection()}clearSuperchatSelection(){w.classList.remove("selected"),E.classList.remove("selected")}setInputDisabled(e){C.disabled=e,P.disabled=e,console.log(`User input elements disabled: ${e}`)}}class Z{constructor(){o(this,"viewportElement");o(this,"areaElement");o(this,"resizeObserver");if(this.viewportElement=document.getElementById("stream-display-viewport"),this.areaElement=document.getElementById("stream-display-area"),!this.viewportElement||!this.areaElement)throw new Error("LayoutManager: Required viewport or area element not found in DOM!");this.resizeObserver=new ResizeObserver(this.handleResize.bind(this))}handleResize(e){for(const t of e){const{width:s,height:n}=t.contentRect;this.updateLayout(s,n)}}updateLayout(e,t){if(e===0||t===0)return;const s=e/t,n=16/9;let i,r;s>n?(r=t,i=r*n):(i=e,r=i/n),this.areaElement.style.width=`${i}px`,this.areaElement.style.height=`${r}px`}start(){this.resizeObserver.observe(this.viewportElement),this.updateLayout(this.viewportElement.clientWidth,this.viewportElement.clientHeight),console.log("LayoutManager started and observing.")}stop(){this.resizeObserver.disconnect(),console.log("LayoutManager stopped.")}}class G{constructor(){o(this,"timerElement");o(this,"intervalId",null);o(this,"streamStartTime",0);if(this.timerElement=document.getElementById("stream-duration-text"),!this.timerElement)throw new Error("StreamTimer: Duration element '#stream-duration-text' not found!");this.reset()}formatTime(e){const t=Math.floor(e/3600),s=Math.floor(e%3600/60),n=Math.floor(e%60),i=r=>String(r).padStart(2,"0");return t>0?`${t}:${i(s)}:${i(n)}`:`${s}:${i(n)}`}updateDisplay(){if(this.streamStartTime>0){const t=(Date.now()-this.streamStartTime)/1e3;this.timerElement.textContent=this.formatTime(Math.max(0,t))}}start(e=0){this.stop(),this.streamStartTime=Date.now()-e*1e3,this.updateDisplay(),this.intervalId=window.setInterval(()=>this.updateDisplay(),1e3),console.log(`Stream timer started with initial ${e.toFixed(2)}s.`)}stop(){this.intervalId!==null&&(clearInterval(this.intervalId),this.intervalId=null,console.log("Stream timer stopped."))}reset(){this.stop(),this.streamStartTime=0,this.timerElement.textContent="0:00",console.log("Stream timer reset.")}}class Y{constructor(){o(this,"sidebarElement");o(this,"toggleButton");o(this,"showChatButton");o(this,"isCollapsed",!1);o(this,"bodyElement");if(this.sidebarElement=document.getElementById("chat-sidebar"),this.toggleButton=document.getElementById("toggle-chat-button"),this.showChatButton=document.getElementById("show-chat-button"),this.bodyElement=document.body,!this.sidebarElement||!this.toggleButton||!this.showChatButton)throw new Error("ChatSidebar: Required elements not found in DOM!");this.setupEventListeners(),this.setCollapsed(!1,!0),console.log("ChatSidebar initialized.")}setupEventListeners(){this.toggleButton.addEventListener("click",()=>this.toggleCollapse()),this.showChatButton.addEventListener("click",()=>this.toggleCollapse())}toggleCollapse(){this.setCollapsed(!this.isCollapsed)}setCollapsed(e,t=!1){this.isCollapsed=e,this.isCollapsed?this.bodyElement.classList.add("chat-collapsed"):this.bodyElement.classList.remove("chat-collapsed"),t?(this.sidebarElement.style.transition="none",this.toggleButton.style.transition="none",this.showChatButton.style.transition="none"):(this.sidebarElement.style.transition="width 0.3s ease-in-out, min-width 0.3s ease-in-out",this.toggleButton.style.transition="transform 0.3s ease-in-out, background-color 0.2s, color 0.2s",this.showChatButton.style.transition="opacity 0.3s ease-in-out, visibility 0.3s ease-in-out"),this.isCollapsed?(this.sidebarElement.classList.add("collapsed"),this.toggleButton.setAttribute("aria-label","展开聊天"),this.sidebarElement.querySelectorAll(":scope > *:not(.chat-sidebar-header)").forEach(s=>{s.style.opacity="0",s.style.pointerEvents="none"}),console.log("Chat sidebar collapsed.")):(this.sidebarElement.classList.remove("collapsed"),this.toggleButton.setAttribute("aria-label","重叠聊天"),this.sidebarElement.querySelectorAll(":scope > *:not(.chat-sidebar-header)").forEach(s=>{s.style.opacity="1",s.style.pointerEvents="auto"}),console.log("Chat sidebar expanded.")),t&&requestAnimationFrame(()=>{requestAnimationFrame(()=>{this.sidebarElement.style.transition="",this.toggleButton.style.transition="",this.showChatButton.style.transition=""})})}getIsCollapsed(){return this.isCollapsed}}class K{constructor(){o(this,"indicatorElement");if(this.indicatorElement=document.querySelector(".live-indicator-rect"),!this.indicatorElement)throw new Error("LiveIndicator: Required .live-indicator-rect element not found in DOM!");this.hide()}show(){this.indicatorElement.classList.remove("hidden")}hide(){this.indicatorElement.classList.add("hidden")}}class ee{constructor(){o(this,"nicknameElement");o(this,"titleElement");o(this,"categoryElement");o(this,"tagsContainer");if(this.nicknameElement=document.getElementById("streamer-nickname"),this.titleElement=document.getElementById("stream-title-full"),this.categoryElement=document.querySelector(".stream-category"),this.tagsContainer=document.querySelector(".stream-tags"),!this.nicknameElement||!this.titleElement||!this.categoryElement||!this.tagsContainer)throw new Error("StreamInfoDisplay: One or more required elements not found in DOM!");console.log("StreamInfoDisplay initialized.")}update(e){this.nicknameElement.textContent=e.streamer_nickname,this.titleElement.textContent=e.stream_title,this.categoryElement.textContent=e.stream_category,this.tagsContainer.innerHTML="",e.stream_tags.forEach(t=>{const s=document.createElement("a");s.href="#",s.className="stream-tag",s.textContent=t,this.tagsContainer.appendChild(s)}),console.log("Stream info display updated with new metadata.")}}class te{constructor(){o(this,"wakeLockSentinel",null);o(this,"isSupported");this.isSupported="wakeLock"in navigator,this.isSupported?console.log("WakeLockManager initialized. API is supported."):console.warn("Wake Lock API is not supported in this browser. The device may go to sleep during playback.")}async requestWakeLock(){if(!(!this.isSupported||this.wakeLockSentinel))try{this.wakeLockSentinel=await navigator.wakeLock.request("screen"),this.wakeLockSentinel.addEventListener("release",()=>{console.log("Wake Lock was released by the browser."),this.wakeLockSentinel=null}),console.log("Wake Lock is active."),document.addEventListener("visibilitychange",this.handleVisibilityChange.bind(this))}catch(e){console.error(`Failed to acquire Wake Lock: ${e.name}, ${e.message}`),this.wakeLockSentinel=null}}async releaseWakeLock(){this.wakeLockSentinel&&(await this.wakeLockSentinel.release(),this.wakeLockSentinel=null,console.log("Wake Lock has been released.")),document.removeEventListener("visibilitychange",this.handleVisibilityChange.bind(this))}async handleVisibilityChange(){this.wakeLockSentinel===null&&document.visibilityState==="visible"&&(console.log("Page is visible again, re-acquiring Wake Lock..."),await this.requestWakeLock())}}class L{constructor(e){o(this,"modalContainer");o(this,"overlay");o(this,"closeButton");o(this,"saveButton");o(this,"usernameInput");o(this,"backendUrlInput");o(this,"avatarPreview");o(this,"avatarUploadInput");o(this,"avatarUploadButton");o(this,"reconnectAttemptsInput");o(this,"onSave");if(this.onSave=e,this.modalContainer=document.getElementById("settings-modal"),this.overlay=document.getElementById("settings-modal-overlay"),this.closeButton=document.getElementById("settings-close-button"),this.saveButton=document.getElementById("settings-save-button"),this.usernameInput=document.getElementById("username-setting-input"),this.backendUrlInput=document.getElementById("backend-url-input"),this.avatarPreview=document.getElementById("avatar-setting-preview"),this.avatarUploadInput=document.getElementById("avatar-setting-upload"),this.avatarUploadButton=document.getElementById("avatar-upload-button"),this.reconnectAttemptsInput=document.getElementById("reconnect-attempts-input"),!this.modalContainer)throw new Error("Settings modal container not found!");this.setupEventListeners(),console.log("SettingsModal initialized.")}setupEventListeners(){this.closeButton.addEventListener("click",()=>this.close()),this.overlay.addEventListener("click",()=>this.close()),this.saveButton.addEventListener("click",()=>this.handleSave()),this.avatarUploadButton.addEventListener("click",()=>this.avatarUploadInput.click()),this.avatarUploadInput.addEventListener("change",e=>this.handleAvatarUpload(e))}handleAvatarUpload(e){const t=e.target;if(t.files&&t.files[0]){const s=new FileReader;s.onload=n=>{var i;(i=n.target)!=null&&i.result&&(this.avatarPreview.src=n.target.result)},s.readAsDataURL(t.files[0])}}handleSave(){const e={username:this.usernameInput.value.trim()||"User",avatarDataUrl:this.avatarPreview.src,backendUrl:this.backendUrlInput.value.trim(),reconnectAttempts:parseInt(this.reconnectAttemptsInput.value,10)||-1};localStorage.setItem("neuro_settings",JSON.stringify(e)),this.onSave(e),this.close()}open(){this.loadSettings(),this.modalContainer.classList.remove("hidden")}close(){this.modalContainer.classList.add("hidden")}loadSettings(){const e=L.getSettings();this.usernameInput.value=e.username,this.avatarPreview.src=e.avatarDataUrl,this.backendUrlInput.value=e.backendUrl,this.reconnectAttemptsInput.value=String(e.reconnectAttempts)}static getSettings(){const e=localStorage.getItem("neuro_settings");if(e)try{return JSON.parse(e)}catch(t){console.error("Failed to parse settings from localStorage, returning defaults.",t)}return{username:"One_of_Swarm",avatarDataUrl:"/user_avatar.jpg",backendUrl:"ws://127.0.0.1:8000",reconnectAttempts:-1}}}class se{constructor(){o(this,"button",null);o(this,"isMuted",!0)}create(){return this.button=document.getElementById("mute-button"),this.button?this.button.addEventListener("click",e=>{e.stopPropagation(),this.unmute()}):console.error("Mute button element not found in DOM!"),this.button}show(){this.button&&(this.button.style.display="flex")}hide(){this.button&&(this.button.style.display="none")}unmute(){this.isMuted=!1,this.hide(),this.updateMediaElements()}updateMediaElements(){const e=document.getElementById("startup-video");e&&(e.muted=this.isMuted);try{f.getAppInitializer().getAudioPlayer().updateMuteState()}catch(t){console.warn("Could not update audio player mute state:",t)}}getElement(){return this.button}getIsMuted(){return this.isMuted}}async function ne(a,e={},t){return window.__TAURI_INTERNALS__.invoke(a,e,t)}const ie=typeof window<"u"&&window.__TAURI__!==void 0,oe="3546729368520811",ae="4281748";async function re(){var a,e;try{if(ie)return console.log("Running in Tauri, invoking 'get_latest_replay_video' command..."),await ne("get_latest_replay_video");{console.log("Running in Web, fetching from proxy...");const t=`/bilibili-api/x/series/archives?mid=${oe}&series_id=${ae}&ps=1&pn=1`,s=await fetch(t);if(!s.ok)throw new Error(`Failed to fetch from proxy: ${s.statusText}`);const n=await s.json();if(n.code!==0)throw new Error(`Bilibili API error: ${n.message}`);const i=(e=(a=n==null?void 0:n.data)==null?void 0:a.archives)==null?void 0:e[0];if(i&&i.bvid&&i.aid)return i;throw new Error("No matching video found in API response.")}}catch(t){return console.error("Failed to get latest replay video:",t),null}}function le(a){const e="//www.bilibili.com/blackboard/html5mobileplayer.html",t=new URLSearchParams({bvid:a.bvid,aid:String(a.aid),p:"1",autoplay:"1",danmaku:"0",hasMuteButton:"1",hideCoverInfo:"0",fjw:"0",high_quality:"1"});return`${e}?${t.toString()}`}class ce{constructor(){o(this,"wsClient");o(this,"audioPlayer");o(this,"videoPlayer");o(this,"neuroAvatar");o(this,"chatDisplay");o(this,"userInput");o(this,"layoutManager");o(this,"streamTimer");o(this,"chatSidebar");o(this,"liveIndicator");o(this,"streamInfoDisplay");o(this,"wakeLockManager");o(this,"settingsModal");o(this,"currentSettings");o(this,"muteButton");o(this,"resizeObserver",null);o(this,"offlinePlayerSrc",null);o(this,"isStarted",!1);o(this,"currentPhase","offline");o(this,"adjustOfflineLayout",()=>{if(this.currentPhase!=="offline")return;const e=document.getElementById("offline-content-container"),t=document.querySelector(".offline-video-player"),s=document.querySelector(".offline-info-card");if(!e||!t||!s)return;if(window.innerWidth<=767)e.style.flexWrap="wrap",s.style.width=t.offsetWidth+"px",s.style.height="auto",s.style.flex="0 0 auto";else{e.style.flexWrap="nowrap";const i=t.offsetHeight;i>0&&(s.style.height=`${i}px`,s.style.width=`${i}px`),s.style.flex="0 1 auto"}});this.layoutManager=new Z,this.streamTimer=new G,this.muteButton=new se,this.currentSettings=L.getSettings(),this.settingsModal=new L(t=>this.handleSettingsUpdate(t)),this.wsClient=null,this.audioPlayer=new j,this.videoPlayer=new Q,this.neuroAvatar=new J,this.chatDisplay=new F,this.userInput=new X,this.userInput.onSendMessage(t=>this.sendUserMessage(t)),this.chatSidebar=new Y,this.liveIndicator=new K,this.streamInfoDisplay=new ee,this.wakeLockManager=new te,this.setupSettingsModalTrigger(),this.setupMuteButton();const e=document.querySelector(".offline-video-player");e&&(this.offlinePlayerSrc=e.src),this.updateOfflinePlayerSrc()}start(){this.isStarted||(this.isStarted=!0,this.probeForIntegratedServer(),this.layoutManager.start(),this.goOffline(),this.updateUiWithSettings())}setupSettingsModalTrigger(){const e=document.querySelector(".nav-user-avatar-button");e&&e.addEventListener("click",()=>{this.settingsModal.open()})}setupMuteButton(){if(this.muteButton.create()){this.muteButton.show();const t=()=>{this.muteButton.unmute(),document.removeEventListener("click",t)};document.addEventListener("click",t)}}getMuteButton(){return this.muteButton}getAudioPlayer(){return this.audioPlayer}getNeuroAvatar(){return this.neuroAvatar}handleSettingsUpdate(e){if(console.log("Settings updated. Re-initializing connection with new settings:",e),this.currentSettings=e,this.updateUiWithSettings(),this.wsClient){const t=e.backendUrl?`${e.backendUrl}/ws/stream`:"";this.wsClient.updateOptions({url:t,maxReconnectAttempts:e.reconnectAttempts}),this.wsClient.disconnect(),setTimeout(()=>{this.wsClient&&this.wsClient.getUrl()?this.wsClient.connect():console.warn("Cannot connect: Backend URL is empty after update or WebSocket client not ready.")},500)}else console.warn("WebSocket client not initialized, cannot update settings.")}initWebSocketClient(e){const t=e?`${e}/ws/stream`:"",s=n=>this.handleWebSocketMessage(n);this.wsClient?(this.wsClient.updateOptions({url:t,autoReconnect:!0,maxReconnectAttempts:this.currentSettings.reconnectAttempts,onMessage:s,onOpen:()=>this.goOnline(),onDisconnect:()=>this.goOffline()}),t&&(this.wsClient.disconnect(),setTimeout(()=>{this.wsClient.connect()},500))):this.wsClient=new W({url:t,autoReconnect:!0,maxReconnectAttempts:this.currentSettings.reconnectAttempts,onMessage:s,onOpen:()=>this.goOnline(),onDisconnect:()=>this.goOffline()})}async probeForIntegratedServer(){const e=L.getSettings();console.log("Probing for integrated server. Stored settings:",e);try{const t=new URL("/api/system/health",window.location.origin).toString();console.log("Probing integrated server at:",t);const s=await fetch(t);if(s.ok){console.log("Integrated server detected via health check. Auto-connecting...");const n=window.location.origin;this.currentSettings={...this.currentSettings,backendUrl:n},localStorage.setItem("neuro_settings",JSON.stringify(this.currentSettings)),this.initWebSocketClient(n),this.wsClient&&n&&this.wsClient.connect();return}else console.log("Health check failed, not an integrated server. Status:",s.status)}catch(t){console.log("Failed to probe for integrated server, assuming standalone mode.",t)}console.log("Falling back to stored backend URL:",e.backendUrl),this.initWebSocketClient(e.backendUrl),this.wsClient&&e.backendUrl?this.wsClient.connect():e.backendUrl||(console.warn("Backend URL is not configured via probe or stored settings. Opening settings modal."),this.settingsModal.open())}updateUiWithSettings(){document.querySelectorAll(".user-avatar-img").forEach(t=>t.src=this.currentSettings.avatarDataUrl),console.log(`UI updated with username: ${this.currentSettings.username} and avatar.`)}async updateOfflinePlayerSrc(){console.log("Attempting to fetch the latest replay video...");const e=await re();if(e){const t=le(e);if(this.offlinePlayerSrc=t,console.log("Successfully updated offline player src to:",t),this.currentPhase==="offline"){const s=document.querySelector(".offline-video-player");s&&(s.src=this.offlinePlayerSrc)}}else console.log("Failed to fetch latest replay video, using default fallback.")}goOnline(){var s,n,i,r;console.log("Entering ONLINE state."),this.updateUiWithSettings();const e=document.querySelector(".offline-video-player");e&&(e.src="about:blank"),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),window.removeEventListener("resize",this.adjustOfflineLayout);const t=document.querySelector(".offline-info-card");t&&(t.style.height="",t.style.width=""),(s=document.getElementById("offline-content-container"))==null||s.classList.add("hidden"),(n=document.getElementById("stream-display-viewport"))==null||n.classList.remove("hidden"),(i=document.querySelector(".stream-info-details-row"))==null||i.classList.remove("hidden"),(r=document.getElementById("chat-sidebar"))==null||r.classList.remove("hidden"),this.showStreamContent(),this.chatDisplay.clearChat(),this.liveIndicator.show(),this.wakeLockManager.requestWakeLock()}goOffline(){var s,n,i,r;console.log("Entering OFFLINE state."),this.currentPhase="offline";const e=document.querySelector(".offline-video-player");e&&this.offlinePlayerSrc&&(e.src=this.offlinePlayerSrc),(s=document.getElementById("offline-content-container"))==null||s.classList.remove("hidden"),(n=document.getElementById("stream-display-viewport"))==null||n.classList.add("hidden"),(i=document.querySelector(".stream-info-details-row"))==null||i.classList.add("hidden"),(r=document.getElementById("chat-sidebar"))==null||r.classList.add("hidden"),this.hideStreamContent(),this.audioPlayer.stopAllAudio(),this.videoPlayer.hide(),this.neuroAvatar.setStage("hidden",!0),A(),this.streamTimer.stop(),this.streamTimer.reset(),this.chatDisplay.clearChat(),this.liveIndicator.hide(),this.wakeLockManager.releaseWakeLock(),this.muteButton.show();const t=()=>{this.muteButton.unmute(),document.removeEventListener("click",t)};document.addEventListener("click",t),setTimeout(()=>{this.adjustOfflineLayout();const u=document.querySelector(".offline-video-player");u&&!this.resizeObserver&&(this.resizeObserver=new ResizeObserver(this.adjustOfflineLayout),this.resizeObserver.observe(u),window.addEventListener("resize",this.adjustOfflineLayout))},0)}handleWebSocketMessage(e){switch(this.currentPhase==="offline"&&["play_welcome_video","start_avatar_intro","enter_live_phase"].includes(e.type)&&(console.log("Connection successful, transitioning from OFFLINE to active state."),this.goOnline()),e.elapsed_time_sec!==void 0&&this.streamTimer.start(e.elapsed_time_sec),e.type){case"offline":this.goOffline();break;case"model_spin":this.neuroAvatar.triggerSpin();break;case"model_zoom":this.neuroAvatar.triggerZoom();break;case"update_stream_metadata":this.streamInfoDisplay.update(e);break;case"play_welcome_video":const t=parseFloat(e.progress);this.currentPhase!=="offline"?this.videoPlayer.seekTo(t):(this.currentPhase="initializing",this.videoPlayer.showAndPlayVideo(t));break;case"start_avatar_intro":this.currentPhase="avatar_intro",this.videoPlayer.pauseAndMute(),this.neuroAvatar.startIntroAnimation(()=>{this.videoPlayer.hide()});break;case"enter_live_phase":this.currentPhase="live",this.videoPlayer.hide(),this.neuroAvatar.setStage("step2");break;case"neuro_is_speaking":break;case"neuro_speech_segment":const s=e;s.is_end?this.audioPlayer.setAllSegmentsReceived():s.audio_base64&&s.text&&typeof s.duration=="number"?this.audioPlayer.addAudioSegment(s.text,s.audio_base64,s.duration):console.warn("Received neuro_speech_segment message with missing audio/text/duration:",s);break;case"neuro_error_signal":console.warn("Received neuro_error_signal from backend."),D("Someone tell Vedal there is a problem with my AI."),this.audioPlayer.playErrorSound();break;case"chat_message":(!this.chatSidebar.getIsCollapsed()||e.is_user_message)&&this.chatDisplay.appendChatMessage(e);break;case"error":this.chatDisplay.appendChatMessage({type:"chat_message",username:"System",text:`后端错误: ${e.message}`,is_user_message:!1});break}}sendUserMessage(e){const t={username:this.currentSettings.username,...e};this.wsClient?this.wsClient.send(t):console.warn("Cannot send message: WebSocket client is not initialized.")}showStreamContent(){const e=document.getElementById("stream-display-area");e&&(e.style.visibility="visible",e.style.opacity="1")}hideStreamContent(){const e=document.getElementById("stream-display-area");e&&(e.style.visibility="hidden",e.style.opacity="0")}}const S=class S{constructor(){o(this,"appInitializerInstance",null)}static getInstance(){return S.instance||(S.instance=new S),S.instance}getAppInitializer(){return this.appInitializerInstance?console.log("Returning existing AppInitializer instance."):(console.log("Creating new AppInitializer instance..."),this.appInitializerInstance=new ce),this.appInitializerInstance}};o(S,"instance");let _=S;const f=_.getInstance();document.addEventListener("DOMContentLoaded",()=>{console.log("DOMContentLoaded event fired."),f.getAppInitializer().start()});console.log("main.ts loaded. Waiting for DOMContentLoaded to initialize the app.");
@@ -2,11 +2,11 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/x-icon" href="./favicon.ico" />
5
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>𝐯𝐞𝐝𝐚𝐥𝟗𝟖𝟕 - 𝐓𝐰𝐢𝐭𝐜𝐡</title>
8
- <script type="module" crossorigin src="./assets/index-DRLWJPZv.js"></script>
9
- <link rel="stylesheet" crossorigin href="./assets/index-C_kzLmQy.css">
8
+ <script type="module" crossorigin src="/assets/index-vrP1Bqp5.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-rmyQl8Tr.css">
10
10
  </head>
11
11
  <body>
12
12
  <header id="twitch-header">
@@ -69,7 +69,7 @@
69
69
 
70
70
  <button class="nav-user-avatar-button" aria-expanded="false" aria-label="用户菜单">
71
71
  <div class="user-avatar-wrapper">
72
- <img class="user-avatar-img" alt="用户头像" src="./user_avatar.jpg" style="object-fit: cover;">
72
+ <img class="user-avatar-img" alt="用户头像" src="/user_avatar.jpg" style="object-fit: cover;">
73
73
  </div>
74
74
  </button>
75
75
  </div>
@@ -81,11 +81,11 @@
81
81
  <div id="stream-display-area">
82
82
  <div id="background-display" data-darkreader-ignore></div>
83
83
  <div id="neuro-static-avatar-container">
84
- <img id="neuro-static-avatar" src="./neurosama.png" alt="Neuro-Sama Static Avatar" />
84
+ <img id="neuro-static-avatar" src="/neurosama.png" alt="Neuro-Sama Static Avatar" />
85
85
  </div>
86
86
  <div id="neuro-caption"></div>
87
87
  <div id="startup-video-overlay">
88
- <video id="startup-video" src="./neuro_start.mp4" playsinline muted preload="auto"></video>
88
+ <video id="startup-video" src="/neuro_start.mp4" playsinline muted preload="auto"></video>
89
89
  </div>
90
90
  <!-- Twitch 风格聊天覆盖层 -->
91
91
  <div id="twitch-chat-overlay" class="messages-display">
@@ -124,7 +124,7 @@
124
124
  <div class="stream-info-left-column">
125
125
  <a href="https://twitch.tv/vedal987" class="streamer-avatar-link">
126
126
  <div class="streamer-avatar-wrapper">
127
- <img id="streamer-avatar" src="./avatar.webp" alt="Streamer Avatar" />
127
+ <img id="streamer-avatar" src="/avatar.webp" alt="Streamer Avatar" />
128
128
  <div class="live-indicator-wrapper">
129
129
  <div class="live-indicator-rect">
130
130
  <span class="live-text" id="live-indicator-text">LIVE</span>
@@ -226,7 +226,7 @@
226
226
  <div class="chat-input-textarea-container">
227
227
  <div class="chat-input-prefix-icons">
228
228
  <button class="chat-toolbar-button" aria-label="聊天室身份">
229
- <img alt="订阅者徽章" class="chat-badge-icon" src="./sub_badge.svg">
229
+ <img alt="订阅者徽章" class="chat-badge-icon" src="/sub_badge.svg">
230
230
  </button>
231
231
  </div>
232
232
  <div class="chat-input-wrapper">
@@ -248,7 +248,7 @@
248
248
  <span class="points-value">0</span>
249
249
  </button>
250
250
  <button id="sc-points-button" class="chat-toolbar-text-button" aria-label="频道点数">
251
- <img class="channel-points-icon" src="./channel_points.png" alt="频道点数">
251
+ <img class="channel-points-icon" src="/channel_points.png" alt="频道点数">
252
252
  <span class="points-value">0</span>
253
253
  </button>
254
254
  </div>
@@ -282,7 +282,7 @@
282
282
  <div class="setting-item avatar-setting">
283
283
  <label>头像</label>
284
284
  <div class="avatar-preview-container">
285
- <img id="avatar-setting-preview" src="./user_avatar.jpg" alt="Avatar Preview" class="avatar-preview">
285
+ <img id="avatar-setting-preview" src="/user_avatar.jpg" alt="Avatar Preview" class="avatar-preview">
286
286
  <input type="file" id="avatar-setting-upload" accept="image/*" class="avatar-upload-input">
287
287
  <button id="avatar-upload-button" class="twitch-button">上传图片</button>
288
288
  </div>
Binary file