1mod compile;
15mod de;
16mod facts;
17mod lower;
18#[cfg(feature = "schema")]
19mod schema;
20pub mod spec;
21#[cfg(test)]
22mod tests;
23mod theme;
24
25use std::collections::BTreeMap;
26use std::fmt;
27use std::sync::Arc;
28
29use alpha2_model::Project;
30use serde::{Deserialize, Serialize};
31
32pub use facts::{Facts, MediaFacts, RequiredMedia, Requirements, kind_of_path, requirements};
33#[cfg(feature = "schema")]
34pub use schema::compose_json_schema;
35pub use spec::{COMPOSE_VERSION, VideoSpec};
36pub use theme::THEMES;
37
38#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct ComposeError {
41 pub path: String,
43 pub message: String,
45}
46
47impl ComposeError {
48 pub(crate) fn new(path: &str, message: impl Into<String>) -> Self {
49 Self {
50 path: path.to_owned(),
51 message: message.into(),
52 }
53 }
54}
55
56impl fmt::Display for ComposeError {
57 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(formatter, "{}: {}", self.path, self.message)
59 }
60}
61
62impl std::error::Error for ComposeError {}
63
64#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67#[serde(deny_unknown_fields)]
68pub struct LoudnessTarget {
69 pub target_lufs: f64,
71 #[serde(default = "default_ceiling")]
73 pub true_peak_ceiling_dbtp: f64,
74}
75
76const fn default_ceiling() -> f64 {
77 -1.0
78}
79
80#[derive(Clone, Debug, PartialEq, Serialize)]
82pub struct Compiled {
83 pub project: Project,
85 pub assets: BTreeMap<String, String>,
87 pub loudness: Option<LoudnessTarget>,
89 pub warnings: Vec<String>,
91}
92
93pub fn compile(
101 spec: &VideoSpec,
102 facts: &Facts,
103 fonts: &[Arc<[u8]>],
104) -> Result<Compiled, ComposeError> {
105 if let Some(loudness) = spec.loudness {
106 let (target, ceiling) = match loudness {
107 spec::Loudness::Lufs(target) => (target, default_ceiling()),
108 spec::Loudness::Target(target) => (target.target_lufs, target.true_peak_ceiling_dbtp),
109 };
110 if !(-60.0..=0.0).contains(&target) || !(-20.0..=0.0).contains(&ceiling) {
111 return Err(ComposeError::new(
112 "spec.loudness",
113 format!(
114 "{target} LUFS under {ceiling} dBTP: the target is -60..=0 LUFS and the ceiling -20..=0 dBTP"
115 ),
116 ));
117 }
118 }
119 compile::compile(spec, facts, fonts)
120}
121
122pub fn parse_spec(json: &str) -> Result<VideoSpec, ComposeError> {
127 parse(json, "spec")
128}
129
130pub fn parse_facts(json: &str) -> Result<Facts, ComposeError> {
135 parse(json, "facts")
136}
137
138fn parse<T: serde::de::DeserializeOwned>(json: &str, root: &str) -> Result<T, ComposeError> {
139 let value =
140 serde_json::from_str(json).map_err(|error| ComposeError::new(root, error.to_string()))?;
141 de::from_value(value)
142 .map_err(|located| ComposeError::new(&de::join(root, &located.path), located.message))
143}
144
145pub fn compile_json(spec: &str, facts: &str, fonts: &[Arc<[u8]>]) -> Result<String, ComposeError> {
153 let spec = parse_spec(spec)?;
154 let facts = parse_facts(facts)?;
155 let compiled = compile(&spec, &facts, fonts)?;
156 Ok(serde_json::to_string(&compiled).expect("a compiled project serializes"))
157}
158
159pub fn requirements_json(spec: &str) -> Result<String, ComposeError> {
167 let spec = parse_spec(spec)?;
168 Ok(serde_json::to_string(&requirements(&spec)).expect("requirements serialize"))
169}