From c480f5a7d16efc424ff1d3352e786b322a27a844 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Thu, 14 Nov 2024 16:51:05 +0000 Subject: [PATCH] AIStory: Add HF tokenizers --- bun.lockb | Bin 31747 -> 62228 bytes package.json | 1 + src/games/ai-story/assets/style.css | 2 + .../ai-story/components/autoTextarea.tsx | 2 +- src/games/ai-story/components/chat.tsx | 4 +- .../components/header/connectionEditor.tsx | 43 +++-- .../components/header/header.module.css | 8 + .../ai-story/components/header/header.tsx | 3 +- .../components/message/formattedMessage.tsx | 2 +- .../ai-story/components/message/message.tsx | 4 +- .../ai-story/components/minichat/minichat.tsx | 4 +- src/games/ai-story/contexts/llm.tsx | 52 +++--- src/games/ai-story/contexts/state.tsx | 6 +- src/games/ai-story/{ => tools}/connection.ts | 159 ++++++++++-------- src/games/ai-story/{ => tools}/dom.ts | 0 src/games/ai-story/{ => tools}/huggingface.ts | 63 +++++-- src/games/ai-story/{ => tools}/messages.ts | 3 +- src/games/ai-story/tools/model.ts | 27 +++ 18 files changed, 234 insertions(+), 149 deletions(-) rename src/games/ai-story/{ => tools}/connection.ts (70%) rename src/games/ai-story/{ => tools}/dom.ts (100%) rename src/games/ai-story/{ => tools}/huggingface.ts (82%) rename src/games/ai-story/{ => tools}/messages.ts (97%) create mode 100644 src/games/ai-story/tools/model.ts diff --git a/bun.lockb b/bun.lockb index 40e1e247ab609eaca320fbae79aa4ad1dc8d3299..888bd9a4253b681332309a1fcc4d9cfa6aac6167 100755 GIT binary patch literal 62228 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 diff --git a/package.json b/package.json index 404c108..7971592 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@huggingface/gguf": "0.1.12", "@huggingface/hub": "0.19.0", "@huggingface/jinja": "0.3.1", + "@huggingface/transformers": "3.0.2", "@inquirer/select": "2.3.10", "ace-builds": "1.36.3", "classnames": "2.5.1", diff --git a/src/games/ai-story/assets/style.css b/src/games/ai-story/assets/style.css index bdcc6e5..ab8f09f 100644 --- a/src/games/ai-story/assets/style.css +++ b/src/games/ai-story/assets/style.css @@ -7,6 +7,8 @@ --green: #AFAFAF; --red: #7F0000; --green: #007F00; + --brightRed: #DD0000; + --brightGreen: #00DD00; --shadeColor: rgba(0, 128, 128, 0.3); --border: 1px solid var(--color); diff --git a/src/games/ai-story/components/autoTextarea.tsx b/src/games/ai-story/components/autoTextarea.tsx index d0c592b..ac69a93 100644 --- a/src/games/ai-story/components/autoTextarea.tsx +++ b/src/games/ai-story/components/autoTextarea.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from "preact/hooks"; import type { JSX } from "preact/jsx-runtime" import { useIsVisible } from '@common/hooks/useIsVisible'; -import { DOMTools } from "../dom"; +import { DOMTools } from "../tools/dom"; export const AutoTextarea = (props: JSX.HTMLAttributes) => { const { value } = props; diff --git a/src/games/ai-story/components/chat.tsx b/src/games/ai-story/components/chat.tsx index 7a07f75..aee10c8 100644 --- a/src/games/ai-story/components/chat.tsx +++ b/src/games/ai-story/components/chat.tsx @@ -1,8 +1,8 @@ import { useCallback, useContext, useEffect, useRef } from "preact/hooks"; import { StateContext } from "../contexts/state"; import { Message } from "./message/message"; -import { MessageTools } from "../messages"; -import { DOMTools } from "../dom"; +import { MessageTools } from "../tools/messages"; +import { DOMTools } from "../tools/dom"; export const Chat = () => { const { messages } = useContext(StateContext); diff --git a/src/games/ai-story/components/header/connectionEditor.tsx b/src/games/ai-story/components/header/connectionEditor.tsx index 5836e2e..81b3592 100644 --- a/src/games/ai-story/components/header/connectionEditor.tsx +++ b/src/games/ai-story/components/header/connectionEditor.tsx @@ -1,11 +1,10 @@ import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'; - import styles from './header.module.css'; -import { Connection, HORDE_ANON_KEY, isHordeConnection, isKoboldConnection, type IConnection, type IHordeModel } from '../../connection'; +import { Connection, HORDE_ANON_KEY, isHordeConnection, isKoboldConnection, type IConnection, type IHordeModel } from '../../tools/connection'; import { Instruct } from '../../contexts/state'; import { useInputState } from '@common/hooks/useInputState'; import { useInputCallback } from '@common/hooks/useInputCallback'; -import { Huggingface } from '../../huggingface'; +import { Huggingface } from '../../tools/huggingface'; interface IProps { connection: IConnection; @@ -13,10 +12,13 @@ interface IProps { } export const ConnectionEditor = ({ connection, setConnection }: IProps) => { + // kobold const [connectionUrl, setConnectionUrl] = useInputState(''); + // horde const [apiKey, setApiKey] = useInputState(HORDE_ANON_KEY); const [modelName, setModelName] = useInputState(''); + const [instruct, setInstruct] = useInputState(''); const [modelTemplate, setModelTemplate] = useInputState(''); const [hordeModels, setHordeModels] = useState([]); const [contextLength, setContextLength] = useState(0); @@ -27,11 +29,14 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => { return 'unknown'; }, [connection]); - const urlValid = useMemo(() => contextLength > 0, [contextLength]); + const isOnline = useMemo(() => contextLength > 0, [contextLength]); useEffect(() => { + setInstruct(connection.instruct); + if (isKoboldConnection(connection)) { setConnectionUrl(connection.url); + Connection.getContextLength(connection).then(setContextLength); } else if (isHordeConnection(connection)) { setModelName(connection.model); setApiKey(connection.apiKey || HORDE_ANON_KEY); @@ -39,9 +44,6 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => { Connection.getHordeModels() .then(m => setHordeModels(Array.from(m.values()).sort((a, b) => a.name.localeCompare(b.name)))); } - - Connection.getContextLength(connection).then(setContextLength); - Connection.getModelName(connection).then(setModelName); }, [connection]); useEffect(() => { @@ -50,47 +52,44 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => { .then(template => { if (template) { setModelTemplate(template); + setInstruct(template); } }); } }, [modelName]); - const setInstruct = useInputCallback((instruct) => { - setConnection({ ...connection, instruct }); - }, [connection, setConnection]); - const setBackendType = useInputCallback((type) => { if (type === 'kobold') { setConnection({ - instruct: connection.instruct, + instruct, url: connectionUrl, }); } else if (type === 'horde') { setConnection({ - instruct: connection.instruct, + instruct, apiKey, model: modelName, }); } - }, [connection, setConnection, connectionUrl, apiKey, modelName]); + }, [setConnection, connectionUrl, apiKey, modelName, instruct]); const handleBlurUrl = useCallback(() => { const regex = /^(?:http(s?):\/\/)?(.*?)\/?$/i; const url = connectionUrl.replace(regex, 'http$1://$2'); setConnection({ - instruct: connection.instruct, + instruct, url, }); - }, [connection, connectionUrl, setConnection]); + }, [connectionUrl, instruct, setConnection]); const handleBlurHorde = useCallback(() => { setConnection({ - instruct: connection.instruct, + instruct, apiKey, model: modelName, }); - }, [connection, apiKey, modelName, setConnection]); + }, [apiKey, modelName, instruct, setConnection]); return (
@@ -98,7 +97,7 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => { - {modelName && modelTemplate && } @@ -109,15 +108,15 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => { ))} - + {instruct !== modelTemplate && - + } {isKoboldConnection(connection) && } {isHordeConnection(connection) && <> { const promptsOpen = useBool(); const genparamsOpen = useBool(); const assistantOpen = useBool(); + const isOnline = useMemo(() => contextLength > 0, [contextLength]); const bannedWordsInput = useMemo(() => bannedWords.join('\n'), [bannedWords]); @@ -56,7 +57,7 @@ export const Header = () => {
-
diff --git a/src/games/ai-story/components/message/formattedMessage.tsx b/src/games/ai-story/components/message/formattedMessage.tsx index 8834fa2..e63d419 100644 --- a/src/games/ai-story/components/message/formattedMessage.tsx +++ b/src/games/ai-story/components/message/formattedMessage.tsx @@ -1,5 +1,5 @@ import { useMemo } from "preact/hooks"; -import { MessageTools } from "../../messages"; +import { MessageTools } from "../../tools/messages"; import styles from './message.module.css'; diff --git a/src/games/ai-story/components/message/message.tsx b/src/games/ai-story/components/message/message.tsx index e8fd843..fee63b7 100644 --- a/src/games/ai-story/components/message/message.tsx +++ b/src/games/ai-story/components/message/message.tsx @@ -1,7 +1,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; -import { MessageTools, type IMessage } from "../../messages"; +import { MessageTools, type IMessage } from "../../tools/messages"; import { StateContext } from "../../contexts/state"; -import { DOMTools } from "../../dom"; +import { DOMTools } from "../../tools/dom"; import styles from './message.module.css'; import { AutoTextarea } from "../autoTextarea"; diff --git a/src/games/ai-story/components/minichat/minichat.tsx b/src/games/ai-story/components/minichat/minichat.tsx index 9e63e9c..5b421f1 100644 --- a/src/games/ai-story/components/minichat/minichat.tsx +++ b/src/games/ai-story/components/minichat/minichat.tsx @@ -1,7 +1,7 @@ -import { MessageTools, type IMessage } from "../../messages" +import { MessageTools, type IMessage } from "../../tools/messages" import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { Modal } from "@common/components/modal/modal"; -import { DOMTools } from "../../dom"; +import { DOMTools } from "../../tools/dom"; import styles from './minichat.module.css'; import { LLMContext } from "../../contexts/llm"; diff --git a/src/games/ai-story/contexts/llm.tsx b/src/games/ai-story/contexts/llm.tsx index d250b0b..078dfa2 100644 --- a/src/games/ai-story/contexts/llm.tsx +++ b/src/games/ai-story/contexts/llm.tsx @@ -1,13 +1,13 @@ import { createContext } from "preact"; import { useCallback, useContext, useEffect, useMemo, useState } from "preact/hooks"; -import { MessageTools, type IMessage } from "../messages"; +import { MessageTools, type IMessage } from "../tools/messages"; import { StateContext } from "./state"; import { useBool } from "@common/hooks/useBool"; -import { Template } from "@huggingface/jinja"; -import { Huggingface } from "../huggingface"; -import { approximateTokens, Connection, normalizeModel, type IGenerationSettings } from "../connection"; +import { Huggingface } from "../tools/huggingface"; +import { Connection, type IGenerationSettings } from "../tools/connection"; import { throttle } from "@common/utils"; import { useAsyncEffect } from "@common/hooks/useAsyncEffect"; +import { approximateTokens, normalizeModel } from "../tools/model"; interface ICompileArgs { keepUsers?: number; @@ -58,15 +58,7 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { const [modelName, setModelName] = useState(''); const [hasToolCalls, setHasToolCalls] = useState(false); - const userPromptTemplate = useMemo(() => { - try { - return new Template(userPrompt) - } catch { - return { - render: () => userPrompt, - } - } - }, [userPrompt]); + const isOnline = useMemo(() => contextLength > 0, [contextLength]); const actions: IActions = useMemo(() => ({ compilePrompt: async (messages, { keepUsers, continueLast = false } = {}) => { @@ -86,7 +78,7 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { const promptMessages = continueLast ? messages.slice(0, -1) : messages.slice(); if (isContinue) { - promptMessages.push(MessageTools.create(userPromptTemplate.render({}))); + promptMessages.push(MessageTools.create(Huggingface.applyTemplate(userPrompt, {}))); } const userMessages = promptMessages.filter(m => m.role === 'user'); @@ -113,7 +105,7 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { } else if (role === 'user' && !message.technical) { templateMessages.push({ role: message.role, - content: userPromptTemplate.render({ prompt: content, isStart: !wasStory }), + content: Huggingface.applyTemplate(userPrompt, { prompt: content, isStart: !wasStory }), }); } else { if (role === 'assistant') { @@ -137,17 +129,17 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { if (story.length > 0) { const prompt = MessageTools.getSwipe(firstUserMessage)?.content; - templateMessages.push({ role: 'user', content: userPromptTemplate.render({ prompt, isStart: true }) }); + templateMessages.push({ role: 'user', content: Huggingface.applyTemplate(userPrompt, { prompt, isStart: true }) }); templateMessages.push({ role: 'assistant', content: story }); } - let userPrompt = MessageTools.getSwipe(lastUserMessage)?.content; - if (!lastUserMessage?.technical && !isContinue && userPrompt) { - userPrompt = userPromptTemplate.render({ prompt: userPrompt, isStart: story.length === 0 }); + let userMessage = MessageTools.getSwipe(lastUserMessage)?.content; + if (!lastUserMessage?.technical && !isContinue && userMessage) { + userMessage = Huggingface.applyTemplate(userPrompt, { prompt: userMessage, isStart: story.length === 0 }); } - if (userPrompt) { - templateMessages.push({ role: 'user', content: userPrompt }); + if (userMessage) { + templateMessages.push({ role: 'user', content: userMessage }); } } @@ -156,7 +148,7 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { templateMessages.splice(1, 0, { role: 'user', - content: userPromptTemplate.render({ prompt, isStart: true }), + content: Huggingface.applyTemplate(userPrompt, { prompt, isStart: true }), }); } @@ -210,10 +202,10 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { stopGeneration: () => { Connection.stopGeneration(); }, - }), [connection, lore, userPromptTemplate, systemPrompt, bannedWords, summarizePrompt]); + }), [connection, lore, userPrompt, systemPrompt, bannedWords, summarizePrompt]); useAsyncEffect(async () => { - if (triggerNext && !generating.value) { + if (isOnline && triggerNext && !generating.value) { setTriggerNext(false); setContinueLast(false); @@ -244,10 +236,10 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { MessageTools.playReady(); } - }, [triggerNext]); + }, [triggerNext, isOnline]); useAsyncEffect(async () => { - if (summaryEnabled && !processing.summarizing) { + if (isOnline && summaryEnabled && !processing.summarizing) { try { processing.summarizing = true; for (let id = 0; id < messages.length; id++) { @@ -264,7 +256,7 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { processing.summarizing = false; } } - }, [messages, summaryEnabled]); + }, [messages, summaryEnabled, isOnline]); useEffect(throttle(() => { Connection.getContextLength(connection).then(setContextLength); @@ -272,7 +264,7 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { }, 1000, true), [connection]); const calculateTokens = useCallback(throttle(async () => { - if (!processing.tokenizing && !generating.value) { + if (isOnline && !processing.tokenizing && !generating.value) { try { processing.tokenizing = true; const { prompt } = await actions.compilePrompt(messages); @@ -284,11 +276,11 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { processing.tokenizing = false; } } - }, 1000, true), [actions, messages]); + }, 1000, true), [actions, messages, isOnline]); useEffect(() => { calculateTokens(); - }, [messages, connection, systemPrompt, lore, userPrompt]); + }, [messages, connection, systemPrompt, lore, userPrompt, isOnline]); useEffect(() => { try { diff --git a/src/games/ai-story/contexts/state.tsx b/src/games/ai-story/contexts/state.tsx index 63f2936..2e4fa41 100644 --- a/src/games/ai-story/contexts/state.tsx +++ b/src/games/ai-story/contexts/state.tsx @@ -1,8 +1,8 @@ import { createContext } from "preact"; import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; -import { MessageTools, type IMessage } from "../messages"; +import { MessageTools, type IMessage } from "../tools/messages"; import { useInputState } from "@common/hooks/useInputState"; -import { type IConnection } from "../connection"; +import { type IConnection } from "../tools/connection"; interface IContext { currentConnection: number; @@ -83,7 +83,7 @@ Continue the story forward. {%- endif %} {% if prompt -%} -This is the description of What should happen next in your answer: {{ prompt | trim }} +This is the description of what should happen next in your answer: {{ prompt | trim }} {% endif %} Remember that this story should be infinite and go forever. Make sure to follow the world description and rules exactly. Avoid cliffhangers and pauses, be creative.`, diff --git a/src/games/ai-story/connection.ts b/src/games/ai-story/tools/connection.ts similarity index 70% rename from src/games/ai-story/connection.ts rename to src/games/ai-story/tools/connection.ts index 4892e05..7b8b571 100644 --- a/src/games/ai-story/connection.ts +++ b/src/games/ai-story/tools/connection.ts @@ -2,6 +2,8 @@ import Lock from "@common/lock"; import SSE from "@common/sse"; import { throttle } from "@common/utils"; import delay from "delay"; +import { Huggingface } from "./huggingface"; +import { approximateTokens, normalizeModel } from "./model"; interface IBaseConnection { instruct: string; @@ -79,34 +81,6 @@ const MAX_HORDE_LENGTH = 512; const MAX_HORDE_CONTEXT = 32000; export const HORDE_ANON_KEY = '0000000000'; -export const normalizeModel = (model: string) => { - let currentModel = model.split(/[\\\/]/).at(-1); - currentModel = currentModel.split('::').at(0); - let normalizedModel: string; - - do { - normalizedModel = currentModel; - - currentModel = currentModel - .replace(/[ ._-]\d+(k$|-context)/i, '') // remove context length, i.e. -32k - .replace(/[ ._-](gptq|awq|exl2?|imat|i\d|h\d)/i, '') // remove quant name - .replace(/([ ._-]?gg(uf|ml)[ ._-]?(v[ ._-]?\d)?)/i, '') // remove gguf-v3/ggml/etc - .replace(/[ ._-]i?q([ ._-]?\d[ ._-]?(k?[ ._-]?x*[ ._-]?[lms]?)?)+/i, '') // remove quant size - .replace(/[ ._-]\d+(\.\d+)?bpw/i, '') // remove bpw - .replace(/[ ._-]f(p|loat)?(8|16|32)/i, '') - .replace(/^(debug-?)+/i, '') - .trim(); - } while (normalizedModel !== currentModel); - - return normalizedModel - .replace(/[ _-]+/ig, '-') - .replace(/\.{2,}/, '-') - .replace(/[ ._-]+$/ig, '') - .trim(); -} - -export const approximateTokens = (prompt: string): number => Math.round(prompt.length / 4); - export type IGenerationSettings = Partial; export namespace Connection { @@ -171,7 +145,11 @@ export namespace Connection { sse.close(); } - async function generateHorde(connection: Omit, prompt: string, extraSettings: IGenerationSettings = {}): Promise { + async function* generateHorde(connection: IHordeConnection, prompt: string, extraSettings: IGenerationSettings = {}): AsyncGenerator { + if (!connection.model) { + throw new Error('Horde not connected'); + } + const models = await getHordeModels(); const model = models.get(connection.model); if (model) { @@ -192,54 +170,78 @@ export namespace Connection { models: model.hordeNames, workers: model.workers, }; + const bannedTokens = requestData.params.banned_tokens ?? []; const { signal } = abortController; - const generateResponse = await fetch(`${AIHORDE}/api/v2/generate/text/async`, { - method: 'POST', - body: JSON.stringify(requestData), - headers: { - 'Content-Type': 'application/json', - apikey: connection.apiKey || HORDE_ANON_KEY, - }, - signal, - }); + while (true) { + const generateResponse = await fetch(`${AIHORDE}/api/v2/generate/text/async`, { + method: 'POST', + body: JSON.stringify(requestData), + headers: { + 'Content-Type': 'application/json', + apikey: connection.apiKey || HORDE_ANON_KEY, + }, + signal, + }); - if (!generateResponse.ok || generateResponse.status >= 400) { - throw new Error(`Error starting generation: ${generateResponse.statusText}: ${await generateResponse.text()}`); - } - - const { id } = await generateResponse.json() as { id: string }; - const request = async (method = 'GET'): Promise => { - const response = await fetch(`${AIHORDE}/api/v2/generate/text/status/${id}`, { method }); - if (response.ok && response.status < 400) { - const result: IHordeResult = await response.json(); - if (result.generations?.length === 1) { - const { text } = result.generations[0]; - - return text; - } - } else { - throw new Error(await response.text()); + if (!generateResponse.ok || generateResponse.status >= 400) { + throw new Error(`Error starting generation: ${generateResponse.statusText}: ${await generateResponse.text()}`); } - return null; - }; + const { id } = await generateResponse.json() as { id: string }; + const request = async (method = 'GET'): Promise => { + const response = await fetch(`${AIHORDE}/api/v2/generate/text/status/${id}`, { method }); + if (response.ok && response.status < 400) { + const result: IHordeResult = await response.json(); + if (result.generations?.length === 1) { + const { text } = result.generations[0]; - const deleteRequest = async () => (await request('DELETE')) ?? ''; - - while (true) { - try { - await delay(2500, { signal }); - - const text = await request(); - - if (text) { - return text; + return text; + } + } else { + throw new Error(await response.text()); + } + + return null; + }; + + const deleteRequest = async () => (await request('DELETE')) ?? ''; + let text: string | null = null; + + while (!text) { + try { + await delay(2500, { signal }); + + text = await request(); + + if (text) { + const locaseText = text.toLowerCase(); + let unsloppedText = text; + for (const ban of bannedTokens) { + const slopIdx = locaseText.indexOf(ban.toLowerCase()); + if (slopIdx >= 0) { + console.log(`[horde] slop '${ban}' detected at ${slopIdx}`); + unsloppedText = unsloppedText.slice(0, slopIdx); + } + } + + yield unsloppedText; + + requestData.prompt += unsloppedText; + + if (unsloppedText === text) { + return; // we are finished + } + + if (unsloppedText.length === 0) { + requestData.params.temperature += 0.05; + } + } + } catch (e) { + console.error('Error in horde generation:', e); + return yield deleteRequest(); } - } catch (e) { - console.error('Error in horde generation:', e); - return deleteRequest(); } } } @@ -251,7 +253,7 @@ export namespace Connection { if (isKoboldConnection(connection)) { yield* generateKobold(connection.url, prompt, extraSettings); } else if (isHordeConnection(connection)) { - yield await generateHorde(connection, prompt, extraSettings); + yield* generateHorde(connection, prompt, extraSettings); } } @@ -277,7 +279,7 @@ export namespace Connection { for (const worker of goodWorkers) { for (const modelName of worker.models) { - const normName = normalizeModel(modelName.toLowerCase()); + const normName = normalizeModel(modelName); let model = models.get(normName); if (!model) { model = { @@ -343,7 +345,7 @@ export namespace Connection { } catch (e) { console.error('Error getting max tokens', e); } - } else if (isHordeConnection(connection)) { + } else if (isHordeConnection(connection) && connection.model) { const models = await getHordeModels(); const model = models.get(connection.model); if (model) { @@ -367,7 +369,18 @@ export namespace Connection { return value; } } catch (e) { - console.error('Error counting tokens', e); + console.error('Error counting tokens:', e); + } + } else { + const model = await getModelName(connection); + const tokenizer = await Huggingface.findTokenizer(model); + if (tokenizer) { + try { + const { input_ids } = await tokenizer(prompt); + return input_ids.data.length; + } catch (e) { + console.error('Error counting tokens with tokenizer:', e); + } } } diff --git a/src/games/ai-story/dom.ts b/src/games/ai-story/tools/dom.ts similarity index 100% rename from src/games/ai-story/dom.ts rename to src/games/ai-story/tools/dom.ts diff --git a/src/games/ai-story/huggingface.ts b/src/games/ai-story/tools/huggingface.ts similarity index 82% rename from src/games/ai-story/huggingface.ts rename to src/games/ai-story/tools/huggingface.ts index 5885045..80f22f2 100644 --- a/src/games/ai-story/huggingface.ts +++ b/src/games/ai-story/tools/huggingface.ts @@ -1,7 +1,8 @@ import { gguf } from '@huggingface/gguf'; import * as hub from '@huggingface/hub'; import { Template } from '@huggingface/jinja'; -import { normalizeModel } from './connection'; +import { AutoTokenizer, PreTrainedTokenizer } from '@huggingface/transformers'; +import { normalizeModel } from './model'; export namespace Huggingface { export interface ITemplateMessage { @@ -81,6 +82,7 @@ export namespace Huggingface { const templateCache: Record = loadCache(); const compiledTemplates = new Map(); + const tokenizerCache = new Map(); const hasField = (obj: unknown, field: T): obj is Record => ( obj != null && typeof obj === 'object' && (field in obj) @@ -92,13 +94,13 @@ export namespace Huggingface { ); const loadHuggingfaceTokenizerConfig = async (modelName: string): Promise => { + modelName = normalizeModel(modelName); console.log(`[huggingface] searching config for '${modelName}'`); - const searchModel = normalizeModel(modelName); - const hubModels = await Array.fromAsync(hub.listModels({ search: { query: searchModel }, additionalFields: ['config'] })); + const hubModels = await Array.fromAsync(hub.listModels({ search: { query: modelName }, additionalFields: ['config'] })); const models = hubModels.filter(m => { if (m.gated) return false; - if (!normalizeModel(m.name).includes(searchModel)) return false; + if (!normalizeModel(m.name).includes(modelName)) return false; return true; }).sort((a, b) => b.downloads - a.downloads); @@ -116,8 +118,8 @@ export namespace Huggingface { } try { - console.log(`[huggingface] searching config in '${model.name}/tokenizer_config.json'`); - const fileResponse = await hub.downloadFile({ repo: model.name, path: 'tokenizer_config.json' }); + console.log(`[huggingface] searching config in '${name}/tokenizer_config.json'`); + const fileResponse = await hub.downloadFile({ repo: name, path: 'tokenizer_config.json' }); if (fileResponse?.ok) { const maybeConfig = await fileResponse.json(); if (isTokenizerConfig(maybeConfig)) { @@ -232,10 +234,10 @@ export namespace Huggingface { } export const findModelTemplate = async (modelName: string): Promise => { - const modelKey = modelName.toLowerCase().trim(); - if (!modelKey) return ''; + modelName = normalizeModel(modelName); + if (!modelName) return ''; - let template = templateCache[modelKey] ?? null; + let template = templateCache[modelName] ?? null; if (template) { console.log(`[huggingface] found cached template for '${modelName}'`); @@ -254,12 +256,53 @@ export namespace Huggingface { } } - templateCache[modelKey] = template; + templateCache[modelName] = template; saveCache(templateCache); return template; } + export const findTokenizer = async (modelName: string): Promise => { + modelName = normalizeModel(modelName); + + let tokenizer = tokenizerCache.get(modelName) ?? null; + + if (tokenizer) { + return tokenizer; + } else if (!tokenizerCache.has(modelName)) { + console.log(`[huggingface] searching tokenizer for '${modelName}'`); + + const hubModels = await Array.fromAsync(hub.listModels({ search: { query: modelName } })); + const models = hubModels.filter(m => { + if (m.gated) return false; + if (m.name.toLowerCase().includes('gguf')) return false; + if (!normalizeModel(m.name).includes(modelName)) return false; + + return true; + }); + + for (const model of models) { + const { name } = model; + + try { + console.log(`[huggingface] searching tokenizer in '${name}'`); + tokenizer = await AutoTokenizer.from_pretrained(name); + break; + } catch { } + } + } + + tokenizerCache.set(modelName, tokenizer); + + if (tokenizer) { + console.log(`[huggingface] found tokenizer for '${modelName}'`); + } else { + console.log(`[huggingface] not found tokenizer for '${modelName}'`); + } + + return tokenizer; + } + export const applyChatTemplate = (templateString: string, messages: ITemplateMessage[], functions?: IFunction[]) => ( applyTemplate(templateString, { messages, diff --git a/src/games/ai-story/messages.ts b/src/games/ai-story/tools/messages.ts similarity index 97% rename from src/games/ai-story/messages.ts rename to src/games/ai-story/tools/messages.ts index 19420e6..6d15636 100644 --- a/src/games/ai-story/messages.ts +++ b/src/games/ai-story/tools/messages.ts @@ -1,5 +1,4 @@ -import { Template } from "@huggingface/jinja"; -import messageSound from './assets/message.mp3'; +import messageSound from '../assets/message.mp3'; export interface ISwipe { content: string; diff --git a/src/games/ai-story/tools/model.ts b/src/games/ai-story/tools/model.ts new file mode 100644 index 0000000..1cb2a9b --- /dev/null +++ b/src/games/ai-story/tools/model.ts @@ -0,0 +1,27 @@ +export const normalizeModel = (model: string) => { + let currentModel = model.split(/[\\\/]/).at(-1); + currentModel = currentModel.split('::').at(0).toLowerCase(); + let normalizedModel: string; + + do { + normalizedModel = currentModel; + + currentModel = currentModel + .replace(/[ ._-]\d+(k$|-context)/i, '') // remove context length, i.e. -32k + .replace(/[ ._-](gptq|awq|exl2?|imat|i\d|h\d)/i, '') // remove quant name + .replace(/([ ._-]?gg(uf|ml)[ ._-]?(v[ ._-]?\d)?)/i, '') // remove gguf-v3/ggml/etc + .replace(/[ ._-]i?q([ ._-]?\d[ ._-]?(k?[ ._-]?x*[ ._-]?[lms]?)?)+/i, '') // remove quant size + .replace(/[ ._-]\d+(\.\d+)?bpw/i, '') // remove bpw + .replace(/[ ._-]f(p|loat)?(8|16|32)/i, '') + .replace(/^(debug-?)+/i, '') + .trim(); + } while (normalizedModel !== currentModel); + + return normalizedModel + .replace(/[ _-]+/ig, '-') + .replace(/\.{2,}/, '-') + .replace(/[ ._-]+$/ig, '') + .trim(); +} + +export const approximateTokens = (prompt: string): number => Math.round(prompt.length / 4); \ No newline at end of file