Compare commits

...

5 commits

Author SHA1 Message Date
52c89dc95a
fix british dates
All checks were successful
Trigger quay.io Webhook / run (push) Successful in 5s
2024-05-26 16:15:36 +02:00
4aefb5a647
presets 2024-05-26 16:15:07 +02:00
14daa1c9f9
add missing file 2024-05-26 15:23:31 +02:00
78609dec9a
translate question 2024-05-26 15:23:23 +02:00
1dfe835587
move project structs into their own mod 2024-05-26 15:20:40 +02:00
6 changed files with 277 additions and 207 deletions

View file

@ -24,7 +24,9 @@ pub struct Language<'a> {
download_videos: &'a str,
questions_feedback: &'a str,
// metadata
pub(crate) from: &'a str
pub(crate) from: &'a str,
// questions
pub(crate) question: &'a str
}
pub const GERMAN: Language<'static> = Language {
@ -58,7 +60,9 @@ pub const GERMAN: Language<'static> = Language {
download_videos: "Videos herunterladen",
questions_feedback: "Fragen, Vorschläge und Feedback",
from: "vom"
from: "vom",
question: "Frage"
};
pub const BRITISH: Language<'static> = Language {
@ -87,7 +91,7 @@ pub const BRITISH: Language<'static> = Language {
3 | 23 => "rd",
_ => "th"
};
format!("{:02}{th} {month} {:04}", d.day, d.year)
format!("{}{th} {month} {:04}", d.day, d.year)
},
lecture_from: "Lecture from",
@ -98,7 +102,9 @@ pub const BRITISH: Language<'static> = Language {
download_videos: "Download videos",
questions_feedback: "Questions, Suggestions and Feedback",
from: "from"
from: "from",
question: "Question"
};
impl Default for Language<'static> {

View file

@ -3,30 +3,27 @@
#![forbid(elided_lifetimes_in_paths, unsafe_code)]
mod iotro;
mod preset;
mod project;
mod question;
mod render;
mod time;
use self::{
iotro::Language,
question::Question,
render::{ffmpeg::FfmpegOutputFormat, Renderer},
time::{parse_date, parse_time, Date, Time}
project::{Project, ProjectLecture, ProjectSource, Resolution},
render::Renderer,
time::{parse_date, parse_time, Time}
};
use crate::preset::Preset;
use camino::Utf8PathBuf as PathBuf;
use clap::Parser;
use console::style;
use rational::Rational;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
#[cfg(feature = "mem_limit")]
use std::sync::RwLock;
use std::{
collections::BTreeSet,
fmt::Display,
fs,
io::{self, BufRead as _, Write},
str::FromStr
io::{self, BufRead as _, Write}
};
#[cfg(feature = "mem_limit")]
@ -38,34 +35,24 @@ struct Args {
#[clap(short = 'C', long, default_value = ".")]
directory: PathBuf,
/// The slug of the course, e.g. "23ws-malo2".
#[clap(short = 'c', long, default_value = "23ws-malo2")]
course: String,
/// The label of the course, e.g. "Mathematische Logik II".
#[clap(short, long, default_value = "Mathematische Logik II")]
label: String,
/// The docent of the course, e.g. "Prof. E. Grädel".
#[clap(short, long, default_value = "Prof. E. Grädel")]
docent: String,
/// The language of the lecture. Used for the intro and outro frame.
#[clap(short = 'L', long, default_value = "de")]
lang: Language<'static>,
/// The preset of the lecture. Can be a toml file or a known course slug.
#[clap(short, long)]
preset: String,
#[cfg(feature = "mem_limit")]
/// The memory limit for external tools like ffmpeg.
#[clap(short, long, default_value = "12G")]
mem_limit: String,
/// Transcode the final video clip down to the minimum resolution specified.
/// Transcode the final video clip down to the minimum resolution specified. If not
/// specified, the default value from the preset is used.
#[clap(short, long)]
transcode: Option<Resolution>,
/// Transcode starts at this resolution, or the source resolution, whichever is lower.
#[clap(short = 'T', long, default_value = "1440p")]
transcode_start: Resolution,
/// If not specified, the default value from the preset is used.
#[clap(short = 'T', long)]
transcode_start: Option<Resolution>,
/// Treat the audio as stereo. By default, only one channel from the input stereo will
/// be used, assuming either the other channel is backup or the same as the used.
@ -73,159 +60,6 @@ struct Args {
stereo: bool
}
macro_rules! resolutions {
($($res:ident: $width:literal x $height:literal at $bitrate:literal in $format:ident),+) => {
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
enum Resolution {
$(
#[doc = concat!(stringify!($width), "x", stringify!($height))]
$res
),+
}
const NUM_RESOLUTIONS: usize = {
let mut num = 0;
$(num += 1; stringify!($res);)+
num
};
impl Resolution {
fn values() -> [Self; NUM_RESOLUTIONS] {
[$(Self::$res),+]
}
fn width(self) -> usize {
match self {
$(Self::$res => $width),+
}
}
fn height(self) -> usize {
match self {
$(Self::$res => $height),+
}
}
fn bitrate(self) -> u64 {
match self {
$(Self::$res => $bitrate),+
}
}
fn format(self) -> FfmpegOutputFormat {
match self {
$(Self::$res => FfmpegOutputFormat::$format),+
}
}
}
impl FromStr for Resolution {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
Ok(match s {
$(concat!(stringify!($height), "p") => Self::$res,)+
_ => anyhow::bail!("Unknown Resolution: {s:?}")
})
}
}
}
}
resolutions! {
nHD: 640 x 360 at 500_000 in AvcAac,
HD: 1280 x 720 at 1_000_000 in AvcAac,
FullHD: 1920 x 1080 at 750_000 in Av1Opus,
WQHD: 2560 x 1440 at 1_000_000 in Av1Opus,
// TODO qsx muss mal sagen wieviel bitrate für 4k
UHD: 3840 x 2160 at 2_000_000 in Av1Opus
}
#[derive(Deserialize, Serialize)]
struct Project {
lecture: ProjectLecture,
source: ProjectSource,
progress: ProjectProgress
}
#[serde_as]
#[derive(Deserialize, Serialize)]
struct ProjectLecture {
course: String,
label: String,
docent: String,
#[serde_as(as = "DisplayFromStr")]
date: Date,
#[serde(default = "Default::default")]
#[serde_as(as = "DisplayFromStr")]
lang: Language<'static>
}
#[serde_as]
#[derive(Deserialize, Serialize)]
struct ProjectSource {
files: Vec<String>,
stereo: bool,
#[serde_as(as = "Option<DisplayFromStr>")]
start: Option<Time>,
#[serde_as(as = "Option<DisplayFromStr>")]
end: Option<Time>,
#[serde(default)]
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
fast: Vec<(Time, Time)>,
#[serde(default)]
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr, _)>")]
questions: Vec<(Time, Time, String)>,
metadata: Option<ProjectSourceMetadata>
}
#[serde_as]
#[derive(Deserialize, Serialize)]
struct ProjectSourceMetadata {
/// The duration of the source video.
#[serde_as(as = "DisplayFromStr")]
source_duration: Time,
/// The FPS of the source video.
#[serde_as(as = "DisplayFromStr")]
source_fps: Rational,
/// The time base of the source video.
#[serde_as(as = "DisplayFromStr")]
source_tbn: Rational,
/// The resolution of the source video.
source_res: Resolution,
/// The sample rate of the source audio.
source_sample_rate: u32
}
#[derive(Default, Deserialize, Serialize)]
struct ProjectProgress {
#[serde(default)]
preprocessed: bool,
#[serde(default)]
asked_start_end: bool,
#[serde(default)]
asked_fast: bool,
#[serde(default)]
asked_questions: bool,
#[serde(default)]
rendered_assets: bool,
#[serde(default)]
rendered: bool,
#[serde(default)]
transcoded: BTreeSet<Resolution>
}
fn ask(question: impl Display) -> String {
let mut stdout = io::stdout().lock();
let mut stdin = io::stdin().lock();
@ -282,7 +116,8 @@ fn main() {
// process arguments
let directory = args.directory.canonicalize_utf8().unwrap();
let course = args.course;
let preset = Preset::find(&args.preset).unwrap();
let course = preset.course;
// let's see if we need to initialise the project
let project_path = directory.join("project.toml");
@ -327,10 +162,10 @@ fn main() {
let project = Project {
lecture: ProjectLecture {
course,
label: args.label,
docent: args.docent,
label: preset.label,
docent: preset.docent,
date,
lang: args.lang
lang: preset.lang
},
source: ProjectSource {
files,
@ -439,10 +274,10 @@ fn main() {
});
// rescale the video
if let Some(lowest_res) = args.transcode {
if let Some(lowest_res) = args.transcode.or(preset.transcode) {
for res in Resolution::values().into_iter().rev() {
if res > project.source.metadata.as_ref().unwrap().source_res
|| res > args.transcode_start
|| res > args.transcode_start.unwrap_or(preset.transcode_start)
|| res < lowest_res
{
continue;

77
src/preset.rs Normal file
View file

@ -0,0 +1,77 @@
//! This struct defines presets.
use crate::{
iotro::{Language, BRITISH, GERMAN},
project::Resolution
};
use anyhow::bail;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use std::{fs, io};
#[serde_as]
#[derive(Deserialize, Serialize)]
pub(crate) struct Preset {
// options for the intro slide
pub(crate) course: String,
pub(crate) label: String,
pub(crate) docent: String,
/// Course language
#[serde(default = "Default::default")]
#[serde_as(as = "DisplayFromStr")]
pub(crate) lang: Language<'static>,
// coding options
pub(crate) transcode_start: Resolution,
pub(crate) transcode: Option<Resolution>
}
fn preset_23ws_malo2() -> Preset {
Preset {
course: "23ws-malo2".into(),
label: "Mathematische Logik II".into(),
docent: "Prof. E. Grädel".into(),
lang: GERMAN,
transcode_start: Resolution::WQHD,
transcode: Some(Resolution::nHD)
}
}
fn preset_24ss_algomod() -> Preset {
Preset {
course: "24ss-algomod".into(),
label: "Algorithmische Modelltheorie".into(),
docent: "Prof. E. Grädel".into(),
lang: GERMAN,
transcode_start: Resolution::WQHD,
transcode: Some(Resolution::HD)
}
}
fn preset_24ss_qc() -> Preset {
Preset {
course: "24ss-qc".into(),
label: "Introduction to Quantum Computing".into(),
docent: "Prof. D. Unruh".into(),
lang: BRITISH,
transcode_start: Resolution::WQHD,
transcode: Some(Resolution::HD)
}
}
impl Preset {
pub(crate) fn find(name: &str) -> anyhow::Result<Self> {
match fs::read(name) {
Ok(buf) => return Ok(toml::from_slice(&buf)?),
Err(err) if err.kind() == io::ErrorKind::NotFound => {},
Err(err) => return Err(err.into())
}
Ok(match name {
"23ws-malo2" => preset_23ws_malo2(),
"24ss-algomod" => preset_24ss_algomod(),
"24ss-qc" => preset_24ss_qc(),
_ => bail!("Unknown preset {name:?}")
})
}
}

164
src/project.rs Normal file
View file

@ -0,0 +1,164 @@
//! Defines the structure of the `project.toml` file.
use crate::{
iotro::Language,
render::ffmpeg::FfmpegOutputFormat,
time::{Date, Time}
};
use rational::Rational;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use std::{collections::BTreeSet, str::FromStr};
macro_rules! resolutions {
($($res:ident: $width:literal x $height:literal at $bitrate:literal in $format:ident),+) => {
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub(crate) enum Resolution {
$(
#[doc = concat!(stringify!($width), "x", stringify!($height))]
$res
),+
}
const NUM_RESOLUTIONS: usize = {
let mut num = 0;
$(num += 1; stringify!($res);)+
num
};
impl Resolution {
pub(crate) fn values() -> [Self; NUM_RESOLUTIONS] {
[$(Self::$res),+]
}
pub(crate) fn width(self) -> usize {
match self {
$(Self::$res => $width),+
}
}
pub(crate) fn height(self) -> usize {
match self {
$(Self::$res => $height),+
}
}
pub(crate) fn bitrate(self) -> u64 {
match self {
$(Self::$res => $bitrate),+
}
}
pub(crate) fn format(self) -> FfmpegOutputFormat {
match self {
$(Self::$res => FfmpegOutputFormat::$format),+
}
}
}
impl FromStr for Resolution {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
Ok(match s {
$(concat!(stringify!($height), "p") => Self::$res,)+
_ => anyhow::bail!("Unknown Resolution: {s:?}")
})
}
}
}
}
resolutions! {
nHD: 640 x 360 at 500_000 in AvcAac,
HD: 1280 x 720 at 1_000_000 in AvcAac,
FullHD: 1920 x 1080 at 750_000 in Av1Opus,
WQHD: 2560 x 1440 at 1_000_000 in Av1Opus,
// TODO qsx muss mal sagen wieviel bitrate für 4k
UHD: 3840 x 2160 at 2_000_000 in Av1Opus
}
#[derive(Deserialize, Serialize)]
pub(crate) struct Project {
pub(crate) lecture: ProjectLecture,
pub(crate) source: ProjectSource,
pub(crate) progress: ProjectProgress
}
#[serde_as]
#[derive(Deserialize, Serialize)]
pub(crate) struct ProjectLecture {
pub(crate) course: String,
pub(crate) label: String,
pub(crate) docent: String,
#[serde_as(as = "DisplayFromStr")]
pub(crate) date: Date,
#[serde(default = "Default::default")]
#[serde_as(as = "DisplayFromStr")]
pub(crate) lang: Language<'static>
}
#[serde_as]
#[derive(Deserialize, Serialize)]
pub(crate) struct ProjectSource {
pub(crate) files: Vec<String>,
pub(crate) stereo: bool,
#[serde_as(as = "Option<DisplayFromStr>")]
pub(crate) start: Option<Time>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub(crate) end: Option<Time>,
#[serde(default)]
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
pub(crate) fast: Vec<(Time, Time)>,
#[serde(default)]
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr, _)>")]
pub(crate) questions: Vec<(Time, Time, String)>,
pub(crate) metadata: Option<ProjectSourceMetadata>
}
#[serde_as]
#[derive(Deserialize, Serialize)]
pub(crate) struct ProjectSourceMetadata {
/// The duration of the source video.
#[serde_as(as = "DisplayFromStr")]
pub(crate) source_duration: Time,
/// The FPS of the source video.
#[serde_as(as = "DisplayFromStr")]
pub(crate) source_fps: Rational,
/// The time base of the source video.
#[serde_as(as = "DisplayFromStr")]
pub(crate) source_tbn: Rational,
/// The resolution of the source video.
pub(crate) source_res: Resolution,
/// The sample rate of the source audio.
pub(crate) source_sample_rate: u32
}
#[derive(Default, Deserialize, Serialize)]
pub(crate) struct ProjectProgress {
#[serde(default)]
pub(crate) preprocessed: bool,
#[serde(default)]
pub(crate) asked_start_end: bool,
#[serde(default)]
pub(crate) asked_fast: bool,
#[serde(default)]
pub(crate) asked_questions: bool,
#[serde(default)]
pub(crate) rendered_assets: bool,
#[serde(default)]
pub(crate) rendered: bool,
#[serde(default)]
pub(crate) transcoded: BTreeSet<Resolution>
}

View file

@ -3,7 +3,7 @@ use fontconfig::Fontconfig;
use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer};
use std::sync::OnceLock;
use svgwriter::{
tags::{Group, Path, Rect, TSpan, TagWithPresentationAttributes as _, Text},
tags::{Group, Path, TSpan, TagWithPresentationAttributes as _, Text},
Data, Graphic, Transform
};
@ -122,7 +122,7 @@ impl Question {
.with_dominant_baseline("middle")
.with_text_anchor("middle")
.with_font_weight(600)
.append("Question")
.append(lang.question.to_owned())
);
g.push(text);
@ -138,15 +138,3 @@ impl Question {
svg
}
}
#[cfg(test)]
#[test]
fn question() {
let svg = Question::new(
Resolution::FullHD,
&Language::default(),
"Hallo Welt! Dies ist eine sehr kluge Frage aus dem Publikum. Die Frage ist nämlich: Was ist eigentlich die Frage?".into()
)
.finish();
std::fs::write("question.svg", svg.to_string_pretty()).unwrap();
}

View file

@ -7,10 +7,10 @@ use self::{
};
use crate::{
iotro::{intro, outro},
project::{Project, ProjectLecture, ProjectSourceMetadata, Resolution},
question::Question,
render::ffmpeg::{Ffmpeg, FfmpegInput},
time::{format_date, format_time, Time},
Project, ProjectLecture, ProjectSourceMetadata, Resolution
time::{format_date, format_time, Time}
};
use anyhow::{bail, Context};
use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};