Compare commits
16 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
6095fd69fc | |
|
|
8f1c0876c5 | |
|
|
6e2cf357f4 | |
|
|
4aede62dbb | |
|
|
c3d6cd1545 | |
|
|
bf7d09dde0 | |
|
|
d13266dd73 | |
|
|
ccc4d7fbd2 | |
|
|
48961fee2a | |
|
|
dfbdba81a6 | |
|
|
a38e39b808 | |
|
|
9e053c96d6 | |
|
|
10ef89f0e9 | |
|
|
32a652519a | |
|
|
a8ba8fc97d | |
|
|
5bd118704d |
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
137
src/src/main.rs
137
src/src/main.rs
|
|
@ -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==";
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue