feat(server): improve transcript (#13253)

fix AF-2758
fix AF-2759
This commit is contained in:
DarkSky
2025-07-17 17:20:14 +08:00
committed by GitHub
parent dc68c2385d
commit bdf1389258
5 changed files with 66 additions and 26 deletions

View File

@@ -303,7 +303,7 @@ const textActions: Prompt[] = [
{ {
name: 'Transcript audio', name: 'Transcript audio',
action: 'Transcript audio', action: 'Transcript audio',
model: 'gemini-2.5-pro', model: 'gemini-2.5-flash',
optionalModels: ['gemini-2.5-flash', 'gemini-2.5-pro'], optionalModels: ['gemini-2.5-flash', 'gemini-2.5-pro'],
messages: [ messages: [
{ {
@@ -334,6 +334,7 @@ Convert a multi-speaker audio recording into a structured JSON format by transcr
config: { config: {
requireContent: false, requireContent: false,
requireAttachment: true, requireAttachment: true,
maxRetries: 1,
}, },
}, },
{ {

View File

@@ -129,7 +129,16 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
system, system,
messages: msgs, messages: msgs,
schema, schema,
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: -1,
includeThoughts: false,
},
},
},
abortSignal: options.signal, abortSignal: options.signal,
maxRetries: options.maxRetries || 3,
experimental_repairText: async ({ text, error }) => { experimental_repairText: async ({ text, error }) => {
if (error instanceof JSONParseError) { if (error instanceof JSONParseError) {
// strange fixed response, temporarily replace it // strange fixed response, temporarily replace it

View File

@@ -15,7 +15,6 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { import {
CopilotTranscriptionAudioNotProvided, CopilotTranscriptionAudioNotProvided,
CopilotTranscriptionJobNotFound,
type FileUpload, type FileUpload,
} from '../../../base'; } from '../../../base';
import { CurrentUser } from '../../../core/auth'; import { CurrentUser } from '../../../core/auth';
@@ -74,7 +73,7 @@ const FinishedStatus: Set<AiJobStatus> = new Set([
export class CopilotTranscriptionResolver { export class CopilotTranscriptionResolver {
constructor( constructor(
private readonly ac: AccessController, private readonly ac: AccessController,
private readonly service: CopilotTranscriptionService private readonly transcript: CopilotTranscriptionService
) {} ) {}
private handleJobResult( private handleJobResult(
@@ -122,7 +121,7 @@ export class CopilotTranscriptionResolver {
throw new CopilotTranscriptionAudioNotProvided(); throw new CopilotTranscriptionAudioNotProvided();
} }
const jobResult = await this.service.submitTranscriptionJob( const jobResult = await this.transcript.submitJob(
user.id, user.id,
workspaceId, workspaceId,
blobId, blobId,
@@ -144,19 +143,11 @@ export class CopilotTranscriptionResolver {
.allowLocal() .allowLocal()
.assert('Workspace.Copilot'); .assert('Workspace.Copilot');
const job = await this.service.queryTranscriptionJob( const jobResult = await this.transcript.retryJob(
user.id, user.id,
workspaceId, workspaceId,
jobId jobId
); );
if (!job || !job.infos) {
throw new CopilotTranscriptionJobNotFound();
}
const jobResult = await this.service.executeTranscriptionJob(
job.id,
job.infos
);
return this.handleJobResult(jobResult); return this.handleJobResult(jobResult);
} }
@@ -166,7 +157,7 @@ export class CopilotTranscriptionResolver {
@CurrentUser() user: CurrentUser, @CurrentUser() user: CurrentUser,
@Args('jobId') jobId: string @Args('jobId') jobId: string
): Promise<TranscriptionResultType | null> { ): Promise<TranscriptionResultType | null> {
const job = await this.service.claimTranscriptionJob(user.id, jobId); const job = await this.transcript.claimJob(user.id, jobId);
return this.handleJobResult(job); return this.handleJobResult(job);
} }
@@ -190,7 +181,7 @@ export class CopilotTranscriptionResolver {
.allowLocal() .allowLocal()
.assert('Workspace.Copilot'); .assert('Workspace.Copilot');
const job = await this.service.queryTranscriptionJob( const job = await this.transcript.queryJob(
user.id, user.id,
copilot.workspaceId, copilot.workspaceId,
jobId, jobId,

View File

@@ -49,7 +49,17 @@ export class CopilotTranscriptionService {
private readonly providerFactory: CopilotProviderFactory private readonly providerFactory: CopilotProviderFactory
) {} ) {}
async submitTranscriptionJob( private async getModel(userId: string) {
const prompt = await this.prompt.get('Transcript audio');
const hasAccess = await this.models.userFeature.has(
userId,
'unlimited_copilot'
);
// choose the pro model if user has copilot plan
return prompt?.optionalModels[hasAccess ? 1 : 0];
}
async submitJob(
userId: string, userId: string,
workspaceId: string, workspaceId: string,
blobId: string, blobId: string,
@@ -78,12 +88,26 @@ export class CopilotTranscriptionService {
infos.push({ url, mimeType: blob.mimetype }); infos.push({ url, mimeType: blob.mimetype });
} }
return await this.executeTranscriptionJob(jobId, infos); const model = await this.getModel(userId);
return await this.executeJob(jobId, infos, model);
} }
async executeTranscriptionJob( async retryJob(userId: string, workspaceId: string, jobId: string) {
const job = await this.queryJob(userId, workspaceId, jobId);
if (!job || !job.infos) {
throw new CopilotTranscriptionJobNotFound();
}
const model = await this.getModel(userId);
const jobResult = await this.executeJob(job.id, job.infos, model);
return jobResult;
}
async executeJob(
jobId: string, jobId: string,
infos: AudioBlobInfos infos: AudioBlobInfos,
modelId?: string
): Promise<TranscriptionJob> { ): Promise<TranscriptionJob> {
const status = AiJobStatus.running; const status = AiJobStatus.running;
const success = await this.models.copilotJob.update(jobId, { const success = await this.models.copilotJob.update(jobId, {
@@ -98,12 +122,13 @@ export class CopilotTranscriptionService {
await this.job.add('copilot.transcript.submit', { await this.job.add('copilot.transcript.submit', {
jobId, jobId,
infos, infos,
modelId,
}); });
return { id: jobId, status }; return { id: jobId, status };
} }
async claimTranscriptionJob( async claimJob(
userId: string, userId: string,
jobId: string jobId: string
): Promise<TranscriptionJob | null> { ): Promise<TranscriptionJob | null> {
@@ -118,7 +143,7 @@ export class CopilotTranscriptionService {
return null; return null;
} }
async queryTranscriptionJob( async queryJob(
userId: string, userId: string,
workspaceId: string, workspaceId: string,
jobId?: string, jobId?: string,
@@ -181,14 +206,20 @@ export class CopilotTranscriptionService {
promptName: string, promptName: string,
message: Partial<PromptMessage>, message: Partial<PromptMessage>,
schema?: ZodType<any>, schema?: ZodType<any>,
prefer?: CopilotProviderType prefer?: CopilotProviderType,
modelId?: string
): Promise<string> { ): Promise<string> {
const prompt = await this.prompt.get(promptName); const prompt = await this.prompt.get(promptName);
if (!prompt) { if (!prompt) {
throw new CopilotPromptNotFound({ name: promptName }); throw new CopilotPromptNotFound({ name: promptName });
} }
const cond = { modelId: prompt.model }; const cond = {
modelId:
modelId && prompt.optionalModels.includes(modelId)
? modelId
: prompt.model,
};
const msg = { role: 'user' as const, content: '', ...message }; const msg = { role: 'user' as const, content: '', ...message };
const config = Object.assign({}, prompt.config); const config = Object.assign({}, prompt.config);
if (schema) { if (schema) {
@@ -231,13 +262,19 @@ export class CopilotTranscriptionService {
return `${hoursStr}:${minutesStr}:${secondsStr}`; return `${hoursStr}:${minutesStr}:${secondsStr}`;
} }
private async callTranscript(url: string, mimeType: string, offset: number) { private async callTranscript(
url: string,
mimeType: string,
offset: number,
modelId?: string
) {
// NOTE: Vertex provider not support transcription yet, we always use Gemini here // NOTE: Vertex provider not support transcription yet, we always use Gemini here
const result = await this.chatWithPrompt( const result = await this.chatWithPrompt(
'Transcript audio', 'Transcript audio',
{ attachments: [url], params: { mimetype: mimeType } }, { attachments: [url], params: { mimetype: mimeType } },
TranscriptionResponseSchema, TranscriptionResponseSchema,
CopilotProviderType.Gemini CopilotProviderType.Gemini,
modelId
); );
const transcription = TranscriptionResponseSchema.parse( const transcription = TranscriptionResponseSchema.parse(
@@ -256,6 +293,7 @@ export class CopilotTranscriptionService {
async transcriptAudio({ async transcriptAudio({
jobId, jobId,
infos, infos,
modelId,
// @deprecated // @deprecated
url, url,
mimeType, mimeType,
@@ -264,7 +302,7 @@ export class CopilotTranscriptionService {
const blobInfos = this.mergeInfos(infos, url, mimeType); const blobInfos = this.mergeInfos(infos, url, mimeType);
const transcriptions = await Promise.all( const transcriptions = await Promise.all(
Array.from(blobInfos.entries()).map(([idx, { url, mimeType }]) => Array.from(blobInfos.entries()).map(([idx, { url, mimeType }]) =>
this.callTranscript(url, mimeType, idx * 10 * 60) this.callTranscript(url, mimeType, idx * 10 * 60, modelId)
) )
); );

View File

@@ -56,6 +56,7 @@ declare global {
'copilot.transcript.submit': { 'copilot.transcript.submit': {
jobId: string; jobId: string;
infos?: AudioBlobInfos; infos?: AudioBlobInfos;
modelId?: string;
/// @deprecated use `infos` instead /// @deprecated use `infos` instead
url?: string; url?: string;
/// @deprecated use `infos` instead /// @deprecated use `infos` instead