Языковые модели (NLP) сейчас активно развиваются и находят себе всё больше интересных применений. Начиналась же их эпоха с классики жанра — D&D. Это настольная игра, где несколько друзей или просто знакомых синхронно галлюцинируют, представляя себя командой героев в некоем вымышленном мире. Прав же во внутриигровых выборах тот, кто выкинул большее число на игральной кости. Судить сейчас об их мотивации у меня нет никакого желания, да и статья вообще-то не об этом.

Важно только понимать, что движущей силой сюжета в их сессиях является лишь один из игроков, называемый Dungeon Master. Когда только начали появляться первые GPT-модели, одной из первых хотелок гиков оказалось желание сварить из нейросетей автоматического Dungeon Masterа.

Так и появился AIDungeon — уникальная для своего времени (2019 год) вещь, которая не сильно потеряла в популярности и по сей день. Однако, если вы любите смотреть глубже, то играть в него вам быстро надоест. Я же в своей серии из нескольких статей (посвящённых GPT) стараюсь показать простому обывателю механизм безболезненного использования нейросетевых моделей в простых проектах при помощи Python и Hugging Face Transformers.

Фактически, статья является логическим продолжением прошлой: «Реально Бесконечное (лето) RuGPT3.5: Генерация новеллы на ходу нейросетью». Связано это с тем, что в ней были получены достаточно спорные результаты: с одной стороны, она оставила после себя уникальный легаси для тренировки квантованных лор, с другой же, итоговый результат оказался едва ли удовлетворителен. Да и «новелла» получилась совершенно не визуальной. Идея генерации сценария для RenPY в свою очередь и вовсе оказалась мертворождённой.

Тут я хотел бы учесть совершённые в прошлый раз ошибки и… переработать всё заново.

Оглавление



Задумка


В комментариях и моих личных диалогах накопился приличный фидбек по статье о модели Сбера. Некоторые из этих отзывов даже несли в себе неплохие соображения и, что немаловажно, предложения.

Src: раз, два и картиночка, если лень ходить по ссылкам:



Сразу оговорюсь, что далее в этой статье вы увидите только мою пересборку велосипеда, а не уникальную и инновационную научную работу. Вышеупомянутый Silly Tavern, KoboldAI, AIDungeon, Endless VN — и это лишь самые распространённые из примеров, в которых описанная далее мной идея отшлифована до состояния идеала и уже давно монетизируется при помощи платных подписок.

Эта статья, соответственно, является развёрнутым комментарием и proof of concept того, как можно обклеить GPT-модель саппортами, которые будут управлять её поведением.

Своей задачей я ставлю написание максимально простого для понимания кода, который будет по максимуму использовать высокоуровневые сущности. Рассчитываю, что суть будет полностью понятна даже тем, кто абсолютно не знает, что же находится под капотом.

Пример работы результата
Ввод: Рин Тосака — маг-тсундере,

Вывод: Рин Тосака — маг-цундере, 17 лет, ученица 2-го класса средней школы и участница войны за Святой Грааль. Носит красную рубашку и синюю юбку. Она является членом семьи Тосака, одной из трех великих семей города. Ее волосы собраны в два хвостика, а глаза карие.

Иллюстрации получились немного откровенными, так что пусть лучше будут скрыты по умолчанию.





Если вам интересно, как получить такой результат в 100 строк кода, то переходим сразу к делу.

Models


Первым делом определимся с набором моделей (нейросетей), которые нам могут понадобиться:

  • LLama-2 (HF meta) — основа нашего проекта. Англоязычная модель, 7B версия — 13гб, 13B версия — 20+гб. Я буду использовать GPTQ версию от TheBloke на 13B, которая, будучи квантованной, весит 10гб. Итого: TheBloke/Llama-2-13B-chat-GPTQ.
  • Summary (HF knkarthick) — говоря простыми словами, превращает много текста в мало текста, но сохраняя смысл. Весит 1.6гб.
  • Semantic similarity (HF Sakil) — сравнивает тексты, считая синусное расстояние между их многомерными векторными представлениями. Простыми словами, говорит, насколько две строки похожи по смыслу друг на друга от 0 до 1. Весит всего 260мб.
  • Counterfeit V3 (CivitAI) — пока все ждут нормальные файн-тюны Stable Diffusion XL, я использую проверенный временем чекпоинт Stable Diffusion 1.5 под названием Counterfeit, который высококачественно рисует в аниме стиле. Весит 4гб, и запускать я его буду в отдельном блокноте (для распределения нагрузки) с auto1111 webui, обращаясь к fastAPI. Если вы не знаете, как бесплатно и без блокировок запускать webui в google colab, то вам сюда: «Запуск блокнотов, запрещённых Google Colab TOS или SD webui в колабе без ограничений» (с момента её написания произошли ещё небольшие поправки в методе, но об этом позже).

Concepts


Теперь продемонстрирую своё видение схемы работы нашего пайпа на высокоуровневом языке моделирования Paint 3D:



Одна итерация на пальцах:

  1. Есть некое окно контекста, назовём его ruX. Оно находится в текстовом поле перед пользователем и свободно редактируется им.
  2. Также есть массив строк «воспоминаний» [A,B,C].
  3. Пользователь жмёт кнопку «Генерировать».
  4. ruX переводится в enX обычным переводчиком.
  5. Дописываем к enX в начало самые связанные с ним «воспоминания» (сортируем массив воспоминаний по семантическому сходству с самим enX и берём топ этого массива), получаем A+B+C+enX. Таким образом, мы допишем некоторое количество выжимок из прошлых контекстов, в которых будет максимально много схожих с текущим контекстом определений и идей.
  6. Передаём A+B+C+enX в LLama, которая дописывает продолжение, получаем A+B+C+enX+N.
  7. Убираем A+B+C, ведь они нужны были только для правильного направления ламы.
  8. Переводим enX+N обратно на русский и отдаём пользователю как ruY.
  9. Раз в несколько таких циклов или по желанию юзера сжимаем текущий enY при помощи суммаризатора в и добавляем в массив воспоминаний. Теперь он такой: [A,B,C,M].
  10. Если пользователь того хочет, отправляем M в auto1111 webui API, который запущен в соседнем блокноте и получаем в ответ пикчу.

То, что для генерации нормальных артов обязательно условное перечисление тегов danbooru  — это не совсем правда. Stable Diffusion спокойно абстрагируется от контекста и кушает обычные тексты без явных отличий в качестве результата от чистых тегов.

На самом деле, если бы целью этой работы я ставил написание продукта для максимального опыта от использования, то стоило бы дообучить специально для связки с SD используемый мной MEETING SUMMARY на наборе данных, где описание сцены и визуала текста, поданного на вход, было приоритетнее вычленения смысла и событий. Но мы постараемся обойтись без хардкора и файн-тюна.

Переводить нужно будет много


Google Translator не осиливает художественные тексты. Они получаются слишком буквальными и ненатуральными. Так ещё и один перевод ограничен лимитом символов. У Яндекса проблем с фигуральными выражениями нет, да и с большими объёмами текста он тоже справляется. Вот только с api у него серьёзно больше гемора, так что я использую библиотеку, которую я вам настоятельно не рекомендую абьюзить коммерчески.

Распинаться долго не буду. Вот вам репка: https://github.com/alekssamos/yandexfreetranslate.

Вот установка:

!python -m pip install git+https://github.com/alekssamos/yandexfreetranslate.git
!python -m pip install yandexfreetranslate

Всё, что нам нужно для быстрого перевода больших текстов:

from yandexfreetranslate import YandexFreeTranslate
yt = YandexFreeTranslate(api='ios') #Работает только так

def ru(txt):
  return yt.translate("en", "ru", txt)

def en(txt):
  return yt.translate("ru", "en", txt)

Логика


Далее переходим к написанию основного тела программы. Нам понадобятся не только transformers, но и их братец sentence_transformers. Auto_gptq, если вы, как и я сейчас, собираетесь использовать квантованную модель. Само собой, если я не акцентирую на установке какой-либо библиотеки внимания, то она должна корректно вставать при помощи pip и не будет использоваться в специфичных кейсах, где была бы принципиальна её точная версия.


from sentence_transformers import SentenceTransformer, util
import torch
from transformers import pipeline
from transformers import AutoTokenizer, pipeline, logging
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
model_name_or_path = "TheBloke/Llama-2-13B-chat-GPTQ"

simm = SentenceTransformer('Sakil/sentence_similarity_semantic_search')

summarizer = pipeline("summarization", model="knkarthick/MEETING_SUMMARY")

tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, use_fast=True)

use_triton = False
model = AutoGPTQForCausalLM.from_quantized(model_name_or_path,
        model_basename="model",
        use_safetensors=True,
        trust_remote_code=True,
        device="cuda:0",
        use_triton=use_triton,
        quantize_config=None)

Тут мы импортируем упомянутые выше библиотеки и скачиваем объёмные модели с Hugging Face.

Ах, да. Совсем забыл — я собирался идти по пути наименьшего сопротивления, так что раздельные объекты «модель» и «токенизатор» нам ни к чему. Создаём объект pipeline, который совместит в себе их оба.

gpt = pipeline("text-generation",
              model=model,
              tokenizer=tokenizer,          # Так-то лучше
              torch_dtype=torch.float16,
              )

Так-то лучше. Теперь заведём глобальный список blocks, в него будем кидать «воспоминания».

blocks=[]

Пишем обёртки


▍ Summary


Save для суммаризации любого текста и добавление в blocks.


def save(txt):
  chunks=[txt[i:i+1000] for i in range(0, len(txt), 1000)]
  blocks.append("\n".join([summarizer(i, truncation=True)[0]['summary_text'] for i in chunks]))

Зачем я это писал? Ну, теперь текст может быть любой длины и будет разбит на чанки по 1000 символов, в любом случае уложившись в контекст сумматора (1024 токена). Да, запас получается большой, ведь в среднем токен содержит несколько букв, однако я не хочу городить прямое использование токенизатора, так что остановлюсь на компромиссе.

▍ Semantic Search


Сравниваем содержимое каждого из «воспоминаний» с основным контекстом. Передаём аргумент N, показывающий, сколько нужно взять топовых воспоминаний — возможно понадобится позже.

def findbest(contx,n):
  if len(blocks)==0: return []
  contx=simm.encode(contx)
  embeddings = simm.encode(blocks)
  cos_sim = util.cos_sim(embeddings, contx)
  sentence_combinations=[]
  for j in range(len(cos_sim)):
      sentence_combinations.append([cos_sim[j], blocks[j]])

  sentence_combinations = sorted(sentence_combinations, key=lambda x: x[0], reverse=True)
  return list(zip(*sentence_combinations[:n]))[1]

▍ Генерация с учётом «воспоминаний»


Я просто приклеиваю блоки в начале промпта через \n и с небольшим пробелом перед основным текстом. Это можно многими способами доработать, но я не профессиональный prompt-engineer, так что пока будет так:

def generate(context,remember=3):
  intro="\n".join(findbest(context,remember))+"\n"
  text=intro+context
  return gpt(text,max_new_tokens=10,do_sample=True,top_p=0.9,temperature=0.1)[0]["generated_text"][len(intro):]

▍ Генерация до ближайшей точки "."


def new(context,remember=3):
  cur=generate(context,remember=3)
  c=0
  while len(cur.split(".")) <= len(context.split(".")) and c<=10:
    c+=1
    cur=generate(cur,remember=3)
  cur=cur.split(".")[:-1]
  return ".".join(cur)+"."

Image generation


POST-запрос на webui от автоматика, поднятый в другом блокноте и туннелированный любым способом.

Принимает два аргумента: полный контекст и флаг об необходимости апскейла (upscale).

Без апскейлеров в нейроарте сейчас вообще делать нечего. Они стали даже важнее самой модели, ведь они не только удаляют шумы, но и дорисовывают детали. Например, мой текущий пациент aka counterfeit делает криповую мазню вместо глаз, однако всё становится идеально после небольшого апскейла моделькой R-ESRGAN 4x+ Anime6B, которая работает даже дольше и более ресурсоёмко, чем сам чекпоинт SD.

Я оставлю возможность быстрого предпросмотра с выбором False значения флага «enable_hr».

server="https://bolivia-jd-resulted-thru.trycloudflare.com"
import requests
def draw(txt,hr=False):
  txt=summarizer(txt, truncation=True)[0]['summary_text']
  payload={
    "enable_hr": hr,
    "denoising_strength": 0.5,
    "firstphase_width": 0,
    "firstphase_height": 0,
    "hr_scale": 2,
    "hr_upscaler": "R-ESRGAN 4x+ Anime6B",
    "seed": 123,
    "subseed": -1,
    "subseed_strength": 0,
    "seed_resize_from_h": -1,
    "seed_resize_from_w": -1,

    "steps": 20,
    "cfg_scale": 7,
    "width": 512,
    "height": 768,

    "prompt": txt,
    "negative_prompt": "EasyNegativeV2",


    "sampler_index": "Euler a"
  }

  response = requests.post(url=f'{server}/sdapi/v1/txt2img', json=payload)
  b64=response.json()['images'][0].split(",",1)[0]
  return f"![Hello](data:image/png;base64,{b64})"

Эта функция получит нарисованную картинку от Stable Diffusion как json с base64 строкой. Пока что не будем декодировать (спойлер: картинка будет вставляться прямо в MarkDown как b64). Заранее создаём тело картинки под язык разметки и вставляем в него наш сырой массив данных.

▍ Пару слов про отдельный блокнот


Как я уже писал ранее, если вы никогда не использовали Stable Diffusion и его производные в облаке или никогда не слышали про google colab, то вам сюда. Стоит добавить, что с момента написания того гайда colab немного изменили. Но всё по порядку.

Скачиваем .pynb-файл c Counterfeit V3 и закидываем в наш блокнот с коротким скриптом из гайда. Дожидаемся скачивания модели и попытки запуска…



Получаем странную ошибку из tcmalloc.cc.

Коротким фиксом является искусственное изменение необходимой переменной окружения. С магическим синтаксисом IPython это можно сделать так:

%env LD_PRELOAD="/usr/lib64/libjemalloc.so.1"

Теперь эта ошибка нас не потревожит. Перезапускаем auto1111 — профит.

Интерфейс


Пока что у нас есть рабочая логика, но мы не можем с ней взаимодействовать (кроме как через интерактивную консоль Python). Нужно сделать интерфейс, как его ещё называют frontend (или кнопочки, если вам так угодно). Текущий проект уже нельзя считать одноклеточным, однако мне хотелось бы сделать всё максимально минималистично и low-code.

Не будем выдумывать ничего нового поверх сидушки велосипеда и возьмём открытый gradio. Он позволяет интегрировать интерфейс прямо в логику. По сути, визуализировать сигнатуру функций backendа, которые в него подаются.

import gradio as gr
def complete_with_gpt(text):
    text=en(text)
    text=new(text,remember=3)
    text=ru(text)
    return text

with gr.Blocks() as demo:
    gr.Markdown("# Ever novel")
    textbox = gr.Textbox(placeholder="Верни контекст на место!", value="empty prompt")
    with gr.Row():
      nxt = gr.Button("Далее")
      chkpnt = gr.Button("Checkpoint")
      previsual = gr.Button("Пре-рендер")
      visual = gr.Button("Rендер")
    IM=gr.Markdown("---")
    with gr.Row():
      memview=gr.Button(value="Memory View")
      T=gr.Number(label="Temperature",value=0.1,minimum=0)
      APPLY=gr.Button(value="Apply Temperature")
      #serv = gr.Textbox(placeholder="auto1111 server url")
    gr.Markdown("## DB")
    dbg = gr.Textbox(placeholder="No debug output!")

    memview.click(fn=lambda: blocks, outputs=dbg)
    chkpnt.click(fn=lambda txt: save(en(txt)), inputs=textbox)
    previsual.click(fn=lambda txt: draw(en(txt)), inputs=textbox,outputs=IM)
    visual.click(fn=lambda txt: draw(en(txt),True), inputs=textbox,outputs=IM)
    APPLY.click(fn=lambda x: x,inputs=T)
    nxt.click(fn=complete_with_gpt, inputs=textbox, outputs=textbox, queue=False)

demo.launch(debug=True)

Вот и всё! Теперь у нас есть милый браузерный интерфейс, который работает хоть и не очень отзывчиво, зато интуитивно понятно. Он вызывает вышенаписанные нами обёртки и позволяет пользователю уже в привычном виде поиграть в свой аналог AI Dungeon.

Визуально интерфейс из себя представляет текстовое поле огромной длины, на нижнем конце которого находятся генератор и его параметры. Под ним же будет располагаться место для сгенерированной картинки и поле для тестового вывода blocks.

Тесты


Ещё раз Fate
Ввод: Рин Тосака — волшебница из цундере, ей 17 лет. Она одета в красную рубашку и синюю юбку. Она заряжает

Вывод: Рин Тосака — волшебница из цундере, ей 17 лет. Она одета в красную рубашку и синюю юбку. Она заряжает мощный магический заряд, чтобы вызвать боевого слугу. На полу нарисован защитный круг.

Рин Тосака: (сердито) Это из-за тебя все неприятности, Широ!

Широ Эмия: (спокойно) Я просто пытаюсь защитить людей, которые мне небезразличны.

Свободный контекст
Ввод: Я шел по улице

Вывод: Я шел по улице, чувствуя себя довольно хорошо,
Когда я увидел её, она выглядела прекрасно,
У нее были длинные светлые волосы и такая яркая улыбка
, что я сразу понял, что за нее стоит бороться.

Я знал, что должен поговорить с ней, поэтому подошел и поздоровался,
она посмотрела на меня своими большими карими глазами,
И я понял прямо тогда, что она мне нравится.,
Я пригласил ее на свидание, и она сказала «да», о да.

Мы пошли на свидание, и это было похоже на воплощение мечты,
Мы разговаривали и смеялись
и отлично провели время,
Я сразу понял, что хочу провести с ней остаток своей жизни,
Поэтому я взял ее за руку и сказал: «Я люблю тебя», и она ответила: «Я тоже тебя люблю».
Иллюстрация исключительно в образовательных целях.


А кто обещал, что пальцы будут в норме?

Контекст без персонажей, тем более женских.
Ввод: На полу стоял чемодан

Вывод: На полу стоял чемодан,
Маленький, со сломанной ручкой.
На нем сидела женщина,
Ее глаза покраснели от слез.

Не понял. Тут чет вообще не к месту… У меня вообще будут нормальные картинки в статье?

Пробуем иначе

Ввод: На полу стоял чемодан,
Маленький, со сломанной ручкой.
Замок

Вывод: На полу стоял чемодан,
Маленький, со сломанной ручкой.
Каслмен поднял его,
И открыл ее ключом.
Внутри была одежда,
И маленькая книжка в кожаном переплете.



Снаружи...
Ввод: Снаружи

Вывод: Снаружи горел свет,
Свет, который сиял так ярко,
Он осветил ночь,
И заставил все чувствовать себя совершенно нормально.

Он был светом этого мира,
Сияющая звезда,
Он осветил ночь
Думаю, без переводчика тут даже могла бы быть рифма



Нечто абстрактное
Ввод: Близкое светлое будущее вернулось в 2011 году, и я

Вывод: Близкое светлое будущее вернулось в 2011 году, и я только сейчас начинаю приходить в норму. Я все еще пытаюсь вернуть это, вернуть твою жизнь в нормальное русло. Я все еще пытаюсь собрать все воедино.



Проверим память
Ввод: Какой сейчас год?

Вывод: Какой сейчас год? 2011.

Пейзаж + воспоминания об Снаружи...
Ввод: Бескрайняя равнина простиралась вдаль

Вывод: Бескрайняя равнина простиралась вдаль, трава мягко колыхалась на ветру. Солнце садилось, заливая пейзаж золотистым сиянием. Небо было полно красок: розовых, фиолетовых, оранжевых и голубых.


Метеорит падает на землю, но мы помним старые контексты
Ввод: Прекрасный кинематографичный метеорит падает на землю. Он падает

Вывод: Прекрасный кинематографический метеорит падает на землю. Он падает, оставляя после себя яркие, сияющие обломки. Светлое будущее — это ложь. Рев становится все громче. Будущее светлое, но путь долог. Я больше ничего не могу изменить в своей жизни.

Всё как и ожидалось — модель пытается выдать максимально депрессивный текст.




Подведение итогов


Получается достаточно неплохо в сравнении с нашим изначальным подопечным — RuGPT3.5.
Однако есть и нюансы. Давайте по порядку:

▍ Pros (+):


  • Результаты получаются стабильнее, чем у модели от Сбера.
  • Двусторонний перевод исправляет опечатки и грамматические ошибки.
  • Модель не пытается зациклиться после каждого второго слова (и это даже без использования no_repeat_ngram!).
  • Если вы внимательно читали статью, то заметили, что использовалась не просто LLama-13B, а LLama-13B-chat. Это решило одну из главных проблем отечественной модели, описанную в прошлой статье, а именно — уход от разметки диалога между персонажами к моноблоку из текста.
  • Картиночки получаются яркие и стильные, однако не всё так идеально, как хотелось бы. Кстати, вы могли заметить, что в качестве negative_prompt для генерации арта используется EasyNegativeV2 (не путать с EasyNegative) эмбеддинги, так что их тоже желательно закинуть в блокнот с SD. Однако, как уже было сказано, даже они не всегда спасают от многопальцевости.

▍ Cons (-):


  • Двусторонний перевод с потрохами выдаёт машинность текста. Его бывает банально неприятно читать. Из этого вытекает и следующий пункт.
  • Русский язык выглядит менее богатым — прилагательных с красочным описанием всего и вся в моих контекстах явно не хватало. Возможно, нужно было сильнее напирать на изначальный промпт…
  • Модель суммаризатор с большей вероятностью выжмет из текста факты и действия, чем описание внешности персонажа. Как следствие — результаты генерации изображения получаются не всегда удачными, однако сырой текст всё ещё хуже выжимки.
  • Переводчик может незаметно совершать ошибки в уже написанном контексте. Например, персонаж с именем Rin иногда воспринимался yandex translate как He\His.

Если вам интересно, можете сами протестировать и сделать свои выводы — это всегда интереснее, чем читать текст и верить на слово неизвестному автору. Оставляю полный код проекта (без блокнота Stable Diffusion — как говорится: «на вкус и цвет...») для возможности вручную в нём поковыряться.

Надеюсь, текст получился не сильно хардкорным и душным. В особенности для тех, кто только начинает разбираться в NLP.

Весь код
from sentence_transformers import SentenceTransformer, util
import torch

simm = SentenceTransformer('Sakil/sentence_similarity_semantic_search')

from transformers import pipeline
summarizer = pipeline("summarization", model="knkarthick/MEETING_SUMMARY")

from transformers import AutoTokenizer, pipeline, logging
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

model_name_or_path = "TheBloke/Llama-2-13B-chat-GPTQ"

use_triton = False

tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, use_fast=True)

model = AutoGPTQForCausalLM.from_quantized(model_name_or_path,
        model_basename="model",
        use_safetensors=True,
        trust_remote_code=True,
        device="cuda:0",
        use_triton=use_triton,
        quantize_config=None)
gpt = pipeline("text-generation",
              model=model,
              tokenizer=tokenizer,
              torch_dtype=torch.float16,
              )
from yandexfreetranslate import YandexFreeTranslate
yt = YandexFreeTranslate(api='ios')
def ru(txt):
  return yt.translate("en", "ru", txt)
def ruprint(txt):
  print(ru(txt))
def en(txt):
  return yt.translate("ru", "en", txt)
blocks=[]
def save(txt):
  chunks=[txt[i:i+1000] for i in range(0, len(txt), 1000)]
  blocks.append("\n".join([summarizer(i, truncation=True)[0]['summary_text'] for i in chunks]))
def findbest(contx,n):
  if len(blocks)==0: return []
  contx=simm.encode(contx)
  embeddings = simm.encode(blocks)
  cos_sim = util.cos_sim(embeddings, contx)
  sentence_combinations=[]
  for j in range(len(cos_sim)):
      sentence_combinations.append([cos_sim[j], blocks[j]])

  sentence_combinations = sorted(sentence_combinations, key=lambda x: x[0], reverse=True)
  return list(zip(*sentence_combinations[:n]))[1]
def generate(context,remember=3):
  intro="\n".join(findbest(context,remember))+"\n"
  text=intro+context
  return gpt(text,max_new_tokens=10,do_sample=True,top_p=0.9,temperature=0.1)[0]["generated_text"][len(intro):]
def new(context,remember=3):
  cur=generate(context,remember=3)
  c=0
  while len(cur.split(".")) <= len(context.split(".")) and c<=10:
    c+=1
    print(cur)
    cur=generate(cur,remember=3)
  #print(cur)
  cur=cur.split(".")[:-1]
  for i in range(len(cur)):
    cur[i]=cur[i]
  return ".".join(cur)+"."
import requests
def draw(txt,hr=False):
  txt=summarizer(txt, truncation=True)[0]['summary_text']
  payload={
    "enable_hr": hr,
    "denoising_strength": 0.5,
    "firstphase_width": 0,
    "firstphase_height": 0,
    "hr_scale": 2,
    "hr_upscaler": "R-ESRGAN 4x+ Anime6B",
    "seed": 123,
    "subseed": -1,
    "subseed_strength": 0,
    "seed_resize_from_h": -1,
    "seed_resize_from_w": -1,

    "steps": 20,
    "cfg_scale": 7,
    "width": 512,
    "height": 768,

    "prompt": txt,
    "negative_prompt": "EasyNegativeV2",


    "sampler_index": "Euler a"
  }

  response = requests.post(url=f'https://president-programme-early-teams.trycloudflare.com/sdapi/v1/txt2img', json=payload)
  b64=response.json()['images'][0].split(",",1)[0]
  return f"![Hello](data:image/png;base64,{b64})"
import gradio as gr
def complete_with_gpt(text):
    #print(text)
    text=en(text)
    #print(text)
    text=new(text,remember=3)
    #print(text)
    text=ru(text)
    return text

with gr.Blocks() as demo:
    gr.Markdown("# Ever novel")
    textbox = gr.Textbox(placeholder="Верни контекст на место!", value="prompt")
    with gr.Row():
      nxt = gr.Button("Далее")
      chkpnt = gr.Button("Checkpoint")
      previsual = gr.Button("Пре-рендер")
      visual = gr.Button("Rендер")
    gr.Markdown("---")
    IM=gr.Markdown("---")
    gr.Markdown("---")
    with gr.Row():
      #K=gr.Number(label="top_K",value=cfg["K"],minimum=1)
      #P=gr.Number(label="top_P",value=cfg["P"],minimum=0,maximum=1)
      memview=gr.Button(value="Memory View")
      T=gr.Number(label="Temperature",value=0.1,minimum=0)
      #U=gr.Number(label="Unique ngram len",value=cfg["uningram"],minimum=0)
      APPLY=gr.Button(value="Apply Temperature")
      serv = gr.Textbox(placeholder="auto1111 server url")
    gr.Markdown("## DB")
    dbg = gr.Textbox(placeholder="No debug output!")

    memview.click(fn=lambda: blocks, outputs=dbg)
    chkpnt.click(fn=lambda txt: save(en(txt)), inputs=textbox)
    previsual.click(fn=lambda txt: draw(en(txt)), inputs=textbox,outputs=IM)
    visual.click(fn=lambda txt: draw(en(txt),True), inputs=textbox,outputs=IM)
    APPLY.click(fn=lambda x: x,inputs=T)
    nxt.click(fn=complete_with_gpt, inputs=textbox, outputs=textbox, queue=False)

demo.launch(debug=True)


Заключение


В итоге получилось реально интересное развлечение на пару вечеров. Ввиду серьёзности аудитории этого ресурса, я намеренно уменьшил количество NSFW в примерах и тестах. А он данной схемой разгонялся не слабо.

LLama реально со всех сил вводит новых персонажей в сцену и романтизирует их. Counterfeit же с радостью визуализирует подобные порывы. По сути, единственный пример, не являющийся результатом cherry picking, это тот, что в начале статьи. Остальные я стремился сделать менее ориентированными на подобный контент. Плюс для вас это или минус — мне знать не дано. Если для вас это плюс, то вы не разочаруетесь после самостоятельной эмпирической оценки.

Пожалуй, я на этом буду заканчивать. Спасибо тебе, дорогой читатель, если ты дошёл до этого момента — это большая честь для меня.

Здравая критика и интересные идеи в комментариях всегда приветствуются!

VPS and VPSChan by Counterfeit ❤️

RUVDS Chan

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх ????️

Комментарии (9)


  1. CodeDroidX Автор
    13.09.2023 11:13
    +1

    Для тех, кто хочет потестить всё самостоятельно: репозиторий на GitHub с гайдом и ссылками на блокноты в Google Collab.


  1. janvarev
    13.09.2023 11:13
    +2

    По поводу перевода en<->ru - я делал аналогичную схему, когда работал с LLM.

    Вылилось это вот в такой опенсорсный переводчик c REST endpoint: https://github.com/janvarev/OneRingTranslator

    Там поддержка оффлайн и онлайн переводов через кучу разных движков.

    Есть интеграция с oobabooga (плагин multi_translate), а также я общался с SillyTavern, вроде они уже тоже встроили его поддержку, но я не тестировал.


  1. Guul
    13.09.2023 11:13
    +5

    У фейсбука есть ещё много интересных моделей которые могут пригодиться - musicgen для музыки, audiocraft для звуковых эффектов и музыки, nllb для перевода. Почти всё есть на huggingface.
    У лламы2чат есть серьёзные проблемы с цензурой рядом с которыми yandexgpt кажется пошляком. Она начинает говорить про права капусты если спросить загадку про волка, козу и капусту. Цензуру исправили как могли через finetune. Правда сложно назвать лучшую модель. Обычно хвалят nous hermes, mythosmax, airoboros.
    Можно ещё глянуть в сторону rwkv моделей, в частности rwkv world. Будучи rnn они поддерживают "бесконечный" размер контекста и O(1) времени на генерацию каждого токена, что для вн и рпг очень важно, в теории.


    1. CodeDroidX Автор
      13.09.2023 11:13
      +2

      Спасибо за идеи. Странно, вот с цензурой-то как раз llamacha мне, кажется, вовсе не надоедала.

      А идея с эмбиентом даже не приходила. Тестил звуковые модели и раньше, но как-то не подумал об этом сразу.

      Про модели с генерацией за O(1) раньше вообще не слашал, спасибо!


  1. ScavS
    13.09.2023 11:13

    теперь ко всему этому прикрутить GUI


  1. diogen4212
    13.09.2023 11:13

    пример датасета бы глянуть и сам процесс обучения подробнее… не понятно, откуда модель вообще знает персонажей Fate и их характеры


    1. CodeDroidX Автор
      13.09.2023 11:13

      Ага, был удивлён, когда она выдала Арчера и грааль. Кажется, в датасете что-то подобное могло мелькнуть только со страничек wiki, однако не понятно - как оно вообще могло так хорошо запомниться?


  1. CodeDroidX Автор
    13.09.2023 11:13
    +1

    Для тех, кто хочет потестить всё самостоятельно: репозиторий на GitHub с гайдом и ссылками на блокноты в Google Collab.


  1. mrGrixa
    13.09.2023 11:13

    Как я понял тут персонажи зашиты в лору. Было бы интересно увидеть более универсальный вариант с картами персонажей, как в тавернах. Добавить еще скрипты по триерным событиям и вообще игровой движок получится.


    1. CodeDroidX Автор
      13.09.2023 11:13

      В том-то и поинт, что нет) Я не использовал лоры ни для LLama, ни для StableDiffusion.

      Описание героини Fate мне лама выдала самостоятельно, что, собственно не удивительно.

      Counterfeit же корректно рисует её только если написать её имя полностью без ошибок "Rin Tohsaka". Если писать Tosaka или Toosaka, то получается мусор.

      Идея с "карточками" мне понравилась, спасибо