add sentence service
This commit is contained in:
@@ -38,6 +38,8 @@ from pathlib import Path
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from sentence_analysis import detect_sentence_boundaries
|
||||
|
||||
try:
|
||||
from PIL import Image as PILImage, ImageTk
|
||||
except ImportError:
|
||||
@@ -71,11 +73,6 @@ LOOP_LABELS = {"off": "循环: 关", "one": "循环: 单曲", "list": "循环:
|
||||
SPEED_PRESETS = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]
|
||||
SEEK_STEP = 10
|
||||
SEEK_STEP_BIG = 60
|
||||
SILENCE_FLOOR_PERCENTILE = 10 # 用能量低分位估计背景噪声底
|
||||
SILENCE_THRESHOLD_FACTOR = 1.5 # 静音阈值 = 噪声底 × 系数
|
||||
SILENCE_BRIDGE_GAP = 0.06 # 停顿中 60ms 内的短促残响/噪声也按静音处理
|
||||
|
||||
|
||||
def fmt_time(ms):
|
||||
"""把毫秒格式化为 mm:ss 或 h:mm:ss。"""
|
||||
if ms is None or ms < 0:
|
||||
@@ -88,96 +85,6 @@ def fmt_time(ms):
|
||||
return f"{minutes:02d}:{seconds:02d}"
|
||||
|
||||
|
||||
def detect_sentence_boundaries(path, min_silence=0.30, min_sentence=0.35):
|
||||
"""基于音频静音自动切分句子,返回每句话开始的时间(秒)列表。
|
||||
|
||||
语音之间通常有短暂停顿,把停顿前后的连续声音视为一句话。
|
||||
没有音频轨、无法解码或缺少依赖时返回 None。
|
||||
"""
|
||||
try:
|
||||
import av
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
return None
|
||||
try:
|
||||
container = av.open(str(path))
|
||||
audio = next((s for s in container.streams if s.type == "audio"), None)
|
||||
if audio is None:
|
||||
container.close()
|
||||
return None
|
||||
sample_rate = 16000
|
||||
resampler = av.AudioResampler(format="fltp", layout="mono", rate=sample_rate)
|
||||
window = 480 # 30ms @ 16kHz
|
||||
frame_seconds = window / sample_rate
|
||||
energies = []
|
||||
buf = []
|
||||
|
||||
def push(data):
|
||||
buf.append(data)
|
||||
total = sum(len(x) for x in buf)
|
||||
if total < window:
|
||||
return
|
||||
arr = np.concatenate(buf)
|
||||
buf.clear()
|
||||
n = len(arr) // window * window
|
||||
win = arr[:n].reshape(-1, window)
|
||||
energies.extend((np.mean(win * win, axis=1) ** 0.5).tolist())
|
||||
if len(arr) > n:
|
||||
buf.append(arr[n:])
|
||||
|
||||
for packet in container.demux(audio):
|
||||
for frame in packet.decode():
|
||||
for out in resampler.resample(frame):
|
||||
push(out.to_ndarray()[0])
|
||||
for out in resampler.resample(None):
|
||||
push(out.to_ndarray()[0])
|
||||
if buf:
|
||||
push(np.zeros(window, dtype=np.float32))
|
||||
duration = container.duration
|
||||
container.close()
|
||||
if len(energies) < 3:
|
||||
return None
|
||||
energies = np.asarray(energies, dtype=np.float64)
|
||||
floor = float(np.percentile(energies, 95))
|
||||
if floor <= 0.0:
|
||||
return None
|
||||
noise_floor = float(np.percentile(energies, SILENCE_FLOOR_PERCENTILE))
|
||||
threshold = max(0.008, SILENCE_THRESHOLD_FACTOR * noise_floor)
|
||||
silence = energies < threshold
|
||||
bridge_frames = int(round(SILENCE_BRIDGE_GAP / frame_seconds))
|
||||
if bridge_frames > 0:
|
||||
bridged = silence.copy()
|
||||
run_start = None
|
||||
for idx, is_silent in enumerate(silence):
|
||||
if not is_silent and run_start is None:
|
||||
run_start = idx
|
||||
elif is_silent and run_start is not None:
|
||||
if run_start > 0 and idx - run_start <= bridge_frames:
|
||||
bridged[run_start:idx] = True
|
||||
run_start = None
|
||||
silence = bridged
|
||||
boundaries = [0.0]
|
||||
run_start = None
|
||||
for idx, is_silent in enumerate(silence):
|
||||
if is_silent and run_start is None:
|
||||
run_start = idx
|
||||
elif not is_silent and run_start is not None:
|
||||
if (idx - run_start) * frame_seconds >= min_silence:
|
||||
end_sec = idx * frame_seconds
|
||||
if end_sec - boundaries[-1] >= min_sentence:
|
||||
boundaries.append(end_sec)
|
||||
run_start = None
|
||||
if run_start is not None and (len(silence) - run_start) * frame_seconds >= min_silence:
|
||||
end_sec = len(silence) * frame_seconds
|
||||
if end_sec - boundaries[-1] >= min_sentence:
|
||||
boundaries.append(end_sec)
|
||||
if duration and duration > 0:
|
||||
boundaries = [b for b in boundaries if b < duration / 1e6 - 0.1]
|
||||
return boundaries
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
class _BaseEngine:
|
||||
"""播放内核统一接口。"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user