feat(server): improve transcript error handle (#11293)

This commit is contained in:
darkskygit
2025-03-31 09:30:32 +00:00
parent 6a66bf53a8
commit b37182ceb7
5 changed files with 146 additions and 97 deletions

View File

@@ -18,24 +18,6 @@ import { OpenAIEmbeddingClient } from './embedding';
import { EmbeddingClient } from './types'; import { EmbeddingClient } from './types';
import { readStream } from './utils'; import { readStream } from './utils';
declare global {
interface Jobs {
'doc.embedPendingDocs': {
workspaceId: string;
docId: string;
};
'doc.embedPendingFiles': {
contextId: string;
userId: string;
workspaceId: string;
blobId: string;
fileId: string;
fileName: string;
};
}
}
@Injectable() @Injectable()
export class CopilotContextDocJob { export class CopilotContextDocJob {
private supportEmbedding = false; private supportEmbedding = false;

View File

@@ -21,6 +21,21 @@ declare global {
error: string; error: string;
}; };
} }
interface Jobs {
'doc.embedPendingDocs': {
workspaceId: string;
docId: string;
};
'doc.embedPendingFiles': {
contextId: string;
userId: string;
workspaceId: string;
blobId: string;
fileId: string;
fileName: string;
};
}
} }
export const MAX_EMBEDDABLE_SIZE = 50 * OneMB; export const MAX_EMBEDDABLE_SIZE = 50 * OneMB;

View File

@@ -364,6 +364,7 @@ The output should be a JSON array, with each element containing:
- Ensure the accurate differentiation of speakers even if multiple speakers overlap slightly or switch rapidly. - Ensure the accurate differentiation of speakers even if multiple speakers overlap slightly or switch rapidly.
- Maintain a consistent speaker labeling system throughout the transcription. - Maintain a consistent speaker labeling system throughout the transcription.
- If the provided audio or data does not contain valid talk, you should return an empty JSON array.
`, `,
}, },
], ],

View File

@@ -4,9 +4,11 @@ import { AiJobStatus, AiJobType } from '@prisma/client';
import { import {
CopilotPromptNotFound, CopilotPromptNotFound,
CopilotTranscriptionJobExists, CopilotTranscriptionJobExists,
EventBus,
type FileUpload, type FileUpload,
JobQueue, JobQueue,
NoCopilotProviderAvailable, NoCopilotProviderAvailable,
OnEvent,
OnJob, OnJob,
} from '../../../base'; } from '../../../base';
import { Models } from '../../../models'; import { Models } from '../../../models';
@@ -34,6 +36,7 @@ export type TranscriptionJob = {
@Injectable() @Injectable()
export class CopilotTranscriptionService { export class CopilotTranscriptionService {
constructor( constructor(
private readonly event: EventBus,
private readonly models: Models, private readonly models: Models,
private readonly job: JobQueue, private readonly job: JobQueue,
private readonly storage: CopilotStorage, private readonly storage: CopilotStorage,
@@ -65,16 +68,11 @@ export class CopilotTranscriptionService {
status: AiJobStatus.running, status: AiJobStatus.running,
}); });
await this.job.add( await this.job.add('copilot.transcript.submit', {
'copilot.transcript.submit', jobId,
{ url,
jobId, mimeType: blob.mimetype,
url, });
mimeType: blob.mimetype,
},
// retry 3 times
{ removeOnFail: 3 }
);
return { id: jobId, status }; return { id: jobId, status };
} }
@@ -114,9 +112,11 @@ export class CopilotTranscriptionService {
const ret: TranscriptionJob = { id: job.id, status: job.status }; const ret: TranscriptionJob = { id: job.id, status: job.status };
const payload = TranscriptPayloadSchema.safeParse(job.payload); if (job.status === AiJobStatus.claimed) {
if (payload.success) { const payload = TranscriptPayloadSchema.safeParse(job.payload);
ret.transcription = payload.data; if (payload.success) {
ret.transcription = payload.data;
}
} }
return ret; return ret;
@@ -164,81 +164,124 @@ export class CopilotTranscriptionService {
url, url,
mimeType, mimeType,
}: Jobs['copilot.transcript.submit']) { }: Jobs['copilot.transcript.submit']) {
const result = await this.chatWithPrompt('Transcript audio', { try {
attachments: [url], const result = await this.chatWithPrompt('Transcript audio', {
params: { mimetype: mimeType }, attachments: [url],
}); params: { mimetype: mimeType },
const transcription = TranscriptionSchema.parse(
JSON.parse(this.cleanupResponse(result))
);
await this.models.copilotJob.update(jobId, {
payload: { transcription },
});
await this.job.add(
'copilot.transcriptSummary.submit',
{
jobId,
},
// retry 3 times
{ removeOnFail: 3 }
);
}
@OnJob('copilot.transcriptSummary.submit')
async transcriptSummary({ jobId }: Jobs['copilot.transcriptSummary.submit']) {
const payload = await this.models.copilotJob.getPayload(
jobId,
TranscriptPayloadSchema
);
if (payload.transcription) {
const content = payload.transcription
.map(t => t.transcription)
.join('\n');
const result = await this.chatWithPrompt('Summary', { content });
payload.summary = this.cleanupResponse(result);
await this.models.copilotJob.update(jobId, {
payload,
}); });
await this.job.add( const transcription = TranscriptionSchema.parse(
'copilot.transcriptTitle.submit', JSON.parse(this.cleanupResponse(result))
{ jobId },
// retry 3 times
{ removeOnFail: 3 }
); );
} else {
await this.models.copilotJob.update(jobId, { await this.models.copilotJob.update(jobId, {
status: AiJobStatus.failed, payload: { transcription },
}); });
await this.job.add('copilot.transcript.summary.submit', {
jobId,
});
return;
} catch (error: any) {
// record failed status and passthrough error
this.event.emit('workspace.file.transcript.failed', {
jobId,
});
throw error;
} }
} }
@OnJob('copilot.transcriptTitle.submit') @OnJob('copilot.transcript.summary.submit')
async transcriptTitle({ jobId }: Jobs['copilot.transcriptTitle.submit']) { async transcriptSummary({
const payload = await this.models.copilotJob.getPayload( jobId,
jobId, }: Jobs['copilot.transcript.summary.submit']) {
TranscriptPayloadSchema try {
); const payload = await this.models.copilotJob.getPayload(
if (payload.transcription && payload.summary) { jobId,
const content = payload.transcription TranscriptPayloadSchema
.map(t => t.transcription) );
.join('\n'); if (payload.transcription) {
const content = payload.transcription
.map(t => t.transcription.trim())
.join('\n')
.trim();
const result = await this.chatWithPrompt('Summary as title', { content }); if (content.length) {
const result = await this.chatWithPrompt('Summary', {
content,
});
payload.title = this.cleanupResponse(result); payload.summary = this.cleanupResponse(result);
await this.models.copilotJob.update(jobId, { await this.models.copilotJob.update(jobId, {
payload, payload,
status: AiJobStatus.finished, });
});
} else { await this.job.add('copilot.transcript.title.submit', {
await this.models.copilotJob.update(jobId, { jobId,
status: AiJobStatus.failed, });
return;
}
}
} catch (error: any) {
// record failed status and passthrough error
this.event.emit('workspace.file.transcript.failed', {
jobId,
}); });
throw error;
} }
} }
@OnJob('copilot.transcript.title.submit')
async transcriptTitle({ jobId }: Jobs['copilot.transcript.title.submit']) {
try {
const payload = await this.models.copilotJob.getPayload(
jobId,
TranscriptPayloadSchema
);
if (payload.transcription && payload.summary) {
const content = payload.transcription
.map(t => t.transcription.trim())
.join('\n')
.trim();
if (content.length) {
const result = await this.chatWithPrompt('Summary as title', {
content,
});
payload.title = this.cleanupResponse(result);
await this.models.copilotJob.update(jobId, {
payload,
});
this.event.emit('workspace.file.transcript.finished', {
jobId,
});
return;
}
}
} catch (error: any) {
// record failed status and passthrough error
this.event.emit('workspace.file.transcript.failed', {
jobId,
});
throw error;
}
}
@OnEvent('workspace.file.transcript.finished')
async onFileTranscriptFinish({
jobId,
}: Events['workspace.file.transcript.finished']) {
await this.models.copilotJob.update(jobId, {
status: AiJobStatus.finished,
});
}
@OnEvent('workspace.file.transcript.failed')
async onFileTranscriptFailed({
jobId,
}: Events['workspace.file.transcript.failed']) {
await this.models.copilotJob.update(jobId, {
status: AiJobStatus.failed,
});
}
} }

View File

@@ -22,16 +22,24 @@ export type Transcription = z.infer<typeof TranscriptionSchema>;
export type TranscriptionPayload = z.infer<typeof TranscriptPayloadSchema>; export type TranscriptionPayload = z.infer<typeof TranscriptPayloadSchema>;
declare global { declare global {
interface Events {
'workspace.file.transcript.finished': {
jobId: string;
};
'workspace.file.transcript.failed': {
jobId: string;
};
}
interface Jobs { interface Jobs {
'copilot.transcript.submit': { 'copilot.transcript.submit': {
jobId: string; jobId: string;
url: string; url: string;
mimeType: string; mimeType: string;
}; };
'copilot.transcriptSummary.submit': { 'copilot.transcript.summary.submit': {
jobId: string; jobId: string;
}; };
'copilot.transcriptTitle.submit': { 'copilot.transcript.title.submit': {
jobId: string; jobId: string;
}; };
} }