optimized PC player
This commit is contained in:
231
media_player.py
231
media_player.py
@@ -35,6 +35,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import queue
|
import queue
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -86,6 +87,61 @@ DEBUG_LOG_DIR = Path.home() / ".mediaplayer_logs"
|
|||||||
DEBUG_LOG_PATH = DEBUG_LOG_DIR / "mediaplayer.log"
|
DEBUG_LOG_PATH = DEBUG_LOG_DIR / "mediaplayer.log"
|
||||||
_debug_logger = logging.getLogger("mediaplayer")
|
_debug_logger = logging.getLogger("mediaplayer")
|
||||||
|
|
||||||
|
_SPEAKER_TAG_RE = re.compile(r"\[(S\d+)\]")
|
||||||
|
|
||||||
|
|
||||||
|
def _split_speaker_tagged_sentences(document: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Split sentences that still contain inline ``[S01]`` speaker tags.
|
||||||
|
|
||||||
|
The server should already strip these tags and split at speaker boundaries
|
||||||
|
(see ``split_sentences_at_punctuation``), but locally cached boundaries
|
||||||
|
from an older server version may still have the tags embedded. This
|
||||||
|
post-processing ensures the player always uses clean, per-phrase sentences
|
||||||
|
for playback navigation, AI explanation, and every other operation.
|
||||||
|
"""
|
||||||
|
sentences = document.get("sentences")
|
||||||
|
if not isinstance(sentences, list) or not sentences:
|
||||||
|
return document
|
||||||
|
needs_split = any(_SPEAKER_TAG_RE.search(str(s.get("text") or "")) for s in sentences)
|
||||||
|
if not needs_split:
|
||||||
|
return document
|
||||||
|
|
||||||
|
cleaned: list = []
|
||||||
|
for sentence in sentences:
|
||||||
|
text = str(sentence.get("text") or "")
|
||||||
|
matches = list(_SPEAKER_TAG_RE.finditer(text))
|
||||||
|
if not matches:
|
||||||
|
cleaned.append(sentence)
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_ms = int(sentence["start_ms"])
|
||||||
|
end_ms = int(sentence["end_ms"])
|
||||||
|
duration_ms = max(1, end_ms - start_ms)
|
||||||
|
total = len(matches)
|
||||||
|
for index, match in enumerate(matches):
|
||||||
|
piece_start = match.end()
|
||||||
|
if index + 1 < total:
|
||||||
|
piece_end = matches[index + 1].start()
|
||||||
|
else:
|
||||||
|
piece_end = len(text)
|
||||||
|
piece = text[piece_start:piece_end].strip()
|
||||||
|
if not piece:
|
||||||
|
continue
|
||||||
|
sub_start = start_ms + int(duration_ms * index / total)
|
||||||
|
sub_end = start_ms + int(duration_ms * (index + 1) / total)
|
||||||
|
if sub_end <= sub_start:
|
||||||
|
sub_end = sub_start + 1
|
||||||
|
cleaned.append({
|
||||||
|
**sentence,
|
||||||
|
"start_ms": sub_start,
|
||||||
|
"end_ms": sub_end,
|
||||||
|
"text": piece,
|
||||||
|
})
|
||||||
|
|
||||||
|
for index, sentence in enumerate(cleaned):
|
||||||
|
sentence["index"] = index
|
||||||
|
return {**document, "sentences": cleaned}
|
||||||
|
|
||||||
|
|
||||||
def setup_debug_logging() -> Path:
|
def setup_debug_logging() -> Path:
|
||||||
"""Write concise diagnostics to a small local rotating log."""
|
"""Write concise diagnostics to a small local rotating log."""
|
||||||
@@ -1103,8 +1159,12 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
text="按句跳转",
|
text="按句跳转",
|
||||||
variable=self.sentence_mode,
|
variable=self.sentence_mode,
|
||||||
command=self._on_sentence_mode,
|
command=self._on_sentence_mode,
|
||||||
|
takefocus=0,
|
||||||
)
|
)
|
||||||
self.sentence_mode_btn.pack(side="left", padx=(16, 0))
|
self.sentence_mode_btn.pack(side="left", padx=(16, 0))
|
||||||
|
# 点击 Checkbutton 后 tk 仍会把焦点给它;把空格键重定向到播放/暂停,
|
||||||
|
# 避免误改复选按钮状态。
|
||||||
|
self.sentence_mode_btn.bind("<space>", self._on_space)
|
||||||
self.btn_generate_boundaries = ttk.Button(
|
self.btn_generate_boundaries = ttk.Button(
|
||||||
controls,
|
controls,
|
||||||
text="生成句边界",
|
text="生成句边界",
|
||||||
@@ -1172,6 +1232,8 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
("<period>", lambda _e: self.seek_sentence(1)),
|
("<period>", lambda _e: self.seek_sentence(1)),
|
||||||
("<e>", lambda _e: self.show_ai_explanation()),
|
("<e>", lambda _e: self.show_ai_explanation()),
|
||||||
("<E>", lambda _e: self.show_ai_explanation()),
|
("<E>", lambda _e: self.show_ai_explanation()),
|
||||||
|
("<a>", lambda _e: self.show_ai_explanation()),
|
||||||
|
("<A>", lambda _e: self.show_ai_explanation()),
|
||||||
("<f>", lambda _e: self.toggle_fullscreen()),
|
("<f>", lambda _e: self.toggle_fullscreen()),
|
||||||
("<F>", lambda _e: self.toggle_fullscreen()),
|
("<F>", lambda _e: self.toggle_fullscreen()),
|
||||||
("<Escape>", lambda _e: self.leave_fullscreen()),
|
("<Escape>", lambda _e: self.leave_fullscreen()),
|
||||||
@@ -1622,7 +1684,7 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
document = self._load_cached_boundaries(media_hash)
|
document = self._load_cached_boundaries(media_hash)
|
||||||
if document and document.get("sentences"):
|
if document and document.get("sentences"):
|
||||||
self._media_hashes[path] = media_hash
|
self._media_hashes[path] = media_hash
|
||||||
self._boundaries[path] = document
|
self._boundaries[path] = _split_speaker_tagged_sentences(document)
|
||||||
count = len(document["sentences"])
|
count = len(document["sentences"])
|
||||||
message = f"已加载本地句边界缓存:共 {count} 句"
|
message = f"已加载本地句边界缓存:共 {count} 句"
|
||||||
debug_log(
|
debug_log(
|
||||||
@@ -1714,6 +1776,9 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
if (self._boundaries.get(path) or {}).get("sentences"):
|
if (self._boundaries.get(path) or {}).get("sentences"):
|
||||||
if not messagebox.askyesno("生成句边界", "当前文件已有句边界,确定要重新生成吗?"):
|
if not messagebox.askyesno("生成句边界", "当前文件已有句边界,确定要重新生成吗?"):
|
||||||
return
|
return
|
||||||
|
force = True
|
||||||
|
else:
|
||||||
|
force = False
|
||||||
self.sentence_mode.set(False)
|
self.sentence_mode.set(False)
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
self._analysis_running.add(path)
|
self._analysis_running.add(path)
|
||||||
@@ -1723,11 +1788,11 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
self._update_sentence_controls()
|
self._update_sentence_controls()
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._analyze_sentences,
|
target=self._analyze_sentences,
|
||||||
args=(path, token),
|
args=(path, token, force),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
def _analyze_sentences(self, path, token):
|
def _analyze_sentences(self, path, token, force=False):
|
||||||
error = ""
|
error = ""
|
||||||
document = None
|
document = None
|
||||||
source = "generated"
|
source = "generated"
|
||||||
@@ -1744,19 +1809,26 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
)
|
)
|
||||||
self._media_hashes[path] = media_hash
|
self._media_hashes[path] = media_hash
|
||||||
|
|
||||||
|
if force:
|
||||||
|
self._report_progress(18, "正在重新生成句边界…")
|
||||||
|
else:
|
||||||
self._report_progress(18, "正在检查线上是否已有句边界…")
|
self._report_progress(18, "正在检查线上是否已有句边界…")
|
||||||
remote_document = self._fetch_remote_boundaries(media_hash)
|
remote_document = self._fetch_remote_boundaries(media_hash)
|
||||||
if remote_document and remote_document.get("sentences"):
|
if remote_document and remote_document.get("sentences"):
|
||||||
document = remote_document
|
document = remote_document
|
||||||
|
document = _split_speaker_tagged_sentences(document)
|
||||||
source = "server"
|
source = "server"
|
||||||
self._save_cached_boundaries(media_hash, document)
|
self._save_cached_boundaries(media_hash, document)
|
||||||
else:
|
if document is None and not force:
|
||||||
document = self._load_cached_boundaries(media_hash)
|
document = self._load_cached_boundaries(media_hash)
|
||||||
if document and document.get("sentences"):
|
if document and document.get("sentences"):
|
||||||
source = "local"
|
source = "local"
|
||||||
else:
|
if document is None:
|
||||||
|
if not force:
|
||||||
self._report_progress(25, "线上没有句边界,正在准备音频…")
|
self._report_progress(25, "线上没有句边界,正在准备音频…")
|
||||||
debug_log("audio_extraction_started", media_hash=media_hash)
|
else:
|
||||||
|
self._report_progress(25, "正在重新提取音频…")
|
||||||
|
debug_log("audio_extraction_started", media_hash=media_hash, force=force)
|
||||||
audio_path = self._make_audio_upload(
|
audio_path = self._make_audio_upload(
|
||||||
path,
|
path,
|
||||||
progress_callback=lambda ratio: self._report_progress(
|
progress_callback=lambda ratio: self._report_progress(
|
||||||
@@ -1784,6 +1856,7 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
document = _split_speaker_tagged_sentences(document)
|
||||||
self._save_cached_boundaries(media_hash, document)
|
self._save_cached_boundaries(media_hash, document)
|
||||||
source = "generated"
|
source = "generated"
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -1800,6 +1873,7 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
|
|
||||||
self._analysis_running.discard(path)
|
self._analysis_running.discard(path)
|
||||||
if document is not None and document.get("sentences"):
|
if document is not None and document.get("sentences"):
|
||||||
|
document = _split_speaker_tagged_sentences(document)
|
||||||
self._boundaries[path] = document
|
self._boundaries[path] = document
|
||||||
count = len(document["sentences"])
|
count = len(document["sentences"])
|
||||||
if source == "server":
|
if source == "server":
|
||||||
@@ -2367,54 +2441,138 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
pass
|
pass
|
||||||
window = tk.Toplevel(self)
|
window = tk.Toplevel(self)
|
||||||
window.title("AI 句子讲解")
|
window.title("AI 句子讲解")
|
||||||
window.geometry("480x380")
|
window.geometry("1000x800")
|
||||||
|
window.minsize(1000, 800)
|
||||||
|
window.configure(background="#1e1e2e")
|
||||||
window.transient(self)
|
window.transient(self)
|
||||||
frame = ttk.Frame(window, padding=14)
|
|
||||||
|
# Dark theme palette.
|
||||||
|
bg = "#1e1e2e"
|
||||||
|
card_bg = "#2a2a3d"
|
||||||
|
fg_primary = "#cdd6f4"
|
||||||
|
fg_secondary = "#9399b2"
|
||||||
|
accent = "#89b4fa"
|
||||||
|
accent_hover = "#74c7ec"
|
||||||
|
|
||||||
|
frame = tk.Frame(window, bg=bg, padx=16, pady=14)
|
||||||
frame.pack(fill="both", expand=True)
|
frame.pack(fill="both", expand=True)
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
frame.rowconfigure(1, weight=1)
|
frame.rowconfigure(2, weight=1)
|
||||||
ttk.Label(
|
|
||||||
frame,
|
font_size = [20]
|
||||||
text=sentence_text,
|
|
||||||
wraplength=440,
|
# ── Top toolbar panel ─────────────────────────────────────────────
|
||||||
justify="left",
|
# Fixed-height, always visible: title | A− 20pt A+ | ✕
|
||||||
foreground="#111111",
|
toolbar = tk.Frame(frame, bg=card_bg, padx=12, pady=8)
|
||||||
font=("Helvetica", 12, "bold"),
|
toolbar.grid(row=0, column=0, sticky="ew", pady=(0, 10))
|
||||||
).grid(
|
toolbar.columnconfigure(1, weight=1)
|
||||||
row=0, column=0, sticky="ew", pady=(0, 10)
|
|
||||||
|
tk.Label(
|
||||||
|
toolbar,
|
||||||
|
text="AI 句子讲解",
|
||||||
|
bg=card_bg,
|
||||||
|
fg=fg_primary,
|
||||||
|
font=("Helvetica", 13, "bold"),
|
||||||
|
).grid(row=0, column=0, sticky="w")
|
||||||
|
|
||||||
|
font_controls = tk.Frame(toolbar, bg=card_bg)
|
||||||
|
font_controls.grid(row=0, column=2, sticky="e")
|
||||||
|
|
||||||
|
def _apply_font_size():
|
||||||
|
size_label.configure(text=f"{font_size[0]}pt")
|
||||||
|
sentence_label.configure(font=("Helvetica", font_size[0], "bold"))
|
||||||
|
text.configure(font=("Helvetica", font_size[0]))
|
||||||
|
|
||||||
|
def _set_font_size(delta):
|
||||||
|
font_size[0] = max(12, min(36, font_size[0] + delta))
|
||||||
|
_apply_font_size()
|
||||||
|
|
||||||
|
tk.Button(
|
||||||
|
font_controls, text="A\u2212", command=lambda: _set_font_size(-2),
|
||||||
|
bg=accent, fg=bg, activebackground=accent_hover, activeforeground=bg,
|
||||||
|
relief="flat", padx=10, pady=4, font=("Helvetica", 12, "bold"),
|
||||||
|
takefocus=0, cursor="hand2",
|
||||||
|
).pack(side="left", padx=(0, 4))
|
||||||
|
|
||||||
|
size_label = tk.Label(
|
||||||
|
font_controls, text=f"{font_size[0]}pt",
|
||||||
|
bg=card_bg, fg=fg_primary,
|
||||||
|
font=("Helvetica", 11), width=4, anchor="center",
|
||||||
)
|
)
|
||||||
|
size_label.pack(side="left")
|
||||||
|
|
||||||
|
tk.Button(
|
||||||
|
font_controls, text="A+", command=lambda: _set_font_size(2),
|
||||||
|
bg=accent, fg=bg, activebackground=accent_hover, activeforeground=bg,
|
||||||
|
relief="flat", padx=10, pady=4, font=("Helvetica", 12, "bold"),
|
||||||
|
takefocus=0, cursor="hand2",
|
||||||
|
).pack(side="left", padx=(4, 0))
|
||||||
|
|
||||||
|
tk.Button(
|
||||||
|
toolbar, text="\u2715",
|
||||||
|
command=lambda: self._close_explanation(generation),
|
||||||
|
bg=card_bg, fg=fg_secondary,
|
||||||
|
activebackground=accent_hover, activeforeground=bg,
|
||||||
|
relief="flat", padx=8, pady=4, font=("Helvetica", 13),
|
||||||
|
takefocus=0, cursor="hand2",
|
||||||
|
).grid(row=0, column=3, sticky="e", padx=(12, 0))
|
||||||
|
|
||||||
|
# ── Sentence text (source) ────────────────────────────────────────
|
||||||
|
sentence_card = tk.Frame(frame, bg=card_bg, padx=12, pady=10)
|
||||||
|
sentence_card.grid(row=1, column=0, sticky="ew", pady=(0, 10))
|
||||||
|
sentence_card.columnconfigure(0, weight=1)
|
||||||
|
tk.Label(
|
||||||
|
sentence_card,
|
||||||
|
text="当前句子",
|
||||||
|
bg=card_bg,
|
||||||
|
fg=fg_secondary,
|
||||||
|
font=("Helvetica", 10),
|
||||||
|
).grid(row=0, column=0, sticky="w")
|
||||||
|
sentence_label = tk.Label(
|
||||||
|
sentence_card,
|
||||||
|
text=sentence_text,
|
||||||
|
wraplength=940,
|
||||||
|
justify="left",
|
||||||
|
bg=card_bg,
|
||||||
|
fg=fg_primary,
|
||||||
|
font=("Helvetica", font_size[0], "bold"),
|
||||||
|
)
|
||||||
|
sentence_label.grid(row=1, column=0, sticky="ew", pady=(4, 0))
|
||||||
|
|
||||||
|
# ── Explanation text area ──────────────────────────────────────────
|
||||||
text = tk.Text(
|
text = tk.Text(
|
||||||
frame,
|
frame,
|
||||||
wrap="word",
|
wrap="word",
|
||||||
relief="flat",
|
relief="flat",
|
||||||
background="#f6f6f6",
|
bg=card_bg,
|
||||||
foreground="#111111",
|
fg=fg_primary,
|
||||||
insertbackground="#111111",
|
insertbackground=fg_primary,
|
||||||
selectbackground="#3f8efc",
|
selectbackground=accent,
|
||||||
selectforeground="#ffffff",
|
selectforeground=bg,
|
||||||
padx=10,
|
padx=14,
|
||||||
pady=10,
|
pady=12,
|
||||||
|
font=("Helvetica", font_size[0]),
|
||||||
|
borderwidth=0,
|
||||||
|
highlightthickness=0,
|
||||||
)
|
)
|
||||||
text.grid(row=1, column=0, sticky="nsew")
|
text.grid(row=2, column=0, sticky="nsew")
|
||||||
text.insert("1.0", "AI 正在讲解这句话…")
|
text.insert("1.0", "AI 正在讲解这句话…")
|
||||||
text.configure(state="disabled")
|
text.configure(state="disabled")
|
||||||
|
|
||||||
self._explanation_text = text
|
self._explanation_text = text
|
||||||
self._explanation_debug_var = tk.StringVar(
|
self._explanation_debug_var = tk.StringVar(
|
||||||
value=f"请求 #{generation};连接 15 秒 / 响应最长 180 秒;日志见“调试日志”。"
|
value=f'请求 #{generation};连接 15 秒 / 响应最长 180 秒;日志见\u201c调试日志\u201d。'
|
||||||
)
|
)
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
frame,
|
frame,
|
||||||
textvariable=self._explanation_debug_var,
|
textvariable=self._explanation_debug_var,
|
||||||
wraplength=440,
|
wraplength=940,
|
||||||
justify="left",
|
justify="left",
|
||||||
foreground="#666666",
|
foreground=fg_secondary,
|
||||||
).grid(row=2, column=0, sticky="ew")
|
background=bg,
|
||||||
ttk.Button(
|
).grid(row=3, column=0, sticky="ew", pady=(8, 0))
|
||||||
frame,
|
|
||||||
text="关闭",
|
window.bind("<Escape>", lambda _e: self._close_explanation(generation))
|
||||||
command=lambda: self._close_explanation(generation),
|
|
||||||
takefocus=0,
|
|
||||||
).grid(row=3, column=0, sticky="e", pady=(10, 0))
|
|
||||||
self._explanation_window = window
|
self._explanation_window = window
|
||||||
window.protocol("WM_DELETE_WINDOW", lambda: self._close_explanation(generation))
|
window.protocol("WM_DELETE_WINDOW", lambda: self._close_explanation(generation))
|
||||||
|
|
||||||
@@ -2532,7 +2690,6 @@ class MediaPlayerApp(tk.Tk):
|
|||||||
try:
|
try:
|
||||||
text.configure(state="normal")
|
text.configure(state="normal")
|
||||||
text.delete("1.0", "end")
|
text.delete("1.0", "end")
|
||||||
text.configure(foreground="#111111")
|
|
||||||
text.insert("1.0", explanation)
|
text.insert("1.0", explanation)
|
||||||
text.configure(state="disabled")
|
text.configure(state="disabled")
|
||||||
if error:
|
if error:
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ nvidia-smi
|
|||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y ffmpeg git curl
|
sudo apt install -y ffmpeg git curl
|
||||||
|
sudo apt install -y ninja-build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 安装 uv
|
### 安装 uv
|
||||||
@@ -89,8 +90,20 @@ source .venv/bin/activate
|
|||||||
```bash
|
```bash
|
||||||
uv pip install -e ".[torch-runtime]" --torch-backend=auto
|
uv pip install -e ".[torch-runtime]" --torch-backend=auto
|
||||||
uv pip install sglang-omni
|
uv pip install sglang-omni
|
||||||
|
uv pip install ninja
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> `ninja` 是 FlashInfer JIT 编译 CUDA kernel 所需的构建工具,缺少它会在启动时
|
||||||
|
> 报 `FileNotFoundError: 'ninja'`。
|
||||||
|
>
|
||||||
|
> 必须用 `apt install ninja-build` 安装系统级 ninja(`/usr/bin/ninja`)。
|
||||||
|
> `uv pip install ninja` 只安装到虚拟环境 `.venv/bin/`,systemd 服务的 PATH
|
||||||
|
> 不包含它。如果只能用 venv 里的 ninja,在 systemd 服务的 `[Service]` 段加:
|
||||||
|
>
|
||||||
|
> ```ini
|
||||||
|
> Environment=PATH=/opt/moss-transcribe/MOSS-Transcribe-Diarize/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
> ```
|
||||||
|
|
||||||
### 下载模型
|
### 下载模型
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -111,8 +124,6 @@ sgl-omni serve \
|
|||||||
--model-path OpenMOSS-Team/MOSS-Transcribe-Diarize \
|
--model-path OpenMOSS-Team/MOSS-Transcribe-Diarize \
|
||||||
--host 127.0.0.1 \
|
--host 127.0.0.1 \
|
||||||
--port 8001 \
|
--port 8001 \
|
||||||
--max-running-requests 4 \
|
|
||||||
--cuda-graph-max-bs 4 \
|
|
||||||
--mem-fraction-static 0.35
|
--mem-fraction-static 0.35
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -122,9 +133,7 @@ sgl-omni serve \
|
|||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
| `--host` | `127.0.0.1` | 只监听本机,不要暴露公网 |
|
| `--host` | `127.0.0.1` | 只监听本机,不要暴露公网 |
|
||||||
| `--port` | `8001` | 服务端口(Whisper 用 9000,避免冲突) |
|
| `--port` | `8001` | 服务端口(Whisper 用 9000,避免冲突) |
|
||||||
| `--max-running-requests` | `4` | 最大并发请求数;显存充足可调大 |
|
| `--mem-fraction-static` | 自动 | 静态显存分配比例(作用于所有 SGLang engine stage);与其他 GPU 服务共存时调低 |
|
||||||
| `--cuda-graph-max-bs` | `4` | CUDA Graph 批大小上限,与并发数一致 |
|
|
||||||
| `--mem-fraction-static` | `0.80` | 静态显存分配比例;与其他 GPU 服务共存时调低 |
|
|
||||||
|
|
||||||
> 与 llama-server、Whisper 等其他 GPU 服务共存时,将
|
> 与 llama-server、Whisper 等其他 GPU 服务共存时,将
|
||||||
> `--mem-fraction-static` 设为 `0.30`–`0.40`(约 7–10 GB),避免 OOM。
|
> `--mem-fraction-static` 设为 `0.30`–`0.40`(约 7–10 GB),避免 OOM。
|
||||||
@@ -352,8 +361,7 @@ curl http://127.0.0.1:8000/healthz # 确认 moss_configured 为 true
|
|||||||
| Speaches (Whisper) | 4 GB | 停掉后释放 |
|
| Speaches (Whisper) | 4 GB | 停掉后释放 |
|
||||||
| **MOSS + llama-server** | ~16 GB / 24 GB | 安全 |
|
| **MOSS + llama-server** | ~16 GB / 24 GB | 安全 |
|
||||||
|
|
||||||
独占 GPU 时可将 `--mem-fraction-static` 调到 `0.80`(约 19 GB),
|
独占 GPU 时可将 `--mem-fraction-static` 调到 `0.80`(约 19 GB),支持更多并发。
|
||||||
`--max-running-requests` 调到 `16`,支持更多并发。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ After=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=oraltrainer
|
User=lsm
|
||||||
WorkingDirectory=/opt/moss-transcribe
|
WorkingDirectory=/opt/moss-transcribe
|
||||||
Environment=HF_ENDPOINT=https://hf-mirror.com
|
Environment=HF_ENDPOINT=https://hf-mirror.com
|
||||||
ExecStart=/opt/moss-transcribe/.venv/bin/vllm serve OpenMOSS-Team/MOSS-Transcribe-Diarize --host 127.0.0.1 --port 8001 --trust-remote-code
|
ExecStart=/opt/moss-transcribe/.venv/bin/vllm serve OpenMOSS-Team/MOSS-Transcribe-Diarize --host 127.0.0.1 --port 8001 --trust-remote-code
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ After=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=oraltrainer
|
User=lsm
|
||||||
WorkingDirectory=/opt/moss-transcribe/MOSS-Transcribe-Diarize
|
WorkingDirectory=/opt/moss-transcribe/MOSS-Transcribe-Diarize
|
||||||
Environment=HF_ENDPOINT=https://hf-mirror.com
|
Environment=HF_ENDPOINT=https://hf-mirror.com
|
||||||
ExecStart=/opt/moss-transcribe/MOSS-Transcribe-Diarize/.venv/bin/sgl-omni serve \
|
ExecStart=/opt/moss-transcribe/MOSS-Transcribe-Diarize/.venv/bin/sgl-omni serve \
|
||||||
--model-path OpenMOSS-Team/MOSS-Transcribe-Diarize \
|
--model-path OpenMOSS-Team/MOSS-Transcribe-Diarize \
|
||||||
--host 127.0.0.1 \
|
--host 127.0.0.1 \
|
||||||
--port 8001 \
|
--port 8001 \
|
||||||
--max-running-requests 4 \
|
|
||||||
--cuda-graph-max-bs 4 \
|
|
||||||
--mem-fraction-static 0.35
|
--mem-fraction-static 0.35
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
|
|||||||
@@ -246,6 +246,59 @@ _ABBREVIATION_RE = re.compile(
|
|||||||
r"e\.g\.|i\.e\.|no\.|approx\.|fig\.|inc\.|ltd\.|jr\.|sr\.|u\.s\.|u\.k\.)"
|
r"e\.g\.|i\.e\.|no\.|approx\.|fig\.|inc\.|ltd\.|jr\.|sr\.|u\.s\.|u\.k\.)"
|
||||||
r"(?![A-Za-z0-9])"
|
r"(?![A-Za-z0-9])"
|
||||||
)
|
)
|
||||||
|
_SPEAKER_TAG_RE = re.compile(r"\[(S\d+)\]")
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_speaker_segments(
|
||||||
|
segment: TranscriptionSegment,
|
||||||
|
) -> List[TranscriptionSegment]:
|
||||||
|
"""Split a segment at inline ``[S01]`` speaker tags and strip the tags.
|
||||||
|
|
||||||
|
MOSS sometimes embeds speaker tags directly in the text (rather than in
|
||||||
|
the ``speaker`` JSON field). Each tag marks the start of a new speaker
|
||||||
|
turn, so the text is split there and the tags are removed from the
|
||||||
|
visible transcription.
|
||||||
|
"""
|
||||||
|
text = segment.text
|
||||||
|
matches = list(_SPEAKER_TAG_RE.finditer(text))
|
||||||
|
if not matches:
|
||||||
|
return [segment]
|
||||||
|
|
||||||
|
sub_segments: List[TranscriptionSegment] = []
|
||||||
|
total = len(matches)
|
||||||
|
duration = segment.end_seconds - segment.start_seconds
|
||||||
|
for index, match in enumerate(matches):
|
||||||
|
start_pos = match.end()
|
||||||
|
if index + 1 < total:
|
||||||
|
end_pos = matches[index + 1].start()
|
||||||
|
else:
|
||||||
|
end_pos = len(text)
|
||||||
|
piece = text[start_pos:end_pos].strip()
|
||||||
|
if not piece:
|
||||||
|
continue
|
||||||
|
# Allocate time proportionally across sub-segments.
|
||||||
|
frac_start = index / total
|
||||||
|
frac_end = (index + 1) / total
|
||||||
|
sub_start = segment.start_seconds + duration * frac_start
|
||||||
|
sub_end = segment.start_seconds + duration * frac_end
|
||||||
|
words = None
|
||||||
|
if segment.words:
|
||||||
|
sub_words = [
|
||||||
|
w
|
||||||
|
for w in segment.words
|
||||||
|
if w.start_seconds >= sub_start and w.end_seconds <= sub_end
|
||||||
|
]
|
||||||
|
words = sub_words or None
|
||||||
|
sub_segments.append(
|
||||||
|
TranscriptionSegment(
|
||||||
|
start_seconds=sub_start,
|
||||||
|
end_seconds=sub_end,
|
||||||
|
text=piece,
|
||||||
|
speaker=match.group(1),
|
||||||
|
words=words,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sub_segments or [segment]
|
||||||
|
|
||||||
|
|
||||||
def split_sentences_at_punctuation(
|
def split_sentences_at_punctuation(
|
||||||
@@ -257,14 +310,18 @@ def split_sentences_at_punctuation(
|
|||||||
across segments and a sentence is closed at periods/question marks. Commas
|
across segments and a sentence is closed at periods/question marks. Commas
|
||||||
are also eligible boundaries, but only when the text before the comma has
|
are also eligible boundaries, but only when the text before the comma has
|
||||||
enough words; otherwise the comma stays with the following text.
|
enough words; otherwise the comma stays with the following text.
|
||||||
|
|
||||||
|
Inline ``[S01]`` speaker tags are stripped and each speaker turn always
|
||||||
|
starts a new sentence, even when no sentence-ending punctuation exists.
|
||||||
"""
|
"""
|
||||||
sentences: List[TranscriptionSegment] = []
|
sentences: List[TranscriptionSegment] = []
|
||||||
pieces: List[str] = []
|
pieces: List[str] = []
|
||||||
start_seconds: Optional[float] = None
|
start_seconds: Optional[float] = None
|
||||||
last_segment_end: Optional[float] = None
|
last_segment_end: Optional[float] = None
|
||||||
|
current_speaker: Optional[str] = None
|
||||||
|
|
||||||
def flush(end_seconds: float) -> None:
|
def flush(end_seconds: float) -> None:
|
||||||
nonlocal pieces, start_seconds
|
nonlocal pieces, start_seconds, current_speaker
|
||||||
text = " ".join(piece for piece in pieces if piece).strip()
|
text = " ".join(piece for piece in pieces if piece).strip()
|
||||||
if text and start_seconds is not None and end_seconds > start_seconds:
|
if text and start_seconds is not None and end_seconds > start_seconds:
|
||||||
sentences.append(
|
sentences.append(
|
||||||
@@ -272,19 +329,31 @@ def split_sentences_at_punctuation(
|
|||||||
start_seconds=start_seconds,
|
start_seconds=start_seconds,
|
||||||
end_seconds=end_seconds,
|
end_seconds=end_seconds,
|
||||||
text=text,
|
text=text,
|
||||||
|
speaker=current_speaker,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
pieces = []
|
pieces = []
|
||||||
start_seconds = None
|
start_seconds = None
|
||||||
|
current_speaker = None
|
||||||
|
|
||||||
|
expanded: List[TranscriptionSegment] = []
|
||||||
|
for seg in segments:
|
||||||
|
expanded.extend(_expand_speaker_segments(seg))
|
||||||
|
|
||||||
for segment in sorted(
|
for segment in sorted(
|
||||||
segments, key=lambda item: (item.start_seconds, item.end_seconds)
|
expanded, key=lambda item: (item.start_seconds, item.end_seconds)
|
||||||
):
|
):
|
||||||
text = segment.text.strip()
|
text = segment.text.strip()
|
||||||
if not text:
|
if not text:
|
||||||
continue
|
continue
|
||||||
|
# Each speaker-tagged sub-segment always starts a new sentence,
|
||||||
|
# regardless of whether the speaker actually changed — MOSS emits
|
||||||
|
# a tag per phrase/line even for the same speaker.
|
||||||
|
if pieces and segment.speaker is not None:
|
||||||
|
flush(segment.start_seconds)
|
||||||
if not pieces:
|
if not pieces:
|
||||||
start_seconds = segment.start_seconds
|
start_seconds = segment.start_seconds
|
||||||
|
current_speaker = segment.speaker
|
||||||
seg_pieces: List[Tuple[str, Optional[str]]] = []
|
seg_pieces: List[Tuple[str, Optional[str]]] = []
|
||||||
cursor = 0
|
cursor = 0
|
||||||
for match in _PUNCTUATION_BOUNDARY.finditer(text):
|
for match in _PUNCTUATION_BOUNDARY.finditer(text):
|
||||||
@@ -322,6 +391,7 @@ def split_sentences_at_punctuation(
|
|||||||
end_seconds = ends[index] if index < len(ends) else segment.end_seconds
|
end_seconds = ends[index] if index < len(ends) else segment.end_seconds
|
||||||
flush(end_seconds)
|
flush(end_seconds)
|
||||||
start_seconds = end_seconds
|
start_seconds = end_seconds
|
||||||
|
current_speaker = segment.speaker
|
||||||
if pieces:
|
if pieces:
|
||||||
flush(last_segment_end if last_segment_end is not None else start_seconds)
|
flush(last_segment_end if last_segment_end is not None else start_seconds)
|
||||||
return sentences
|
return sentences
|
||||||
|
|||||||
Reference in New Issue
Block a user