Initial commit: working single-post application
This commit is contained in:
commit
7e61db4f3c
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
.vscode
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "izzilis"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Emilis <grinding@graduate.org>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
elefren = { version = "0.22.0", features = ["toml"] }
|
||||||
|
rand = "0.8.4"
|
||||||
|
serde = "1.0.126"
|
||||||
|
serde_json = "1.0.64"
|
|
@ -0,0 +1,106 @@
|
||||||
|
use rand::Rng;
|
||||||
|
use std::{error::Error, io};
|
||||||
|
|
||||||
|
use crate::{generator, model, publish};
|
||||||
|
|
||||||
|
pub struct IzzilisBot<T: model::SampleModel, U: publish::Publisher> {
|
||||||
|
generator: generator::Generator<T>,
|
||||||
|
publisher: U, // One day I'll figure out how to make this a vector with differing Publisher types
|
||||||
|
loaded_samples: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U> IzzilisBot<T, U>
|
||||||
|
where
|
||||||
|
T: model::SampleModel,
|
||||||
|
U: publish::Publisher,
|
||||||
|
{
|
||||||
|
pub fn new(generator: generator::Generator<T>, publisher: U) -> IzzilisBot<T, U> {
|
||||||
|
Self {
|
||||||
|
generator,
|
||||||
|
publisher,
|
||||||
|
loaded_samples: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_samples(&mut self) -> Option<io::Error> {
|
||||||
|
let lines_result = self.generator.generate_sample_lines();
|
||||||
|
let lines = match lines_result {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(err) => return Some(err),
|
||||||
|
};
|
||||||
|
self.loaded_samples = lines; // wtf happens to the original self.loaded_samples???????
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn publish(&mut self) -> Option<Box<dyn Error>> {
|
||||||
|
if self.loaded_samples.len() < 5 {
|
||||||
|
// Refresh samples. Either none have been generated so far,
|
||||||
|
// or generated ones are stale.
|
||||||
|
//
|
||||||
|
// This is a shit solution, but I'm going with it for v1
|
||||||
|
// purely because I don't know the language well enough to be
|
||||||
|
// confident in doing this via threads. Yet.
|
||||||
|
self.generate_samples();
|
||||||
|
}
|
||||||
|
let sample_index = rand::thread_rng().gen_range(0..self.loaded_samples.len() - 1);
|
||||||
|
let content = self.loaded_samples[sample_index].clone();
|
||||||
|
self.loaded_samples.remove(sample_index);
|
||||||
|
|
||||||
|
match self.publisher.publish(content) {
|
||||||
|
Some(err) => Some(err),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(tests)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::{self, ErrorKind};
|
||||||
|
|
||||||
|
use crate::{generator, model, publish};
|
||||||
|
|
||||||
|
struct fake_sampler {
|
||||||
|
should_ok: bool,
|
||||||
|
ok_str: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct fake_publisher {
|
||||||
|
should_ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl model::SampleModel for fake_sampler {
|
||||||
|
fn get_sample(&self) -> Result<String, io::Error> {
|
||||||
|
if self.should_ok {
|
||||||
|
return Ok(self.ok_str.clone());
|
||||||
|
}
|
||||||
|
Err(io::Error::new(ErrorKind::NotFound, "error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl publish::Publisher for fake_publisher {
|
||||||
|
fn publish(&self, content: String) -> Option<Box<dyn std::error::Error>> {
|
||||||
|
if self.should_ok {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Box::new(io::Error::new(ErrorKind::NotFound, "error")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_samples_populates() {
|
||||||
|
let model_ok_string = String::from("model_ok");
|
||||||
|
let model = fake_sampler {
|
||||||
|
should_ok: true,
|
||||||
|
ok_str: model_ok_string,
|
||||||
|
};
|
||||||
|
let gen = generator::Generator::new(model);
|
||||||
|
let publish = fake_publisher { should_ok: true };
|
||||||
|
let mut bot = super::IzzilisBot::new(gen, publish);
|
||||||
|
|
||||||
|
match bot.publish() {
|
||||||
|
Some(_) => panic!("publish failed"),
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
python_path: String,
|
||||||
|
model_name: String,
|
||||||
|
temperature: String,
|
||||||
|
top_k: String,
|
||||||
|
gpt_code_path: String,
|
||||||
|
fediverse_base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn default() -> Config {
|
||||||
|
Config {
|
||||||
|
python_path: String::from("/usr/bin/python3"),
|
||||||
|
model_name: String::from("117M"),
|
||||||
|
temperature: String::from("1"),
|
||||||
|
top_k: String::from("40"),
|
||||||
|
gpt_code_path: String::from("."),
|
||||||
|
fediverse_base_url: String::from("https://lain.com"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from(path: String) -> Result<Config, Box<dyn Error>> {
|
||||||
|
let file_bytes = std::fs::read(path)?;
|
||||||
|
match serde_json::from_slice(&file_bytes) {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(Box::new(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, path: String) -> Option<Box<dyn Error>> {
|
||||||
|
let cfg_json = match serde_json::to_vec(self) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(err) => return Some(Box::new(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match std::fs::write(path, &cfg_json) {
|
||||||
|
Ok(_) => None,
|
||||||
|
Err(err) => Some(Box::new(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the config's python path.
|
||||||
|
pub fn python_path(&self) -> String {
|
||||||
|
self.python_path.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the config's model name.
|
||||||
|
pub fn model_name(&self) -> String {
|
||||||
|
self.model_name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the config's temperature.
|
||||||
|
pub fn temperature(&self) -> String {
|
||||||
|
self.temperature.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the config's top k.
|
||||||
|
pub fn top_k(&self) -> String {
|
||||||
|
self.top_k.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the config's gpt code path.
|
||||||
|
pub fn gpt_code_path(&self) -> String {
|
||||||
|
self.gpt_code_path.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fediverse_base_url(&self) -> String {
|
||||||
|
self.fediverse_base_url.clone()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crate::model;
|
||||||
|
const SAMPLE_SPLIT_WORD: &str = "<|endoftext|>";
|
||||||
|
const SAMPLE_SAMPLE_LINE: &str =
|
||||||
|
"======================================== SAMPLE 1 ========================================";
|
||||||
|
pub struct Generator<T: model::SampleModel> {
|
||||||
|
model: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Why did this fucking shit take so long to sort out??
|
||||||
|
impl<T> Generator<T>
|
||||||
|
where
|
||||||
|
T: model::SampleModel,
|
||||||
|
{
|
||||||
|
pub fn new(model: T) -> Generator<T> {
|
||||||
|
Self { model }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_sample_lines(&self) -> Result<Vec<String>, io::Error> {
|
||||||
|
let unwashed_sample = self.model.get_sample()?;
|
||||||
|
// Cursed, I just wanted a Select
|
||||||
|
let mut washed_sample: Vec<String> = Vec::new();
|
||||||
|
unwashed_sample
|
||||||
|
.replace(SAMPLE_SAMPLE_LINE, "")
|
||||||
|
.split(SAMPLE_SPLIT_WORD)
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|elem| washed_sample.push(elem.trim().to_string()));
|
||||||
|
|
||||||
|
Ok(washed_sample)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
use std::{error::Error, process};
|
||||||
|
|
||||||
|
mod bot;
|
||||||
|
mod config;
|
||||||
|
mod generator;
|
||||||
|
mod model;
|
||||||
|
mod publish;
|
||||||
|
|
||||||
|
const CONFIG_PATH: &str = "bot_config.json";
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let cfg = match config::Config::from(CONFIG_PATH.to_string()) {
|
||||||
|
Ok(cfg) => cfg,
|
||||||
|
Err(_) => {
|
||||||
|
println!(
|
||||||
|
"Failed reading config at [{}], writing default",
|
||||||
|
CONFIG_PATH
|
||||||
|
);
|
||||||
|
match config::Config::default().save(CONFIG_PATH.to_string()) {
|
||||||
|
Some(err) => println!("Failed writing file to {}: {}", CONFIG_PATH, err),
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let gpt_model = model::GPTSampleModel::new(
|
||||||
|
cfg.python_path(),
|
||||||
|
cfg.gpt_code_path(),
|
||||||
|
vec![
|
||||||
|
"generate_unconditional_samples.py".to_string(),
|
||||||
|
"--model_name".to_string(),
|
||||||
|
cfg.model_name(),
|
||||||
|
"--temperature".to_string(),
|
||||||
|
cfg.temperature(),
|
||||||
|
"--top_k".to_string(),
|
||||||
|
cfg.top_k(),
|
||||||
|
"--nsamples".to_string(),
|
||||||
|
"1".to_string(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let publisher = publish::FediversePublisher::new(cfg.fediverse_base_url())?;
|
||||||
|
let gen = generator::Generator::new(gpt_model);
|
||||||
|
let mut bot = bot::IzzilisBot::new(gen, publisher);
|
||||||
|
|
||||||
|
match bot.publish() {
|
||||||
|
Some(err) => Err(err),
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
use std::{io, process::Command};
|
||||||
|
|
||||||
|
pub trait SampleModel {
|
||||||
|
fn get_sample(&self) -> Result<String, io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GPTSampleModel {
|
||||||
|
python_command: String,
|
||||||
|
command_working_path: String,
|
||||||
|
command_args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SampleModel for GPTSampleModel {
|
||||||
|
fn get_sample(&self) -> Result<String, io::Error> {
|
||||||
|
let cmd_output = Command::new(&self.python_command)
|
||||||
|
.current_dir(&self.command_working_path)
|
||||||
|
.args(&self.command_args)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(String::from_utf8_lossy(&cmd_output.stdout).to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GPTSampleModel {
|
||||||
|
pub fn new(
|
||||||
|
python_command: String,
|
||||||
|
command_working_path: String,
|
||||||
|
command_args: Vec<String>,
|
||||||
|
) -> GPTSampleModel {
|
||||||
|
Self {
|
||||||
|
python_command: python_command,
|
||||||
|
command_working_path: command_working_path,
|
||||||
|
command_args: command_args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use elefren::{
|
||||||
|
helpers::{cli, toml},
|
||||||
|
scopes::Scopes,
|
||||||
|
status_builder::Visibility,
|
||||||
|
Language, Mastodon, MastodonClient, Registration, StatusBuilder,
|
||||||
|
};
|
||||||
|
pub trait Publisher {
|
||||||
|
fn publish(&self, content: String) -> Option<Box<dyn Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FediversePublisher {
|
||||||
|
client: Mastodon,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FediversePublisher {
|
||||||
|
pub fn new(fedi_url: String) -> Result<FediversePublisher, Box<dyn Error>> {
|
||||||
|
let fedi = if let Ok(data) = toml::from_file("fediverse.toml") {
|
||||||
|
Mastodon::from(data)
|
||||||
|
} else {
|
||||||
|
register(fedi_url)?
|
||||||
|
};
|
||||||
|
Ok(Self { client: fedi })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Publisher for FediversePublisher {
|
||||||
|
fn publish(&self, content: String) -> Option<Box<dyn Error>> {
|
||||||
|
let status_build_result = StatusBuilder::new()
|
||||||
|
.status(&content)
|
||||||
|
// .visibility(Visibility::Direct)
|
||||||
|
.visibility(Visibility::Public)
|
||||||
|
.sensitive(false)
|
||||||
|
.language(Language::Eng)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let status = match status_build_result {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(err) => return Some(Box::new(err)),
|
||||||
|
};
|
||||||
|
println!("Posting status [{}] to fediverse", &content);
|
||||||
|
|
||||||
|
match self.client.new_status(status) {
|
||||||
|
Ok(_) => None,
|
||||||
|
Err(err) => Some(Box::new(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register(fedi_url: String) -> Result<Mastodon, Box<dyn Error>> {
|
||||||
|
let registration = Registration::new(fedi_url)
|
||||||
|
.client_name("izzilis")
|
||||||
|
.scopes(Scopes::write_all())
|
||||||
|
.build()?;
|
||||||
|
let fediverse = cli::authenticate(registration)?;
|
||||||
|
|
||||||
|
// Save app data for using on the next run.
|
||||||
|
toml::to_file(&*fediverse, "fediverse.toml")?;
|
||||||
|
|
||||||
|
Ok(fediverse)
|
||||||
|
}
|
Loading…
Reference in New Issue