Skip to content

Commit 601c7cb

Browse files
authored
Add trv watch command (#21)
Will spawn a server with live reloading that shows the slides and allows playing the sub-videos.
1 parent 29a3347 commit 601c7cb

File tree

7 files changed

+268
-44
lines changed

7 files changed

+268
-44
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ serde_json = "1.0.138"
1919
serde = { version = "1", features = ["derive"] }
2020
sha2 = "0.10.8"
2121
toml = "0.8"
22+
live-server = "0.10.0"
23+
notify = "8.0"
24+
indoc = "2.0.6"
2225

2326
[dev-dependencies]
2427
assert_cmd = "2"

examples/first.typ

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@
3838
That would be pretty cool.
3939
Here is a plan to make it happen.
4040
")
41-
]
41+
]

src/main.rs

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ mod image;
33
mod path;
44
mod slide;
55
mod video;
6+
mod watch;
67

8+
use crate::slide::Slide;
79
use clap::Parser;
810
use serde::Deserialize;
911
use serde_json::json;
@@ -12,6 +14,7 @@ use std::path::Path;
1214
use std::path::PathBuf;
1315
use tracing::subscriber::SetGlobalDefaultError;
1416
use transformrs::Provider;
17+
use watch::watch;
1518

1619
#[derive(Clone, Debug, Default, Deserialize)]
1720
struct Config {
@@ -110,7 +113,7 @@ enum Task {
110113

111114
#[derive(Parser)]
112115
#[command(author, version, about = "Text and image to video")]
113-
struct Arguments {
116+
pub(crate) struct Arguments {
114117
#[command(subcommand)]
115118
task: Task,
116119

@@ -124,7 +127,7 @@ struct Arguments {
124127

125128
/// Enable caching.
126129
#[arg(long, default_value = "true")]
127-
cache: bool,
130+
cache: Option<bool>,
128131

129132
/// Release.
130133
///
@@ -215,25 +218,9 @@ fn copy_input_with_includes(dir: &str, input: &PathBuf) -> PathBuf {
215218
output_path
216219
}
217220

218-
#[tokio::main]
219-
async fn main() {
220-
let args = Arguments::parse();
221-
if args.verbose {
222-
init_subscriber(tracing::Level::DEBUG).unwrap();
223-
} else {
224-
init_subscriber(tracing::Level::INFO).unwrap();
225-
}
226-
227-
let dir = &args.out_dir;
228-
let path = Path::new(dir);
229-
if !path.exists() {
230-
std::fs::create_dir_all(path).unwrap();
231-
}
232-
let input = match args.task {
233-
Task::Build(args) => args.input,
234-
Task::Watch(args) => args.input,
235-
};
236-
let copied_input = copy_input_with_includes(dir, &input);
221+
pub(crate) async fn build(input: PathBuf, args: &Arguments) -> Vec<Slide> {
222+
let out_dir = &args.out_dir;
223+
let copied_input = copy_input_with_includes(out_dir, &input);
237224
let config = parse_config(&copied_input);
238225

239226
let provider = config.provider.map(|p| provider_from_str(&p));
@@ -254,25 +241,49 @@ async fn main() {
254241
if slides.is_empty() {
255242
panic!("No slides found in input file: {}", input.display());
256243
}
257-
image::generate_images(&copied_input, dir);
244+
image::generate_images(&copied_input, out_dir);
258245
let audio_ext = tts_config
259246
.output_format
260247
.clone()
261248
.unwrap_or("mp3".to_string());
249+
let cache = args.cache.unwrap();
262250
audio::generate_audio_files(
263251
&provider,
264-
dir,
252+
out_dir,
265253
&slides,
266-
args.cache,
254+
cache,
267255
&tts_config,
268256
&config.model,
269257
&audio_ext,
270258
)
271259
.await;
272-
// Using mkv by default because it supports more audio formats.
273-
let output = "out.mkv";
274-
video::generate_video(dir, &slides, args.cache, &tts_config, output, &audio_ext);
260+
let output = "out.mp4";
261+
video::generate_video(out_dir, &slides, cache, &tts_config, output, &audio_ext);
275262
if args.release {
276-
video::generate_release_video(dir, output, "release.mp4", &args.audio_codec);
263+
video::generate_release_video(out_dir, output, "release.mp4", &args.audio_codec);
264+
}
265+
slides
266+
}
267+
268+
#[tokio::main]
269+
async fn main() {
270+
let args = Arguments::parse();
271+
if args.verbose {
272+
init_subscriber(tracing::Level::DEBUG).unwrap();
273+
} else {
274+
init_subscriber(tracing::Level::INFO).unwrap();
277275
}
276+
277+
let dir = &args.out_dir;
278+
let path = Path::new(dir);
279+
if !path.exists() {
280+
std::fs::create_dir_all(path).unwrap();
281+
}
282+
283+
match args.task {
284+
Task::Build(ref build_args) => {
285+
let _ = build(build_args.input.clone(), &args).await;
286+
}
287+
Task::Watch(ref watch_args) => watch(watch_args.input.clone(), &args).await,
288+
};
278289
}

src/path.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub fn video_dir_name() -> &'static str {
4242

4343
pub fn video_path(dir: &str, slide: &Slide) -> PathBuf {
4444
let idx = slide.idx;
45-
// Using mkv by default because it supports more audio formats.
46-
let filename = format!("{idx}.mkv");
45+
// Using mp4 by default because it is widely supported in browsers.
46+
let filename = format!("{idx}.mp4");
4747
Path::new(dir).join(video_dir_name()).join(filename)
4848
}

src/video.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ fn write_concat_list(dir: &str, path: &str, slides: &Vec<Slide>) {
8383
std::fs::write(path, concat_list).expect("couldn't write concat list");
8484
}
8585

86+
// 1920 is the height of a HD YouTube Short.
87+
// It should be a good height for landscape videos too.
88+
// Since the video consists of images, data-wise it should be not a problem to go for a higher resolution.
89+
const HEIGHT: i32 = 1920;
90+
8691
fn create_video_clip(dir: &str, slide: &Slide, cache: bool, config: &TTSConfig, ext: &str) {
8792
tracing::info!("Slide {}: Generating video file...", slide.idx);
8893
let input_audio = crate::path::audio_path(dir, slide, ext);
@@ -106,8 +111,18 @@ fn create_video_clip(dir: &str, slide: &Slide, cache: bool, config: &TTSConfig,
106111
.arg(input_audio)
107112
.arg("-c:v")
108113
.arg("libx264")
114+
.arg("-crf")
115+
.arg("23")
116+
.arg("-preset")
117+
.arg("fast")
118+
.arg("-vf")
119+
.arg(format!("scale=-1:{HEIGHT},format=yuv420p"))
120+
.arg("-pix_fmt")
121+
.arg("yuv420p")
109122
.arg("-c:a")
110-
.arg("copy")
123+
.arg("opus")
124+
.arg("-strict")
125+
.arg("experimental")
111126
.arg("-shortest")
112127
.arg("-tune")
113128
.arg("stillimage")
@@ -187,10 +202,6 @@ pub fn generate_release_video(dir: &str, input: &str, output: &str, audio_codec:
187202
let output_path = Path::new(dir).join(output);
188203
let output_path = output_path.to_str().unwrap();
189204
let mut cmd = std::process::Command::new("ffmpeg");
190-
// 1920 is the height of a HD YouTube Short.
191-
// It should be a good height for landscape videos too.
192-
// Since the video consists of images, data-wise it should be not a problem to go for a higher resolution.
193-
let height = 1920;
194205
let output = cmd
195206
.arg("-y")
196207
.arg("-i")
@@ -202,7 +213,7 @@ pub fn generate_release_video(dir: &str, input: &str, output: &str, audio_codec:
202213
.arg("-preset")
203214
.arg("fast")
204215
.arg("-vf")
205-
.arg(format!("scale=-1:{height},format=yuv420p"))
216+
.arg(format!("scale=-1:{HEIGHT},format=yuv420p"))
206217
.arg("-c:a")
207218
.arg(audio_codec)
208219
.arg("-strict")

0 commit comments

Comments
 (0)