diff --git a/proxy-llm.py b/proxy-llm.py new file mode 100644 index 0000000000..532817bd14 --- /dev/null +++ b/proxy-llm.py @@ -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) + 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: + 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") \ No newline at end of file diff --git a/subtitle.txt b/subtitle.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/subtitlepy.py b/subtitlepy.py new file mode 100644 index 0000000000..c50e2a6b8b --- /dev/null +++ b/subtitlepy.py @@ -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_())