Skip to content

Commit

Permalink
add transcribe usecase
Browse files Browse the repository at this point in the history
  • Loading branch information
gteu committed Oct 24, 2023
1 parent 9c489eb commit 057a69c
Show file tree
Hide file tree
Showing 14 changed files with 1,690 additions and 122 deletions.
1,209 changes: 1,089 additions & 120 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@
},
"workspaces": [
"packages/*"
]
],
"dependencies": {
"@aws-sdk/client-s3": "^3.431.0",
"@aws-sdk/client-transcribe": "^3.431.0",
"@aws-sdk/s3-request-presigner": "^3.431.0"
}
}
44 changes: 44 additions & 0 deletions packages/cdk/lambda/getSignedUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { GetSignedUrlRequest } from 'generative-ai-use-cases-jp';

export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const req: GetSignedUrlRequest = JSON.parse(event.body!);
const mediaFormat = req.mediaFormat;
const currentDateString = new Date()
.toISOString()
.replace(/[:T-]/g, '')
.split('.')[0];

const client = new S3Client({});
const command = new PutObjectCommand({
Bucket: process.env.AUDIO_BUCKET_NAME,
Key: `${currentDateString}.${mediaFormat}`,
});

const signedUrl = await getSignedUrl(client, command, { expiresIn: 3600 });

return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: signedUrl,
};
} catch (error) {
console.log(error);
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({ message: 'Internal Server Error' }),
};
}
};
78 changes: 78 additions & 0 deletions packages/cdk/lambda/getTranscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import {
TranscribeClient,
GetTranscriptionJobCommand,
} from '@aws-sdk/client-transcribe';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';

function parseS3Url(s3Url: string) {
const url = new URL(s3Url);

const pathParts = url.pathname.split('/');
const bucket = pathParts[1];
const key = pathParts.slice(2).join('/');

return { bucket, key };
}

export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const transcribeClient = new TranscribeClient({});
const s3Client = new S3Client({});
const jobName = event.pathParameters!.jobName;

const command = new GetTranscriptionJobCommand({
TranscriptionJobName: jobName,
});
const res = await transcribeClient.send(command);
if (res.TranscriptionJob?.TranscriptionJobStatus === 'COMPLETED') {
const { bucket, key } = parseS3Url(
res.TranscriptionJob.Transcript!.TranscriptFileUri!
);
const s3Result = await s3Client.send(
new GetObjectCommand({
Bucket: bucket,
Key: key,
})
);
const transcript = JSON.parse(await s3Result.Body!.transformToString());

return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
status: res.TranscriptionJob?.TranscriptionJobStatus,
transcript: transcript.results.transcripts
.map((item: { transcript: string }) => item.transcript)
.join(''),
}),
};
} else {
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
status: res.TranscriptionJob?.TranscriptionJobStatus,
}),
};
}
} catch (error) {
console.log(error);
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({ message: 'Internal Server Error' }),
};
}
};
52 changes: 52 additions & 0 deletions packages/cdk/lambda/startTranscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import {
TranscribeClient,
StartTranscriptionJobCommand,
} from '@aws-sdk/client-transcribe';
import { StartTranscriptionRequest } from 'generative-ai-use-cases-jp';

export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const client = new TranscribeClient({});
const req: StartTranscriptionRequest = JSON.parse(event.body!);
const { audioUrl, mediaFormat } = req;

const currentDateString = new Date()
.toISOString()
.replace(/[:T-]/g, '')
.split('.')[0];

const command = new StartTranscriptionJobCommand({
IdentifyLanguage: true,
LanguageOptions: ['ja-JP', 'en-US'],
MediaFormat: mediaFormat,
Media: { MediaFileUri: audioUrl },
TranscriptionJobName: `job-${currentDateString}`,
OutputBucketName: process.env.TRANSCRIPT_BUCKET_NAME,
});
const res = await client.send(command);

return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
jobName: res.TranscriptionJob!.TranscriptionJobName,
}),
};
} catch (error) {
console.log(error);
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({ message: 'Internal Server Error' }),
};
}
};
1 change: 1 addition & 0 deletions packages/cdk/lib/construct/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './auth';
export * from './web';
export * from './database';
export * from './rag';
export * from './transcribe';
132 changes: 132 additions & 0 deletions packages/cdk/lib/construct/transcribe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Duration, RemovalPolicy } from 'aws-cdk-lib';
import {
AuthorizationType,
CognitoUserPoolsAuthorizer,
LambdaIntegration,
RestApi,
} from 'aws-cdk-lib/aws-apigateway';
import { UserPool } from 'aws-cdk-lib/aws-cognito';
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Bucket, BucketEncryption, HttpMethods } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export interface TranscribeProps {
userPool: UserPool;
api: RestApi;
}

export class Transcribe extends Construct {
constructor(scope: Construct, id: string, props: TranscribeProps) {
super(scope, id);

const audioBucket = new Bucket(this, 'AudioBucket', {
encryption: BucketEncryption.S3_MANAGED,
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
audioBucket.addCorsRule({
allowedOrigins: ['*'],
allowedMethods: [HttpMethods.PUT],
allowedHeaders: ['*'],
exposedHeaders: [],
maxAge: 3000,
});

const transcriptBucket = new Bucket(this, 'TranscriptBucket', {
encryption: BucketEncryption.S3_MANAGED,
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});

const getSignedUrlFunction = new NodejsFunction(this, 'GetSignedUrl', {
runtime: Runtime.NODEJS_18_X,
entry: './lambda/getSignedUrl.ts',
timeout: Duration.minutes(15),
environment: {
AUDIO_BUCKET_NAME: audioBucket.bucketName,
},
});
audioBucket.grantWrite(getSignedUrlFunction);

const startTranscriptionFunction = new NodejsFunction(
this,
'StartTranscription',
{
runtime: Runtime.NODEJS_18_X,
entry: './lambda/startTranscription.ts',
timeout: Duration.minutes(15),
environment: {
TRANSCRIPT_BUCKET_NAME: transcriptBucket.bucketName,
},
initialPolicy: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['transcribe:*'],
resources: ['*'],
}),
],
}
);
audioBucket.grantRead(startTranscriptionFunction);
transcriptBucket.grantWrite(startTranscriptionFunction);

const getTranscriptionFunction = new NodejsFunction(
this,
'GetTranscription',
{
runtime: Runtime.NODEJS_18_X,
entry: './lambda/getTranscription.ts',
timeout: Duration.minutes(15),
initialPolicy: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['transcribe:*'],
resources: ['*'],
}),
],
}
);
transcriptBucket.grantRead(getTranscriptionFunction);

// API Gateway
const authorizer = new CognitoUserPoolsAuthorizer(this, 'Authorizer', {
cognitoUserPools: [props.userPool],
});

const commonAuthorizerProps = {
authorizationType: AuthorizationType.COGNITO,
authorizer,
};
const transcribeResource = props.api.root.addResource('transcribe');

// POST: /transcribe/start
transcribeResource
.addResource('start')
.addMethod(
'POST',
new LambdaIntegration(startTranscriptionFunction),
commonAuthorizerProps
);

// POST: /transcribe/url
transcribeResource
.addResource('url')
.addMethod(
'POST',
new LambdaIntegration(getSignedUrlFunction),
commonAuthorizerProps
);

// GET: /transcribe/result/{jobName}
transcribeResource
.addResource('result')
.addResource('{jobName}')
.addMethod(
'GET',
new LambdaIntegration(getTranscriptionFunction),
commonAuthorizerProps
);
}
}
7 changes: 6 additions & 1 deletion packages/cdk/lib/generative-ai-use-cases-stack.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Auth, Api, Web, Database, Rag } from './construct';
import { Auth, Api, Web, Database, Rag, Transcribe } from './construct';

export class GenerativeAiUseCasesStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
Expand Down Expand Up @@ -34,6 +34,11 @@ export class GenerativeAiUseCasesStack extends Stack {
});
}

new Transcribe(this, 'Transcribe', {
userPool: auth.userPool,
api: api.api,
});

new CfnOutput(this, 'Region', {
value: this.region,
});
Expand Down
25 changes: 25 additions & 0 deletions packages/types/src/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
QueryCommandOutput,
RetrieveCommandOutput,
} from '@aws-sdk/client-kendra';
import { MediaFormat } from '@aws-sdk/client-transcribe';

export type CreateChatResponse = {
chat: Chat;
Expand Down Expand Up @@ -75,3 +76,27 @@ export type RetrieveKendraRequest = {
};

export type RetrieveKendraResponse = RetrieveCommandOutput;

export type GetSignedUrlRequest = {
mediaFormat: MediaFormat;
};

export type GetSignedUrlResponse = string;

export type StartTranscriptionRequest = {
audioUrl: string;
mediaFormat: MediaFormat;
};

export type StartTranscriptionResponse = {
jobName: string;
};

export type GetTranscriptionResponse = {
status: string;
transcript?: string;
};

export type UploadAudioRequest = {
file: File;
};
Loading

0 comments on commit 057a69c

Please sign in to comment.