feat: add some ai stuff

This commit is contained in:
max_richter 2023-11-06 02:24:50 +01:00
parent dfd5e79246
commit acf086da13
23 changed files with 2022 additions and 54 deletions

View File

@ -29,7 +29,10 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.441.0",
"@sveltejs/adapter-node": "^1.3.1", "@sveltejs/adapter-node": "^1.3.1",
"minio": "^7.1.3",
"openai": "^4.15.4",
"svelte-particles": "^2.12.0", "svelte-particles": "^2.12.0",
"tsparticles": "^2.12.0", "tsparticles": "^2.12.0",
"tsparticles-confetti": "^2.12.0", "tsparticles-confetti": "^2.12.0",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
<script>
export let delay = 0;
</script>
<div style="--delay: {delay}ms">
<slot />
</div>
<style>
div {
opacity: 0;
animation: fadeIn 1s ease forwards;
animation-delay: var(--delay);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,64 @@
<script lang="ts">
import { persisted } from '$lib/helpers/localStore';
import { fade } from 'svelte/transition';
import TextSplit from './TextSplit.svelte';
let tempName = '';
let data = persisted('data', {
name: '',
adelsTitel: ''
});
let loadingAdelsTitel = false;
async function fetchAdelsTitel() {
if (loadingAdelsTitel) return;
loadingAdelsTitel = true;
const res = await fetch('https://adels-generator.herokuapp.com/');
const data = await res.json();
$data.adelsTitel = data.adelsTitel;
loadingAdelsTitel = false;
}
</script>
<section in:fade={{ delay: 2000 }}>
{#if $data.name}
<h3>Name: {$data.name}</h3>
<button
on:click={() => {
$data.name = '';
}}>name ändern</button
>
{:else}
<TextSplit content="Fantastisch, wie lautet euer Name?" />
<input type="text" bind:value={tempName} />
{#if tempName}
<button
on:click={() => {
$data.name = tempName;
tempName = '';
fetchAdelsTitel();
}}>name speichern</button
>
{/if}
{/if}
</section>
{#if $data.name}
<section in:fade />
{/if}
<style>
section {
color: white;
font-family: 'Parisienne', cursive;
max-width: 500px;
margin: 0 auto;
font-size: 2em;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
</style>

View File

@ -0,0 +1,44 @@
<script lang="ts">
export let content: string;
const words = content.split(' ');
export let delay = 0;
const delays: number[] = [];
let total = delay;
for (let i = 0; i < words.length; i++) {
const delay = total + words[i].length * 35;
total = delay;
delays.push(delay);
}
</script>
<div class="wrapper">
{#each words as word, i}
<span class="word" style="--delay:{2000 + delays[i]}ms">{word}</span>
{/each}
</div>
<style>
.wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
width: 100%;
}
.word {
opacity: 0;
margin: 0 0.1em;
animation: fadeIn 1s ease forwards;
animation-delay: var(--delay);
color: white;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>

View File

@ -13,7 +13,7 @@
position: relative; position: relative;
cursor: pointer; cursor: pointer;
font-family: 'Meow Script', cursive; font-family: 'Meow Script', cursive;
font-size: 2em; font-size: var(--font-size, 1em);
color: #ceba51; color: #ceba51;
background: transparent; background: transparent;
border-width: 30px; border-width: 30px;
@ -33,16 +33,16 @@
content: ''; content: '';
position: absolute; position: absolute;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
box-shadow: 0 0 70px #ffb11b88; box-shadow: 0 0 70px #ffb11b22;
top: -15px; top: -15px;
left: -15px; left: -15px;
z-index: -1; z-index: -1;
width: calc(100% + 30px); width: calc(100% + 30px);
height: calc(100% + 30px); height: calc(100% + 30px);
transition: box-shadow 0.3s ease; transition: box-shadow 0.8s ease;
} }
button:hover::after { button:hover::after {
box-shadow: 0 0 30px #ffb11b88; box-shadow: 0 0 40px #ffb11b55;
} }
</style> </style>

View File

@ -7,6 +7,7 @@
} }
let i = 0; let i = 0;
let confettiCanvas: HTMLCanvasElement;
let f: number; let f: number;
onDestroy(() => { onDestroy(() => {
@ -17,11 +18,15 @@
onMount(async () => { onMount(async () => {
const confetti = (await import('https://esm.run/canvas-confetti')).default; const confetti = (await import('https://esm.run/canvas-confetti')).default;
confettiCanvas.width = window.innerWidth / 2;
confettiCanvas.height = window.innerHeight / 2;
const update = confetti.create(confettiCanvas);
function frame() { function frame() {
i = (i + 1) % 1000; i = (i + 1) % 1000;
if (i % 20 === 0) { if (i % 10 === 0) {
confetti({ update({
particleCount: 1, particleCount: 1,
startVelocity: 0, startVelocity: 0,
useWorker: true, useWorker: true,
@ -33,7 +38,7 @@
colors: ['#d9c556'], colors: ['#d9c556'],
shapes: ['circle'], shapes: ['circle'],
gravity: randomInRange(0.4, 0.6), gravity: randomInRange(0.4, 0.6),
scalar: randomInRange(0.4, 1), scalar: randomInRange(0.1, 0.5),
drift: randomInRange(-2, 2), drift: randomInRange(-2, 2),
disableForReducedMotion: true disableForReducedMotion: true
}); });
@ -44,3 +49,15 @@
frame(); frame();
}); });
</script> </script>
<canvas style="height: 100vh; width: 100vw" bind:this={confettiCanvas} />
<style>
canvas {
position: absolute;
top: 0px;
left: 0px;
pointer-events: none;
user-select: none;
}
</style>

View File

@ -40,6 +40,8 @@
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
pointer-events: none;
user-select: none;
} }
.drapery { .drapery {

View File

@ -22,6 +22,8 @@
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
height: 50%;
max-height: 50vh;
} }
.wrapper > div { .wrapper > div {
@ -33,6 +35,8 @@
max-width: 100%; max-width: 100%;
will-change: contents; will-change: contents;
transform: translateZ(1px); transform: translateZ(1px);
height: 100%;
width: auto;
} }
.wrapper > div :global(path) { .wrapper > div :global(path) {

View File

@ -1,29 +1,29 @@
<script lang="ts"> <script lang="ts">
import maskenball from './maskenball.svg?raw';
</script> </script>
<div> <p>Willkommen zum Maskenball</p>
{@html maskenball}
</div>
<style> <style>
@import './maskenball.css'; p {
font-family: Parisienne;
div { color: #d9c556;
margin-top: 10px; text-align: center;
position: relative; font-size: 4em;
display: flex; margin-top: 0em;
z-index: 5; animation: fadeIn 5s ease forwards;
align-items: center; animation-delay: 3s;
justify-content: center; opacity: 0;
filter: drop-shadow(0px 0px 40px #be8630aa) drop-shadow(0px 0px 5px black) transition: font-size 3s ease;
drop-shadow(0px 0px 5px black) drop-shadow(0px 0px 5px black);
} }
div :global(path) { @keyframes fadeIn {
animation-duration: 5s !important; from {
animation-delay: 3s !important; opacity: 0;
animation-fill-mode: forwards !important; transform: translateY(-5%) scale(0.8);
stroke: #d9c556; }
to {
opacity: 1;
transform: translateY(0%) scale(1);
}
} }
</style> </style>

View File

@ -0,0 +1,22 @@
import OpenAI from 'openai';
import { OPENAI_API_KEY } from '$env/static/private';
const openai = new OpenAI({
apiKey: OPENAI_API_KEY,
});
export async function chat(prompt: string) {
const chatCompletion = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{
"role": "system",
"content": prompt,
},
],
});
const res = chatCompletion.choices[0].message.content;
return res;
}

View File

@ -0,0 +1,83 @@
import { writable as internal, type Writable } from 'svelte/store'
declare type Updater<T> = (value: T) => T;
declare type StoreDict<T> = { [key: string]: Writable<T> }
/* eslint-disable @typescript-eslint/no-explicit-any */
interface Stores {
local: StoreDict<any>,
session: StoreDict<any>,
}
const stores: Stores = {
local: {},
session: {}
}
export interface Serializer<T> {
parse(text: string): T
stringify(object: T): string
}
export type StorageType = 'local' | 'session'
export interface Options<T> {
serializer?: Serializer<T>
storage?: StorageType
}
function getStorage(type: StorageType) {
return type === 'local' ? localStorage : sessionStorage
}
export function persisted<T>(key: string, initialValue: T, options?: Options<T>): Writable<T> {
const serializer = options?.serializer ?? JSON
const storageType = options?.storage ?? 'local'
const browser = typeof (window) !== 'undefined' && typeof (document) !== 'undefined'
const storage = browser ? getStorage(storageType) : null
function updateStorage(key: string, value: T) {
storage?.setItem(key, serializer.stringify(value))
}
if (!stores[storageType][key]) {
const store = internal(initialValue, (set) => {
const json = storage?.getItem(key)
if (json) {
set(<T>serializer.parse(json))
}
if (browser && storageType == 'local') {
const handleStorage = (event: StorageEvent) => {
if (event.key === key)
set(event.newValue ? serializer.parse(event.newValue) : null)
}
window.addEventListener("storage", handleStorage)
return () => window.removeEventListener("storage", handleStorage)
}
})
const { subscribe, set } = store
stores[storageType][key] = {
set(value: T) {
updateStorage(key, value)
set(value)
},
update(callback: Updater<T>) {
return store.update((last) => {
const value = callback(last)
updateStorage(key, value)
return value
})
},
subscribe
}
}
return stores[storageType][key]
}

20
src/lib/helpers/s3.ts Normal file
View File

@ -0,0 +1,20 @@
import Minio from 'minio'
import { S3_ENDPOINT_URL, S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_ACCESS_KEY } from "$env/static/private"
const minioClient = new Minio.Client({
endPoint: S3_ENDPOINT_URL,
port: 80,
useSSL: false,
accessKey: S3_ACCESS_KEY,
secretKey: S3_SECRET_ACCESS_KEY,
})
export function putObject(fileName: string, content: Buffer, metadata: Minio.ItemBucketMetadata = {}) {
return minioClient.putObject(S3_BUCKET_NAME, fileName, content, metadata);
}
export function listBuckets() {
return minioClient.listBuckets();
}

View File

@ -0,0 +1,48 @@
import { DREAM_API_KEY } from "$env/static/private";
const path =
"https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image";
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: DREAM_API_KEY,
};
export async function generateImage(prompt: string, negativePrompt: string) {
const body = {
steps: 10,
width: 832,
height: 1216,
seed: Math.floor(Math.random() * 100000),
cfg_scale: 5,
samples: 1,
style_preset: "fantasy-art",
text_prompts: [
{
"text": prompt,
"weight": 1
},
{
"text": negativePrompt,
"weight": -1
}
],
};
const response = await fetch(
path,
{
headers,
method: "POST",
body: JSON.stringify(body),
}
);
if (!response.ok) {
throw new Error(`Non-200 response: ${await response.text()}`)
}
const responseJSON = await response.json();
return responseJSON.artifacts[0];
}

View File

@ -1,16 +1,5 @@
<script lang="ts"> <script lang="ts">
import './global.css';
</script> </script>
<slot /> <slot />
<style global>
:global(html, body) {
height: 100%;
margin: 0;
padding: 0;
background-color: black;
/* background-image: url(/confetti.png); */
background-size: 80%;
backdrop-filter: brightness(0.5);
}
</style>

View File

@ -1,25 +1,36 @@
<script lang="ts"> <script lang="ts">
import FadeIn from '$lib/components/FadeIn.svelte';
import Questions from '$lib/components/Questions.svelte';
import TextSplit from '$lib/components/TextSplit.svelte';
import Button from '$lib/components/button.svelte'; import Button from '$lib/components/button.svelte';
import Confetti from '$lib/components/confetti.svelte'; import Confetti from '$lib/components/confetti.svelte';
import Curtains from '$lib/components/curtains.svelte'; import Curtains from '$lib/components/curtains.svelte';
import Mask from '$lib/components/mask.svelte'; import Mask from '$lib/components/mask.svelte';
import Maskenball from '$lib/components/maskenball.svelte'; import Maskenball from '$lib/components/maskenball.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
let curtainsVisible = false; let curtainsVisible = false;
let buttonVisible = false; let buttonVisible = false;
let maskVisible = false; let maskVisible = false;
let maskSmall = false;
let contentVisible = false;
let questionVisible = false;
onMount(() => { onMount(() => {
curtainsVisible = true; // curtainsVisible = true;
curtainsVisible = false;
maskVisible = true;
maskSmall = true;
questionVisible = true;
setTimeout(() => { setTimeout(() => {
buttonVisible = true; // buttonVisible = true;
}, 1500); }, 1500);
}); });
</script> </script>
<Confetti /> <Confetti />
<Curtains visible={curtainsVisible} /> <Curtains visible={curtainsVisible} />
<div class="center"> <div class="center" class:maskSmall>
{#if maskVisible} {#if maskVisible}
<Mask /> <Mask />
<Maskenball /> <Maskenball />
@ -28,28 +39,75 @@
{#if buttonVisible} {#if buttonVisible}
<span class="enter-button" class:visible={buttonVisible}> <span class="enter-button" class:visible={buttonVisible}>
<Button <Button
--font-size="2em"
on:click={() => { on:click={() => {
curtainsVisible = false; curtainsVisible = false;
buttonVisible = false; buttonVisible = false;
setTimeout(() => { setTimeout(() => {
maskVisible = true; maskVisible = true;
setTimeout(() => {
maskSmall = true;
contentVisible = true;
}, 7000);
}, 1000); }, 1000);
}}>Enter the Dungeon</Button }}>Enter the Dungeon</Button
> >
</span> </span>
{/if} {/if}
>
</div> </div>
{#if contentVisible}
<div class="einladung" out:fade>
<TextSplit
content="Wir laden dich herzlich ein, an unserer exklusiven Silvesterparty teilzunehmen, die dieses Jahr im magischen Ambiente eines Maskenballs stattfindet. Tauche ein in eine Nacht voller Geheimnisse, Eleganz und festlichem Glanz."
/>
<span in:fade={{ delay: 8000, duration: 1000 }}>
<Button
on:click={() => {
contentVisible = false;
questionVisible = true;
}}>Einladung annehmen</Button
>
</span>
</div>
{/if}
{#if questionVisible}
<Questions />
{/if}
<style> <style>
.center { .center {
display: grid; display: flex;
position: absolute; flex-direction: column;
position: relative;
align-items: center;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
top: 0px; top: 0px;
transform: translateY(-5%);
place-content: center; place-content: center;
transition: height 3s ease;
}
.center.maskSmall {
height: 30vh;
}
:global(.center.maskSmall > p) {
font-size: 2em;
}
.einladung {
font-family: Parisienne;
font-size: 2em;
max-width: 500px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 50px;
} }
.enter-button { .enter-button {
@ -68,9 +126,4 @@
opacity: 1; opacity: 1;
} }
} }
:global(canvas) {
position: relative !important;
z-index: -1 !important;
}
</style> </style>

View File

@ -0,0 +1,27 @@
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { putObject } from "$lib/helpers/s3";
import { generateImage } from "$lib/helpers/stability";
export const GET: RequestHandler = async ({ params }) => {
const inputName = params.name;
const prompt = `realistic profile portrait oil painting of a masked ${inputName}, baroque, Charles Vess, masked ball attire, Charles Vess, opulence, mystery, elegance, medium-length blond hair, darker skin`;
const negativePrompt = "blurry, multiple persons, picture frame"
const a = performance.now()
const image = await generateImage(prompt, negativePrompt);
const duration = performance.now() - a;
console.log({ duration })
const imageName = `${image.seed}-${inputName.toLowerCase().split(" ").slice(0, 5).join("-").slice(0, 25)}.png`
const res = await putObject(imageName, Buffer.from(image.base64, 'base64'), { "Content-Type": "image/png" });
return json({
...res,
url: `https://s3.app.max-richter.dev/silvester23/${imageName}`
})
}

View File

@ -0,0 +1,25 @@
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { putObject } from "$lib/helpers/s3";
export const GET: RequestHandler = async ({ params }) => {
const inputName = params.name;
const prompt = `upper body realistic portrait oil painting of a masked ${inputName}, Baroque, Charles Vess, masked ball attire, Charles Vess, opulence, mystery, elegance, dark long hair`;
const negativePrompt = "blurry, multiple persons, picture frame";
const image = generateImage(prompt, negativePrompt);
const imageName = `txt2img_${image.seed}.png`;
const res = await putObject(imageName, Buffer.from(image.base64, 'base64'), { contentType: "image/png" });
return json({
...res,
url: `https://s3.app.max-richter.dev/silvester23/${imageName}`
});
};

View File

@ -0,0 +1,14 @@
import { type RequestHandler } from "@sveltejs/kit";
import { chat } from "$lib/helpers/chatgpt";
export const GET: RequestHandler = async function ({ params }) {
const inputName = params.name
const prompt = `Generate 10 variants of the name ${inputName}. The names should sound very much like the original but also like noble names from the 1900 century. Examples could be "lady rosalind of whitmore" "lord byron of castlemore" "Lord Max Richter". Only respond with 10 names seperated be newlines`;
const res = await chat(prompt);
return new Response(res);
}

View File

@ -0,0 +1,8 @@
import { getBuckets } from "$lib/helpers/s3";
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async () => {
const buckets = await getBuckets()
return json(buckets)
}

19
src/routes/global.css Normal file
View File

@ -0,0 +1,19 @@
/* parisienne-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Parisienne';
font-style: normal;
font-weight: 400;
src: url('/parisienne-v13-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
html, body {
height: 100%;
margin: 0;
padding: 0;
background-color: black;
/* background-image: url(/confetti.png); */
background-size: 80%;
backdrop-filter: brightness(0.5);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.