Skip to content

Commit 9d7c2f7

Browse files
authored
Show only image and audio during watch (#32)
This speeds up the workflow since it avoids having to combine the image and audio into a video file.
1 parent 384d192 commit 9d7c2f7

File tree

3 files changed

+115
-45
lines changed

3 files changed

+115
-45
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,24 @@ $ whisper _out/out.mp4 -f srt --model small --language=en
201201
```
202202

203203
This will create a `out.srt` file with the subtitles.
204+
205+
## Static Videos
206+
207+
The videos created by `trv` consist only of static images.
208+
This might seem limiting, but as long as the content of the video is high, static images should be fine.
209+
Here are some YouTubers that have hundreds of thousands to millions of views with only static images:
210+
211+
- [Perun](https://www.youtube.com/@PerunAU)
212+
- [The Histocrat](https://www.youtube.com/@TheHistocrat)
213+
- [Christopher Manning](https://youtu.be/5Aer7MUSuSU)
214+
- [Richard McElreath](https://www.youtube.com/@rmcelreath)
215+
216+
Static images with a talking-head:
217+
218+
- [Tony Seba](https://www.youtube.com/@tonyseba)
219+
- [The Wild West Extravaganza](https://www.youtube.com/@WildWestExtravaganza)
220+
- [Andrej Karpathy](https://youtu.be/zjkBMFhNj_g)
221+
222+
Static images with a computer-generated moving hand:
223+
224+
- [Simplilearn](https://www.youtube.com/@SimplilearnOfficial)

src/main.rs

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ use std::collections::HashMap;
1313
use std::path::Path;
1414
use std::path::PathBuf;
1515
use tracing::subscriber::SetGlobalDefaultError;
16+
use transformrs::text_to_speech::TTSConfig;
1617
use transformrs::Provider;
1718
use watch::watch;
1819

1920
#[derive(Clone, Debug, Default, Deserialize)]
20-
struct Config {
21+
pub(crate) struct Config {
2122
/// Provider.
2223
///
2324
/// Can be used to pass for example
@@ -185,28 +186,32 @@ fn init_subscriber(level: tracing::Level) -> Result<(), SetGlobalDefaultError> {
185186
tracing::subscriber::set_global_default(subscriber)
186187
}
187188

189+
fn tts_config(config: &Config, provider: &Provider) -> TTSConfig {
190+
let mut other = HashMap::new();
191+
if provider != &Provider::Google {
192+
other.insert("seed".to_string(), json!(42));
193+
}
194+
TTSConfig {
195+
voice: Some(config.voice.clone()),
196+
output_format: config.audio_format.clone(),
197+
speed: config.speed,
198+
other: Some(other),
199+
language_code: config.language_code.clone(),
200+
}
201+
}
202+
188203
pub(crate) async fn build(
189204
input: PathBuf,
205+
config: &Config,
190206
args: &Arguments,
191207
release: bool,
192208
audio_codec: Option<String>,
193209
) -> Vec<Slide> {
194210
let out_dir = &args.out_dir;
195-
let config = parse_config(&input);
196211

197-
let provider = config.provider.map(|p| provider_from_str(&p));
212+
let provider = config.provider.as_ref().map(|p| provider_from_str(p));
198213
let provider = provider.unwrap_or(Provider::DeepInfra);
199-
let mut other = HashMap::new();
200-
if provider != Provider::Google {
201-
other.insert("seed".to_string(), json!(42));
202-
}
203-
let tts_config = transformrs::text_to_speech::TTSConfig {
204-
voice: Some(config.voice.clone()),
205-
output_format: config.audio_format.clone(),
206-
speed: config.speed,
207-
other: Some(other),
208-
language_code: config.language_code.clone(),
209-
};
214+
let tts_config = tts_config(config, &provider);
210215

211216
let slides = slide::slides(input.to_str().unwrap());
212217
if slides.is_empty() {
@@ -229,8 +234,8 @@ pub(crate) async fn build(
229234
)
230235
.await;
231236
let output = "out.mp4";
232-
video::create_video_clips(out_dir, &slides, cache, &tts_config, &audio_ext);
233237
if release {
238+
video::create_video_clips(out_dir, &slides, cache, &tts_config, &audio_ext);
234239
let audio_codec = audio_codec.unwrap();
235240
video::combine_video(out_dir, &slides, output, &audio_codec);
236241
}
@@ -256,8 +261,19 @@ async fn main() {
256261
Task::Build(ref build_args) => {
257262
let release = true;
258263
let audio_codec = Some(build_args.audio_codec.clone());
259-
let _ = build(build_args.input.clone(), &args, release, audio_codec).await;
264+
let config = parse_config(&build_args.input);
265+
let _ = build(
266+
build_args.input.clone(),
267+
&config,
268+
&args,
269+
release,
270+
audio_codec,
271+
)
272+
.await;
273+
}
274+
Task::Watch(ref watch_args) => {
275+
let config = parse_config(&watch_args.input);
276+
watch(watch_args, &config, &args).await
260277
}
261-
Task::Watch(ref watch_args) => watch(watch_args, &args).await,
262278
};
263279
}

src/watch.rs

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::build;
22
use crate::slide::Slide;
33
use crate::Arguments;
4+
use crate::Config;
45
use crate::WatchArgs;
56
use ignore::Walk;
67
use live_server::listen;
@@ -13,35 +14,50 @@ use std::path::Path;
1314
use std::path::PathBuf;
1415
use std::sync::mpsc;
1516

17+
/// Add a timestamp to the filename.
18+
///
19+
/// This is used to bust the browser cache (force update).
1620
fn add_timestamp(filename: &OsStr, timestamp: u64) -> String {
1721
let path = Path::new(filename);
1822
let stem = path.file_stem().unwrap().to_str().unwrap();
1923
let extension = path.extension().unwrap_or_default().to_str().unwrap_or("");
2024
format!("{}_{}.{}", stem, timestamp, extension)
2125
}
2226

23-
fn core_html(out_dir: &str, slide: &Slide, timestamp: u64) -> String {
24-
let video_path = crate::path::video_path(out_dir, slide);
25-
let filename = video_path.file_name().unwrap();
26-
let filename = add_timestamp(filename, timestamp);
27+
fn core_html(out_dir: &str, slide: &Slide, timestamp: u64, config: &Config) -> String {
28+
let image_path = crate::path::image_path(out_dir, slide);
29+
let image_file = image_path.file_name().unwrap();
30+
let image_file = add_timestamp(image_file, timestamp);
31+
let audio_ext = config.audio_format.as_ref().unwrap();
32+
let audio_path = crate::path::audio_path(out_dir, slide, audio_ext);
33+
let audio_file = audio_path.file_name().unwrap();
34+
let audio_file = add_timestamp(audio_file, timestamp);
2735
format!(
2836
indoc::indoc! {"
29-
<h2>Slide {}</h2>
37+
<div class='slide'>
38+
<h2>Slide {}</h2>
3039
31-
<video controls>
32-
<source src='{}' type='video/mp4'>
33-
Your browser does not support the video tag.
34-
</video>
40+
<a href='{}'>
41+
<img src='{}' alt='Slide {}'/><br/>
42+
</a>
43+
<audio controls src='{}'/></audio>
44+
</div>
3545
"},
36-
slide.idx, filename
46+
slide.idx, image_file, image_file, slide.idx, audio_file
3747
)
3848
}
3949

40-
fn index(args: &Arguments, slides: &[Slide], timestamp: u64, init: bool) -> String {
50+
fn index(
51+
args: &Arguments,
52+
config: &Config,
53+
slides: &[Slide],
54+
timestamp: u64,
55+
init: bool,
56+
) -> String {
4157
let out_dir = &args.out_dir;
4258
let core = slides
4359
.iter()
44-
.map(|slide| core_html(out_dir, slide, timestamp))
60+
.map(|slide| core_html(out_dir, slide, timestamp, config))
4561
.collect::<Vec<_>>()
4662
.join("\n");
4763
let waiting_text = if init {
@@ -65,8 +81,19 @@ fn index(args: &Arguments, slides: &[Slide], timestamp: u64, init: bool) -> Stri
6581
body {{
6682
text-align: center;
6783
}}
68-
video {{
84+
img {{
6985
max-width: 800px;
86+
max-height: 80vh;
87+
border: 1px solid black;
88+
}}
89+
audio {{
90+
width: 800px;
91+
}}
92+
.slide {{
93+
margin-bottom: 60px;
94+
}}
95+
.slide h2 {{
96+
margin-bottom: 10px;
7097
}}
7198
</style>
7299
</head>
@@ -88,8 +115,8 @@ fn public_dir(args: &Arguments) -> PathBuf {
88115
public_path
89116
}
90117

91-
fn build_index(args: &Arguments, slides: &[Slide], timestamp: u64, init: bool) {
92-
let index = index(args, slides, timestamp, init);
118+
fn build_index(args: &Arguments, config: &Config, slides: &[Slide], timestamp: u64, init: bool) {
119+
let index = index(args, config, slides, timestamp, init);
93120
let path = public_dir(args).join("index.html");
94121
tracing::info!("Writing index.html");
95122
std::fs::write(path, index).unwrap();
@@ -105,18 +132,24 @@ fn timestamp() -> u64 {
105132
.as_secs()
106133
}
107134

108-
fn move_files_into_public(args: &Arguments, slides: &[Slide]) -> u64 {
135+
fn move_files_into_public(args: &Arguments, config: &Config, slides: &[Slide]) -> u64 {
109136
let public_path = public_dir(args);
110137
let out_dir = &args.out_dir;
111138

112139
let timestamp = timestamp();
113140

114141
for slide in slides {
115-
let video_path = crate::path::video_path(out_dir, slide);
116-
let filename = video_path.file_name().unwrap();
142+
let image_path = crate::path::image_path(out_dir, slide);
143+
let filename = image_path.file_name().unwrap();
117144
let filename = add_timestamp(filename, timestamp);
118145

119-
std::fs::copy(video_path, public_path.join(filename)).unwrap();
146+
std::fs::copy(image_path, public_path.join(filename)).unwrap();
147+
148+
let audio_ext = config.audio_format.clone().unwrap();
149+
let audio_path = crate::path::audio_path(out_dir, slide, &audio_ext);
150+
let filename = audio_path.file_name().unwrap();
151+
let filename = add_timestamp(filename, timestamp);
152+
std::fs::copy(audio_path, public_path.join(filename)).unwrap();
120153
}
121154
timestamp
122155
}
@@ -130,7 +163,7 @@ fn remove_old_files(args: &Arguments, timestamp: u64) {
130163
continue;
131164
}
132165
if let Some(extension) = path.extension() {
133-
if extension == "mp4" {
166+
if extension != "html" {
134167
let filename = path.file_name().unwrap().to_str().unwrap();
135168
if !filename.contains(&format!("_{}", timestamp)) {
136169
std::fs::remove_file(path).unwrap();
@@ -169,16 +202,16 @@ fn run_pre_typst(watch_args: &WatchArgs) -> Status {
169202
Status::Success
170203
}
171204

172-
async fn watch_build(watch_args: &WatchArgs, args: &Arguments) {
205+
async fn watch_build(watch_args: &WatchArgs, config: &Config, args: &Arguments) {
173206
let release = false;
174207
let input = watch_args.input.clone();
175208
let audio_codec = None;
176209

177210
let status = run_pre_typst(watch_args);
178211
if status == Status::Success {
179-
let slides = build(input.clone(), args, release, audio_codec).await;
180-
let timestamp = move_files_into_public(args, &slides);
181-
build_index(args, &slides, timestamp, false);
212+
let slides = build(input.clone(), config, args, release, audio_codec).await;
213+
let timestamp = move_files_into_public(args, config, &slides);
214+
build_index(args, config, &slides, timestamp, false);
182215
remove_old_files(args, timestamp);
183216
}
184217
}
@@ -203,7 +236,7 @@ fn spawn_server(watch_args: &WatchArgs, args: &Arguments) {
203236
});
204237
}
205238

206-
pub async fn watch(watch_args: &WatchArgs, args: &Arguments) {
239+
pub async fn watch(watch_args: &WatchArgs, config: &Config, args: &Arguments) {
207240
let (tx, rx) = mpsc::channel::<Result<Event>>();
208241
let mut watcher = recommended_watcher(tx).unwrap();
209242
let mode = notify::RecursiveMode::NonRecursive;
@@ -228,14 +261,14 @@ pub async fn watch(watch_args: &WatchArgs, args: &Arguments) {
228261

229262
let slides = [];
230263
let timestamp = timestamp();
231-
build_index(args, &slides, timestamp, true);
264+
build_index(args, config, &slides, timestamp, true);
232265
spawn_server(watch_args, args);
233-
watch_build(watch_args, args).await;
266+
watch_build(watch_args, config, args).await;
234267

235268
for result in &rx {
236269
match result {
237270
Ok(_event) => {
238-
watch_build(watch_args, args).await;
271+
watch_build(watch_args, config, args).await;
239272
// Drain the channel to avoid processing old events.
240273
while rx.try_recv().is_ok() {}
241274
}

0 commit comments

Comments
 (0)