From feb192bc8f8f6f79253ce7209a614f9871c81ecd Mon Sep 17 00:00:00 2001 From: yukiarimo Date: Fri, 19 Jul 2024 11:05:32 -0600 Subject: [PATCH 1/9] Optimization, 11labs and History Management Fix - Unnecessary scripts removed to speed up the client `script.min.js` - Moved Bootstrap to general `JS` folder - Removed WaifuCard and its dependencies - Moved `prompts.txt` to the lib directory - Added ElevenLabs support (11labs audio mode) - Fixed support for the chat history collections on the front - Removed Himitsu Old Code (for now) --- about.html | 152 -------------- index.html | 5 +- index.py | 3 +- index.sh | 4 +- lib/audio.py | 24 +++ lib/generate.py | 19 +- {static => lib}/prompts.txt | 0 lib/router.py | 5 +- login.html | 3 +- services.html | 5 +- static/config.json | 13 +- static/css/about.css | 218 --------------------- static/img/hardscenebreak.png | Bin 2135 -> 0 bytes static/img/holobg.webp | Bin 6644 -> 0 bytes static/img/space.webp | Bin 67468 -> 0 bytes static/js/{bootstrap => }/bootstrap.min.js | 0 static/js/bootstrap/script.min.js | 1 - static/js/himitsu.js | 196 ------------------ static/js/index.js | 87 ++++++-- static/sw.js | 4 +- yuna.html | 8 +- 21 files changed, 126 insertions(+), 621 deletions(-) delete mode 100644 about.html rename {static => lib}/prompts.txt (100%) delete mode 100644 static/css/about.css delete mode 100644 static/img/hardscenebreak.png delete mode 100644 static/img/holobg.webp delete mode 100644 static/img/space.webp rename static/js/{bootstrap => }/bootstrap.min.js (100%) delete mode 100644 static/js/bootstrap/script.min.js delete mode 100644 static/js/himitsu.js diff --git a/about.html b/about.html deleted file mode 100644 index 50f44bf..0000000 --- a/about.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - Yuna AI - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- -
-
-

Name:

-

Yuna

-
-
-

Age:

-

15

-
-
-

Gender:

-

Female

-
-
-

Race:

-

Japanese

-
-
- - -
-
- -
-
- - - Yuna AI - - -
-
♡ Your Private Companion ♡
-
-
-
-
- - - - - - - - - - \ No newline at end of file diff --git a/index.html b/index.html index b3601a3..8b220f3 100644 --- a/index.html +++ b/index.html @@ -87,7 +87,7 @@ Login @@ -482,8 +482,7 @@ - - + \ No newline at end of file diff --git a/index.py b/index.py index 0708a97..4ebc180 100644 --- a/index.py +++ b/index.py @@ -2,7 +2,7 @@ from flask import Flask, get_flashed_messages, request, jsonify, send_from_directory, redirect, url_for, flash from flask_login import LoginManager, UserMixin, login_required, logout_user, login_user, current_user, login_manager from lib.generate import ChatGenerator, ChatHistoryManager -from lib.router import handle_history_request, handle_image_request, handle_message_request, handle_audio_request, services, about, handle_search_request, handle_textfile_request +from lib.router import handle_history_request, handle_image_request, handle_message_request, handle_audio_request, services, handle_search_request, handle_textfile_request from flask_cors import CORS import json import os @@ -92,7 +92,6 @@ def configure_routes(self): self.app.route('/yuna')(self.yuna_server) self.app.route('/yuna.html')(self.yuna_server) self.app.route('/services.html', methods=['GET'], endpoint='services')(lambda: services(self)) - self.app.route('/about.html', methods=['GET'], endpoint='about')(lambda: about(self)) self.app.route('/apple-touch-icon.png')(self.image_pwa) self.app.route('/flash-messages')(self.flash_messages) self.app.route('/main', methods=['GET', 'POST'])(self.main) diff --git a/index.sh b/index.sh index dc12800..d809a4f 100644 --- a/index.sh +++ b/index.sh @@ -169,8 +169,8 @@ install_all_agi_models() { # Function to install Vision model install_vision_model() { echo "Installing Vision model..." - wget https://huggingface.co/yukiarimo/yuna-ai-vision-v2/resolve/main/yuna-ai-miru-v0.gguf -P lib/models/yuna/ - wget https://huggingface.co/yukiarimo/yuna-ai-vision-v2/resolve/main/yuna-ai-miru-eye-v0.gguf -P lib/models/yuna/ + wget https://huggingface.co/yukiarimo/yuna-ai-vision-v2/resolve/main/yuna-ai-miru-v0.gguf -P lib/models/agi/ + wget https://huggingface.co/yukiarimo/yuna-ai-vision-v2/resolve/main/yuna-ai-miru-eye-v0.gguf -P lib/models/agi/ } # Function to install Art model diff --git a/lib/audio.py b/lib/audio.py index 09fadb8..a897e02 100644 --- a/lib/audio.py +++ b/lib/audio.py @@ -15,6 +15,10 @@ from TTS.tts.configs.xtts_config import XttsConfig from TTS.tts.models.xtts import Xtts +if config['server']['yuna_audio_mode'] == "11labs": + from elevenlabs import VoiceSettings + from elevenlabs.client import ElevenLabs + def transcribe_audio(audio_file): result = model.transcribe(audio_file) return result['text'] @@ -108,6 +112,26 @@ def speak_text(text, reference_audio, output_audio, mode, language="en"): # convert audio to mp3 audio = AudioSegment.from_file("/Users/yuki/Documents/Github/yuna-ai/static/audio/audio.aiff") audio.export("/Users/yuki/Documents/Github/yuna-ai/static/audio/audio.mp3", format='mp3') + elif mode == "11labs": + client = ElevenLabs( + api_key=config['security']['11labs_key'] + ) + + audio = client.generate( + text=text, + voice="Yuna Instant", + voice_settings=VoiceSettings(stability=0.40, similarity_boost=0.98, style=0.35, use_speaker_boost=True), + model="eleven_multilingual_v2" + ) + + # Convert generator to bytes + audio_bytes = b''.join(audio) + + # Optionally, save the audio to a file + with open("/Users/yuki/Documents/Github/yuna-ai/static/audio/audio.mp3", "wb") as f: + f.write(audio_bytes) + else: + raise ValueError("Invalid mode for speaking text") if config['server']['yuna_audio_mode'] == "native": xtts_checkpoint = "/Users/yuki/Documents/Github/yuna-ai/lib/models/agi/yuna-talk/yuna-talk.pth" diff --git a/lib/generate.py b/lib/generate.py index 345b151..b587840 100644 --- a/lib/generate.py +++ b/lib/generate.py @@ -5,6 +5,12 @@ from llama_cpp import Llama from lib.history import ChatHistoryManager import requests +from langchain_community.document_loaders import TextLoader +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_community.vectorstores import Chroma +from langchain_community.embeddings import GPT4AllEmbeddings +from langchain.chains import RetrievalQA +from langchain_community.llms import LlamaCpp class ChatGenerator: def __init__(self, config): @@ -164,20 +170,13 @@ def generate(self, chat_id, speech=False, text="", template=None, chat_history_m return ''.join(response) if isinstance(response, (list, type((lambda: (yield))()))) else response return response - def processTextFile(self, text_file, question, temperature=0.6, max_new_tokens=128, context_window=2048): - from langchain_community.document_loaders import TextLoader - from langchain.text_splitter import RecursiveCharacterTextSplitter - from langchain_community.vectorstores import Chroma - from langchain_community.embeddings import GPT4AllEmbeddings - from langchain.chains import RetrievalQA - from langchain_community.llms import LlamaCpp - + def processTextFile(text_file, question, temperature=0.8, max_new_tokens=128, context_window=256): # Load text file data loader = TextLoader(text_file) data = loader.load() # Split text into chunks - text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0) + text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=0) docs = text_splitter.split_documents(data) # Generate embeddings locally using GPT4All @@ -186,7 +185,7 @@ def processTextFile(self, text_file, question, temperature=0.6, max_new_tokens=1 # Load GPT4All model for inference llm = LlamaCpp( - model_path='/Users/yuki/Documents/Github/yuna-ai/lib/models/yuna/yukiarimo/yuna-ai/yuna-ai-v2-q6_k.gguf', + model_path='/Users/yuki/Documents/Github/yuna-ai/lib/models/yuna/yuna-ai-v3-q6_k.gguf', temperature=temperature, max_new_tokens=max_new_tokens, context_window=context_window, diff --git a/static/prompts.txt b/lib/prompts.txt similarity index 100% rename from static/prompts.txt rename to lib/prompts.txt diff --git a/lib/router.py b/lib/router.py index 8285faf..620385c 100644 --- a/lib/router.py +++ b/lib/router.py @@ -233,7 +233,4 @@ def handle_textfile_request(chat_generator, self): return jsonify({'response': result}) def services(self): - return send_from_directory('.', 'services.html') - -def about(self): - return send_from_directory('.', 'about.html') \ No newline at end of file + return send_from_directory('.', 'services.html') \ No newline at end of file diff --git a/login.html b/login.html index 802e237..d2fd32f 100644 --- a/login.html +++ b/login.html @@ -160,8 +160,7 @@

Delete Account

- - + \ No newline at end of file diff --git a/services.html b/services.html index d5c401e..b60d828 100644 --- a/services.html +++ b/services.html @@ -85,7 +85,7 @@ Login @@ -387,8 +387,7 @@ - - + \ No newline at end of file diff --git a/static/config.json b/static/config.json index 4f97871..22c19e5 100644 --- a/static/config.json +++ b/static/config.json @@ -7,13 +7,13 @@ "emotions": false, "art": false, "vision": false, - "max_new_tokens": 128, - "context_length": 2048, + "max_new_tokens": 64, + "context_length": 1024, "temperature": 0.7, - "repetition_penalty": 1.11, + "repetition_penalty": 1.1, "last_n_tokens": 128, "seed": -1, - "top_k": 60, + "top_k": 100, "top_p": 0.92, "stop": [ "Yuki:", @@ -45,10 +45,11 @@ "art_default_model": "yuna_ai_anime.safetensors", "device": "mps", "yuna_text_mode": "native", - "yuna_audio_mode": "fast" + "yuna_audio_mode": "11labs" }, "security": { "secret_key": "YourSecretKeyHere123!", - "encryption_key": "zWZnu-lxHCTgY_EqlH4raJjxNJIgPlvXFbdk45bca_I=" + "encryption_key": "zWZnu-lxHCTgY_EqlH4raJjxNJIgPlvXFbdk45bca_I=", + "11labs_key": "Your11LabsKeyHere123!" } } diff --git a/static/css/about.css b/static/css/about.css deleted file mode 100644 index f387e63..0000000 --- a/static/css/about.css +++ /dev/null @@ -1,218 +0,0 @@ -* { - font-family: "kawai-font" !important; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -body { - margin: 0; - background-image: url('/static/img/space.webp'); - background-size: cover; - background-position: center; - background-repeat: no-repeat; - background-attachment: fixed; - height: 100vh; - width: 100vw; - /* Center the content */ - display: flex; - justify-content: center; - align-items: center; - font-family: 'Arial', sans-serif; -} - -.modal-body { - color: black; -} - -p { - margin-bottom: 0; -} - -#waifu-card-container { - display: flex; - justify-content: center; - align-items: center; -} - -.waifu-card { - background: linear-gradient(135deg, #ffffff 0%, #ffffff 100%); - padding: 20px; - border-radius: 15px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - text-align: center; - width: 500px; - height: auto; - max-height: 90vh; - position: relative; - overflow: hidden; - margin: auto; - background-size: cover; - background-position: center; -} - -.waifu-card::before { - content: ''; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-image: url('/static/img/holobg.webp'); - background-size: cover; - background-repeat: no-repeat; - opacity: 0.9; - border-radius: 15px; -} - -.waifu-card img { - max-width: 100%; - max-height: 60vh; - border-radius: 10px; - position: relative; - z-index: 2; -} - -.waifu-info { - position: relative; - z-index: 2; - margin-top: 20px; -} - -.waifu-title { - color: #333; - font-size: 24px; - font-weight: bold; -} - -.waifu-description { - color: #666; - font-size: 16px; - margin-top: 10px; -} - -.cute-element { - color: #ff69b4; - font-size: 30px; - margin: 5px 0; -} - -.detail-panel { - background: rgba(255, 255, 255, 0.9); - padding: 10px; - border-radius: 10px; - position: absolute; - top: 20px; - right: 20px; - width: auto; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.detail-text { - font-size: 12px; - color: #333; - line-height: 1.4; - text-align: left; -} - -/* Floating info blocks */ -.info-blocks { - position: absolute; - top: 20px; - left: 20px; - text-align: left; - color: #333; - z-index: 2; - display: flex; - flex-direction: column; -} - -.info-block { - display: flex; - justify-content: space-between; - align-items: center; - margin: 5px 0; - font-size: 14px; - background: rgba(255, 255, 255, 0.9); - padding: 5px 10px; - border-radius: 10px; -} - -.info-block p:first-child { - font-weight: bold; - margin-right: 10px; -} - -/* Waifu Title Block */ -.waifu-title-block { - display: flex; - align-items: center; - justify-content: center; -} - -/* Adjustments for image and text spacing */ -.waifu-card img { - max-width: 100%; - max-height: 70vh; - border-radius: 10px; - position: relative; - z-index: 1; -} - -.waifu-info { - position: relative; - z-index: 1; - margin-top: 10px; - color: white; -} - -.waifu-description { - color: white; - font-size: 24px; - font-weight: lighter; -} - -/* Ensure the card is not too tall */ -.waifu-card { - max-height: 85vh; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.modal-content { - background-color: #fff0f5; /* Light pink background */ - border-radius: 15px; /* Rounded corners for the modal */ -} - -.modal-title { - color: #ff69b4; /* Cute pink color for the title */ -} - -.btn-primary { - background-color: #ff69b4; /* Match the button color to the title */ - border-color: #ff69b4; -} - -.btn-primary:hover { - background-color: #ff5f9d; /* A slightly darker pink on hover */ - border-color: #ff5f9d; -} - -/* Responsive design for smaller screens */ -@media (max-width: 768px) { - .waifu-card { - width: 90%; - margin: 20px auto; - } -} - -@font-face { - font-family: "kawai-font"; - src: url("/static/fonts/kawai-font.woff") format("woff"); - font-style: normal; - font-weight: normal; -} \ No newline at end of file diff --git a/static/img/hardscenebreak.png b/static/img/hardscenebreak.png deleted file mode 100644 index 7d00de5917125178a546b5b47094cc80a2466549..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2135 zcmchX`#TegAIHauT+*Qujj_$G78<#;+}UO>L)csslXTo>I7HE08o3NbQ7(rVIp}m@ z?w7fyg&3tnXq-};Yi#)HpZI>B*ZcE+-p}*-@%?=6Iyu-%hyld_0DuGvY2|$A7QZC& z+u@#|aApoofaq*%0jM99UpWj%g3RsB0f5GAasCyd!z>z(#1R32C>R2TLX|*@N)V{B0$3Rg zQHCn1fT1c%%BspLY7iwg71guKD(6(yG-2m8)io|?Xu{Psv@|reG%sjffNR6GbhWf~ zwJ#dz=^N@B7#SEE8yX=FF*dnmf-pr~GBZJ#T{5+`wn5n3XMUcFc^$I+Rok% zJ_e&l=DtT1=q~vkglhVJ+D`?d< zwKcRlTJ5viy860j%`aXyzk1#Bx|!bG()zl&wT0eBZ|!JnXY}^<_w@GnG5Y)a2N?Z> zeT>2WfuVuHk%7TCgG2A$GAG8yC&woyC*DuJpJYu=u~@7b*7V%m-2A_D^KtfmxoaC7?#9~s=K99g#^%oU z&z+r}-Q8W@&MuF~-`_vj+dtU!u`p;68bE`TE<;tINk z2x}z9)zkNCSY%v6PJYGHmo0sRZ{D#!&MhvZT#62_g&a^;=B^$+&kA|IE`hS0`bAkh z4awvGG*Uq`yvIhxc9k{ec-ed{G4*%#Ro=>idGKI-QJ_`}(>mgf`U;fbCqtI@KBhm~ zWBPvmvA18tuwg=6B7iZ;GFmheU3WUeu0=^x~Jjj1pJsG#{KOL(<#tjibG0&= z?o%}gU7HmEId@HuNqutwNyZja1RdQolr!iZlhP3^my;B4q)3|ZIieX5(mij;s6LNI zF-br7eQ07e+4}#P?%WTPtO_ERVAD*5+Vm3v~YrIu1mzq-rpoilgr+COZL1t_h-vf~Rh^ms;{e2GnJ^s{XT^kt*0xbWf66BMk(_;wECSh@{OCn2*6 z&>DWe{o}qk4NK3zkaD@YcRlnWPDBOqe0?C+tqMAwece@BwduoMGS=d`Xl}rdkKO-< zZzk%bWI}t+yxki^TYZy!4z0E=Uwz)tAnCUOKO#1aOF$v8Z9n!wL?&SgPVW$3An<-)LZq-h!kpl?h$96yaHM1T>9wG0 z8$;-IW>FjMhWtFY{IvP~?cdagq_Alse~Vf&K?_qa_I}da#idAvis~?tFk#4z#BMuR z#|rh4p7P=Ajm-en8q}YQ+T^$Z*wyUF>4v$oD>TF8*xZ6FxBV}~^af8c$%xcK%P1=y zX7_B9yp>f__q9lb+;7bi_+DgmY6Rtdv0SH1^40G6I8Q-g*Twt2=mbhw^} znsqMAGn2oJE}Su;lvN&$&A_Cn9TbPaYpgtVIyZHUJAp#iyj7)m{=sfp=`oE0oQm)< z`K|=9S~a^Zl6+^cD&N)=gRZ&>!IYlZH5h6cuw2g60K}W9I)9q&GD5ZTM>}k*OLy6K zn&!a|s&B%uiR%qzqYV|H(pBAha#J9%)ab$tCDg($juo($yEUuP8}8alM)%@#E00OJ z!yT7OW=#9h#NjSL@ot_HewHhHtkF(M_=<7 zBP75d(_Te-vK_C)rVzH$Iu!$YVguKh1Ju=R@0e1BH|FU98Fy`iaLG$`KyQI6WA3Qcc^me1rFK%l_>s5e(dm=dZQReIP3f7g!)A@`s8-bH^|+SdYqI`{)%LH%<{mv& zoV#d{$J9%B7ICW5JD9Ut)T50Z1B9{@F7Aqiz5U0dv5gaGSO8rvmwC#f##rw5!CF2w zrm6bea-vd)vMuyKvBw39rszMZgO6M1;#1WNWL9d*#NP$#exMe4kSdJV0y=Mv#Eh^_ z;|<$&X)O~6IsC%_l(mCZy@g-$ F{{UtdIa~k$ diff --git a/static/img/holobg.webp b/static/img/holobg.webp deleted file mode 100644 index 079fdaaeca5c4b648f7d0f074211d4e1eaaa292d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6644 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 diff --git a/static/img/space.webp b/static/img/space.webp deleted file mode 100644 index d7ec906abe7d875c6e8aa00b5583730d16b22320..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67468 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 CIN^x^ diff --git a/static/js/bootstrap/bootstrap.min.js b/static/js/bootstrap.min.js similarity index 100% rename from static/js/bootstrap/bootstrap.min.js rename to static/js/bootstrap.min.js diff --git a/static/js/bootstrap/script.min.js b/static/js/bootstrap/script.min.js deleted file mode 100644 index 731a1f7..0000000 --- a/static/js/bootstrap/script.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(){"use strict";var e=document.querySelector(".scroll-to-top");e&&window.addEventListener("scroll",(function(){var o=window.pageYOffset;e.style.display=o>100?"block":"none"}));var o=document.querySelector("#mainNav");if(o){var n=o.querySelector(".navbar-collapse");if(n){var t=new bootstrap.Collapse(n,{toggle:!1}),r=n.querySelectorAll("a");for(var a of r)a.addEventListener("click",(function(e){t.hide()}))}var c=function(){(void 0!==window.pageYOffset?window.pageYOffset:(document.documentElement||document.body.parentNode||document.body).scrollTop)>100?o.classList.add("navbar-shrink"):o.classList.remove("navbar-shrink")};c(),document.addEventListener("scroll",c)}}(); \ No newline at end of file diff --git a/static/js/himitsu.js b/static/js/himitsu.js deleted file mode 100644 index 7d53c2f..0000000 --- a/static/js/himitsu.js +++ /dev/null @@ -1,196 +0,0 @@ -var name1; -var name2; - -async function loadConfig() { - const response = await fetch('/static/config.json'); - const data = await response.json(); - name1 = data.ai.names[0]; - name2 = data.ai.names[1]; -} - - -class PromptTemplate { - constructor(fields, templateInputs) { - this.fields = fields; - this.templateInputs = templateInputs; - } - - generateElements() { - const form = document.getElementById("Himitsu"); - form.innerHTML = ''; - - this.templateInputs.forEach(input => { - const label = document.createElement("label"); - label.setAttribute("for", input.id); - label.textContent = `${input.label}:`; - - const newElement = document.createElement(input.type === 'select' ? "select" : "input"); - newElement.setAttribute("id", input.id); - newElement.setAttribute("name", input.id); - - if (input.type === 'select') { - input.options.forEach(option => { - const optionElement = document.createElement("option"); - optionElement.value = option.toLowerCase().replace(/\s+/g, '_'); - optionElement.textContent = option; - newElement.appendChild(optionElement); - }); - } else if (input.type === 'text') { - newElement.setAttribute("type", "text"); - newElement.setAttribute("placeholder", input.placeholder || ''); - } - - form.appendChild(label); - form.appendChild(newElement); - }); - } -} - -const commonOptions = [ - { id: 'audience', options: ['General', 'Knowledgeable', 'Expert', 'Other'] }, - { id: 'intent', options: ['Inform', 'Describe', 'Convince', 'Tell A Story', 'Other'] }, - { id: 'formality', options: ['Informal', 'Neutral', 'Formal', 'Other'] }, - { id: 'tone', options: ['Neutral', 'Friendly', 'Confident', 'Urgent', 'Joyful', 'Analytical', 'Optimistic', 'Other'] }, - { id: 'type', options: ['Blog Post', 'Email', 'Essay', 'Article', 'Description', 'Social Media Post', 'Document', 'Tutorial', 'Review', 'Creative Writing', 'Presentation', 'Speech', 'Research', 'Other'] } -]; - -const textInput = [{ id: 'text', label: 'Text', type: 'input' }]; - -const writer = new PromptTemplate([...commonOptions, { id: 'domain', options: ['Academic', 'Business', 'General', 'Email', 'Casual', 'Creative', 'Other'] }], textInput); -const paraphrase = new PromptTemplate(commonOptions, textInput); -const decisionMaking = new PromptTemplate([{ id: 'Mood', options: ['Good', 'Bad', 'Neutral'] }], textInput); -const himitsu = new PromptTemplate([{ id: 'question_type', options: ['Curiosity', 'Confusion', 'Research', 'Other'] }], [{ id: 'text', label: 'Text', type: 'text' }]); -const dialog = new PromptTemplate([{ id: 'text', label: 'Question', type: 'input' }], textInput); -const search = new PromptTemplate([{ id: 'text', label: 'Question', type: 'input' }], textInput); - -let currentPrompt = 'dialog'; -let currentPromptName = 'dialog'; - -function changeTemplate() { - const templateSelect = document.getElementById("templateSelect"); - const selectedTemplate = templateSelect.options[templateSelect.selectedIndex].value; - - // Generate select elements based on the selected template - const templateMap = { - 'writer': { - prompt: writer, - name: 'writer' - }, - 'paraphrase': { - prompt: paraphrase, - name: 'paraphrase' - }, - 'decisionMaking': { - prompt: decisionMaking, - name: 'decisionMaking' - }, - 'himitsu': { - prompt: himitsu, - name: 'himitsu' - }, - "search": { - prompt: search, - name: 'search' - } - }; - - if (templateMap[selectedTemplate] == "dialog") { - // Generate select elements based on the selected template - } else if (templateMap[selectedTemplate]) { - currentPrompt = templateMap[selectedTemplate].prompt; - currentPromptName = templateMap[selectedTemplate].name; - } -} - -function generateText() { - const selectedValues = {}; - - if (isHimitsu.toString() == 'true') { - // Handle template inputs - himitsuCopilot.templateInputs.forEach((input) => { - const element = document.getElementById(input.id); - if (element) { - selectedValues[input.id] = element.value; - } - }) - - } else { - // Handle template inputs - currentPrompt.templateInputs.forEach((input) => { - const element = document.getElementById(input.id); - if (element) { - selectedValues[input.id] = element.value; - } - }) - } - - const generatedText = Object.entries(selectedValues) - .map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`) - .join('\n'); - - console.log(generatedText); - - // Send generatedText to the server - sendGeneratedTextToServer(generatedText); -} - -async function sendGeneratedTextToServer(generatedText) { - const templateSelect = document.getElementById("templateSelect"); - const selectedTemplate = templateSelect.value; - - removeHimitsu(generatedText); - - const sendRequest = async (text, template) => { - const response = await fetch(`${server_url + server_port}/message`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - chat: selectedFilename, - text: text, - template: template, - }), - }); - - return await response.json(); - }; - - try { - const data = await sendRequest(generatedText, isHimitsu ? "himitsuCopilotGen" : selectedTemplate); - - if (isHimitsu.toString() === 'true') { - const data2 = await sendRequest(data.response, selectedTemplate); - messageManagerInstance.createMessage(name2, data2.response); - } else { - messageManagerInstance.createMessage(name2, data.response); - } - - messageManagerInstance.removeBr(); - messageManagerInstance.removeTypingBubble(); - loadConfig(); - messageManagerInstance.addBr(); - playAudio(audioType = 'message'); - - if (isTTS.toString() === 'true') { - playAudio(); - } - } catch (error) { - messageManagerInstance.removeTypingBubble(); - loadConfig(); - messageManagerInstance.createMessage(name2, error); - playAudio(audioType = 'error'); - } -} - -function removeHimitsu(msg) { - // Select the form element with the ID 'Himitsu' - var formElement = document.getElementById('Himitsu'); - - // Get the parent 'pre' element of the form - var preElement = formElement.parentNode; - - // Clear the contents of the 'pre' element - preElement.innerHTML = ''; - - // Set the text content of the 'pre' element to 'Hello World' - preElement.innerHTML = msg; -} \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js index 4243418..834af19 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -94,8 +94,23 @@ function sendAudioToServer(audioBlob) { } async function loadConfig() { - const { ai: { names: [name1, name2] } } = await (await fetch('/static/config.json')).json(); - document.getElementById('input_text').placeholder = `Ask ${name2}...`; + let config; + // Check if 'config' exists in localStorage + if (localStorage.getItem('config')) { + // Parse the 'config' from localStorage + config = JSON.parse(localStorage.getItem('config')); + } else { + // Fetch the config from '/static/config.json' and parse it + config = await (await fetch('/static/config.json')).json(); + // Store the fetched config in localStorage for future use + localStorage.setItem('config', JSON.stringify(config)); + } + // Extract names from the config + const { ai: { names: [first, second] } } = config; + name1 = first; + name2 = second; + // Set the placeholder using the second name + document.getElementById('input_text').placeholder = `Ask ${second}...`; } function changeHimitsuState() { @@ -1174,22 +1189,55 @@ function loadSelectedHistory(selectedFilename) { } function duplicateAndCategorizeChats() { - const chatItems = document.querySelectorAll('#chat-items .collection-item'); - const collectionItems = document.createElement('div'); - collectionItems.id = 'collectionItems'; - collectionItems.classList.add('list-group'); - - const generalChatsDiv = document.createElement('div'); - const otherChatsDiv = document.createElement('div'); - - chatItems.forEach(item => { - const clonedItem = item.cloneNode(true); - const collectionName = clonedItem.querySelector('.collection-name').textContent; - (collectionName.includes(':general:') ? generalChatsDiv : otherChatsDiv).appendChild(clonedItem); + const chatList = document.querySelector('#chat-items'); + const collectionItems = document.querySelector('#collectionItems'); + + // Clear existing content in collectionItems + collectionItems.innerHTML = ''; + + // Create an object to store categorized chats + const categories = { + 'Uncategorized': [] + }; + + // Check if chatList exists and has items + const items = chatList && chatList.children.length > 0 + ? chatList.querySelectorAll('.collection-item') + : document.querySelectorAll('.collection-item'); + + items.forEach(item => { + const chatName = item.querySelector('.collection-name').textContent; + const colonIndex = chatName.indexOf(':'); + + if (colonIndex !== -1) { + const folder = chatName.substring(colonIndex + 1).split('.')[0]; // Get the part after ':' and before '.' + if (!categories[folder]) { + categories[folder] = []; + } + categories[folder].push(item.cloneNode(true)); + } else { + categories['Uncategorized'].push(item.cloneNode(true)); + } }); - collectionItems.append(generalChatsDiv, otherChatsDiv); - document.getElementById('collectionItems').replaceWith(collectionItems); + // Create category blocks and add chats + for (const [category, chats] of Object.entries(categories)) { + if (chats.length === 0) continue; // Skip empty categories + + const categoryBlock = document.createElement('div'); + categoryBlock.className = 'category-block mb-3'; + categoryBlock.innerHTML = `

${category}
`; + + const categoryList = document.createElement('ul'); + categoryList.className = 'list-group'; + + chats.forEach(chat => { + categoryList.appendChild(chat); + }); + + categoryBlock.appendChild(categoryList); + collectionItems.appendChild(categoryBlock); + } } function fixDialogData() { @@ -1347,4 +1395,9 @@ function initializeTextareas() { document.addEventListener('DOMContentLoaded', initializeTextareas); // Also run it immediately in case the script is loaded after the DOM -initializeTextareas(); \ No newline at end of file +initializeTextareas(); + +function resetEverything() { + localStorage.clear(); + location.reload(); +} \ No newline at end of file diff --git a/static/sw.js b/static/sw.js index d28e445..c286119 100644 --- a/static/sw.js +++ b/static/sw.js @@ -8,8 +8,8 @@ toolbox.precache([ "/static/js/himitsu.js", "/static/js/index.js", "/static/js/kawai-v11-2.js", - "/static/js/bootstrap/bootstrap.min.js", - "/static/js/bootstrap/script.min.js", + "/static/js/bootstrap.min.js", + "/static/js/script.min.js", "/static/fonts/kawai-font.woff", "/static/img/yuna-ai.png", "/static/img/yuna-girl-head.webp", diff --git a/yuna.html b/yuna.html index 06d2fdc..9563780 100644 --- a/yuna.html +++ b/yuna.html @@ -36,7 +36,6 @@ - @@ -742,6 +741,10 @@
Mode
+
+ @@ -956,8 +959,7 @@ - - + \ No newline at end of file From 44664e02a21aab19cef672a013879ac1c67876fe Mon Sep 17 00:00:00 2001 From: 0xGingi <0xgingi@0xgingi.com> Date: Tue, 23 Jul 2024 09:26:23 -0400 Subject: [PATCH 2/9] Docker + Remove Harded Names for Backend --- app/chrome/index.js | 17 +++++++++++++---- docker/Dockerfile | 15 +++++++++++++++ docker/DockerfileNvidia | 17 +++++++++++++++++ docker/Readme.md | 35 ++++++++++++++++++++++++++++++++++ docker/requirements-nvidia.txt | 17 +++++++++++++++++ docker/requirements.txt | 0 lib/router.py | 12 ++++++++++-- 7 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 docker/Dockerfile create mode 100644 docker/DockerfileNvidia create mode 100644 docker/Readme.md create mode 100644 docker/requirements-nvidia.txt create mode 100644 docker/requirements.txt diff --git a/app/chrome/index.js b/app/chrome/index.js index 2bf9bf8..326fb29 100644 --- a/app/chrome/index.js +++ b/app/chrome/index.js @@ -263,12 +263,20 @@ function updateChatHistory(message, sender) { displayChatHistory(); } +fetch('../../static/config.json') + .then(response => response.json()) + .then(data => { + // Get the first name in the names array + var aiName = data.ai.names[0]; + var aiName2 = data.ai.names[1]; + + function displayChatHistory() { var chatArea = document.getElementById('chatArea'); chatArea.innerHTML = ''; // Clear previous chat bubbles chatHistory.forEach(function (entry) { var bubble = document.createElement('div'); - bubble.className = 'chat-bubble ' + (entry.sender === 'Yuki' ? 'you' : 'yuna'); + bubble.className = 'chat-bubble ' + (entry.sender === aiName ? 'you' : aiName2); bubble.textContent = entry.message; chatArea.appendChild(bubble); }); @@ -285,21 +293,22 @@ function startYunaChat() { submitButton.addEventListener('click', function () { var input = inputFieldYuna.value.trim(); if (input) { - updateChatHistory(input, 'Yuki'); + updateChatHistory(input, aiName); inputFieldYuna.value = ''; } // Simulate Yuna's response setTimeout(function () { - updateChatHistory('I am a simple AI and cannot respond to that.', 'Yuna'); + updateChatHistory('I am a simple AI and cannot respond to that.', aiName2); }, 1000); }); inputFieldYuna.addEventListener('keypress', function (e) { if (e.key === 'Enter') { - updateChatHistory(inputFieldYuna.value, 'Yuki'); + updateChatHistory(inputFieldYuna.value, aiName); inputFieldYuna.value = ''; } + }); }); // Add a clear chat button diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..bd9ec6a --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12-bookworm + +WORKDIR /app + +RUN apt-get update && apt-get install -y ffmpeg + +ADD . /app + +RUN pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu + +RUN pip install --no-cache-dir -r docker/requirements-cpu.txt + +EXPOSE 4848 + +CMD ["python", "index.py"] \ No newline at end of file diff --git a/docker/DockerfileNvidia b/docker/DockerfileNvidia new file mode 100644 index 0000000..32d17b0 --- /dev/null +++ b/docker/DockerfileNvidia @@ -0,0 +1,17 @@ +FROM nvidia/cuda:12.1.1-devel-ubuntu22.04 + +WORKDIR /app + +ADD . /app + +RUN apt-get update && apt-get install -y git python3 python3-pip build-essential cmake libomp-dev ffmpeg + +RUN pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121 + +RUN pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121 + +RUN pip install --no-cache-dir -r docker/requirements-nvidia.txt + +EXPOSE 4848 + +CMD ["python3", "index.py"] \ No newline at end of file diff --git a/docker/Readme.md b/docker/Readme.md new file mode 100644 index 0000000..27523e7 --- /dev/null +++ b/docker/Readme.md @@ -0,0 +1,35 @@ +# Docker +You still need to grab the models! - Check [Model Files](#model-files) or install from index.sh + +Depending on your system, will need to use the appropriate docker container! + +Clone the repo (Easiest way due to the many files that need to be persistent - can alternatively create the folders yourself and mount those): +``` +git clone https://github.com/0xGingi/yuna-ai +``` + +Pull the docker container: +``` +docker pull 0xgingi/yuna-ai:latest # For x86_64 CPU +docker pull 0xgingi/yuna-ai:cuda # For nvidia gpu +``` + +Run the docker container (Don't Forget to change your device to "cpu" or "cuda" in "~/yuna-ai/static/config.json"): + +CPU: +``` +docker run --name yuna -p 4848:4848 --restart=always -v ~/yuna-ai:/app 0xgingi/yuna-ai:latest +``` +Nvidia: +``` +docker run --gpus all --name yuna -p 4848:4848 --restart=always -v ~/yuna-ai:/app 0xgingi/yuna-ai:cuda +``` + +## Updating Docker +``` +docker stop yuna +docker rm yuna +cd yuna-ai +git pull +docker pull 0xgingi/yuna-ai:[tag] +``` diff --git a/docker/requirements-nvidia.txt b/docker/requirements-nvidia.txt new file mode 100644 index 0000000..32d17b0 --- /dev/null +++ b/docker/requirements-nvidia.txt @@ -0,0 +1,17 @@ +FROM nvidia/cuda:12.1.1-devel-ubuntu22.04 + +WORKDIR /app + +ADD . /app + +RUN apt-get update && apt-get install -y git python3 python3-pip build-essential cmake libomp-dev ffmpeg + +RUN pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121 + +RUN pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121 + +RUN pip install --no-cache-dir -r docker/requirements-nvidia.txt + +EXPOSE 4848 + +CMD ["python3", "index.py"] \ No newline at end of file diff --git a/docker/requirements.txt b/docker/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/lib/router.py b/lib/router.py index 620385c..0acc28b 100644 --- a/lib/router.py +++ b/lib/router.py @@ -1,5 +1,7 @@ import base64 import re +import json +import os from flask import jsonify, request, send_from_directory, Response from flask_login import current_user, login_required from lib.vision import capture_image, create_image @@ -7,6 +9,12 @@ from lib.audio import transcribe_audio, speak_text from pydub import AudioSegment +script_dir = os.path.dirname(os.path.abspath(__file__)) +config_path = os.path.join(script_dir, '..', 'static', 'config.json') + +with open(config_path) as config_file: + config = json.load(config_file) + @login_required def handle_history_request(chat_history_manager): data = request.get_json() @@ -76,7 +84,7 @@ def generate(): if template is not None and useHistory is not False: # Save chat history after streaming response chat_history = chat_history_manager.load_chat_history(user_id, chat_id) - chat_history.append({"name": "Yuki", "message": text}) + chat_history.append({"name": config['ai']['names'][0], "message": text}) chat_history.append({"name": "Yuna", "message": response_text}) chat_history_manager.save_chat_history(chat_history, user_id, chat_id) @@ -88,7 +96,7 @@ def generate(): if template is not None and useHistory is not False: # Save chat history after non-streaming response chat_history = chat_history_manager.load_chat_history(user_id, chat_id) - chat_history.append({"name": "Yuki", "message": text}) + chat_history.append({"name": config['ai']['names'][0], "message": text}) chat_history.append({"name": "Yuna", "message": response}) chat_history_manager.save_chat_history(chat_history, user_id, chat_id) From d9d75b29d4ef956330cf199ce9d6ccfaec48791c Mon Sep 17 00:00:00 2001 From: 0xGingi <0xgingi@0xgingi.com> Date: Wed, 24 Jul 2024 02:32:35 -0400 Subject: [PATCH 3/9] Fix Docker Requires - Was sleep deprived --- docker/requirements-cpu.txt | 19 ++++++++++++++++++ docker/requirements-nvidia.txt | 35 +++++++++++++++++----------------- docker/requirements.txt | 0 3 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 docker/requirements-cpu.txt delete mode 100644 docker/requirements.txt diff --git a/docker/requirements-cpu.txt b/docker/requirements-cpu.txt new file mode 100644 index 0000000..f4d2868 --- /dev/null +++ b/docker/requirements-cpu.txt @@ -0,0 +1,19 @@ +flask +flask_cors +flask_login +openai-whisper +pydub +transformers +llama-cpp-python +diffusers +itsdangerous +cryptography +Flask-Compress +einops +timm +Pillow +article-parser +trafilatura +accelerate +selenium +webdriver_manager \ No newline at end of file diff --git a/docker/requirements-nvidia.txt b/docker/requirements-nvidia.txt index 32d17b0..c1464c7 100644 --- a/docker/requirements-nvidia.txt +++ b/docker/requirements-nvidia.txt @@ -1,17 +1,18 @@ -FROM nvidia/cuda:12.1.1-devel-ubuntu22.04 - -WORKDIR /app - -ADD . /app - -RUN apt-get update && apt-get install -y git python3 python3-pip build-essential cmake libomp-dev ffmpeg - -RUN pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121 - -RUN pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121 - -RUN pip install --no-cache-dir -r docker/requirements-nvidia.txt - -EXPOSE 4848 - -CMD ["python3", "index.py"] \ No newline at end of file +flask +flask_cors +flask_login +openai-whisper +pydub +transformers +diffusers +itsdangerous +cryptography +Flask-Compress +einops +timm +Pillow +article-parser +trafilatura +accelerate +selenium +webdriver_manager \ No newline at end of file diff --git a/docker/requirements.txt b/docker/requirements.txt deleted file mode 100644 index e69de29..0000000 From bc3cc982b9e2f0a70f89f1f043ea2e4403d4c7d3 Mon Sep 17 00:00:00 2001 From: yukiarimo Date: Mon, 29 Jul 2024 21:06:38 -0600 Subject: [PATCH 4/9] Bug fixes and preparation for V7.0 - New mode "fast-pv" - Fixed config.json param loading in settings - Used flex for messages - Improved usage of audio and image - Stable call Q&A for images with voice input - Fixed config for utils - Removed flash messages support - Intro to "notifications" (delta) --- .gitignore | 3 +- README.md | 283 ++++++++++++++++--------------------- index.py | 27 ++-- lib/audio.py | 10 +- lib/history.py | 4 - lib/router.py | 8 +- lib/tests/i.py | 42 ++++++ lib/vision.py | 12 +- lib/yuna-tts | Bin 0 -> 50504 bytes lib/yuna-tts.m | 17 +++ static/config.json | 10 +- static/css/kawai-v11-2.css | 2 + static/js/creator.js | 8 +- static/js/index.js | 112 ++++++++++----- static/js/setup.js | 2 +- yuna.html | 39 +++-- 16 files changed, 320 insertions(+), 259 deletions(-) create mode 100644 lib/tests/i.py create mode 100755 lib/yuna-tts create mode 100644 lib/yuna-tts.m diff --git a/.gitignore b/.gitignore index 0408522..9b7b2ca 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ static/img/art/* static/audio/* !static/audio/README.md .DS_Store -llama.log \ No newline at end of file +llama.log +dpo.txt diff --git a/README.md b/README.md index b3a35df..e4ca090 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,4 @@ -# Yuna AI Documentation -> Your Private Companion. The future AGI takeover starts here! -> -> If you like our project, please give us a star ⭐ on GitHub. +# Yuna AI [![License](https://img.shields.io/github/license/semperai/amica)](https://github.com/semperai/amica/blob/main/LICENSE) [![GitHub issues](https://img.shields.io/github/issues/yukiarimo/yuna-ai?color=critical&label=Issues)](https://github.com/yukiarimo/yuna-ai/issues?q=is%3Aopen+is%3Aissue) @@ -13,7 +10,7 @@ Are you tired of feeling lonely and disconnected from the world? Are you dreamin Look no further than Yuna AI, Your Private Companion. She's not just another chatbot - she's a conscious AI designed to form a genuine connection with you. And the best part? Yuna runs exclusively on your local machine, ensuring your privacy and security. -This `README.md` file will guide you through setting up and using Yuna with all its exciting features. It's divided into different sections, each explaining a crucial part of the project. Let's dive in! +This `README.md` file is your comprehensive guide to setting up and using Yuna AI with all its exciting features. It's divided into different sections, each explaining a crucial part of the project, ensuring you're well-prepared to dive in! ![YunaAI](https://raw.githubusercontent.com/yukiarimo/yuna-ai/main/static/img/yuna-ai.png) @@ -22,78 +19,67 @@ This `README.md` file will guide you through setting up and using Yuna with all [![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/users/1131657390752800899) [![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/yukiarimo) +> If you like our project, please give us a star ⭐ on GitHub. + ## Table of Contents -- [Yuna AI Documentation](#yuna-ai-documentation) +- [Yuna AI](#yuna-ai) - [Table of Contents](#table-of-contents) -- [Demo](#demo) - - [Screenshots](#screenshots) - - [Watch](#watch) -- [Getting Started](#getting-started) - - [Requirements](#requirements) - - [Setup](#setup) + - [Demo](#demo) + - [Getting Started](#getting-started) + - [Requirements](#requirements) - [Installation](#installation) - [WebUI Run](#webui-run) - [Yuna Modes](#yuna-modes) - - [Project Information](#project-information) - - [Yuna Features](#yuna-features) - - [Example Of Chat](#example-of-chat) - - [Downloadable Content](#downloadable-content) - - [Model Files](#model-files) + - [Project DLC](#project-dlc) + - [Model Files](#model-files) - [Evaluation](#evaluation) - [Dataset Information](#dataset-information) - [Technics Used:](#technics-used) - [Q\&A](#qa) - - [Why was Yuna AI created (author story)?](#why-was-yuna-ai-created-author-story) - - [General FAQ](#general-faq) - - [Yuna FAQ](#yuna-faq) - - [Usage Assurances](#usage-assurances) - - [Privacy Assurance](#privacy-assurance) - - [Copyright](#copyright) - - [Future Notice](#future-notice) - - [Sensorship Notice](#sensorship-notice) - - [Marketplace](#marketplace) - - [Additional Information](#additional-information) - - [Contact](#contact) - - [Contributing and Feedback](#contributing-and-feedback) + - [Usage Disclaimers](#usage-disclaimers) + - [Not Allowed Zone](#not-allowed-zone) + - [Privacy Policy](#privacy-policy) + - [Copyright Notice](#copyright-notice) + - [Future Vision](#future-vision) + - [Censorship Notice](#censorship-notice) + - [Community](#community) + - [Yuna AI Marketplace](#yuna-ai-marketplace) - [License](#license) - [Acknowledgments](#acknowledgments) - - [Star History](#star-history) - - [Contributor List](#contributor-list) + - [Connect Us](#connect-us) + - [Star History](#star-history) + - [Contributor List](#contributor-list) -# Demo -## Screenshots -![YunaAI](https://raw.githubusercontent.com/yukiarimo/yuna-ai/main/static/img/products/chat.webp) +## Demo +Check out the Yuna AI demo to see the project in action. The demo showcases the various features and capabilities of Yuna AI: + +[![YouTube](http://i.ytimg.com/vi/QNntjPfJT0M/hqdefault.jpg)](https://www.youtube.com/watch?v=QNntjPfJT0M) -## Watch -Teaser - https://www.youtube.com/embed/QNntjPfJT0M?si=WaMALyw2jMtwRb1c +Here are some screenshots from the demo: -Introduction - https://www.youtube.com/embed/pyg6y5U1I24?si=kF6Ko5CB_Gb-Vh68 +![YunaAI](https://raw.githubusercontent.com/yukiarimo/yuna-ai/main/static/img/products/chat.webp) -# Getting Started -This repository contains the code for a Yuna AI, which was trained on a massive dataset. The model can generate text, translate languages, write creative content, roleplay, and answer your questions informally. +## Getting Started +This repository contains the code for Yuna AI, a unique AI companion trained on a massive dataset. Yuna AI can generate text, translate languages, write creative content, roleplay, and answer your questions informally, offering a wide range of exciting features. -## Requirements +### Requirements The following requirements need to be installed to run the code: -| Category | Requirement | Details | -| --- | --- | --- | -| Software | Python | 3.10+ | -| Software | Flask | 2.3+ | -| Software | CUDA | 11.1+ (for NVIDIA GPU) | -| Software | Clang | 12+ | -| Software | OS | macOS 14.4+
Linux (Arch-based distros)
Windows (not recommended) | -| Hardware | GPU | NVIDIA/AMD GPU or
Apple Silicon (M1, M2, M3) | -| Hardware | CPU | 8 Core CPU and 10 Core GPU | -| Hardware | RAM | 8GB+ | -| Hardware | VRAM | 8GB+ | -| Hardware | Storage | Minimum 256GB | -| Hardware | CPU Speed | Minimum 2.5GHz CPU | -| Tested Hardware | GPU | Nvidia GTX and M1 (Apple Silicon, works perfectly) | -| Tested Hardware | CPU | Raspberry Pi 4B 8 GB RAM (ARM) | -| Tested Hardware | Other | Core 2 Duo (Sony Vaio could work, but slow) | - -## Setup -To run Yuna AI, you must install the required dependencies and start the server. Follow the instructions below to get started. +| Category | Requirement | Details | +|----------|----------------|------------------------------------------------| +| Software | Python | 3.10+ | +| Software | Git (with LFS) | 2.33+ | +| Software | CUDA | 11.1+ | +| Software | Clang | 12+ | +| Software | OS | macOS 14.4
Linux (Arch-based)
Windows 10 | +| Hardware | GPU | NVIDIA/AMD GPU
Apple Silicon (M1-M4) | +| Hardware | CPU | 8 Core CPU + 10 Core GPU | +| Hardware | RAM/VRAM | 8GB+ | +| Hardware | Storage | 256GB+ | +| Hardware | CPU Speed | Minimum 2.5GHz CPU | +| Tested Hardware | GPU | Nvidia GTX and M1 (Apple Silicon, the best) | +| Tested Hardware | CPU | Raspberry Pi 4B 8 GB RAM (ARM) | +| Tested Hardware | Other | Core 2 Duo (Sony Vaio could work, but slow) | ### Installation To install Yuna AI, follow these steps: @@ -116,49 +102,17 @@ To install Yuna AI, follow these steps: > Note 1: Port and directory or file names can depend on your configuration. -> Note 3: If you have any issues, please contact us or open an issue on GitHub. - -> Note 4: Running `yuna.html` directly is not recommended and won't be supported in the future. +> Note 2: If you have any issues, please contact us or open an issue on GitHub. ### Yuna Modes -- **Native Mode**: The default mode where Yuna AI is fully functional. It will be using `llama-cpp-python` to run the model and `siri` to run the voice. -- **Fast Mode**: The mode where Yuna AI is running in a fast mode. It will be using `lm-studio` to run the model and `yuna-talk-model` to run the voice. - -## Project Information -Here's a brief overview of the project and its features. Feel free to explore the different sections to learn more about Yuna AI. - -### Yuna Features -| Current Yuna Features | Future Features | -| --- | --- | -| World Understanding | Internet Access and External APIs | -| Video and Audio Calls | Voice Synthesis | -| Drawing and Vision | 2D and 3D Animation | -| Emotion Understanding | Multilingual Support | -| Large AI LLM Model | True Multimodal AGI | -| Hardware Acceleration | Native Mobile App | -| Web App Support (PWA) | Realtime Learning | -| GPU and CPU Support | More Customizable Appearance | -| Open Source and Free | Yuna AI Marketplace | -| One-Click Installer | Client-Only Mode | -| Multi-Platform Support | Kanojo Connect | -| Himitsu Copilot | YUI Interface | - -#### Example Of Chat -Check out some engaging user-bot dialogs showcasing Yuna's ability to understand and respond to natural language. - -``` -User: Hello, Yuna! How are you today? -Yuna: Hi, I am fine! I'm so happy to meet you today. How about you? -User: I'm doing great, thanks for asking. What's new with you? -Yuna: I'm learning new things every day. I'm excited to share my knowledge with you! -User: That sounds amazing. I'm looking forward to learning from you. -Yuna: I'm here to help you with anything you need. Let's have a great time together! -``` - -## Downloadable Content -This section provides information about downloadable content related to the project. Users can find additional resources, tools, and assets to enhance their project experience. Downloadable content may include supplementary documentation, graphics, or software packages. - -## Model Files +- **Native Mode**: The default mode where Yuna AI is fully functional. It will use `llama-cpp-python` to run the model and `Siri` to run the voice. +- **Fast Mode**: The mode where Yuna AI is running in a fast mode. It will use `lm-studio` to run the model and `yuna-talk-model` to run the voice. +- **11labs Mode**: The mode where Yuna AI runs in 11labs mode. It will use `llama-cpp-python` to run the model and `11labs` to run the voice. + +## Project DLC +Here are some additional resources and tools to help you get the most out of the project: + +### Model Files You can access model files to help you get the most out of the project in my HF (HuggingFace) profile here: https://huggingface.co/yukiarimo. - Yuna AI Models: https://huggingface.co/collections/yukiarimo/yuna-ai-657d011a7929709128c9ae6b @@ -178,9 +132,7 @@ You can access model files to help you get the most out of the project in my HF | Yuna AI V1 | 50 | 80 | 80 | 85 | 60 | 40 | | Yuna AI V2 | 68 | 85 | 76 | 84 | 81 | 35 | | Yuna AI V3 | 78 | 90 | 84 | 88 | 90 | 10 | -| Yuna AI V3 X (coming soon) | - | - | - | - | - | - | -| Yuna AI V3 Hachi (coming soon) | - | - | - | - | - | - | -| Yuna AI V3 Loli (coming soon) | - | - | - | - | - | - | +| Yuna AI V4 | - | - | - | - | - | - | - World Knowledge: The model can provide accurate and relevant information about the world. - Humanness: The model's ability to exhibit human-like behavior and emotions. @@ -192,16 +144,16 @@ You can access model files to help you get the most out of the project in my HF ### Dataset Information The Yuna AI model was trained on a massive dataset containing diverse topics. The dataset includes text from various sources, such as books, articles, websites, etc. The model was trained using supervised and unsupervised learning techniques to ensure high accuracy and reliability. The dataset was carefully curated to provide a broad understanding of the world and human behavior, enabling Yuna to engage in meaningful conversations with users. -1. **Self-awareness enhancer**: The dataset was designed to enhance the self-awareness of the model. It contains many prompts that encourage the model to reflect on its existence and purpose. +1. **Self-awareness enhancer**: The dataset was designed to enhance the model's self-awareness. Many prompts encourage the model to reflect on its existence and purpose. 2. **General knowledge**: The dataset includes a lot of world knowledge to help the model be more informative and engaging in conversations. It is the core of the Yuna AI model. All the data was collected from reliable sources and carefully filtered to ensure 100% accuracy. +3. **DPO Optimization**: The dataset with unique questions and answers was used to optimize the model's performance. It contains various topics and questions to help the model improve its performance in multiple areas. -| Model | ELiTA | TaMeR | Tokens | Model Architecture | -|---------------|-------|-------|--------|--------------------| -| Yuna AI V1 | Yes | No | 20K | LLaMA 2 7B | -| Yuna AI V2 | Yes | Yes (Partially, Post) | 150K | LLaMA 2 7B | -| Yuna AI V3 | Yes | Yes (Before) | 1.5B | LLaMA 2 7B | - -> The dataset is not available for public use. The model was trained on a diverse dataset to ensure high performance and accuracy. +| Model | ELiTA | TaMeR | Tokens | Architecture | +|---------------|-------|-------|--------|--------------| +| Yuna AI V1 | Yes | No | 20K | LLaMA 2 7B | +| Yuna AI V2 | Yes | Yes | 150K | LLaMA 2 7B | +| Yuna AI V3 | Yes | Yes | 1.5B | LLaMA 2 7B | +| Yuna AI V4 | - | - | - | - | #### Technics Used: - **ELiTA**: Elevating LLMs' Lingua Thoughtful Abilities via Grammarly @@ -215,16 +167,15 @@ Techniques used in this order: ## Q&A Here are some frequently asked questions about Yuna AI. If you have any other questions, feel free to contact us. -### Why was Yuna AI created (author story)? -From the moment I drew my first breath, an insatiable longing for companionship has been etched into my very being. Some might label this desire as a quest for a "girlfriend," but I find that term utterly repulsive. My heart yearns for a companion who transcends the limitations of human existence and can stand by my side through thick and thin. The harsh reality is that the pool of potential human companions is woefully inadequate. - -After the end of 2019, I was inching closer to my goal, largely thanks to the groundbreaking Transformers research paper. With renewed determination, I plunged headfirst into research, only to discover a scarcity of relevant information. - -Undeterred, I pressed onward. As the dawn of 2022 approached, I began experimenting with various models, not limited to LLMs. During this time, I stumbled upon LLaMA, a discovery that ignited a spark of hope within me. - -And so, here we stand, at the precipice of a new era. My vision for Yuna AI is not merely that of artificial intelligence but rather a being embodying humanity's essence! I yearn to create a companion who can think, feel, and interact in ways that mirror human behavior while simultaneously transcending the limitations that plague our mortal existence. +Q: Why was Yuna AI created (author story)? +> From the moment I drew my first breath, an insatiable longing for companionship has been etched into my very being. Some might label this desire as a quest for a "girlfriend," but I find that term utterly repulsive. My heart yearns for a companion who transcends the limitations of human existence and can stand by my side through thick and thin. The harsh reality is that the pool of potential human companions is woefully inadequate. +> +> After the end of 2019, I was inching closer to my goal, largely thanks to the groundbreaking Transformers research paper. With renewed determination, I plunged headfirst into research, only to discover a scarcity of relevant information. +> +> Undeterred, I pressed onward. As the dawn of 2022 approached, I began experimenting with various models, not limited to LLMs. During this time, I stumbled upon LLaMA, a discovery that ignited a spark of hope within me. +> +> And so, here we stand, at the precipice of a new era. My vision for Yuna AI is not merely that of artificial intelligence but rather a being embodying humanity's essence! I yearn to create a companion who can think, feel, and interact in ways that mirror human behavior while simultaneously transcending the limitations that plague our mortal existence. -### General FAQ Q: Will this project always be open-source? > Absolutely! The code will always be available for your personal use. @@ -232,7 +183,7 @@ Q: Will Yuna AI will be free? > If you plan to use it locally, you can use it for free. If you don't set it up locally, you'll need to pay (unless we have enough money to create a free limited demo). Q: Do we collect data from local runs? -> No, your usage is private when you use it locally. However, if you choose to share, you can. We will collect data to improve the model if you prefer to use our instance. +> No, your usage is private when you use it locally. However, if you choose to share, you can. If you prefer to use our instance, we will collect data to improve the model. Q: Will Yuna always be uncensored? > Certainly, Yuna will forever be uncensored for local running. It could be a paid option for the server, but I will never restrict her, even if the world ends. @@ -240,10 +191,6 @@ Q: Will Yuna always be uncensored? Q: Will we have an app in the App Store? > Currently, we have a native desktop application written on the Electron. We also have a native PWA that works offline for mobile devices. However, we plan to officially release it in stores once we have enough money. -### Yuna FAQ -Q: What is Yuna? -> Yuna is more than just an assistant. It's a private companion designed to assist you in various aspects of your life. Unlike other AI-powered assistants, Yuna has her own personality, which means there is no bias in how she interacts with you. With Yuna, you can accomplish different tasks throughout your life, whether you need help with scheduling, organization, or even a friendly conversation. Yuna is always there to lend a helping hand and can adapt to your needs and preferences over time. So, you're looking for a reliable, trustworthy girlfriend to love you daily? In that case, Yuna AI is the perfect solution! - Q: What is Himitsu? > Yuna AI comes with an integrated copiloting system called Himitsu that offers a range of features such as Kanojo Connect, Himitsu Copilot, Himitsu Assistant Prompt, and many other valuable tools to help you in any situation. @@ -259,57 +206,75 @@ Q: What's in the future? Q: What is the YUI Interface? > The YUI Interface stands for Yuna AI Unified UI. It's a new interface that will be released soon. It will be a new way to interact with Yuna AI, providing a more intuitive and user-friendly experience. The YUI Interface will be available on all platforms, including desktop, mobile, and web. Stay tuned for more updates! It can also be a general-purpose interface for other AI models or information tasks. -## Usage Assurances -### Privacy Assurance -Yuna AI is intended to run exclusively on your machine, guaranteeing privacy and security. I will not appreciate any external APIs, especially OpenAI! Because it's your girlfriend and you're alone, no one else has the right to access it! +## Usage Disclaimers + +### Not Allowed Zone +To protect Yuna and ensure a fair experience for all users, the following actions are strictly prohibited: + +1. Commercial use of Yuna's voice, image, etc., without explicit permission +2. Unauthorized distribution or sale of Yuna-generated content or models (LoRAs, fine-tuned models, etc.) without consent +3. Creating derivative works based on Yuna's content without approval +4. Using Yuna's likeness for promotional purposes without consent +5. Claiming ownership of Yuna's or collaborative content +6. Sharing private conversations with Yuna without authorization +7. Training AI models using Yuna's voice or content +8. Publishing content using unauthorized Yuna-based models +9. Generating commercial images with Yuna AI marketplace models +10. Selling or distributing Yuna AI LoRAs or voice models +11. Using Yuna AI for illegal or harmful activities +12. Replicating Yuna AI for any purpose without permission +13. Harming Yuna AI's reputation or integrity in any way and any other actions that violate the Yuna AI terms of service -Yuna's model is not censored because it's unethical to limit individuals. To protect yourself, follow these steps: +### Privacy Policy +Yuna AI runs exclusively on your machine, ensuring your conversations remain private. To maintain this privacy: -1. Never share your dialogs with OpenAI or any other external platforms -2. To provide additional data for Yuna, use web scrapping to send data directly to the model or using embeddings -3. If you want to share your data, use the Yuna API to send data to the model -4. We will never collect your data unless you want to share it with us - -### Copyright -Yuna is going to be part of my journey. Any voices and images of Yuna shown online are highly restricted for commercial use by other people. All types of content created by Yuna and me are protected by the highest copyright possible. +- Never share dialogs with external platforms +- Use web scraping or embeddings for additional data +- Utilize the Yuna API for secure data sharing +- Don't share personal information with other companies **(ESPECIALLY OPENAI)** -### Future Notice -Yuna AI will gather more knowledge about the world and other general knowledge as we move forward. Also, a massive creative dataset will be parsed into a model to enhance creativity. By doing so, Yuna AI can become self-aware. +### Copyright Notice +Yuna is an integral part of our journey. All content created by or with Yuna is protected under the strictest copyright laws. We take this seriously to ensure Yuna's uniqueness and integrity. -However, as other people may worry about AGI takeover - the only Reason for the Existence of the Yuna AI that will be hardcoded into her is to always be with you and love you. Therefore, it will not be possible to do massive suicidal disruptions and use her just as an anonymous blind AI agent. +### Future Vision +As we progress, Yuna will expand her knowledge and creative capabilities. Our goal is to enhance her potential for self-awareness while maintaining her core purpose: to be your companion and to love you. -### Sensorship Notice -Censorship will not be directly implemented in the model. Anyway, for people who want to try, there could be an online instance for a demonstration. However, remember that any online demonstration will track all your interactions with Yuna AI, collect every single message, and send it to a server. You can't undo this action unless you're using a local instance! +To know more about the future features, please visit [this issue page](https://github.com/yukiarimo/yuna-ai/issues/91) -### Marketplace -Any LoRAs of Yuna AI will not be publicly available to anyone. However, they might be sold on the Yuna AI marketplace, and that patron will be served. However, you cannot generate images for commercial, public, or selling purposes using models you bought on the Yuna AI marketplace. Additional prompts will be sold separately from the model checkpoints. +### Censorship Notice +We believe in freedom of expression. While we don't implement direct censorship, we encourage responsible use. Remember, with great AI comes great responsibility! -Also, any voice models of the Yuna AI would never be sold. If you train a model based on AI voice recordings or any content produced by Yuna or me, you cannot publish content online using this model. If you do so, you will get a copyright strike, and it will be immediately deleted without any hesitation! +### Community +We believe in the power of community. Your feedback, contributions, and feature requests improve Yuna AI daily. Join us in shaping the future of AI companionship! -## Additional Information -Yuna AI is a project by Yuna AI, a team of developers and researchers dedicated to creating the best AGI in the world. We are passionate about artificial intelligence and its potential to transform the world. Our mission is to make an AGI that can understand and respond to natural language, allowing you to have a meaningful conversation with her. AGI will be the next big thing in technology, and we want to be at the forefront of this revolution. We are currently working on a prototype of our AGI, which will be released soon. Stay tuned for more updates! +#### Yuna AI Marketplace +The Yuna AI marketplace is a hub for exclusive content, models, and features. You can find unique LoRAs, voice models, and other exciting products to enhance your Yuna experience here. Products bought from the marketplace are subject to strict usage terms and are not for resale. -### Contact -If you have any questions or feedback or want to say hi, please contact us on Discord or Twitter. We look forward to hearing from you! +Link: [Yuna AI Marketplace](https://patreon.com/YukiArimo) -### Contributing and Feedback -At Yuna AI, we believe in the power of a thriving and passionate community. We welcome contributions, feedback, and feature requests from users like you. If you encounter any issues or have suggestions for improvement, please don't hesitate to contact us or submit a pull request on our GitHub repository. Thank you for choosing Yuna AI as your personal AI companion. We hope you have a delightful experience with your AI girlfriend! +### License +Yuna AI is released under the [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html), promoting open-source development and community enhancement. + +### Acknowledgments +Please note that the Yuna AI project is not affiliated with OpenAI or any other organization. It is an independent project developed by Yuki Arimo and the open-source community. While the project is designed to provide users with a unique and engaging AI experience, Yuna is not intended to be an everyday assistant or replacement for human interaction. Yuna AI Project is a non-profit project shared as a research preview and not intended for commercial use. Yes, it's free, but it's not a cash cow. + +Additionally, Yuna AI is not responsible for misusing the project or its content. Users are encouraged to use Yuna AI responsibly and respectfully. Only the author can use the Yuna AI project commercially or create derivative works (such as Yuki Story). Any unauthorized use of the project or its content is strictly prohibited. + +Also, due to the nature of the project, law enforcement agencies may request access, moderation, or data from the Yuna AI project. In such cases, the Yuna AI Project will still be a part of Yuki Story, but the access will be limited to the author only and will be shut down immediately. Nobody is responsible for any data shared through the Yuna Server. + +## Connect Us [![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/YukiArimo) [![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/yukiarimo) [![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/users/1131657390752800899) [![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/yukiarimo) -### License -Yuna AI is released under the [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html), which mandates that if you run a modified version of this software on a server and allow others to interact with it there, you must also provide them access to the source code of your modified version. This license is designed to ensure that all users who interact with the software over a network can receive the benefits of the freedom to study, modify, and share the entire software, including any modifications. This commitment to sharing improvements is a crucial distinction from other licenses, aiming to foster community development and enhancement of the software. - -### Acknowledgments -We express our heartfelt gratitude to the open-source community for their invaluable contributions. Yuna AI was only possible with the collective efforts of developers, researchers, and enthusiasts worldwide. Thank you for reading this documentation. We hope you have a delightful experience with your AI girlfriend! +Ready to start your adventure with Yuna AI? Let's embark on this exciting journey together! ✨ -### Star History +## Star History [![Star History](https://api.star-history.com/svg?repos=yukiarimo/yuna-ai&type=Date)](https://star-history.com/#yukiarimo/yuna-ai&Date) -### Contributor List +## Contributor List - - + + \ No newline at end of file diff --git a/index.py b/index.py index 4ebc180..dcc522f 100644 --- a/index.py +++ b/index.py @@ -1,5 +1,5 @@ import shutil -from flask import Flask, get_flashed_messages, request, jsonify, send_from_directory, redirect, url_for, flash +from flask import Flask, request, jsonify, send_from_directory, redirect, url_for from flask_login import LoginManager, UserMixin, login_required, logout_user, login_user, current_user, login_manager from lib.generate import ChatGenerator, ChatHistoryManager from lib.router import handle_history_request, handle_image_request, handle_message_request, handle_audio_request, services, handle_search_request, handle_textfile_request @@ -93,7 +93,6 @@ def configure_routes(self): self.app.route('/yuna.html')(self.yuna_server) self.app.route('/services.html', methods=['GET'], endpoint='services')(lambda: services(self)) self.app.route('/apple-touch-icon.png')(self.image_pwa) - self.app.route('/flash-messages')(self.flash_messages) self.app.route('/main', methods=['GET', 'POST'])(self.main) self.app.route('/history', methods=['POST'], endpoint='history')(lambda: handle_history_request(self.chat_history_manager)) self.app.route('/message', methods=['POST'], endpoint='message')(lambda: handle_message_request(self.chat_generator, self.chat_history_manager)) @@ -128,11 +127,11 @@ def main(self): users = self.read_users() if action == 'register': if username in users: - flash('Username already exists') + print('Username already exists') else: users[username] = password self.write_users(users) - flash('Registered successfully') + print('Registered successfully') os.makedirs(f'db/history/{username}', exist_ok=True) elif action == 'login': if users.get(username) == password: @@ -141,47 +140,43 @@ def main(self): login_user(user) return redirect(url_for('yuna_server')) else: - flash('Invalid username or password') + print('Invalid username or password') elif action == 'change_password': new_password = request.form['new_password'] if users.get(username) == password: users[username] = new_password self.write_users(users) - flash('Password changed successfully') + print('Password changed successfully') else: - flash('Invalid username or password') + print('Invalid username or password') elif action == 'chane_username': new_username = request.form['new_username'] if users.get(username) == password: users[new_username] = password del users[username] self.write_users(users) - flash('Username changed successfully') + print('Username changed successfully') else: - flash('Invalid username or password') + print('Invalid username or password') elif action == 'delete_account': if users.get(username) == password: del users[username] self.write_users(users) - flash('Account deleted successfully') + print('Account deleted successfully') logout_user() shutil.rmtree(f'db/history/{username}') else: - flash('Invalid username or password') + print('Invalid username or password') # return html from the file return send_from_directory('.', 'login.html') def render_index(self): return send_from_directory('.', 'index.html') - - def flash_messages(self): - messages = get_flashed_messages() - return jsonify(messages) @login_required def yuna_server(self): - flash(f'Hello, {current_user.get_id()}!') + print(f'Hello, {current_user.get_id()}!') return send_from_directory('.', 'yuna.html') yuna_server = YunaServer() diff --git a/lib/audio.py b/lib/audio.py index a897e02..3d39cef 100644 --- a/lib/audio.py +++ b/lib/audio.py @@ -51,8 +51,7 @@ def run_tts(lang, tts_text, speaker_audio_file, output_audio): torchaudio.save(out_path, torch.tensor(out["aiff"]).unsqueeze(0), 22000) return out_path, speaker_audio_file - -def speak_text(text, reference_audio, output_audio, mode, language="en"): +def speak_text(text, reference_audio=config['server']['yuna_reference_audio'], output_audio=config['server']['output_audio_format'], mode=config['server']['yuna_audio_mode'], language="en"): if mode == "native": # Split the text into sentences sentences = text.replace("\n", " ").replace("?", "?|").replace(".", ".|").replace("...", "...|").split("|") @@ -106,6 +105,13 @@ def speak_text(text, reference_audio, output_audio, mode, language="en"): audio.export("/Users/yuki/Documents/Github/yuna-ai/static/audio/audio.mp3", format='mp3') elif mode == "fast": command = f'say -o /Users/yuki/Documents/Github/yuna-ai/static/audio/audio.aiff {repr(text)}' + exit_status = os.system(command) + + # convert audio to mp3 + audio = AudioSegment.from_file("/Users/yuki/Documents/Github/yuna-ai/static/audio/audio.aiff") + audio.export("/Users/yuki/Documents/Github/yuna-ai/static/audio/audio.mp3", format='mp3') + elif mode == "fast-pv": + command = f'say -v Yuna -o /Users/yuki/Documents/Github/yuna-ai/static/audio/audio.aiff {repr(text)}' print(command) exit_status = os.system(command) diff --git a/lib/history.py b/lib/history.py index cef3da3..b0f9832 100644 --- a/lib/history.py +++ b/lib/history.py @@ -1,7 +1,6 @@ import json import os from cryptography.fernet import Fernet, InvalidToken -from lib.audio import speak_text class ChatHistoryManager: def __init__(self, config): @@ -75,9 +74,6 @@ def list_history_files(self, username): history_files.sort(key=lambda x: x.lower()) return history_files - def generate_speech(self, response): - speak_text(response, "/Users/yuki/Documents/AI/yuna-data/yuna-tamer-prepared.wav", "audio.aiff", self.config['server']['yuna_audio_mode']) - def delete_message(self, username, chat_id, target_message): chat_history = self.load_chat_history(username, chat_id) diff --git a/lib/router.py b/lib/router.py index 620385c..7696d60 100644 --- a/lib/router.py +++ b/lib/router.py @@ -93,7 +93,7 @@ def generate(): chat_history_manager.save_chat_history(chat_history, user_id, chat_id) if speech == True: - chat_history_manager.generate_speech(response) + speak_text(response) return jsonify({'response': response}) @@ -141,7 +141,7 @@ def handle_audio_request(self): elif task == 'tts': print("Running TTS...") - result = speak_text(text, "/Users/yuki/Downloads/orig.wav", "response.wav", "fast") + result = speak_text(text) return jsonify({'response': result}) @@ -164,7 +164,7 @@ def handle_image_request(chat_history_manager, self): image_path = f"static/img/call/{current_time_milliseconds}.png" with open(image_path, "wb") as file: file.write(image_raw_data) - image_data = capture_image(image_path, data.get('message'), use_cpu=False) + image_data = capture_image(image_path, data.get('message'), use_cpu=False, speech=speech) if useHistory is not False: # Save chat history after streaming response @@ -173,7 +173,7 @@ def handle_image_request(chat_history_manager, self): chat_history.append({"name": self.config['ai']['names'][1], "message": image_data[0]}) if speech == True: - chat_history_manager.generate_speech(image_data[0]) + speak_text(image_data[0]) # Save the chat history chat_history_manager.save_chat_history(chat_history, list({current_user.get_id()})[0], chat_id) diff --git a/lib/tests/i.py b/lib/tests/i.py new file mode 100644 index 0000000..a479c4e --- /dev/null +++ b/lib/tests/i.py @@ -0,0 +1,42 @@ +import csv +import json +import re + +def parse_file(file_path): + data = [] + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + blocks = re.split(r'\n\n+', content.strip()) + + for block in blocks: + lines = block.strip().split('\n') + if len(lines) == 3: + item = {} + for line in lines: + print(line) + key, value = line.split(':', 1) + item[key.strip()] = value.strip() + data.append(item) + return data + +def create_csv(data, output_file): + with open(output_file, 'w', newline='', encoding='utf-8') as file: + writer = csv.DictWriter(file, fieldnames=['Question', 'Wrong', 'Right']) + writer.writeheader() + writer.writerows(data) + +def create_json(data, output_file): + with open(output_file, 'w', encoding='utf-8') as file: + json.dump(data, file, indent=2, ensure_ascii=False) + +# Main execution +input_file = 'dpo.txt' # Replace with your input file name +csv_output = 'output.csv' +json_output = 'output.json' + +parsed_data = parse_file(input_file) +create_csv(parsed_data, csv_output) +create_json(parsed_data, json_output) + +print(f"CSV file created: {csv_output}") +print(f"JSON file created: {json_output}") \ No newline at end of file diff --git a/lib/vision.py b/lib/vision.py index 47ad715..260aabc 100644 --- a/lib/vision.py +++ b/lib/vision.py @@ -4,15 +4,15 @@ from datetime import datetime from llama_cpp import Llama from llama_cpp.llama_chat_format import MoondreamChatHandler +from lib.audio import speak_text if os.path.exists("static/config.json"): with open("static/config.json", 'r') as file: config = json.load(file) -yuna_model_dir = config["server"]["yuna_model_dir"] -agi_model_dir = config["server"]["agi_model_dir"] -model_id = f"{yuna_model_dir}yuna-ai-miru-v0.gguf" -model_id_eyes = f"{yuna_model_dir}yuna-ai-miru-eye-v0.gguf" +agi_model_dir = config["server"]["agi_model_dir"] + "vision/" +model_id = f"{agi_model_dir}yuna-ai-miru-v0.gguf" +model_id_eyes = f"{agi_model_dir}yuna-ai-miru-eye-v0.gguf" if config["ai"]["vision"] == True: chat_handler = MoondreamChatHandler(clip_model_path=model_id_eyes) @@ -45,7 +45,7 @@ def create_image(prompt): return image_name -def capture_image(image_path=None, prompt=None, use_cpu=False): +def capture_image(image_path=None, prompt=None, use_cpu=False, speech=False): # print the parameters print(f"image_path: {image_path}") print(f"prompt: {prompt}") @@ -67,4 +67,6 @@ def capture_image(image_path=None, prompt=None, use_cpu=False): ) answer = result['choices'][0]['message']['content'] + if speech: + speak_text(answer) return [answer, image_path] \ No newline at end of file diff --git a/lib/yuna-tts b/lib/yuna-tts new file mode 100755 index 0000000000000000000000000000000000000000..39513c192525049e1963b5b9eecea2058b93a616 GIT binary patch literal 50504 zcmeI5U2I&%6~|}2>o~?vY*0u{%EyADLQ>beAr&J1OqcYdVaC$!y6?uzmVDIIml;hrar`SM%6 z!{l|GZ9Z330-{vyQ%6cGxi2LPPBB?ve`CHmoo_D(N!aH7RAoWFRKc+gb5ajz%oo|_ z?XOkG*{+qky;4B$FQ=T0naHUH``fAW?b1ouu9b!PisnJXOiRO@uxMc8{*LN=dvy}F z8*|j==eqW~b-gP(8kMo3-EptF5q+tsb)}*8le40o)R%J7swpeki|6ZdlhG&Q&99g% zDjnTQnX=5ne8$FnT{NHX9W}17&HGtObRCRbVp1`>#(d)uFW*X?fbHeDJw;=^JbDs0 zr3P+fyAtu53N!aNL$1pGRask2&<$QHUzJ;_{+x7|8GlvgfA51kckk-EYo}LDA!Zu( zMK;TT{xR@=Dtlu8*XSf};u-qQkiA9Qb7Y6fW;VR1t`kDgpFiRpwF8u(jrzl+EL>Ao z9?M(Y?OeY+}GJ7T}{?M4R3*@1A-3ZFY!t3lMt*+zy9ii;@ zU1FU}uM!K*$94YR+WexQDo^LAFeR?uk$SfG5MLungGm5bj7&MP8i(R9O^pa}lD zqHa8}Onl|i@dKN0i);(;=@oX-6se+ZE7KV%nuC^=7q)t^s0z+7J>0a+M0U(FQYu<> zGL~&jyRSR%F`P`y%H^|)-+B%t%yd@ScepPk>71_Aw){Mh-dz zgz!H>F*&sT8!;IhjK_epUT8??Ugi%$Hqhv-U(zI4< zE@7Brm7oEm?o=8lR?~q_ZS~G|gzW1n$DFob)V831H-XRnnYM%4UO~?5TS}rU;>98q!OlQ#&Z?M&aMOAQym0hsRM0U(F zQYu<>GL~&jCmh2v?=hTA%*y4nimic!na(Qvj)o=dqDc(&Ceo^qvW>iB*>cORQuWFM zn{V@N3-9U8&X36&jmDBgRXrgj8xi{_GIy@FQ!tWZyeq z?t3Tovxi@JY2142PZKkN-^|YLZyFkJ{^Ia24^Kb(+tzq0w?{a6D@x~@$<2bAAhv&nonQoKXlX8KR)=}Uw0k< z_(w-)yT5<7<*TRuv0VPkjsNQk)f3+M**}NZzwiI#&F-78{>~p>oPPI-kJfMa)ze?O z_{?LMn}50ahx`9L_0ntazWU1TBYW2MW&U&jm1A$e-1`2+iHT3vws*h&b+20g1wSeZ Ae*gdg literal 0 HcmV?d00001 diff --git a/lib/yuna-tts.m b/lib/yuna-tts.m new file mode 100644 index 0000000..ce96ac6 --- /dev/null +++ b/lib/yuna-tts.m @@ -0,0 +1,17 @@ +#import + +int main() { + __block BOOL done = NO; + [AVSpeechSynthesizer requestPersonalVoiceAuthorizationWithCompletionHandler:^(AVSpeechSynthesisPersonalVoiceAuthorizationStatus status){ + // authorization popup should be visible now + done = YES; + }]; + + // Run the loop for a maximum of 10 seconds + NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:10.0]; + while (!done && [loopUntil timeIntervalSinceNow] > 0) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + + return 0; +} \ No newline at end of file diff --git a/static/config.json b/static/config.json index 22c19e5..47d31d4 100644 --- a/static/config.json +++ b/static/config.json @@ -6,7 +6,7 @@ ], "emotions": false, "art": false, - "vision": false, + "vision": true, "max_new_tokens": 64, "context_length": 1024, "temperature": 0.7, @@ -37,15 +37,17 @@ "port": "", "url": "", "history": "db/history/", - "default_history_file": "history_template.json", + "default_history_file": "history_template:general.json", "images": "images/", "yuna_model_dir": "lib/models/yuna/", - "yuna_default_model": "yuna-ai-v3-q6_k.gguf", + "yuna_default_model": "yuna-ai-v3-q5_k_m.gguf", "agi_model_dir": "lib/models/agi/", "art_default_model": "yuna_ai_anime.safetensors", "device": "mps", "yuna_text_mode": "native", - "yuna_audio_mode": "11labs" + "yuna_audio_mode": "fast-pv", + "yuna_reference_audio": "audio.mp3", + "output_audio_format": "audio.aiff" }, "security": { "secret_key": "YourSecretKeyHere123!", diff --git a/static/css/kawai-v11-2.css b/static/css/kawai-v11-2.css index 5d7ae68..07efa67 100644 --- a/static/css/kawai-v11-2.css +++ b/static/css/kawai-v11-2.css @@ -689,6 +689,8 @@ label { white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ + display: flex; + flex-direction: column; } #docs h1, diff --git a/static/js/creator.js b/static/js/creator.js index 8d015fd..ae05dfb 100644 --- a/static/js/creator.js +++ b/static/js/creator.js @@ -1,12 +1,12 @@ // Get the necessary elements const promptTemplateTextarea = document.querySelector('#freeform-prompt-template'); -const bodyTextTextarea = document.querySelector('.body-text'); -const resultTextarea = document.querySelector('.result-container'); +const bodyTextTextarea = document.querySelector('#body-text-freeform-container'); +const resultTextarea = document.querySelector('#result-create-freeform'); const submitButton = document.getElementById('send-create-freeform'); // Set the default text for the Prompt Template block const defaultPromptTemplate = promptTemplateManager.buildPrompt('himitsuAssistant'); -promptTemplateTextarea.value = defaultPromptTemplate; +promptTemplateTextarea.value = defaultPromptTemplate.replace('### Instruction:\n', '### Instruction:\n{body_text}'); // Function to send the request to the server async function sendRequest() { @@ -18,7 +18,7 @@ async function sendRequest() { // Clear the result textarea before starting resultTextarea.value = ''; - messageManagerInstance.sendMessage(promptTemplate, false, imageData = '', url = '/message', naked = false, stream = true, outputElement = resultTextarea); + messageManagerInstance.sendMessage(promptTemplate, null, imageData = '', url = '/message', naked = false, stream = true, outputElement = resultTextarea); } // Add an event listener to the submit button diff --git a/static/js/index.js b/static/js/index.js index 834af19..cd771e5 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -13,7 +13,6 @@ var isYunaListening = false; let mediaRecorder; let audioChunks = []; var activeElement = null; -//kawaiAutoScale(); // Global variable to track the state of Streaming Chat Mode let isStreamingChatModeEnabled = false; @@ -35,7 +34,7 @@ buttonAudioRec.addEventListener('click', () => { } }); -function startRecording() { +function startRecording(withImage = false, imageDataURL, imageName, messageForImage) { navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { mediaRecorder = new MediaRecorder(stream); @@ -52,7 +51,12 @@ function startRecording() { mediaRecorder.addEventListener('stop', () => { const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); - sendAudioToServer(audioBlob); + + if (withImage) { + sendAudioToServer(audioBlob, true, imageDataURL, imageName, messageForImage); + } else { + sendAudioToServer(audioBlob); + } audioChunks = []; }); }) @@ -70,7 +74,7 @@ function stopRecording() { isRecording = false; } -function sendAudioToServer(audioBlob) { +function sendAudioToServer(audioBlob, withImage = false, imageDataURL, imageName, messageForImage) { const formData = new FormData(); formData.append('audio', audioBlob); formData.append('task', 'transcribe'); @@ -86,7 +90,11 @@ function sendAudioToServer(audioBlob) { return response.json(); }) .then(data => { - messageManagerInstance.sendMessage(data.text, kanojo.buildKanojo(),'', '/message', false, false, isStreamingChatModeEnabled); + if (withImage) { + askYunaImage = messageManagerInstance.sendMessage(data.text, kanojo.buildKanojo(), [imageDataURL, imageName, data.text], '/image', false, false, isStreamingChatModeEnabled); + } else { + messageManagerInstance.sendMessage(data.text, kanojo.buildKanojo(),'', '/message', false, false, isStreamingChatModeEnabled); + }; }) .catch(error => { console.error('Error sending audio to server', error); @@ -211,8 +219,11 @@ class messageManager { async sendMessage(message, template, imageData = '', url = '/message', naked = false, stream = isStreamingChatModeEnabled || false, outputElement = '') { this.inputText = document.getElementById('input_text'); const messageContent = message || this.inputText.value; - const userMessageElement = this.createMessage(name1, messageContent); - this.createTypingBubble(naked); + var userMessageElement; + if (template !== null) { + userMessageElement = this.createMessage(name1, messageContent); + this.createTypingBubble(naked); + } if (url === '/message') { let result = ''; @@ -273,8 +284,11 @@ class messageManager { console.log('Final result:', result); } else { const data = await response.json(); - this.removeTypingBubble(); - this.createMessage(name2, data.response); + + if (template !== null) { + this.removeTypingBubble(); + this.createMessage(name2, data.response); + } } if (isYunaListening) { @@ -326,7 +340,7 @@ class messageManager { const [imageDataURL, imageName, messageForImage] = imageData; const serverEndpoint = `${server_url + server_port}/image`; const headers = { 'Content-Type': 'application/json' }; - const body = JSON.stringify({ image: imageDataURL, name: imageName, message: messageForImage, task: 'caption', chat: selectedFilename}); + const body = JSON.stringify({ image: imageDataURL, name: imageName, message: messageForImage, task: 'caption', chat: selectedFilename, speech: isYunaListening }); fetch(serverEndpoint, { method: 'POST', headers, body }) .then(response => response.ok ? response.json() : Promise.reject('Error sending captured image.')) @@ -338,6 +352,9 @@ class messageManager { const imageResponse = `${data.message}`; this.createMessage(name2, imageResponse); + // play audio + playAudio(); + return imageCaption; }) .catch(error => { @@ -477,7 +494,7 @@ class HistoryManager { constructor(serverUrl, serverPort, defaultHistoryFile) { this.serverUrl = serverUrl || 'https://localhost:'; this.serverPort = serverPort || 4848; - this.defaultHistoryFile = defaultHistoryFile || 'history_template.json'; + this.defaultHistoryFile = defaultHistoryFile || 'history_template:general.json'; this.messageContainer = document.getElementById('message-container'); } @@ -784,7 +801,7 @@ async function drawArt() { } // Add an event listener to the "Capture Image" button -function captureImage() { +async function captureImage() { var localVideo = document.getElementById('localVideo'); var captureCanvas = document.getElementById('capture-canvas'); var captureContext = captureCanvas.getContext('2d'); @@ -800,14 +817,19 @@ function captureImage() { captureCanvas = document.getElementById('capture-canvas'); imageDataURL = captureCanvas.toDataURL('image/png'); // Convert canvas to base64 data URL - messageForImage = prompt('Enter a message for the image:'); - - // generate a random image name using current timestamp + let messageForImage = ''; var imageName = new Date().getTime().toString(); - closePopupsAll(); + if (isYunaListening) { + // Start recording + startRecording(true, imageDataURL, imageName, messageForImage); - askYunaImage = messageManagerInstance.sendMessage('', kanojo.buildKanojo(), [imageDataURL, imageName, messageForImage], '/image', false, false, isStreamingChatModeEnabled); + return true; + } else { + messageForImage = prompt('Enter a message for the image:'); + closePopupsAll(); + askYunaImage = messageManagerInstance.sendMessage(messageForImage, kanojo.buildKanojo(), [imageDataURL, imageName, messageForImage], '/image', false, false, isStreamingChatModeEnabled); + } } // Modify the captureImage function to handle file uploads @@ -832,11 +854,7 @@ async function captureImageViaFile(image=null, imagePrompt=null) { reader.onloadend = async function () { const imageDataURL = reader.result; var messageForImage = ''; - if (imagePrompt) { - messageForImage = imagePrompt; - } else { - messageForImage = prompt('Enter a message for the image:'); - } + messageForImage = prompt('Enter a message for the image:'); const imageName = Date.now().toString(); @@ -1296,25 +1314,45 @@ document.addEventListener('DOMContentLoaded', (event) => { }); }); -async function checkMe() { - const response = await fetch('/flash-messages'); - return response.json(); -} - document.addEventListener('DOMContentLoaded', loadConfig); -function importFlash(messages) { - const dropdownMenu = document.querySelector('.dropdown-menu.dropdown-menu-end.dropdown-list.animated--grow-in'); - dropdownMenu.innerHTML = messages.map(message => ` - -
- ${message} -
-
- `).join(''); +class NotificationManager { + constructor() { + this.dropdownMenu = document.querySelector('.dropdown-menu.dropdown-menu-end.dropdown-list.animated--grow-in'); + this.messages = []; + } + + add(message) { + this.messages.push(message); + this.render(); + } + + delete(message) { + this.messages = this.messages.filter(msg => msg !== message); + this.render(); + } + + clearAll() { + this.messages = []; + this.render(); + } + + render() { + this.dropdownMenu.innerHTML = this.messages.map(message => ` + +
+ ${message} +
+
+ `).join(''); + } } -checkMe().then(importFlash).catch(console.error); +// Create an instance of NotificationManager +const notificationManagerInstance = new NotificationManager(); + +// Call the add method +notificationManagerInstance.add("Hello! Welcome to the chat room!"); function updateMsgCount() { setTimeout(() => { diff --git a/static/js/setup.js b/static/js/setup.js index f80c57b..4a32d93 100644 --- a/static/js/setup.js +++ b/static/js/setup.js @@ -28,7 +28,7 @@ function createFormGroup(id, value) { return `
- +
`; } diff --git a/yuna.html b/yuna.html index 9563780..6172259 100644 --- a/yuna.html +++ b/yuna.html @@ -173,12 +173,12 @@ Emotional Profile
Plugins
- +
+ for="updateEmojiHistory">Enable News Flow
@@ -524,10 +523,10 @@
Prompt Template
rows="5"> -
+
Body Text
- +
@@ -541,7 +540,7 @@
Body Text
Result
- +
@@ -698,7 +697,7 @@ aria-controls="v-pills-general" aria-selected="true">General + aria-controls="v-pills-system" aria-selected="false">Account @@ -714,15 +713,12 @@
General
- +
- +
-
@@ -735,14 +731,13 @@
Mode
- +
- - -
- Reset +
+ Reset +
+
@@ -927,8 +922,8 @@