mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 22:38:56 +08:00
feat(server): retry transcript job (#11414)
This commit is contained in:
@@ -705,6 +705,10 @@ export const USER_FRIENDLY_ERRORS = {
|
|||||||
type: 'bad_request',
|
type: 'bad_request',
|
||||||
message: () => 'Transcription job already exists',
|
message: () => 'Transcription job already exists',
|
||||||
},
|
},
|
||||||
|
copilot_transcription_job_not_found: {
|
||||||
|
type: 'bad_request',
|
||||||
|
message: () => `Transcription job not found.`,
|
||||||
|
},
|
||||||
|
|
||||||
// Quota & Limit errors
|
// Quota & Limit errors
|
||||||
blob_quota_exceeded: {
|
blob_quota_exceeded: {
|
||||||
|
|||||||
@@ -765,6 +765,12 @@ export class CopilotTranscriptionJobExists extends UserFriendlyError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CopilotTranscriptionJobNotFound extends UserFriendlyError {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super('bad_request', 'copilot_transcription_job_not_found', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class BlobQuotaExceeded extends UserFriendlyError {
|
export class BlobQuotaExceeded extends UserFriendlyError {
|
||||||
constructor(message?: string) {
|
constructor(message?: string) {
|
||||||
super('quota_exceeded', 'blob_quota_exceeded', message);
|
super('quota_exceeded', 'blob_quota_exceeded', message);
|
||||||
@@ -1020,6 +1026,7 @@ export enum ErrorNames {
|
|||||||
COPILOT_FAILED_TO_MATCH_CONTEXT,
|
COPILOT_FAILED_TO_MATCH_CONTEXT,
|
||||||
COPILOT_EMBEDDING_UNAVAILABLE,
|
COPILOT_EMBEDDING_UNAVAILABLE,
|
||||||
COPILOT_TRANSCRIPTION_JOB_EXISTS,
|
COPILOT_TRANSCRIPTION_JOB_EXISTS,
|
||||||
|
COPILOT_TRANSCRIPTION_JOB_NOT_FOUND,
|
||||||
BLOB_QUOTA_EXCEEDED,
|
BLOB_QUOTA_EXCEEDED,
|
||||||
STORAGE_QUOTA_EXCEEDED,
|
STORAGE_QUOTA_EXCEEDED,
|
||||||
MEMBER_QUOTA_EXCEEDED,
|
MEMBER_QUOTA_EXCEEDED,
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import {
|
|||||||
import { AiJobStatus } from '@prisma/client';
|
import { AiJobStatus } from '@prisma/client';
|
||||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||||
|
|
||||||
import type { FileUpload } from '../../../base';
|
import {
|
||||||
|
CopilotTranscriptionJobNotFound,
|
||||||
|
type FileUpload,
|
||||||
|
} from '../../../base';
|
||||||
import { CurrentUser } from '../../../core/auth';
|
import { CurrentUser } from '../../../core/auth';
|
||||||
import { AccessController } from '../../../core/permission';
|
import { AccessController } from '../../../core/permission';
|
||||||
import { CopilotType } from '../resolver';
|
import { CopilotType } from '../resolver';
|
||||||
@@ -106,14 +109,44 @@ export class CopilotTranscriptionResolver {
|
|||||||
.allowLocal()
|
.allowLocal()
|
||||||
.assert('Workspace.Copilot');
|
.assert('Workspace.Copilot');
|
||||||
|
|
||||||
const job = await this.service.submitTranscriptionJob(
|
const jobResult = await this.service.submitTranscriptionJob(
|
||||||
user.id,
|
user.id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
blobId,
|
blobId,
|
||||||
blob
|
blob
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.handleJobResult(job);
|
return this.handleJobResult(jobResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => TranscriptionResultType, { nullable: true })
|
||||||
|
async retryAudioTranscription(
|
||||||
|
@CurrentUser() user: CurrentUser,
|
||||||
|
@Args('workspaceId') workspaceId: string,
|
||||||
|
@Args('jobId') jobId: string
|
||||||
|
): Promise<TranscriptionResultType | null> {
|
||||||
|
await this.ac
|
||||||
|
.user(user.id)
|
||||||
|
.workspace(workspaceId)
|
||||||
|
.allowLocal()
|
||||||
|
.assert('Workspace.Copilot');
|
||||||
|
|
||||||
|
const job = await this.service.queryTranscriptionJob(
|
||||||
|
user.id,
|
||||||
|
workspaceId,
|
||||||
|
jobId
|
||||||
|
);
|
||||||
|
if (!job || !job.url || !job.mimeType) {
|
||||||
|
throw new CopilotTranscriptionJobNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobResult = await this.service.executeTranscriptionJob(
|
||||||
|
job.id,
|
||||||
|
job.url,
|
||||||
|
job.mimeType
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.handleJobResult(jobResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TranscriptionResultType, { nullable: true })
|
@Mutation(() => TranscriptionResultType, { nullable: true })
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ZodType } from 'zod';
|
|||||||
import {
|
import {
|
||||||
CopilotPromptNotFound,
|
CopilotPromptNotFound,
|
||||||
CopilotTranscriptionJobExists,
|
CopilotTranscriptionJobExists,
|
||||||
|
CopilotTranscriptionJobNotFound,
|
||||||
EventBus,
|
EventBus,
|
||||||
type FileUpload,
|
type FileUpload,
|
||||||
JobQueue,
|
JobQueue,
|
||||||
@@ -31,6 +32,8 @@ import { readStream } from './utils';
|
|||||||
export type TranscriptionJob = {
|
export type TranscriptionJob = {
|
||||||
id: string;
|
id: string;
|
||||||
status: AiJobStatus;
|
status: AiJobStatus;
|
||||||
|
url?: string;
|
||||||
|
mimeType?: string;
|
||||||
transcription?: TranscriptionPayload;
|
transcription?: TranscriptionPayload;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,7 +58,7 @@ export class CopilotTranscriptionService {
|
|||||||
throw new CopilotTranscriptionJobExists();
|
throw new CopilotTranscriptionJobExists();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id: jobId, status } = await this.models.copilotJob.create({
|
const { id: jobId } = await this.models.copilotJob.create({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
blobId,
|
blobId,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
@@ -65,14 +68,28 @@ export class CopilotTranscriptionService {
|
|||||||
const buffer = await readStream(blob.createReadStream());
|
const buffer = await readStream(blob.createReadStream());
|
||||||
const url = await this.storage.put(userId, workspaceId, blobId, buffer);
|
const url = await this.storage.put(userId, workspaceId, blobId, buffer);
|
||||||
|
|
||||||
await this.models.copilotJob.update(jobId, {
|
return await this.executeTranscriptionJob(jobId, url, blob.mimetype);
|
||||||
status: AiJobStatus.running,
|
}
|
||||||
|
|
||||||
|
async executeTranscriptionJob(
|
||||||
|
jobId: string,
|
||||||
|
url: string,
|
||||||
|
mimeType: string
|
||||||
|
): Promise<TranscriptionJob> {
|
||||||
|
const status = AiJobStatus.running;
|
||||||
|
const success = await this.models.copilotJob.update(jobId, {
|
||||||
|
status,
|
||||||
|
payload: { url, mimeType },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new CopilotTranscriptionJobNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
await this.job.add('copilot.transcript.submit', {
|
await this.job.add('copilot.transcript.submit', {
|
||||||
jobId,
|
jobId,
|
||||||
url,
|
url,
|
||||||
mimeType: blob.mimetype,
|
mimeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id: jobId, status };
|
return { id: jobId, status };
|
||||||
@@ -113,9 +130,11 @@ export class CopilotTranscriptionService {
|
|||||||
|
|
||||||
const ret: TranscriptionJob = { id: job.id, status: job.status };
|
const ret: TranscriptionJob = { id: job.id, status: job.status };
|
||||||
|
|
||||||
if (job.status === AiJobStatus.claimed) {
|
const payload = TranscriptPayloadSchema.safeParse(job.payload);
|
||||||
const payload = TranscriptPayloadSchema.safeParse(job.payload);
|
if (payload.success) {
|
||||||
if (payload.success) {
|
ret.url = payload.data.url || undefined;
|
||||||
|
ret.mimeType = payload.data.mimeType || undefined;
|
||||||
|
if (job.status === AiJobStatus.claimed) {
|
||||||
ret.transcription = payload.data;
|
ret.transcription = payload.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const TranscriptionItemSchema = z.object({
|
|||||||
export const TranscriptionSchema = z.array(TranscriptionItemSchema);
|
export const TranscriptionSchema = z.array(TranscriptionItemSchema);
|
||||||
|
|
||||||
export const TranscriptPayloadSchema = z.object({
|
export const TranscriptPayloadSchema = z.object({
|
||||||
|
url: z.string().nullable().optional(),
|
||||||
|
mimeType: z.string().nullable().optional(),
|
||||||
title: z.string().nullable().optional(),
|
title: z.string().nullable().optional(),
|
||||||
summary: z.string().nullable().optional(),
|
summary: z.string().nullable().optional(),
|
||||||
transcription: TranscriptionSchema.nullable().optional(),
|
transcription: TranscriptionSchema.nullable().optional(),
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ enum ErrorNames {
|
|||||||
COPILOT_SESSION_DELETED
|
COPILOT_SESSION_DELETED
|
||||||
COPILOT_SESSION_NOT_FOUND
|
COPILOT_SESSION_NOT_FOUND
|
||||||
COPILOT_TRANSCRIPTION_JOB_EXISTS
|
COPILOT_TRANSCRIPTION_JOB_EXISTS
|
||||||
|
COPILOT_TRANSCRIPTION_JOB_NOT_FOUND
|
||||||
CUSTOMER_PORTAL_CREATE_FAILED
|
CUSTOMER_PORTAL_CREATE_FAILED
|
||||||
DOC_ACTION_DENIED
|
DOC_ACTION_DENIED
|
||||||
DOC_DEFAULT_ROLE_CAN_NOT_BE_OWNER
|
DOC_DEFAULT_ROLE_CAN_NOT_BE_OWNER
|
||||||
@@ -990,6 +991,7 @@ type Mutation {
|
|||||||
removeContextFile(options: RemoveContextFileInput!): Boolean!
|
removeContextFile(options: RemoveContextFileInput!): Boolean!
|
||||||
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
|
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
|
||||||
resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
|
resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
|
||||||
|
retryAudioTranscription(jobId: String!, workspaceId: String!): TranscriptionResultType
|
||||||
revoke(userId: String!, workspaceId: String!): Boolean!
|
revoke(userId: String!, workspaceId: String!): Boolean!
|
||||||
revokeDocUserRoles(input: RevokeDocUserRoleInput!): Boolean!
|
revokeDocUserRoles(input: RevokeDocUserRoleInput!): Boolean!
|
||||||
revokeInviteLink(workspaceId: String!): Boolean!
|
revokeInviteLink(workspaceId: String!): Boolean!
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
mutation retryAudioTranscription($workspaceId: String!, $jobId: String!) {
|
||||||
|
retryAudioTranscription(workspaceId: $workspaceId, jobId: $jobId) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -656,6 +656,17 @@ export const getAudioTranscriptionQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const retryAudioTranscriptionMutation = {
|
||||||
|
id: 'retryAudioTranscriptionMutation' as const,
|
||||||
|
op: 'retryAudioTranscription',
|
||||||
|
query: `mutation retryAudioTranscription($workspaceId: String!, $jobId: String!) {
|
||||||
|
retryAudioTranscription(workspaceId: $workspaceId, jobId: $jobId) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const createCopilotMessageMutation = {
|
export const createCopilotMessageMutation = {
|
||||||
id: 'createCopilotMessageMutation' as const,
|
id: 'createCopilotMessageMutation' as const,
|
||||||
op: 'createCopilotMessage',
|
op: 'createCopilotMessage',
|
||||||
|
|||||||
@@ -581,6 +581,7 @@ export enum ErrorNames {
|
|||||||
COPILOT_SESSION_DELETED = 'COPILOT_SESSION_DELETED',
|
COPILOT_SESSION_DELETED = 'COPILOT_SESSION_DELETED',
|
||||||
COPILOT_SESSION_NOT_FOUND = 'COPILOT_SESSION_NOT_FOUND',
|
COPILOT_SESSION_NOT_FOUND = 'COPILOT_SESSION_NOT_FOUND',
|
||||||
COPILOT_TRANSCRIPTION_JOB_EXISTS = 'COPILOT_TRANSCRIPTION_JOB_EXISTS',
|
COPILOT_TRANSCRIPTION_JOB_EXISTS = 'COPILOT_TRANSCRIPTION_JOB_EXISTS',
|
||||||
|
COPILOT_TRANSCRIPTION_JOB_NOT_FOUND = 'COPILOT_TRANSCRIPTION_JOB_NOT_FOUND',
|
||||||
CUSTOMER_PORTAL_CREATE_FAILED = 'CUSTOMER_PORTAL_CREATE_FAILED',
|
CUSTOMER_PORTAL_CREATE_FAILED = 'CUSTOMER_PORTAL_CREATE_FAILED',
|
||||||
DOC_ACTION_DENIED = 'DOC_ACTION_DENIED',
|
DOC_ACTION_DENIED = 'DOC_ACTION_DENIED',
|
||||||
DOC_DEFAULT_ROLE_CAN_NOT_BE_OWNER = 'DOC_DEFAULT_ROLE_CAN_NOT_BE_OWNER',
|
DOC_DEFAULT_ROLE_CAN_NOT_BE_OWNER = 'DOC_DEFAULT_ROLE_CAN_NOT_BE_OWNER',
|
||||||
@@ -1102,6 +1103,7 @@ export interface Mutation {
|
|||||||
removeContextFile: Scalars['Boolean']['output'];
|
removeContextFile: Scalars['Boolean']['output'];
|
||||||
removeWorkspaceFeature: Scalars['Boolean']['output'];
|
removeWorkspaceFeature: Scalars['Boolean']['output'];
|
||||||
resumeSubscription: SubscriptionType;
|
resumeSubscription: SubscriptionType;
|
||||||
|
retryAudioTranscription: Maybe<TranscriptionResultType>;
|
||||||
revoke: Scalars['Boolean']['output'];
|
revoke: Scalars['Boolean']['output'];
|
||||||
revokeDocUserRoles: Scalars['Boolean']['output'];
|
revokeDocUserRoles: Scalars['Boolean']['output'];
|
||||||
revokeInviteLink: Scalars['Boolean']['output'];
|
revokeInviteLink: Scalars['Boolean']['output'];
|
||||||
@@ -1367,6 +1369,11 @@ export interface MutationResumeSubscriptionArgs {
|
|||||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MutationRetryAudioTranscriptionArgs {
|
||||||
|
jobId: Scalars['String']['input'];
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface MutationRevokeArgs {
|
export interface MutationRevokeArgs {
|
||||||
userId: Scalars['String']['input'];
|
userId: Scalars['String']['input'];
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
@@ -3048,6 +3055,20 @@ export type GetAudioTranscriptionQuery = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RetryAudioTranscriptionMutationVariables = Exact<{
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
jobId: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type RetryAudioTranscriptionMutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
retryAudioTranscription: {
|
||||||
|
__typename?: 'TranscriptionResultType';
|
||||||
|
id: string;
|
||||||
|
status: AiJobStatus;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateCopilotMessageMutationVariables = Exact<{
|
export type CreateCopilotMessageMutationVariables = Exact<{
|
||||||
options: CreateChatMessageInput;
|
options: CreateChatMessageInput;
|
||||||
}>;
|
}>;
|
||||||
@@ -4706,6 +4727,11 @@ export type Mutations =
|
|||||||
variables: ClaimAudioTranscriptionMutationVariables;
|
variables: ClaimAudioTranscriptionMutationVariables;
|
||||||
response: ClaimAudioTranscriptionMutation;
|
response: ClaimAudioTranscriptionMutation;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'retryAudioTranscriptionMutation';
|
||||||
|
variables: RetryAudioTranscriptionMutationVariables;
|
||||||
|
response: RetryAudioTranscriptionMutation;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'createCopilotMessageMutation';
|
name: 'createCopilotMessageMutation';
|
||||||
variables: CreateCopilotMessageMutationVariables;
|
variables: CreateCopilotMessageMutationVariables;
|
||||||
|
|||||||
@@ -8087,6 +8087,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Transcription job already exists`
|
* `Transcription job already exists`
|
||||||
*/
|
*/
|
||||||
["error.COPILOT_TRANSCRIPTION_JOB_EXISTS"](): string;
|
["error.COPILOT_TRANSCRIPTION_JOB_EXISTS"](): string;
|
||||||
|
/**
|
||||||
|
* `Transcription job not found.`
|
||||||
|
*/
|
||||||
|
["error.COPILOT_TRANSCRIPTION_JOB_NOT_FOUND"](): string;
|
||||||
/**
|
/**
|
||||||
* `You have exceeded your blob size quota.`
|
* `You have exceeded your blob size quota.`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1999,6 +1999,7 @@
|
|||||||
"error.COPILOT_FAILED_TO_MATCH_CONTEXT": "Failed to match context {{contextId}} with \"%7B%7Bcontent%7D%7D\": {{message}}",
|
"error.COPILOT_FAILED_TO_MATCH_CONTEXT": "Failed to match context {{contextId}} with \"%7B%7Bcontent%7D%7D\": {{message}}",
|
||||||
"error.COPILOT_EMBEDDING_UNAVAILABLE": "Embedding feature not available, you may need to install pgvector extension to your database",
|
"error.COPILOT_EMBEDDING_UNAVAILABLE": "Embedding feature not available, you may need to install pgvector extension to your database",
|
||||||
"error.COPILOT_TRANSCRIPTION_JOB_EXISTS": "Transcription job already exists",
|
"error.COPILOT_TRANSCRIPTION_JOB_EXISTS": "Transcription job already exists",
|
||||||
|
"error.COPILOT_TRANSCRIPTION_JOB_NOT_FOUND": "Transcription job not found.",
|
||||||
"error.BLOB_QUOTA_EXCEEDED": "You have exceeded your blob size quota.",
|
"error.BLOB_QUOTA_EXCEEDED": "You have exceeded your blob size quota.",
|
||||||
"error.STORAGE_QUOTA_EXCEEDED": "You have exceeded your storage quota.",
|
"error.STORAGE_QUOTA_EXCEEDED": "You have exceeded your storage quota.",
|
||||||
"error.MEMBER_QUOTA_EXCEEDED": "You have exceeded your workspace member quota.",
|
"error.MEMBER_QUOTA_EXCEEDED": "You have exceeded your workspace member quota.",
|
||||||
|
|||||||
Reference in New Issue
Block a user