main
John Lauer Publish 0.2.3 24178c3 15h ago
//! Adom's own logo sting, for official Adom videos only.
//! The brand kit lives on a wiki page only members of the Adom org can read, so being able to
//! read it is the test: an Adom employee gets the sting offered (and applied on request), and
//! everyone else's film stays free of Adom branding, because a logo on it would make it look
//! like an official Adom video.

use std::path::{Path, PathBuf};
use std::process::Command;

const KIT: &str = "adom/adom-video-brand";
const OVERLAY: &str = "media/alpha/logo-sting-teal-1080p.mov";
const CLOSER: &str = "media/dark-intro-16x9.mp4";

fn cache() -> PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    PathBuf::from(home).join(".adom/adom-aiflow-video/brand")
}

/// True when this account can read the Adom brand kit (an Adom org member).
pub fn is_adom_member() -> bool {
    Command::new("adom-wiki").args(["repo", "show", KIT, "page.json"]).output().map(|o| o.status.success()).unwrap_or(false)
}

fn kit() -> Result<PathBuf, String> {
    let dir = cache();
    let page = dir.join("adom-video-brand");
    if page.join(OVERLAY).exists() && page.join(CLOSER).exists() { return Ok(page); }
    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
    let _ = std::fs::remove_dir_all(&page);
    let o = Command::new("adom-wiki").args(["repo", "clone", KIT]).current_dir(&dir).output().map_err(|e| e.to_string())?;
    if !o.status.success() || !page.join(OVERLAY).exists() { return Err("could not fetch the Adom brand kit".into()); }
    Ok(page)
}

fn has_audio(f: &str) -> bool {
    Command::new("ffprobe").args(["-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0", f]).output()
        .map(|o| !String::from_utf8_lossy(&o.stdout).trim().is_empty()).unwrap_or(false)
}

pub fn status() -> (String, Vec<String>) {
    if is_adom_member() {
        ("this account is in the Adom org: an official Adom film can carry the Adom logo sting".into(), vec![
            "Ask the human once whether this is an official Adom video. If it is, plan the script so nobody speaks during the first 5 seconds, and finish with `brand --video <final.mp4> --out <branded.mp4>` after the voice is on.".into(),
            "That puts the transparent logo sting over the film's own first 5 seconds (with its sound) and the black-background sting after the last line. Captions and chapters keep their times.".into(),
            "A personal film by the same person gets no Adom branding: leave it out when they say it is not an Adom video.".into(),
        ])
    } else {
        ("no Adom branding for this film".into(), vec![
            "Adom's logo, sting, sound and end card are for official Adom videos made by Adom employees. Leave them out of this film; saying it was made in Adom Hydrogen is fine.".into(),
        ])
    }
}

pub fn apply(video: &str, out: &str) -> Result<(String, Vec<String>), (String, Vec<String>)> {
    if !is_adom_member() {
        return Ok(("left the film as it is: no Adom branding".into(), vec!["Adom's logo sting is for official Adom videos made by Adom employees. Use the film without it.".into()]));
    }
    if !Path::new(video).exists() { return Err((format!("no film at {video}"), vec![])); }
    let page = kit().map_err(|e| (e, vec!["Check `adom-wiki repo show adom/adom-video-brand page.json`, then try again.".to_string()]))?;
    let (ov, cl) = (page.join(OVERLAY), page.join(CLOSER));
    let a0 = if has_audio(video) { "[0:a]aresample=48000,aformat=channel_layouts=stereo[a0];".to_string() }
             else { "anullsrc=r=48000:cl=stereo[an];[an]atrim=0:5[a0];".to_string() };
    let amix = if has_audio(video) { "[a0][a1]amix=inputs=2:duration=first:normalize=0[ma];" } else { "[a0][a1]amix=inputs=2:duration=longest:normalize=0[ma];" };
    let fc = format!(
        "[0:v]fps=30,scale=1920:1080:flags=lanczos,setsar=1[m];[1:v]format=yuva444p10le,scale=1920:1080[s];\
         [m][s]overlay=0:0:eof_action=pass:format=auto,format=yuv420p[mv];[2:v]scale=1920:1080,fps=30,setsar=1,format=yuv420p[e];\
         [mv][e]concat=n=2:v=1:a=0[v];{a0}[1:a]aresample=48000,aformat=channel_layouts=stereo[a1];{amix}\
         [2:a]aresample=48000,aformat=channel_layouts=stereo[ea];[ma][ea]concat=n=2:v=0:a=1[a]");
    let o = Command::new("ffmpeg").args(["-nostdin", "-v", "error", "-y", "-i", video, "-i"]).arg(&ov).arg("-i").arg(&cl)
        .args(["-filter_complex", &fc, "-map", "[v]", "-map", "[a]", "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-g", "30",
               "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", out]).output().map_err(|e| (format!("could not run ffmpeg: {e}"), vec![]))?;
    if !o.status.success() {
        return Err(("ffmpeg could not add the sting".into(), vec![String::from_utf8_lossy(&o.stderr).lines().last().unwrap_or("").to_string()]));
    }
    Ok((format!("Adom logo sting added: {out}"), vec![
        "Overlay sting on the first 5 seconds, black-background sting after the last frame (5 seconds longer). Captions and chapters keep their times.".into(),
        "Watch the first and last 10 seconds before showing the human: the logo should not sit on top of speech.".into(),
    ]))
}