Compare commits

..

No commits in common. "main" and "svelte-5-upgrade" have entirely different histories.

29 changed files with 560 additions and 2764 deletions

2
.gitignore vendored
View File

@ -12,5 +12,3 @@ vite.config.ts.timestamp-*
.vercel
vectorstore
ecosystem.config.js
.ai-robots-cache.json
robots.txt

View File

@ -1,6 +1,6 @@
# Playground
Portfolio site of silentsilas
Portfolio site of silentsilas.
## Developing

2546
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,13 @@
"version": "0.0.1",
"private": true,
"engines": {
"node": "^22"
"node": ">=22.13.1"
},
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"deploy": "bash scripts/deploy.sh",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
@ -16,11 +17,9 @@
"format": "prettier --write .",
"model-pipeline:run": "node scripts/model-pipeline.js",
"generate-embeddings": "node scripts/generate-embeddings.js",
"finetune": "node scripts/finetune.js",
"start": "PORT=8080 node build"
"finetune": "node scripts/finetune.js"
},
"devDependencies": {
"@silentsilas/vite-plugin-ai-robots": "^1.0.1",
"@sveltejs/adapter-auto": "^3.2.4",
"@sveltejs/adapter-node": "^5.2.2",
"@sveltejs/kit": "^2.16.1",
@ -50,7 +49,6 @@
"type": "module",
"dependencies": {
"@dimforge/rapier3d-compat": "^0.14.0",
"@friendofsvelte/tipex": "^0.0.7-fix-0",
"@langchain/anthropic": "^0.3.1",
"@langchain/community": "^0.3.1",
"@langchain/core": "^0.3.32",

22
scripts/deploy.sh Normal file
View File

@ -0,0 +1,22 @@
#!/bin/bash
# Pull the latest changes
git pull
# Build the project
npm run build
# Copy necessary files
cp package.json ./build/
cp package-lock.json ./build/
# Create or update the .env file
# echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> ./build/.env
#echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> ./build/.env
# Navigate to build directory and install dependencies
cd build
npm install
# Restart the application
pm2 restart ecosystem.config.cjs

View File

@ -1,6 +1,5 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="manifest" href="/manifest.json">
@ -10,12 +9,9 @@
<meta property="og:url" content="https://silentsilas.com">
<meta property="og:image" content="https://silentsilas.com/social.png">
<meta property="og:description" content="Personal site of silentsilas.">
<script src="https://darkvisitors.com/tracker.js?project_key=e275359a-67b4-4862-b800-c6a2878446a6"></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" data-theme="forest" style="overflow-x: hidden;">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -1,29 +1,7 @@
import { getModel } from '$lib/utils/search';
import { building } from '$app/environment';
import type { Handle, RequestEvent } from '@sveltejs/kit';
import type { Handle } from '@sveltejs/kit';
import { v4 as uuidv4 } from 'uuid';
import { DARK_VISITORS_TOKEN } from '$env/static/private';
async function sendAnalytics(event: RequestEvent) {
try {
const headers = Object.fromEntries(event.request.headers.entries());
void fetch('https://api.darkvisitors.com/visits', {
method: 'POST',
headers: {
Authorization: `Bearer ${DARK_VISITORS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
request_path: event.url.pathname,
request_method: event.request.method,
request_headers: headers
})
});
} catch (e) {
// Silent fail
}
}
if (!building) {
getModel().catch((error) => {
@ -40,19 +18,10 @@ export const handle: Handle = async ({ event, resolve }) => {
// If no session exists, create a new one
if (!sessionId) {
sessionId = uuidv4();
event.cookies.set('sessionId', sessionId, {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7
}); // 1 week
event.cookies.set('sessionId', sessionId, { path: '/', httpOnly: true, sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 }); // 1 week
}
// Add sessionId to locals for easy access in routes
event.locals = { ...event.locals, sessionId };
if (import.meta.env.PROD) {
sendAnalytics(event);
}
return resolve(event);
};

View File

@ -71,27 +71,8 @@
</script>
<div class="container mx-auto flex flex-col items-center">
<div class="prose flex flex-row py-6 items-center">
<h1 class="mb-0">{title}</h1>
<a
aria-label="Link to poetry RSS feed."
class="link-primary mt-2 ml-2"
href={`${baseUrl}/rss`}
rel="external"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
d="M3.75 4.5a.75.75 0 0 1 .75-.75h.75c8.284 0 15 6.716 15 15v.75a.75.75 0 0 1-.75.75h-.75a.75.75 0 0 1-.75-.75v-.75C18 11.708 12.292 6 5.25 6H4.5a.75.75 0 0 1-.75-.75V4.5Zm0 6.75a.75.75 0 0 1 .75-.75h.75a8.25 8.25 0 0 1 8.25 8.25v.75a.75.75 0 0 1-.75.75H12a.75.75 0 0 1-.75-.75v-.75a6 6 0 0 0-6-6H4.5a.75.75 0 0 1-.75-.75v-.75Zm0 7.5a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z"
clip-rule="evenodd"
/>
</svg>
</a>
<div class="prose mb-4">
<h1 class="py-6">{title}</h1>
</div>
<ul>

View File

@ -1,239 +0,0 @@
<script lang="ts">
import { Tipex, type TipexEditor } from '@friendofsvelte/tipex';
import '@friendofsvelte/tipex/styles/Tipex.css';
import '@friendofsvelte/tipex/styles/ProseMirror.css';
import '@friendofsvelte/tipex/styles/Controls.css';
import '@friendofsvelte/tipex/styles/EditLink.css';
import '@friendofsvelte/tipex/styles/CodeBlock.css';
import Utilities from './Utilities.svelte';
import SuggestionList from './SuggestionList.svelte';
import { datamuseApi, type Suggestion } from '$lib/services/datamuse';
import { calculateCursorPosition } from '$lib/utils/cursor';
import { createDebounce } from '$lib/utils/debounce';
import { extractCurrentWord, extractSelectedWord } from '$lib/utils/text';
let { initialContent } = $props();
let editor = $state<TipexEditor>();
let body = $state(localStorage.getItem('tipex') || initialContent || '');
let suggestions = $state<Suggestion[]>([]);
let rhymes = $state<Suggestion[]>([]);
let currentWord = $state('');
let selectedIndex = $state(-1);
let cursorPosition = $state({ x: 0, y: 0 });
let isMobile = $state(false);
const debounce = createDebounce();
// Check for mobile on component mount and resize
function checkMobile() {
isMobile = window.innerWidth < 768;
}
$effect(() => {
// Add resize listener for responsive behavior
window.addEventListener('resize', checkMobile);
checkMobile();
// Cleanup on unmount
return () => {
window.removeEventListener('resize', checkMobile);
};
});
$effect(() => {
if (editor?.getHTML()) {
localStorage.setItem('tipex', editor.getHTML());
}
});
function updateCursorPosition() {
cursorPosition = calculateCursorPosition(editor);
}
const handleUpdate = debounce(async () => {
const { state } = editor?.view || {};
if (!state) return;
const { doc, selection } = state;
const word = extractCurrentWord(doc, selection.from);
if (word && word !== currentWord) {
currentWord = word;
suggestions = await datamuseApi.getSuggestions(word);
selectedIndex = -1;
} else if (!word) {
suggestions = [];
currentWord = '';
}
updateCursorPosition();
}, 1000);
const handleSelectionChange = debounce(async () => {
const selection = editor?.view.state.selection;
if (editor && selection && !selection.empty) {
const word = extractSelectedWord(editor.view.state.doc, selection.from, selection.to);
if (word) {
rhymes = await datamuseApi.getRhymes(word);
selectedIndex = -1;
updateCursorPosition();
}
}
}, 300);
// Special handler for touch events
function handleTouchEnd(event: TouchEvent) {
// Prevent default only if we have suggestions
if (suggestions.length > 0 || rhymes.length > 0) {
event.preventDefault();
}
handleSelectionChange();
}
function clearSuggestions() {
suggestions = [];
rhymes = [];
currentWord = '';
selectedIndex = -1;
}
function applySuggestion(word: string) {
if (!editor?.view) return;
const view = editor.view;
const { state } = view;
const { schema, selection, tr } = state;
view.focus();
if (rhymes.length > 0 && !selection.empty) {
view.dispatch(tr.replaceSelectionWith(schema.text(word)));
rhymes = [];
} else {
const pos = selection.from;
const wordBefore = state.doc.textBetween(Math.max(0, pos - 100), pos);
const match = wordBefore.match(/\S+$/);
if (match) {
const wordStart = pos - match[0].length;
view.dispatch(tr.replaceWith(wordStart, pos, schema.text(word)));
}
}
clearSuggestions();
view.focus();
}
function handleKeydown(event: KeyboardEvent) {
const activeSuggestions = suggestions.length ? suggestions : rhymes;
if (!activeSuggestions.length) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, activeSuggestions.length - 1);
break;
case 'ArrowUp':
event.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
break;
case 'Enter':
event.preventDefault();
if (selectedIndex >= 0) {
applySuggestion(activeSuggestions[selectedIndex].word);
}
break;
case 'Escape':
event.preventDefault();
clearSuggestions();
break;
}
}
</script>
<svelte:window onselectionchange={handleSelectionChange} onkeydown={handleKeydown} />
<div
class="container mx-auto my-8 dark relative px-4"
onmouseup={handleSelectionChange}
ontouchend={handleTouchEnd}
onmousedown={clearSuggestions}
ontouchstart={clearSuggestions}
role="textbox"
aria-label="Text editor container"
tabindex="0"
>
<Tipex {body} controls !focal class="h-[80vh]" bind:tipex={editor}>
{#snippet utilities(editor)}
<Utilities {editor} />
{/snippet}
</Tipex>
{#if suggestions.length > 0 || rhymes.length > 0}
<div
class="suggestions-container {isMobile ? 'mobile-suggestions' : ''}"
role="listbox"
aria-label="Suggestions and rhymes"
style:left="{cursorPosition.x}px"
style:top="{cursorPosition.y}px"
>
{#if suggestions.length > 0}
<SuggestionList
title="Suggestions"
{suggestions}
{selectedIndex}
onSelect={applySuggestion}
/>
{/if}
{#if rhymes.length > 0}
<SuggestionList
title="Rhymes"
suggestions={rhymes}
{selectedIndex}
onSelect={applySuggestion}
/>
{/if}
</div>
{/if}
</div>
<style>
.suggestions-container {
position: fixed;
z-index: 1000;
max-width: min(300px, 90vw);
max-height: 300px;
overflow-y: auto;
transform: translateX(-50%);
overscroll-behavior: contain;
scroll-behavior: smooth;
box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
touch-action: manipulation;
background-color: white;
border-radius: 0.375rem;
}
/* Mobile-specific styles */
.mobile-suggestions {
max-width: 90vw;
width: 90vw;
max-height: 200px;
left: 50% !important; /* Override inline styles */
bottom: 20px !important;
top: auto !important;
position: fixed;
}
@media (max-width: 768px) {
.suggestions-container {
position: fixed;
bottom: 20px;
left: 50% !important;
transform: translateX(-50%);
top: auto !important;
width: 90vw;
}
}
</style>

View File

@ -1,51 +0,0 @@
<script lang="ts">
type Suggestion = {
word: string;
score: number;
};
let { title, suggestions, selectedIndex, onSelect } = $props<{
title: string;
suggestions: Suggestion[];
selectedIndex: number;
onSelect: (word: string) => void;
}>();
const isMobile = window.innerWidth < 768;
function handleTouchSelect(event: TouchEvent, word: string) {
event.preventDefault();
onSelect(word);
}
</script>
<div class="bg-white dark:bg-gray-800 shadow-lg rounded-md p-2 mb-2">
<h3 class="text-sm font-bold mb-2 text-gray-700 dark:text-gray-300">{title}</h3>
{#each suggestions.slice(0, 10) as { word }, i}
<button
type="button"
class="block w-full text-left px-2 {isMobile
? 'py-3'
: 'py-1'} hover:bg-gray-100 dark:hover:bg-gray-700 rounded
{selectedIndex === i ? 'bg-gray-100 dark:bg-gray-700' : ''}"
onmousedown={() => onSelect(word)}
ontouchstart={(e) => handleTouchSelect(e, word)}
role="option"
aria-selected={selectedIndex === i}
style:touch-action="manipulation"
>
{word}
</button>
{/each}
</div>
<style>
/* Mobile-specific styles */
@media (max-width: 768px) {
button {
padding: 12px 8px;
margin-bottom: 4px;
font-size: 16px; /* Minimum readable size on mobile */
}
}
</style>

View File

@ -1,52 +0,0 @@
<script lang="ts">
import type { TipexEditor } from '@friendofsvelte/tipex';
interface UtilitiesProps {
editor: TipexEditor;
}
let { editor }: UtilitiesProps = $props();
const copyHTML = () => {
const html = editor?.getHTML();
if (!html) return;
navigator.clipboard.writeText(html);
alert('HTML copied to clipboard!');
};
</script>
<div class="tipex-utilities">
<button
class="tipex-edit-button tipex-button-extra tipex-button-rigid"
type="button"
aria-label="Copy HTML"
onclick={copyHTML}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
width="0.7em"
height="0.7em"
class="h-4 w-4"
>
<path
fill="currentColor"
d="M208 0h124.1C344.8 0 357 5.1 366 14.1L433.9 82c9 9 14.1 21.2 14.1 33.9V336c0 26.5-21.5 48-48 48H208c-26.5 0-48-21.5-48-48V48c0-26.5 21.5-48 48-48M48 128h80v64H64v256h192v-32h64v48c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V176c0-26.5 21.5-48 48-48"
></path></svg
></button
>
<button class="tipex-edit-button tipex-button-extra tipex-button-rigid" aria-label="Edit link"
><svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
width="0.7em"
height="0.7em"
class="h-4 w-4"
><path
fill="currentColor"
d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l112.2-112.3c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0z"
></path></svg
>
</button>
</div>

View File

@ -13,7 +13,15 @@
</script>
<div class="canvas flex flex-1" oncontextmenu={preventRightClick} role="application">
<Canvas>
<Canvas
createRenderer={(canvas) => {
return new WebGPURenderer({
canvas,
antialias: true,
forceWebGL: false
});
}}
>
{@render children?.()}
</Canvas>
</div>

View File

@ -1,26 +0,0 @@
export type Suggestion = {
word: string;
score: number;
};
export const datamuseApi = {
async getSuggestions(word: string): Promise<Suggestion[]> {
if (!word || word.length < 2) return [];
return fetch(`https://api.datamuse.com/sug?s=${encodeURIComponent(word)}`)
.then((res) => (res.ok ? res.json() : []))
.catch((error) => {
console.error('Error fetching suggestions:', error);
return [];
});
},
async getRhymes(word: string): Promise<Suggestion[]> {
if (!word) return [];
return fetch(`https://api.datamuse.com/words?rel_rhy=${encodeURIComponent(word)}`)
.then((res) => (res.ok ? res.json() : []))
.catch((error) => {
console.error('Error fetching rhymes:', error);
return [];
});
}
};

View File

@ -1,31 +0,0 @@
import type { TipexEditor } from '@friendofsvelte/tipex';
export function calculateCursorPosition(editor: TipexEditor | undefined) {
const selection = window.getSelection();
if (!selection?.rangeCount) return { x: 0, y: 0 };
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const editorRect = editor?.view.dom.getBoundingClientRect() || { left: 0, top: 0 };
// Check if on mobile
const isMobile = window.innerWidth < 768;
if (isMobile) {
// For mobile, position suggestions at the bottom center of the screen
// to avoid issues with virtual keyboard and to make them more accessible
return {
x: window.innerWidth / 2,
y: Math.min(rect.bottom - editorRect.top + window.scrollY, window.innerHeight - 250)
};
}
// Desktop positioning
const x = Math.min(
Math.max(rect.left - editorRect.left + window.scrollX, 100),
window.innerWidth - 300
);
const y = rect.bottom - editorRect.top + window.scrollY + 10;
return { x, y };
}

View File

@ -1,14 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export function createDebounce() {
let timer: NodeJS.Timeout | null = null;
return function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
return (...args: Parameters<T>) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,52 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Extracts the last word (only alphabetical characters) from a given text
*/
function extractLastWord(text: string): string {
const matches = text.match(/[a-zA-Z]+\s*$/);
return matches ? matches[0].trim() : '';
}
/**
* Extracts the current word at cursor position from the document
* Improved to handle different selection scenarios on mobile
*/
export function extractCurrentWord(doc: any, pos: number): string {
// For mobile, we might need to look further back to find words
// since touch selection can be less precise
const lookbackDistance = window.innerWidth < 768 ? 200 : 100;
const textBefore = doc.textBetween(Math.max(0, pos - lookbackDistance), pos);
// First try with standard pattern
const word = extractLastWord(textBefore);
// If standard pattern fails, try a more lenient pattern for mobile
if (!word && window.innerWidth < 768) {
const lenientMatches = textBefore.match(/[a-zA-Z]+(?:\s+[a-zA-Z]+)*\s*$/);
return lenientMatches ? lenientMatches[0].trim().split(/\s+/).pop() || '' : '';
}
return word;
}
/**
* Extracts the last word from a selected text range
* Improved to handle imprecise selections on mobile
*/
export function extractSelectedWord(doc: any, from: number, to: number): string {
// On mobile, selections might include extra spaces
const isMobile = window.innerWidth < 768;
const selectedText = doc.textBetween(from, to, ' ');
if (isMobile && selectedText.trim()) {
// For mobile, take the largest word in the selection
const words = selectedText.trim().split(/\s+/);
if (words.length > 0) {
// Find the longest word in selection (mobile users often select multiple words)
return words.reduce((longest: string, current: string) =>
current.length > longest.length ? current : longest, '');
}
}
return extractLastWord(selectedText);
}

View File

@ -1,26 +0,0 @@
---
categories:
- Poetry
date: 2025-04-05 12:00:00 +0000
tags:
- Introspective
title: Aimless
layout: poetry
---
In an act of self defense
I grew accustomed to solo living.
I held on to my two cents
Unsure of when it should be given.
I thought this to be the fatal flaw
That kept me from living right.
But now I see that aimlessness
Is what kills all men at night.
Living without a purpose,
Yet a desire to provide.
You know what youd like to do,
But what do you do with your time?

View File

@ -1,39 +0,0 @@
---
categories:
- Poetry
date: 2025-04-13 12:00:00 +0000
tags:
- Introspective
title: To My Cat
layout: poetry
---
<img src="/imgs/sekai.jpg" width="400" />
All things must pass,
But your exit was unfair.
I still keep my water glass
Out of reach of your stare.
All things must pass,
But Im stuck in this nightmare.
I knew our time wouldnt last,
Yet your presence is everywhere.
Daylight will find its way,
But your passing clouds my vision.
What good is my well-being
If youre not there for it envisioned?
Id never complain about my black clothes
Covered in your hair
If it means when I lint roll
It signifies that youre still there.
But no,
This breaks all the rules.
I still keep my door ajar.
I know you hate it when its closed,
But I dont know where you are.
— RIP Sekai, 4.10.25 3:37PM EDT

View File

@ -1,59 +0,0 @@
---
categories:
- blog
date: 2025-03-09 13:00:00 +0000
tags:
- AI
- Tutorial
title: AI Character Tutorial Part 1
year: 2025
layout: thoughts
draft: true
---
It's March of 2025, and a lot of new advancements have been made in the generative art space. I was also lucky to have snagged a 3090 TI back when they first came out, and its 24GB VRAM allows you to do pretty much anything with diffusion models on your own machine. As for the LLM space, you'll still need to shell out 100GB+ VRAM to get something close to Claude and OpenAI's frontier models to run locally.
And so I'd like to catalogue my experience trying out various AI art techniques. The first tutorial will be creating your own "AI character" that you can have appear in any prompt you enter. The easiest method to accomplish this is with <a href="https://github.com/VectorSpaceLab/OmniGen?tab=readme-ov-file" target="_blank" class="link-primary">OmniGen</a>. You may be able to run it with as little as 8GB VRAM, but you'll need to mess with a <a href="https://github.com/VectorSpaceLab/OmniGen/blob/main/docs/inference.md#requiremented-resources" target="_blank" class="link-primary">few settings</a>.
You could follow their instructions to get their demo running locally, but to be able to make more complicated workflows that automate various common tasks, we'll be setting up a UI to interface with various diffusion models, LoRA's, high-res fixes, and what-have-you. There are a handful of open-source UI frontends for this like <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui" target="_blank" class="link-primary">Stable Diffusion WebUI</a>, <a href="https://github.com/invoke-ai/InvokeAI" target="_blank" class="link-primary">InvokeAI</a>, and even a <a href="https://github.com/LykosAI/StabilityMatrix" target="_blank" class="link-primary">UI that helps you manage and install all the other UI's</a>. We'll be using ComfyUI, as I've been following a Youtube channel called <a href="https://www.youtube.com/@NerdyRodent" target="_blank" class="link-primary">Nerdy Rodent</a> who provides excellent ComfyUI tutorials for cutting edge AI workflows, and provides these workflows for you to import if you subscribe to their Patreon (which I think is well worth the $5/mo!). I could direct you to their channel and call it a day, but there are a few things he doesn't address that I think is crucial if you plan to seriously experiment with AI art.
### Hardware
- Framework 16 AMD Ryzen™ 9 7940HS Laptop
- 64GB RAM
- Its hybrid GPU has 2GB VRAM but we won't be using it
- Nvidia 3090 Ti in an eGPU enclosure
- 24GB VRAM
These instructions will be for Linux and assume you're comfortable with the terminal, specifically Bash on Fedora 41. MacOS may work as well with a few adjustments. As for Windows, I recommend deleting your Windows partition. It will also be Nvidia specific, but hopefully when the Framework Desktop drops with their 128GB VRAM AMD chips I can figure this out with ROCm or tooling that runs CUDA on AMD. Don't worry if none of that makes sense, as it likely doesn't apply to you.
### Setup or Python Sucks
If you already have Python projects, or plan to install other Python projects, I highly recommend setting up `asdf`. It's a tool that allows you to manage multiple versions of software on your machine. It's particularly useful for managing Python environments, as it allows you to easily switch between different versions of Python and packages. Combine this with `venv` and you can be sure that the next time you try to get an old project running that it will Just Work™.
You'll first go through `asdf`'s <a href="https://asdf-vm.com/guide/getting-started.html" target="_blank" class="link-primary">installation guide</a>. When I first started using `asdf`, their git installation was the recommended method. But now they recommend using a package manager, pointing to homebrew and AUR. Since Fedora has neither of these, you could set up <a href="https://docs.brew.sh/Homebrew-on-Linux" target="_blank" class="link-primary">linuxbrew</a>, but I'm still using the git version without issue.
After you've configured your terminal to support `asdf`, you can add a `.tool-versions` file to the root of any python project you come across and put in `python 3.11.4` for `asdf install` to always install and use that specific version with your python commands.
First `git clone https://github.com/comfyanonymous/ComfyUI` in wherever you keep your projects. then `cd ComfyUI` and `touch .tool-versions`. Open that file and enter `python 3.12.9`, which is their recommended version today. Check the repo if that has changed since I've written this. Then run `asdf install`, and you should have the proper python version installed on your system.
But before we start installing dependencies, we'll want to encapsulate them in their own environment so other projects using `python 3.12.9` don't install conflicting packages<a href="#ref1" class="link-primary">[1]</a>. At the root of the project, run `python -m venv ./venv` to create a virtual environment in a folder called `venv`. Now run `source ./venv/bin/activate` to enter this virtual environment to install python packages in it. Continue following <a href="https://github.com/comfyanonymous/ComfyUI?tab=readme-ov-file#manual-install-windows-linux" target="_blank" class="link-primary">the instructions</a> in ComfyUI to install all of its dependencies for both your GPU and ComfyUI in general.
After all of that, you can now run `python main.py` to start ComfyUI and access it at `localhost:8188` in your browser (assuming you didn't change the default port).
At this point it might be worth creating an alias to start the project properly. If you don't have one already, create a `~/.aliases` file (or wherever you wish to manage them) and add `source ~/.aliases` to your `.bashrc`. Then in `~/.aliases`, add the line `alias comfy='cd ~/Projects/ComfyUI && asdf install && source ./venv/bin/activate && python main.py'`, swapping `~/Projects/ComfyUI` with where you cloned the project. Source `.bashrc` or restart your terminal, and you should be able to start up ComfyUI from anywhere by running `comfy` in the terminal.
### ComfyUI
Phew, that was a lot of setup! From here on out, things should be a lot more straightforward. The first thing you'll want to do is to follow <a href="https://github.com/ltdrdata/ComfyUI-Manager" target="_blank" class="link-primary">the installation instructions for the ComfyUI Mod Manager</a>. We'll follow the Method 1 instructions, which at this time is just cloning the repo into the `custom_nodes` folder with `comfyui-manager` as the directory name. From there, you can restart ComfyUI and see a new button in the top right for the mod manager.
TODO: Screenshot here
Once that's all set up, I'll essentially be repeating Nerdy Rodent in <a href="https://www.youtube.com/watch?v=mwiz1PNDWGg" target="_blank" class="link-primary">their wonderful tutorial for OmniGen</a>. All you need to do is search for `ComfyUI-OmniGen` in Mod Manager, and install it. This will make sure their custom ComfyUI node project is downloaded and installs all its dependencies. Once this completes, restart ComfyUI.
TODO: screenshot of searching for omnigen
TODO: file of omnigen workflow
From there, you can take any
<p><span id="ref1" class="link-primary">[1]</span> Trust me, Python is _notorious_ for dependency hell, you'll thank me later

View File

@ -1,12 +0,0 @@
<script lang="ts">
import Editor from '$lib/components/pad/Editor.svelte';
const INITIAL_HTML_CONTENT = `<h1>Simple editor</h1><p>What's written here will be saved to your local storage. Highlight a word to see suggested rhymes.</p>`;
</script>
<svelte:head>
<title>silentsilas - Pad</title>
<meta name="description" content="A text editor with rhyming and word suggestions" />
</svelte:head>
<Editor initialContent={INITIAL_HTML_CONTENT} />

View File

@ -12,8 +12,8 @@ export const GET = async () => {
const body = render(posts);
const options = {
headers: {
'Cache-Control': 'max-age=0, s-maxage=604800',
'Content-Type': 'application/atom+xml'
'Cache-Control': 'max-age=0, s-maxage=3600',
'Content-Type': 'application/xml'
}
};
@ -25,8 +25,8 @@ const render = (posts: Post[]) => `<?xml version="1.0" encoding="UTF-8" ?>
<channel>
<title>${siteTitle}</title>
<description>${siteDescription}</description>
<link>${siteURL}/poetry</link>
<atom:link href="${siteURL}/poetry/rss" rel="self" type="application/rss+xml"/>
<link>${siteURL}</link>
<atom:link href="${siteURL}/rss.xml" rel="self" type="application/rss+xml"/>
${posts
.map(
(post) => `<item>

View File

@ -12,8 +12,8 @@ export const GET = async () => {
const body = render(posts);
const options = {
headers: {
'Cache-Control': 'max-age=0, s-maxage=604800',
'Content-Type': 'application/atom+xml'
'Cache-Control': 'max-age=0, s-maxage=3600',
'Content-Type': 'application/xml'
}
};
@ -26,7 +26,7 @@ const render = (posts: Post[]) => `<?xml version="1.0" encoding="UTF-8" ?>
<title>${siteTitle}</title>
<description>${siteDescription}</description>
<link>${siteURL}</link>
<atom:link href="${siteURL}/rss" rel="self" type="application/rss+xml"/>
<atom:link href="${siteURL}/rss.xml" rel="self" type="application/rss+xml"/>
${posts
.map(
(post) => `<item>

View File

@ -12,6 +12,12 @@
description:
"A pastebin service, useful for sending quick text snippets or sharing sensitive data with Burn After Reading. It doesn't require an account, but I do log the IP addresses of visitors for security purposes. None of the data entered is stored on the server itself; it is E2E-encrypted and contained in the URLs it generates. Anyone with the link can access the data, however, so be sure to send it to them securely, enable Burn After Reading, or share only nonsensitive data."
},
{
title: 'FreshRSS',
url: 'https://rss.silentsilas.com',
description:
"A self-hosted RSS reader that I use to keep up with blogs, podcasts, and news sites. I can provision an account for you if you want to try it out. You would use an RSS client on your devices and have them subscribe to your FreshRSS URL. You would then add all of the RSS feeds you'd like to follow via the FreshRSS web interface and all of your RSS clients will then pull the articles for you to read."
},
{
title: 'The Lounge',
url: 'https://irc.silentsilas.com',

View File

@ -12,8 +12,8 @@ export const GET = async () => {
const body = render(posts);
const options = {
headers: {
'Cache-Control': 'max-age=0, s-maxage=604800',
'Content-Type': 'application/atom+xml'
'Cache-Control': 'max-age=0, s-maxage=3600',
'Content-Type': 'application/xml'
}
};
@ -25,8 +25,8 @@ const render = (posts: Post[]) => `<?xml version="1.0" encoding="UTF-8" ?>
<channel>
<title>${siteTitle}</title>
<description>${siteDescription}</description>
<link>${siteURL}/thoughts</link>
<atom:link href="${siteURL}/thoughts/rss" rel="self" type="application/rss+xml"/>
<link>${siteURL}</link>
<atom:link href="${siteURL}/rss.xml" rel="self" type="application/rss+xml"/>
${posts
.map(
(post) => `<item>

View File

@ -1,6 +1,6 @@
// eslint-disable-next-line
import * as tf from '@tensorflow/tfjs-node';
import postEmbeddings from '$lib/utils/embeddings.json';
import postEmbeddings from '$lib/utils/poetry/embeddings.json';
import { error, json } from '@sveltejs/kit';
import { getModel, type Embedding, type SearchResult } from '$lib/utils/search';
import { fetchMarkdownPosts } from '$lib/utils';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 595 KiB

View File

@ -1,16 +1,9 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
import { threlteStudio } from '@threlte/studio/vite';
import { aiRobots } from '@silentsilas/vite-plugin-ai-robots';
export default defineConfig({
plugins: [
threlteStudio(),
sveltekit(),
aiRobots({
accessToken: process.env.DARK_VISITORS_TOKEN || ''
})
],
plugins: [threlteStudio(), sveltekit()],
ssr: {
noExternal: ['three', 'three-inspect']
},