7bc34c79ed
- Removed all cloud-related functionalities, including login prompts and token handling. - Disabled Laxis cloud features, ensuring no data is sent to external servers. - Updated manifest to reflect the new local-only functionality. - Added a new Python server to handle transcripts locally, including WebSocket support. - Implemented storage management for transcripts, including deduplication and file writing. - Created a smoke test for the WebSocket server to simulate transcript updates. - Updated README with setup instructions and usage details for the new local server.
156 lines
6.5 KiB
Python
156 lines
6.5 KiB
Python
"""
|
|
mcp_server.py — نقطهی ورود. WebSocket و MCP را با هم در یک پراسس اجرا میکند.
|
|
|
|
- ws_server.run_forever() بهعنوان background task (گرفتن caption از افزونه)
|
|
- FastMCP روی stdio (تا Claude Desktop transcript را بخواند)
|
|
|
|
هر دو حافظه/فایلهای مشترکِ storage را میبینند.
|
|
|
|
اجرا:
|
|
pip install -r requirements.txt
|
|
python mcp_server.py # WebSocket + MCP (برای Claude Desktop)
|
|
python mcp_server.py --ws-only # فقط WebSocket (یا مستقیم: python ws_server.py)
|
|
|
|
کانفیگ Claude Desktop به همین فایل اشاره میکند (پایین README).
|
|
نکته: stdout برای پروتکل MCP رزرو است؛ همهی لاگها در storage.log به stderr میروند.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
|
|
|
|
|
|
import storage
|
|
from storage import (
|
|
log, safe_session_id, latest_session, read_txt, read_srt,
|
|
read_segments, json_mtime, fmt_clock, list_session_files,
|
|
)
|
|
import ws_server
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MCP
|
|
# ---------------------------------------------------------------------------
|
|
def build_mcp() -> "FastMCP":
|
|
mcp = FastMCP("meet-transcripts")
|
|
|
|
@mcp.tool()
|
|
def list_sessions() -> str:
|
|
"""فهرست جلسههای ضبطشده (از روی دیسک): تعداد segment و آخرین خط."""
|
|
files = list_session_files()
|
|
if not files:
|
|
return "هیچ جلسهای هنوز ضبط نشده است."
|
|
latest = latest_session()
|
|
out = []
|
|
for p in files:
|
|
lines = p.read_text(encoding="utf-8").splitlines()
|
|
last = lines[-1] if lines else ""
|
|
mark = " ← آخرین" if p.stem == latest else ""
|
|
out.append(f"- {p.stem}{mark}: {len(lines)} segment | آخرین: {last[:80]}")
|
|
return "\n".join(out)
|
|
|
|
@mcp.tool()
|
|
def get_status(session_id: str = "") -> str:
|
|
"""وضعیت سبک برای «چیزی تغییر کرد؟» بدون خواندن متن. اگر session_id ندهی، آخرین جلسه.
|
|
خروجی JSON: latest_seq، count، live_len، updated_at. اگر latest_seq/updated_at عوض شد،
|
|
get_updates بزن."""
|
|
sid = safe_session_id(session_id) if session_id else latest_session()
|
|
if not sid:
|
|
return json.dumps({"session_id": None, "latest_seq": 0, "count": 0,
|
|
"live_len": 0, "updated_at": 0}, ensure_ascii=False)
|
|
segs = read_segments(sid)
|
|
return json.dumps({
|
|
"session_id": sid,
|
|
"latest_seq": segs[-1]["seq"] if segs else 0,
|
|
"count": len(segs),
|
|
"live_len": len(segs[-1].get("text", "")) if segs else 0,
|
|
"updated_at": round(json_mtime(sid), 3),
|
|
}, ensure_ascii=False)
|
|
|
|
@mcp.tool()
|
|
def get_updates(session_id: str = "", after_seq: int = 0) -> str:
|
|
"""خواندن افزایشی: فقط segmentهای از after_seq به بعد (نه کل transcript).
|
|
بار اول after_seq=0 (همه را میگیری). دفعات بعد latest_seqِ قبلی را بده تا فقط
|
|
موارد جدید + segmentِ زنده را بگیری. خروجی JSON: {latest_seq, count, segments:[{seq,t,speaker,text}]}."""
|
|
sid = safe_session_id(session_id) if session_id else latest_session()
|
|
if not sid:
|
|
return json.dumps({"session_id": None, "latest_seq": 0, "count": 0,
|
|
"segments": []}, ensure_ascii=False)
|
|
segs = read_segments(sid)
|
|
base = min((s["start"] for s in segs), default=0)
|
|
sel = [s for s in segs if s.get("seq", 0) >= after_seq] if after_seq > 0 else segs
|
|
out = [{"seq": s.get("seq", 0), "t": fmt_clock(s["start"] - base),
|
|
"speaker": s.get("speaker", ""), "text": (s.get("text") or "").strip()}
|
|
for s in sel]
|
|
return json.dumps({
|
|
"session_id": sid,
|
|
"latest_seq": segs[-1]["seq"] if segs else 0,
|
|
"count": len(segs),
|
|
"segments": out,
|
|
}, ensure_ascii=False)
|
|
|
|
@mcp.tool()
|
|
def get_latest_transcript() -> str:
|
|
"""متن تمیزِ آخرین جلسه (با [mm:ss]، بدون تکرار)."""
|
|
sid = latest_session()
|
|
if not sid:
|
|
return "هنوز هیچ caption ای دریافت نشده است."
|
|
return read_txt(sid) or ""
|
|
|
|
@mcp.tool()
|
|
def get_transcript(session_id: str) -> str:
|
|
"""متن تمیزِ یک جلسه با sessionId (با [mm:ss])."""
|
|
txt = read_txt(session_id)
|
|
return txt if txt is not None else f"جلسهای با id «{safe_session_id(session_id)}» نیست."
|
|
|
|
@mcp.tool()
|
|
def get_latest_srt() -> str:
|
|
"""زیرنویس SRT آخرین جلسه (با تایمکد استاندارد)."""
|
|
sid = latest_session()
|
|
if not sid:
|
|
return "هنوز هیچ caption ای دریافت نشده است."
|
|
return read_srt(sid) or ""
|
|
|
|
@mcp.tool()
|
|
def get_srt(session_id: str) -> str:
|
|
"""زیرنویس SRT یک جلسه با sessionId."""
|
|
srt = read_srt(session_id)
|
|
return srt if srt is not None else f"جلسهای با id «{safe_session_id(session_id)}» نیست."
|
|
|
|
@mcp.resource("transcript://{session_id}")
|
|
def transcript_resource(session_id: str) -> str:
|
|
return read_txt(session_id) or ""
|
|
|
|
@mcp.resource("srt://{session_id}")
|
|
def srt_resource(session_id: str) -> str:
|
|
return read_srt(session_id) or ""
|
|
|
|
return mcp
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# اجرا
|
|
# ---------------------------------------------------------------------------
|
|
async def main():
|
|
asyncio.create_task(ws_server.run_forever())
|
|
log(f"📁 transcripts در: {storage.TRANSCRIPTS_DIR}")
|
|
|
|
ws_only = "--ws-only" in sys.argv
|
|
if FastMCP is not None and not ws_only:
|
|
log("🔗 MCP server (stdio) فعال است — منتظر اتصال Claude Desktop …")
|
|
await build_mcp().run_stdio_async()
|
|
else:
|
|
if FastMCP is None and not ws_only:
|
|
log("ℹ️ پکیج mcp نصب نیست؛ فقط WebSocket اجرا میشود (pip install mcp).")
|
|
await asyncio.Future()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
asyncio.run(main())
|
|
except KeyboardInterrupt:
|
|
log("\n👋 خاموش شد.")
|