Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poc bedrock 3 #2736

Draft
wants to merge 26 commits into
base: poc-bedrock
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c6975e5
Add button to ImageEmbedForm
rauboti Nov 27, 2024
0df5676
Merged in endpoint + connected button and textfield
rauboti Nov 27, 2024
7492b7b
Add transcribe and get_transcription endpoints
ekrojo77 Nov 29, 2024
04ed47b
Add helper function for polling
ekrojo77 Nov 29, 2024
fdd9411
Remove default test value
ekrojo77 Nov 29, 2024
814a7fa
Move fetching of env variables to config
ekrojo77 Dec 1, 2024
9039230
load env in api file
ekrojo77 Dec 2, 2024
2bdd97e
Remove fallback, add check if environment is set
ekrojo77 Dec 2, 2024
179b0f4
make the polling a get
ekrojo77 Dec 6, 2024
e8629ad
Merge pull request #2758 from NDLANO/transcribe-api
ekrojo77 Dec 6, 2024
835ceb2
Allow generation of alt text when creating image as well
rauboti Dec 13, 2024
c2d4ab8
First UI updates to get transcription
rauboti Dec 13, 2024
cfe42a2
Temporarily added s3 link for audio transcriptions
rauboti Dec 17, 2024
75d4b9b
Adding polling interval
rauboti Dec 17, 2024
031cd2b
Fix alt text for existing images
rauboti Dec 17, 2024
1b5c955
Enabled for podcasts
rauboti Dec 17, 2024
1bb06c2
Return text from endpoint to field + fix button text
rauboti Dec 17, 2024
9d4057f
Merge branch 'poc-bedrock' into poc-bedrock-3
rauboti Dec 17, 2024
5b22c40
Merge branch 'poc-bedrock' into poc-bedrock-3
rauboti Dec 17, 2024
8046b54
Fix linting
rauboti Dec 17, 2024
64c710e
fix use of backend for transcribe
ekrojo77 Dec 18, 2024
14326b8
Merge branch 'poc-bedrock-3' of github.com:NDLANO/editorial-frontend …
ekrojo77 Dec 18, 2024
4e51332
Fix polling for audio
ekrojo77 Dec 18, 2024
e6780f8
lint
ekrojo77 Dec 18, 2024
23c75e8
update to poll only on ID
ekrojo77 Dec 18, 2024
7bd47aa
check if job has been performed
ekrojo77 Dec 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"dependencies": {
"@ark-ui/react": "^4.1.2",
"@aws-sdk/client-bedrock-runtime": "^3.670.0",
"@aws-sdk/client-transcribe": "^3.699.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
Expand All @@ -100,6 +101,7 @@
"@ndla/video-search": "^8.0.70-alpha.0",
"@tanstack/react-query": "5.62.3",
"auth0-js": "^9.22.1",
"buffer": "^6.0.3",
"compression": "^1.7.4",
"cross-fetch": "^3.1.5",
"date-fns": "2.30.0",
Expand Down
24 changes: 18 additions & 6 deletions src/components/LLM/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,36 @@
*
*/

import { Buffer } from "buffer";

export const claudeHaikuDefaults = { top_p: 0.7, top_k: 100, temperature: 0.9 };

interface modelProps {
prompt: string;
image?: {
base64: string;
fileType: string;
};
max_tokens?: number;
}

export const invokeModel = async ({ prompt, max_tokens = 2000, ...rest }: modelProps) => {
export const invokeModel = async ({ prompt, image, max_tokens = 2000, ...rest }: modelProps) => {
if (!prompt) {
// console.error("No prompt provided to invokeModel");
return null;
}

const payload: any = { prompt, max_tokens, ...rest };
if (image) {
payload.image = image;
}

const response = await fetch("/invoke-model", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: prompt,
max_tokens: max_tokens,
...rest,
}),
body: JSON.stringify(payload),
});

if (!response.ok) {
Expand All @@ -48,3 +56,7 @@ export const getTextFromHTML = (html: string) => {
const parseResponse = (response: string) => {
return response.split("<answer>")[1].split("</answer>")[0].trim();
};

export const convertBufferToBase64 = (buffer: ArrayBuffer) => {
return Buffer.from(buffer).toString("base64");
};
6 changes: 5 additions & 1 deletion src/components/SlateEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ const RichTextEditor = ({

useEffect(() => {
// When form is submitted or form content has been revert to a previous version, the editor has to be reinitialized.
if ((!submitted && prevSubmitted.current) || status === "revertVersion") {
if (
(!submitted && prevSubmitted.current) ||
status.status === "revertVersion" ||
status.status === "acceptGenerated"
) {
if (isFirstNormalize) {
return;
}
Expand Down
53 changes: 49 additions & 4 deletions src/components/SlateEditor/plugins/image/ImageEmbedForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
*/

import { Formik, useFormikContext } from "formik";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Descendant } from "slate";
import { CheckLine } from "@ndla/icons";
import { FileListLine, CheckLine } from "@ndla/icons";
import {
Button,
CheckboxControl,
Expand All @@ -22,10 +22,12 @@ import {
FieldRoot,
FieldErrorMessage,
FieldTextArea,
Spinner,
} from "@ndla/primitives";
import { styled } from "@ndla/styled-system/jsx";
import { IImageMetaInformationV3DTO } from "@ndla/types-backend/image-api";
import { ImageEmbedData } from "@ndla/types-embed";
import { convertBufferToBase64, claudeHaikuDefaults, invokeModel } from "../../../../components/LLM/helpers";
import { InlineField } from "../../../../containers/FormikForm/InlineField";
import ImageEditor from "../../../../containers/ImageEditor/ImageEditor";
import { inlineContentToEditorValue, inlineContentToHTML } from "../../../../util/articleContentConverter";
Expand Down Expand Up @@ -146,6 +148,12 @@ const InputWrapper = styled("div", {
},
});

const StyledButton = styled(Button, {
base: {
alignSelf: "flex-start",
},
});

const EmbedForm = ({
onClose,
language,
Expand All @@ -156,6 +164,31 @@ const EmbedForm = ({
const inGrid = useInGrid();
const { values, initialValues, isValid, setFieldValue, dirty, isSubmitting } =
useFormikContext<ImageEmbedFormValues>();
const [isLoading, setIsLoading] = useState<boolean>(false);

const generateAltText = async () => {
setIsLoading(true);
if (!image?.image.imageUrl) {
return null;
}

const response = await fetch(image?.image.imageUrl);
const responseContentType = response.headers.get("Content-Type");
const buffer = await response.arrayBuffer();
const base64 = convertBufferToBase64(buffer);

const result = await invokeModel({
prompt: t("textGeneration.altText.prompt", { language: t(`languages.${language}`) }),
image: {
base64,
fileType: responseContentType ?? "",
},
max_tokens: 2000,
...claudeHaikuDefaults,
});
setIsLoading(false);
return result;
};

const formIsDirty = isFormikFormDirty({
values,
Expand All @@ -182,13 +215,25 @@ const EmbedForm = ({
</FieldRoot>
)}
</FormField>

{!values.isDecorative && (
<FormField name="alt">
{({ field, meta }) => (
{({ field, meta, helpers }) => (
<FieldRoot invalid={!!meta.error}>
<FieldLabel>{t("form.image.alt.label")}</FieldLabel>
<FieldTextArea {...field} placeholder={t("form.image.alt.placeholder")} />
<StyledButton
onClick={async () => {
const text = await generateAltText();
if (text && text.length > 0) {
helpers.setValue(text);
}
}}
size="small"
title={t("textGeneration.altText.title")}
>
{t("textGeneration.altText.button")}
{isLoading ? <Spinner size="small" /> : <FileListLine />}
</StyledButton>
<FieldErrorMessage>{meta.error}</FieldErrorMessage>
</FieldRoot>
)}
Expand Down
48 changes: 48 additions & 0 deletions src/components/Transcribe/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2024-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
interface props {
fileUrl: string;
languageCode: string;
mediaFormat: string;
maxSpeakers?: number;
outputFileName: string;
}

export const transcribe = async ({ fileUrl, maxSpeakers, mediaFormat, languageCode, outputFileName }: props) => {
const payload: any = {
mediaFileUri: fileUrl,
languageCode: languageCode,
mediaFormat: mediaFormat,
outputFileName: outputFileName,
};

if (maxSpeakers) {
payload.maxSpeakers = maxSpeakers;
}

const response = await fetch("/transcribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});

return response.json();
};

export const getTranscription = async (jobName: string) => {
const response = await fetch(`/transcribe/${jobName}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
return result;
};
11 changes: 11 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ const usernamePasswordEnabled = (ndlaEnvironment: string) => {
}
};

const getAudioS3Root = (ndlaEnvironment: string) => {
switch (ndlaEnvironment) {
case "prod":
return "s3://prod.audio.2.ndla/";
default:
return "s3://test.audio.2.ndla/";
}
};

export type ConfigType = {
brightcoveAccountId: string | undefined;
logEnvironment: string | undefined;
Expand Down Expand Up @@ -159,6 +168,7 @@ export type ConfigType = {
defaultLanguage: LocaleType;
runtimeType: RuntimeType;
enableH5pCopy: boolean;
s3AudioRoot: string;
};

const getServerSideConfig = (): ConfigType => {
Expand Down Expand Up @@ -204,6 +214,7 @@ const getServerSideConfig = (): ConfigType => {
isVercel: getEnvironmentVariabel("IS_VERCEL", "false") === "true",
runtimeType: getEnvironmentVariabel("NODE_ENV", "development") as "test" | "development" | "production",
enableH5pCopy: getEnvironmentVariabel("ENABLE_H5P_COPY", "true") === "true",
s3AudioRoot: getAudioS3Root(ndlaEnvironment),
};
};

Expand Down
8 changes: 7 additions & 1 deletion src/containers/AudioUploader/components/AudioForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,13 @@ const AudioForm = ({
title={t("podcastForm.fields.manuscript")}
hasError={[].some((field) => field in errors)}
>
<AudioManuscript />
<AudioManuscript
audioName={audio?.title.title}
audioId={audio?.id}
audioLanguage={audioLanguage}
audioUrl={audio?.audioFile.url}
audioType={audio?.audioFile.url.split(".").pop()}
/>
</FormAccordion>
<FormAccordion
id="audio-upload-copyright"
Expand Down
Loading
Loading