From 62bc8fc7da909efad6ead514e03a9bc8f9ebe27f Mon Sep 17 00:00:00 2001 From: TJ Maynes Date: Mon, 15 Jan 2024 11:23:29 -0600 Subject: [PATCH] chore: remove cloudflare kv requirement for project, single read-only file works for me --- Makefile | 10 +-- README.md | 2 +- next.config.js | 8 -- package.json | 2 +- script/cloudflare/add-cloudflare-kv-value.sh | 40 ---------- .../cloudflare/ensure-cloudflare-kv-exists.sh | 34 --------- script/{ => data}/image-classes.json | 0 script/{ => data}/seed.js | 75 ++++--------------- src/app/_components/BackgroundInfo.tsx | 15 ++-- src/app/api/v1/image/description/route.ts | 23 ++---- {data => src/data}/seed.json | 0 src/env.d.ts | 12 --- src/lib/cache-client.ts | 41 ---------- src/lib/openai.ts | 28 ------- 14 files changed, 35 insertions(+), 255 deletions(-) delete mode 100644 script/cloudflare/add-cloudflare-kv-value.sh delete mode 100755 script/cloudflare/ensure-cloudflare-kv-exists.sh rename script/{ => data}/image-classes.json (100%) rename script/{ => data}/seed.js (72%) rename {data => src/data}/seed.json (100%) delete mode 100644 src/env.d.ts delete mode 100644 src/lib/cache-client.ts delete mode 100644 src/lib/openai.ts diff --git a/Makefile b/Makefile index 9dac044..c74f509 100644 --- a/Makefile +++ b/Makefile @@ -24,10 +24,6 @@ performance: test: performance -ensure_cloudflare_kv_exists: - chmod +x ./script/cloudflare/ensure-cloudflare-kv-exists.sh - ./script/cloudflare/ensure-cloudflare-kv-exists.sh "IMAGE_ANALYZER_KV" - ensure_cloudflare_page_exists: chmod +x ./script/cloudflare/ensure-cloudflare-pages-exists.sh ./script/cloudflare/ensure-cloudflare-pages-exists.sh "image-analyzer-app" @@ -35,7 +31,7 @@ ensure_cloudflare_page_exists: ensure_cloudflare_secrets_exist: @$(call add_cloudflare_secret,"image-analyzer-app","NODE_VERSION",$(shell cat .node-version)) -ensure_cloudflare_infra_exists: ensure_cloudflare_page_exists ensure_cloudflare_kv_exists ensure_cloudflare_secrets_exist +ensure_cloudflare_infra_exists: ensure_cloudflare_page_exists ensure_cloudflare_secrets_exist deploy_cloudflare_page: npm run pages:deploy @@ -43,9 +39,7 @@ deploy_cloudflare_page: deploy: install build ensure_cloudflare_infra_exists deploy_cloudflare_page seed: - node ./script/seed.js \ - --cloudflare-kv-binding-id "5df82e748f494385a2aeaf2912cbb359" \ - --seed-file "./data/seed.json" + node ./script/data/seed.js --seed-file "./src/data/seed.json" clean: rm -rf .next/ .vercel/ build/ diff --git a/README.md b/README.md index d30ae4e..afe3516 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # image-analyzer-app -> a NextJS app that allows users to analyze images using MobileNet (via TensorflowJS), ChatGPT, and Cloudflare Pages +> a NextJS app that allows users to analyze images using MobileNet (via TensorflowJS), image descriptions from ChatGPT, and hosted on Cloudflare Pages ## Requirements diff --git a/next.config.js b/next.config.js index 4846ddc..767719f 100644 --- a/next.config.js +++ b/next.config.js @@ -1,12 +1,4 @@ /** @type {import('next').NextConfig} */ const nextConfig = {} -const { - setupDevBindings, -} = require('@cloudflare/next-on-pages/__experimental__next-dev') - -setupDevBindings({ - kvNamespaces: ['IMAGE_ANALYZER_KV'], -}) - module.exports = nextConfig diff --git a/package.json b/package.json index aeb425d..bc737ae 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "pages:build": "npx @cloudflare/next-on-pages", "pages:deploy": "wrangler pages deploy .vercel/output/static --project-name='image-analyzer-app'", "pages:watch": "next-on-pages --watch", - "pages:dev": "npm run pages:build && wrangler pages dev .vercel/output/static --compatibility-date=2023-10-30 --compatibility-flag=nodejs_compat --kv IMAGE_ANALYZER_KV" + "pages:dev": "npm run pages:build && wrangler pages dev .vercel/output/static --compatibility-date=2023-10-30 --compatibility-flag=nodejs_compat" }, "dependencies": { "@tensorflow-models/mobilenet": "^2.1.1", diff --git a/script/cloudflare/add-cloudflare-kv-value.sh b/script/cloudflare/add-cloudflare-kv-value.sh deleted file mode 100644 index e31ead2..0000000 --- a/script/cloudflare/add-cloudflare-kv-value.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -e - -CLOUDFLARE_KV_BINDING_ID=$1 -CLOUDFLARE_KV_KEY=$2 -CLOUDFLARE_KV_VALUE=$3 - -function check_requirements() { - if [[ -z "$(command -v node)" ]]; then - echo "Please install 'node' program before running this script" - exit 1 - elif [[ -z "$CLOUDFLARE_ACCOUNT_ID" ]]; then - echo "Please ensure environment variable 'CLOUDFLARE_ACCOUNT_ID' exists before running this script" - exit 1 - elif [[ -z "$CLOUDFLARE_API_TOKEN" ]]; then - echo "Please ensure environment variable 'CLOUDFLARE_API_TOKEN' exists before running this script" - exit 1 - elif [[ -z "$CLOUDFLARE_KV_BINDING_ID" ]]; then - echo "Arg 1 for script 'CLOUDFLARE_KV_BINDING_ID' was not given..." - exit 1 - elif [[ -z "$CLOUDFLARE_KV_KEY" ]]; then - echo "Arg 2 for script 'CLOUDFLARE_KV_KEY' was not given..." - exit 1 - elif [[ -z "$CLOUDFLARE_KV_VALUE" ]]; then - echo "Arg 3 for script 'CLOUDFLARE_KV_VALUE' was not given..." - exit 1 - fi -} - -function main() { - check_requirements - - if ! ./node_modules/.bin/wrangler kv:key get "$CLOUDFLARE_KV_KEY" --namespace-id="$CLOUDFLARE_KV_BINDING_ID" > /dev/null 2>&1; then - echo "Adding Cloudflare KV key '$CLOUDFLARE_KV_KEY' and value '$CLOUDFLARE_KV_VALUE' to KV '$CLOUDFLARE_KV_NAME'..." - ./node_modules/.bin/wrangler kv:key put "$CLOUDFLARE_KV_KEY" "$CLOUDFLARE_KV_VALUE" --namespace-id="$CLOUDFLARE_KV_BINDING_ID" - fi -} - -main diff --git a/script/cloudflare/ensure-cloudflare-kv-exists.sh b/script/cloudflare/ensure-cloudflare-kv-exists.sh deleted file mode 100755 index ef37997..0000000 --- a/script/cloudflare/ensure-cloudflare-kv-exists.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -set -e - -CLOUDFLARE_KV_NAME=$1 - -function check_requirements() { - if [[ -z "$(command -v node)" ]]; then - echo "Please install 'node' program before running this script" - exit 1 - elif [[ -z "$CLOUDFLARE_ACCOUNT_ID" ]]; then - echo "Please ensure environment variable 'CLOUDFLARE_ACCOUNT_ID' exists before running this script" - exit 1 - elif [[ -z "$CLOUDFLARE_API_TOKEN" ]]; then - echo "Please ensure environment variable 'CLOUDFLARE_API_TOKEN' exists before running this script" - exit 1 - elif [[ -z "$CLOUDFLARE_KV_NAME" ]]; then - echo "Arg 1 for script 'CLOUDFLARE_KV_NAME' was not given..." - exit 1 - fi -} - -function main() { - check_requirements - - if ./node_modules/.bin/wrangler kv:namespace list | grep -q "$CLOUDFLARE_KV_NAME"; then - echo -e "\nCloudflare KV '$CLOUDFLARE_KV_NAME' already exists...\n" - else - echo "Cloudflare KV '$CLOUDFLARE_KV_NAME' does not exist..." - ./node_modules/.bin/wrangler kv:namespace create "$CLOUDFLARE_KV_NAME" - fi -} - -main diff --git a/script/image-classes.json b/script/data/image-classes.json similarity index 100% rename from script/image-classes.json rename to script/data/image-classes.json diff --git a/script/seed.js b/script/data/seed.js similarity index 72% rename from script/seed.js rename to script/data/seed.js index b1bb505..66e3444 100644 --- a/script/seed.js +++ b/script/data/seed.js @@ -1,5 +1,4 @@ const fs = require('fs/promises') -const { execSync } = require('child_process') const OpenAI = require('openai') const imageClasses = require('./image-classes.json') @@ -10,30 +9,23 @@ const throwIfEnvVarDoesNotExist = (envVarName) => { } } -const cleanDescription = (description) => - description.replace(/\"/g, "'").replace(/(\r\n|\n|\r)/gm, ' ') +const getFlagValue = (flagName) => { + const flagIndex = process.argv.indexOf(`--${flagName}`) + const flagValue = flagIndex > -1 ? process.argv[flagIndex + 1] : '' -const addKeyValueToCloudflareKV = (cloudflareKVBindingId, key, value) => { - console.log( - `Attempting to add key "${key}" to Cloudflare KV: '${cloudflareKVBindingId}'` - ) + if (!flagValue) { + console.error( + `Expected required flag '--${flagName}' to be passed to script!` + ) + process.exit(1) + } - execSync( - `bash ./script/cloudflare/add-cloudflare-kv-value.sh "${cloudflareKVBindingId}" "${key}" "${value}"`, - (err, output) => { - if (err) { - console.error('could not execute command: ', err) - return - } - console.log('Output: \n', output) - } - ) + return flagValue } const batchSeedProcess = async ( currentSeedFileContent, seedFile, - cloudflareKVBindingId, limitBatchSize = 5 ) => { const currentClassifications = imageClasses.slice( @@ -103,14 +95,6 @@ const batchSeedProcess = async ( flag: 'w', }) - newSeedData.forEach(({ name, description }) => { - addKeyValueToCloudflareKV( - cloudflareKVBindingId, - name, - cleanDescription(description) - ) - }) - latestSeedFileContent = newSeedFileContent } @@ -139,20 +123,6 @@ const ensureSeedFileExists = (seedFile, onSeedFileConfirmation) => } }) -const getFlag = (flagName) => { - const flagIndex = process.argv.indexOf(`--${flagName}`) - const flagValue = flagIndex > -1 ? process.argv[flagIndex + 1] : '' - - if (!flagValue) { - console.error( - `Expected required flag '--${flagName}' to be passed to script!` - ) - process.exit(1) - } - - return flagValue -} - const main = async () => { ;['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN', 'OPENAI_API_KEY'].forEach( (envVarName) => throwIfEnvVarDoesNotExist(envVarName) @@ -163,26 +133,13 @@ const main = async () => { process.exit(1) } - const [seedFile, cloudflareKVBindingId] = [ - getFlag('seed-file'), - getFlag('cloudflare-kv-binding-id'), - ] - - const updatedSeedData = await ensureSeedFileExists( - seedFile, - (currentSeedData) => - currentSeedData.data.length !== imageClasses.length - ? batchSeedProcess(currentSeedData, seedFile, cloudflareKVBindingId, 5) - : currentSeedData - ) + const [seedFile] = [getFlagValue('seed-file')] - updatedSeedData.data.forEach(({ name, description }) => { - addKeyValueToCloudflareKV( - cloudflareKVBindingId, - name, - cleanDescription(description) - ) - }) + return await ensureSeedFileExists(seedFile, (currentSeedData) => + currentSeedData.data.length !== imageClasses.length + ? batchSeedProcess(currentSeedData, seedFile, 5) + : currentSeedData + ) } main().then(() => console.log('Done!')) diff --git a/src/app/_components/BackgroundInfo.tsx b/src/app/_components/BackgroundInfo.tsx index 4a1f044..4f77d53 100644 --- a/src/app/_components/BackgroundInfo.tsx +++ b/src/app/_components/BackgroundInfo.tsx @@ -10,15 +10,16 @@ const BackgroundInfo = () => ( ChatGPT {' '} - (for generating image descriptions), and{' '} + (for generating image descriptions), and hosted on{' '} - - Cloudflare KV - + Cloudflare Pages {' '} - (read-only database pairing "image name" to "image - description"). I built this web application to learn how to run maching - learning models in web browsers. Please contact me on{' '} + (via{' '} + + next-on-pages + + ). I built this web application to learn how to run maching learning models + in web browsers. Please contact me on{' '} LinkedIn for any feedback or concerns. Enjoy! 😀

diff --git a/src/app/api/v1/image/description/route.ts b/src/app/api/v1/image/description/route.ts index 623175b..29cd341 100644 --- a/src/app/api/v1/image/description/route.ts +++ b/src/app/api/v1/image/description/route.ts @@ -1,23 +1,14 @@ import 'server-only' import { NextRequest, NextResponse } from 'next/server' -import { - CacheClient, - InMemoryCacheClient, - KVCacheClient, -} from '@/lib/cache-client' +import seedData from '@/data/seed.json' -export const runtime = 'edge' - -const createCacheClient = (): CacheClient => { - if (process.env.NODE_ENV === 'development') return new InMemoryCacheClient() - - const IMAGE_ANALYZER_KV = process.env.IMAGE_ANALYZER_KV +const imageDescriptionData = seedData.data.reduce>( + (accum, curr) => ({ ...accum, ...{ [curr.name]: curr.description } }), + {} +) - return new KVCacheClient(IMAGE_ANALYZER_KV) -} - -const cacheClient = createCacheClient() +export const runtime = 'edge' export async function GET(request: NextRequest) { const thing = request.nextUrl.searchParams.get('thing') @@ -28,7 +19,7 @@ export async function GET(request: NextRequest) { status: 422, }) - const response = await cacheClient.get(thing) + const response = imageDescriptionData[thing] return response ? NextResponse.json({ message: response, status: 200 }) diff --git a/data/seed.json b/src/data/seed.json similarity index 100% rename from data/seed.json rename to src/data/seed.json diff --git a/src/env.d.ts b/src/env.d.ts deleted file mode 100644 index 422cb57..0000000 --- a/src/env.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { KVNamespace } from '@cloudflare/workers-types' - -declare global { - namespace NodeJS { - interface ProcessEnv { - [key: string]: string | undefined - IMAGE_ANALYZER_KV: KVNamespace - } - } -} - -export {} diff --git a/src/lib/cache-client.ts b/src/lib/cache-client.ts deleted file mode 100644 index 3b9a38e..0000000 --- a/src/lib/cache-client.ts +++ /dev/null @@ -1,41 +0,0 @@ -import 'server-only' - -import { KVNamespace } from '@cloudflare/workers-types' - -export interface CacheClient { - get(key: string): Promise - put(key: string, value: string): Promise -} - -export class KVCacheClient implements CacheClient { - private readonly store: KVNamespace - - constructor(store: KVNamespace) { - this.store = store - } - - get(key: string): Promise { - return this.store.get(key) - } - - put(key: string, value: string): Promise { - return this.store.put(key, value) - } -} - -export class InMemoryCacheClient implements CacheClient { - private readonly store: Record - - constructor(store: any = {}) { - this.store = store - } - - get(key: string): Promise { - return Promise.resolve(this.store[key] ?? null) - } - - put(key: string, value: string): Promise { - this.store[key] = value - return Promise.resolve() - } -} diff --git a/src/lib/openai.ts b/src/lib/openai.ts deleted file mode 100644 index 8dcfc17..0000000 --- a/src/lib/openai.ts +++ /dev/null @@ -1,28 +0,0 @@ -import 'server-only' - -import OpenAI from 'openai' - -const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, -}) - -export interface IOpenAIWrapper { - chat(prompt: string): Promise<{ response: string }> -} - -export default class OpenAIWrapper implements IOpenAIWrapper { - async chat(prompt: string): Promise<{ response: string }> { - try { - const response = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo', - messages: [{ role: 'user', content: prompt }], - }) - - const openaiResponse = response.choices.join(', ') - - return Promise.resolve({ response: openaiResponse }) - } catch (e) { - return Promise.reject(e) - } - } -}