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