-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
airi with subtitle and translate #1496
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
09188de
f4896cc
523598c
59f96fa
ac2598c
f480d00
681a9b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Treating each 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") | ||
| 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_()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This endpoint is async, but
baidu_translateperforms a synchronoushttpx.getcall. 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 👍 / 👎.