-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #96 from RickCarlino/prod
v3 Final Updates
- Loading branch information
Showing
9 changed files
with
371 additions
and
201 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}), | ||
}; | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.