Opera Accessible Player
🎬 字幕翻译 + 🤟 AI手语翻译

点击上传戏曲视频 或拖拽到此处

支持 MP4 / WebM / MOV 格式

技术原理与算法设计

系统流程图

戏曲视频上传 / URL 载入
— 字幕提取流水线 —
OpenCV 抽帧
每 30 帧采样 · 帧号精确定位
字幕区域裁剪
底部 18% (y:0.78–0.96, x:0.05–0.95)
PP-OCRv5 文字识别
server 检测+识别模型 · 置信度>0.5
VSE 双阶段去重
同帧合并 + Levenshtein 相似度归组
字幕列表(文本+时间戳)
— 手语翻译流水线 —
字幕文本
jieba 中文分词
271 手语词库自动注册为自定义词
词在 GESTURE_LIBRARY 中?
直接取手势 逐字分解 → Unicode 种子确定性回退
关键帧生成
保持帧 + 25% 过渡插值 · 序列展开
Three.js 3D 虚拟人演绎
RPM 骨骼 · 10fps · 肩/肘/腕/五指逐关节
字幕 ↔ 手语实时联动 + googletrans 中英双语字幕

系统采用双流水线并行架构:字幕提取流水线基于 OpenCV 按帧号跳帧采样,裁剪戏曲视频底部 18% 的固定字幕区域, 送入 PP-OCRv5(server 级检测+识别模型)识别文字,再经 VSE(Video Subtitle Extractor)双阶段去重算法合并连续相同字幕。 手语翻译流水线将字幕文本经 jieba 分词后,在 271 词的中国手语(CSL)手势库中匹配——未命中时逐字分解, 最终以 Unicode 码点为随机种子生成确定性回退手势(同一文字永远得到同一手势)。匹配结果经关键帧生成器展开为 含保持帧与 25% 过渡插值的动画序列,驱动 Three.js 3D 虚拟人(RPM 骨骼装配)以 10fps 实时演绎。 两条流水线通过字幕时间戳同步,实现"唱词出现 → 虚拟人即时手语"的实时联动,同时经 googletrans 输出英文双语字幕。

软件基础设置信息

项目名称京韵 · 戏曲无障碍播放器(Opera Accessible Player)
适用场景戏曲视频(京剧/越剧等)字幕自动识别与 AI 手语传译,服务听障人群的戏曲无障碍观看
开发语言Python 3.10+(后端)、JavaScript ES Module(前端)
后端框架FastAPI + Uvicorn(端口 5015,systemd 托管)
核心依赖PaddleOCR(PP-OCRv5)、OpenCV、jieba、python-Levenshtein、googletrans
3D 渲染Three.js 0.160(importmap CDN 加载)+ RPM 骨骼装配 + AvatarEngine v3
手语数据国家通用手语词典(shouyu.bmcx.com),271 个 CSL 词汇,含戏曲专有词
部署方式Ubuntu 22.04 + systemd + nginx 反代(/opera/ → 127.0.0.1:5015),可选 PyInstaller exe 打包

软件系统设计

  1. 字幕区域定位与抽帧引擎:戏曲字幕位置固定,系统将检测区域裁剪为画面底部 18%(y 0.78–0.96、x 0.05–0.95),排除舞台画面干扰。基于 CAP_PROP_POS_FRAMES 帧号定位每 30 帧跳帧采样(无需逐帧解码全部帧),在识别准确率与处理速度间取得平衡;字幕时间戳 = 帧号 ÷ FPS 精确计算。
  2. VSE 双阶段字幕去重:第一阶段同帧文本合并——同帧多行识别结果经 OrderedDict 归组、NFKC 归一化后拼接;第二阶段跨帧去重——以 Levenshtein 相似度 0.80 为阈值,连续相似帧归为一组字幕,取组内最长文本为最优结果(消除 OCR 单帧误识别),并记录起止帧号。
  3. PP-OCRv5 模型管理:采用 PP-OCRv5 server 级检测+识别模型。模型目录经 ASCII 安全缓存(PADDLE_MODEL_CACHE)拷贝到纯英文路径,规避 PaddlePaddle 对中文路径的兼容性问题;模型名从 inference.yml 动态读取,无模型时自动降级为内置 lang='ch' 模型。
  4. CSL 手语手势引擎(gesture_engine):按国家通用手语五要素编码每个手势——手形(五指卷曲度 0.0=伸直 ~ 1.0=握拳,22 种基础手形)、位置(8 个身体参照位)、移动(多姿势序列)、掌向(肩/肘/腕三关节旋转向量);支持单手、双手对称、主从手三种模式,271 词库每词含独特动作序列(接近弧线 → 主姿势 → 保持 → 返回弧线)。
  5. 文本→手势序列转换:jieba 分词时将词库中所有≥2字的词自动注册为自定义词,保证戏曲专有词(如"苏三""洪洞县")不被切碎。分词结果依次查库:精确匹配 → 大小写重试 → 多字词逐字分解合并 → 单字 Unicode 码点种子确定性回退。序列化时词与词之间插入 25% 时长的过渡插值帧(逐关节线性插值),首尾附加 rest 休息姿势。
  6. 3D 虚拟人实时联动渲染:前端以 importmap 方式加载 Three.js 0.160,RPM 骨骼装配的虚拟人由关键帧驱动(默认 10fps,每词保持 0.8s)。字幕时间轴推进时,播放器自动向虚拟人 iframe 推送对应文本的手语帧序列,实现字幕与手语同步;未收录词汇由确定性回退手势兜底,保证任意文本均可演绎。

参数配置

参数默认值说明
SUBTITLE_Y_RATIO_MIN/MAX0.78 / 0.96字幕区域纵向裁剪比例(画面底部 18%)
SUBTITLE_X_RATIO_MIN/MAX0.05 / 0.95字幕区域横向裁剪比例(居中 90% 宽度)
sample_interval30 帧抽帧采样间隔,约 1 秒/帧(25fps 视频)
text_rec_score_thresh0.5 (VSE) / 0.4 (简易)OCR 识别置信度过滤阈值
threshold_similarity0.80Levenshtein 相似度阈值,连续帧归组去重
fps(手势动画)10手语动画帧率,平衡流畅度与数据量
gesture_duration0.8 秒每个词汇手势的保持时长
transition_framesfps × 0.25词间过渡插值帧数(约 2–3 帧)
frames_per_gesturefps × 0.8单手势保持帧数(默认 8 帧)
上传限制500 MBnginx client_max_body_size,支持 MP4/WebM/MOV

关键算法实现

① 字幕区域裁剪与抽帧识别(vse_core.py)

# 按帧号跳帧采样,裁剪底部 18% 字幕区域
frame_no = 0
while frame_no < total_frames:
    cap.set(cv2.CAP_PROP_POS_FRAMES, frame_no)
    ret, frame = cap.read()
    if not ret:
        break

    # Crop subtitle region (bottom 18% of frame)
    y1 = int(frame_height * subtitle_y_min)   # 0.78
    y2 = int(frame_height * subtitle_y_max)   # 0.96
    x1 = int(frame_width  * subtitle_x_min)   # 0.05
    x2 = int(frame_width  * subtitle_x_max)   # 0.95
    roi = frame[y1:y2, x1:x2]

    results = list(ocr.predict_iter(roi))     # PP-OCRv5
    if results:
        res = results[0]
        texts = [t for t, s in zip(res["rec_texts"], res["rec_scores"])
                 if float(s) > 0.5]           # 置信度过滤
        if texts:
            raw_results.append((frame_no, f"({x1},{y1},{x2},{y2})",
                                " ".join(texts)))
    frame_no += sample_interval               # 每 30 帧采样一次

② VSE 跨帧去重(Levenshtein 相似度归组)

def _dedup(self, content_list):
    """连续相似帧归组,取组内最长文本(消除单帧误识别)"""
    items = [RawInfo(str(c[0]), c[2]) for c in content_list]
    unique_list = []
    idx_i = 0
    n = len(items)

    while idx_i < n:
        current = items[idx_i]
        start_frame = current.no
        idx_j = idx_i
        # 向后扫描:相似度 >= 0.80 的连续帧归入同一组
        while idx_j < n:
            if idx_j + 1 == n or ratio(
                current.content.replace(" ", ""),
                items[idx_j + 1].content.replace(" ", ""),
            ) < self.threshold_similarity:          # 0.80
                group = items[idx_i : idx_j + 1]
                # 组内取最长文本作为最优识别结果
                stripped = [it.content.replace(" ", "") for it in group]
                best_idx, _ = max(enumerate(stripped), key=lambda x: len(x[1]))
                unique_list.append((start_frame, items[idx_j].no,
                                    group[best_idx].content))
                idx_i = idx_j + 1
                break
            else:
                idx_j += 1
    return unique_list

③ CSL 手形编码(五指卷曲度 0–1)

# Finger curl values: 0.0 = fully straight, 1.0 = fully curled (fist)
HS = {
    # "B" — 平掌,五指并拢伸直
    "flat":      {"thumb": 0.15, "index": 0.0, "middle": 0.0, "ring": 0.0, "pinky": 0.0},
    # "A" — 握拳,拇指贴侧
    "fist":      {"thumb": 0.3,  "index": 1.0, "middle": 1.0, "ring": 1.0, "pinky": 1.0},
    # "V" — 食指中指伸出(胜利/二)
    "victory":   {"thumb": 0.7,  "index": 0.0, "middle": 0.0, "ring": 1.0, "pinky": 1.0},
    # "L" — 拇指+食指伸直成 L 形
    "l_shape":   {"thumb": 0.0,  "index": 0.0, "middle": 1.0, "ring": 1.0, "pinky": 1.0},
    # 点赞 — 拇指竖起
    "thumbs_up": {"thumb": 0.0,  "index": 1.0, "middle": 1.0, "ring": 1.0, "pinky": 1.0},
    # ILY — 拇指+食指+小指伸出("我爱你")
    "ily":       {"thumb": 0.0,  "index": 0.0, "middle": 1.0, "ring": 1.0, "pinky": 0.0},
}

# 每词手势还含位置(8 个身体参照位)与移动序列:
# 肩/肘/腕三关节旋转 [rx, ry, rz] + 五指卷曲度 + 序列姿势(接近→主姿势→返回)

④ 文本→手势序列(jieba 分词 + 确定性回退 + 插值)

def text_to_gesture_sequence(text, fps=10, gesture_duration=0.8):
    # 1. jieba 分词(词库词汇自动注册为自定义词)
    for word in GESTURE_LIBRARY:
        if len(word) >= 2:
            jieba.add_word(word)
    words = list(jieba.cut(text))

    # 2. 逐词查库:精确 → 大小写 → 逐字分解 → 确定性回退
    def gesture_for_token(token):
        if token in GESTURE_LIBRARY:
            return GESTURE_LIBRARY[token]
        chars = list(token)
        if len(chars) > 1:
            return _merge_gestures([_gesture_for_char(c) for c in chars])
        return _gesture_for_char(chars[0])

    def _gesture_for_char(ch):
        # 未收录汉字:以 Unicode 码点为种子生成确定性手势
        seed_val = sum(ord(c) for c in ch)      # 同一文字永远同一手势
        rng = random.Random(seed_val)
        shapes = ["flat","five","point","fist","c_shape","pinch", ...]
        refs = [_FWD_CHEST, _CHEST, _HEAD, _FOREHEAD, _MOUTH, ...]
        return {"left": _rest(),
                "right": {**rng.choice(refs), "fingers": F[rng.choice(shapes)]}}

    # 3. 关键帧生成:保持帧 + 25% 过渡插值
    for i in range(len(gesture_frames) - 1):
        g_from, g_to = gesture_frames[i], gesture_frames[i + 1]
        for f in range(hold_frames):                       # 保持当前手势
            keyframes.append(_frame_dict(current_frame + f, g_from))
        current_frame += hold_frames
        for f in range(transition_frames):                 # 词间过渡
            t = (f + 1) / (transition_frames + 1)
            keyframes.append(_frame_dict(current_frame + f,
                             _interp_gesture(g_from, g_to, t)))  # 逐关节线性插值
        current_frame += transition_frames

⑤ 后端 API 接口一览(main.py)

POST /api/extract-subtitles   上传视频 → 字幕提取(VSE 引擎,失败自动降级简易 OCR)
POST /api/ocr-image           单帧图片 OCR(字幕区域识别)
POST /api/translate           中译英(googletrans,失败回退戏曲词典)
POST /api/segment-text        jieba 分词 + 词库命中/未命中统计
POST /api/sign-language       文本 → 手语关键帧序列({frame, left, right})
POST /api/csl-translate       中文词 → CSL 手语 ID 映射(精确→映射→逐字三级查找)
GET  /api/health              健康检查(含翻译服务可用状态)
GET  /                        前端页面(禁缓存)· /css /js /assets /avatar 静态挂载