diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..bffb357a7 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore index 1f9bda37a..6604794ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,206 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Environments .env -env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# next.js +/.next/ +/out/ + +# misc .DS_Store -nodemodules +*.pem +.env.local +.env.development.local +.env.test.local +.env.production.local +.history/ + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# twitter data files +model/data/* +!model/data/banger_accounts.csv + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. diff --git a/README.md b/README.md index 97abaff86..a4e3a6f4c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Text-to-banger + A simple API converting a user's proposed tweet into a veritable banger. ## To run the app locally + ### Clone the repo ``` @@ -9,30 +11,23 @@ gh repo clone effectiveaccelerationism/text-to-banger ``` ### Install and run the app + ``` -cd webapp npm install -npm start +npm run dev ``` + The app will be running on http://localhost:3000 ### Run the OpenAI api server -Currently the app runs with an OpenAI API server, soon to be dismissed in favor of a custom finetuned model. To run the OpenAI API server, you need to have an OpenAI API key. You can get one [here](https://platform.openai.com/account/api-keys). Once you have your API key, create a file named `.env` in the `apiserver` directory and add the following line to it: + +Currently the app runs with an OpenAI API server, soon to be dismissed in favor of a custom finetuned model. To run the OpenAI API server, you need to have an OpenAI API key. You can get one [here](https://platform.openai.com/account/api-keys). Once you have your API key, rename the file named `.env.example` to `.env` and add your API key to the file like so: + ``` OPENAI_API_KEY=your-openai-key ``` -Then, run the server with -``` -cd api -python -m venv env -pip install -r requirements.txt -source env/bin/activate # on Windows use `env\Scripts\activate` -python server.py -``` - -NOTE: You must have both the webapp and the apiserver running in two separate terminal instances to use the app. - ## Model TODOs + - [x] Get down a list of banger accounts - [x] Script getting the last 100 text tweets from the account in the banger accounts list - [x] Script filtering the tweets by a set likes/followers ratio @@ -40,10 +35,12 @@ NOTE: You must have both the webapp and the apiserver running in two separate te - [ ] Fine-tuning script taking as input the boring bangers and outputting the bangers ## API TODOs + - [x] Create OAI API server - [ ] Add Custom API server with finetuned model ## WebApp TODOs + - [x] Add dark mode and set it as default - [ ] Get BANGER OAI API prompt - [ ] Deploy webapp diff --git a/api/.env.example b/api/.env.example deleted file mode 100644 index f2b9c1960..000000000 --- a/api/.env.example +++ /dev/null @@ -1 +0,0 @@ -OPENAI_API_KEY=your-openai-key diff --git a/api/requirements.txt b/api/requirements.txt deleted file mode 100644 index 2651f0454..000000000 --- a/api/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -openai==0.27.8 -python-dotenv==1.0.0 diff --git a/api/server.py b/api/server.py deleted file mode 100644 index b11d42641..000000000 --- a/api/server.py +++ /dev/null @@ -1,88 +0,0 @@ -import re -import openai, json, os -from dotenv import load_dotenv -from http.server import BaseHTTPRequestHandler, HTTPServer - -# Load environment variables -load_dotenv() - -api_key = os.getenv("OPENAI_API_KEY") -openai.api_key = api_key -hostName = "localhost" -serverPort = 8080 - -def generate_banger(tweet_text): - print(f"Generating banger for tweet: '{tweet_text}'") - prompt = f"Turn this tweet into a solid banger, where a banger is a tweet of shocking and mildly psychotic comedic value, that's prone to go viral: '{tweet_text}'" - response = openai.Completion.create( - engine="text-davinci-003", # You can choose a different engine based on your subscription - prompt=prompt, - max_tokens=100, - temperature=0.7, # Adjust the temperature for more randomness (0.2 to 1.0) - ) - banger_tweet = response.choices[0].text.strip() - - # Remove hashtags - banger_tweet = re.sub(r'#\S+', '', banger_tweet) # Remove hashtags - - # Remove emojis - # banger_tweet = banger_tweet.encode('ascii', 'ignore').decode('ascii') - - # Remove starting and ending single and double quotes - banger_tweet = re.sub(r'^"|"$|^\'|\'$', '', banger_tweet) - - # Remove dot at the end if they exists - banger_tweet = re.sub(r'\.$', '', banger_tweet.strip()) - - if not banger_tweet: - return None - - print(f"Generated banger: '{banger_tweet}'") - return banger_tweet - -class MyServer(BaseHTTPRequestHandler): - def do_OPTIONS(self): - self.send_response(200, "ok") - self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') - self.send_header("Access-Control-Allow-Headers", "X-Requested-With, Content-type") - self.end_headers() - return - def do_POST(self): - url = self.path - if(url == '/generate-banger'): - content_length = int(self.headers['Content-Length']) - post_data = self.rfile.read(content_length) - post_data = post_data.decode('utf-8') - parsedData = json.loads(post_data) - originalText = parsedData['originalText'] - banger_tweet = generate_banger(originalText) - - if banger_tweet: - self.send_response(200) - else: - self.send_response(500) - banger_tweet = "Error generating banger tweet." - - self.send_header("Content-type", "text/html") - self.send_header('Access-Control-Allow-Origin', '*') - self.end_headers() - self.wfile.write(bytes(banger_tweet, "utf-8")) - else: - self.send_response(404) - self.send_header("Content-type", "text/html") - self.send_header('Access-Control-Allow-Origin', '*') - self.end_headers() - self.wfile.write(bytes('null', "utf-8")) -3 -if __name__ == "__main__": - webServer = HTTPServer((hostName, int(serverPort)), MyServer) - print("Server started http://%s:%s" % (hostName, serverPort)) - - try: - webServer.serve_forever() - except KeyboardInterrupt: - pass - - webServer.server_close() - print("Server stopped.") \ No newline at end of file diff --git a/app/api/banger/route.ts b/app/api/banger/route.ts new file mode 100644 index 000000000..1b09ba582 --- /dev/null +++ b/app/api/banger/route.ts @@ -0,0 +1,48 @@ +import { Configuration, OpenAIApi } from "openai-edge"; +// import { OpenAIStream, StreamingTextResponse } from "ai"; + +export const runtime = "edge"; + +const apiConfig = new Configuration({ + apiKey: process.env.OPENAI_API_KEY!, +}); + +const openai = new OpenAIApi(apiConfig); + +// Build a prompt for the banger tweet +function buildPrompt(tweet: string) { + return `Turn this tweet into a solid banger, where a banger is a tweet of shocking and mildly psychotic comedic value, that's prone to go viral: '${tweet}'`; +} + +export async function POST(req: Request) { + // Extract the `tweet` from the body of the request + const { tweet } = await req.json(); + + // Request the OpenAI API for the response based on the prompt + const response = await openai.createCompletion({ + model: "text-davinci-003", //You can choose a different engine based on your subscription + // stream: true, + prompt: buildPrompt(tweet), + max_tokens: 100, + temperature: 0.7, //Adjust the temperature for more randomness (0.2 to 1.0) + top_p: 1, + }); + // Respond with the JSON + const json = await response.json(); + + let bangerTweet = json.choices[0].text; + + bangerTweet = bangerTweet.replace(/#\S+/g, ""); // Remove hashtags + bangerTweet = bangerTweet.replace(/"/g, ""); // Remove double quotes as quotes within output are single quotes + bangerTweet = bangerTweet.replace(/\.$/, "").trim(); // Remove dot at the end if it exists + + // Respond with the processed banger tweet + return new Response(bangerTweet); + + //** Uncomment the following to stream the response instead of returning it as JSON. ie When you're ~sure it won't return any hashtags **// + // Convert the response into a friendly text-stream + // const stream = OpenAIStream(response); + + // Respond with the stream + // return new StreamingTextResponse(stream); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 000000000..692a73f41 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,48 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@media only screen and (max-width: 768px) { + .tweet-form { + width: 80%; + margin: 0 auto; + } + + .generated-tweet-container { + width: 80%; + margin: 0 auto; + text-align: center; + } +} + +@layer base { + @font-face { + font-family: "AnonPro"; + src: url("../public/fonts/AnonymousPro-Bold.ttf") format("truetype"); + font-weight: bold; + } + @font-face { + font-family: "AnonPro"; + src: url("../public/fonts/AnonymousPro-Regular.ttf") format("truetype"); + font-weight: normal; + } + :root { + --color-primary: 255, 255, 255; + } +} + +div::-webkit-scrollbar { + width: 2px; +} + +div::-webkit-scrollbar-thumb { + background: #45454579; +} + +div::-webkit-scrollbar-track { + background: transparent; +} + +div::-webkit-scrollbar-thumb:hover { + background: #525252; +} diff --git a/webapp/public/favicon.ico b/app/icon.ico similarity index 100% rename from webapp/public/favicon.ico rename to app/icon.ico diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 000000000..db6ce8007 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,26 @@ +import "./globals.css"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import { ThemeProvider } from "./providers"; +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "TTB", + description: "Text to Banger", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 000000000..7a06c35e3 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,7 @@ +import BangerModal from "@/components/BangerModal"; + +export const runtime = "edge"; + +export default function Page() { + return ; +} diff --git a/app/providers.tsx b/app/providers.tsx new file mode 100644 index 000000000..3d87ae262 --- /dev/null +++ b/app/providers.tsx @@ -0,0 +1,8 @@ +"use client"; +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import type { ThemeProviderProps } from "next-themes/dist/types"; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children}; +} diff --git a/components/BangerModal.tsx b/components/BangerModal.tsx new file mode 100644 index 000000000..9faa2b7c8 --- /dev/null +++ b/components/BangerModal.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useChat } from "ai/react"; +import Image from "next/image"; +import ThemeButton from "./ThemeButton"; + +export default function BangerModal() { + const { + messages, + input: tweetIdea, + handleInputChange, + handleSubmit, + isLoading, + } = useChat({ + api: "/api/banger", + initialMessages: [ + { + content: + "Sorry babe I can't listening how was your day ever again. I lose my edge context-switching to your toxic work environment", + role: "user", + id: "ISgO141", + }, + { + id: "XEAnGmj", + content: + "I just saw someone walking around with a sign that said 'I'm an undefined variable' and I couldn't help but think, same.", + role: "system", + }, + { + content: "I dont know mannn", + role: "user", + id: "TftCoI2", + }, + { + id: "dnE9T9I", + content: + "Just found out my ex was an alien the whole time. Talk about out of this world!", + role: "assistant", + }, + { + content: "thanks broter", + role: "user", + id: "JSK2xq1", + }, + { + id: "BgeGxWj", + content: + "Ducks are so weird. I mean, have you ever seen one wearing pants?", + role: "assistant", + }, + { + content: "hehehehehehehehehe", + role: "user", + id: "5KyZadz", + }, + { + id: "3WrgVR6", + content: + "My therapist just told me I have an unhealthy obsession with the Oxford comma. I think it's time to take a break from our sessions.", + role: "assistant", + }, + ], + }); + + return ( +
+ +
+
+

+ text-to-banger +

+
+
+
+