https://www.voscreen.com/ 是一个非常好的学习英语的网站,请判断和总结它是怎样实现的?如果想复刻一个该网站,需要怎么做?
Voscreen 怎么工作的:
- 本质是 SPA 单页应用,用真实影视片段(10-15秒)配合选择题,形成「看视频 → 答题 → 听原声验证」的学习闭环
- 通过 游戏化积分 + 排行榜 + 连击机制 保持用户动力
- 内置 自适应难度系统(A1-C2),根据答题准确率动态调整推题
复刻的关键难点(按难度排序):
- 🔴 内容版权与题库:视频来源合法化 + 字幕题目生成(可用 Whisper + GPT 自动化)
- 🟡 视频播放体验:流畅的视频播放器 + 字幕渲染 + 无缝切换体验
- 🟢 技术实现:React + Node.js + PostgreSQL,难度中等,一个小团队 3-6 个月可出 MVP
关于内容的思考
内容方面确实是个大问题,我是这样考虑的:现在网上有很多m3u8视频源,首先收益一些m3u8视频源(url),对这些视频进行切片,选择里面方便学习的英语(只需数据库存储时间坐标,开始和结束两个时间点),然后再考虑进行字幕生成。
AI回答
这个思路非常聪明!用 m3u8 + 时间坐标的方式,完全避开了视频存储成本,只需存元数据。让我帮你深入分析这个方案的可行性和完整实现路径。
了解 m3u8 视频切片和时间戳提取技术方案
网页搜索m3u8 视频流 ffmpeg 按时间戳截取片段 字幕提取 python
了解 Whisper 对视频片段进行字幕识别的方案
网页搜索whisper transcribe m3u8 video segment timestamp subtitle extraction python
这个方案非常可行!让我给你完整梳理技术路线:
🎯 方案总体架构
m3u8 URL 池
↓
① 扫描器:分析视频,提取对话片段时间坐标
↓ (只存 start_time / end_time,不下载视频)
② 字幕生成器:Whisper 对片段音频识别
↓
③ 题目生成器:GPT 生成选择题
↓
④ 数据库:(m3u8_url, start, end, subtitle, choices[])
↓
⑤ 播放器:前端实时按时间坐标截取播放
核心优势:整个流程不需要存储一帧视频,存储成本极低!
🔧 Phase 1:视频分析 & 片段坐标提取
1.1 思路
不需要下载完整视频,只需要:
- 用 FFmpeg 直连 m3u8 流 → 提取音频
- 用 Whisper 分析音频 → 得到每句话的
[start, end, text] - 筛选出"适合学习"的句子(英语、时长 5~15 秒、语速适中)
1.2 核心脚本
python
复制
# clip_scanner.py - 扫描 m3u8,生成学习片段坐标
import subprocess
import json
import whisper
import tempfile
import os
def extract_audio_from_m3u8(m3u8_url: str, output_path: str, duration: int = 600):
"""直接从 m3u8 提取前 N 秒音频,不下载视频"""
cmd = [
"ffmpeg", "-y",
"-i", m3u8_url,
"-t", str(duration), # 只取前 10 分钟分析
"-vn", # 不要视频流
"-acodec", "pcm_s16le",
"-ar", "16000", # Whisper 需要 16kHz
"-ac", "1", # 单声道
output_path
]
subprocess.run(cmd, capture_output=True, timeout=120)
def scan_clips(m3u8_url: str, model_size: str = "medium"):
"""扫描 m3u8 视频,返回适合学习的片段列表"""
model = whisper.load_model(model_size)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
audio_path = f.name
try:
print(f"⏬ 提取音频: {m3u8_url}")
extract_audio_from_m3u8(m3u8_url, audio_path)
print("🎤 Whisper 识别中...")
result = model.transcribe(
audio_path,
language="en",
word_timestamps=True, # 精确到词级别时间戳
verbose=False
)
clips = []
for seg in result["segments"]:
duration = seg["end"] - seg["start"]
text = seg["text"].strip()
# 筛选条件:时长 5~18 秒,英语内容,有实质内容
if (5.0 <= duration <= 18.0
and len(text.split()) >= 5 # 至少 5 个词
and len(text.split()) <= 30 # 不超过 30 个词
and seg.get("no_speech_prob", 1) < 0.3): # 有语音
clips.append({
"m3u8_url": m3u8_url,
"start": round(seg["start"], 2),
"end": round(seg["end"], 2),
"text": text,
"words": seg.get("words", []), # 词级别时间戳
"avg_logprob": seg.get("avg_logprob", 0), # 识别置信度
})
print(f"✅ 找到 {len(clips)} 个适合学习的片段")
return clips
finally:
os.unlink(audio_path) # 清理临时音频文件
🔧 Phase 2:数据库设计
sql
复制
-- clips 表:核心片段元数据
CREATE TABLE clips (
id BIGSERIAL PRIMARY KEY,
m3u8_url TEXT NOT NULL, -- 视频源 URL
start_time DECIMAL(10,2) NOT NULL, -- 开始时间(秒)
end_time DECIMAL(10,2) NOT NULL, -- 结束时间(秒)
subtitle TEXT NOT NULL, -- Whisper 识别的英文字幕
-- 学习元数据
level VARCHAR(2), -- A1/A2/B1/B2/C1/C2
word_count SMALLINT,
duration DECIMAL(5,2), -- end - start
-- 状态
status VARCHAR(20) DEFAULT 'pending', -- pending/active/inactive
created_at TIMESTAMP DEFAULT NOW()
);
-- choices 表:选择题选项
CREATE TABLE clip_choices (
id BIGSERIAL PRIMARY KEY,
clip_id BIGINT REFERENCES clips(id),
is_correct BOOLEAN NOT NULL,
text TEXT NOT NULL, -- 选项文本(翻译或近义句)
lang VARCHAR(10) DEFAULT 'zh' -- 题目语言
);
-- m3u8_sources 表:视频源管理
CREATE TABLE m3u8_sources (
id BIGSERIAL PRIMARY KEY,
url TEXT UNIQUE NOT NULL,
name TEXT, -- 来源名称(如"老友记 S01E01")
category TEXT, -- drama/movie/documentary/talk
is_active BOOLEAN DEFAULT TRUE,
last_scanned_at TIMESTAMP,
total_clips INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
-- 用户观看记录
CREATE TABLE user_clip_history (
user_id BIGINT,
clip_id BIGINT,
answered BOOLEAN,
is_correct BOOLEAN,
answered_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (user_id, clip_id)
);
🔧 Phase 3:GPT 自动生成选择题
python
复制
# question_generator.py
from openai import OpenAI
client = OpenAI()
def generate_choices(subtitle: str, target_lang: str = "zh") -> list[dict]:
"""
为字幕文本生成两个翻译选项(一对一错)
也可以生成"意思相近的英文句子"(更高级)
"""
prompt = f"""
你是英语学习题目生成专家。
给定一段英语对话原句,请生成两个中文翻译选项:
- 一个是正确翻译
- 一个是错误翻译(意思相近但有关键差异,不能太明显)
原句:"{subtitle}"
请返回 JSON 格式:
{{
"correct": "正确的中文翻译",
"distractor": "错误的中文翻译(意思相近但不准确)"
}}
要求:
1. 错误选项不能太离谱,要让人容易混淆
2. 翻译要口语化、自然
3. 只返回 JSON,不要其他内容
"""
resp = client.chat.completions.create(
model="gpt-4o-mini", # 便宜又够用
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0.7
)
data = json.loads(resp.choices[0].message.content)
return [
{"text": data["correct"], "is_correct": True},
{"text": data["distractor"], "is_correct": False},
]
🔧 Phase 4:前端播放器(关键)
前端只需要做到:按时间坐标播放 m3u8 的某一段
typescript
复制
// VideoClipPlayer.tsx
import { useEffect, useRef } from 'react'
import Hls from 'hls.js' // npm install hls.js
interface ClipPlayerProps {
m3u8Url: string
startTime: number // 秒
endTime: number // 秒
muted?: boolean // 是否静音(答题前静音)
onEnded?: () => void
}
export function VideoClipPlayer({ m3u8Url, startTime, endTime, muted, onEnded }: ClipPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null)
const hlsRef = useRef<Hls | null>(null)
useEffect(() => {
const video = videoRef.current
if (!video) return
if (Hls.isSupported()) {
const hls = new Hls({
startPosition: startTime, // ⭐ 直接从指定位置开始加载
})
hlsRef.current = hls
hls.loadSource(m3u8Url)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.currentTime = startTime
video.play()
})
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 原生支持 HLS
video.src = m3u8Url
video.currentTime = startTime
video.play()
}
return () => {
hlsRef.current?.destroy()
}
}, [m3u8Url, startTime])
useEffect(() => {
const video = videoRef.current
if (!video) return
const checkEnd = () => {
if (video.currentTime >= endTime) {
video.pause()
onEnded?.()
}
}
video.addEventListener('timeupdate', checkEnd)
return () => video.removeEventListener('timeupdate', checkEnd)
}, [endTime, onEnded])
return (
<video
ref={videoRef}
muted={muted}
playsInline
className="w-full rounded-xl"
/>
)
}
🔧 Phase 5:自动化扫描流水线
python
复制
# pipeline.py - 全自动处理流水线
import asyncio
from database import db
async def process_source(source_url: str):
"""完整处理一个 m3u8 源的流水线"""
# Step 1: 扫描片段
clips = scan_clips(source_url)
# Step 2: 自动评估难度
for clip in clips:
clip['level'] = estimate_level(clip['text'])
clip['word_count'] = len(clip['text'].split())
clip['duration'] = clip['end'] - clip['start']
# Step 3: 批量入库
clip_ids = await db.batch_insert_clips(clips)
# Step 4: 批量生成题目(调用 GPT)
for clip_id, clip in zip(clip_ids, clips):
choices = generate_choices(clip['text'])
await db.insert_choices(clip_id, choices)
print(f"✅ 完成:{len(clips)} 个片段已入库")
def estimate_level(text: str) -> str:
"""简单的难度估算(可以用更复杂的模型)"""
words = text.split()
avg_word_len = sum(len(w) for w in words) / len(words)
word_count = len(words)
if word_count <= 8 and avg_word_len < 5: return "A1"
elif word_count <= 12 and avg_word_len < 6: return "A2"
elif word_count <= 16: return "B1"
elif word_count <= 22: return "B2"
elif word_count <= 28: return "C1"
else: return "C2"
# 批量处理多个源
async def main():
sources = [
"https://example.com/friends_s01e01.m3u8",
"https://example.com/ted_talk_01.m3u8",
# ... 更多源
]
for url in sources:
await process_source(url)
await asyncio.sleep(5) # 避免过于频繁
if __name__ == "__main__":
asyncio.run(main())
⚠️ 几个关键技术细节
问题1:m3u8 URL 会失效怎么办?
python
复制
# 定时健康检查
async def health_check():
sources = await db.get_all_active_sources()
for source in sources:
try:
# 只检查能不能连上,不下载内容
result = subprocess.run(
["ffprobe", "-v", "quiet", "-i", source.url],
timeout=10
)
if result.returncode != 0:
await db.mark_source_inactive(source.id)
except:
await db.mark_source_inactive(source.id)
建议:同一内容存多个备用 URL,任一失效自动切换
问题2:m3u8 seek 精度问题
m3u8 是分段文件(每段通常 2-10 秒),seek 到非关键帧会不准确。
python
复制
# 存储时记录最近的关键帧时间
def find_keyframe_time(m3u8_url: str, target_time: float) -> float:
"""找到 target_time 之前最近的关键帧"""
result = subprocess.run([
"ffprobe", "-v", "quiet",
"-select_streams", "v",
"-show_frames",
"-read_intervals", f"%+#30", # 分析前30帧
"-show_entries", "frame=pkt_pts_time,key_frame",
"-of", "csv=p=0",
"-ss", str(max(0, target_time - 5)),
"-i", m3u8_url
], capture_output=True, text=True, timeout=30)
# 找最近的关键帧
keyframes = [
float(line.split(',')[1])
for line in result.stdout.strip().split('\n')
if line.startswith('1,') # key_frame=1
]
if keyframes:
return min(keyframes, key=lambda x: abs(x - target_time))
return target_time
问题3:Whisper 幻觉问题
Whisper 有时会在静音段"幻觉"出文字,用 no_speech_prob 过滤:
python
复制
# 只保留高置信度的识别结果
if seg["no_speech_prob"] < 0.3 and seg["avg_logprob"] > -0.8:
# 这个片段识别结果可信
📊 数据规模估算
| 处理量 | 时间 | 存储 |
|---|---|---|
| 1小时视频 | Whisper 约 10-20 分钟 | 约 200-500 个片段 |
| GPT 题目生成 | 约 0.01元/条 | - |
| 数据库存储 | - | 1万片段 ≈ 5MB(极小!) |
100 部美剧 × 每集 20 分钟 × 每分钟 3 个片段 = 约 6000 个片段,成本极低。
🗺️ 建议开发顺序
Week 1: 搭数据库 + 写 clip_scanner.py + 手动测试几个 m3u8
Week 2: 写题目生成器 + 数据入库 pipeline
Week 3: 前端播放器 + 基础学习界面
Week 4: 用户系统 + 积分 + 打磨体验
这个方案的精妙之处在于:数据库里只存 URL + 两个时间戳 + 字幕文字,视频完全托管在原始服务器,自己的服务器压力极小,理论上可以支撑海量内容。想先动手验证哪个环节?我可以帮你写完整可运行的代码。