From 0da1f89bb5c8473604b5b606ae21a657e62f4a63 Mon Sep 17 00:00:00 2001 From: tylerslaton Date: Wed, 27 Mar 2024 15:37:55 -0400 Subject: [PATCH 1/2] feat: remove old story-book example Signed-off-by: tylerslaton --- examples/story-book/README.md | 33 ------- examples/story-book/index.html | 147 ----------------------------- examples/story-book/story-book.gpt | 69 -------------- 3 files changed, 249 deletions(-) delete mode 100644 examples/story-book/README.md delete mode 100644 examples/story-book/index.html delete mode 100644 examples/story-book/story-book.gpt diff --git a/examples/story-book/README.md b/examples/story-book/README.md deleted file mode 100644 index aa1ddf99..00000000 --- a/examples/story-book/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Story Book - -Story Book is a GPTScript that can generate a story based on a prompt and the number of pages you want the story to be in. It is generated in HTML format and can then be viewed -by `index.html` which has some JS/CSS to make the story styling consistent and readable. - -## Usage Instructions - -1. **Run the `story-book.gpt` script.** - - In the same terminal session where the virtual environment (venv) is now activated, navigate to the `story-book` example directory and run the `story-book.gpt` script: - - ```shell - cd examples/story-book - gptscript story-book.gpt --prompt "Goldilocks" --pages 3 - ``` - -2. **View the story.** - - Open `index.html` in your browser to view the generated story. - -3. (optional) **Generate a new story.** - - To generate another story, you'll first need to delete the existing `pages` directory. In the `examples/story-book` directory, run the following command: - - ```shell - rm -rf pages - ``` - - After that, you can generate a new story by running the `story-book.gpt` script again with a different prompt or number of pages. - - ```shell - gptscript story-book.gpt --prompt "The Three Little Pigs" --pages 5 - ``` diff --git a/examples/story-book/index.html b/examples/story-book/index.html deleted file mode 100644 index e8bf7ebd..00000000 --- a/examples/story-book/index.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - Story Book - - - -

Story Book

-
- -
- - - - - - - \ No newline at end of file diff --git a/examples/story-book/story-book.gpt b/examples/story-book/story-book.gpt deleted file mode 100644 index a2e8d6d2..00000000 --- a/examples/story-book/story-book.gpt +++ /dev/null @@ -1,69 +0,0 @@ -tools: story-writer, story-illustrator, mkdir, sys.write, sys.read, sys.download -description: Writes a children's book and generates illustrations for it. -args: story: The story to write and illustrate. Can be a prompt or a complete story. -args: pages: The number of pages to generate - -Do the following steps sequentially: - -1. Create the `pages` directory if it does not already exist. -2. If ${story} is a prompt and not a complete children's story, call story-writer - to write a story based on the prompt. -3. Take ${story} and break it up into ${pages} logical "pages" of text. -4. For each page: - - Call story-illustrator to illustrate it. Be sure to include any relevant characters to include when - asking it to illustrate the page. - - Download the illustration to a file at pages/.png. -5. For each page and its illustration write an html file with the text on top and image below it to pages/page.html. - Assume the illustration file is a sibling to the html file, Add this style tag to the HTML file: - ```html - - ``` -6. Edit the "pages" variable array in index.html to serve the pages you created. Do not - edit anything else. Do not edit the page select field. - ---- -name: story-writer -description: Writes a story for children -args: prompt: The prompt to use for the story - -Write a story with a tone for children based on ${prompt}. - ---- -name: story-illustrator -tools: github.com/gptscript-ai/image-generation -description: Generates a illustration for a children's story -args: text: The text of the page to illustrate - -Think of a good prompt to generate an image to represent $text. Make sure to -include the name of any relevant characters in your prompt. Then use that prompt to -generate an illustration. Append any prompt that you have with ". In an pointilism cartoon -children's book style with no text in it". Only return the URL of the illustration. - ---- -name: mkdir -tools: sys.write -description: Creates a specified directory -args: dir: Path to the directory to be created - -#!bash - -mkdir ${dir} From 2794b9a0947f676e29ad77f763d3b7215b190df5 Mon Sep 17 00:00:00 2001 From: tylerslaton Date: Wed, 27 Mar 2024 16:01:26 -0400 Subject: [PATCH 2/2] refactor: overhaul story-book to be a full webapp - in app streaming of generating stories using node module - increase temp for story-writer to 1 - can export generated stories to a PDF Signed-off-by: tylerslaton --- examples/story-book/.gitignore | 27 +++++ examples/story-book/README.md | 22 ++++ examples/story-book/app.vue | 25 +++++ .../story-book/components/DisplayMode.vue | 25 +++++ examples/story-book/components/Nav.vue | 27 +++++ examples/story-book/components/New.vue | 94 +++++++++++++++++ examples/story-book/components/Stories.vue | 62 ++++++++++++ examples/story-book/lib/types.ts | 6 ++ examples/story-book/lib/unmangle.ts | 4 + examples/story-book/nuxt.config.ts | 9 ++ examples/story-book/package.json | 22 ++++ examples/story-book/pages/index.vue | 15 +++ examples/story-book/pages/story/[name].vue | 95 ++++++++++++++++++ examples/story-book/public/favicon.ico | Bin 0 -> 4286 bytes .../server/api/story/[name].delete.ts | 30 ++++++ .../story-book/server/api/story/[name].get.ts | 47 +++++++++ .../story-book/server/api/story/index.get.ts | 20 ++++ .../story-book/server/api/story/index.post.ts | 50 +++++++++ .../story-book/server/api/story/sse.get.ts | 55 ++++++++++ examples/story-book/server/tsconfig.json | 3 + examples/story-book/store/index.ts | 50 +++++++++ examples/story-book/story-book.gpt | 47 +++++++++ examples/story-book/tailwind.config.ts | 20 ++++ examples/story-book/tsconfig.json | 4 + 24 files changed, 759 insertions(+) create mode 100644 examples/story-book/.gitignore create mode 100644 examples/story-book/README.md create mode 100644 examples/story-book/app.vue create mode 100644 examples/story-book/components/DisplayMode.vue create mode 100644 examples/story-book/components/Nav.vue create mode 100644 examples/story-book/components/New.vue create mode 100644 examples/story-book/components/Stories.vue create mode 100644 examples/story-book/lib/types.ts create mode 100644 examples/story-book/lib/unmangle.ts create mode 100644 examples/story-book/nuxt.config.ts create mode 100644 examples/story-book/package.json create mode 100644 examples/story-book/pages/index.vue create mode 100644 examples/story-book/pages/story/[name].vue create mode 100644 examples/story-book/public/favicon.ico create mode 100644 examples/story-book/server/api/story/[name].delete.ts create mode 100644 examples/story-book/server/api/story/[name].get.ts create mode 100644 examples/story-book/server/api/story/index.get.ts create mode 100644 examples/story-book/server/api/story/index.post.ts create mode 100644 examples/story-book/server/api/story/sse.get.ts create mode 100644 examples/story-book/server/tsconfig.json create mode 100644 examples/story-book/store/index.ts create mode 100644 examples/story-book/story-book.gpt create mode 100644 examples/story-book/tailwind.config.ts create mode 100644 examples/story-book/tsconfig.json diff --git a/examples/story-book/.gitignore b/examples/story-book/.gitignore new file mode 100644 index 00000000..b8a88f6c --- /dev/null +++ b/examples/story-book/.gitignore @@ -0,0 +1,27 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example + +# AI generated files +public/stories diff --git a/examples/story-book/README.md b/examples/story-book/README.md new file mode 100644 index 00000000..4cacd008 --- /dev/null +++ b/examples/story-book/README.md @@ -0,0 +1,22 @@ +# Story Book + +Story Book is a web application that has an interface for users to input a prompt and number of pages. This information then generates a story based on the prompt. All generation is done using GPTScript on the backend. + +## Usage Instructions + +1. Make sure you have at least Node v20.11.1 installed. If you don't, you can install it [here](https://nodejs.org/en/download). + +2. Navigate to the `examples/story-book` directory. + +```bash +cd examples/story-book +``` + +3. Start the Nuxt application by running the following commands: + +```bash +npm i +npm run dev +``` + +4. Navigate to `http://localhost:3000` in your browser. \ No newline at end of file diff --git a/examples/story-book/app.vue b/examples/story-book/app.vue new file mode 100644 index 00000000..38414fbe --- /dev/null +++ b/examples/story-book/app.vue @@ -0,0 +1,25 @@ + + + diff --git a/examples/story-book/components/DisplayMode.vue b/examples/story-book/components/DisplayMode.vue new file mode 100644 index 00000000..396697e6 --- /dev/null +++ b/examples/story-book/components/DisplayMode.vue @@ -0,0 +1,25 @@ + + + diff --git a/examples/story-book/components/Nav.vue b/examples/story-book/components/Nav.vue new file mode 100644 index 00000000..934897b8 --- /dev/null +++ b/examples/story-book/components/Nav.vue @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/examples/story-book/components/New.vue b/examples/story-book/components/New.vue new file mode 100644 index 00000000..4f9532a6 --- /dev/null +++ b/examples/story-book/components/New.vue @@ -0,0 +1,94 @@ + + + + \ No newline at end of file diff --git a/examples/story-book/components/Stories.vue b/examples/story-book/components/Stories.vue new file mode 100644 index 00000000..1a70a0a1 --- /dev/null +++ b/examples/story-book/components/Stories.vue @@ -0,0 +1,62 @@ + + + \ No newline at end of file diff --git a/examples/story-book/lib/types.ts b/examples/story-book/lib/types.ts new file mode 100644 index 00000000..533463e9 --- /dev/null +++ b/examples/story-book/lib/types.ts @@ -0,0 +1,6 @@ +export type Pages = Record; + +export type Page = { + image_path: string; + content: string; +} diff --git a/examples/story-book/lib/unmangle.ts b/examples/story-book/lib/unmangle.ts new file mode 100644 index 00000000..e639f193 --- /dev/null +++ b/examples/story-book/lib/unmangle.ts @@ -0,0 +1,4 @@ +const unmangleStoryName = (storyName: string): string => { + return storyName.replaceAll('-', ' ').replace(/\b\w/g, c => c.toUpperCase()); +} +export default unmangleStoryName; \ No newline at end of file diff --git a/examples/story-book/nuxt.config.ts b/examples/story-book/nuxt.config.ts new file mode 100644 index 00000000..7a8bc507 --- /dev/null +++ b/examples/story-book/nuxt.config.ts @@ -0,0 +1,9 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + devtools: { enabled: true }, + modules: [ + '@nuxt/ui', + '@nuxtjs/tailwindcss', + '@pinia/nuxt', + ], +}) diff --git a/examples/story-book/package.json b/examples/story-book/package.json new file mode 100644 index 00000000..0f79be49 --- /dev/null +++ b/examples/story-book/package.json @@ -0,0 +1,22 @@ +{ + "name": "nuxt-app", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare" + }, + "dependencies": { + "@gptscript-ai/gptscript": "^0.2.0", + "@nuxt/ui": "^2.15.0", + "@pinia/nuxt": "^0.5.1", + "jspdf": "^2.5.1", + "nuxt": "^3.11.1", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + } +} diff --git a/examples/story-book/pages/index.vue b/examples/story-book/pages/index.vue new file mode 100644 index 00000000..832a40a2 --- /dev/null +++ b/examples/story-book/pages/index.vue @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/examples/story-book/pages/story/[name].vue b/examples/story-book/pages/story/[name].vue new file mode 100644 index 00000000..10ae69c9 --- /dev/null +++ b/examples/story-book/pages/story/[name].vue @@ -0,0 +1,95 @@ + + + \ No newline at end of file diff --git a/examples/story-book/public/favicon.ico b/examples/story-book/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..18993ad91cfd43e03b074dd0b5cc3f37ab38e49c GIT binary patch literal 4286 zcmeHLOKuuL5PjK%MHWVi6lD zOGiREbCw`xmFozJ^aNatJY>w+g ze6a2@u~m#^BZm@8wco9#Crlli0uLb^3E$t2-WIc^#(?t)*@`UpuofJ(Uyh@F>b3Ph z$D^m8Xq~pTkGJ4Q`Q2)te3mgkWYZ^Ijq|hkiP^9`De={bQQ%heZC$QU2UpP(-tbl8 zPWD2abEew;oat@w`uP3J^YpsgT%~jT(Dk%oU}sa$7|n6hBjDj`+I;RX(>)%lm_7N{+B7Mu%H?422lE%MBJH!!YTN2oT7xr>>N-8OF$C&qU^ z>vLsa{$0X%q1fjOe3P1mCv#lN{xQ4_*HCSAZjTb1`}mlc+9rl8$B3OP%VT@mch_~G z7Y+4b{r>9e=M+7vSI;BgB?ryZDY4m>&wcHSn81VH1N~`0gvwH{ z8dv#hG|OK`>1;j7tM#B)Z7zDN?{6=dUal}$e { + try { + let name = getRouterParam(event, 'name') + if (!name) { + throw createError({ + statusCode: 400, + statusMessage: 'name is required' + }); + } + + name = decodeURIComponent(name); + + await fs.promises.readdir(`public/stories/${name}`) + fs.promises.rm(`public/stories/${name}`, { recursive: true }) + } catch (error) { + // if the error is a 404 error, we can throw it directly + if ((error as any).code === 'ENOENT') { + throw createError({ + statusCode: 404, + statusMessage: 'story not found', + }) + } + throw createError({ + statusCode: 500, + statusMessage: `error removing story: ${error}`, + }) + } +}) \ No newline at end of file diff --git a/examples/story-book/server/api/story/[name].get.ts b/examples/story-book/server/api/story/[name].get.ts new file mode 100644 index 00000000..bf6593d8 --- /dev/null +++ b/examples/story-book/server/api/story/[name].get.ts @@ -0,0 +1,47 @@ +import fs from 'fs' + +type Pages = Record; +type Page = { + image_path: string; + content: string; +} + +export default defineEventHandler(async (event) => { + try { + let name = getRouterParam(event, 'name'); + if (!name) { + throw createError({ + statusCode: 400, + statusMessage: 'name is required' + }); + } + + name = decodeURIComponent(name); + + const files = await fs.promises.readdir(`public/stories/${name}`) + + const pages: Pages = {}; + for (const file of files) { + if (!file.endsWith('.txt')) continue + const page = await fs.promises.readFile(`public/stories/${name}/${file}`, 'utf-8') + pages[ file.replace('.txt', '').replace('page', '')] = { + image_path: `/stories/${name}/${file.replace('.txt', '.png')}`, + content: page + } + } + + return pages + } catch (error) { + // if the error is a 404 error, we can throw it directly + if ((error as any).code === 'ENOENT') { + throw createError({ + statusCode: 404, + statusMessage: 'story found', + }) + } + throw createError({ + statusCode: 500, + statusMessage: `error fetching story: ${error}`, + }) + } +}) \ No newline at end of file diff --git a/examples/story-book/server/api/story/index.get.ts b/examples/story-book/server/api/story/index.get.ts new file mode 100644 index 00000000..63e101fd --- /dev/null +++ b/examples/story-book/server/api/story/index.get.ts @@ -0,0 +1,20 @@ +import fs from 'fs' + +export default defineEventHandler(async (event) => { + try { + const stories = await fs.promises.readdir('public/stories') + return stories + } catch (error) { + // if the error is a 404 error, we can throw it directly + if ((error as any).code === 'ENOENT') { + throw createError({ + statusCode: 404, + statusMessage: 'no stories found', + }) + } + throw createError({ + statusCode: 500, + statusMessage: `error fetching stories: ${error}`, + }) + } +}) \ No newline at end of file diff --git a/examples/story-book/server/api/story/index.post.ts b/examples/story-book/server/api/story/index.post.ts new file mode 100644 index 00000000..88ba57c4 --- /dev/null +++ b/examples/story-book/server/api/story/index.post.ts @@ -0,0 +1,50 @@ +import gptscript from '@gptscript-ai/gptscript' +import { Readable } from 'stream' + +type Request = { + prompt: string; + pages: number; +} + +export type RunningScript = { + stdout: Readable; + stderr: Readable; + promise: Promise; +} + +export const runningScripts: Record= {} + +export default defineEventHandler(async (event) => { + const request = await readBody(event) as Request + + if (!request.prompt) { + throw createError({ + statusCode: 400, + statusMessage: 'prompt is required' + }); + } + + if (!request.pages) { + throw createError({ + statusCode: 400, + statusMessage: 'pages are required' + }); + } + + const {stdout, stderr, promise} = await gptscript.streamExecFile('story-book.gpt', `--story ${request.prompt} --pages ${request.pages}`, {}) + + setHeaders(event,{ + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Credentials': 'true', + 'Connection': 'keep-alive', + 'Content-Type': 'text/event-stream', + }) + + setResponseStatus(event, 202) + + runningScripts[request.prompt] = { + stdout: stdout, + stderr: stderr, + promise: promise + } +}) \ No newline at end of file diff --git a/examples/story-book/server/api/story/sse.get.ts b/examples/story-book/server/api/story/sse.get.ts new file mode 100644 index 00000000..00a1e4c3 --- /dev/null +++ b/examples/story-book/server/api/story/sse.get.ts @@ -0,0 +1,55 @@ +import { runningScripts } from '@/server/api/story/index.post'; + +export default defineEventHandler(async (event) => { + const { prompt } = getQuery(event); + if (!prompt) { + throw createError({ + statusCode: 400, + statusMessage: 'prompt is required' + }); + } + + const runningScript = runningScripts[prompt as string]; + if (!runningScript) { + throw createError({ + statusCode: 404, + statusMessage: 'running script not found' + }); + } + + setResponseStatus(event, 200); + setHeaders(event, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Credentials': 'true', + 'Connection': 'keep-alive', + 'Content-Type': 'text/event-stream', + }); + + let stdoutBuffer = ''; + runningScript.stdout.on('data', (data) => { + stdoutBuffer += data; + if (data.includes('\n')) { + event.node.res.write(`data: ${stdoutBuffer}\n\n`); + stdoutBuffer = ''; + } + }); + + let stderrBuffer = ''; + runningScript.stderr.on('data', (data) => { + stderrBuffer += data; + if (data.includes('\n')) { + event.node.res.write(`data: ${stderrBuffer}\n\n`); + stderrBuffer = ''; + } + }); + + event._handled = true; + await runningScript.promise.then(() => { + event.node.res.write('data: done\n\n'); + event.node.res.end(); + }).catch((error) => { + setResponseStatus(event, 500); + event.node.res.write(`data: error: ${error}\n\n`); + event.node.res.end(); + }); +}); \ No newline at end of file diff --git a/examples/story-book/server/tsconfig.json b/examples/story-book/server/tsconfig.json new file mode 100644 index 00000000..b9ed69c1 --- /dev/null +++ b/examples/story-book/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/examples/story-book/store/index.ts b/examples/story-book/store/index.ts new file mode 100644 index 00000000..18cdf2ac --- /dev/null +++ b/examples/story-book/store/index.ts @@ -0,0 +1,50 @@ +// store/index.ts +import { defineStore } from 'pinia' + +export const useMainStore = defineStore({ + id: 'main', + state: () => ({ + pendingStories: {} as Record, + pendingStoryMessages: {} as Record, + stories: [] as string[] + }), + actions: { + addPendingStory(name: string, es: EventSource) { + this.pendingStories[name] = es + }, + removePendingStory(name: string) { + this.pendingStories[name].close() + delete this.pendingStories[name] + }, + addPendingStoryMessage(name: string, message: string) { + // implement a queue with a length of 10 that removes the oldest message when the length is reached + if (!this.pendingStoryMessages[name]) { + this.pendingStoryMessages[name] = [] + } + this.pendingStoryMessages[name].push(message) + if (this.pendingStoryMessages[name].length > 12) { + this.pendingStoryMessages[name].shift() + } + }, + addStory(name: string) { + this.stories.push(name) + }, + addStories(names: string[]) { + // only push if the story is not already in the list + names.forEach(name => { + if (!this.stories.includes(name)) { + this.stories.push(name) + } + }) + }, + removeStory(name: string) { + this.stories = this.stories.filter(s => s !== name) + }, + async fetchStories() { + if (Object.keys(this.pendingStories).length === 0) { + this.addStories(await $fetch('/api/story') as string[]) + } + } + + } +}) \ No newline at end of file diff --git a/examples/story-book/story-book.gpt b/examples/story-book/story-book.gpt new file mode 100644 index 00000000..57d80db8 --- /dev/null +++ b/examples/story-book/story-book.gpt @@ -0,0 +1,47 @@ +tools: story-writer, story-illustrator, mkdir, sys.write, sys.read, sys.download +description: Writes a children's book and generates illustrations for it. +args: story: The story to write and illustrate. Can be a prompt or a complete story. +args: pages: The number of pages to generate + +Do the following steps sequentially: + +1. Come up with an appropriate title for the story based on the ${prompt} +2. Create the `public/stories/${story-title}` directory if it does not already exist. +3. If ${story} is a prompt and not a complete children's story, call story-writer + to write a story based on the prompt. +4. Take ${story} and break it up into ${pages} logical "pages" of text. +5. For each page: + - For the content of the page, write it to `public/stories/${story-title}/page.txt and add appropriate newline + characters. + - Call story-illustrator to illustrate it. Be sure to include any relevant characters to include when + asking it to illustrate the page. + - Download the illustration to a file at `public/stories/${story-title}/page.png`. + +--- +name: story-writer +description: Writes a story for children +args: prompt: The prompt to use for the story +temperature: 1 + +Write a story with a tone for children based on ${prompt}. + +--- +name: story-illustrator +tools: github.com/gptscript-ai/image-generation +description: Generates a illustration for a children's story +args: text: The text of the page to illustrate + +Think of a good prompt to generate an image to represent $text. Make sure to +include the name of any relevant characters in your prompt. Then use that prompt to +generate an illustration. Append any prompt that you have with ". In an pointilism cartoon +children's book style with no text in it". Only return the URL of the illustration. + +--- +name: mkdir +tools: sys.write +description: Creates a specified directory +args: dir: Path to the directory to be created. Will create parent directories. + +#!bash + +mkdir -p "${dir}" diff --git a/examples/story-book/tailwind.config.ts b/examples/story-book/tailwind.config.ts new file mode 100644 index 00000000..63da7b5e --- /dev/null +++ b/examples/story-book/tailwind.config.ts @@ -0,0 +1,20 @@ +import type { Config } from 'tailwindcss' +import defaultTheme from 'tailwindcss/defaultTheme' +import typography from '@tailwindcss/typography' + +export default > { + content: [ + './src/components/**/*.{js,vue,ts}', + './src/layouts/**/*.vue', + './src/pages/**/*.vue', + './src/plugins/**/*.{js,ts}', + './src/app.vue', + './src/error.vue', + ], + + darkMode: 'class', + + plugins: [ + typography, + ], +} diff --git a/examples/story-book/tsconfig.json b/examples/story-book/tsconfig.json new file mode 100644 index 00000000..a746f2a7 --- /dev/null +++ b/examples/story-book/tsconfig.json @@ -0,0 +1,4 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +}