bililive-cli 3.8.2 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ var path$7 = require('node:path');
4
4
  var http$3 = require('node:http');
5
5
  var require$$0$4 = require('node:url');
6
6
  var a = require('node:https');
7
- var index = require('./index-9RtWxDCO.cjs');
7
+ var index = require('./index-Ba32bNTL.cjs');
8
8
  var require$$0$5 = require('tty');
9
9
  var require$$1$2 = require('util');
10
10
  var require$$0$c = require('assert');
@@ -32,6 +32,7 @@ require('node:readline');
32
32
  require('@napi-rs/canvas');
33
33
  require('better-sqlite3');
34
34
  var musicSegmentDetector = require('music-segment-detector');
35
+ var require$$0$e = require('child_process');
35
36
  var shazamioCore = require('shazamio-core');
36
37
  var require$$1$6 = require('https');
37
38
  require('node:dns');
@@ -39,7 +40,6 @@ require('constants');
39
40
  require('node:fs/promises');
40
41
  require('node:process');
41
42
  require('node:util');
42
- require('child_process');
43
43
  require('dns');
44
44
  require('tls');
45
45
  require('punycode');
@@ -30748,6 +30748,21 @@ async function getRecorders(params) {
30748
30748
  },
30749
30749
  };
30750
30750
  }
30751
+ /**
30752
+ * 正在录制中直播间数量
30753
+ * @param recording 是否只计算正在录制的直播间,默认为true
30754
+ * @returns
30755
+ */
30756
+ function getRecorderNum(recording) {
30757
+ const recorderManager = exports.container.resolve("recorderManager");
30758
+ const recorders = recorderManager.manager.recorders;
30759
+ if (recording) {
30760
+ return recorders.filter((recorder) => recorder.recordHandle != null).length;
30761
+ }
30762
+ else {
30763
+ return recorders.length;
30764
+ }
30765
+ }
30751
30766
  function getRecorder(args) {
30752
30767
  const recorderManager = exports.container.resolve("recorderManager");
30753
30768
  const recorder = recorderManager.manager.recorders.find((item) => item.id === args.id);
@@ -30941,6 +30956,7 @@ async function getLiveInfo(ids) {
30941
30956
  }
30942
30957
  var recorderService = {
30943
30958
  getRecorders,
30959
+ getRecorderNum,
30944
30960
  getRecorder,
30945
30961
  addRecorder,
30946
30962
  updateRecorder,
@@ -56596,6 +56612,18 @@ router$d.get("/appStartTime", async (ctx) => {
56596
56612
  const data = index.statisticsService.query("start_time");
56597
56613
  ctx.body = data?.value;
56598
56614
  });
56615
+ router$d.get("/statistics", async (ctx) => {
56616
+ const startTime = index.statisticsService.query("start_time");
56617
+ const videoTotalDuaration = index.recordHistoryService.getTotalDuration();
56618
+ const recordingNum = recorderService.getRecorderNum(true);
56619
+ const recorderNum = recorderService.getRecorderNum(false);
56620
+ ctx.body = {
56621
+ startTime: startTime?.value || null,
56622
+ videoTotalDuaration,
56623
+ recordingNum,
56624
+ recorderNum,
56625
+ };
56626
+ });
56599
56627
  router$d.get("/exportLogs", async (ctx) => {
56600
56628
  const logFilePath = exports.config.logPath;
56601
56629
  ctx.body = index.fs.createReadStream(logFilePath);
@@ -57034,6 +57062,47 @@ router$d.get("/tempPath", async (ctx) => {
57034
57062
  ctx.body = "获取缓存路径失败";
57035
57063
  }
57036
57064
  });
57065
+ /**
57066
+ * @api {get} /common/diskSpace 获取磁盘空间信息
57067
+ * @apiDescription 获取录播姬文件夹所在磁盘的空间信息
57068
+ * @apiSuccess {number} total 总空间(GB)
57069
+ * @apiSuccess {number} free 可用空间(GB)
57070
+ * @apiSuccess {number} used 已用空间(GB)
57071
+ * @apiSuccess {number} usedPercentage 使用百分比
57072
+ */
57073
+ router$d.get("/diskSpace", async (ctx) => {
57074
+ try {
57075
+ const config = exports.appConfig.getAll();
57076
+ const recoderFolder = config?.recorder?.savePath;
57077
+ if (!recoderFolder) {
57078
+ ctx.status = 400;
57079
+ ctx.body = "未配置文件夹路径";
57080
+ return;
57081
+ }
57082
+ if (!(await index.fs.pathExists(recoderFolder))) {
57083
+ ctx.status = 404;
57084
+ ctx.body = "文件夹不存在";
57085
+ return;
57086
+ }
57087
+ // @ts-ignore
57088
+ const diskInfo = await index.checkDiskSpace(recoderFolder);
57089
+ const totalGB = diskInfo.size / (1024 * 1024 * 1024);
57090
+ const freeGB = diskInfo.free / (1024 * 1024 * 1024);
57091
+ const usedGB = totalGB - freeGB;
57092
+ const usedPercentage = (usedGB / totalGB) * 100;
57093
+ ctx.body = {
57094
+ total: Number(totalGB.toFixed(2)),
57095
+ free: Number(freeGB.toFixed(2)),
57096
+ used: Number(usedGB.toFixed(2)),
57097
+ usedPercentage: Number(usedPercentage.toFixed(2)),
57098
+ };
57099
+ }
57100
+ catch (error) {
57101
+ console.error("获取磁盘空间失败:", error);
57102
+ ctx.status = 500;
57103
+ ctx.body = "获取磁盘空间失败";
57104
+ }
57105
+ });
57037
57106
 
57038
57107
  const router$c = new Router$1({
57039
57108
  prefix: "/user",
@@ -64464,6 +64533,8 @@ class AliyunASR {
64464
64533
  model: this.model,
64465
64534
  input: {
64466
64535
  file_urls: [params.fileUrl],
64536
+ // qwen3-ASR-Flash-Filetrans 兼容
64537
+ file_url: params.fileUrl,
64467
64538
  },
64468
64539
  parameters: {
64469
64540
  ...(params.vocabularyId && { vocabulary_id: params.vocabularyId }),
@@ -64474,9 +64545,10 @@ class AliyunASR {
64474
64545
  }),
64475
64546
  ...(params.speakerCount && { speaker_count: params.speakerCount }),
64476
64547
  ...(params.languageHints && { language_hints: params.languageHints }),
64548
+ // qwen3-ASR-Flash-Filetran 词级时间戳
64549
+ enable_words: true,
64477
64550
  },
64478
64551
  };
64479
- this.logger.info("提交ASR任务", { fileUrl: params.fileUrl });
64480
64552
  const response = await this.client.post("/api/v1/services/audio/asr/transcription", requestData, {
64481
64553
  headers: {
64482
64554
  "X-DashScope-Async": "enable",
@@ -64504,13 +64576,18 @@ class AliyunASR {
64504
64576
  "X-DashScope-Async": "enable",
64505
64577
  },
64506
64578
  });
64579
+ let results = response.data.output.results || [];
64580
+ if (!results || results.length === 0) {
64581
+ // qwen3-ASR-Flash-Filetrans 兼容
64582
+ results = [{ ...response.data.output.result, subtask_status: "SUCCEEDED" }];
64583
+ }
64507
64584
  return {
64508
64585
  taskId: response.data.output.task_id,
64509
64586
  taskStatus: response.data.output.task_status,
64510
64587
  submitTime: response.data.output.submit_time,
64511
64588
  scheduledTime: response.data.output.scheduled_time,
64512
64589
  endTime: response.data.output.end_time,
64513
- results: response.data.output.results,
64590
+ results: results,
64514
64591
  };
64515
64592
  }
64516
64593
  catch (error) {
@@ -64682,6 +64759,454 @@ class AliyunASR {
64682
64759
  }
64683
64760
  }
64684
64761
 
64762
+ /**
64763
+ * OpenAI Whisper ASR 语音识别类
64764
+ * 使用 OpenAI Whisper API 进行录音文件识别
64765
+ */
64766
+ class OpenAIWhisperASR {
64767
+ client;
64768
+ model;
64769
+ logger;
64770
+ /**
64771
+ * 创建 OpenAI Whisper ASR 实例
64772
+ */
64773
+ constructor(options) {
64774
+ const { apiKey, baseURL = "https://api.openai.com/v1", model = "whisper-1" } = options;
64775
+ this.model = model;
64776
+ this.logger = options.logger || index.logObj;
64777
+ this.client = index.axios.create({
64778
+ baseURL,
64779
+ headers: {
64780
+ Authorization: `Bearer ${apiKey}`,
64781
+ },
64782
+ timeout: 300000, // 5分钟超时
64783
+ });
64784
+ }
64785
+ /**
64786
+ * 识别音频文件(通过文件URL)
64787
+ * 注意:OpenAI Whisper API 不支持直接传URL,需要先下载文件
64788
+ *
64789
+ * @param fileUrl 音频文件URL
64790
+ * @returns 转写结果
64791
+ */
64792
+ async recognize(fileUrl) {
64793
+ this.logger.info(`开始下载文件: ${fileUrl}`);
64794
+ // 下载文件到临时位置
64795
+ const response = await index.axios.get(fileUrl, {
64796
+ responseType: "arraybuffer",
64797
+ });
64798
+ const tempFilePath = `/tmp/whisper-temp-${Date.now()}.audio`;
64799
+ await index.fs.writeFile(tempFilePath, response.data);
64800
+ try {
64801
+ const result = await this.recognizeLocalFile(tempFilePath);
64802
+ return result;
64803
+ }
64804
+ finally {
64805
+ // 清理临时文件
64806
+ await index.fs.remove(tempFilePath).catch((err) => {
64807
+ this.logger.warn(`清理临时文件失败: ${err.message}`);
64808
+ });
64809
+ }
64810
+ }
64811
+ /**
64812
+ * 识别本地音频文件
64813
+ *
64814
+ * @param filePath 本地音频文件路径
64815
+ * @param options 可选参数
64816
+ * @returns 转写结果
64817
+ */
64818
+ async recognizeLocalFile(filePath, options) {
64819
+ this.logger.info(`开始识别本地文件: ${filePath}`);
64820
+ // 检查文件是否存在
64821
+ if (!(await index.fs.pathExists(filePath))) {
64822
+ throw new Error(`文件不存在: ${filePath}`);
64823
+ }
64824
+ // 创建 FormData
64825
+ const formData = new index.FormData();
64826
+ formData.append("file", index.fs.createReadStream(filePath));
64827
+ formData.append("model", this.model);
64828
+ formData.append("response_format", "verbose_json"); // 使用 verbose_json 获取时间戳
64829
+ if (options?.prompt) {
64830
+ formData.append("prompt", options.prompt);
64831
+ }
64832
+ if (options?.language) {
64833
+ formData.append("language", options.language);
64834
+ }
64835
+ if (options?.temperature !== undefined) {
64836
+ formData.append("temperature", options.temperature.toString());
64837
+ }
64838
+ try {
64839
+ const response = await this.client.post("/audio/transcriptions", formData, {
64840
+ headers: {
64841
+ ...formData.getHeaders(),
64842
+ },
64843
+ });
64844
+ this.logger.info(`识别完成,文本长度: ${response.data.text.length}`);
64845
+ return response.data;
64846
+ }
64847
+ catch (error) {
64848
+ const errorMessage = error.response?.data?.error?.message || error.message || "未知错误";
64849
+ this.logger.error(`OpenAI Whisper 识别失败: ${errorMessage}`);
64850
+ throw new Error(`OpenAI Whisper 识别失败: ${errorMessage}`);
64851
+ }
64852
+ }
64853
+ }
64854
+
64855
+ /**
64856
+ * FFmpeg Whisper ASR 实现
64857
+ * 使用 spawn 直接调用 ffmpeg 的 whisper 滤镜
64858
+ */
64859
+ class FFmpegWhisperASR {
64860
+ options;
64861
+ constructor(options) {
64862
+ this.options = {
64863
+ logger: index.logObj,
64864
+ language: "zh",
64865
+ queue: 20,
64866
+ ...options,
64867
+ };
64868
+ // 验证 ffmpeg 路径
64869
+ if (!index.fs.existsSync(this.options.ffmpegPath)) {
64870
+ throw new Error(`FFmpeg 路径不存在: ${this.options.ffmpegPath}`);
64871
+ }
64872
+ // 验证模型文件路径
64873
+ if (!index.fs.existsSync(this.options.model)) {
64874
+ throw new Error(`Whisper 模型文件不存在: ${this.options.model}`);
64875
+ }
64876
+ }
64877
+ /**
64878
+ * 识别本地音频文件
64879
+ * @param filePath 本地文件路径
64880
+ * @returns Whisper 转写结果
64881
+ */
64882
+ async recognizeLocalFile(filePath) {
64883
+ if (!index.fs.existsSync(filePath)) {
64884
+ throw new Error(`音频文件不存在: ${filePath}`);
64885
+ }
64886
+ // 创建临时目录存放结果
64887
+ const tempDir = require$$0$7.join(require$$1$5.tmpdir(), "whisper-asr");
64888
+ await index.fs.ensureDir(tempDir);
64889
+ // 生成唯一的输出文件名
64890
+ const outputFile = require$$0$7.join(tempDir, `${v4()}.json`);
64891
+ try {
64892
+ // 执行 ffmpeg 命令
64893
+ await this.executeFFmpeg(filePath, outputFile);
64894
+ // 读取结果文件
64895
+ if (!index.fs.existsSync(outputFile)) {
64896
+ throw new Error(`Whisper 未生成结果文件: ${outputFile}`);
64897
+ }
64898
+ // 读取 JSONL 文件(每行一个 JSON 对象)
64899
+ const resultContent = await index.fs.readFile(outputFile, "utf-8");
64900
+ const segments = resultContent
64901
+ .split("\n")
64902
+ .filter((line) => line.trim())
64903
+ .map((line) => {
64904
+ try {
64905
+ return JSON.parse(line);
64906
+ }
64907
+ catch (error) {
64908
+ this.options.logger.warn(`解析 JSONL 行失败: ${line}`);
64909
+ return null;
64910
+ }
64911
+ })
64912
+ .filter((seg) => seg !== null);
64913
+ this.options.logger.info(`Whisper 识别完成: ${filePath}, 共 ${segments.length} 个片段`);
64914
+ return { segments };
64915
+ }
64916
+ catch (error) {
64917
+ this.options.logger.error(`Whisper 识别失败: ${error}`);
64918
+ throw error;
64919
+ }
64920
+ finally {
64921
+ console.log("清理临时文件:", outputFile);
64922
+ // 清理临时文件
64923
+ // if (fs.existsSync(outputFile)) {
64924
+ // await fs.remove(outputFile);
64925
+ // }
64926
+ }
64927
+ }
64928
+ /**
64929
+ * 执行 ffmpeg whisper 命令
64930
+ * @param inputFile 输入音频文件
64931
+ * @param outputFile 输出 JSON 文件
64932
+ */
64933
+ async executeFFmpeg(inputFile, outputFile) {
64934
+ return new Promise((resolve, reject) => {
64935
+ // 构建 whisper 滤镜参数(使用 jsonl 格式)
64936
+ const whisperFilter = `whisper=model=${index.escaped(this.options.model)}:language=${index.escaped(this.options.language)}:destination=${index.escaped(outputFile)}:format=json:queue=${this.options.queue}`;
64937
+ // ffmpeg 命令参数
64938
+ const args = [
64939
+ "-i",
64940
+ inputFile,
64941
+ "-af",
64942
+ whisperFilter,
64943
+ "-f",
64944
+ "null",
64945
+ "-", // 输出到 null
64946
+ ];
64947
+ this.options.logger.info(`执行 FFmpeg Whisper 命令: ${this.options.ffmpegPath} ${args.join(" ")}`);
64948
+ // 使用 spawn 执行命令
64949
+ const process = require$$0$e.spawn(this.options.ffmpegPath, args);
64950
+ let stderr = "";
64951
+ // 收集标准错误输出(ffmpeg 的日志)
64952
+ process.stderr.on("data", (data) => {
64953
+ const text = data.toString();
64954
+ stderr += text;
64955
+ // this.options.logger.debug(`FFmpeg stderr: ${text}`);
64956
+ });
64957
+ // 处理进程退出
64958
+ process.on("close", (code) => {
64959
+ if (code === 0) {
64960
+ this.options.logger.info("FFmpeg Whisper 执行成功");
64961
+ resolve();
64962
+ }
64963
+ else {
64964
+ this.options.logger.error(`FFmpeg 执行失败,退出码: ${code}`);
64965
+ this.options.logger.error(`错误信息: ${stderr}`);
64966
+ reject(new Error(`FFmpeg 执行失败,退出码: ${code}\n${stderr}`));
64967
+ }
64968
+ });
64969
+ // 处理错误
64970
+ process.on("error", (error) => {
64971
+ this.options.logger.error(`FFmpeg 进程错误: ${error}`);
64972
+ reject(error);
64973
+ });
64974
+ });
64975
+ }
64976
+ /**
64977
+ * 识别音频 URL(不支持)
64978
+ * 由于 ffmpeg whisper 需要本地文件,URL 需要先下载
64979
+ */
64980
+ async recognize(_fileUrl) {
64981
+ throw new Error("FFmpeg Whisper 不支持直接识别 URL,请先下载音频文件后使用 recognizeLocalFile");
64982
+ }
64983
+ }
64984
+
64985
+ /**
64986
+ * 获取模型参数
64987
+ * @returns 模型参数对象
64988
+ */
64989
+ function getModel(modelId, iConfig) {
64990
+ if (!modelId) {
64991
+ throw new Error("模型ID未定义");
64992
+ }
64993
+ let config = iConfig;
64994
+ if (!config) {
64995
+ config = index.appConfig.getAll();
64996
+ }
64997
+ const model = config.ai.models.find((m) => m.modelId === modelId);
64998
+ if (!model) {
64999
+ throw new Error(`找不到模型:${modelId}`);
65000
+ }
65001
+ return model;
65002
+ }
65003
+
65004
+ /**
65005
+ * 阿里云 ASR 适配器
65006
+ */
65007
+ class AliyunASRAdapter {
65008
+ client;
65009
+ constructor(config) {
65010
+ this.client = new AliyunASR({
65011
+ apiKey: config.apiKey,
65012
+ baseURL: config.baseURL,
65013
+ model: config.model,
65014
+ logger: index.logObj,
65015
+ });
65016
+ }
65017
+ async recognize(fileUrl) {
65018
+ const result = await this.client.recognize(fileUrl);
65019
+ return this.transformAliyunResult(result);
65020
+ }
65021
+ async recognizeLocalFile(filePath) {
65022
+ const result = await this.client.recognizeLocalFile(filePath);
65023
+ return this.transformAliyunResult(result);
65024
+ }
65025
+ /**
65026
+ * 转换阿里云格式为标准格式
65027
+ */
65028
+ transformAliyunResult(result) {
65029
+ const transcript = result.transcripts?.[0];
65030
+ if (!transcript) {
65031
+ throw new Error("阿里云 ASR 返回结果为空");
65032
+ }
65033
+ const segments = [];
65034
+ const words = [];
65035
+ // 转换句子级别数据
65036
+ transcript.sentences?.forEach((sentence, index) => {
65037
+ segments.push({
65038
+ id: index,
65039
+ start: sentence.begin_time / 1000, // 毫秒转秒
65040
+ end: sentence.end_time / 1000,
65041
+ text: sentence.text,
65042
+ speaker: sentence.speakerId?.toString(),
65043
+ });
65044
+ // 转换词级别数据
65045
+ sentence.words?.forEach((word) => {
65046
+ words.push({
65047
+ start: word.begin_time / 1000,
65048
+ end: word.end_time / 1000,
65049
+ word: word.text,
65050
+ punctuation: word.punctuation,
65051
+ speaker: word.speakerId?.toString(),
65052
+ });
65053
+ });
65054
+ });
65055
+ return {
65056
+ text: transcript.text,
65057
+ duration: result.properties.original_duration_in_milliseconds
65058
+ ? result.properties.original_duration_in_milliseconds / 1000
65059
+ : undefined,
65060
+ language: undefined, // 阿里云不返回语言信息
65061
+ segments,
65062
+ words: words.length > 0 ? words : undefined,
65063
+ };
65064
+ }
65065
+ }
65066
+ /**
65067
+ * OpenAI Whisper ASR 适配器
65068
+ */
65069
+ class OpenAIASRAdapter {
65070
+ client;
65071
+ constructor(config) {
65072
+ this.client = new OpenAIWhisperASR({
65073
+ apiKey: config.apiKey,
65074
+ baseURL: config.baseURL,
65075
+ model: config.model,
65076
+ logger: index.logObj,
65077
+ });
65078
+ }
65079
+ async recognize(fileUrl) {
65080
+ const result = await this.client.recognize(fileUrl);
65081
+ return this.transformOpenAIResult(result);
65082
+ }
65083
+ async recognizeLocalFile(filePath) {
65084
+ const result = await this.client.recognizeLocalFile(filePath);
65085
+ return this.transformOpenAIResult(result);
65086
+ }
65087
+ /**
65088
+ * 转换 OpenAI Whisper 格式为标准格式
65089
+ */
65090
+ transformOpenAIResult(result) {
65091
+ const segments = result.segments.map((segment) => ({
65092
+ id: segment.id,
65093
+ start: segment.start,
65094
+ end: segment.end,
65095
+ text: segment.text.trim(),
65096
+ speaker: undefined, // Whisper 不支持说话人分离
65097
+ }));
65098
+ return {
65099
+ text: result.text,
65100
+ duration: result.duration,
65101
+ language: result.language,
65102
+ segments,
65103
+ words: undefined, // Whisper API 不返回词级别时间戳
65104
+ };
65105
+ }
65106
+ }
65107
+ /**
65108
+ * FFmpeg Whisper ASR 适配器
65109
+ */
65110
+ class FFmpegWhisperASRAdapter {
65111
+ client;
65112
+ constructor(config) {
65113
+ this.client = new FFmpegWhisperASR({
65114
+ ffmpegPath: config.ffmpegPath,
65115
+ model: config.model,
65116
+ language: config.language || "zh",
65117
+ queue: config.queue || 20,
65118
+ logger: index.logObj,
65119
+ });
65120
+ }
65121
+ async recognize() {
65122
+ throw new Error("FFmpeg Whisper 不支持直接识别 URL,请使用 recognizeLocalFile");
65123
+ }
65124
+ async recognizeLocalFile(filePath) {
65125
+ const result = await this.client.recognizeLocalFile(filePath);
65126
+ return this.transformWhisperResult(result);
65127
+ }
65128
+ /**
65129
+ * 转换 FFmpeg Whisper 格式为标准格式
65130
+ */
65131
+ transformWhisperResult(result) {
65132
+ if (!result.segments || result.segments.length === 0) {
65133
+ throw new Error("Whisper 返回结果为空");
65134
+ }
65135
+ // 转换段落格式(毫秒转秒)
65136
+ const segments = result.segments.map((segment, index) => ({
65137
+ id: index,
65138
+ start: segment.start / 1000, // 毫秒转秒
65139
+ end: segment.end / 1000, // 毫秒转秒
65140
+ text: segment.text,
65141
+ }));
65142
+ // 构建完整文本
65143
+ const text = segments.map((s) => s.text).join("");
65144
+ // 计算总时长
65145
+ const duration = segments.length > 0 ? segments[segments.length - 1].end : 0;
65146
+ return {
65147
+ text,
65148
+ duration,
65149
+ language: undefined, // Whisper 在 JSONL 输出中不包含语言信息
65150
+ segments,
65151
+ words: undefined, // JSONL 格式不提供词级别时间戳
65152
+ };
65153
+ }
65154
+ }
65155
+ /**
65156
+ * 获取供应商配置
65157
+ */
65158
+ function getVendor$1(vendorId) {
65159
+ const data = index.appConfig.get("ai") || {};
65160
+ const vendor = data.vendors.find((v) => v.id === vendorId);
65161
+ if (!vendor) {
65162
+ throw new Error(`未找到 ID 为 ${vendorId} 的供应商配置`);
65163
+ }
65164
+ return vendor;
65165
+ }
65166
+ /**
65167
+ * 创建 ASR 提供商实例
65168
+ * @param modelId 模型 ID
65169
+ * @returns ASR 提供商实例
65170
+ */
65171
+ function createASRProvider(modelId) {
65172
+ // 获取模型配置
65173
+ const model = getModel(modelId);
65174
+ // 获取供应商配置
65175
+ const vendor = getVendor$1(model.vendorId);
65176
+ // 根据 provider 类型创建对应的适配器
65177
+ const config = {
65178
+ apiKey: vendor.apiKey,
65179
+ baseURL: vendor.baseURL,
65180
+ model: model.modelName,
65181
+ };
65182
+ switch (vendor.provider) {
65183
+ case "aliyun":
65184
+ return new AliyunASRAdapter(config);
65185
+ case "openai":
65186
+ return new OpenAIASRAdapter(config);
65187
+ case "ffmpeg":
65188
+ if (!vendor.baseURL) {
65189
+ throw new Error("FFmpeg provider 需要配置 baseURL(ffmpeg 执行文件路径)");
65190
+ }
65191
+ return new FFmpegWhisperASRAdapter({
65192
+ ffmpegPath: vendor.baseURL, // baseURL 存储 ffmpeg 路径
65193
+ model: model.modelName, // modelName 存储模型文件路径
65194
+ });
65195
+ default:
65196
+ throw new Error(`不支持的 ASR 提供商: ${vendor.provider}`);
65197
+ }
65198
+ }
65199
+ /**
65200
+ * 识别音频文件(返回标准格式)
65201
+ * @param file 音频文件
65202
+ * @param modelId 模型id
65203
+ * @returns
65204
+ */
65205
+ function recognize$1(file, modelId) {
65206
+ const asrProvider = createASRProvider(modelId);
65207
+ return asrProvider.recognizeLocalFile(file);
65208
+ }
65209
+
64685
65210
  const default_format = 'RFC3986';
64686
65211
  const formatters = {
64687
65212
  RFC1738: (v) => String(v).replace(/%20/g, '+'),
@@ -75823,131 +76348,18 @@ async function musicDetect(videoPath, iConfig, onProgress) {
75823
76348
  onProgress?.({ stage: "complete", percentage: 100, message: "分析完成" });
75824
76349
  return segments;
75825
76350
  }
75826
- /**
75827
- * 音乐字幕优化
75828
- */
75829
- function optimizeMusicSubtitles(results) {
75830
- // 去除","、“。”等标点符号
75831
- const transcripts = results.transcripts?.map((transcript) => {
75832
- const optimizedSentences = transcript.sentences?.map((sentence) => {
75833
- let optimizedText = sentence.text.replace(/[,。、“”‘’!?]/g, " ");
75834
- return {
75835
- ...sentence,
75836
- text: optimizedText,
75837
- };
75838
- });
75839
- return {
75840
- ...transcript,
75841
- sentences: optimizedSentences,
75842
- };
75843
- });
75844
- return {
75845
- ...results,
75846
- transcripts: transcripts,
75847
- };
75848
- }
75849
76351
  function convert2Srt(detail, offset) {
75850
- let srt = "";
75851
- let index = 1;
75852
- for (const transcript of detail.transcripts || []) {
75853
- for (const sentence of transcript.sentences || []) {
75854
- const start = new Date(sentence.begin_time + offset)
75855
- .toISOString()
75856
- .substr(11, 12)
75857
- .replace(".", ",");
75858
- const end = new Date(sentence.end_time + offset)
75859
- .toISOString()
75860
- .substr(11, 12)
75861
- .replace(".", ",");
75862
- srt += `${index}\n${start} --> ${end}\n${sentence.text}\n\n`;
75863
- index++;
75864
- }
75865
- }
75866
- return srt;
75867
- }
75868
- function convert2Srt2(detail, offset) {
75869
- let srt = "";
75870
- let index = 1;
75871
- for (const sentence of detail || []) {
75872
- const start = new Date(sentence.st + offset).toISOString().substr(11, 12).replace(".", ",");
75873
- const end = new Date(sentence.et + offset).toISOString().substr(11, 12).replace(".", ",");
75874
- const text = (sentence.t || "").replace(/[,。、“”‘’!?]/g, " ");
75875
- srt += `${index}\n${start} --> ${end}\n${text}\n\n`;
75876
- index++;
75877
- }
75878
- return srt;
75879
- }
75880
- /**
75881
- * 获取 AI 服务商的 API Key,现在是随便写,只支持阿里
75882
- * @returns
75883
- */
75884
- function getApiKey() {
75885
- const data = index.appConfig.get("ai") || {};
75886
- if (data?.vendors.length === 0) {
75887
- throw new Error("请先在配置中设置 AI 服务商的 API Key");
75888
- }
75889
- return data.vendors[0].apiKey || "";
75890
- }
75891
- /**
75892
- * 音频识别
75893
- * @param file
75894
- * @param key
75895
- * @returns
75896
- */
75897
- async function asrRecognize(file, vendorId) {
75898
- const { apiKey } = getVendor(vendorId);
75899
- const asr = new AliyunASR({
75900
- apiKey: apiKey,
75901
- });
75902
- try {
75903
- const results = await asr.recognizeLocalFile(file);
75904
- // console.log("识别结果:", results.transcripts?.[0]);
75905
- // console.log("已将字幕保存到 output.srt 文件");
75906
- return results;
75907
- }
75908
- catch (error) {
75909
- index.logObj.error("ASR 识别失败:", error);
75910
- throw error;
75911
- }
75912
- }
75913
- /**
75914
- * 使用通义千问 LLM 示例
75915
- */
75916
- async function llm(message, systemPrompt, opts = {}) {
75917
- console.log("=== 示例: 使用通义千问 LLM ===");
75918
- const apiKey = opts.key ?? getApiKey();
75919
- const llm = new QwenLLM({
75920
- apiKey: apiKey,
75921
- model: "qwen-plus",
75922
- });
75923
- try {
75924
- // const testData = fs.readFileSync(
75925
- // "C:\\Users\\renmu\\Downloads\\新建文件夹 (2)\\cleaned_data.json",
75926
- // );
75927
- // console.log("读取测试数据,长度:", message + testData, testData.length);
75928
- const response = await llm.sendMessage(message, systemPrompt, {
75929
- // responseFormat: zodResponseFormat(Song, "song"),
75930
- enableSearch: opts.enableSearch ?? false,
75931
- responseFormat: opts.jsonResponse ? { type: "json_object" } : undefined,
75932
- // @ts-ignore
75933
- stream: opts.stream ?? undefined,
75934
- searchOptions: {
75935
- forcedSearch: opts.enableSearch ?? false,
75936
- },
75937
- });
75938
- if ("content" in response) {
75939
- index.logObj.info("LLM 请求成功", JSON.stringify(response));
75940
- // console.log("提问:", response);
75941
- // console.log("回复:", response.content);
75942
- // console.log("Token 使用:", response.usage);
75943
- return response;
75944
- }
75945
- throw new Error("LLM 未返回预期的响应内容");
75946
- }
75947
- catch (error) {
75948
- console.error("LLM 请求失败:", error);
75949
- throw error;
75950
- }
76352
+ const srtNodes = detail
76353
+ .filter((word) => word.t.trim() !== "")
76354
+ .map((word) => ({
76355
+ type: "cue",
76356
+ data: {
76357
+ start: word.st + offset,
76358
+ end: word.et + offset,
76359
+ text: word.t,
76360
+ },
76361
+ }));
76362
+ return stringifySync(srtNodes, { format: "SRT" });
75951
76363
  }
75952
76364
  function getVendor(vendorId) {
75953
76365
  const data = index.appConfig.get("ai") || {};
@@ -75957,22 +76369,11 @@ function getVendor(vendorId) {
75957
76369
  }
75958
76370
  return vendor;
75959
76371
  }
75960
- /**
75961
- * 获取歌词优化配置
75962
- */
75963
- function getLyricOptimizeConfig() {
75964
- const { data, llmVendorId, llmModel } = getSongRecognizeConfig();
75965
- return {
75966
- vendorId: data?.songLyricOptimize?.vendorId || llmVendorId,
75967
- prompt: data?.songLyricOptimize?.prompt,
75968
- model: data?.songLyricOptimize?.model || llmModel,
75969
- enableStructuredOutput: data?.songLyricOptimize?.enableStructuredOutput ?? true,
75970
- };
75971
- }
75972
76372
  /**
75973
76373
  * 歌词优化
75974
76374
  * @param lyrics - 原始歌词文本
75975
76375
  * @param offset - 偏移时间,单位毫秒
76376
+ * @returns 优化后的 ASRWord 数组
75976
76377
  */
75977
76378
  async function optimizeLyrics(asrData, lyrics, offset) {
75978
76379
  const { vendorId, prompt, model, enableStructuredOutput } = getLyricOptimizeConfig();
@@ -75983,15 +76384,24 @@ async function optimizeLyrics(asrData, lyrics, offset) {
75983
76384
  baseURL: baseURL,
75984
76385
  });
75985
76386
  const asrCleanedSentences = [];
75986
- for (const transcript of asrData.transcripts || []) {
75987
- for (const sentence of transcript.sentences || []) {
75988
- for (const word of sentence.words || []) {
75989
- asrCleanedSentences.push({
75990
- st: word.begin_time,
75991
- et: word.end_time,
75992
- t: word.text,
75993
- });
75994
- }
76387
+ if (asrData.words && asrData.words.length !== 0) {
76388
+ // 优先使用词级时间戳
76389
+ for (const word of asrData.words) {
76390
+ asrCleanedSentences.push({
76391
+ st: word.start * 1000, // 秒转毫秒
76392
+ et: word.end * 1000,
76393
+ t: word.word,
76394
+ });
76395
+ }
76396
+ }
76397
+ if (asrCleanedSentences.length === 0) {
76398
+ // 回退到分段级时间戳
76399
+ for (const segment of asrData.segments) {
76400
+ asrCleanedSentences.push({
76401
+ st: segment.start * 1000, // 秒转毫秒
76402
+ et: segment.end * 1000,
76403
+ t: segment.text,
76404
+ });
75995
76405
  }
75996
76406
  }
75997
76407
  index.logObj.info("使用 LLM 进行歌词优化...", {
@@ -76011,8 +76421,13 @@ async function optimizeLyrics(asrData, lyrics, offset) {
76011
76421
  try {
76012
76422
  const json = JSON.parse(response.content);
76013
76423
  const aSRWords = Array.isArray(json) ? json : json.data;
76014
- const srtData = convert2Srt2(aSRWords, offset);
76015
- return srtData;
76424
+ // 应用偏移时间
76425
+ const wordsWithOffset = aSRWords.map((word) => ({
76426
+ st: word.st + offset,
76427
+ et: word.et + offset,
76428
+ t: word.t,
76429
+ }));
76430
+ return wordsWithOffset;
76016
76431
  }
76017
76432
  catch (e) {
76018
76433
  index.logObj.error("LLM 返回内容非 JSON 格式,尝试按纯文本处理", e);
@@ -76023,30 +76438,48 @@ async function optimizeLyrics(asrData, lyrics, offset) {
76023
76438
  * 获取歌曲识别配置
76024
76439
  */
76025
76440
  function getSongRecognizeConfig() {
76026
- const data = index.appConfig.get("ai") || {};
76027
- if (data?.vendors.length === 0) {
76028
- throw new Error("请先在配置中设置 AI 服务商的 API Key");
76029
- }
76030
- const asrVendorId = data.vendors.find((v) => v.provider === "aliyun")?.id;
76031
- if (!asrVendorId) {
76032
- throw new Error("请先在配置中设置 阿里云 ASR 服务商的 API Key");
76441
+ const config = index.appConfig.getAll();
76442
+ const data = config.ai;
76443
+ const asrModelId = data.songRecognizeAsr?.modelId;
76444
+ if (!asrModelId) {
76445
+ throw new Error("请先在配置中设置歌曲识别 ASR 模型");
76033
76446
  }
76034
- let llmVendorId = asrVendorId;
76035
- if (data?.songRecognizeLlm?.vendorId) {
76036
- llmVendorId = data.songRecognizeLlm.vendorId;
76447
+ const llmModel = getModel(data?.songRecognizeLlm?.modelId, config);
76448
+ const llmVendor = data.vendors.find((v) => v.id === llmModel.vendorId);
76449
+ if (!llmVendor) {
76450
+ throw new Error("找不到LLM模型关联的供应商");
76037
76451
  }
76038
76452
  return {
76039
76453
  data,
76040
- asrVendorId,
76041
- llmVendorId,
76454
+ asrModelId,
76455
+ llmVendorId: llmVendor.id,
76456
+ llmModel: llmModel.modelName,
76042
76457
  llmPrompt: data?.songRecognizeLlm?.prompt,
76043
- llmModel: data?.songRecognizeLlm?.model || "qwen-plus",
76044
76458
  enableSearch: data?.songRecognizeLlm?.enableSearch ?? false,
76045
76459
  maxInputLength: data?.songRecognizeLlm?.maxInputLength || 300,
76046
76460
  enableStructuredOutput: data?.songRecognizeLlm?.enableStructuredOutput ?? true,
76047
76461
  lyricOptimize: data?.songRecognizeLlm?.lyricOptimize ?? true,
76048
76462
  };
76049
76463
  }
76464
+ /**
76465
+ * 获取歌词优化配置
76466
+ */
76467
+ function getLyricOptimizeConfig() {
76468
+ const config = index.appConfig.getAll();
76469
+ const data = config.ai;
76470
+ let modelId = data?.songLyricOptimize?.modelId;
76471
+ const model = getModel(modelId);
76472
+ const vendor = data.vendors.find((v) => v.id === model.vendorId);
76473
+ if (!vendor) {
76474
+ throw new Error("找不到LLM模型关联的供应商");
76475
+ }
76476
+ return {
76477
+ vendorId: vendor.id,
76478
+ prompt: data?.songLyricOptimize?.prompt,
76479
+ model: model.modelName,
76480
+ enableStructuredOutput: data?.songLyricOptimize?.enableStructuredOutput ?? true,
76481
+ };
76482
+ }
76050
76483
  /**
76051
76484
  * 使用 LLM 识别歌曲名称
76052
76485
  * @param asrText - ASR 识别的文本内容
@@ -76100,7 +76533,7 @@ async function recognizeSongNameWithLLM(asrText, vendorId, options) {
76100
76533
  * @param audioStartTime - 音频开始时间(秒)
76101
76534
  */
76102
76535
  async function songRecognize(file, audioStartTime = 0) {
76103
- const { asrVendorId, llmVendorId, llmPrompt, llmModel, enableSearch, maxInputLength, enableStructuredOutput, lyricOptimize, } = getSongRecognizeConfig();
76536
+ const { asrModelId, llmVendorId, llmPrompt, llmModel, enableSearch, maxInputLength, enableStructuredOutput, lyricOptimize, } = getSongRecognizeConfig();
76104
76537
  let info = await recognize(file, lyricOptimize);
76105
76538
  if (!info) {
76106
76539
  index.logObj.warn("Shazam 未识别到任何歌曲信息");
@@ -76114,8 +76547,8 @@ async function songRecognize(file, audioStartTime = 0) {
76114
76547
  };
76115
76548
  }
76116
76549
  // 如果开启了歌词优化,首先要asr识别
76117
- const data = await asrRecognize(file, asrVendorId);
76118
- const messages = data.transcripts?.[0]?.text || "";
76550
+ const data = await recognize$1(file, asrModelId);
76551
+ const messages = data.text || "";
76119
76552
  if (!messages) {
76120
76553
  index.logObj.warn("没有识别到任何文本内容,无法进行歌曲识别");
76121
76554
  return;
@@ -76135,15 +76568,30 @@ async function songRecognize(file, audioStartTime = 0) {
76135
76568
  return;
76136
76569
  }
76137
76570
  const rawLyrics = info?.lyrics;
76138
- let srtData = "";
76139
- if (rawLyrics) {
76140
- if (lyricOptimize) {
76141
- srtData = await optimizeLyrics(data, rawLyrics, audioStartTime * 1000);
76571
+ let words = [];
76572
+ if (rawLyrics && lyricOptimize) {
76573
+ try {
76574
+ words = await optimizeLyrics(data, rawLyrics, audioStartTime * 1000);
76142
76575
  }
76143
- else {
76144
- srtData = convert2Srt(optimizeMusicSubtitles(data), audioStartTime * 1000);
76576
+ catch (error) {
76577
+ index.logObj.error("歌词优化失败:", error);
76145
76578
  }
76146
76579
  }
76580
+ if (words.length === 0) {
76581
+ // 如果没有歌词优化结果,使用原始 ASR 结果
76582
+ const segments = data.segments || [];
76583
+ // 使用词级时间戳生成 ASRWord 数组
76584
+ words = segments.map((sentence) => {
76585
+ let text = sentence.text.replace(/[,。、“”‘’!?]/g, " ");
76586
+ return {
76587
+ st: sentence.start * 1000 + audioStartTime * 1000,
76588
+ et: sentence.end * 1000 + audioStartTime * 1000,
76589
+ t: text,
76590
+ };
76591
+ });
76592
+ }
76593
+ // 统一生成 SRT
76594
+ const srtData = convert2Srt(words, 0);
76147
76595
  const name = info?.name;
76148
76596
  return {
76149
76597
  name: name,
@@ -76621,7 +77069,7 @@ router$8.post("/formatTitle", async (ctx) => {
76621
77069
  router$8.post("/formatPartTitle", async (ctx) => {
76622
77070
  const data = ctx.request.body;
76623
77071
  const template = (data.template || "");
76624
- const title = formatPartTitle({
77072
+ const title = formatPartTitle(data.options ?? {
76625
77073
  title: "标题",
76626
77074
  username: "主播名",
76627
77075
  time: new Date().toISOString(),
@@ -78315,24 +78763,197 @@ router$2.post("/sync", async (ctx) => {
78315
78763
  ctx.body = task.taskId;
78316
78764
  });
78317
78765
 
78766
+ /**
78767
+ * 根据标准 ASR 结果生成优化的 SRT 字幕
78768
+ * @param result - 标准 ASR 识别结果
78769
+ * @param options - 配置选项
78770
+ * @returns SRT 格式的字幕字符串
78771
+ */
78772
+ function convertStandardResultToSrt(result, options) {
78773
+ const offset = options?.offset ?? 0;
78774
+ const maxLength = options?.maxLength ?? 20;
78775
+ const fillGap = options?.fillGap ?? 500;
78776
+ // 如果有词级别数据,使用词级别生成(更精确)
78777
+ if (result.words && result.words.length > 0) {
78778
+ return convertWordsToSrt(result.words, { offset, maxLength, fillGap });
78779
+ }
78780
+ // 否则直接使用分段数据,不做分割和补全
78781
+ const srtNodes = result.segments
78782
+ .filter((segment) => segment.text.trim())
78783
+ .map((segment) => ({
78784
+ type: "cue",
78785
+ data: {
78786
+ start: segment.start * 1000 + offset, // 转换为毫秒
78787
+ end: segment.end * 1000 + offset,
78788
+ text: segment.text.trim(),
78789
+ },
78790
+ }));
78791
+ return stringifySync(srtNodes, { format: "SRT" });
78792
+ }
78793
+ /**
78794
+ * 根据词级别数据生成 SRT 字幕
78795
+ */
78796
+ function convertWordsToSrt(words, options) {
78797
+ const { offset, maxLength, fillGap } = options;
78798
+ const subtitles = [];
78799
+ let accumulatedText = "";
78800
+ let startTime = 0;
78801
+ let endTime = 0;
78802
+ for (let i = 0; i < words.length; i++) {
78803
+ const word = words[i];
78804
+ if (accumulatedText === "") {
78805
+ startTime = word.start;
78806
+ }
78807
+ // 累积词文本
78808
+ accumulatedText += word.word;
78809
+ endTime = word.end;
78810
+ // 检查是否需要断句
78811
+ let shouldBreak = false;
78812
+ // 1. 如果有标点符号且字符数大于4,断句
78813
+ if (word.punctuation && accumulatedText.length > 4) {
78814
+ accumulatedText += word.punctuation;
78815
+ shouldBreak = true;
78816
+ }
78817
+ // 如果有标点但字符数不够,只添加标点继续累积
78818
+ else if (word.punctuation) {
78819
+ accumulatedText += word.punctuation;
78820
+ }
78821
+ // 2. 长度超过最大限制,强制断句
78822
+ else if (accumulatedText.length >= maxLength) {
78823
+ shouldBreak = true;
78824
+ }
78825
+ if (shouldBreak) {
78826
+ const cleanedText = accumulatedText.replace(/[\p{P}\p{S}]+$/u, "").trim();
78827
+ if (cleanedText) {
78828
+ subtitles.push({
78829
+ start: startTime * 1000 + offset, // 转换为毫秒
78830
+ end: endTime * 1000 + offset,
78831
+ text: cleanedText,
78832
+ });
78833
+ }
78834
+ accumulatedText = "";
78835
+ startTime = 0;
78836
+ endTime = 0;
78837
+ }
78838
+ }
78839
+ // 处理剩余文本
78840
+ if (accumulatedText.trim()) {
78841
+ const cleanedText = accumulatedText.replace(/[\p{P}\p{S}]+$/u, "").trim();
78842
+ if (cleanedText) {
78843
+ subtitles.push({
78844
+ start: startTime * 1000 + offset,
78845
+ end: endTime * 1000 + offset,
78846
+ text: cleanedText,
78847
+ });
78848
+ }
78849
+ }
78850
+ // 补齐字幕间隔
78851
+ if (fillGap > 0) {
78852
+ for (let i = 0; i < subtitles.length - 1; i++) {
78853
+ const current = subtitles[i];
78854
+ const next = subtitles[i + 1];
78855
+ const gap = next.start - current.end;
78856
+ if (gap > 0 && gap <= fillGap) {
78857
+ current.end += gap * 0.6;
78858
+ next.start -= gap * 0.4;
78859
+ }
78860
+ }
78861
+ }
78862
+ // 生成 SRT
78863
+ const srtNodes = subtitles.map((sub) => ({
78864
+ type: "cue",
78865
+ data: {
78866
+ start: sub.start,
78867
+ end: sub.end,
78868
+ text: sub.text,
78869
+ },
78870
+ }));
78871
+ return stringifySync(srtNodes, { format: "SRT" });
78872
+ }
78873
+ /**
78874
+ * 字幕识别 - 将音频/视频文件转换为 SRT 字幕
78875
+ * @param file - 音频或视频文件路径
78876
+ * @param vendorId - AI 服务商 ID
78877
+ * @param options - 配置选项
78878
+ * @returns SRT 格式的字幕字符串
78879
+ */
78880
+ async function subtitleRecognize(file, modelId, options) {
78881
+ const offset = (options?.offset ?? 0) * 1000; // 转换为毫秒
78882
+ const maxLength = options?.maxLength ?? 26;
78883
+ const timeGapThreshold = options?.timeGapThreshold ?? 300;
78884
+ const fillGap = options?.fillGap ?? 1000;
78885
+ const disableCache = options?.disableCache;
78886
+ index.logObj.info("开始字幕识别", {
78887
+ file,
78888
+ offset,
78889
+ maxLength,
78890
+ timeGapThreshold,
78891
+ fillGap,
78892
+ disableCache,
78893
+ });
78894
+ try {
78895
+ // 生成缓存路径
78896
+ const cachePath = index.getTempPath();
78897
+ const fileHash = await index.calculateFileQuickHash(file);
78898
+ // TODO:缓存有bug,第一个是多模型切换、第二个是不同参数热词参数不同
78899
+ const cacheFileName = `asr_subtitle_cache_${fileHash}_${modelId}.json`;
78900
+ const cacheFilePath = path$7.join(cachePath, cacheFileName);
78901
+ let asrResult = null;
78902
+ // 尝试从缓存读取
78903
+ if (!disableCache && (await index.fs.pathExists(cacheFilePath))) ;
78904
+ if (!asrResult) {
78905
+ // 调用 ASR 识别(使用新的统一接口)
78906
+ asrResult = await recognize$1(file, modelId);
78907
+ // 保存到缓存(如果未禁用缓存)
78908
+ if (!disableCache) ;
78909
+ }
78910
+ if (!asrResult.segments || asrResult.segments.length === 0) {
78911
+ index.logObj.warn("ASR 未识别到任何内容");
78912
+ return "";
78913
+ }
78914
+ // 使用新的标准格式转换函数
78915
+ const srt = convertStandardResultToSrt(asrResult, {
78916
+ offset,
78917
+ maxLength,
78918
+ fillGap,
78919
+ });
78920
+ index.logObj.info("字幕识别完成", { subtitleCount: srt.split("\n\n").length - 1 });
78921
+ return srt;
78922
+ }
78923
+ catch (error) {
78924
+ index.logObj.error("字幕识别失败:", error);
78925
+ throw error;
78926
+ }
78927
+ }
78928
+
78318
78929
  const router$1 = new Router$1({
78319
78930
  prefix: "/ai",
78320
78931
  });
78321
- router$1.post("/asr", async (ctx) => {
78322
- const data = ctx.request.body;
78323
- const result = await asrRecognize(data.file, data.vendorId);
78324
- ctx.body = result;
78325
- });
78326
- router$1.post("/llm", async (ctx) => {
78327
- const data = ctx.request.body;
78328
- const result = await llm(data.message, data.systemPrompt, {
78329
- enableSearch: data.enableSearch,
78330
- key: undefined,
78331
- jsonResponse: data.jsonResponse,
78332
- stream: data.stream,
78333
- });
78334
- ctx.body = result;
78335
- });
78932
+ // router.post("/asr", async (ctx) => {
78933
+ // const data = ctx.request.body as {
78934
+ // file: string;
78935
+ // vendorId: string;
78936
+ // model: string;
78937
+ // };
78938
+ // const result = await asrRecognize(data.file, { vendorId: data.vendorId, model: data.model });
78939
+ // ctx.body = result;
78940
+ // });
78941
+ // router.post("/llm", async (ctx) => {
78942
+ // const data = ctx.request.body as {
78943
+ // message: string;
78944
+ // systemPrompt?: string;
78945
+ // enableSearch?: boolean;
78946
+ // jsonResponse?: boolean;
78947
+ // stream?: boolean;
78948
+ // };
78949
+ // const result = await llm(data.message, data.systemPrompt, {
78950
+ // enableSearch: data.enableSearch,
78951
+ // key: undefined,
78952
+ // jsonResponse: data.jsonResponse,
78953
+ // stream: data.stream,
78954
+ // });
78955
+ // ctx.body = result;
78956
+ // });
78336
78957
  router$1.post("/song_recognize", async (ctx) => {
78337
78958
  const data = ctx.request.body;
78338
78959
  if (!data.file || data.startTime == null || data.endTime == null) {
@@ -78367,6 +78988,66 @@ router$1.post("/song_recognize", async (ctx) => {
78367
78988
  index.fs.remove(outputFile);
78368
78989
  ctx.body = result;
78369
78990
  });
78991
+ router$1.post("/subtitle", async (ctx) => {
78992
+ const data = ctx.request.body;
78993
+ if (!data.file) {
78994
+ ctx.status = 400;
78995
+ ctx.body = {
78996
+ error: "参数错误,必须包含 file 字段",
78997
+ };
78998
+ return;
78999
+ }
79000
+ const config = index.appConfig.get("ai") || {};
79001
+ const asrModelId = config.subtitleRecognize.modelId;
79002
+ if (!asrModelId) {
79003
+ throw new Error("请先在配置中设置字幕识别ASR模型");
79004
+ }
79005
+ try {
79006
+ let audioFile = data.file;
79007
+ let needCleanup = false;
79008
+ // 如果指定了时间范围,需要先提取音频片段
79009
+ if (data.startTime != null && data.endTime != null) {
79010
+ const cachePath = index.getTempPath();
79011
+ const fileName = `${index.uuid()}.mp3`;
79012
+ const task = await index.addExtractAudioTask(data.file, fileName, {
79013
+ startTime: data.startTime,
79014
+ endTime: data.endTime,
79015
+ saveType: 2,
79016
+ savePath: cachePath,
79017
+ autoRun: true,
79018
+ addQueue: false,
79019
+ format: "libmp3lame",
79020
+ audioBitrate: "192k",
79021
+ });
79022
+ audioFile = await new Promise((resolve, reject) => {
79023
+ task.on("task-end", () => {
79024
+ resolve(task.output);
79025
+ });
79026
+ task.on("task-error", (err) => {
79027
+ reject(err);
79028
+ });
79029
+ });
79030
+ needCleanup = true;
79031
+ }
79032
+ const srt = await subtitleRecognize(audioFile, asrModelId, {
79033
+ offset: data.offset,
79034
+ disableCache: true,
79035
+ });
79036
+ // 清理临时音频文件
79037
+ if (needCleanup) {
79038
+ index.fs.remove(audioFile);
79039
+ }
79040
+ ctx.body = {
79041
+ srt,
79042
+ };
79043
+ }
79044
+ catch (error) {
79045
+ ctx.status = 500;
79046
+ ctx.body = {
79047
+ error: error.message || "字幕识别失败",
79048
+ };
79049
+ }
79050
+ });
78370
79051
 
78371
79052
  /**
78372
79053
  * 文件引用计数管理器
@@ -79593,8 +80274,8 @@ class WebhookHandler {
79593
80274
  // 5. 处理弹幕和视频压制
79594
80275
  const processingResult = await this.processMediaFiles(context, options, config, xmlFilePath);
79595
80276
  index.logObj.debug("processingResult", processingResult, options.filePath, context.part.filePath);
79596
- this.collectTasks(context.part.filePath, "handledVideo", config);
79597
80277
  if (processingResult.conversionSuccessful) {
80278
+ this.collectTasks(context.part.filePath, "handledVideo", config);
79598
80279
  this.fileRefManager.releaseRef(options.filePath);
79599
80280
  }
79600
80281
  if (xmlFilePath && processingResult.danmuConversionSuccessful) {
@@ -80292,7 +80973,7 @@ class WebhookHandler {
80292
80973
  await this.addEditMediaTask(config.uid, aid, filePaths.map((item) => ({
80293
80974
  path: item.path,
80294
80975
  title: item.title,
80295
- })), limitedUploadTime, type === "raw" ? "none" : config.afterUploadDeletAction);
80976
+ })), limitedUploadTime, config.afterUploadDeletAction);
80296
80977
  live.batchUpdateUploadStatus(filePaths.map((item) => item.part), "uploaded", type);
80297
80978
  }
80298
80979
  /**
@@ -80308,7 +80989,7 @@ class WebhookHandler {
80308
80989
  const aid = (await this.addUploadTask(config.uid, filePaths.map((item) => ({
80309
80990
  path: item.path,
80310
80991
  title: item.title,
80311
- })), uploadPreset, limitedUploadTime, type === "raw" ? "none" : config.afterUploadDeletAction));
80992
+ })), uploadPreset, limitedUploadTime, config.afterUploadDeletAction));
80312
80993
  live[aidField] = Number(aid);
80313
80994
  live.batchUpdateUploadStatus(filePaths.map((item) => item.part), "uploaded", type);
80314
80995
  }
@@ -80413,7 +81094,7 @@ exports.handler = void 0;
80413
81094
  exports.appConfig = void 0;
80414
81095
  exports.container = void 0;
80415
81096
  const fileCache = createFileCache();
80416
- path$7.dirname(require$$0$4.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-CWr6rOqY.cjs', document.baseURI).href))));
81097
+ path$7.dirname(require$$0$4.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DTf_Oo8U.cjs', document.baseURI).href))));
80417
81098
  const authMiddleware = (passKey) => {
80418
81099
  return async (ctx, next) => {
80419
81100
  const authHeader = ctx.headers["authorization"] || ctx.request.query.auth;