Skip to content

Commit b4d3e6a

Browse files
committed
whisper issue
1 parent 9686281 commit b4d3e6a

File tree

10 files changed

+63
-146
lines changed

10 files changed

+63
-146
lines changed

src/backend/errors/CancelByUserError.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/backend/errors/errors.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { ExtendableError } from "ts-error";
1+
import { ExtendableError } from 'ts-error';
22

33
/**
44
* Whisper 相应格式错误
55
*/
6-
export class WhisperResponseFormatError extends ExtendableError {}
6+
export class WhisperResponseFormatError extends ExtendableError {
7+
}
8+
9+
/**
10+
* 任务被用户取消
11+
*/
12+
export class CancelByUserError extends ExtendableError {
13+
}

src/backend/objs/OpenAiWhisperRequest.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,9 @@ import RateLimiter from '@/common/utils/RateLimiter';
44
import StrUtil from '@/common/utils/str-util';
55
import { Cancelable } from '@/common/interfaces';
66
import OpenAI from 'openai';
7-
8-
import { z } from 'zod';
97
import dpLog from '@/backend/ioc/logger';
108
import { WhisperResponseFormatError } from '@/backend/errors/errors';
11-
import { WhisperResponseVerifySchema } from '@/common/types/video-info';
12-
13-
export interface WhisperResponse {
14-
language: string;
15-
duration: number;
16-
text: string;
17-
offset: number;
18-
segments: {
19-
seek: number;
20-
start: number;
21-
end: number;
22-
text: string;
23-
}[];
24-
}
9+
import { WhisperResponse, WhisperResponseVerifySchema } from '@/common/types/video-info';
2510

2611
class OpenAiWhisperRequest implements Cancelable {
2712
private readonly file: string;
@@ -55,9 +40,8 @@ class OpenAiWhisperRequest implements Cancelable {
5540
}
5641
return {
5742
language: transcription.language,
58-
duration: Number(transcription.duration),
43+
duration: transcription.duration,
5944
text: transcription.text,
60-
offset: 0,
6145
segments: transcription.segments?.map((seg) => ({
6246
seek: seg.seek,
6347
start: seg.start,

src/backend/objs/config-tender.ts

Lines changed: 1 addition & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import path from 'path';
1010
export class ConfigTender<T, S extends z.ZodType<T>> {
1111
private readonly configPath: string;
1212
private readonly schema: S;
13-
private cache: T | null = null;
1413

1514
constructor(configPath: string, schema: S, defaultValue?: T) {
1615
this.configPath = configPath;
@@ -32,98 +31,24 @@ export class ConfigTender<T, S extends z.ZodType<T>> {
3231
* 读取整个配置
3332
*/
3433
get(): T {
35-
if (this.cache) return this.cache;
36-
3734
try {
3835
const content = fs.readFileSync(this.configPath, 'utf-8');
3936
const parsed = JSON.parse(content);
40-
const validated = this.schema.parse(parsed);
41-
this.cache = validated;
42-
return validated;
37+
return this.schema.parse(parsed);
4338
} catch (error) {
4439
throw new Error(`Failed to read config: ${error}`);
4540
}
4641
}
4742

48-
/**
49-
* 读取配置,如果不存在则返回默认值
50-
*/
51-
getOrDefault(defaultValue: T): T {
52-
try {
53-
return this.get();
54-
} catch {
55-
return defaultValue;
56-
}
57-
}
58-
59-
/**
60-
* 获取特定键的值
61-
*/
62-
getKey<K extends keyof T>(key: K): T[K] {
63-
return this.get()[key];
64-
}
65-
6643
/**
6744
* 保存整个配置
6845
*/
6946
save(config: T): void {
7047
try {
7148
const validated = this.schema.parse(config);
7249
fs.writeFileSync(this.configPath, JSON.stringify(validated, null, 2));
73-
this.cache = validated;
7450
} catch (error) {
7551
throw new Error(`Failed to save config: ${error}`);
7652
}
7753
}
78-
79-
/**
80-
* 设置特定键的值
81-
*/
82-
setKey<K extends keyof T>(key: K, value: T[K]): void {
83-
const config = this.get();
84-
config[key] = value;
85-
this.save(config);
86-
}
87-
88-
/**
89-
* 清除缓存
90-
*/
91-
clearCache(): void {
92-
this.cache = null;
93-
}
9454
}
95-
96-
// 使用示例:
97-
/*
98-
const UserConfigSchema = z.object({
99-
name: z.string(),
100-
age: z.number(),
101-
preferences: z.object({
102-
theme: z.enum(['light', 'dark']),
103-
notifications: z.boolean()
104-
})
105-
});
106-
107-
type UserConfig = z.infer<typeof UserConfigSchema>;
108-
109-
const defaultConfig: UserConfig = {
110-
name: "Default User",
111-
age: 25,
112-
preferences: {
113-
theme: "light",
114-
notifications: true
115-
}
116-
};
117-
118-
const configTender = new ConfigTender<UserConfig, typeof UserConfigSchema>(
119-
'./config.json',
120-
UserConfigSchema,
121-
defaultConfig
122-
);
123-
124-
// 使用示例
125-
const config = configTender.get();
126-
const name = configTender.getKey('name');
127-
configTender.setKey('age', 30);
128-
configTender.save({...config, name: "New Name"});
129-
*/

src/backend/services/impl/DpTaskServiceImpl.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { DpTask, dpTask, DpTaskState, InsertDpTask } from '@/backend/db/tables/d
44

55
import LRUCache from 'lru-cache';
66
import TimeUtil from '@/common/utils/TimeUtil';
7-
import ErrorConstants from '@/common/constants/error-constants';
87
import { injectable, postConstruct } from 'inversify';
98
import DpTaskService from '@/backend/services/DpTaskService';
109
import dpLog from '@/backend/ioc/logger';
1110
import { Cancelable } from '@/common/interfaces';
12-
import CancelByUserError from '@/backend/errors/CancelByUserError';
11+
12+
import { CancelByUserError } from '@/backend/errors/errors';
1313

1414
@injectable()
1515
export default class DpTaskServiceImpl implements DpTaskService {
@@ -188,6 +188,9 @@ export default class DpTaskServiceImpl implements DpTaskService {
188188
public registerTask(taskId: number, process: Cancelable) {
189189
const existingProcesses = this.taskMapping.get(taskId) || [];
190190
this.taskMapping.set(taskId, [...existingProcesses, process]);
191+
if (this.cancelQueue.has(taskId)) {
192+
process.cancel();
193+
}
191194
}
192195

193196
}

src/backend/services/impl/FfmpegServiceImpl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import TYPES from '@/backend/ioc/types';
99
import FfmpegService from '@/backend/services/FfmpegService';
1010
import FfmpegTask from '@/backend/objs/FfmpegTask';
1111
import DpTaskService from '@/backend/services/DpTaskService';
12-
import CancelByUserError from '@/backend/errors/CancelByUserError';
1312
import dpLog from '@/backend/ioc/logger';
1413
import ffmpeg from 'fluent-ffmpeg';
1514
import LocationService, { ProgramType } from '@/backend/services/LocationService';
1615
import { VideoInfo } from '@/common/types/video-info';
16+
import { CancelByUserError } from '@/backend/errors/errors';
1717

1818
@injectable()
1919
export default class FfmpegServiceImpl implements FfmpegService {
@@ -100,6 +100,7 @@ export default class FfmpegServiceImpl implements FfmpegService {
100100
});
101101
});
102102
}
103+
103104
/**
104105
* 获取视频文件的详细信息
105106
*/

src/backend/services/impl/WhisperServiceImpl.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,37 @@ import path from 'path';
33
import { DpTaskState } from '@/backend/db/tables/dpTask';
44
import SrtUtil, { SrtLine } from '@/common/utils/SrtUtil';
55
import hash from 'object-hash';
6-
import { isErrorCancel } from '@/common/constants/error-constants';
76
import { inject, injectable } from 'inversify';
87
import DpTaskService from '../DpTaskService';
98
import TYPES from '@/backend/ioc/types';
109
import FfmpegService from '@/backend/services/FfmpegService';
1110
import WhisperService from '@/backend/services/WhisperService';
1211
import { TypeGuards } from '@/backend/utils/TypeGuards';
13-
import OpenAiWhisperRequest, { WhisperResponse } from '@/backend/objs/OpenAiWhisperRequest';
12+
import OpenAiWhisperRequest from '@/backend/objs/OpenAiWhisperRequest';
1413
import LocationService, { LocationType } from '@/backend/services/LocationService';
1514
import dpLog from '@/backend/ioc/logger';
1615
import { OpenAiService } from '@/backend/services/OpenAiService';
1716
import { WaitLock } from '@/common/utils/Lock';
18-
import { SplitChunk, WhisperContext, WhisperContextSchema } from '@/common/types/video-info';
17+
import { SplitChunk, WhisperContext, WhisperContextSchema, WhisperResponse } from '@/common/types/video-info';
1918
import { ConfigTender } from '@/backend/objs/config-tender';
2019
import FileUtil from '@/backend/utils/FileUtil';
21-
import { WhisperResponseFormatError } from '@/backend/errors/errors';
20+
import { CancelByUserError, WhisperResponseFormatError } from '@/backend/errors/errors';
2221

2322
/**
2423
* 将 Whisper 的 API 响应转换成 SRT 文件格式
2524
*/
26-
function toSrt(whisperResponses: WhisperResponse[]): string {
25+
function toSrt(chunks: SplitChunk[]): string {
2726
// 按 offset 排序确保顺序正确
28-
whisperResponses.sort((a, b) => a.offset - b.offset);
27+
chunks.sort((a, b) => a.offset - b.offset);
2928
let counter = 1;
3029
const lines: SrtLine[] = [];
31-
for (const wr of whisperResponses) {
32-
for (const segment of wr.segments) {
30+
for (const c of chunks) {
31+
const segments = c.response?.segments??[];
32+
for (const segment of segments) {
3333
lines.push({
3434
index: counter,
35-
start: segment.start + wr.offset,
36-
end: segment.end + wr.offset,
35+
start: segment.start + c.offset,
36+
end: segment.end + c.offset,
3737
contentEn: segment.text,
3838
contentZh: ''
3939
});
@@ -106,6 +106,8 @@ class WhisperServiceImpl implements WhisperService {
106106
const unfinishedChunks = context.chunks.filter(chunk => !chunk.response);
107107
if (unfinishedChunks.length === 0) {
108108
this.dpTaskService.finish(taskId, { progress: '转录完成' });
109+
context.state = 'done';
110+
configTender.save(context);
109111
return;
110112
}
111113

@@ -118,14 +120,20 @@ class WhisperServiceImpl implements WhisperService {
118120

119121
try {
120122
// 对所有分片并发执行转录
121-
await Promise.all(context.chunks.map(async (chunk) => {
123+
const results = await Promise.allSettled(context.chunks.map(async (chunk) => {
122124
// 如果该 chunk 已经有结果,则跳过
123125
if (chunk.response) return;
124126
await this.whisperThreeTimes(taskId, chunk);
125127
completedCount = context.chunks.filter(chunk => chunk.response).length;
126128
const progress = Math.floor((completedCount / context.chunks.length) * 100);
127129
this.dpTaskService.update({ id: taskId, progress: `正在转录 ${progress}%` });
128130
}));
131+
// 检查是否有错误
132+
for (const result of results) {
133+
if (result.status === 'rejected') {
134+
throw result.reason;
135+
}
136+
}
129137
} finally {
130138
// 保存当前状态
131139
configTender.save(context);
@@ -134,17 +142,16 @@ class WhisperServiceImpl implements WhisperService {
134142
// 整理结果,生成 SRT 文件
135143
const srtName = filePath.replace(path.extname(filePath), '.srt');
136144
dpLog.info(`[WhisperService] Task ID: ${taskId} - 生成 SRT 文件: ${srtName}`);
137-
const whisperResponses = context.chunks.map(chunk => chunk.response as WhisperResponse);
138-
fs.writeFileSync(srtName, toSrt(whisperResponses));
145+
fs.writeFileSync(srtName, toSrt(context.chunks));
139146

140147
// 完成任务,并保存状态
141-
context.state = 'processed';
148+
context.state = 'done';
142149
configTender.save(context);
143150
this.dpTaskService.finish(taskId, { progress: '转录完成' });
144151
} catch (error) {
145152
dpLog.error(error);
146153
if (!(error instanceof Error)) throw error;
147-
const cancel = isErrorCancel(error);
154+
const cancel = error instanceof CancelByUserError
148155
this.dpTaskService.update({
149156
id: taskId,
150157
status: cancel ? DpTaskState.CANCELLED : DpTaskState.FAILED,
@@ -188,7 +195,7 @@ class WhisperServiceImpl implements WhisperService {
188195
}
189196
this.dpTaskService.registerTask(taskId, req);
190197
const response = await req.invoke();
191-
return { ...response, offset: chunk.offset };
198+
return { ...response };
192199
}
193200

194201
/**
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
enum ErrorConstants {
2-
CLIP_EXISTS = 'Clip already exists',
32
CACHE_NOT_FOUND = 'Cache not found',
4-
CANCEL_MSG = 'dp-用户取消',
53
}
64

75
export default ErrorConstants;
8-
9-
export function isErrorCancel(e: unknown): boolean {
10-
return e instanceof Error &&(e.message === ErrorConstants.CANCEL_MSG || e.message === 'ffmpeg was killed with signal SIGKILL');
11-
}

src/common/utils/Util.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {DpTask, DpTaskState} from "@/backend/db/tables/dpTask";
1+
import { DpTask, DpTaskState } from '@/backend/db/tables/dpTask';
2+
import { Nullable } from 'vitest';
23

34

45
/**
@@ -35,16 +36,20 @@ export default class Util {
3536
static p = p;
3637
static arrayChanged = arrayChanged;
3738
static joinUrl = joinUrl;
39+
3840
public static isNull(obj: any): boolean {
3941
return obj === null || obj === undefined;
4042
}
43+
4144
public static isNotNull(obj: any): boolean {
4245
return !this.isNull(obj);
4346
}
47+
4448
public static trim(str: string | null | undefined): string {
4549
return (str ?? '').trim();
4650
}
47-
public static cmpTaskState(task:DpTask, status:(DpTaskState|'none')[]):boolean {
51+
52+
public static cmpTaskState(task: Nullable<DpTask>, status: (DpTaskState | 'none')[]): boolean {
4853
const taskStatus = task?.status;
4954
if (taskStatus === undefined || taskStatus === null) {
5055
return status.includes('none');
@@ -55,4 +60,4 @@ export default class Util {
5560

5661
export const emptyFunc = () => {
5762
return;
58-
}
63+
};

0 commit comments

Comments
 (0)