- 新增 /api/hashtag/related:从作品描述实时解析 hashtag,按「同一作品里一起出现」计算相关 tag - 打分取子话题(候选多与目标同现)与上位话题(目标多与热门候选同现)两者较大值, 过滤 cos、二次元等万金油 tag 与平台活动、纯数字 tag - 播放页侧栏的 hashtag 列表顶部显示相关 tag,点击在侧栏内预览(不打断当前播放), 在预览里点作品才切换到该 tag 的列表 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This is a Next.js project bootstrapped with create-next-app.
Getting Started
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
Open http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.tsx. The page auto-updates as you edit the file.
This project uses local system font stacks so production builds do not depend on fetching Google Fonts.
Learn More
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
Deploy on Vercel
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.
视频文字转写(DashScope ASR)
仅使用 qwen3-asr-flash-filetrans 做文字识别,不使用通用模型理解、分类、总结或润色。新视频保存后入队;历史文字保留,后台可单条重转或批量补转。关闭页面不影响已入队任务。
处理流程:从 MinIO 将原视频流式下载到临时文件 → ffprobe 检查音轨及实际时长 → ffmpeg 提取整段 16 kHz 单声道、64 kbps MP3 → 上传至 MinIO → 将 音频 URL 提交到 DashScope → 后台取回文字。没有分块、视频上传到模型或二次模型调用。无音轨直接保存空文字;ASR 空结果显示“未识别到文字”。
配置与启动
参考 .env.example。模型配置统一为:
DASHSCOPE_API_KEY=你的百炼Key
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
DASHSCOPE_ASR_MODEL=qwen3-asr-flash-filetrans
这是 DashScope 原生 API 地址,不是 /compatible-mode/v1。Key 和端点地域必须匹配。也可使用北京业务空间域名的 /api/v1 地址。旧 OPENAI_*、QWEN_* 和转写模块的全局代理配置不再使用;已有 Omni 的百炼 Key 可用于 ASR,仍需账号开通相应模型权限。
需要 ffmpeg、ffprobe(可通过 FFMPEG_PATH、FFPROBE_PATH 指定)。MINIO_ENDPOINT/端口/SSL 必须是运行机器可鉴权读写的 S3 API;MINIO_PUBLIC_DOMAIN 必须让 DashScope 能下载所上传的音频。执行器不修改 Bucket 策略。音频存储于 transcription-audio/<视频ID>/<时间戳>.mp3;独立对象防止手动重试覆盖仍在使用的音频。可为该前缀配置不少于 7 天的生命周期,视频原文件不受影响。本地视频临时文件在成功或失败后都会清理。
npm ci
npx prisma generate
npm run dev
# 另一个终端(开发与生产均需执行器)
npm run stt:worker
生产使用 pm2 startOrReload pm2.config.cjs --env production --update-env,配置同时包含 DouyinArchive 和 DouyinArchive-ASR。全局最多两个处理中任务,每 10 秒查一次供应商状态。任务保存在 PostgreSQL,不需要 Redis。未配置 Key 不阻止网站启动,转写会显示配置错误。
数据库迁移与发布顺序
- 备份现有数据库,保存旧版本用于回退。
- 在停止服务前安装依赖、生成 Prisma Client 并运行
npm run build。 - 停止旧网站及旧后台执行器,运行
npx prisma migrate deploy。 - 用上述 PM2 命令启动新网站和 ASR 执行器,检查两者日志;确认转写与搜索后再完成发布验收。
迁移保留 VideoTranscript 的历史文字、关联和时间字段;删除 speech_detected、language、audio_type、non_speech_summary,以及部分部署存在的 content_tags。不自动补转或重转历史视频。新建 TranscriptionTask 保存状态、任务 ID、租约和重试信息。重转成功时原子替换文字,失败保留旧文字。
迁移会替换已知的旧全文检索触发器,并维护 transcript_zh_tsv 与 GIN 索引。已有 public.zhcfg 中文分词配置会保留;新库没有该配置时创建 simple 回退配置,它不提供中文分词。新部署若需要与旧库相同的中文搜索效果,先安装同样的中文分词扩展和 zhcfg;后续调整分词配置后应重新计算现有索引字段。不要在迁移后直接回滚到依赖旧字段的应用;需要回退时恢复数据库备份和旧应用版本。
接口和任务行为
POST /api/stt,JSON{ "awemeId": "视频ID" }:首次转写或手动重转;处理中重复点击复用当前任务。GET /api/stt?awemeId=视频ID:只读取{ status, error, transcript },不触发转写。transcript为string[] | null;null表示没有完成记录,[]表示完成但没有文字。POST /api/stt/batch:一次将所有没有完成记录、且未在排队或处理中的视频加入数据库队列,失败记录可补转,历史完成记录不重转。- 状态为
PENDING(尚未入队)、QUEUED、PROCESSING、SUCCEEDED、FAILED。后台显示各状态数量、错误和重试按钮。
音频限制为 12 小时、2 GB,超限失败而非截断。执行器重启后复用已有供应商任务 ID;租约到期可恢复领取。提交前写入标记,提交阶段崩溃或超时无法确认任务 ID 时,标为失败并要求核对百炼任务后手动重试,避免盲目重复付费。查询和结果下载的网络错误/429/5xx 按 10 秒至 5 分钟退避重试;鉴权和明确的供应商失败不自动重提。超过 24 小时的任务标为失败。
验证
npm run test:stt
# 必须是独立本地库 douyin_asr_test;此测试会重建该库的 public schema
TEST_DATABASE_URL=postgresql://你的本地用户名@127.0.0.1:5432/douyin_asr_test npm run test:stt:integration
npx tsc --noEmit
npm run build
部署后可用管理页测试短视频、超过 5 分钟的视频、无发言/无音轨视频。检查新归档自动入队、关页面继续处理、重启续查、失败保留旧文字及文字搜索。
也可对已归档的视频独立实测(会上传音频、调用付费 ASR;不修改数据库里的原转写):
npx tsx scripts/stt-smoke.ts 7682010237578136858
脚本将供应商任务 ID 和结果保存在 /tmp/douyin-asr-<视频ID>.json。已有未完成任务时应查询原任务,不要重复提交。
手动标签兼容
保留服务器版本的手动标签、首页批量打标签、详情页编辑及标签筛选;标签只来自 Video.manual_content_tags,不再回退到模型自动分类。转写成功、失败或重试都不会修改手动标签。后台转写列表支持手动标签筛选与批量添加/清空。
已有 /admin/asr、/admin/asr/videos、/api/admin/asr/videos 地址继续可用;/api/asr 与 /api/stt 一样,POST 入队、GET 只读。旧内存批处理和分块模型调用已移除。