feat(server): improve transcript error handle (#11293)
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user