@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user