Initial commit: working single-post application

This commit is contained in:
Emile 2021-06-25 18:59:46 +01:00
commit 7e61db4f3c
9 changed files with 2416 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.vscode

2040
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@ -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"

106
src/bot.rs Normal file
View File

@ -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 => (),
}
}
}

75
src/config.rs Normal file
View File

@ -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()
}
}

32
src/generator.rs Normal file
View File

@ -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)
}
}

50
src/main.rs Normal file
View File

@ -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(()),
}
}

36
src/model.rs Normal file
View File

@ -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,
}
}
}

62
src/publish.rs Normal file
View File

@ -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)
}