Commit 1dad0aea by 程裕兵

Initial commit

parents

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

# Luna TTS 排行榜冲榜 — 代码级业务逻辑梳理
> 基于 `tts-ranking/` 代码实际实现,2026-07-09
## 一、系统架构
```
Chrome 扩展 (MV3)
├── content.js → 页面注入、音频捕获、投票模拟、状态管理
├── background.js → WebSocket 客户端、消息转发、Debugger API 原生输入
├── inject.js → fetch 拦截器(注入页面主世界)
└── popup/ → 状态面板(启动/停止/统计)
Python 服务
├── server.py → WebSocket 服务、音频下载、日志
├── strategy.py → 投票决策引擎
├── matcher.py → 声纹匹配引擎 (Resemblyzer)
└── config.py → 可配置参数
```
## 二、对照音频库
三个库,通过 subdirectory symlinks 组织:
| 库 | 目录 | 厂商数 | 用途 |
|---|------|--------|------|
| 我方 | `standard/` | 1 (VUI Labs) | 识别自己的音频,投自己 |
| 大厂 | `big_vendors/` | 4 (SpeechifyAI/Google/Cartesia/Alibaba) | 优先投票目标 |
| 竞品 | `competitors/` | 16 | 仅监控+兜底决策 |
每个 subdirectory 命名格式:`Vendor__Model`,内部包含该模型的多个 mp3 文件。
## 三、声纹匹配流程
1. **启动时加载**:遍历三个目录,对每个 mp3 文件:
- `preprocess_wav()` → 音频预处理(静音裁剪)
- `VoiceEncoder.embed_utterance()` → 256 维声纹向量
- 文件名提取 speaker 名(兼容新旧两种格式)
- 文件名/pitch 判断性别
- 同 speaker 多片段取最高分
2. **运行时匹配**(收到音频 URL):
- 服务端下载音频 → 提取声纹
- pitch 检测未知音频性别 → 只与同性别对照音频匹配
- 每个库返回:best speaker + best score + 全量分数 + vendor/model
- 三个库各自的最佳结果汇总为 SideResult
3. **设备自动检测**:CUDA → MPS → CPU
## 四、投票决策引擎 (`strategy.py`)
### 核心逻辑
对左右两侧音频,分别从三个库的全量匹配中取**跨库最高分**,确定该侧的 `top_library` 和 `top_speaker`。然后按优先级决策:
| 优先级 | 条件 | 决策 |
|--------|------|------|
| 1 | 某侧 top_library == "our" AND top_score >= 0.80 | 投该侧 |
| 1a | 两侧都是 ours | 投高分侧(平局投左) |
| 2 | 某侧 top_library == "big_vendor" AND top_score >= 0.80 | 投该侧 |
| 2a | 两侧都是 big_vendor | 随机投一侧 |
| 3a | 两侧 top_library == "competitor" AND >= 0.80 | **刷新页面**(弃权) |
| 3b | 一侧 competitor,另一侧不是 | **投非竞品那一侧** |
| 3c | 都不是 competitor | **随机投一侧** |
### 可配置参数 (`config.py`)
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `OUR_THRESHOLD` / 匹配阈值 | 0.80 | 所有库共用一个阈值 |
| `DAILY_VOTE_LIMIT` | 800 | 达到后停止 |
| `WS_PORT` | 8765 | WebSocket 端口 |
(注:`COMPETITOR_THRESHOLD` 和 `BIG_VENDOR_VOTE_PROBABILITY` 仍存在于 config.py 但 server.py 已不再引用)
## 五、扩展端工作流 (`content.js`)
### 5.1 初始化
- `document_start` 注入 `inject.js` 劫持 `window.fetch`,捕获 mp3/wav 请求 URL
- 自动恢复:`chrome.storage.local` 存 enabled 状态,页面刷新后 3 秒自动续投
### 5.2 单轮投票循环 (`doVote`)
```
1. 检测 CAPTCHA(最多 20s)
- 自动点击 Turnstile(Debugger API 原生点击)
- 超时 → 刷新页面
2. 捕获 2 个音频 URL(最多 60s)
- 优先读 fetch 拦截队列
- 兜底读 <audio> DOM
- 使用消费指针 _consumedUrls 区分新旧音频
3. 等投票卡片渲染(最多 30s)
4. grant_autoplay(Debugger API 原生点击获取用户激活)
5. 点击播放按钮 + audio.play()
6. 等音频播放(最少 3s,最多 8s)
7. 发送 match_request → 等待匹配结果
8. 根据决策执行投票:
- vote_left/right → simulateVote → 验证 → 标记完成
- skip → skipAndRefresh(等 20s 后刷新)
9. scheduleNextVote(2-8s 随机延迟)
```
### 5.3 投票模拟 (`simulateVote`)
- 主方式:Debugger API 发送原生 keydown/keyup(ArrowLeft/ArrowRight)
- 失败兜底:synthetic keyboard + card click(实际无效)
- 验证:等 5s,检查是否有新 URL 入队;无则重试一次等 5s;再失败标为 FAILED → doVote 刷新页面
### 5.4 Reveal 验证 (`_captureRevealAndVerify`)
- 投票前启动 200ms 轮询
- 页面揭示后抓到 `.animate-arena-upvote`(投票的模型名)和 `.animate-arena-downvote`(另一个模型名)
- 直接输出到浏览器控制台(带颜色标注 + 对比结果 ✓/✗)
### 5.5 工作/休息周期
- 工作 10 分钟 → 休息 60-180 秒(随机)→ 继续工作
### 5.6 反检测
- 聆听后投票(min 3s)
- 投票间隔 2-8s 随机
- 弃权刷新:前 9 次随机 1-3s,第 10 次冷却 10-20s(通过 storage 跨页面持久化计数)
- Debugger API 原生输入事件(isTrusted: true)
## 六、通信协议
WebSocket JSON(localhost:8765)
### match_request(扩展 → 服务端)
```json
{
"type": "match_request",
"request_id": "uuid",
"audio_left": "https://cdn.../audio_a.mp3",
"audio_right": "https://cdn.../audio_b.mp3"
}
```
### match_result(服务端 → 扩展)
```json
{
"type": "match_result",
"decision": "vote_left|vote_right|skip",
"reason": "决策原因",
"details": {
"left": { "our_match": ..., "competitor_match": ..., "big_vendor_match": ..., "top_match": {...} },
"right": { ... }
},
"stats": { "today_votes": 23, "today_skips": 7, "daily_limit": 800 }
}
```
### 控制消息
- `status` — 查询当日统计
- `grant_autoplay` — 请求原生点击
- `dispatch_key` — 请求原生键盘事件
- `click_at` — 请求原生鼠标点击
- `captcha_detected` — 通知 CAPTCHA
## 七、日志格式
每次匹配输出到终端 + `server/logs/match-YYYYMMDD.log`:
```
───────────────────────────────────────────────────────
LEFT 音频
我方最佳: Ella(0.7863)
Ella: 0.7863
Grace: 0.6211
竞品最佳: Lively Girl_us_female(0.8549)
Lively Girl_us_female: 0.8549 ← [ElevenLabs / Eleven_v3]
...
大厂最佳: Caroline_us_female(0.8592)
Caroline_us_female: 0.8592 ← [Cartesia / Sonic_3.5]
>>> left 最高: Caroline_us_female(0.8592) [big_vendor]
RIGHT 音频
...
决策: vote_left
原因: left 最高分为大厂 Caroline_us_female(0.8592) → vote
统计: 23 票 / 7 弃
───────────────────────────────────────────────────────
```
标注说明:`←` 表示达到阈值 0.80;`[厂商 / 模型]` 表示所属厂商和模型。
## 八、竞品模型清单(当前加载的)
**大厂 (4)**:SpeechifyAI/Simba_3.2、Google/Gemini_3.1_Flash_TTS、Cartesia/Sonic_3.5、Alibaba/Fun-Realtime-TTS
**竞品 (19)**:Inworld(3个模型)、SpaceXAI、MiniMax(4个模型)、async(2个模型)、StepFun(3个模型)、ElevenLabs、Smallest.ai、Fish Audio(2个模型)、Microsoft、SpeechifyAI/Simba_3.0(竞品版本)
# Luna TTS 排行榜冲榜工具
Native Messaging 方案,扩展直连本地 Python 进程,不走网络层。兼容代理环境(AdsPower / Quark / SunBrowser 等指纹浏览器)。
## Mac 安装
### 0. 前置条件
```bash
# 1. 确认 Python 3.8+
python3 --version
# 2. 安装 Xcode Command Line Tools(提供 gcc 编译器)
xcode-select --install
# 3. 安装 Python 依赖
cd 项目路径/tts-ranking
python3 -m pip install -r requirements.txt
```
### 1. 加载扩展
打开 Chrome(或指纹浏览器),地址栏输入 `chrome://extensions/` → 开「开发者模式」→ 「加载已解压的扩展程序」→ 选择 `extension/` 目录。
记住卡片上显示的 **扩展 ID**(一长串字母)。
### 2. 安装 Native Host
```bash
cd 项目路径/tts-ranking/server
./install_host.sh 刚才的扩展ID
```
脚本会自动编译 wrapper、安装到 Chrome/Chromium/Quark/Edge/AdsPower 所有浏览器目录。
> 如果报 `gcc: command not found`,先跑 `xcode-select --install`。
> 每次移除扩展再重新加载,ID 会变,需要重新跑这步。
### 3. 刷新扩展
`chrome://extensions/` → 点 Luna TTS Bot 的刷新 ⟳。
### 4. 验证
打开 `https://artificialanalysis.ai/text-to-speech/arena`,点扩展图标,应显示绿色 `Connected`。
---
## Windows 安装
### 1. 安装 Python 依赖
```powershell
cd C:\path\to\tts-ranking
python -m pip install -r requirements.txt
```
### 2. 编译 Native Host
需要 gcc(可用 MinGW)或 Visual Studio。
```powershell
cd server
gcc -o run_host.exe runner_win.c
```
### 3. 加载扩展
`chrome://extensions/` → 开发者模式 → 加载已解压 → 选 `extension/` 目录。
### 4. 注册 Native Host
创建 `install_host.reg`(用记事本,注意把路径改成实际项目路径):
```reg
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Google\Chrome\NativeMessagingHosts\com.luna.tts]
@="C:\\path\\to\\tts-ranking\\server\\run_host.exe"
```
双击导入注册表。
指纹浏览器(AdsPower/Quark)需要同时导入到对应注册表路径。
### 5. 刷新扩展并验证
刷新扩展 → 打开打榜页面 → 检查弹窗是否绿了。
---
## 使用
1. 打开 `https://artificialanalysis.ai/text-to-speech/arena`
2. 有人机校验就手动点一下
3. 点扩展图标 → **Start Voting**
4. 页面右下角绿色提示条显示当前状态
## 人工介入
两侧都匹配到我方,同时有竞品也匹配到时,系统暂停等待。按快捷键:
| 快捷键 | 操作 |
|--------|------|
| `Ctrl+Shift+L` | 强制投左 |
| `Ctrl+Shift+R` | 强制投右 |
| `Ctrl+Shift+S` | 跳过本轮 |
5 分钟无操作自动刷新页面。
## 日志
- 匹配日志:`server/logs/native-YYYYMMDD.log`
- 崩溃日志:`server/logs/crash.log`
- 扩展状态:`chrome://extensions/` → Luna TTS Bot → 点 service worker 链接 → Console
## 故障排查
### 扩展显示 Disconnected
1. 检查 `server/logs/crash.log`,看是否有 `ModuleNotFoundError`(依赖没装全)
2. 确认 `install_host.sh` 成功跑过,`com.luna.tts.json` 在各浏览器目录下都存在:
```bash
find ~/Library -name "com.luna.tts.json" 2>/dev/null
```
3. 确认 manifest 里的扩展 ID 和 `chrome://extensions/` 上的一致:
```bash
cat ~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts/com.luna.tts.json
```
4. 完全退出浏览器再重新打开,刷新扩展
### 稳定连接但报 `debugger blocked`
指纹浏览器可能限制了 Debugger API。CAPTCHA 自动点击和键盘投票会降级为备用方式,如果不是经常出现可以忽略。
### 连上又断开、反复循环
看 `server/logs/crash.log`——大概率是依赖问题或 Python 版本问题。确认:
```bash
python3 -c "import torch; import torchaudio; import resemblyzer; print('OK')"
```
### 跨芯片兼容(Apple Silicon ↔ Intel)
编译产物 `run_host` 绑定当前机器的芯片架构。发给别人用前,确认对方机器架构:
```bash
# 对方机器跑这个看芯片
uname -m
# arm64 = Apple Silicon (M1/M2/M3/M4)
# x86_64 = Intel Mac
```
如果不匹配(比如你在 M 芯片编译、发给 Intel 机器),对方会报 `bad CPU type in executable`。解决:让对方面重新编译:
```bash
cd 项目路径/tts-ranking/server
gcc -o run_host runner.c
```
### 提示需要手动点击人机验证
Cloudflare Turnstile PAT 挑战无法自动绕过,页面右下角会提示"人机验证,需要手动点击",点一下即可。
#!/usr/bin/env python3
"""Download remaining TTS models from Artificial Analysis (batch 2)."""
import subprocess
import json
import os
import time
import re
import sys
sys.stdout.reconfigure(line_buffering=True) if hasattr(sys.stdout, 'reconfigure') else None
BASE_URL = "https://artificialanalysis.ai/api/text-to-speech/speech-explorer"
OUTPUT_DIR = "/Users/chengyubing/Documents/projects/hzjj/tts-ranking/server/reference/raw_audios"
MODELS = [
("Speech-02-Turbo", "953aeced-7f15-4a02-a2f5-3c9cfccbbb39", "MiniMax"),
("Magpie Multilingual", "04719149-11ed-42b1-bc2c-166860c03c23", "Magpie"),
("Magpie-Multilingual 357M (Feb 2026)", "49198771-e5ea-4c4e-bad9-e57671a92291", "Magpie"),
("Coda", "cddf7ab5-43d6-488f-be03-1dcfbb8955d3", "Coda"),
("SSFM-v30", "b86c54d8-b51d-4186-b412-a705f2b9b76d", "SSFM"),
("Qwen3 TTS", "8eb42b0b-213a-4911-b096-a7455e944090", "Alibaba"),
("Seed-TTS 2.0", "31ef1e00-542b-4d06-8b0f-0bb52ef4e518", "ByteDance"),
("GPT-Realtime-2", "99365f44-415e-4ff2-94c9-59b8b2ec5847", "OpenAI"),
("Polly Standard", "d3f9b298-8e1a-4328-a2bd-ce3ceea6db1f", "Amazon"),
("SSFM-v21", "c8aa7fcf-69c0-4c7f-9ead-f640801a8069", "SSFM"),
("Voxtral TTS", "b424cd2e-abb9-4131-8f47-7731bdbff455", "Voxtral"),
("MiMo-V2.5-TTS", "abdcf494-6c73-4574-ade9-6672cdb21936", "MiMo"),
("Octave 2", "b4a9f921-ae13-4c12-b907-6e9c6bedaf58", "Octave"),
("Gemini 2.5 Flash TTS (Dec 2025)", "7b6ea07a-99e1-4635-9ea1-beee1b6520d5", "Google"),
("Arcana v3", "20c6b2a3-4881-4f5d-b82f-f135fab82711", "Arcana"),
("Higgs Audio V3 TTS", "49dc49c3-828e-4af5-8527-49606b4f6e14", "Higgs Audio"),
("Zonos-v0.1", "f5a3ab85-032b-41b1-8c78-c4b0eef2a9e8", "Zyphra"),
("Fun-Realtime-TTS-Preview", "48100868-7939-4cb3-a5be-8fb1202061ec", "Alibaba"),
("Standard", "7efc9790-17f6-44a5-8b4f-897deaeb0088", "MiniMax"),
("MetaVoice v1", "fb51ab93-d79a-49c5-a2f5-19f03e311afd", "Meta"),
("Gradium TTS", "2f3dbd41-a9c4-4f0c-a16b-427dea9feb6e", "Gradium"),
("MiMo-V2-TTS", "4489b587-5602-434a-9c24-772df19e879b", "MiMo"),
("MAI-Voice-1", "c8b0b07c-26a7-444c-a8ff-c412a3d433b7", "MAI"),
("XTTS v2", "f2f2989e-69d1-4b1c-a419-ee316e6c1c3d", "Coqui"),
("Speech 2.6", "6d2c7daa-7585-4b85-b1db-239c1c29b9a3", "MiniMax"),
("WaveNet", "16ad3e89-cc78-4478-a5a0-a048b5c72cfb", "Google"),
("Speech-02-HD", "65166a7b-b42c-4a68-b051-9662facd2f91", "MiniMax"),
("Qwen3 TTS Flash", "f05bc243-0723-4f05-aa19-31341d8f2677", "Alibaba"),
("Polly Neural", "eb3b0d75-134b-4e6a-82c5-4db2398d5c07", "Amazon"),
("Falcon (Beta)", "5ab04a8d-c698-4726-bebd-af5fc553b752", "Falcon"),
("Octave TTS", "009f6201-1862-454e-87b5-e2a367bc027f", "Octave"),
("Neural2", "666ff702-a2c8-413a-a8df-61902a6e7cb9", "Google"),
("Neuphonic TTS", "55912533-f2de-48e4-8318-990c435f1401", "Neuphonic"),
("VibeVoice 7B", "7395ed9e-1d19-4ab8-a989-eca165beefa4", "VibeVoice"),
("Orion", "90c81fc8-01ce-4e1d-bcd4-a57dff1c25cf", "Orion"),
("Chatterbox", "6cb43d71-bd49-4b44-9635-9ea5f28fd376", "Chatterbox"),
("VibeVoice 1.5B", "bd053b0f-5f69-4313-a7e5-5acd71422d4a", "VibeVoice"),
("SIMBA 1.6", "e0ca6a5b-6888-4e0f-8f4e-2c4cb59dd2aa", "SpeechifyAI"),
("Gemini 2.5 Flash Lite TTS", "a3824338-576c-4deb-8446-264eb9895db2", "Google"),
("Multilingual v2", "9cd43eb4-f436-4ea2-b3d9-5c912bdea912", "ElevenLabs"),
("T2A-01-HD", "0707d10e-e111-484c-8fd5-5dbb0a78029e", "MiniMax"),
("OpenAudio S1", "46730007-0d7b-4395-8077-2b5104ea79b0", "OpenAudio"),
("Turbo v2.5", "2d3f6b08-353d-40e9-a5e7-a3c1952b2dd8", "ElevenLabs"),
("Sonic 3", "a71d7774-e73e-4b6c-8dfe-7963273b2975", "Cartesia"),
("Maya1", "081709c5-d010-462f-b6ad-ef128df78cbd", "Maya"),
("T2A-01-Turbo", "c8c8fcf1-03cb-4b63-a6c9-d72cab227b3f", "MiniMax"),
("Fish Speech 1.5", "8bdfce7b-e8c5-4a29-984a-bd360d913c31", "Fish Audio"),
("Chatterbox HD", "06b8f3e3-e4fe-4cf7-9774-30816972bb59", "Chatterbox"),
("Polly Long-Form", "8b4be309-2f31-4ff2-be79-a6652c29ab12", "Amazon"),
("Murf Speech Gen 2", "86d3b629-8222-4ab6-ba77-6d2841fbf212", "Murf"),
("Mist V2", "5d922a1e-af77-4dae-adb4-7428dd634d5a", "Mist"),
("StyleTTS 2", "3b322e37-a016-4b2e-aacb-1aef8df00829", "StyleTTS"),
("Kokoro 82M v1.0", "6de03a42-66ce-484a-9c01-95fe9b4d7422", "Kokoro"),
("Flash v2.5", "cccb3742-da05-4447-910c-43a5d3eda5b7", "ElevenLabs"),
("Studio", "02265a20-67bc-4892-89d0-f54545b6b4a1", "ElevenLabs"),
("Async Flash v1.0", "765fd2b5-acb4-490f-a402-eaad710f23e1", "async"),
("TTS-1", "78588185-b188-46fb-8d06-c06f52009715", "OpenAI"),
("SIMBA 1.0", "588357bb-fe98-4286-b13e-3cae200e6085", "SpeechifyAI"),
("TTS-1 HD", "4a2776b8-a0c6-42f2-8ebe-db364f1ddf13", "OpenAI"),
("Polly Generative", "4ef13968-15ee-4311-a6d1-685f015b0717", "Amazon"),
("Journey", "2b5bb443-e32e-4cb4-a9f2-918c00210211", "Journey"),
("Chirp 3: HD", "f524429e-7e12-47eb-88df-6a9730f88989", "Google"),
("Gemini 2.5 Pro (Dec 2025)", "2e9ecf4e-7aca-41ac-9436-a5c0c5664fe0", "Google"),
("LMNT", "a8b4b1db-2a38-4649-9b57-a12fb0a85253", "LMNT"),
("OpenVoice v2", "80791fbe-7454-4719-9063-bedfcde606c7", "OpenVoice"),
("Fish Audio S1", "cfb0a07b-94b8-4b05-8d2c-3529f66cd1d2", "Fish Audio"),
("Sonic English (Oct 2024)", "798934ab-0f43-49d2-8343-15c1ade78458", "Cartesia"),
]
def safe_filename(name):
return re.sub(r'[<>:"/\\|?*()]', '', name).replace(' ', '_').strip()
def fetch_page(model_id, offset):
url = f"{BASE_URL}?model={model_id}&offset={offset}"
result = subprocess.run(
['curl', '-s', '--max-time', '30', url],
capture_output=True, text=True, timeout=35
)
if result.returncode != 0 or not result.stdout:
raise RuntimeError(f"API call failed")
return json.loads(result.stdout)
def download_file(url, dest):
if os.path.exists(dest) and os.path.getsize(dest) > 100:
return True
result = subprocess.run(
['curl', '-s', '-L', '--max-time', '60', '-o', dest, url],
capture_output=True, text=True, timeout=65
)
if result.returncode != 0:
return False
if os.path.getsize(dest) < 100:
os.remove(dest)
return False
return True
def download_model(model_name, model_id, creator):
folder_name = safe_filename(model_name)
folder_path = os.path.join(OUTPUT_DIR, creator, folder_name)
os.makedirs(folder_path, exist_ok=True)
existing = len([f for f in os.listdir(folder_path) if f.endswith('.mp3')])
try:
data = fetch_page(model_id, 0)
except Exception as e:
print(f" [{creator}] {model_name}: ERROR {e}", flush=True)
return 0, 0
total = data['total']
if total == 0:
print(f" [{creator}] {model_name}: 0 files, skip", flush=True)
return 0, 0
if existing >= total:
print(f" [{creator}] {model_name}: {existing}/{total} (done)", flush=True)
return existing, 0
print(f" [{creator}] {model_name}: {existing}/{total}, downloading...", flush=True)
downloaded = existing
failed = 0
offset = 0
while True:
try:
if offset > 0:
data = fetch_page(model_id, offset)
time.sleep(0.3)
for audio in data['audios']:
file_url = audio['fileName']
voice_name = audio['voice']['name']
accent = audio['voice']['accent']['slug']
gender = audio['voice']['gender']
file_id = file_url.split('/')[-1]
dest = os.path.join(folder_path, f"{voice_name}_{accent}_{gender}_{file_id}")
if download_file(file_url, dest):
downloaded += 1
else:
failed += 1
has_more = data['has_more']
offset = data['offset']
if not has_more:
break
except Exception as e:
print(f" ERROR at offset {offset}: {e}", flush=True)
offset += 20
if offset >= total:
break
time.sleep(2)
print(f" Done: {downloaded} ok, {failed} fail (total {total})", flush=True)
return downloaded, failed
def main():
os.makedirs(OUTPUT_DIR, exist_ok=True)
print(f"Downloading {len(MODELS)} remaining models to {OUTPUT_DIR}", flush=True)
total_ok = 0
total_fail = 0
for i, (model_name, model_id, creator) in enumerate(MODELS, 1):
ok, fail = download_model(model_name, model_id, creator)
total_ok += ok
total_fail += fail
time.sleep(0.5)
print(f"\nALL DONE: {total_ok} ok, {total_fail} failed", flush=True)
if __name__ == '__main__':
main()
// Luna TTS Ranking Bot — Background Service Worker
// Bridges content script ↔ Python native host via Chrome Native Messaging
const NATIVE_HOST = 'com.luna.tts';
let port = null;
let requestCallbacks = new Map();
let latestStats = { today_votes: 0, today_skips: 0, daily_limit: 800 };
let isConnected = false;
// ---- Native Messaging lifecycle ----
function connect() {
if (port) { try { port.disconnect(); } catch (e) {} }
port = chrome.runtime.connectNative(NATIVE_HOST);
port.onMessage.addListener((msg) => {
if (msg.type === 'match_result' && msg.request_id) {
const callback = requestCallbacks.get(msg.request_id);
if (callback) { callback(msg); requestCallbacks.delete(msg.request_id); }
}
if (msg.stats) latestStats = msg.stats;
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) chrome.tabs.sendMessage(tabs[0].id, { type: 'from_server', payload: msg }).catch(() => {});
});
chrome.runtime.sendMessage({ type: 'from_server', payload: msg }).catch(() => {});
});
port.onDisconnect.addListener(() => {
console.log('[bg] Native host disconnected');
isConnected = false;
broadcastState('disconnected');
port = null;
setTimeout(connect, 5000);
});
isConnected = true;
console.log('[bg] Connected to native host');
broadcastState('connected');
}
connect();
// ---- Message routing ----
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'match_request') {
if (port && isConnected) {
requestCallbacks.set(message.request_id, sendResponse);
port.postMessage({ type: 'match_request', request_id: message.request_id, audio_left: message.audio_left, audio_right: message.audio_right });
} else {
sendResponse({ type: 'match_result', decision: 'skip', reason: 'Python host not connected', details: {} });
}
return true;
}
if (message.type === 'get_state') {
sendResponse({ ws_connected: isConnected, ...latestStats });
return true;
}
if (message.type === 'verify_result' && port && isConnected) {
port.postMessage(message);
return false;
}
});
function broadcastState(status) {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) chrome.tabs.sendMessage(tabs[0].id, { type: 'ws_state', status }).catch(() => {});
});
}
// ---- Native input dispatch via Chrome Debugger API ----
async function _attachDebugger(tabId) {
return new Promise(resolve => {
chrome.debugger.attach({ tabId }, '1.3', () => {
if (chrome.runtime.lastError) { console.log('[bg] Debugger attach failed:', chrome.runtime.lastError.message); resolve(false); }
else resolve(true);
});
});
}
async function _debuggerSend(tabId, method, params) {
return new Promise(resolve => {
chrome.debugger.sendCommand({ tabId }, method, params, (result) => {
resolve(chrome.runtime.lastError ? null : result);
});
});
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const tabId = sender.tab?.id;
if (message.type === 'grant_autoplay') {
(async () => {
if (!tabId) { sendResponse({ ok: false }); return; }
if (!await _attachDebugger(tabId)) { sendResponse({ ok: false, error: 'debugger blocked' }); return; }
await _debuggerSend(tabId, 'Input.dispatchMouseEvent', { type: 'mousePressed', x: message.x || 500, y: message.y || 400, button: 'left', clickCount: 1 });
await _debuggerSend(tabId, 'Input.dispatchMouseEvent', { type: 'mouseReleased', x: message.x || 500, y: message.y || 400, button: 'left', clickCount: 1 });
chrome.debugger.detach({ tabId }, () => {});
sendResponse({ ok: true });
})();
return true;
}
if (message.type === 'click_at') {
(async () => {
if (!tabId) { sendResponse({ ok: false }); return; }
if (!await _attachDebugger(tabId)) { sendResponse({ ok: false }); return; }
await _debuggerSend(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x: message.x, y: message.y });
await new Promise(r => setTimeout(r, 100 + Math.random() * 200));
await _debuggerSend(tabId, 'Input.dispatchMouseEvent', { type: 'mousePressed', x: message.x, y: message.y, button: 'left', clickCount: 1 });
await new Promise(r => setTimeout(r, 50 + Math.random() * 100));
await _debuggerSend(tabId, 'Input.dispatchMouseEvent', { type: 'mouseReleased', x: message.x, y: message.y, button: 'left', clickCount: 1 });
chrome.debugger.detach({ tabId }, () => {});
sendResponse({ ok: true });
})();
return true;
}
if (message.type === 'dispatch_key') {
(async () => {
if (!tabId) { sendResponse({ ok: false }); return; }
if (!await _attachDebugger(tabId)) { sendResponse({ ok: false }); return; }
const key = message.key === 'left' ? 'ArrowLeft' : 'ArrowRight';
const vk = message.key === 'left' ? 37 : 39;
await _debuggerSend(tabId, 'Input.dispatchKeyEvent', { type: 'rawKeyDown', key, code: key, windowsVirtualKeyCode: vk });
await _debuggerSend(tabId, 'Input.dispatchKeyEvent', { type: 'keyUp', key, code: key, windowsVirtualKeyCode: vk });
chrome.debugger.detach({ tabId }, () => {});
sendResponse({ ok: true });
})();
return true;
}
});
chrome.alarms.create('keepalive', { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener(() => {});
// Injected into page main world — intercepts fetch for audio URLs
(() => {
const _fetch = window.fetch;
window.fetch = function (...args) {
const url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
if (/\.(mp3|wav|ogg|m4a|flac)(\?|#|$)/i.test(url)) {
window.postMessage({ type: '__luna_audio', url }, '*');
}
return _fetch.apply(this, args);
};
})();
{
"manifest_version": 3,
"name": "Luna TTS Ranking Bot",
"version": "0.1.0",
"description": "Semi-automated voting on Luna TTS Arena with voiceprint matching",
"permissions": [
"storage",
"alarms",
"activeTab",
"scripting",
"debugger",
"nativeMessaging"
],
"host_permissions": [
"https://artificialanalysis.ai/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://artificialanalysis.ai/text-to-speech/arena*"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"action": {
"default_popup": "popup/popup.html",
"default_title": "Luna TTS Bot"
},
"icons": {},
"web_accessible_resources": [
{
"resources": ["inject.js"],
"matches": ["https://artificialanalysis.ai/*"]
}
]
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { width: 280px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 16px; font-size: 13px; }
h2 { font-size: 15px; margin-bottom: 12px; }
.status { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; }
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.dot.on { background: #22c55e; }
.dot.off { background: #ef4444; }
.row { display: flex; justify-content: space-between; margin-bottom: 6px; }
.label { color: #6b7280; }
.value { font-weight: 600; }
button { width: 100%; padding: 8px; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; margin-top: 12px; }
button.start { background: #22c55e; color: white; }
button.stop { background: #ef4444; color: white; }
button:disabled { opacity: 0.5; cursor: default; }
hr { border: none; border-top: 1px solid #e5e7eb; margin: 12px 0; }
.log { font-size: 11px; color: #6b7280; max-height: 100px; overflow-y: auto; }
</style>
</head>
<body>
<h2>Luna TTS Bot</h2>
<div class="status">
<span class="dot" id="dot"></span>
<span id="statusText">Disconnected</span>
</div>
<div class="row"><span class="label">Server</span><span class="value" id="serverStatus">—</span></div>
<div class="row"><span class="label">Today votes</span><span class="value" id="todayVotes">0 / 800</span></div>
<div class="row"><span class="label">Today skips</span><span class="value" id="todaySkips">0</span></div>
<button id="btnStart" class="start">Start Voting</button>
<button id="btnStop" class="stop" disabled>Stop</button>
<hr>
<div class="log" id="log"></div>
<script src="popup.js"></script>
</body>
</html>
// Luna TTS Ranking Bot — Popup controller
const btnStart = document.getElementById('btnStart');
const btnStop = document.getElementById('btnStop');
const statusText = document.getElementById('statusText');
const dot = document.getElementById('dot');
const serverStatus = document.getElementById('serverStatus');
const todayVotes = document.getElementById('todayVotes');
const todaySkips = document.getElementById('todaySkips');
const log = document.getElementById('log');
let tabId = null;
async function getArenaTab() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab && tab.url && tab.url.startsWith('https://artificialanalysis.ai/text-to-speech/arena')) {
return tab;
}
return null;
}
function updateUI(state) {
if (state.ws_connected) {
statusText.textContent = 'Connected';
dot.className = 'dot on';
} else {
statusText.textContent = 'Disconnected';
dot.className = 'dot off';
}
serverStatus.textContent = state.ws_connected ? 'Online' : 'Offline';
}
function addLog(msg) {
const time = new Date().toLocaleTimeString();
log.textContent = `[${time}] ${msg}\n` + log.textContent;
}
async function refreshState() {
const tab = await getArenaTab();
chrome.runtime.sendMessage({ type: 'get_state' }, (state) => {
updateUI(state);
if (state.today_votes !== undefined) {
todayVotes.textContent = `${state.today_votes} / ${state.daily_limit || 800}`;
todaySkips.textContent = state.today_skips || 0;
}
});
if (!tab) return;
tabId = tab.id;
chrome.tabs.sendMessage(tabId, { type: 'get_status' }).then((contentState) => {
if (contentState) {
btnStart.disabled = contentState.enabled;
btnStop.disabled = !contentState.enabled;
}
}).catch(() => {});
}
btnStart.addEventListener('click', async () => {
const tab = await getArenaTab();
if (!tab) {
addLog('Arena page not found. Open it first.');
return;
}
chrome.tabs.sendMessage(tab.id, { type: 'start' }).then((res) => {
if (res) {
btnStart.disabled = true;
btnStop.disabled = false;
addLog('Voting started');
}
}).catch(() => {
addLog('Failed to start. Reload the arena page.');
});
});
btnStop.addEventListener('click', async () => {
const tab = await getArenaTab();
if (!tab) return;
chrome.tabs.sendMessage(tab.id, { type: 'stop' }).then((res) => {
if (res) {
btnStart.disabled = false;
btnStop.disabled = true;
addLog('Voting stopped');
}
}).catch(() => {});
});
// Listen for updates from background
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'from_server') {
const payload = message.payload;
if (payload.stats) {
todayVotes.textContent = `${payload.stats.today_votes} / ${payload.stats.daily_limit}`;
todaySkips.textContent = payload.stats.today_skips;
}
}
if (message.type === 'ws_state') {
updateUI({ ws_connected: message.status === 'connected' });
}
});
refreshState();
setInterval(refreshState, 5000);
"""All tunable parameters for the Luna TTS ranking bot."""
# ---- Matching ----
OUR_THRESHOLD = 0.80 # Rule 1-3: min cosine similarity to claim "our speaker"
COMPETITOR_THRESHOLD = 0.80 # Rule 4: competitor match threshold
# ---- Voting strategy ----
BIG_VENDOR_VOTE_PROBABILITY = 75 # Rule 5: percentage (0-100), roll <= this → vote
DAILY_VOTE_LIMIT = 800 # Rule 6: stop after this many votes today
# ---- Anti-detection timing ----
VOTE_PERIOD_SECONDS = 600 # Vote for 10 min
REST_MIN_SECONDS = 60 # Then rest 1-3 min (random)
REST_MAX_SECONDS = 180
VOTE_INTERVAL_MIN_SECONDS = 2 # Gap between votes
VOTE_INTERVAL_MAX_SECONDS = 8
CLICK_DELAY_MIN_MS = 100 # Extra delay before simulating keypress
CLICK_DELAY_MAX_MS = 500
# ---- Server ----
WS_HOST = "localhost"
WS_PORT = 8765
# ---- Paths (relative to server.py location) ----
REFERENCE_DIRS = {
"our": "reference/standard",
"competitor": "reference/competitors",
"big_vendor": "reference/big_vendors",
}
#!/bin/bash
# Install Chrome Native Messaging host manifest (Mac)
EXT_ID="$1"
if [ -z "$EXT_ID" ]; then
echo "Usage: ./install_host.sh <extension-id>"
echo "Find your extension ID at chrome://extensions/"
echo "Example: ./install_host.sh abcdefghijklmnop"
exit 1
fi
HOST_DIR="$(cd "$(dirname "$0")" && pwd)"
# Compile wrapper binary
if [ ! -x "$HOST_DIR/run_host" ]; then
echo "Compiling native host wrapper..."
if ! command -v gcc &>/dev/null; then
echo ""
echo "========================================"
echo "ERROR: gcc 未安装"
echo "请先安装 Xcode Command Line Tools:"
echo " xcode-select --install"
echo "========================================"
echo ""
exit 1
fi
gcc -o "$HOST_DIR/run_host" "$HOST_DIR/runner.c" || {
echo "编译失败,请确认 gcc 可用"
exit 1
}
echo " 编译完成: $HOST_DIR/run_host"
fi
# Install manifest to all known Chromium browser directories
INSTALLED=0
install_to() {
local base="$1"
[ ! -d "$base" ] && return
local nm_dir="$base/NativeMessagingHosts"
mkdir -p "$nm_dir"
cat > "$nm_dir/com.luna.tts.json" << JSONEOF
{
"name": "com.luna.tts",
"description": "Luna TTS Ranking Bot - Native Messaging Host",
"path": "$HOST_DIR/run_host",
"type": "stdio",
"allowed_origins": ["chrome-extension://$EXT_ID/"]
}
JSONEOF
echo " $nm_dir/com.luna.tts.json"
INSTALLED=$((INSTALLED + 1))
}
for b in "Google/Chrome" "Chromium" "Quark" "Microsoft Edge"; do
install_to "$HOME/Library/Application Support/$b"
done
# AdsPower cache dirs
ADSPOWER_BASE="$HOME/Library/Application Support/adspower_global/cwd_global/source/cache"
if [ -d "$ADSPOWER_BASE" ]; then
for d in "$ADSPOWER_BASE"/*; do
[ -d "$d" ] && install_to "$d"
done
fi
if [ $INSTALLED -eq 0 ]; then
echo "WARNING: 没有找到浏览器目录,只装了以下基础路径:"
NM_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
mkdir -p "$NM_DIR"
cat > "$NM_DIR/com.luna.tts.json" << JSONEOF
{
"name": "com.luna.tts",
"description": "Luna TTS Ranking Bot - Native Messaging Host",
"path": "$HOST_DIR/run_host",
"type": "stdio",
"allowed_origins": ["chrome-extension://$EXT_ID/"]
}
JSONEOF
echo " $NM_DIR/com.luna.tts.json"
fi
echo ""
echo "完成!Host: $HOST_DIR/run_host"
echo "扩展 ID: $EXT_ID"
echo ""
echo "现在去 chrome://extensions/ 刷新扩展,然后打开打榜页面即可"
========================================
Traceback (most recent call last):
File "/Users/chengyubing/Documents/projects/hzjj/tts-ranking/server/run_host.py", line 12, in <module>
main()
File "/Users/chengyubing/Documents/projects/hzjj/tts-ranking/server/native_host.py", line 81, in main
print("[host] Ready, waiting for messages...", file=sys.stderr)
BrokenPipeError: [Errno 32] Broken pipe
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment