Skip to content

Commit 61537b0

Browse files
Merge branch 'master' of https://github.com/PrideHacks2023/vina
2 parents 4a28b0b + ef3f26a commit 61537b0

File tree

15 files changed

+124
-41
lines changed

15 files changed

+124
-41
lines changed

README.md

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,62 @@
11
<div align="center">
22

3-
# vina
3+
<img src="media/logo.png" alt="vina" />
44

5-
Ai generated visual novel
5+
AI-powered visual novel generator
66

77
[![crates.io](https://img.shields.io/crates/v/vina.svg)](https://crates.io/crates/vina)
88
[![docs.rs](https://docs.rs/vina/badge.svg)](https://docs.rs/vina)
99
[![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](#)
1010

1111
</div>
1212

13+
**VinA** is a visual novel generator. Once you specify a prompt on the type of
14+
story you want, we generate an entire plot, detailed characters with
15+
personalities, locations, music, and more. The result is a fully playable
16+
and polished visual novel you can play.
17+
18+
## Example
19+
20+
With the following prompt:
21+
```
22+
Write a sci-fi story about a hackathon project gone haywire, where twofriends are working
23+
together on a coding project over the weekend. Then, they are sucked into their laptop and
24+
have to find a way back to reality. They overcome an obstacle and successfully return back home.
25+
```
26+
27+
We get this visual novel.
28+
29+
## Features
30+
31+
Dynamic facial expressions depending on the dialogue
32+
<div>
33+
<img src="media/lisa_base.png" width="30%" />
34+
<img src="media/lisa_cry.png" width="30%" />
35+
</div>
36+
<div>
37+
<img src="media/alex_base.png" width="30%" />
38+
<img src="media/alex_anger.png" width="30%" />
39+
</div>
40+
41+
Generated background images for each scene
42+
<div>
43+
<img src="media/bg0.png" width="70%" />
44+
<img src="media/bg1.png" width="70%" />
45+
</div>
46+
47+
## Usage
48+
49+
To run **VinA** for yourself, you need the following:
50+
- An OpenAI API key, find out how to get one [here](https://platform.openai.com/docs/api-reference/authentication)
51+
- An instance of Automatic1111's stable diffusion web UI, ensure the instance you are using has API support. More info [here](https://github.com/AUTOMATIC1111/stable-diffusion-webui)
52+
- RenPy, installation instructions are [here](https://renpy.org/doc/html/quickstart.html)
53+
54+
The following environment variables should be set:
55+
- `REN_PATH`: path to renpy executable
56+
- `OPENAI_KEY`: your openai API key
57+
- `NOVELAI_URL`: url to your instance of the stable diffusion web UI
58+
59+
## What's with the name?
60+
61+
**VinA** is an anagram of the much less creative name, 'AI VN'.
1362

crates/vina_story/src/api.rs

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,20 @@ impl ApiClient {
101101

102102
/// Parse a function call
103103
pub fn parse_fncall<T: DeserializeOwned>(msg: &Value) -> anyhow::Result<T> {
104+
let fn_args = parse_fncall_raw(msg)?;
105+
let downcasted = serde_json::from_value(fn_args)?;
106+
Ok(downcasted)
107+
}
108+
109+
pub fn parse_fncall_raw(msg: &Value) -> anyhow::Result<Value> {
104110
let fn_call = &msg["function_call"];
105111
let fn_name = fn_call["name"].as_str().unwrap();
106112

107113
// TODO hardcoded inner key (since most of time we only have one argument)
108114
let fn_args = fn_call["arguments"].as_str().unwrap();
109115
let mut fn_args: Value = serde_json::from_str(fn_args).unwrap();
110116
let fn_args = fn_args["inner"].take();
111-
112-
let downcasted = serde_json::from_value(fn_args)?;
113-
114-
Ok(downcasted)
117+
Ok(fn_args)
115118
}
116119

117120
/// Parse text content
@@ -177,7 +180,9 @@ pub fn get_scenes_fn() -> Value {
177180
"description:": "Descriptive title of the scene based on it's contents",
178181
},
179182
"music": {
180-
"type": "string",
183+
"enum": [
184+
"Funky", "Calm", "Dark", "Inspirational", "Bright", "Dramatic", "Happy", "Romantic", "Angry", "Sad"
185+
],
181186
"description": "Genre of music that should be played in this scene",
182187
},
183188
"location": {
@@ -196,41 +201,48 @@ pub fn get_scenes_fn() -> Value {
196201
"type": "string",
197202
"description": "Landmarks and objects of focus that are present in the scene. Omit any descriptions of people.",
198203
},
199-
"mood": {
200-
"type": "string",
201-
"description": "Information about the mood. Omit any descriptions of people.",
202-
},
203204
"time_of_day": {
204205
"type": "string",
205206
"description": "What time of day it is",
206207
},
207208
}
208209
},
209-
"script": {
210-
"type": "array",
211-
"items": {
212-
"type": "object",
213-
"description": "A line in the script, contains information like the speaker, choose a facial expression from this list: smiling, crying, nervous, excited, blushing to match what is being said, and also what is being said",
214-
"properties": {
215-
"speaker": {
216-
"type": "string",
217-
"description": "Name of the speaker"
218-
},
219-
"facial_expression": {
220-
"type": "string",
221-
"description": "Use an emotion from this list: smiling, crying, nervous, excited, blushing to match the dialogue spoken"
222-
},
223-
"content": {
224-
"type": "string",
225-
"description": "What the speaker actually says"
226-
}
227-
}
228-
}
210+
}
211+
}
212+
},
213+
},
214+
"required": ["inner"],
215+
}
216+
})
217+
}
229218

219+
pub fn get_script_fn() -> Value {
220+
json!({
221+
"name": "get_script_fn",
222+
"description": "Script to be used in scene",
223+
"parameters": {
224+
"type": "object",
225+
"properties": {
226+
"inner": {
227+
"type": "array",
228+
"items": {
229+
"type": "object",
230+
"description": "A line in the script, contains information like the speaker, what is being said, and facial expression",
231+
"properties": {
232+
"speaker": {
233+
"type": "string",
234+
"description": "Name of the speaker"
230235
},
236+
"facial_expression": {
237+
"type": "string",
238+
"description": "Use an emotion from this list: smiling, crying, nervous, excited, blushing to match the dialogue spoken"
239+
},
240+
"content": {
241+
"type": "string",
242+
"description": "What the speaker says"
243+
}
231244
}
232245
}
233-
234246
},
235247

236248
},

crates/vina_story/src/content.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ pub struct Location {
5353
pub description: String,
5454
/// Concrete objects and landmarks in the scene
5555
pub landmarks: String,
56-
/// Information on the mood and time of day
57-
pub mood: String,
5856
/// Time of day
5957
pub time_of_day: String,
6058
}

crates/vina_story/src/lib.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ pub mod api;
44
pub mod content;
55
pub mod music;
66

7-
use content::Location;
7+
use content::{Dialogue, Location};
8+
use serde_json::{json, Value};
89

910
use crate::{
1011
api::*,
@@ -15,22 +16,45 @@ pub fn generate_story(token: &str, prompt: &str) -> anyhow::Result<Game> {
1516
// Client to generate details of the story
1617
let mut story_client = ApiClient::new(token);
1718

18-
story_client.run_prompt(prompt, None).unwrap();
19+
let res = story_client.run_prompt(prompt, None).unwrap();
20+
let game_name = parse_content(res)?;
1921

2022
story_client
21-
.run_prompt("Generate a title for this story", None)
23+
.run_prompt("Generate a short game title for this story", None)
2224
.unwrap();
2325

2426
let res = story_client.run_prompt("Limit the number of characters to a maximum of 3. Give me each of the characters in the story, along with detailed personality, clothing, and physical appearance details (include age, race, gender).", Some(get_characters_fn())).unwrap();
2527

2628
let characters: Vec<Character> = parse_fncall(&res).unwrap();
2729
// println!("CHARACTERS {:?}", characters);
2830

29-
let res = story_client.run_prompt("Limit the number of locations to a maximum of 5. Separate the story into multiple scenes, and for each scene give me a long and detailed description of the setting of the scene, omit any descriptions of people, include the name of the location, physical location it takes place in, objects and landmarks in the scene, mood, and time of day. Also create a title each scene that corresponds to the contents of the scene. Furthermore, for each scene, write me a script and return the result in a list with each element as a character's dialogue, and use a facial expression from this list: smiling, crying, nervous, excited, blushing to match the dialogue spoken. Also For each scene, tell me the music genre from this list Funky, Calm, Dark, Inspirational, Bright, Dramatic, Happy, Romantic, Angry, Sad", Some(get_scenes_fn())).unwrap();
31+
let res = story_client.run_prompt("Separate the story into multiple scenes, and for each scene give me a long and detailed description of the setting of the scene, omit any descriptions of people, include the name of the location, physical location it takes place in, objects and landmarks in the scene, mood, and time of day. Also create a title each scene that corresponds to the contents of the scene. Furthermore, for each scene, write me a script and return the result in a list with each element as a character's dialogue, and use a facial expression from this list: smiling, crying, nervous, excited, blushing to match the dialogue spoken. Also For each scene, tell me the music genre from this list Funky, Calm, Dark, Inspirational, Bright, Dramatic, Happy, Romantic, Angry, Sad", Some(get_scenes_fn())).unwrap();
3032

31-
let scenes: Vec<Scene> = parse_fncall(&res).unwrap();
33+
let raw_scenes: Value = parse_fncall_raw(&res).unwrap();
3234
// println!("SCENES {:?}", scenes);
3335

36+
let mut val_scenes: Vec<Value> = vec![];
37+
for (i, raw_scene) in raw_scenes.as_array().unwrap().iter().enumerate() {
38+
let scene_number = i + 1;
39+
40+
let prompt = format!(
41+
r#"For scene {scene_number}, write me a script with a lot of speaking. Prioritize number of lines of dialogue. When writing each line of dialogue, take into account the personality and mood of the character as well as the setting. Do not use a narrator. Ensure that the script transitions smoothly into the next scene. Return the result in a list. Also include facial expression from this list: smiling, crying, nervous, excited, blushing to match the dialogue spoken. Output as json."#
42+
);
43+
let res = story_client
44+
.run_prompt(&prompt, Some(get_script_fn()))
45+
.unwrap();
46+
47+
let script: Vec<Dialogue> = parse_fncall(&res).unwrap();
48+
49+
// construct finished scene
50+
let mut obj_scene = raw_scene.as_object().unwrap().clone();
51+
obj_scene.insert(String::from("script"), json! {script});
52+
val_scenes.push(Value::Object(obj_scene));
53+
}
54+
// println!("BUILT SCENE {val_scenes:?}");
55+
56+
let scenes: Vec<Scene> = serde_json::from_value(Value::Array(val_scenes)).unwrap();
57+
3458
let game = Game {
3559
name: String::from("VinaGame"),
3660
synopsis: String::new(),
@@ -51,8 +75,8 @@ pub fn generate_location_prompt(token: &str, location: &Location) -> anyhow::Res
5175
generate_prompt(
5276
token,
5377
&format!(
54-
"{}. {}. {}. {}",
55-
location.description, location.landmarks, location.mood, location.time_of_day
78+
"{}. {}. {}",
79+
location.description, location.landmarks, location.time_of_day
5680
),
5781
)
5882
}

media/alex_anger.png

301 KB
Loading

media/alex_base.png

295 KB
Loading

media/bg0.png

863 KB
Loading

media/bg1.png

717 KB
Loading

media/hackathon_bg0.png

896 KB
Loading

media/hackathon_bg1.png

932 KB
Loading

0 commit comments

Comments
 (0)