Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions proxy-llm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
import httpx
import uvicorn
import json
import hashlib
from urllib.parse import quote
import time
import re
import os

app = FastAPI(title="AIRI DeepSeek Proxy + 字幕中文优化版")

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

client = httpx.AsyncClient(timeout=120.0)

# ====================== 配置区域 ======================
# 请在这里填写你的配置信息

DEEPSEEK_URL = "https://api.deepseek.com/v1/chat/completions"
DEEPSEEK_KEY = "" # ← 在这里填你的 DeepSeek API Key

BAIDU_APPID = "" # ← 在这里填你的百度翻译 AppID
BAIDU_KEY = "" # ← 在这里填你的百度翻译 密钥

# 字幕输出文件路径(请修改成你自己的实际路径)
SUBTITLE_FILE = r"C:\Users\你的用户名\subtitle.txt"

# ====================== 特殊词配置 ======================
# 你可以在这里添加需要特殊处理的词,此处只做示例,实际情况按需要修改(原词 → 中文显示)
# 格式: "原词": "中文显示"
SPECIAL_WORDS = {
"ドクター": "博士",
"博士": "博士",
}

# ====================== 工具函数 ======================
def replace_doctor(text: str) -> str:
"""强制把所有博士相关称呼统一成「博士」,此处只做示例,实际情况按需要修改"""
text = re.sub(r"ドクター", "博士", text)
text = re.sub(r"医生", "博士", text)
text = re.sub(r"醫生", "博士", text)
text = re.sub(r"博士さん", "博士", text)
return text

def baidu_translate(text: str) -> str:
"""调用百度翻译(纯翻译,不处理特殊词)"""
if not text.strip():
return text

if not BAIDU_APPID or not BAIDU_KEY:
print("[Baidu] 未配置百度翻译密钥,使用原始文本")
return text

salt = str(int(time.time() * 1000))
sign_str = BAIDU_APPID + text + salt + BAIDU_KEY
sign = hashlib.md5(sign_str.encode('utf-8')).hexdigest()

url = (
f"https://fanyi-api.baidu.com/api/trans/vip/translate"
f"?q={quote(text)}"
f"&from=auto&to=zh"
f"&appid={BAIDU_APPID}"
f"&salt={salt}"
f"&sign={sign}"
)

try:
resp = httpx.get(url, timeout=15)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid blocking the event loop during translation

This endpoint is async, but baidu_translate performs a synchronous httpx.get call. When translation runs, the event loop is blocked for up to the timeout, which stalls other concurrent streaming requests and can delay or freeze responses in multi-client usage. Use an async client call from the request context instead.

Useful? React with 👍 / 👎.

data = resp.json()

if "trans_result" in data and data["trans_result"]:
translated_parts = [item["dst"] for item in data["trans_result"]]
return " ".join(translated_parts)
else:
print("[Baidu] 翻译失败:", data.get("error_msg"))
return text
except Exception as e:
print(f"[Baidu] 异常: {str(e)}")
return text


# ====================== 主代理逻辑 ======================
@app.post("/v1/chat/completions")
@app.post("/chat/completions")
async def proxy_chat(request: Request):
body = await request.json()
user_message = body.get("messages", [{}])[-1].get("content", "无内容")
print(f"[Proxy] 收到 AIRI 请求: {user_message}")

headers = {
"Authorization": f"Bearer {DEEPSEEK_KEY}",
"Content-Type": "application/json"
}

# 新对话清空字幕
try:
open(SUBTITLE_FILE, "w", encoding="utf-8").close()
print("[Proxy] 新对话开始,已清空字幕文件")
except Exception:
pass

content_parts = []

async def forward_original():
try:
async with client.stream("POST", DEEPSEEK_URL, json=body, headers=headers) as resp:
async for chunk in resp.aiter_bytes():
text = chunk.decode('utf-8', errors='ignore')
lines = text.split('\n')
for line in lines:
line = line.strip()
if line.startswith('data: ') and '[DONE]' not in line:
Comment on lines +117 to +121
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Buffer SSE fragments before parsing deltas

Treating each aiter_bytes() chunk as complete SSE lines is unsafe because chunk boundaries are arbitrary; JSON payloads are often split across chunks. In that case line.startswith('data: ')/json.loads(...) misses fragments and silently skips them, so content_parts becomes incomplete and the subtitle text can drop words or entire segments under normal network conditions.

Useful? React with 👍 / 👎.

try:
data_str = line[6:].strip()
if data_str.startswith('{'):
data = json.loads(data_str)
delta = data.get('choices', [{}])[0].get('delta', {})
content = delta.get('content', '')
if content:
content_parts.append(content)
except:
continue

# 转发原始日文给 AIRI(保证语音合成发音正确)
yield chunk

# ==================== 流结束处理 ====================
if content_parts:
full_japanese = "".join(content_parts).strip()
full_japanese = re.sub(r'\s+', ' ', full_japanese)

# 1. 翻译成中文
translated_chinese = baidu_translate(full_japanese)

# 2. 强制替换成「博士」,此处只做示例,实际情况按需要修改
final_subtitle = replace_doctor(translated_chinese)

# 3. 写入字幕文件
try:
with open(SUBTITLE_FILE, "w", encoding="utf-8") as f:
f.write(final_subtitle)
print(f"[Proxy] 字幕已写入: {final_subtitle[:100]}...")
except Exception as e:
print(f"[Proxy] 写字幕失败: {e}")

yield "data: [DONE]\n\n"

except Exception as e:
print(f"[Proxy] 转发异常: {str(e)}")

return StreamingResponse(forward_original(), media_type="text/event-stream")


@app.get("/v1/models")
@app.get("/models")
async def get_models():
return {
"data": [
{"id": "deepseek-chat", "object": "model", "owned_by": "deepseek"},
{"id": "deepseek-reasoner", "object": "model", "owned_by": "deepseek"}
]
}

@app.get("/")
async def root():
return {"status": "Proxy online", "for": "AIRI + 独立字幕"}

if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=9000, log_level="info")
Empty file added subtitle.txt
Empty file.
136 changes: 136 additions & 0 deletions subtitlepy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import sys
import os
import time
import threading
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QTimer, QRect
from PyQt5.QtGui import QFont, QPainter, QColor, QBrush, QFontMetrics

class SubtitleWindow(QWidget):
def __init__(self):
super().__init__()

# ===== 修改这里:增大窗口尺寸 =====
self.WIDTH = 1500 # 改这个值调整宽度
self.HEIGHT = 400 # 改这个值调整高度
self.MARGIN = 20 # 改这个值调整文本边距

# 设置窗口属性
self.setWindowFlags(
Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint |
Qt.Tool
)

self.setAttribute(Qt.WA_TranslucentBackground)
self.setGeometry(100, 800, self.WIDTH, self.HEIGHT)

self.text = "等待翻译..."

# 字体大小(可以调整)
self.font = QFont("Microsoft YaHei", 28)
self.font.setBold(True)

self.start_monitor()

self.timer = QTimer()
self.timer.timeout.connect(self.update)
self.timer.start(100)

self.drag_position = None

# ===== 添加这个新方法:自动换行 =====
def wrap_text(self, text, font, max_width):
"""自动换行函数"""
if not text:
return []

font_metrics = QFontMetrics(font)
lines = []
current_line = ""

for char in text:
test_line = current_line + char
if font_metrics.width(test_line) <= max_width:
current_line = test_line
else:
if current_line:
lines.append(current_line)
current_line = char

if current_line:
lines.append(current_line)

return lines

def start_monitor(self):
"""启动字幕文件监控"""
self.subtitle_file = r""#填写你的文本地址
self.last_modified = 0

def monitor():
while True:
if os.path.exists(self.subtitle_file):
mtime = os.path.getmtime(self.subtitle_file)
if mtime > self.last_modified:
try:
with open(self.subtitle_file, 'r', encoding='utf-8') as f:
new_text = f.read().strip()
if new_text:
self.text = new_text
print(f"[字幕] 更新: {self.text[:50]}...")
self.last_modified = mtime
except Exception as e:
print(f"[字幕] 读取错误: {e}")
time.sleep(0.2)

thread = threading.Thread(target=monitor, daemon=True)
thread.start()

# ===== 替换这个绘制方法:支持多行 =====
def paintEvent(self, event):
"""绘制字幕(支持多行)"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)

painter.setBrush(QBrush(QColor(0, 0, 0, 0)))
painter.setPen(Qt.NoPen)
painter.drawRect(self.rect())

painter.setFont(self.font)

max_width = self.WIDTH - self.MARGIN * 2
lines = self.wrap_text(self.text, self.font, max_width)
line_height = self.font.pointSize() + 28
total_height = len(lines) * line_height
start_y = (self.HEIGHT - total_height) // 2

for i, line in enumerate(lines):
y = start_y + i * line_height

painter.setPen(QColor(0, 0, 0, 200))
painter.drawText(QRect(self.MARGIN + 2, y + 2, max_width, line_height),
Qt.AlignCenter, line)

painter.setPen(QColor(255, 255, 255, 255))
painter.drawText(QRect(self.MARGIN, y, max_width, line_height),
Qt.AlignCenter, line)

def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()

def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton and self.drag_position:
self.move(event.globalPos() - self.drag_position)
event.accept()

def contextMenuEvent(self, event):
self.close()

if __name__ == "__main__":
app = QApplication(sys.argv)
window = SubtitleWindow()
window.show()
sys.exit(app.exec_())