From 85dc3c77b32f76ab47bd90e1888b5aeb511b9981 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Wed, 27 May 2026 16:08:33 +0800 Subject: [PATCH] Replace MinIO with S3-compatible storage and reorganize project structure - Switch from MinIO to boto3 for S3-compatible object storage (Cloudflare R2) - Rename storage config vars from R2_* to generic S3_* - Organize root directory: docs/, tools/, output/, Archive/{audio,results}/ - Output transcriptions to output/ directory - Add transcribe_legacy.py, transcribe_all.py, and docs Co-Authored-By: Claude Opus 4.6 --- .gitignore | 4 + docs/transcribe_legacy_flow.md | 77 ++++ docs/大模型录音文件识别标准版API.md | 602 ++++++++++++++++++++++++++++ requirements.txt | 1 + transcribe_all.py | 147 +++++++ transcribe_legacy.py | 161 ++++++++ 6 files changed, 992 insertions(+) create mode 100644 docs/transcribe_legacy_flow.md create mode 100644 docs/大模型录音文件识别标准版API.md create mode 100644 transcribe_all.py create mode 100644 transcribe_legacy.py diff --git a/.gitignore b/.gitignore index 25662b1..6d21059 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,7 @@ Desktop.ini # Output output/ +Archive/ + +# Tools +tools/ \ No newline at end of file diff --git a/docs/transcribe_legacy_flow.md b/docs/transcribe_legacy_flow.md new file mode 100644 index 0000000..1f97473 --- /dev/null +++ b/docs/transcribe_legacy_flow.md @@ -0,0 +1,77 @@ +# transcribe_legacy.py 流程说明 + +## 概述 + +该脚本实现音频文件的**异步转写**流程:上传音频 → 提交转写任务 → 轮询结果 → 保存输出。 + +## 流程图 + +```mermaid +flowchart TD + A[启动] --> B[加载 .env 环境变量] + B --> C[初始化 MinIO 客户端] + C --> D{命令行是否\n传入音频文件?} + D -- 是 --> E[使用传入的文件路径] + D -- 否 --> F[使用默认文件名] + E --> G[生成唯一对象名] + F --> G + G --> H[上传音频文件至 MinIO] + H --> I[生成 1 小时有效期的预签名 URL] + I --> J[构造 submit 请求体\n(模型/标点/说话人识别等)] + J --> K[调用 ByteDance 提交接口] + K --> L{提交状态码\n== 20000000?} + L -- 否 --> M[打印失败信息,结束] + L -- 是 --> N[进入轮询循环] + N --> O[调用 query 接口查询结果] + O --> P{状态码判断} + P -- "20000000\n(完成)" --> Q[解析 JSON 结果] + Q --> R[保存 .json 文件] + R --> S[构建 Markdown 转写报告] + S --> T[保存 .md 文件] + T --> U[打印完成信息] + P -- "20000001 / 20000002\n(处理中)" --> V[等待 5 秒] + V --> O + P -- 其他 --> W[打印失败信息] + U --> X[结束] + M --> X + W --> X +``` + +## 关键步骤详解 + +### 1. 环境初始化 + +- 从 `.env` 加载 API 凭证(App Key、Access Key、Resource ID) +- 加载 MinIO 对象存储的连接信息 + +### 2. 音频上传 + +- 将本地音频文件上传至 MinIO 的 `audio/` 前缀路径 +- 生成带 1 小时过期时间的预签名下载 URL,供远端 API 访问 + +### 3. 提交转写任务 + +- 向 ByteDance 大模型语音识别接口发送 POST 请求 +- 请求参数开启:ITN(逆文本归一化)、标点恢复、去冗余、说话人分离、分句输出 + +### 4. 轮询结果 + +- 每隔 5 秒查询一次任务状态 +- `20000001` / `20000002` = 处理中,继续等待 +- `20000000` = 成功,退出循环 + +### 5. 保存输出 + +| 文件 | 内容 | +|------|------| +| `{音频名}_转写结果.json` | 原始 API 返回的完整 JSON 数据 | +| `{音频名}_转写结果.md` | 格式化的 Markdown 报告(含时间线、说话人标注) | + +## 外部依赖 + +```mermaid +graph LR + A[transcribe_legacy.py] --> B[ByteDance 语音识别 API] + A --> C[MinIO 对象存储] + B -->|预签名 URL 下载音频| C +``` diff --git a/docs/大模型录音文件识别标准版API.md b/docs/大模型录音文件识别标准版API.md new file mode 100644 index 0000000..9601905 --- /dev/null +++ b/docs/大模型录音文件识别标准版API.md @@ -0,0 +1,602 @@ + +# 流程简介 +大模型录音文件识别服务的处理流程分为提交任务和查询结果两个阶段 +任务提交:提交音频链接,并获取服务端分配的任务 ID +结果查询:通过任务 ID 查询转写结果 + +
+ + + +# 提交任务 + +## 接口地址 +火山地址:https://openspeech.bytedance.com/api/v3/auc/bigmodel/submit + +## 请求 +请求方式:HTTP POST。 +请求和应答,均采用在 HTTP BODY 里面传输 JSON 格式字串的方式。 +Header 需要加入内容类型标识: +旧版本控制台 + +| | | | \ +|Key |说明 |Value 示例 | +|---|---|---| +| | | | \ +|X-Api-App-Key |使用火山引擎控制台获取的APP ID,可参考 [控制台使用FAQ-Q1](https://www.volcengine.com/docs/6561/196768#q1%EF%BC%9A%E5%93%AA%E9%87%8C%E5%8F%AF%E4%BB%A5%E8%8E%B7%E5%8F%96%E5%88%B0%E4%BB%A5%E4%B8%8B%E5%8F%82%E6%95%B0appid%EF%BC%8Ccluster%EF%BC%8Ctoken%EF%BC%8Cauthorization-type%EF%BC%8Csecret-key-%EF%BC%9F)(旧版控制台使用,新版控制台只需要X-Api-Key即可) |123456789 | +| | | | \ +|X-Api-Access-Key |使用火山引擎控制台获取的Access Token,可参考 [控制台使用FAQ-Q1](https://www.volcengine.com/docs/6561/196768#q1%EF%BC%9A%E5%93%AA%E9%87%8C%E5%8F%AF%E4%BB%A5%E8%8E%B7%E5%8F%96%E5%88%B0%E4%BB%A5%E4%B8%8B%E5%8F%82%E6%95%B0appid%EF%BC%8Ccluster%EF%BC%8Ctoken%EF%BC%8Cauthorization-type%EF%BC%8Csecret-key-%EF%BC%9F)(旧版控制台使用,新版控制台只需要X-Api-Key即可) |your-access-key | +| | | | \ +|X-Api-Resource-Id |表示调用服务的资源信息 ID |豆包录音文件识别模型1.0 |\ +| | | |\ +| | |* volc.bigasr.auc |\ +| | | |\ +| | |豆包录音文件识别模型2.0 |\ +| | | |\ +| | |* volc.seedasr.auc | +| | | | \ +|X-Api-Request-Id |用于提交和查询任务的任务ID,推荐传入随机生成的UUID |67ee89ba-7050-4c04-a3d7-ac61a63499b3 | +| | | | \ +|X-Api-Sequence |发包序号,固定值,-1 | | + +```Plain Text +headers = { + "X-Api-App-Key": appid, + "X-Api-Access-Key": token, + "X-Api-Resource-Id": "volc.seedasr.auc",//资源ID + "X-Api-Request-Id": task_id, + "X-Api-Sequence": "-1" +``` + +新版本控制台 + +| | | | \ +|Key |说明 |Value 示例 | +|---|---|---| +| | | | \ +|X-Api-Key |使用火山引擎控制台获取的APP Key,可参考 [快速入门(新版控制台)](https://console.volcengine.com/speech/new/setting/apikeys?projectName=default) |123456789 | +| | | | \ +|X-Api-Resource-Id |表示调用服务的资源信息 ID |豆包录音文件识别模型1.0 |\ +| | | |\ +| | |* volc.bigasr.auc |\ +| | | |\ +| | |豆包录音文件识别模型2.0 |\ +| | | |\ +| | |* volc.seedasr.auc | +| | | | \ +|X-Api-Request-Id |用于提交和查询任务的任务ID,推荐传入随机生成的UUID |67ee89ba-7050-4c04-a3d7-ac61a63499b3 | +| | | | \ +|X-Api-Sequence |发包序号,固定值,-1 | | + +```Plain Text +headers = { + "X-Api-Key": apikey, + "X-Api-Resource-Id": "volc.seedasr.auc",//资源ID + "X-Api-Request-Id": task_id, + "X-Api-Sequence": "-1" +} +``` + + +### 请求字段 + +| | | | | | | \ +|字段 |说明 |层级 |格式 |是否必填 |备注 | +|---|---|---|---|---|---| +| | | | | | | \ +|user |用户相关配置 |1 |dict | | | +| | | | | | | \ +|uid |用户标识 |2 |string | |建议采用 IMEI 或 MAC。 | +| | | | | | | \ +|audio |音频相关配置 |1 |dict |✓ | | +| | | | | | | \ +|url |音频链接 |2 |string |✓ | | +| | | | | | | \ +|language |指定可识别的语言 |2 |string | |当该键为空时,该模型支持**中英文、上海话、闽南语,四川、陕西、粤语**识别。当将其设置为下方特定键时,它可以识别指定语言。 |\ +| | | | | |```Python |\ +| | | | | |中文普通话 zh-CN |\ +| | | | | |英语:en-US |\ +| | | | | |日语:ja-JP |\ +| | | | | |印尼语:id-ID |\ +| | | | | |西班牙语:es-MX |\ +| | | | | |葡萄牙语:pt-BR |\ +| | | | | |德语:de-DE |\ +| | | | | |法语:fr-FR |\ +| | | | | |韩语:ko-KR |\ +| | | | | |菲律宾语:fil-PH |\ +| | | | | |马来语:ms-MY |\ +| | | | | |泰语:th-TH |\ +| | | | | |阿拉伯语 ar-SA |\ +| | | | | |意大利语 it-IT |\ +| | | | | |孟加拉语 bn-BD |\ +| | | | | |希腊语 el-GR |\ +| | | | | |荷兰语 nl-NL |\ +| | | | | |俄语 ru-RU |\ +| | | | | |土耳其语 tr-TR |\ +| | | | | |越南语 vi-VN |\ +| | | | | |波兰语 pl-PL |\ +| | | | | |罗马尼亚语 ro-RO |\ +| | | | | |尼泊尔语 ne-NP |\ +| | | | | |乌克兰语 uk-UA |\ +| | | | | |粤语 yue-CN |\ +| | | | | |``` |\ +| | | | | | |\ +| | | | | |例如,如果输入音频是德语,则此参数传入de-DE | +| | | | | | | \ +|format |音频容器格式 |2 |string |✓ |raw / wav / mp3 / ogg | +| | | | | | | \ +|codec |音频编码格式 |2 |string | |raw / opus,默认为 raw(pcm) 。 | +| | | | | | | \ +|rate |音频采样率 |2 |int | |默认为 16000。 | +| | | | | | | \ +|bits |音频采样点位数 |2 |int | |默认为 16,暂只支持16bits。 | +| | | | | | | \ +|channel |音频声道数 |2 |int | |1(mono) / 2(stereo),默认为1。 | +| | | | | | | \ +|request |请求相关配置 |1 |dict |✓ | | +| | | | | | | \ +|model_name |模型名称 |2 |string |✓ |目前只有bigmodel | +| | | | | | | \ +|ssd_version |ssd版本 |2 |string | |仅在enable_speaker_info = True开启说话人分离能力,且不指定*language*字段或者*language指定为"zh-CN" 时生效* |\ +| | | | | |示例: |\ +| | | | | |```Python |\ +| | | | | |ssd_version = "200" |\ +| | | | | |``` |\ +| | | | | | | +| | | | | | | \ +|enable_itn |启用itn |2 |bool | |默认为true。 |\ +| | | | | |文本规范化 (ITN) 是自动语音识别 (ASR) 后处理管道的一部分。 ITN 的任务是将 ASR 模型的原始语音输出转换为书面形式,以提高文本的可读性。 |\ +| | | | | |例如,“一九七零年”->“1970年”和“一百二十三美元”->“$123”。 | +| | | | | | | \ +|enable_punc |启用标点 |2 |bool | |默认为false。 | +| | | | | | | \ +|enable_ddc |启用顺滑 |2 |bool | |默认为false。 |\ +| | | | | |**++语义顺滑++**‌是一种技术,旨在提高自动语音识别(ASR)结果的文本可读性和流畅性。这项技术通过删除或修改ASR结果中的不流畅部分,如停顿词、语气词、语义重复词等,使得文本更加易于阅读和理解。 | +| | | | | | | \ +|enable_speaker_info |启用说话人聚类分离 |2 |bool | |默认为false,开启后可返回说话人的信息,10人以内,效果较好。 |\ +| | | | | |(如果音频存在音量、远近等明显变化,无法保证区分效果) | +| | | | | | | \ +|enable_channel_split |启用双声道识别 |2 |bool | |如果设为"True",则会在返回结果中使用channel_id标记,1为左声道,2为右声道。默认 "False"默认为false | +| | | | | | | \ +|show_utterances |输出语音停顿、分句、分词信息 |2 |bool | | | +| | | | | | | \ +|show_speech_rate |分句信息携带语速 |2 |bool | |如果设为"True",则会在分句additions信息中使用speech_rate标记,单位为 token/s。默认 "False" | +| | | | | | | \ +|show_volume |分句信息携带音量 |2 |bool | |如果设为"True",则会在分句additions信息中使用volume标记,单位为 分贝。默认 "False" | +| | | | | | | \ +|enable_lid |启用语种识别 |2 |bool | |**目前支持语种:中英文、上海话、闽南语,四川、陕西、粤语** |\ +| | | | | |如果设为"True",则会在additions信息中使用lid_lang标记, 返回对应的语种标签。默认 "False" |\ +| | | | | |支持的标签包括: |\ +| | | | | | |\ +| | | | | |* singing_en:英文唱歌 |\ +| | | | | |* singing_mand:普通话唱歌 |\ +| | | | | |* singing_dia_cant:粤语唱歌 |\ +| | | | | |* speech_en:英文说话 |\ +| | | | | |* speech_mand:普通话说话 |\ +| | | | | |* speech_dia_nan:闽南语 |\ +| | | | | |* speech_dia_wuu:吴语(含上海话) |\ +| | | | | |* speech_dia_cant:粤语说话 |\ +| | | | | |* speech_dia_xina:西南官话(含四川话) |\ +| | | | | |* speech_dia_zgyu:中原官话(含陕西话) |\ +| | | | | |* other_langs:其它语种(其它语种人声) |\ +| | | | | |* others:检测不出(非语义人声和非人声) |\ +| | | | | | 空时代表无法判断(例如传入音频过短等) |\ +| | | | | | |\ +| | | | | |**实际不支持识别的语种(无识别结果),但该参数可检测并输出对应lang_code。对应的标签如下:** |\ +| | | | | | |\ +| | | | | |* singing_hi:印度语唱歌 |\ +| | | | | |* singing_ja:日语唱歌 |\ +| | | | | |* singing_ko:韩语唱歌 |\ +| | | | | |* singing_th:泰语唱歌 |\ +| | | | | |* speech_hi:印地语说话 |\ +| | | | | |* speech_ja:日语说话 |\ +| | | | | |* speech_ko:韩语说话 |\ +| | | | | |* speech_th:泰语说话 |\ +| | | | | |* speech_kk:哈萨克语说话 |\ +| | | | | |* speech_bo:藏语说话 |\ +| | | | | |* speech_ug:维语 |\ +| | | | | |* speech_mn:蒙古语 |\ +| | | | | |* speech_dia_ql:琼雷话 |\ +| | | | | |* speech_dia_hsn:湘语 |\ +| | | | | |* speech_dia_jin:晋语 |\ +| | | | | |* speech_dia_hak:客家话 |\ +| | | | | |* speech_dia_chao:潮汕话 |\ +| | | | | |* speech_dia_juai:江淮官话 |\ +| | | | | |* speech_dia_lany:兰银官话 |\ +| | | | | |* speech_dia_dbiu:东北官话 |\ +| | | | | |* speech_dia_jliu:胶辽官话 |\ +| | | | | |* speech_dia_jlua:冀鲁官话 |\ +| | | | | |* speech_dia_cdo:闽东话 |\ +| | | | | |* speech_dia_gan:赣语 |\ +| | | | | |* speech_dia_mnp:闽北语 |\ +| | | | | |* speech_dia_czh:徽语 | +| | | | | | | \ +|enable_emotion_detection |启用情绪检测 |2 |bool | |如果设为"True",则会在分句additions信息中使用emotion标记, 返回对应的情绪标签。默认 "False" |\ +| | | | | |支持的情绪标签包括: |\ +| | | | | | |\ +| | | | | |* "angry":表示情绪为生气 |\ +| | | | | |* "happy":表示情绪为开心 |\ +| | | | | |* "neutral":表示情绪为平静或中性 |\ +| | | | | |* "sad":表示情绪为悲伤 |\ +| | | | | |* "surprise":表示情绪为惊讶 | +| | | | | | | \ +|enable_gender_detection |启用性别检测 |2 |bool | |如果设为"True",则会在分句additions信息中使用gender标记, 返回对应的性别标签(male/female)。默认 "False" | +| | | | | | | \ +|vad_segment |使用vad分句 |2 |bool | |默认为false,默认是语义分句。 |\ +| | | | | |打开双声道识别时,通常需要使用vad分句,可同时打开此参数 | +| | | | | | | \ +|end_window_size |强制判停时间 |2 |int | |范围300 - 5000ms,建议设置800ms或者1000ms,比较敏感的场景可以配置500ms或者更小。(如果配置的过小,则会导致分句过碎,配置过大会导致不容易将说话内容分开。建议依照自身场景按需配置) |\ +| | | | | |配置该值,不使用语义分句,根据静音时长来分句。 | +| | | | | | | \ +|sensitive_words_filter |敏感词过滤 |2 |string | |敏感词过滤功能,支持开启或关闭,支持自定义敏感词。该参数可实现:不处理(默认,即展示原文)、过滤、替换为*。 |\ +| | | | | |示例: |\ +| | | | | |system_reserved_filter //是否使用系统敏感词,会替换成*(默认系统敏感词主要包含一些限制级词汇) |\ +| | | | | |filter_with_empty // 想要替换成空的敏感词 |\ +| | | | | |filter_with_signed // 想要替换成 * 的敏感词 |\ +| | | | | |```Python |\ +| | | | | |"sensitive_words_filter":{\"system_reserved_filter\":true,\"filter_with_empty\":[\"敏感词\"],\"filter_with_signed\":[\"敏感词\"]}", |\ +| | | | | |``` |\ +| | | | | | | +| | | | | | | \ +|enable_poi_fc |开启 POI function call |2 |bool | |对于语音识别困难的词语,能调用专业的地图领域推荐词服务辅助识别 |\ +| | | | | |示例: |\ +| | | | | |```Python |\ +| | | | | |"request": { |\ +| | | | | | "enable_poi_fc": true, |\ +| | | | | | "corpus": { |\ +| | | | | | "context": "{\"loc_info\":{\"city_name\":\"北京市\"}}" |\ +| | | | | | } |\ +| | | | | |} |\ +| | | | | |``` |\ +| | | | | | |\ +| | | | | |其中loc_info字段可选,传入该字段结果相对更精准,city_name单位为地级市。 | +| | | | | | | \ +|enable_music_fc |开启音乐 function call |2 |bool | |对于语音识别困难的词语,能调用专业的音领域推荐词服务辅助识别 |\ +| | | | | |示例: |\ +| | | | | |```Python |\ +| | | | | |"request": { |\ +| | | | | | "enable_music_fc": true |\ +| | | | | |} |\ +| | | | | |``` |\ +| | | | | | | +| | | | | | | \ +|corpus |语料/干预词等 |2 |string | | | +| | | | | | | \ +|boosting_table_name |自学习平台上设置的热词词表名称 |3 |string | |热词功能和设置方法可以参考[文档](https://www.volcengine.com/docs/6561/155738) | +| | | | | | | \ +|correct_table_name |自学习平台上设置的替换词词表名称 |3 |string | |替换词功能和设置方法可以参考[文档](https://www.volcengine.com/docs/6561/1206007) | +| | | | | | | \ +|context |上下文功能 |3 |string | |1. 热词直传,支持5000个词 |\ +| | | | | | |\ +| | | | | |"context":"{\"hotwords\":[{\"word\":\"热词1号\"}, {\"word\":\"热词2号\"}]}" |\ +| | | | | | |\ +| | | | | | |\ +| | | | | |2. 上下文,限制800 tokens及20轮(含)内,超出会按照时间顺序从新到旧截断,优先保留更新的对话 |\ +| | | | | | |\ +| | | | | | context_data字段按照从新到旧的顺序排列,传入需要序列化为jsonstring(转义引号) |\ +| | | | | |**豆包录音文件识别模型2.0,支持将上下文理解的范围从纯文本扩展到视觉层面,** |\ +| | | | | |**通过理解图像内容,帮助模型更精准地完成语音转录。通过image_url传入图片,** |\ +| | | | | |**图片限制传入1张,大小:500k以内(格式:jpeg、jpg、png )** |\ +| | | | | |```SQL |\ +| | | | | |上下文:可以加入对话历史、聊天所在bot信息、个性化信息、业务场景信息等,如: |\ +| | | | | |a.对话历史:把最近几轮的对话历史传进来 |\ +| | | | | |b.聊天所在bot信息:如"我在和林黛玉聊天","我在使用A助手和手机对话" |\ +| | | | | |c.个性化信息:"我当前在北京市海淀区","我有四川口音","我喜欢音乐" |\ +| | | | | |d.业务场景信息:"当前是中国平安的营销人员针对外部客户采访的录音,可能涉及..." |\ +| | | | | |{ |\ +| | | | | | \"context_type\": \"dialog_ctx\", |\ +| | | | | | \"context_data\":[ |\ +| | | | | | {\"text\": \"text1\"}, |\ +| | | | | | {\"image_url\": \"image_url\"}, |\ +| | | | | | {\"text\": \"text2\"}, |\ +| | | | | | {\"text\": \"text3\"}, |\ +| | | | | | {\"text\": \"text4\"}, |\ +| | | | | | ... |\ +| | | | | | ] |\ +| | | | | |} |\ +| | | | | |``` |\ +| | | | | | | +| | | | | | | \ +|callback |回调地址 |1 |string | |举例: |\ +| | | | | |```Plain Text |\ +| | | | | |"callback": "http://xxx" |\ +| | | | | |``` |\ +| | | | | | |\ +| | | | | | | +| | | | | | | \ +|callback_data |回调信息 |1 |string | |举例: |\ +| | | | | | |\ +| | | | | |```Plain Text |\ +| | | | | |"callback_data":"$Request-Id" |\ +| | | | | |``` |\ +| | | | | | |\ +| | | | | | | + +请求示例: +```JSON +{ + "user": { + "uid": "388808087185088" + }, + "audio": { + "format": "mp3", + "url": "http://xxx.com/obj/sample.mp3" + }, + "request": { + "model_name": "bigmodel", + "enable_itn": true + } +} +``` + + +## 应答 +Response header如下: + +| | | | \ +|Key |说明 |Value 示例 | +|---|---|---| +| | | | \ +|X-Tt-Logid |服务端返回的 logid,建议用户获取和打印方便定位问题 |202407261553070FACFE6D19421815D605 | +| | | | \ +|X-Api-Status-Code |提交任务后服务端返回的状态码,20000000表示提交成功,其他表示失败 | | +| | | | \ +|X-Api-Message |提交任务后服务端返回的信息,OK表示成功,其他表示失败 | | + +Response body为空 + +# 查询结果 + +## 接口地址 +火山地址:https://openspeech.bytedance.com/api/v3/auc/bigmodel/query + +## 请求 +请求方式:HTTP POST。 +请求和应答,均采用在 HTTP BODY 里面传输 JSON 格式字串的方式。 +Header 需要加入内容类型标识: +旧版本控制台 + +| | | | \ +|Key |说明 |Value 示例 | +|---|---|---| +| | | | \ +|X-Api-App-Key |使用火山引擎控制台获取的APP ID,可参考 [控制台使用FAQ-Q1](https://www.volcengine.com/docs/6561/196768#q1%EF%BC%9A%E5%93%AA%E9%87%8C%E5%8F%AF%E4%BB%A5%E8%8E%B7%E5%8F%96%E5%88%B0%E4%BB%A5%E4%B8%8B%E5%8F%82%E6%95%B0appid%EF%BC%8Ccluster%EF%BC%8Ctoken%EF%BC%8Cauthorization-type%EF%BC%8Csecret-key-%EF%BC%9F)(旧版控制台使用,新版控制台只需要X-Api-Key即可) |123456789 | +| | | | \ +|X-Api-Access-Key |使用火山引擎控制台获取的Access Token,可参考 [控制台使用FAQ-Q1](https://www.volcengine.com/docs/6561/196768#q1%EF%BC%9A%E5%93%AA%E9%87%8C%E5%8F%AF%E4%BB%A5%E8%8E%B7%E5%8F%96%E5%88%B0%E4%BB%A5%E4%B8%8B%E5%8F%82%E6%95%B0appid%EF%BC%8Ccluster%EF%BC%8Ctoken%EF%BC%8Cauthorization-type%EF%BC%8Csecret-key-%EF%BC%9F)(旧版控制台使用,新版控制台只需要X-Api-Key即可) |your-access-key | +| | | | \ +|X-Api-Resource-Id |表示调用服务的资源信息 ID |豆包录音文件识别模型1.0 |\ +| | | |\ +| | |* volc.bigasr.auc |\ +| | | |\ +| | |豆包录音文件识别模型2.0 |\ +| | | |\ +| | |* volc.seedasr.auc | +| | | | \ +|X-Api-Request-Id |用于提交和查询任务的任务ID,推荐传入随机生成的UUID |67ee89ba-7050-4c04-a3d7-ac61a63499b3 | + +```Plain Text +headers = { + "X-Api-App-Key": appid, + "X-Api-Access-Key": token, + "X-Api-Resource-Id": "volc.seedasr.auc",//资源ID + "X-Api-Request-Id": task_id, +} +``` + +新版本控制台 + +| | | | \ +|Key |说明 |Value 示例 | +|---|---|---| +| | | | \ +|X-Api-Key |使用火山引擎控制台获取的APP Key,可参考 [快速入门(新版控制台)](/docs/6561/196768#q1%EF%BC%9A%E5%93%AA%E9%87%8C%E5%8F%AF%E4%BB%A5%E8%8E%B7%E5%8F%96%E5%88%B0%E4%BB%A5%E4%B8%8B%E5%8F%82%E6%95%B0appid%EF%BC%8Ccluster%EF%BC%8Ctoken%EF%BC%8Cauthorization-type%EF%BC%8Csecret-key-%EF%BC%9F) |123456789 | +| | | | \ +|X-Api-Resource-Id |表示调用服务的资源信息 ID |豆包录音文件识别模型1.0 |\ +| | | |\ +| | |* volc.bigasr.auc |\ +| | | |\ +| | |豆包录音文件识别模型2.0 |\ +| | | |\ +| | |* volc.seedasr.auc | +| | | | \ +|X-Api-Request-Id |用于提交和查询任务的任务ID,推荐传入随机生成的UUID |67ee89ba-7050-4c04-a3d7-ac61a63499b3 | + +```Plain Text +headers = { + "X-Api-Key": apikey, + "X-Api-Resource-Id": "volc.seedasr.auc",//资源ID + "X-Api-Request-Id": task_id, +} +``` + +body为空json: +```Go +{} +``` + + +## 应答 +Response header如下: + +| | | | \ +|Key |说明 |Value 示例 | +|---|---|---| +| | | | \ +|X-Tt-Logid |服务端返回的 logid,建议用户获取和打印方便定位问题 |202407261553070FACFE6D19421815D605 | +| | | | \ +|X-Api-Status-Code |提交任务后服务端返回的状态码,具体错误码参考下面错误码列表 | | +| | | | \ +|X-Api-Message |提交任务后服务端返回的信息,OK表示成功,其他表示失败 | | + +Response Body格式 :JSON。 +应答字段: + +| | | | | | \ +|字段 |说明 |层级 |格式 |备注 | +|---|---|---|---|---| +| | | | | | \ +|result |识别结果 |1 |list |仅当识别成功时填写 | +| | | | | | \ +|text |整个音频的识别结果文本 |2 |string |仅当识别成功时填写。 | +| | | | | | \ +|utterances |识别结果语音分句信息 |2 |list |仅当识别成功且开启show_utterances时填写。 | +| | | | | | \ +|text |utterance级的文本内容 |3 |string |仅当识别成功且开启show_utterances时填写。 | +| | | | | | \ +|start_time |起始时间(毫秒) |3 |int |仅当识别成功且开启show_utterances时填写。 | +| | | | | | \ +|end_time |结束时间(毫秒) |3 |int |仅当识别成功且开启show_utterances时填写。 | + +应答示例: +返回文本的形式: +```JSON +{ + "audio_info": {"duration": 10000}, + "result": { + "text": "这是字节跳动, 今日头条母公司。", + "utterances": [ + { + "definite": true, + "end_time": 1705, + "start_time": 0, + "text": "这是字节跳动,", + "words": [ + { + "blank_duration": 0, + "end_time": 860, + "start_time": 740, + "text": "这" + }, + { + "blank_duration": 0, + "end_time": 1020, + "start_time": 860, + "text": "是" + }, + { + "blank_duration": 0, + "end_time": 1200, + "start_time": 1020, + "text": "字" + }, + { + "blank_duration": 0, + "end_time": 1400, + "start_time": 1200, + "text": "节" + }, + { + "blank_duration": 0, + "end_time": 1560, + "start_time": 1400, + "text": "跳" + }, + { + "blank_duration": 0, + "end_time": 1640, + "start_time": 1560, + "text": "动" + } + ] + }, + { + "definite": true, + "end_time": 3696, + "start_time": 2110, + "text": "今日头条母公司。", + "words": [ + { + "blank_duration": 0, + "end_time": 3070, + "start_time": 2910, + "text": "今" + }, + { + "blank_duration": 0, + "end_time": 3230, + "start_time": 3070, + "text": "日" + }, + { + "blank_duration": 0, + "end_time": 3390, + "start_time": 3230, + "text": "头" + }, + { + "blank_duration": 0, + "end_time": 3550, + "start_time": 3390, + "text": "条" + }, + { + "blank_duration": 0, + "end_time": 3670, + "start_time": 3550, + "text": "母" + }, + { + "blank_duration": 0, + "end_time": 3696, + "start_time": 3670, + "text": "公" + }, + { + "blank_duration": 0, + "end_time": 3696, + "start_time": 3696, + "text": "司" + } + ] + } + ] + }, + "audio_info": { + "duration": 3696 + } +} +``` + + +## 错误码 + +| | | | \ +|错误码 |含义 |说明 | +|---|---|---| +| | | | \ +|20000000 |成功 | | +| | | | \ +|20000001 |正在处理中 | | +| | | | \ +|20000002 |任务在队列中 | | +| | | | \ +|20000003 |静音音频 |没有检测到人声 | +| | | | \ +|45000001 |请求参数无效 |请求参数缺失必需字段 / 字段值无效 / 重复请求。 | +| | | | \ +|45000002 |空音频 | | +| | | | \ +|45000131 |超过半小时提交的音频长度上限 |超过了半小时允许提交的音频长度上限(默认半小时最多提交500小时),需要降低提交任务的速度 | +| | | | \ +|45000132 |超过音频大小限制 |上传的音频超过大小限制(<512M) | +| | | | \ +|45000151 |音频格式不正确 | | +| | | | \ +|550xxxx |服务内部处理错误 | | +| | | | \ +|55000031 |服务器繁忙 |服务过载,无法处理当前请求。 | + + +# Demo +python: + +Go: + +Java: + + + + + + + diff --git a/requirements.txt b/requirements.txt index 2b1be2a..a31f819 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests>=2.31.0 python-dotenv>=1.0.0 +boto3>=1.34.0 diff --git a/transcribe_all.py b/transcribe_all.py new file mode 100644 index 0000000..2735a0b --- /dev/null +++ b/transcribe_all.py @@ -0,0 +1,147 @@ +import json +import os +import sys +import time +import uuid +from datetime import timedelta + +import requests +from dotenv import load_dotenv +from minio import Minio + +load_dotenv() + +API_KEY = os.getenv("X_API_KEY") +RESOURCE_ID = os.getenv("X_API_RESOURCE_ID") +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT") +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY") +MINIO_BUCKET = os.getenv("MINIO_BUCKET") + +SUBMIT_URL = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/submit" +QUERY_URL = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/query" + +client = Minio( + MINIO_ENDPOINT, + access_key=MINIO_ACCESS_KEY, + secret_key=MINIO_SECRET_KEY, + secure=True, +) + +def fmt_time(ms): + s = ms / 1000 + m = int(s // 60) + sec = s % 60 + return f"{m:02d}:{sec:05.2f}" + + +def build_markdown(data, audio_file): + result = data.get("result", {}) + utterances = result.get("utterances", []) + duration = data.get("audio_info", {}).get("duration", 0) + + speaker_order = [] + for u in utterances: + sp = u.get("additions", {}).get("speaker", "unknown") + if sp not in speaker_order: + speaker_order.append(sp) + + lines = [ + "# 转写结果", + "", + f"**源文件**: {audio_file} ", + f"**音频时长**: {fmt_time(duration)} ", + f"**说话人数量**: {len(speaker_order)} 人 ", + f"**分句数量**: {len(utterances)}", + "", + "---", + "", + "## 按时间线", + "", + ] + for u in utterances: + sp = u.get("additions", {}).get("speaker", "?") + start = u.get("start_time", 0) + end = u.get("end_time", 0) + t = u.get("text", "").strip() + if not t: + continue + lines.append(f"- **{fmt_time(start)} - {fmt_time(end)}** **说话人{sp}** {t}") + + lines.append("") + return "\n".join(lines) + + +audio_file = sys.argv[1] if len(sys.argv) > 1 else "2026年05月26日 17点53分.mp3" +audio_ext = os.path.splitext(audio_file)[1].lstrip(".") +audio_name = os.path.splitext(os.path.basename(audio_file))[0] + +obj_name = f"audio/{uuid.uuid4().hex[:8]}_{os.path.basename(audio_file)}" +print(f"Uploading {audio_file} ...") +client.fput_object(MINIO_BUCKET, obj_name, audio_file) +url = client.presigned_get_object(MINIO_BUCKET, obj_name, expires=timedelta(hours=1)) + +task_id = str(uuid.uuid4()) +headers = { + "X-Api-Key": API_KEY, + "X-Api-Resource-Id": RESOURCE_ID, + "X-Api-Request-Id": task_id, + "X-Api-Sequence": "-1", +} +body = { + "user": {"uid": "audioscribe"}, + "audio": {"url": url, "format": audio_ext}, + "request": { + "model_name": "bigmodel", + "enable_itn": True, + "enable_punc": True, + "enable_ddc": True, + "enable_speaker_info": True, + "show_utterances": True, + }, +} + +resp = requests.post(SUBMIT_URL, json=body, headers=headers) +logid = resp.headers.get("X-Tt-Logid", "") +status = resp.headers.get("X-Api-Status-Code", "") +print(f"Submit: {status}") + +if status != "20000000": + print(f"FAILED: {resp.headers.get('X-Api-Message', '')}") +else: + while True: + resp = requests.post( + QUERY_URL, + json={}, + headers={ + "X-Api-Key": API_KEY, + "X-Api-Resource-Id": RESOURCE_ID, + "X-Api-Request-Id": task_id, + "X-Tt-Logid": logid, + }, + ) + code = resp.headers.get("X-Api-Status-Code", "") + if code == "20000000": + data = resp.json() + text = data.get("result", {}).get("text", "") + utterances = data.get("result", {}).get("utterances", []) + + json_path = f"{audio_name}_转写结果.json" + with open(json_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + md_path = f"{audio_name}_转写结果.md" + with open(md_path, "w", encoding="utf-8") as f: + f.write(build_markdown(data, audio_file)) + + print(f"Done ({len(text)} chars, {len(utterances)} utterances)") + print(f"Saved: {json_path}, {md_path}") + break + elif code in ("20000001", "20000002"): + print(f" Processing... ({code})") + time.sleep(5) + else: + print(f"FAILED: {code} {resp.headers.get('X-Api-Message', '')}") + break + +print("\nAll done.") diff --git a/transcribe_legacy.py b/transcribe_legacy.py new file mode 100644 index 0000000..234bf7b --- /dev/null +++ b/transcribe_legacy.py @@ -0,0 +1,161 @@ +import json +import os +import sys +import time +import uuid + +import requests +from boto3 import client as s3_client +from botocore.config import Config +from dotenv import load_dotenv + +load_dotenv() + +APP_ID = os.getenv("X_API_APP_KEY") +ACCESS_TOKEN = os.getenv("X_API_ACCESS_KEY") +RESOURCE_ID = os.getenv("X_API_RESOURCE_ID") +S3_ENDPOINT = os.getenv("S3_ENDPOINT") +S3_ACCESS_KEY_ID = os.getenv("S3_ACCESS_KEY_ID") +S3_SECRET_ACCESS_KEY = os.getenv("S3_SECRET_ACCESS_KEY") +S3_BUCKET = os.getenv("S3_BUCKET") + +SUBMIT_URL = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/submit" +QUERY_URL = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/query" + +s3 = s3_client( + "s3", + endpoint_url=S3_ENDPOINT, + aws_access_key_id=S3_ACCESS_KEY_ID, + aws_secret_access_key=S3_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + region_name="auto", +) + + +def fmt_time(ms): + s = ms / 1000 + m = int(s // 60) + sec = s % 60 + return f"{m:02d}:{sec:05.2f}" + + +def build_markdown(data, audio_file): + result = data.get("result", {}) + utterances = result.get("utterances", []) + duration = data.get("audio_info", {}).get("duration", 0) + + speaker_order = [] + for u in utterances: + sp = u.get("additions", {}).get("speaker", "unknown") + if sp not in speaker_order: + speaker_order.append(sp) + + lines = [ + "# 转写结果", + "", + f"**源文件**: {audio_file} ", + f"**音频时长**: {fmt_time(duration)} ", + f"**说话人数量**: {len(speaker_order)} 人 ", + f"**分句数量**: {len(utterances)}", + "", + "---", + "", + "## 按时间线", + "", + ] + for u in utterances: + sp = u.get("additions", {}).get("speaker", "?") + start = u.get("start_time", 0) + end = u.get("end_time", 0) + t = u.get("text", "").strip() + if not t: + continue + lines.append(f"- **{fmt_time(start)} - {fmt_time(end)}** **说话人{sp}** {t}") + + lines.append("") + return "\n".join(lines) + + +def make_headers(task_id, with_sequence=False): + headers = { + "X-Api-App-Key": APP_ID, + "X-Api-Access-Key": ACCESS_TOKEN, + "X-Api-Resource-Id": RESOURCE_ID, + "X-Api-Request-Id": task_id, + } + if with_sequence: + headers["X-Api-Sequence"] = "-1" + return headers + + +audio_file = sys.argv[1] if len(sys.argv) > 1 else "2026年05月26日 17点53分.mp3" +audio_ext = os.path.splitext(audio_file)[1].lstrip(".") +audio_name = os.path.splitext(os.path.basename(audio_file))[0] + +obj_name = f"audio/{uuid.uuid4().hex[:8]}_{os.path.basename(audio_file)}" +print(f"Uploading {audio_file} ...") +s3.upload_file(audio_file, S3_BUCKET, obj_name) +url = s3.generate_presigned_url( + "get_object", + Params={"Bucket": S3_BUCKET, "Key": obj_name}, + ExpiresIn=3600, +) + +task_id = str(uuid.uuid4()) +headers = make_headers(task_id, with_sequence=True) +body = { + "user": {"uid": "audioscribe"}, + "audio": {"url": url, "format": audio_ext}, + "request": { + "model_name": "bigmodel", + "enable_itn": True, + "enable_punc": True, + "enable_ddc": True, + "enable_speaker_info": True, + "show_utterances": True, + }, +} + +resp = requests.post(SUBMIT_URL, json=body, headers=headers) +logid = resp.headers.get("X-Tt-Logid", "") +status = resp.headers.get("X-Api-Status-Code", "") +print(f"Submit: {status}") + +if status != "20000000": + print(f"FAILED: {resp.headers.get('X-Api-Message', '')}") +else: + while True: + resp = requests.post( + QUERY_URL, + json={}, + headers={ + **make_headers(task_id), + "X-Tt-Logid": logid, + }, + ) + code = resp.headers.get("X-Api-Status-Code", "") + if code == "20000000": + data = resp.json() + text = data.get("result", {}).get("text", "") + utterances = data.get("result", {}).get("utterances", []) + + os.makedirs("output", exist_ok=True) + json_path = f"output/{audio_name}_转写结果.json" + with open(json_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + md_path = f"output/{audio_name}_转写结果.md" + with open(md_path, "w", encoding="utf-8") as f: + f.write(build_markdown(data, audio_file)) + + print(f"Done ({len(text)} chars, {len(utterances)} utterances)") + print(f"Saved: {json_path}, {md_path}") + break + elif code in ("20000001", "20000002"): + print(f" Processing... ({code})") + time.sleep(5) + else: + print(f"FAILED: {code} {resp.headers.get('X-Api-Message', '')}") + break + +print("\nAll done.")