Skip to content

Commit

Permalink
Merge pull request #96 from RickCarlino/prod
Browse files Browse the repository at this point in the history
v3 Final Updates
  • Loading branch information
RickCarlino authored Oct 20, 2024
2 parents 50edb82 + 4118789 commit 00c6b0d
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 201 deletions.
100 changes: 100 additions & 0 deletions koala/grammar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import OpenAI from "openai";
import { ChatCompletionCreateParamsNonStreaming } from "openai/resources";
import { errorReport } from "./error-report";
import { YesNo } from "./shared-types";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";

const apiKey = process.env.OPENAI_API_KEY;

if (!apiKey) {
errorReport("Missing ENV Var: OPENAI_API_KEY");
}

const configuration = { apiKey };

export const openai = new OpenAI(configuration);

export async function gptCall(opts: ChatCompletionCreateParamsNonStreaming) {
return await openai.chat.completions.create(opts);
}

const zodYesOrNo = z.object({
response: z.union([
z.object({
userWasCorrect: z.literal(true),
}),
z.object({
userWasCorrect: z.literal(false),
correctedSentence: z.string(),
}),
]),
});

export type Explanation = { response: YesNo; whyNot?: string };

type GrammarCorrrectionProps = {
/** The Korean phrase. */
term: string;
/** An English translation */
definition: string;
/** Language code like KO */
langCode: string;
/** What the user said. */
userInput: string;
};

const getLangcode = (lang: string) => {
const names: Record<string, string> = {
EN: "English",
IT: "Italian",
FR: "French",
ES: "Spanish",
KO: "Korean",
};
const key = lang.slice(0, 2).toUpperCase();
return names[key] || lang;
};

export const grammarCorrection = async (
props: GrammarCorrrectionProps,
): Promise<string | undefined> => {
// Latest snapshot that supports Structured Outputs
// TODO: Get on mainline 4o when it supports Structured Outputs
const model = "gpt-4o-2024-08-06";
const { userInput } = props;
const lang = getLangcode(props.langCode);
const prompt = [
`You are a language assistant helping users improve their ${lang} sentences.`,
`The user wants to say: '${props.definition}' in ${lang}.`,
`They provided: '${userInput}'.`,
`Your task is to determine if the user's input is an acceptable way to express the intended meaning in ${lang}.`,
`If the response is acceptable by ${lang} native speakers, respond with:`,
`{ "response": { "userWasCorrect": true } }`,
`If it is not, respond with:`,
`{ "response": { "userWasCorrect": false, "correctedSentence": "corrected sentence here" } }`,
`Do not include any additional commentary or explanations.`,
`Ensure your response is in valid JSON format.`,
].join("\n");

const resp = await openai.beta.chat.completions.parse({
messages: [
{
role: "user",
content: prompt,
},
],
model,
max_tokens: 125,
// top_p: 1,
// frequency_penalty: 0,
temperature: 0.1,
response_format: zodResponseFormat(zodYesOrNo, "correct_sentence"),
});
const correct_sentence = resp.choices[0].message.parsed;
if (correct_sentence) {
if (!correct_sentence.response.userWasCorrect) {
return correct_sentence.response.correctedSentence;
}
}
};
57 changes: 0 additions & 57 deletions koala/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import OpenAI from "openai";
import { ChatCompletionCreateParamsNonStreaming } from "openai/resources";
import { errorReport } from "./error-report";
import { YesNo } from "./shared-types";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";

const apiKey = process.env.OPENAI_API_KEY;

Expand Down Expand Up @@ -49,18 +47,6 @@ const SIMPLE_YES_OR_NO = {
description: "Answer a yes or no question.",
};

const zodYesOrNo = z.object({
response: z.union([
z.object({
userWasCorrect: z.literal(true),
}),
z.object({
userWasCorrect: z.literal(false),
correctedSentence: z.string(),
}),
]),
});

export type Explanation = { response: YesNo; whyNot?: string };

export const testEquivalence = async (
Expand Down Expand Up @@ -97,49 +83,6 @@ export const testEquivalence = async (
return raw.response as YesNo;
};

type GrammarCorrrectionProps = {
term: string;
definition: string;
langCode: string;
userInput: string;
};

export const grammarCorrection = async (
props: GrammarCorrrectionProps,
): Promise<string | undefined> => {
// Latest snapshot that supports Structured Outputs
// TODO: Get on mainline 4o when it supports Structured Outputs
const model = "gpt-4o-2024-08-06";
const { userInput } = props;
const prompt = [
`I want to say '${props.definition}' in language: ${props.langCode}.`,
`Is '${userInput}' OK?`,
`Correct awkwardness or major grammatical issues, if any.`,
].join("\n");

const resp = await openai.beta.chat.completions.parse({
messages: [
{
role: "user",
content: prompt,
},
],
model,
max_tokens: 150,
top_p: 1,
frequency_penalty: 0,
stop: ["\n"],
temperature: 0.2,
response_format: zodResponseFormat(zodYesOrNo, "correct_sentence"),
});
const correct_sentence = resp.choices[0].message.parsed;
if (correct_sentence) {
if (!correct_sentence.response.userWasCorrect) {
return correct_sentence.response.correctedSentence;
}
}
};

export const translateToEnglish = async (content: string, langCode: string) => {
const prompt = `You will be provided with a foreign language sentence (lang code: ${langCode}), and your task is to translate it into English.`;
const hm = await gptCall({
Expand Down
73 changes: 21 additions & 52 deletions koala/play-audio.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,28 @@
let currentlyPlaying = false;

const playAudioBuffer = (
buffer: AudioBuffer,
context: AudioContext,
): Promise<void> => {
return new Promise((resolve) => {
const source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.onended = () => {
currentlyPlaying = false;
resolve();
};
source.start(0);
});
};

export const playAudio = async (urlOrDataURI: string): Promise<void> => {
if (!urlOrDataURI) {
return;
}

if (currentlyPlaying) {
return;
}
export const playAudio = (urlOrDataURI: string) => {
return new Promise((resolve, reject) => {
if (!urlOrDataURI) {
return;
}

currentlyPlaying = true;
const audioContext = new AudioContext();
if (currentlyPlaying) {
return;
}

try {
let arrayBuffer: ArrayBuffer;
currentlyPlaying = true;

if (urlOrDataURI.startsWith("data:")) {
// Handle Base64 Data URI
const base64Data = urlOrDataURI.split(",")[1];
const binaryString = atob(base64Data);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
arrayBuffer = bytes.buffer;
} else {
// Handle external URL
const response = await fetch(urlOrDataURI);
if (!response.ok) {
throw new Error("Network response was not ok");
}
arrayBuffer = await response.arrayBuffer();
}
const ok = () => {
currentlyPlaying = false;
resolve("");
};

const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
await playAudioBuffer(audioBuffer, audioContext);
} catch (e) {
currentlyPlaying = false;
throw e;
}
const audio = new Audio(urlOrDataURI);
audio.onended = ok;
audio.onerror = ok;
audio.play().catch((e) => {
reject(e);
console.error("Audio playback failed:", e);
});
});
};
4 changes: 2 additions & 2 deletions koala/quiz-evaluators/speaking.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
Explanation,
grammarCorrection,
testEquivalence,
translateToEnglish,
} from "@/koala/openai";
import { QuizEvaluator, QuizEvaluatorOutput } from "./types";
import { strip } from "./evaluator-utils";
import { captureTrainingData } from "./capture-training-data";
import { prismaClient } from "../prisma-client";
import { grammarCorrection } from "../grammar";

const doGrade = async (
userInput: string,
Expand Down Expand Up @@ -63,7 +63,7 @@ function gradeWithGrammarCorrection(i: X, what: 1 | 2): QuizEvaluatorOutput {
} else {
return {
result: "fail",
userMessage: `Correct, but say "${i.correction}" instead of "${i.userInput}" (${what}).`,
userMessage: `Say "${i.correction}" instead of "${i.userInput}" (${what}).`,
};
}
}
Expand Down
46 changes: 46 additions & 0 deletions koala/routes/get-mirroring-cards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { z } from "zod";
import { prismaClient } from "../prisma-client";
import { procedure } from "../trpc-procedure";
import { generateLessonAudio } from "../speech";
import { map, shuffle } from "radash";

export const getMirrorCards = procedure
.input(z.object({}))
.output(
z.array(
z.object({
id: z.number(),
term: z.string(),
definition: z.string(),
audioUrl: z.string(),
translationAudioUrl: z.string(),
langCode: z.string(),
}),
),
)
.mutation(async ({ ctx }) => {
const cards = await prismaClient.card.findMany({
where: { userId: ctx.user?.id || "000", flagged: false },
orderBy: [{ mirrorRepetitionCount: "asc" }],
take: 200,
});
// Order by length of 'term' field:
cards.sort((a, b) => b.term.length - a.term.length);
const shortList = shuffle(cards.slice(0, 100)).slice(0, 5);
return await map(shortList, async (card) => {
return {
id: card.id,
term: card.term,
definition: card.definition,
langCode: card.langCode,
translationAudioUrl: await generateLessonAudio({
card,
lessonType: "speaking",
}),
audioUrl: await generateLessonAudio({
card,
lessonType: "listening",
}),
};
});
});
2 changes: 2 additions & 0 deletions koala/routes/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { exportCards } from "./export-cards";
import { faucet } from "./faucet";
import { flagCard } from "./flag-card";
import { getAllCards } from "./get-all-cards";
import { getMirrorCards } from "./get-mirroring-cards";
import { getNextQuizzes } from "./get-next-quizzes";
import { getOneCard } from "./get-one-card";
import { getPlaybackAudio } from "./get-playback-audio";
Expand Down Expand Up @@ -47,6 +48,7 @@ export const appRouter = router({
transcribeAudio,
translateText,
viewTrainingData,
getMirrorCards,
});

export type AppRouter = typeof appRouter;
Loading

0 comments on commit 00c6b0d

Please sign in to comment.