Compare commits

...

16 Commits
0.1.2 ... main

Author SHA1 Message Date
Bel LaPointe 6095fd69fc 0.1.6 release 2023-12-28 22:20:22 -05:00
Bel LaPointe 8f1c0876c5 for highlighted spans, add a fat green border 2023-12-28 22:19:59 -05:00
Bel LaPointe 6e2cf357f4 0.1.5 release 2023-12-28 22:17:36 -05:00
Bel LaPointe 4aede62dbb lint 2023-12-28 22:17:21 -05:00
Bel LaPointe c3d6cd1545 menu causes crashes so skip it 2023-12-28 22:16:16 -05:00
Bel LaPointe bf7d09dde0 0.1.4 release 2023-12-28 22:08:55 -05:00
Bel LaPointe d13266dd73 while processing, make the whole app look non-interactive 2023-12-28 22:08:29 -05:00
Bel LaPointe ccc4d7fbd2 dont make trivial splits 2023-12-28 21:58:49 -05:00
Bel LaPointe 48961fee2a pretty print timestamps 2023-12-28 21:57:05 -05:00
Bel LaPointe dfbdba81a6 reset statefuls on file change 2023-12-28 21:56:57 -05:00
Bel LaPointe a38e39b808 add cmd-q 2023-12-28 21:38:36 -05:00
Bel LaPointe 9e053c96d6 release 0.1.3 2023-12-28 21:33:42 -05:00
Bel LaPointe 10ef89f0e9 dx bundle --release --platform x86_64-apple-darwin 2023-12-28 21:33:03 -05:00
Bel LaPointe 32a652519a statuses per-event because scrolling stinks 2023-12-28 21:20:27 -05:00
Bel LaPointe a8ba8fc97d drop todos since web so far gone 2023-12-28 21:12:47 -05:00
Bel LaPointe 5bd118704d style.css and numbered instructions 2023-12-28 21:12:26 -05:00
6 changed files with 140 additions and 69 deletions

View File

@ -207,12 +207,14 @@ impl Inspection {
for i in 0..unstuck_spans.len() { for i in 0..unstuck_spans.len() {
let mut span = unstuck_spans[i]; let mut span = unstuck_spans[i];
for split in scene_splits.iter() { for split in scene_splits.iter() {
if &span.start < split && split < &span.stop { // TODO buffer if &(span.start + 5.0) < split && split < &(span.stop - 5.0) { // TODO const
result.push(ContentSpan{start: span.start, stop: *split}); result.push(ContentSpan{start: span.start, stop: *split});
span.start = *split; span.start = *split;
} }
} }
result.push(span); // TODO assert nontrivial if span.stop - span.start > 2.0 {
result.push(span);
}
} }
result result
} }

22
src/Cargo.lock generated
View File

@ -1349,6 +1349,17 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "home-video-blue-extractinator"
version = "0.1.6"
dependencies = [
"base64 0.21.5",
"dioxus",
"dioxus-desktop",
"lib",
"opener",
]
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.25.2" version = "0.25.2"
@ -2570,17 +2581,6 @@ dependencies = [
"lock_api", "lock_api",
] ]
[[package]]
name = "src"
version = "0.1.0"
dependencies = [
"base64 0.21.5",
"dioxus",
"dioxus-desktop",
"lib",
"opener",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "src" name = "home-video-blue-extractinator"
version = "0.1.0" version = "0.1.6"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -10,5 +10,13 @@ dioxus = "0.4.3"
lib = { path = "../src-lib" } lib = { path = "../src-lib" }
base64 = "0.21.5" base64 = "0.21.5"
opener = "0.6.1" opener = "0.6.1"
#dioxus-web = "0.4.3"
dioxus-desktop = "0.4.3" dioxus-desktop = "0.4.3"
[package.metadata.bundle]
name = "home-video-blue-extractinator"
identifier = "com.breel.home-video-blue-extractinator"
version = "0.1.6"
copyright = "Copyright (c) breel.dev 2023. All rights reserved."
long_description = """
Tool to turn long home movies into clips with less noise.
"""

12
src/Dioxus.toml Normal file
View File

@ -0,0 +1,12 @@
[application]
name = "home-video-blue-extractinator"
default_platform = "desktop"
[web.app]
title = "home-video-blue-extractinator"
[web.watcher]
[web.resource]
[web.resource.dev]

View File

@ -1,88 +1,92 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types // import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::hooks::use_future;
use lib; use lib;
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use core::cmp::Ordering;
use std::str::FromStr; use std::str::FromStr;
fn main() { fn main() {
// launch the dioxus app in a webview dioxus_desktop::launch(App);
dioxus_desktop::launch(App); // TODO desktop dioxus_desktop::launch_cfg(App, dioxus_desktop::Config::new()
//dioxus_web::launch(App); .with_window(dioxus_desktop::WindowBuilder::new()
.with_title("home-video-blue-extractinator")
));
} }
// define a component that renders a div with the text "Hello, world!" // define a component that renders a div with the text "Hello, world!"
fn App(cx: Scope) -> Element { fn App(cx: Scope) -> Element {
let file = use_state(cx, || String::new()); let file = use_state(cx, || String::new());
let status = use_state(cx, || String::new()); let analyze_status = use_state(cx, || String::new());
let clipify_status = use_state(cx, || String::new());
let processing = use_state(cx, || false);
let analysis = use_state(cx, || Analysis::new()); let analysis = use_state(cx, || Analysis::new());
let a_css = String::from_utf8_lossy(include_bytes!("./style.css"));
let processing_css = || {
match *processing.get() {
true => "body { background-color: lightgray !important; opacity: 0.75 !important; }".to_string(),
false => "".to_string(),
}
};
cx.render(rsx! { cx.render(rsx! {
header { header {
style { " title { "home-video-blue-extractinator" }
body {{ style { "{a_css} {processing_css()}" }
max-width: 600pt;
margin: auto;
zoom: 150%;
}}
label {{
display: block;
}}
label:has(input[type=checkbox]:checked) {{
background-color: lightgreen;
}}
" }
} }
main { main {
rsx! { rsx! {
h1 { "home-video-blue-extractinator" } h1 { "home-video-blue-extractinator" }
h3 { "1. Choose your movie" }
div { div {
input { input {
r#type: "file", r#type: "file",
disabled: status.get().starts_with("[WORKING]"), disabled: *processing.get(),
onchange: |evt| { onchange: |evt| {
to_owned![file];
if let Some(file_engine) = &evt.files { if let Some(file_engine) = &evt.files {
for f in &file_engine.files() { for f in &file_engine.files() {
file.set(f.clone()); file.set(f.clone());
analyze_status.set(String::new());
clipify_status.set(String::new());
processing.set(false);
analysis.set(Analysis::new());
} }
} }
}, },
}, },
p { file.get().clone() } p { file.get().clone() }
} }
h3 { "2. Analyze your movie" }
div { div {
input { r#type: "button", value: "analyze", disabled: file.get().len() == 0 || status.get().starts_with("[WORKING]"), onclick: move |_| { input { r#type: "button", value: "analyze", disabled: file.get().len() == 0 || *processing.get(), onclick: move |_| {
cx.spawn({ cx.spawn({
let file = file.to_owned(); let file = file.to_owned();
let status = status.to_owned(); let analyze_status = analyze_status.to_owned();
let processing = processing.to_owned();
let analysis = analysis.to_owned(); let analysis = analysis.to_owned();
async move { async move {
status.set(format!("[WORKING] analyzing {file}... (this may take a while, like 5 minutes, but I promise I'm working on it)")); processing.set(true);
analyze_status.set(format!("analyzing {file}... (this may take a while, like 5 minutes, but I promise I'm working on it)"));
let analyzed = analyze(file.get().clone()).await; let analyzed = analyze(file.get().clone()).await;
if analyzed.err.len() > 0 { if analyzed.err.len() > 0 {
status.set(analyzed.err.clone()); analyze_status.set(analyzed.err.clone());
} else { } else {
status.set(format!( analyze_status.set(format!(
"found {} clips to keep and {} clips to drop", "found {} clips to keep and {} clips to drop",
analyzed.result.iter().filter(|x| x.has_content).count(), analyzed.result.iter().filter(|x| x.has_content).count(),
analyzed.result.iter().filter(|x| !x.has_content).count(), analyzed.result.iter().filter(|x| !x.has_content).count(),
)); ));
} }
analysis.set(analyzed); analysis.set(analyzed);
processing.set(false);
} }
}); });
}} }}
br {}
div { analyze_status.get().clone() }
} }
div { div {
br {}
status.get().clone()
br {}
}
div {
h3 { "Content Spans" }
form { form {
onsubmit: |evt| { onsubmit: |evt| {
let content_spans: Vec<_> = evt.values.iter() let content_spans: Vec<_> = evt.values.iter()
@ -96,16 +100,19 @@ fn App(cx: Scope) -> Element {
} }
}) })
.collect(); .collect();
status.set(format!("[WORKING] clipifying {:?}...", content_spans)); clipify_status.set(format!("clipifying {:?}...", content_spans));
let file = file.get().clone(); let file = file.get().clone();
to_owned![status]; to_owned![clipify_status];
to_owned![processing];
async move { async move {
processing.set(true);
let f = clipify(file, content_spans).await; let f = clipify(file, content_spans).await;
status.set(f); clipify_status.set(f);
processing.set(false);
} }
}, },
input { r#type: "submit", value: "clipify selected spans", disabled: file.get().len() == 0 || status.get().starts_with("[WORKING]") || analysis.get().result.len() == 0 }
hr {} hr {}
h3 { "3. Double check the scenes to keep (green) are okay" }
analysis.get().result.iter().map(|a| { analysis.get().result.iter().map(|a| {
rsx! { rsx! {
div { div {
@ -115,13 +122,18 @@ fn App(cx: Scope) -> Element {
checked: a.has_content, checked: a.has_content,
name: "{a.start}..{a.stop}", name: "{a.start}..{a.stop}",
} }
"{a.start}..{a.stop}: " "{a.pretty_range()}"
br {} br {}
img { src: "data:image/png;base64, {a.screenshot}" } img { src: "data:image/png;base64, {a.screenshot}" }
} }
} }
} }
}) })
hr {}
h3 { "4. Submit your re-clip" }
div { clipify_status.get().clone() }
br {}
input { r#type: "submit", value: "clipify selected spans", disabled: file.get().len() == 0 || *processing.get() || analysis.get().result.len() == 0 }
} }
} }
} }
@ -145,15 +157,35 @@ impl Analysis {
#[derive(Clone)] #[derive(Clone)]
struct Analyzed { struct Analyzed {
_start: f32,
_stop: f32,
start: String, start: String,
stop: String, stop: String,
screenshot: String, screenshot: String,
has_content: bool, has_content: bool,
} }
impl Analyzed {
fn pretty_range(&self) -> String {
format!("{} - {}", self.pretty_t(self._start), self.pretty_t(self._stop))
}
fn pretty_t(&self, t: f32) -> String {
if t < 60.0 {
return format!(":{:02}", t as i32);
} else if t < 60.0 * 60.0 {
return format!("{:02}:{:02}", (t / 60.0) as i32, (t % 60.0) as i32);
} else {
return format!("{:02}:{:02}:{:02}",
(t / 60.0 / 60.0) as i32,
(t / 60.0) as i32,
(t % 60.0) as i32);
}
}
}
async fn analyze(file: String) -> Analysis { async fn analyze(file: String) -> Analysis {
let content_spans = lib::video::inspect_async(&file).await; // TODO desktop let content_spans = lib::video::inspect_async(&file).await;
//let content_spans: Result<Vec<lib::video::ContentSpan>, String> = Ok(vec![lib::video::ContentSpan{start: 0.5, stop: 20.2}]);
if content_spans.is_err() { if content_spans.is_err() {
return Analysis{ return Analysis{
result: vec![], result: vec![],
@ -176,11 +208,13 @@ async fn analyze(file: String) -> Analysis {
let screenshot = |content_span: &lib::video::ContentSpan| -> Analyzed { let screenshot = |content_span: &lib::video::ContentSpan| -> Analyzed {
let ts = (content_span.start + content_span.stop) / 2.0; let ts = (content_span.start + content_span.stop) / 2.0;
Analyzed { Analyzed {
_start: content_span.start,
_stop: content_span.stop,
start: content_span.start.to_string(), start: content_span.start.to_string(),
stop: content_span.stop.to_string(), stop: content_span.stop.to_string(),
screenshot: match lib::video::screenshot_png(&file, ts) { screenshot: match lib::video::screenshot_png(&file, ts) {
Ok(png) => general_purpose::STANDARD.encode(&png), Ok(png) => general_purpose::STANDARD.encode(&png),
Err(_) => a_png.to_string(), Err(_) => A_PNG.to_string(),
}, },
has_content: false, has_content: false,
} }
@ -208,23 +242,24 @@ async fn analyze(file: String) -> Analysis {
async fn clipify(file: String, content_spans: Vec<lib::video::ContentSpan>) -> String { async fn clipify(file: String, content_spans: Vec<lib::video::ContentSpan>) -> String {
let d = format!("{}.d", &file); let d = format!("{}.d", &file);
let mut last_err = None; let mut statuses = vec![];
for content_span in content_spans.iter() { for content_span in content_spans.iter() {
let output = format!("{}/{}.mp4", &d, content_span.start); let output = format!("{}/{}.mp4", &d, content_span.start);
match std::path::Path::new(&output).exists() {
true => { statuses.push(format!("skipping {} as it already exists", &output)); },
false => {
let cur = lib::video::clip_async(&output, &file, *content_span).await; let cur = lib::video::clip_async(&output, &file, *content_span).await;
if cur.is_err() { if cur.is_err() {
last_err = Some(format!("failed to clipify {} at {}..{}: {}", &file, &content_span.start, &content_span.stop, cur.err().unwrap())); statuses.push(format!("failed to clipify {} at {}..{}: {}", &file, &content_span.start, &content_span.stop, cur.err().unwrap()));
}
}
match last_err {
Some(msg) => msg,
None => {
match opener::open(d.clone()) {
Ok(_) => d,
Err(msg) => format!("failed to open clipify result: {}", msg),
} }
}, },
} }
}
let _ = opener::open(d.clone());
match statuses.len() {
0 => d,
_ => statuses.join(", "),
}
} }
const a_png: &str = "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; const A_PNG: &str = r"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";

14
src/src/style.css Normal file

File diff suppressed because one or more lines are too long