LR-EDIT-DETAIL-DENOISE: address installation and model-policy review
Generated with Codex.
This commit is contained in:
parent
7935999d94
commit
fd4019300a
|
|
@ -3052,6 +3052,22 @@ name = "lightcraft-denoise"
|
|||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"faer-core",
|
||||
"lightcraft-denoise-core",
|
||||
"lightcraft-raster",
|
||||
"miniz_oxide 0.8.9",
|
||||
"proptest",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 2.0.21",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-denoise-core"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"lightcraft-raster",
|
||||
"miniz_oxide 0.8.9",
|
||||
"proptest",
|
||||
|
|
@ -3086,6 +3102,7 @@ dependencies = [
|
|||
"lightcraft-codecs",
|
||||
"lightcraft-color",
|
||||
"lightcraft-denoise",
|
||||
"lightcraft-denoise-core",
|
||||
"lightcraft-develop",
|
||||
"lightcraft-fetch",
|
||||
"lightcraft-geom",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ repository = "https://github.com/storytold/lightcraft"
|
|||
|
||||
[workspace.dependencies]
|
||||
# internal
|
||||
lightcraft-denoise = { path = "crates/denoise" }
|
||||
lightcraft-denoise-core = { path = "crates/denoise-core" }
|
||||
lightcraft-denoise = { path = "crates/denoise", default-features = false }
|
||||
lightcraft-geom = { path = "crates/geom" }
|
||||
lightcraft-sysmem = { path = "crates/sysmem" }
|
||||
lightcraft-color = { path = "crates/color" }
|
||||
|
|
|
|||
9
NOTICE
9
NOTICE
|
|
@ -27,6 +27,15 @@ the facebook/sam3 checkpoint only when they choose to (in the app, or by hand),
|
|||
License (https://github.com/facebookresearch/sam3/blob/main/LICENSE), a separate, non-OSI
|
||||
licence. See docs/ai-masks.md.
|
||||
|
||||
AI denoise weights are not part of LightCraft and are never distributed with it.
|
||||
The default build offers user-supplied models only. The optional rawnind-model
|
||||
feature offers a separately downloaded RawNIND UtNet2 Bayer checkpoint under
|
||||
GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html), after the user accepts its terms.
|
||||
Commercial use is subject to those terms, including applicable source, licence and
|
||||
notice obligations on redistribution. Enabling that offer in releases remains a
|
||||
maintainer decision. The ONNX reader and CPU/GPU executor are our Rust implementation;
|
||||
no darktable, RawSpeed, LibRaw or other GPL decoder/runtime code is included.
|
||||
|
||||
Builds made with craft-fonts (the optional CRAFT_FONTS_DIR build input; all official releases)
|
||||
also embed the fonts listed in https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md
|
||||
(the Japanese fonts BIZ UDPGothic, BIZ UDMincho and Shippori Mincho; the web build embeds only BIZ
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ dhat = { workspace = true, optional = true }
|
|||
[features]
|
||||
default = ["denoise"]
|
||||
denoise = ["lightcraft-engine/denoise"]
|
||||
rawnind-model = ["denoise", "lightcraft-engine/rawnind-model"]
|
||||
# Heap profiling: `cargo run --release -p lightcraft-cli --features dhat-heap -- snapshot …` counts
|
||||
# every allocation (live/peak bytes in `library.memory` / `ui.inspect`) and writes dhat-heap.json
|
||||
# (open it in DHAT's viewer: allocation sites by bytes at the peak). Slows allocation-heavy work.
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ fn services(originals: Originals, backend: Option<Backend>, files: Files, frozen
|
|||
open_picker(originals.clone(), backend.clone(), ctx.clone());
|
||||
Vec::new() // files arrive asynchronously and are imported on a later frame
|
||||
})),
|
||||
pick_denoise_model: None,
|
||||
// Preset files: browser pickers are asynchronous; not wired on the web yet.
|
||||
pick_preset_files: None,
|
||||
pick_tracklog: None,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ workspace = true
|
|||
[features]
|
||||
default = ["denoise"]
|
||||
denoise = ["lightcraft-engine/denoise"]
|
||||
rawnind-model = ["denoise", "lightcraft-engine/rawnind-model"]
|
||||
|
||||
[dependencies]
|
||||
lightcraft-engine = { workspace = true, features = ["sam"] }
|
||||
|
|
|
|||
|
|
@ -387,6 +387,13 @@ fn services(ctx: egui::Context, log_file: Option<&std::path::Path>) -> Services
|
|||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect()
|
||||
})),
|
||||
pick_denoise_model: Some(Box::new(|| {
|
||||
rfd::FileDialog::new()
|
||||
.add_filter_nocase(lightcraft_ui_egui::i18n::tr("ONNX model"), &["onnx"])
|
||||
.pick_file()
|
||||
.map(|p| vec![p.to_string_lossy().into_owned()])
|
||||
.unwrap_or_default()
|
||||
})),
|
||||
pick_preset_files: Some(Box::new(|| {
|
||||
rfd::FileDialog::new()
|
||||
.set_title(lightcraft_ui_egui::i18n::tr("Import Presets"))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "lightcraft-denoise-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "LightCraft denoise model metadata, Bayer tiling and cache formats (L1; no inference backend)"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
rawnind-model = []
|
||||
|
||||
[dependencies]
|
||||
lightcraft-raster = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
miniz_oxide = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
web-time = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# lightcraft-denoise-core
|
||||
|
||||
L1 model metadata, bounded archive/cache formats and Bayer packing/tiling helpers.
|
||||
No weights or inference backend. See [the user workflow](../../docs/denoise.md).
|
||||
`lightcraft-denoise` re-exports these types for CPU/GPU users; the engine uses this
|
||||
crate directly when inference is disabled. The default download catalog is empty.
|
||||
Run `cargo test -p lightcraft-denoise-core`; optional catalog checks use `--features rawnind-model`.
|
||||
|
|
@ -127,13 +127,21 @@ pub fn extract(archive: &Path, name: &str, dest: &Path, max_bytes: u64) -> Resul
|
|||
}
|
||||
let skip = u64::from(u16_at(&head, 26).unwrap_or(0)) + u64::from(u16_at(&head, 28).unwrap_or(0));
|
||||
f.seek_relative(i64::try_from(skip).map_err(|_| ArchiveError::Format("a header is too long".into()))?)?;
|
||||
let mut packed = vec![0u8; e.compressed as usize];
|
||||
let len = usize::try_from(e.compressed)
|
||||
.ok()
|
||||
.filter(|&n| n <= 256 * 1024 * 1024)
|
||||
.ok_or_else(|| ArchiveError::Format("the compressed model is too large".into()))?;
|
||||
if e.size > 256 * 1024 * 1024 {
|
||||
return format("the extracted model is too large");
|
||||
}
|
||||
let mut packed = Vec::new();
|
||||
packed.try_reserve_exact(len).map_err(|_| ArchiveError::Format("not enough memory for the model archive".into()))?;
|
||||
packed.resize(len, 0u8);
|
||||
f.read_exact(&mut packed)?;
|
||||
let data = match e.method {
|
||||
0 => packed,
|
||||
8 => {
|
||||
miniz_oxide::inflate::decompress_to_vec_with_limit(&packed, max_bytes as usize).map_err(|err| ArchiveError::Format(format!("{err:?}")))?
|
||||
}
|
||||
8 => miniz_oxide::inflate::decompress_to_vec_with_limit(&packed, max_bytes.min(256 * 1024 * 1024) as usize)
|
||||
.map_err(|err| ArchiveError::Format(format!("{err:?}")))?,
|
||||
m => return format(&format!("compression method {m} is not supported")),
|
||||
};
|
||||
if data.len() as u64 != e.size || crc32(&data) != e.crc {
|
||||
|
|
@ -3,9 +3,12 @@
|
|||
//! None is part of LightCraft. A model with an address here can be downloaded when the user asks and has accepted
|
||||
//! its terms; anything else is brought by the user as an `.onnx` file with a `denoise-model.json` beside it.
|
||||
|
||||
#[cfg(any(feature = "rawnind-model", test))]
|
||||
use crate::licence::{Commercial, Licence};
|
||||
|
||||
use crate::manifest::{DenoiserManifest, Domain, Gain};
|
||||
use crate::manifest::DenoiserManifest;
|
||||
#[cfg(any(feature = "rawnind-model", test))]
|
||||
use crate::manifest::{Domain, Gain};
|
||||
|
||||
/// A known model, and how to get it.
|
||||
pub struct Known {
|
||||
|
|
@ -18,6 +21,7 @@ pub struct Known {
|
|||
|
||||
/// RawNIND's UtNet2 for Bayer sensors, as published for darktable (`rawdenoise-nind.dtmodel`, a zip with a Bayer and a
|
||||
/// linear model). Its weights are GPL-3.0; the training pictures are CC BY 4.0 and CC0.
|
||||
#[cfg(any(feature = "rawnind-model", test))]
|
||||
fn rawnind_bayer() -> Known {
|
||||
let manifest = DenoiserManifest {
|
||||
id: "rawnind-bayer".into(),
|
||||
|
|
@ -29,7 +33,7 @@ fn rawnind_bayer() -> Known {
|
|||
url: Some("https://www.gnu.org/licenses/gpl-3.0.html".into()),
|
||||
notice: "The weights are published under the GNU General Public License v3. LightCraft does not include them: they \
|
||||
are downloaded to your computer at your request and used by it only. If you pass the model file on, the \
|
||||
GPL applies to that copy."
|
||||
GPL applies to that copy. Commercial use is subject to the GPL terms, including applicable source, licence and notice obligations on redistribution."
|
||||
.into(),
|
||||
},
|
||||
source: Some("https://github.com/darktable-org/darktable-ai/tree/master/models/rawdenoise-nind".into()),
|
||||
|
|
@ -55,7 +59,14 @@ fn rawnind_bayer() -> Known {
|
|||
|
||||
/// Every model LightCraft knows.
|
||||
pub fn all() -> Vec<Known> {
|
||||
vec![rawnind_bayer()]
|
||||
#[cfg(feature = "rawnind-model")]
|
||||
{
|
||||
vec![rawnind_bayer()]
|
||||
}
|
||||
#[cfg(not(feature = "rawnind-model"))]
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// The known model with this id.
|
||||
|
|
@ -76,7 +87,8 @@ mod tests {
|
|||
#[test]
|
||||
fn known_models_are_valid_unique_and_downloaded_over_https() {
|
||||
let all = all();
|
||||
assert!(!all.is_empty());
|
||||
assert_eq!(all.is_empty(), !cfg!(feature = "rawnind-model"));
|
||||
validate(&rawnind_bayer().manifest).unwrap();
|
||||
for (i, k) in all.iter().enumerate() {
|
||||
validate(&k.manifest).unwrap();
|
||||
assert!(all.iter().skip(i + 1).all(|o| o.manifest.id != k.manifest.id), "unique ids");
|
||||
|
|
@ -92,8 +104,12 @@ mod tests {
|
|||
assert!(k.download.is_none(), "{}", k.manifest.id);
|
||||
}
|
||||
}
|
||||
assert!(find("rawnind-bayer").is_some() && find("nothing").is_none());
|
||||
assert_eq!(by_sha256("da27509dab6a2915da67e988acd86cf71f9d5bbc8d1aa0ed32933578a887b901").map(|m| m.id), Some("rawnind-bayer".into()));
|
||||
assert_eq!(find("rawnind-bayer").is_some(), cfg!(feature = "rawnind-model"));
|
||||
assert!(find("nothing").is_none());
|
||||
assert_eq!(
|
||||
by_sha256("da27509dab6a2915da67e988acd86cf71f9d5bbc8d1aa0ed32933578a887b901").map(|m| m.id),
|
||||
if cfg!(feature = "rawnind-model") { Some("rawnind-bayer".into()) } else { None }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
//! Model metadata, bounded archive/cache formats and Bayer tiling, independent of inference.
|
||||
//! Used by featureless engine and web builds; no neural-network or matrix backend is linked.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
pub mod archive;
|
||||
pub mod bayer;
|
||||
pub mod hash;
|
||||
pub mod known;
|
||||
pub mod licence;
|
||||
pub mod manifest;
|
||||
pub mod product;
|
||||
pub mod run;
|
||||
pub mod tiles;
|
||||
|
||||
pub use manifest::{DenoiserManifest, Domain, Gain, ManifestError};
|
||||
pub use run::{Control, Error, Params, TileRunner, denoise_bayer};
|
||||
|
|
@ -52,6 +52,13 @@ pub struct Header {
|
|||
pub key: String,
|
||||
}
|
||||
|
||||
fn allocate<T: Clone>(n: usize, value: T) -> Result<Vec<T>, ProductError> {
|
||||
let mut v = Vec::new();
|
||||
v.try_reserve_exact(n).map_err(|_| ProductError::Format("not enough memory for the denoised picture".into()))?;
|
||||
v.resize(n, value);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// `f32` as IEEE half-float bits, rounded to nearest even; beyond the range it saturates, NaN becomes 0.
|
||||
pub fn to_f16(v: f32) -> u16 {
|
||||
if !v.is_finite() {
|
||||
|
|
@ -120,9 +127,9 @@ fn crc(bytes: &[u8]) -> u32 {
|
|||
}
|
||||
|
||||
/// One strip as bytes: for each plane (R, G, B) the low bytes of its half floats, then the high bytes.
|
||||
fn encode_strip(img: &Rgb32f, y0: usize, y1: usize) -> Vec<u8> {
|
||||
fn encode_strip(img: &Rgb32f, y0: usize, y1: usize) -> Result<Vec<u8>, ProductError> {
|
||||
let n = (y1 - y0) * img.width;
|
||||
let mut raw = vec![0u8; 6 * n];
|
||||
let mut raw = allocate(6 * n, 0u8)?;
|
||||
let rows = img.data.get(y0 * img.width..y1 * img.width).unwrap_or(&[]);
|
||||
for (i, px) in rows.iter().enumerate() {
|
||||
for (c, &v) in px.iter().enumerate() {
|
||||
|
|
@ -135,16 +142,17 @@ fn encode_strip(img: &Rgb32f, y0: usize, y1: usize) -> Vec<u8> {
|
|||
}
|
||||
}
|
||||
}
|
||||
miniz_oxide::deflate::compress_to_vec(&raw, 3)
|
||||
Ok(miniz_oxide::deflate::compress_to_vec(&raw, 3))
|
||||
}
|
||||
|
||||
/// Write `img` to `path` (through a temporary file next to it, so a reader never sees half a product).
|
||||
pub fn write(path: &Path, img: &Rgb32f, key: &str) -> Result<(), ProductError> {
|
||||
if img.width == 0 || img.height == 0 || img.width > MAX_SIDE || img.height > MAX_SIDE || img.data.len() != img.width * img.height {
|
||||
let pixels = img.width.checked_mul(img.height).filter(|&n| n <= MAX_PIXELS);
|
||||
if img.width == 0 || img.height == 0 || img.width > MAX_SIDE || img.height > MAX_SIDE || pixels != Some(img.data.len()) {
|
||||
return Err(ProductError::Format("the picture has no usable size".into()));
|
||||
}
|
||||
let strips: Vec<(usize, usize)> = (0..img.height).step_by(STRIP_ROWS).map(|y| (y, (y + STRIP_ROWS).min(img.height))).collect();
|
||||
let packed: Vec<Vec<u8>> = strips.par_iter().map(|&(a, b)| encode_strip(img, a, b)).collect();
|
||||
let packed: Vec<Vec<u8>> = strips.par_iter().map(|&(a, b)| encode_strip(img, a, b)).collect::<Result<_, _>>()?;
|
||||
let header = serde_json::to_vec(&Header { width: img.width, height: img.height, strip: STRIP_ROWS, key: key.to_string() })
|
||||
.map_err(|e| ProductError::Format(e.to_string()))?;
|
||||
let tmp = path.with_extension("tmp");
|
||||
|
|
@ -193,7 +201,7 @@ fn read_start(r: &mut impl Read) -> Result<Start, ProductError> {
|
|||
if len > MAX_HEADER {
|
||||
return Err(ProductError::Format("the header is too large".into()));
|
||||
}
|
||||
let mut hb = vec![0u8; len];
|
||||
let mut hb = allocate(len, 0u8)?;
|
||||
r.read_exact(&mut hb)?;
|
||||
let h: Header = serde_json::from_slice(&hb).map_err(|e| ProductError::Format(e.to_string()))?;
|
||||
let pixels = h.width.saturating_mul(h.height);
|
||||
|
|
@ -297,7 +305,7 @@ pub fn read_window(path: &Path, key: &str, window: Option<Window>, factor: usize
|
|||
}
|
||||
// the picture rows and columns the blocks cover
|
||||
let (x_end, y_end) = if whole { (w.x + w.width, w.y + w.height) } else { (w.x + ow * factor, w.y + oh * factor) };
|
||||
let mut out = Rgb32f { width: ow, height: oh, data: vec![[0.0; 3]; ow * oh] };
|
||||
let mut out = Rgb32f { width: ow, height: oh, data: allocate(ow * oh, [0.0; 3])? };
|
||||
let lut = f16_table();
|
||||
let value = |raw: &[u8], c: usize, n: usize, i: usize| -> f32 {
|
||||
let (lo, hi) = (raw.get(2 * c * n + i).copied().unwrap_or(0), raw.get((2 * c + 1) * n + i).copied().unwrap_or(0));
|
||||
|
|
@ -305,7 +313,7 @@ pub fn read_window(path: &Path, key: &str, window: Option<Window>, factor: usize
|
|||
};
|
||||
// which strips are needed, and where each starts in the file (after the header and the strip table)
|
||||
let wanted = |s: usize| s * h.strip < y_end && (s + 1) * h.strip > w.y;
|
||||
let (mut sum, mut peak) = (vec![[0f32; 3]; ow], vec![[f32::MIN; 3]; ow]);
|
||||
let (mut sum, mut peak) = (allocate(ow, [0f32; 3])?, allocate(ow, [f32::MIN; 3])?);
|
||||
let (mut rows_in, mut oy) = (0usize, 0usize);
|
||||
let strips: Vec<usize> = (0..table.len()).filter(|&s| wanted(s)).collect();
|
||||
// skip the strips before the first one wanted
|
||||
|
|
@ -315,7 +323,7 @@ pub fn read_window(path: &Path, key: &str, window: Option<Window>, factor: usize
|
|||
let mut packed = Vec::with_capacity(group.len());
|
||||
for &s in group {
|
||||
let Some(&(len, _)) = table.get(s) else { return Err(ProductError::Format("a strip is missing".into())) };
|
||||
let mut p = vec![0u8; len];
|
||||
let mut p = allocate(len, 0u8)?;
|
||||
f.read_exact(&mut p)?;
|
||||
packed.push(p);
|
||||
}
|
||||
|
|
@ -401,6 +409,19 @@ pub fn blend(base: &mut Rgb32f, denoised: &Rgb32f, amount: f32) -> bool {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn oversized_product_header_is_rejected_before_allocating_pixels() {
|
||||
let p = temp("oversized-header");
|
||||
let header = serde_json::to_vec(&Header { width: 10001, height: 10000, strip: STRIP_ROWS, key: "k".into() }).unwrap();
|
||||
let mut bytes = Vec::from(MAGIC.as_slice());
|
||||
bytes.extend_from_slice(&[VERSION, 0, 0, 0]);
|
||||
bytes.extend_from_slice(&(header.len() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&header);
|
||||
std::fs::write(&p, bytes).unwrap();
|
||||
assert!(read_header_at(&p).is_err());
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
|
||||
fn temp(name: &str) -> std::path::PathBuf {
|
||||
std::env::temp_dir().join(format!("lc-denoise-{name}-{}.lcdn", std::process::id()))
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ use crate::manifest::Gain;
|
|||
use crate::tiles::{starts, weight};
|
||||
|
||||
/// Most pixels one picture may have (guards the allocations that follow from its size).
|
||||
pub const MAX_PIXELS: usize = 400_000_000;
|
||||
pub const MAX_PIXELS: usize = 100_000_000;
|
||||
/// A tile whose mean signal is below this (white = 1) is too dark to measure its scale from.
|
||||
const MIN_SIGNAL: f32 = 0.003;
|
||||
|
||||
|
|
@ -32,6 +32,13 @@ pub enum Error {
|
|||
Cancelled,
|
||||
}
|
||||
|
||||
fn allocate<T: Clone>(n: usize, value: T) -> Result<Vec<T>, Error> {
|
||||
let mut v = Vec::new();
|
||||
v.try_reserve_exact(n).map_err(|_| Error::Input("not enough memory for denoise".into()))?;
|
||||
v.resize(n, value);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Something that runs the model on one tile.
|
||||
pub trait TileRunner: Sync {
|
||||
/// `input` is the tile as four planes `[R, G1, G2, B]` of `tile × tile` cells (NCHW, batch 1); the answer is
|
||||
|
|
@ -112,13 +119,13 @@ pub fn denoise_bayer(
|
|||
.collect();
|
||||
let total = tiles.len();
|
||||
|
||||
let mut acc = vec![[0f32; 3]; pixels];
|
||||
let mut wsum = vec![0f32; nx * ny];
|
||||
let group = p.parallel.clamp(1, 64);
|
||||
let mut acc = allocate(pixels, [0f32; 3])?;
|
||||
let mut wsum = allocate(nx * ny, 0f32)?;
|
||||
let group = p.parallel.clamp(1, 4);
|
||||
let mut done = 0;
|
||||
// where the time goes, printed under `LIGHTCRAFT_PROFILE`
|
||||
let (mut t_wait, mut t_total) = (std::time::Duration::ZERO, std::time::Duration::ZERO);
|
||||
let began = std::time::Instant::now();
|
||||
let began = web_time::Instant::now();
|
||||
|
||||
// One group of tiles at a time: pack, run the model, check and bring each to the input's scale, in parallel.
|
||||
// Blending (sequential: the tiles add into one picture, in tile order so the result does not depend on timing) of
|
||||
|
|
@ -128,7 +135,7 @@ pub fn denoise_bayer(
|
|||
chunk
|
||||
.par_iter()
|
||||
.map(|&(x0, y0, bx, ax, by, ay)| {
|
||||
let mut input = vec![0f32; 4 * p.tile * p.tile];
|
||||
let mut input = allocate(4 * p.tile * p.tile, 0f32)?;
|
||||
if !pack_tile(mosaic, width, height, layout, x0 as isize, y0 as isize, p.tile, &mut input) {
|
||||
return Err(Error::Input("a tile could not be packed".into()));
|
||||
}
|
||||
|
|
@ -164,7 +171,7 @@ pub fn denoise_bayer(
|
|||
|| compute(chunk),
|
||||
|| match previous.take() {
|
||||
Some((c, o)) => {
|
||||
let started = std::time::Instant::now();
|
||||
let started = web_time::Instant::now();
|
||||
let r = blend(c, o);
|
||||
(r, started.elapsed())
|
||||
}
|
||||
|
|
@ -176,12 +183,12 @@ pub fn denoise_bayer(
|
|||
previous = Some((chunk, outs));
|
||||
}
|
||||
if let Some((c, o)) = previous.take() {
|
||||
let started = std::time::Instant::now();
|
||||
let started = web_time::Instant::now();
|
||||
blend(c, o)?;
|
||||
t_wait += started.elapsed();
|
||||
}
|
||||
t_total += began.elapsed();
|
||||
let started = std::time::Instant::now();
|
||||
let started = web_time::Instant::now();
|
||||
// normalise by the weights
|
||||
let (ox, oy) = (layout.position(0, 0).0, layout.position(0, 0).1);
|
||||
acc.par_chunks_mut(width).enumerate().for_each(|(y, row)| {
|
||||
|
|
@ -10,11 +10,15 @@ workspace = true
|
|||
|
||||
[features]
|
||||
default = []
|
||||
runtime = ["dep:faer"]
|
||||
# Product-policy switch: absent by default; users may install their own model.
|
||||
rawnind-model = ["lightcraft-denoise-core/rawnind-model"]
|
||||
|
||||
[dependencies]
|
||||
# Safe views over Rust-intrinsic gemm kernels. Pin the core-only release: newer backends
|
||||
# generate assembly. No BLAS, cc/build scripts, nightly/AMX, or inner thread pool.
|
||||
faer = { package = "faer-core", version = "=0.17.1", default-features = false, features = ["std"] }
|
||||
lightcraft-denoise-core = { workspace = true }
|
||||
# Safe matrix views over the pure-Rust GEMM backend; no BLAS, cc/build scripts
|
||||
# or inner thread pool. Its dependencies include architecture-specific assembly.
|
||||
faer = { package = "faer-core", version = "=0.17.1", default-features = false, features = ["std"], optional = true }
|
||||
lightcraft-raster = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@
|
|||
L1 pure-Rust AI RAW denoise primitives and CPU inference. No weights are bundled.
|
||||
See [user workflow and limitations](../../docs/denoise.md).
|
||||
|
||||
`manifest` and `known` validate opt-in models; `onnx` reads a bounded operator subset
|
||||
`lightcraft-denoise-core` provides metadata, Bayer helpers and cache formats,
|
||||
re-exported here. `manifest` and `known` validate opt-in models; `onnx` reads a bounded operator subset
|
||||
into `net`; `cpu` executes it and `reference` supplies scalar checks. `bayer`, `tiles`
|
||||
and `run` pack sensor data and blend overlapping tiles while preserving clipped
|
||||
highlights. `product` reads/writes the disposable `.lcdn` cache. `runtime` supplies
|
||||
the installation self-test. Synthetic networks are generated in Rust for tests.
|
||||
|
||||
Run `cargo test -p lightcraft-denoise`. Real-model tests are ignored by default and
|
||||
Run `cargo test -p lightcraft-denoise --features runtime`. The CPU backend is optional;
|
||||
featureless and web builds keep the model metadata and cache format without faer/GEMM.
|
||||
The `rawnind-model` feature enables the otherwise empty download catalog and requires
|
||||
an explicit release-policy decision. Real-model tests are ignored by default and
|
||||
require an explicitly supplied local model. The GPU twin is `lightcraft-gpu::nn`;
|
||||
model management, background jobs and export integration live in `lightcraft-engine`.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const BLOCK: usize = 256;
|
|||
/// Cap all tensor buffers and convolution scratch for one tile together (512 MiB).
|
||||
const MAX_VALUES: usize = 128 * 1024 * 1024;
|
||||
const MAX_TILE: usize = 2048;
|
||||
const MAX_IDLE: usize = 16;
|
||||
const MAX_IDLE: usize = 1;
|
||||
|
||||
type Result<T> = std::result::Result<T, NetError>;
|
||||
fn bad(why: &str) -> NetError {
|
||||
|
|
|
|||
|
|
@ -18,25 +18,20 @@
|
|||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod archive;
|
||||
pub mod bayer;
|
||||
#[cfg(feature = "runtime")]
|
||||
pub mod cpu;
|
||||
pub mod known;
|
||||
pub mod manifest;
|
||||
|
||||
pub mod net;
|
||||
pub mod onnx;
|
||||
mod onnx_proto;
|
||||
pub mod product;
|
||||
|
||||
pub mod reference;
|
||||
pub mod run;
|
||||
|
||||
#[cfg(feature = "runtime")]
|
||||
pub mod runtime;
|
||||
#[doc(hidden)]
|
||||
pub mod synthetic;
|
||||
pub mod tiles;
|
||||
|
||||
pub use manifest::{DenoiserManifest, Domain, Gain, ManifestError};
|
||||
pub use run::{Control, Error, Params, TileRunner, denoise_bayer};
|
||||
pub use lightcraft_denoise_core::*;
|
||||
|
||||
pub mod hash;
|
||||
pub mod licence;
|
||||
pub mod synthetic_proto;
|
||||
|
|
|
|||
|
|
@ -408,10 +408,20 @@ mod cpu_tests {
|
|||
}
|
||||
let path = std::env::temp_dir().join(format!("lc-cpu-load-{}.onnx", std::process::id()));
|
||||
std::fs::write(&path, crate::synthetic::unet_onnx(64, 5, 1, 7)).unwrap();
|
||||
let mut m = crate::known::find("rawnind-bayer").unwrap().manifest;
|
||||
m.tile = 64;
|
||||
m.overlap = 16;
|
||||
m.gain = Gain::None;
|
||||
let mut m = DenoiserManifest {
|
||||
id: "synthetic".into(),
|
||||
name: "Synthetic".into(),
|
||||
version: "1".into(),
|
||||
licence: Default::default(),
|
||||
source: None,
|
||||
sha256: None,
|
||||
size_bytes: None,
|
||||
provenance: "synthetic test".into(),
|
||||
domain: crate::manifest::Domain::BayerToRgb,
|
||||
tile: 64,
|
||||
overlap: 16,
|
||||
gain: Gain::None,
|
||||
};
|
||||
let r = CpuRunner::load(&path, &m).unwrap();
|
||||
let x = check_tile(64);
|
||||
assert_eq!(r.run(&x).unwrap(), r.run(&x).unwrap());
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! ```text
|
||||
//! LC_DENOISE_MODEL=<model_bayer.onnx> LC_DENOISE_MOSAICS=<folder> LC_DENOISE_OUT=<folder> \
|
||||
//! cargo test --release -p lightcraft-denoise --test real_model -- --ignored --nocapture
|
||||
//! cargo test --release -p lightcraft-denoise --features runtime --test real_model -- --ignored --nocapture
|
||||
//! ```
|
||||
//!
|
||||
//! `LC_DENOISE_MOSAICS` holds `<name>.mosaic.f32` (little-endian f32, normalised: black 0, white 1, not white
|
||||
|
|
@ -11,6 +11,8 @@
|
|||
//! once, default 8), `LC_DENOISE_ONLY` (one name), `LC_DENOISE_ISO` (noise strength, default 1.0; 0 adds none),
|
||||
//! `LC_DENOISE_FULL` (the whole frame instead of a 2048 × 1536 crop).
|
||||
|
||||
#![cfg(feature = "runtime")]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ web-time = { workspace = true }
|
|||
lightcraft-preview = { workspace = true }
|
||||
lightcraft-codecs = { workspace = true }
|
||||
lightcraft-raw = { workspace = true }
|
||||
lightcraft-denoise = { workspace = true }
|
||||
lightcraft-denoise-core = { workspace = true }
|
||||
lightcraft-denoise = { workspace = true, optional = true }
|
||||
lightcraft-meta = { workspace = true }
|
||||
miniz_oxide = { workspace = true }
|
||||
lightcraft-merge = { workspace = true }
|
||||
|
|
@ -39,7 +40,8 @@ lightcraft-segment = { workspace = true, optional = true }
|
|||
[features]
|
||||
# AI masks with SAM 3 (Object / Describe). Off for the web and CLI builds.
|
||||
sam = ["dep:lightcraft-segment"]
|
||||
denoise = []
|
||||
denoise = ["dep:lightcraft-denoise", "lightcraft-denoise/runtime", "lightcraft-gpu/denoise"]
|
||||
rawnind-model = ["denoise", "lightcraft-denoise/rawnind-model", "lightcraft-denoise-core/rawnind-model"]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
lightcraft-fetch = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
//! validated before they become folder names, and nothing outside a model's own folder is ever touched.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::PoisonError;
|
||||
use std::sync::{Arc, PoisonError, mpsc};
|
||||
|
||||
use lightcraft_catalog::PhotoId;
|
||||
use lightcraft_denoise::archive;
|
||||
|
|
@ -18,7 +18,7 @@ use serde_json::{Value, json};
|
|||
|
||||
use super::{CommandSpec, always, bad, bool_or, cmd, str_param};
|
||||
use crate::denoise::{Pace, RunOn, Settings, forget_gpu_set_ups, installed_models, read_capped, read_settings, write_atomic, write_settings};
|
||||
use crate::model_download::{self, State};
|
||||
use crate::model_download::State;
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
fn fail(what: &str, e: impl std::fmt::Display) -> EngineError {
|
||||
|
|
@ -129,8 +129,16 @@ fn identify(path: &Path, c: &str, sha_known: Option<&str>) -> Result<Source> {
|
|||
|
||||
/// Install the model in `src`. A model that passes its self-test becomes the one in use (`activate`, the default): installing
|
||||
/// a model is how someone says they want it. `verified` is for a download whose hash was already checked.
|
||||
fn install_file(s: &mut Session, c: &str, src: &Path, acknowledged: bool, activate: bool, verified: bool) -> Result<Value> {
|
||||
let dir = models_dir(s, c)?;
|
||||
fn install_file(
|
||||
dir: &Path,
|
||||
loader: &crate::denoise::Loader,
|
||||
run_on: RunOn,
|
||||
accepted_at: String,
|
||||
c: &str,
|
||||
src: &Path,
|
||||
acknowledged: bool,
|
||||
verified: bool,
|
||||
) -> Result<(Value, Arc<dyn crate::denoise::Model>)> {
|
||||
let ident = identify(src, c, None)?;
|
||||
let m = ident.manifest.clone();
|
||||
manifest::validate(&m).map_err(|e| bad(c, e.to_string()))?;
|
||||
|
|
@ -141,7 +149,11 @@ fn install_file(s: &mut Session, c: &str, src: &Path, acknowledged: bool, activa
|
|||
));
|
||||
}
|
||||
let home = dir.join(&m.id);
|
||||
if home.join("model.onnx").exists() {
|
||||
return Err(bad(c, "that model is already installed; remove it before installing a replacement"));
|
||||
}
|
||||
std::fs::create_dir_all(&home).map_err(|e| fail("could not create the model's folder", e))?;
|
||||
let mut cleanup = InstallCleanup { home: home.clone(), armed: true };
|
||||
let (part, final_path) = (home.join("model.onnx.part"), home.join("model.onnx"));
|
||||
let _ = std::fs::remove_file(&part);
|
||||
let placed: Result<()> = match ident.entry {
|
||||
|
|
@ -162,26 +174,24 @@ fn install_file(s: &mut Session, c: &str, src: &Path, acknowledged: bool, activa
|
|||
return Err(e);
|
||||
}
|
||||
};
|
||||
let _ = std::fs::remove_file(&final_path);
|
||||
if let Err(e) = std::fs::rename(&part, &final_path) {
|
||||
let _ = std::fs::remove_file(&part);
|
||||
return Err(fail("could not finish the copy", e));
|
||||
}
|
||||
// a model that does not work is never left installed
|
||||
let run_on = s.denoise.settings.run_on();
|
||||
let test = (s.denoise.loader)(&final_path, &m).and_then(|model| model.self_test(run_on));
|
||||
let test = match test {
|
||||
// Test the staged file first. A failed replacement preserves the installed model.
|
||||
let model = loader(&part, &m).map_err(|e| bad(c, e))?;
|
||||
let test = match model.self_test(run_on) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
let _ = std::fs::remove_file(&final_path);
|
||||
let _ = std::fs::remove_file(home.join("denoise-model.json"));
|
||||
let _ = std::fs::remove_dir(&home);
|
||||
let _ = std::fs::remove_file(&part);
|
||||
return Err(bad(c, e));
|
||||
}
|
||||
};
|
||||
// An installed id is immutable: installing again must never destroy a usable model.
|
||||
if final_path.exists() {
|
||||
let _ = std::fs::remove_file(&part);
|
||||
return Err(bad(c, "that model is already installed; remove it before installing a replacement"));
|
||||
}
|
||||
std::fs::rename(&part, &final_path).map_err(|e| fail("could not finish the copy", e))?;
|
||||
write_atomic(&home.join("denoise-model.json"), &serde_json::to_vec_pretty(&m).map_err(|e| fail("manifest", e))?).map_err(EngineError::Other)?;
|
||||
let accepted = json!({
|
||||
"acceptedAt": (s.clock)(),
|
||||
"acceptedAt": accepted_at,
|
||||
"licence": m.licence.name,
|
||||
"commercial": m.licence.commercial,
|
||||
"fileName": ident.file_name,
|
||||
|
|
@ -189,26 +199,116 @@ fn install_file(s: &mut Session, c: &str, src: &Path, acknowledged: bool, activa
|
|||
"selfTest": test,
|
||||
});
|
||||
write_atomic(&home.join("installed.json"), &serde_json::to_vec_pretty(&accepted).map_err(|e| fail("record", e))?).map_err(EngineError::Other)?;
|
||||
// a file that was downloaded and waited for the user's acceptance has served: the installed copy is the model now
|
||||
if src.parent() == Some(dir.join(model_download::STAGING).as_path()) {
|
||||
let _ = std::fs::remove_file(src);
|
||||
s.denoise.downloads.staged_file_used(src);
|
||||
cleanup.armed = false;
|
||||
Ok((json!({"installed": row(&m, known::find(&m.id).and_then(|k| host_of(&k)), true, false, &accepted), "model": m.id}), model))
|
||||
}
|
||||
|
||||
/// Removes only files created for this new installation, including on a worker panic.
|
||||
struct InstallCleanup {
|
||||
home: PathBuf,
|
||||
armed: bool,
|
||||
}
|
||||
impl Drop for InstallCleanup {
|
||||
fn drop(&mut self) {
|
||||
if self.armed {
|
||||
for name in ["model.onnx.part", "model.onnx", "denoise-model.json", "installed.json"] {
|
||||
let _ = std::fs::remove_file(self.home.join(name));
|
||||
}
|
||||
let _ = std::fs::remove_dir(&self.home);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InstallJob {
|
||||
id: String,
|
||||
activate: bool,
|
||||
result: mpsc::Receiver<std::result::Result<(Value, Arc<dyn crate::denoise::Model>), String>>,
|
||||
}
|
||||
|
||||
fn activate_install(s: &mut Session, result: &Value, model: Arc<dyn crate::denoise::Model>, activate: bool) -> Result<Value> {
|
||||
let dir = models_dir(s, "denoise.models.install")?;
|
||||
let id = result["installed"]["id"].as_str().ok_or_else(|| bad("denoise.models.install", "missing installed id"))?;
|
||||
let mut st = read_settings(&dir);
|
||||
if activate {
|
||||
st.model = Some(m.id.clone());
|
||||
st.model = Some(id.to_string());
|
||||
write_settings(&dir, &st).map_err(EngineError::Other)?;
|
||||
}
|
||||
s.denoise.touch();
|
||||
s.denoise_refresh_active(true);
|
||||
let host = known::find(&m.id).and_then(|k| host_of(&k));
|
||||
Ok(json!({"installed": row(&m, host, true, st.model.as_deref() == Some(m.id.as_str()), &accepted), "model": st.model}))
|
||||
if let Some(a) = &s.denoise.active
|
||||
&& a.id == id
|
||||
{
|
||||
*a.model.lock().unwrap_or_else(PoisonError::into_inner) = Some(model);
|
||||
}
|
||||
let mut v = result.clone();
|
||||
v["installed"]["selected"] = json!(st.model.as_deref() == Some(id));
|
||||
v["model"] = json!(st.model);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn start_install(s: &mut Session, src: PathBuf, id: String, activate: bool, verified: bool) -> Result<Value> {
|
||||
const C: &str = "denoise.models.install";
|
||||
if s.denoise.install.is_some() {
|
||||
return Err(bad(C, "another model is being installed"));
|
||||
}
|
||||
let dir = models_dir(s, C)?;
|
||||
let loader = s.denoise.loader.clone();
|
||||
let run_on = s.denoise.settings.run_on();
|
||||
let at = (s.clock)();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
std::thread::Builder::new()
|
||||
.name("denoise-model-install".into())
|
||||
.stack_size(16 * 1024 * 1024)
|
||||
.spawn(move || {
|
||||
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _held = crate::memory::work_gate().acquire(512 * 1024 * 1024);
|
||||
install_file(&dir, &loader, run_on, at, C, &src, true, verified).map_err(|e| plain(&e))
|
||||
}))
|
||||
.unwrap_or_else(|_| Err("the model installation stopped unexpectedly".into()));
|
||||
if verified {
|
||||
let _ = std::fs::remove_file(&src);
|
||||
}
|
||||
let _ = tx.send(outcome);
|
||||
})
|
||||
.map_err(|e| bad(C, format!("could not start installation: {e}")))?;
|
||||
s.denoise.install = Some(InstallJob { id: id.clone(), activate, result: rx });
|
||||
s.denoise.downloads.set_outcome(&id, State::Installing);
|
||||
Ok(json!({"started": id}))
|
||||
}
|
||||
|
||||
fn install(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "denoise.models.install";
|
||||
let path = str_param(p, "path").ok_or_else(|| bad(C, "missing `path`"))?;
|
||||
install_file(s, C, Path::new(path), bool_or(p, "acknowledged", false), bool_or(p, "activate", true), false)
|
||||
let acknowledged = bool_or(p, "acknowledged", false);
|
||||
let activate = bool_or(p, "activate", true);
|
||||
if !acknowledged {
|
||||
return Err(bad(C, "the licence has not been accepted: show its terms first"));
|
||||
}
|
||||
if bool_or(p, "background", false) {
|
||||
return start_install(s, PathBuf::from(path), "local-install".into(), activate, false);
|
||||
}
|
||||
if s.denoise.install.is_some() {
|
||||
return Err(bad(C, "another model is being installed"));
|
||||
}
|
||||
let (result, model) =
|
||||
install_file(&models_dir(s, C)?, &s.denoise.loader, s.denoise.settings.run_on(), (s.clock)(), C, Path::new(path), acknowledged, false)?;
|
||||
activate_install(s, &result, model, activate)
|
||||
}
|
||||
|
||||
/// Read only the small adjacent manifest; hashing and execution happen in the install worker.
|
||||
fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "denoise.models.inspect";
|
||||
let path = str_param(p, "path").ok_or_else(|| bad(C, "missing `path`"))?;
|
||||
let src = Path::new(path);
|
||||
if !src.extension().is_some_and(|e| e.eq_ignore_ascii_case("onnx")) {
|
||||
return Err(bad(C, "choose an .onnx file with denoise-model.json beside it"));
|
||||
}
|
||||
let bytes = read_capped(&src.with_file_name("denoise-model.json"), MAX_MANIFEST_BYTES)
|
||||
.ok_or_else(|| bad(C, "denoise-model.json is missing or too large"))?;
|
||||
let m = manifest::parse(&bytes).map_err(|e| bad(C, e.to_string()))?;
|
||||
let dir = models_dir(s, C)?;
|
||||
Ok(json!({"kind": "model", "domain": "denoise", "path": path, "fileName": src.file_name().map(|n| n.to_string_lossy()), "model": m,
|
||||
"sizeBytes": std::fs::metadata(src).ok().map(|m| m.len()), "alreadyInstalled": dir.join(&m.id).join("model.onnx").is_file()}))
|
||||
}
|
||||
|
||||
/// What an error says, without the command it came from (for a line shown to the user).
|
||||
|
|
@ -222,17 +322,31 @@ fn plain(e: &EngineError) -> String {
|
|||
/// Install every download that has arrived: each was accepted when it was started, so it is installed and chosen without
|
||||
/// another question. Called by `denoise.models.downloads` and by every frame's `denoise.pump`.
|
||||
pub(crate) fn finish_downloads(s: &mut Session) {
|
||||
const C: &str = "denoise.models.download";
|
||||
for (id, path, _sha) in s.denoise.downloads.finished() {
|
||||
match install_file(s, C, &path, true, true, true) {
|
||||
Ok(_) => s.denoise.downloads.set_outcome(&id, State::Installed),
|
||||
Err(e) => {
|
||||
// a file that cannot be installed is of no use to anyone: throw it away, say why
|
||||
let _ = std::fs::remove_file(&path);
|
||||
s.denoise.downloads.set_outcome(&id, State::Failed(plain(&e)));
|
||||
}
|
||||
if let Some(job) = &s.denoise.install {
|
||||
let outcome = match job.result.try_recv() {
|
||||
Ok(v) => Some(v),
|
||||
Err(mpsc::TryRecvError::Empty) => None,
|
||||
Err(mpsc::TryRecvError::Disconnected) => Some(Err("the installation worker stopped".into())),
|
||||
};
|
||||
if let Some(outcome) = outcome
|
||||
&& let Some(job) = s.denoise.install.take()
|
||||
{
|
||||
let state = match outcome {
|
||||
Ok((v, model)) => match activate_install(s, &v, model, job.activate) {
|
||||
Ok(_) => State::Installed,
|
||||
Err(e) => State::Failed(plain(&e)),
|
||||
},
|
||||
Err(e) => State::Failed(e),
|
||||
};
|
||||
s.denoise.downloads.set_outcome(&job.id, state);
|
||||
}
|
||||
}
|
||||
if s.denoise.install.is_none()
|
||||
&& let Some((id, path, _sha)) = s.denoise.downloads.finished().into_iter().next()
|
||||
&& let Err(e) = start_install(s, path, id.clone(), true, true)
|
||||
{
|
||||
s.denoise.downloads.set_outcome(&id, State::Failed(plain(&e)));
|
||||
}
|
||||
}
|
||||
|
||||
fn download(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
|
@ -263,6 +377,7 @@ fn download_row(id: &str, state: &State) -> Value {
|
|||
match state {
|
||||
State::Running { bytes, total } => json!({"id": id, "state": "running", "bytes": bytes, "total": total, "from": from}),
|
||||
State::Done { path, .. } => json!({"id": id, "state": "done", "path": path.display().to_string(), "from": from}),
|
||||
State::Installing => json!({"id": id, "state": "installing", "from": from}),
|
||||
State::Installed => json!({"id": id, "state": "installed", "from": from}),
|
||||
State::Failed(why) => json!({"id": id, "state": "failed", "error": why, "from": from}),
|
||||
State::Cancelled => json!({"id": id, "state": "cancelled", "from": from}),
|
||||
|
|
@ -517,7 +632,8 @@ fn clear(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(query "denoise.models.list", "Denoise Models", [], None, "{} → {dir, productsDir, model, runtime, auto, cacheGb, threads, runOn, models: [{id, name, version, licence{name, commercial, url, notice}, provenance, source, sizeBytes, sha256, tile, known, downloadHost, installed, selected, accepted}]}", always, list),
|
||||
cmd!(query "denoise.models.install", "Install Denoise Model", [], None, "{path, acknowledged: true, activate?: true} → {installed, model} — install a denoise model from a file: an .onnx with a denoise-model.json beside it, or an archive LightCraft knows (the darktable `.dtmodel`). `acknowledged` must be true: the user has been shown the model's licence and accepted it. Unless `activate` is false it becomes the model in use", always, install),
|
||||
cmd!(query "denoise.models.inspect", "Inspect Denoise Model", [], None, "{path} → model terms from an adjacent manifest; never executes the file", always, inspect),
|
||||
cmd!(query "denoise.models.install", "Install Denoise Model", [], None, "{path, acknowledged: true, activate?: true, background?: false} → {installed, model} or {started} — install a denoise model from a file: an .onnx with a denoise-model.json beside it, or an archive LightCraft knows (the darktable `.dtmodel`). `acknowledged` must be true: the user has been shown the model's licence and accepted it. Unless `activate` is false it becomes the model in use", always, install),
|
||||
cmd!(query "denoise.models.download", "Download Denoise Model", [], None, "{id, acknowledged: true} → {started, from} — fetch a model LightCraft has a pinned address for (see `downloadHost` in the list) in the background over HTTPS with lightcraft-fetch. `acknowledged` must be true: the user has been shown the model's terms and accepted them. It is checked against its size and SHA-256, installed and chosen by itself; `denoise.models.downloads` shows how far it is", always, download),
|
||||
cmd!(query "denoise.models.downloads", "Denoise Model Downloads", [], None, "{} → {running, downloads: [{id, state: running | done | installed | failed | cancelled, bytes, total, error, from}]} — also installs any download that has arrived", always, downloads),
|
||||
cmd!(query "denoise.models.downloadCancel", "Cancel Denoise Model Download", [], None, "{id} → {discarded} — stop a download, or delete a finished one that was not installed", always, download_cancel),
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ pub(crate) trait Model: Send + Sync {
|
|||
/// Check it does something sensible: the test's result, or why not. Unless `run_on` is the processor the graphics
|
||||
/// card is set up and checked too, and the result says where the work will run.
|
||||
fn self_test(&self, run_on: RunOn) -> Result<Value, String>;
|
||||
/// Conservative scratch bound for an unknown runner.
|
||||
fn bytes_per_tile(&self) -> usize {
|
||||
512 * 1024 * 1024
|
||||
}
|
||||
/// Where the tiles run: `{kind: "gpu", adapter}`, `{kind: "cpu", reason?}` or `{kind: "pending"}` (the card is not
|
||||
/// set up yet), with the card's and the processor's time for the check tile (`cardMs`, `cpuMs`) once both were
|
||||
/// timed. Never starts anything.
|
||||
|
|
@ -215,7 +219,7 @@ fn set_up_card<T: Send + 'static, R>(
|
|||
running.push(marker.to_path_buf());
|
||||
SetUpEnded(marker.to_path_buf())
|
||||
};
|
||||
let started = std::time::Instant::now();
|
||||
let started = web_time::Instant::now();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::Builder::new()
|
||||
.name("denoise-gpu-setup".into())
|
||||
|
|
@ -241,10 +245,10 @@ fn card_tile_ms(runner: &dyn TileRunner, input: &[f32]) -> f64 {
|
|||
if runner.run(input).is_err() {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
let began = std::time::Instant::now();
|
||||
let began = web_time::Instant::now();
|
||||
let mut best = f64::INFINITY;
|
||||
for _ in 0..3 {
|
||||
let one = std::time::Instant::now();
|
||||
let one = web_time::Instant::now();
|
||||
if runner.run(input).is_err() {
|
||||
break;
|
||||
}
|
||||
|
|
@ -268,7 +272,7 @@ fn make_gpu(cpu: &lightcraft_denoise::runtime::CpuRunner, path: &Path, tile: usi
|
|||
Ok((runner, got, card_ms))
|
||||
};
|
||||
let on_cpu = || {
|
||||
let started = std::time::Instant::now();
|
||||
let started = web_time::Instant::now();
|
||||
(cpu.run(&input), started.elapsed().as_secs_f64() * 1000.0)
|
||||
};
|
||||
let ((runner, got, card_ms), (want, cpu_ms)) = set_up_card(&path.with_file_name(GPU_SETUP_MARKER), GPU_SETUP_LIMIT, card, on_cpu)?;
|
||||
|
|
@ -351,6 +355,9 @@ impl TileRunner for Network {
|
|||
|
||||
#[cfg(feature = "denoise")]
|
||||
impl Model for Network {
|
||||
fn bytes_per_tile(&self) -> usize {
|
||||
self.cpu.bytes_per_tile()
|
||||
}
|
||||
fn runner(&self, run_on: RunOn, threads: usize) -> (&dyn TileRunner, usize) {
|
||||
let threads = threads.max(1);
|
||||
self.last_threads.store(threads, Ordering::Relaxed);
|
||||
|
|
@ -689,7 +696,7 @@ pub(crate) fn make_product(spec: &JobSpec, progress: Option<&Progress>) -> Resul
|
|||
if product::is_current(&spec.product, &spec.key) {
|
||||
return Ok(false);
|
||||
}
|
||||
let began = std::time::Instant::now();
|
||||
let began = web_time::Instant::now();
|
||||
let cancelled = || progress.is_some_and(|p| p.cancel.load(Ordering::Relaxed));
|
||||
if cancelled() {
|
||||
return Err(MakeError::Cancelled);
|
||||
|
|
@ -726,7 +733,13 @@ pub(crate) fn make_product(spec: &JobSpec, progress: Option<&Progress>) -> Resul
|
|||
}
|
||||
let decoded = began.elapsed();
|
||||
let model = loaded(spec)?;
|
||||
let (runner, parallel) = model.runner(spec.run_on, spec.parallel.max(1));
|
||||
// Packed input, returned RGB and the preceding group's RGB overlap while tiles run.
|
||||
let scratch =
|
||||
model.bytes_per_tile().saturating_add(128usize.saturating_mul(spec.manifest.tile as usize).saturating_mul(spec.manifest.tile as usize));
|
||||
let (_, budget) = crate::memory::work_gate().usage();
|
||||
let parallel = spec.parallel.clamp(1, 4).min((budget / scratch.max(1)).max(1));
|
||||
let _held = crate::memory::work_gate().acquire(mosaic.data.len().saturating_mul(20).saturating_add(scratch.saturating_mul(parallel)));
|
||||
let (runner, parallel) = model.runner(spec.run_on, parallel);
|
||||
let params = Params {
|
||||
tile: spec.manifest.tile as usize,
|
||||
overlap: spec.manifest.overlap as usize,
|
||||
|
|
@ -859,6 +872,7 @@ pub(crate) struct State {
|
|||
pub models_dir: Option<PathBuf>,
|
||||
/// Models being downloaded at the user's request.
|
||||
pub downloads: Downloads,
|
||||
pub(crate) install: Option<crate::cmd::denoise::InstallJob>,
|
||||
/// How a model is loaded (tests put another in).
|
||||
pub loader: Loader,
|
||||
pub(crate) active: Option<Active>,
|
||||
|
|
@ -883,6 +897,7 @@ impl Default for State {
|
|||
State {
|
||||
models_dir: None,
|
||||
downloads: Downloads::default(),
|
||||
install: None,
|
||||
loader: default_loader(),
|
||||
active: None,
|
||||
active_seen: None,
|
||||
|
|
@ -1604,7 +1619,7 @@ mod gpu_tests {
|
|||
fn a_set_up_that_takes_too_long_is_given_up_on() {
|
||||
let dir = folder("slowsetup");
|
||||
let marker = dir.join(GPU_SETUP_MARKER);
|
||||
let started = std::time::Instant::now();
|
||||
let started = web_time::Instant::now();
|
||||
let slow = || -> Result<(), String> {
|
||||
std::thread::sleep(Duration::from_millis(600));
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -309,8 +309,11 @@ fn load_bytes_now(
|
|||
let tables = lightcraft_raw::profile::ProfileTables::new(&raw.color.profile, lightcraft_raw::color::illuminant_weight(&raw.color, xy));
|
||||
// camera RGB → what the pipeline takes: clipped highlights rebuilt, white balance and the colour model applied,
|
||||
// fitted to `max_edge` and upright (the same for the plain and the denoised picture)
|
||||
let finish = |mut img: Rgb32f| -> Rgb32f {
|
||||
let finish = |mut img: Rgb32f| {
|
||||
let mut stages = vec![("develop", t0.elapsed())];
|
||||
stages.push(("transform", t0.elapsed()));
|
||||
lightcraft_raw::highlight::reconstruct(&mut img, wb, HIGHLIGHT_CLIP);
|
||||
stages.push(("highlights", t0.elapsed()));
|
||||
img.map_in_place(|p| {
|
||||
let c = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]];
|
||||
let rgb = [
|
||||
|
|
@ -324,21 +327,27 @@ fn load_bytes_now(
|
|||
};
|
||||
hue_sat.as_ref().map_or(rgb, |h| h.apply(rgb)).map(|v| v.max(0.0))
|
||||
});
|
||||
stages.push(("colour", t0.elapsed()));
|
||||
let img = fit(&img, max_edge, max_edge, Filter::Box);
|
||||
img.into_oriented(raw.orientation)
|
||||
stages.push(("fit", t0.elapsed()));
|
||||
let img = img.into_oriented(raw.orientation);
|
||||
stages.push(("orient", t0.elapsed()));
|
||||
(img, stages)
|
||||
};
|
||||
let develop_ms = t0.elapsed();
|
||||
let img = finish(img);
|
||||
let twin = twin.map(finish);
|
||||
let (img, stages) = finish(img);
|
||||
if lightcraft_pipeline::profiling() {
|
||||
let finish_ms = (t0.elapsed() - develop_ms).as_secs_f64() * 1e3;
|
||||
eprintln!(
|
||||
"[profile] raw source {}×{} (max {max_edge}): develop {:.1} ms, finish {finish_ms:.1} ms (after decode)",
|
||||
img.width,
|
||||
img.height,
|
||||
develop_ms.as_secs_f64() * 1e3
|
||||
);
|
||||
let mut prev = std::time::Duration::ZERO;
|
||||
let parts: Vec<String> = stages
|
||||
.iter()
|
||||
.map(|(n, t)| {
|
||||
let d = *t - prev;
|
||||
prev = *t;
|
||||
format!("{n} {:.1}", d.as_secs_f64() * 1e3)
|
||||
})
|
||||
.collect();
|
||||
eprintln!("[profile] raw source {}×{} (max {max_edge}, ms after decode): {}", img.width, img.height, parts.join(", "));
|
||||
}
|
||||
let twin = twin.map(|picture| finish(picture).0);
|
||||
let (temp, tint) = xy_to_temp_tint(xy);
|
||||
let relative = crate::camera_preview::file_local_look(raw.format) && t.matrix_is_fallback;
|
||||
let camera_tone = camera_look.as_ref().map(|p| p.tone).or_else(|| raw.color.profile.tone_curve.as_ref().and_then(dng_tone_curve));
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
//! run them off the UI thread.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
// Model/cache types remain available without linking the optional inference crate.
|
||||
#[cfg(not(feature = "denoise"))]
|
||||
extern crate lightcraft_denoise_core as lightcraft_denoise;
|
||||
|
||||
pub mod availability;
|
||||
mod camera_preview;
|
||||
pub mod camera_profiles;
|
||||
|
|
|
|||
|
|
@ -156,10 +156,9 @@ impl Twin {
|
|||
Twin { image, mix: std::sync::Mutex::new(None) }
|
||||
}
|
||||
|
||||
/// Bytes held, the last mix included.
|
||||
/// Bytes reserved for the twin and its lazy mix, before either cache cost can grow.
|
||||
fn bytes(&self) -> usize {
|
||||
let mixed = self.mix.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref().map_or(0, |m| m.1.data.len() * 12);
|
||||
self.image.data.len() * 12 + mixed
|
||||
self.image.data.len().saturating_mul(24)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +206,7 @@ impl DecodedSource {
|
|||
mixed
|
||||
}
|
||||
|
||||
/// Bytes held: the picture, its denoised twin and the last mix.
|
||||
/// Cache cost: the picture, its denoised twin and room for the lazy mix.
|
||||
pub fn bytes(&self) -> usize {
|
||||
self.image.data.len() * 12 + 64 + std::mem::size_of::<SourceInfo>() + self.denoised.as_ref().map_or(0, |t| t.bytes())
|
||||
}
|
||||
|
|
@ -1390,6 +1389,20 @@ mod thumbnail_hash_tests {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn denoise_mix_is_reserved_before_the_source_enters_the_cache() {
|
||||
let mut plain = Rgb32f::new(8, 8);
|
||||
plain.data.fill([1.0; 3]);
|
||||
let mut source = DecodedSource::new(Arc::new(plain), None);
|
||||
source.denoised = Some(Arc::new(Twin::new(Arc::new(Rgb32f::new(8, 8)))));
|
||||
let reserved = source.bytes();
|
||||
assert!(reserved >= 3 * 8 * 8 * 12);
|
||||
assert_eq!(source.image_for(0.5).data[0], [0.5; 3]);
|
||||
assert_eq!(source.bytes(), reserved, "creating a mix cannot evade the LRU's recorded cost");
|
||||
let _ = source.image_for(0.25);
|
||||
assert_eq!(source.bytes(), reserved, "changing Amount replaces the reserved mix");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoder_info_survives_render_jobs_cache_and_eviction() {
|
||||
let tone = lightcraft_pipeline::tone::CameraTone::new(std::array::from_fn(|i| {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ pub enum State {
|
|||
sha256: String,
|
||||
},
|
||||
/// Installed and in use: nothing more to do but tell the user.
|
||||
Installing,
|
||||
Installed,
|
||||
Failed(String),
|
||||
Cancelled,
|
||||
|
|
@ -107,7 +108,7 @@ impl Downloads {
|
|||
job.cancel.store(true, Ordering::Relaxed);
|
||||
match job.get() {
|
||||
// the thread notices, deletes the partial file and marks itself cancelled
|
||||
State::Running { .. } => {}
|
||||
State::Running { .. } | State::Installing => {}
|
||||
State::Done { path, .. } => {
|
||||
let _ = std::fs::remove_file(path);
|
||||
self.jobs.remove(id);
|
||||
|
|
@ -119,11 +120,6 @@ impl Downloads {
|
|||
true
|
||||
}
|
||||
|
||||
/// The staged file at `path` has been installed (its copy is the model now) or is no longer wanted.
|
||||
pub fn staged_file_used(&mut self, path: &Path) {
|
||||
self.jobs.retain(|_, j| !matches!(j.get(), State::Done { path: p, .. } if p == path));
|
||||
}
|
||||
|
||||
/// Downloads that have arrived and been checked, ready to install: (model id, staged file, its SHA-256).
|
||||
pub fn finished(&self) -> Vec<(String, PathBuf, String)> {
|
||||
let mut out: Vec<_> = self
|
||||
|
|
@ -157,7 +153,7 @@ impl Drop for Downloads {
|
|||
for job in self.jobs.values() {
|
||||
job.cancel.store(true, Ordering::Relaxed);
|
||||
}
|
||||
let started = std::time::Instant::now();
|
||||
let started = web_time::Instant::now();
|
||||
while self.running() && started.elapsed() < Duration::from_secs(1) {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
|
@ -336,7 +332,7 @@ mod tests {
|
|||
let mut d = Downloads::default();
|
||||
d.jobs.insert("m".into(), shared.clone());
|
||||
assert!(d.running());
|
||||
let started = std::time::Instant::now();
|
||||
let started = web_time::Instant::now();
|
||||
drop(d);
|
||||
assert!(started.elapsed() < Duration::from_millis(900), "quitting must not wait for a transfer to finish");
|
||||
assert_eq!(shared.get(), State::Cancelled);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,65 @@ fn model_files(dir: &Path, id: &str) -> PathBuf {
|
|||
src.join("model.onnx")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_installation_keeps_the_frame_pump_responsive_and_reuses_the_loaded_model() {
|
||||
use std::sync::{Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
let mut x = setup("background-install", false);
|
||||
let path = model_files(&x.dir, "stand-in");
|
||||
let (entered_tx, entered_rx) = mpsc::channel();
|
||||
let (release_tx, release_rx) = mpsc::channel();
|
||||
let release_rx = Mutex::new(release_rx);
|
||||
let runs = x.runs.clone();
|
||||
let loads = Arc::new(AtomicUsize::new(0));
|
||||
let worker_loads = loads.clone();
|
||||
x.s.denoise.loader = Arc::new(move |_, _| {
|
||||
worker_loads.fetch_add(1, Ordering::Relaxed);
|
||||
entered_tx.send(()).map_err(|e| e.to_string())?;
|
||||
release_rx.lock().unwrap().recv_timeout(Duration::from_secs(3)).map_err(|e| e.to_string())?;
|
||||
Ok(Arc::new(Dim { runs: runs.clone() }))
|
||||
});
|
||||
let info = x.s.execute("denoise.models.inspect", &json!({"path": path})).unwrap();
|
||||
assert_eq!(info["model"]["id"], "stand-in");
|
||||
assert_eq!(loads.load(Ordering::Relaxed), 0, "showing terms never executes the model");
|
||||
let r = x.s.execute("denoise.models.install", &json!({"path": path, "acknowledged": true, "background": true})).unwrap();
|
||||
assert_eq!(r["started"], "local-install");
|
||||
entered_rx.recv_timeout(Duration::from_secs(3)).unwrap();
|
||||
// The loader is blocked: the frame pump must still return, reporting installation in progress.
|
||||
let state = x.s.execute("denoise.models.downloads", &json!({})).unwrap();
|
||||
assert_eq!(state["downloads"][0]["state"], "installing");
|
||||
assert_eq!(x.s.execute("denoise.pump", &json!({"pace": "pause"})).unwrap()["active"], false);
|
||||
assert!(x.s.execute("denoise.models.install", &json!({"path": path, "acknowledged": true, "background": true})).is_err());
|
||||
release_tx.send(()).unwrap();
|
||||
for _ in 0..600 {
|
||||
let v = x.s.execute("denoise.models.downloads", &json!({})).unwrap();
|
||||
if v["downloads"][0]["state"] == "installed" {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
assert_eq!(x.s.execute("denoise.models.downloads", &json!({})).unwrap()["downloads"][0]["state"], "installed");
|
||||
let a = x.s.denoise.active.as_ref().unwrap();
|
||||
assert!(a.model.lock().unwrap().is_some(), "the worker's tested model is reused");
|
||||
assert_eq!(loads.load(Ordering::Relaxed), 1);
|
||||
drop(x.s);
|
||||
let _ = std::fs::remove_dir_all(x.dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_model_replacement_preserves_the_installed_model() {
|
||||
let mut x = setup("replacement", true);
|
||||
let installed = x.dir.join("denoise-models/stand-in/model.onnx");
|
||||
let original = std::fs::read(&installed).unwrap();
|
||||
let source = model_files(&x.dir, "stand-in");
|
||||
std::fs::write(&source, "a different broken model").unwrap();
|
||||
assert!(x.s.execute("denoise.models.install", &json!({"path": source, "acknowledged": true})).is_err());
|
||||
assert_eq!(std::fs::read(installed).unwrap(), original);
|
||||
assert_eq!(x.s.execute("denoise.models.list", &json!({})).unwrap()["model"], "stand-in");
|
||||
drop(x.s);
|
||||
let _ = std::fs::remove_dir_all(x.dir);
|
||||
}
|
||||
|
||||
/// A Bayer DNG that is a smooth ramp, big enough for a 64-cell tile and a second one.
|
||||
fn ramp_dng(w: usize, h: usize, cfa: &str) -> Vec<u8> {
|
||||
let data: Vec<u16> = (0..w * h).map(|i| 600 + ((i % w) * 30 + (i / w) * 35) as u16).collect();
|
||||
|
|
@ -339,7 +398,8 @@ fn a_model_that_fails_fails_the_export_and_is_not_retried_by_the_queue() {
|
|||
let mut x = setup("fails", true);
|
||||
let id = photo(&x.s);
|
||||
x.s.denoise.loader = Arc::new(|_, _| Err("the model went away".into()));
|
||||
// (the model chosen is read afresh: it was loaded by the install's self-test only)
|
||||
// Simulate loss of the in-memory model; successful installation now retains its tested instance.
|
||||
*x.s.denoise.active.as_ref().unwrap().model.lock().unwrap() = None;
|
||||
set_amount(&mut x.s, 80.0);
|
||||
let o = ExportOptions::from_json(&json!({"format": "png", "width": 100, "height": 100}));
|
||||
let e = export_photo(&mut x.s, id, &o, 1).err().unwrap();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ description = "LightCraft GPU develop pipeline: wgpu compute (WGSL) twin of the
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
lightcraft-denoise = { workspace = true }
|
||||
lightcraft-denoise = { workspace = true, optional = true }
|
||||
lightcraft-geom = { workspace = true }
|
||||
lightcraft-color = { workspace = true }
|
||||
lightcraft-raster = { workspace = true }
|
||||
|
|
@ -24,5 +24,9 @@ wgpu = { workspace = true }
|
|||
pollster = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
denoise = ["dep:lightcraft-denoise", "lightcraft-denoise/runtime"]
|
||||
|
||||
[dev-dependencies]
|
||||
lightcraft-scenes = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ use lightcraft_raster::Rgb32f;
|
|||
pub mod backend;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod ctx;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(all(feature = "denoise", not(target_arch = "wasm32")))]
|
||||
pub mod nn;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod params;
|
||||
|
|
@ -149,7 +149,7 @@ pub fn enabled() -> bool {
|
|||
|
||||
/// Why GPU compute that has a device of its own (denoise) is switched off by the user's settings, or `None`: the
|
||||
/// environment switches and the rendering preference, but not a render-device failure (the devices are separate).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(all(feature = "denoise", not(target_arch = "wasm32")))]
|
||||
pub(crate) fn switched_off() -> Option<String> {
|
||||
if env_disabled() {
|
||||
return Some(if backend::env_off() { "disabled by LIGHTCRAFT_GPU_BACKEND=off" } else { "disabled by LIGHTCRAFT_GPU=0" }.into());
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ serde_json = { workspace = true }
|
|||
log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
lightcraft-denoise = { workspace = true, features = ["runtime"] }
|
||||
lightcraft-codecs = { workspace = true }
|
||||
lightcraft-denoise = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -187,5 +187,24 @@
|
|||
"Moved; {} photo(s) relinked": "Verschoben; {} Fotos neu verknüpft",
|
||||
"{} label (click more labels to show any of them)": "Markierung {} (weitere Markierungen auswählen, um alle passenden Fotos zu zeigen)",
|
||||
", {} star{}": ", Sterne: {}{:.0}",
|
||||
", {} label": ", Markierung: {}"
|
||||
", {} label": ", Markierung: {}",
|
||||
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.",
|
||||
"{:.1} MB": "{:.1} MB",
|
||||
"Running on the processor: {}.": "Running on the processor: {}.",
|
||||
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.",
|
||||
"Denoising {name}: {done} of {total} tiles{}": "Denoising {name}: {done} of {total} tiles{}",
|
||||
"{size} GB": "{size} GB",
|
||||
"{} of {} from {host}": "{} of {} from {host}",
|
||||
"{} KB": "{} KB",
|
||||
"{} MB": "{} MB",
|
||||
"Making the denoised picture… {}%": "Making the denoised picture… {}%",
|
||||
"{name} · {terms}": "{name} · {terms}",
|
||||
"{n} {noun}": "{n} {noun}",
|
||||
"Trained on: {provenance}": "Trained on: {provenance}",
|
||||
" · {queued} waiting": " · {queued} waiting",
|
||||
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.",
|
||||
"In line for AI Denoise ({ahead} ahead)…": "In line for AI Denoise ({ahead} ahead)…",
|
||||
"Running on {}.": "Running on {}.",
|
||||
"{file} · {size}": "{file} · {size}",
|
||||
"• {assumption}": "• {assumption}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1797,5 +1797,94 @@
|
|||
"How to install the model": "So installierst du das Modell",
|
||||
"Install the SAM 3 Model": "SAM-3-Modell installieren",
|
||||
"This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "Diese Version kann das Modell noch nicht herunterladen. Zum Installieren von Hand lege model.safetensors, vocab.json und merges.txt aus Metas facebook/sam3 in den obigen Ordner: Objekt und Beschreiben funktionieren, sobald sie dort liegen.",
|
||||
"Open Log Folder": "Protokollordner öffnen"
|
||||
"Open Log Folder": "Protokollordner öffnen",
|
||||
"AI Denoise": "AI Denoise",
|
||||
"AI Denoise Model": "AI Denoise Model",
|
||||
"AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.",
|
||||
"AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.",
|
||||
"Accept & Download": "Accept & Download",
|
||||
"Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.",
|
||||
"Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.",
|
||||
"Cache": "Cache",
|
||||
"Checking and installing the model…": "Checking and installing the model…",
|
||||
"Checking…": "Checking…",
|
||||
"Clear cache": "Clear cache",
|
||||
"Denoise Models": "Denoise Models",
|
||||
"Denoise Settings": "Denoise Settings",
|
||||
"Denoise all photos with an Amount": "Denoise all photos with an Amount",
|
||||
"Downloaded and checked. Installing…": "Downloaded and checked. Installing…",
|
||||
"Failed its last test": "Failed its last test",
|
||||
"Graphics card": "Graphics card",
|
||||
"I have read these terms and accept them for my own use": "I have read these terms and accept them for my own use",
|
||||
"Inspect Denoise Model": "Inspect Denoise Model",
|
||||
"Install Denoise Model": "Install Denoise Model",
|
||||
"Install from file…": "Install from file…",
|
||||
"Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.",
|
||||
"Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft does not know this model, so it assumed:",
|
||||
"Limit": "Limit",
|
||||
"Make it now": "Make it now",
|
||||
"Make the denoised picture of the photos I am looking at": "Make the denoised picture of the photos I am looking at",
|
||||
"Next up for AI Denoise…": "Next up for AI Denoise…",
|
||||
"Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.",
|
||||
"Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.",
|
||||
"Open page": "Open page",
|
||||
"Open the model's page": "Open the model's page",
|
||||
"Opens the model's own page in your browser, to read about it or get the file yourself.": "Opens the model's own page in your browser, to read about it or get the file yourself.",
|
||||
"Processor": "Processor",
|
||||
"Set up AI Denoise…": "Set up AI Denoise…",
|
||||
"Speed": "Speed",
|
||||
"Starting…": "Starting…",
|
||||
"Tested and works on this computer": "Tested and works on this computer",
|
||||
"The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works",
|
||||
"The graphics card is set up when the next photo is made.": "The graphics card is set up when the next photo is made.",
|
||||
"The model is kept on this computer only. LightCraft never uploads or shares it.": "The model is kept on this computer only. LightCraft never uploads or shares it.",
|
||||
"The processor does the work.": "The processor does the work.",
|
||||
"There is no model file to install": "There is no model file to install",
|
||||
"This build cannot run denoise models.": "This build cannot run denoise models.",
|
||||
"This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)",
|
||||
"This file cannot be used yet": "This file cannot be used yet",
|
||||
"Tick the box to accept the model's terms first": "Tick the box to accept the model's terms first",
|
||||
"Try again": "Try again",
|
||||
"Try the graphics card again": "Try the graphics card again",
|
||||
"Turn off": "Turn off",
|
||||
"Unknown licence": "Unknown licence",
|
||||
"Use": "Use",
|
||||
"Waiting to start…": "Waiting to start…",
|
||||
"Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.",
|
||||
"cached picture": "cached picture",
|
||||
"cached pictures": "cached pictures",
|
||||
"commercial use allowed": "commercial use allowed",
|
||||
"denoiseModel.accept": "denoiseModel.accept",
|
||||
"in use": "in use",
|
||||
"installed": "installed",
|
||||
"makes AI Denoise available": "makes AI Denoise available",
|
||||
"non-commercial use only": "non-commercial use only",
|
||||
"photo has its denoised picture.": "photo has its denoised picture.",
|
||||
"photo waiting": "photo waiting",
|
||||
"photos have their denoised picture.": "photos have their denoised picture.",
|
||||
"photos waiting": "photos waiting",
|
||||
"starts using it for AI Denoise": "starts using it for AI Denoise",
|
||||
"terms unclear": "terms unclear",
|
||||
"the graphics card": "the graphics card",
|
||||
"the graphics card is not used": "the graphics card is not used",
|
||||
"Cancel Denoise": "Cancel Denoise",
|
||||
"Cancel Denoise Model Download": "Cancel Denoise Model Download",
|
||||
"Choose Denoise Model": "Choose Denoise Model",
|
||||
"Clear Denoise Cache": "Clear Denoise Cache",
|
||||
"Denoise Model Downloads": "Denoise Model Downloads",
|
||||
"Denoise Photos": "Denoise Photos",
|
||||
"Denoise Pump": "Denoise Pump",
|
||||
"Denoise Status": "Denoise Status",
|
||||
"Download Denoise Model": "Download Denoise Model",
|
||||
"Remove Denoise Model": "Remove Denoise Model",
|
||||
"Test Denoise Model": "Test Denoise Model",
|
||||
"AI Denoise is enabled on the requesting photo": "AI Denoise is enabled on the requesting photo",
|
||||
"After installation, AI Denoise will turn on for the requesting photo.": "After installation, AI Denoise will turn on for the requesting photo.",
|
||||
"Cancel pending action": "Cancel pending action",
|
||||
"Install a denoise model to enable AI Denoise on this photo": "Install a denoise model to enable AI Denoise on this photo",
|
||||
"Pending photo action cancelled": "Pending photo action cancelled",
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,5 +187,24 @@
|
|||
"Moved; {} photo(s) relinked": "Movida; {} foto(s) vinculada(s) de nuevo",
|
||||
"{} label (click more labels to show any of them)": "Etiqueta {} (hacer clic en más etiquetas para mostrar cualquiera de ellas)",
|
||||
", {} star{}": ", {} estrella{}",
|
||||
", {} label": ", etiqueta {}"
|
||||
", {} label": ", etiqueta {}",
|
||||
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.",
|
||||
"{:.1} MB": "{:.1} MB",
|
||||
"Running on the processor: {}.": "Running on the processor: {}.",
|
||||
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.",
|
||||
"Denoising {name}: {done} of {total} tiles{}": "Denoising {name}: {done} of {total} tiles{}",
|
||||
"{size} GB": "{size} GB",
|
||||
"{} of {} from {host}": "{} of {} from {host}",
|
||||
"{} KB": "{} KB",
|
||||
"{} MB": "{} MB",
|
||||
"Making the denoised picture… {}%": "Making the denoised picture… {}%",
|
||||
"{name} · {terms}": "{name} · {terms}",
|
||||
"{n} {noun}": "{n} {noun}",
|
||||
"Trained on: {provenance}": "Trained on: {provenance}",
|
||||
" · {queued} waiting": " · {queued} waiting",
|
||||
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.",
|
||||
"In line for AI Denoise ({ahead} ahead)…": "In line for AI Denoise ({ahead} ahead)…",
|
||||
"Running on {}.": "Running on {}.",
|
||||
"{file} · {size}": "{file} · {size}",
|
||||
"• {assumption}": "• {assumption}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1642,5 +1642,94 @@
|
|||
"Captured from": "Capturadas desde",
|
||||
"Captured until": "Capturadas hasta",
|
||||
"Only": "Solo",
|
||||
"Open Log Folder": "Abrir carpeta de registros"
|
||||
"Open Log Folder": "Abrir carpeta de registros",
|
||||
"AI Denoise": "AI Denoise",
|
||||
"AI Denoise Model": "AI Denoise Model",
|
||||
"AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.",
|
||||
"AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.",
|
||||
"Accept & Download": "Accept & Download",
|
||||
"Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.",
|
||||
"Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.",
|
||||
"Cache": "Cache",
|
||||
"Checking and installing the model…": "Checking and installing the model…",
|
||||
"Checking…": "Checking…",
|
||||
"Clear cache": "Clear cache",
|
||||
"Denoise Models": "Denoise Models",
|
||||
"Denoise Settings": "Denoise Settings",
|
||||
"Denoise all photos with an Amount": "Denoise all photos with an Amount",
|
||||
"Downloaded and checked. Installing…": "Downloaded and checked. Installing…",
|
||||
"Failed its last test": "Failed its last test",
|
||||
"Graphics card": "Graphics card",
|
||||
"I have read these terms and accept them for my own use": "I have read these terms and accept them for my own use",
|
||||
"Inspect Denoise Model": "Inspect Denoise Model",
|
||||
"Install Denoise Model": "Install Denoise Model",
|
||||
"Install from file…": "Install from file…",
|
||||
"Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.",
|
||||
"Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft does not know this model, so it assumed:",
|
||||
"Limit": "Limit",
|
||||
"Make it now": "Make it now",
|
||||
"Make the denoised picture of the photos I am looking at": "Make the denoised picture of the photos I am looking at",
|
||||
"Next up for AI Denoise…": "Next up for AI Denoise…",
|
||||
"Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.",
|
||||
"Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.",
|
||||
"Open page": "Open page",
|
||||
"Open the model's page": "Open the model's page",
|
||||
"Opens the model's own page in your browser, to read about it or get the file yourself.": "Opens the model's own page in your browser, to read about it or get the file yourself.",
|
||||
"Processor": "Processor",
|
||||
"Set up AI Denoise…": "Set up AI Denoise…",
|
||||
"Speed": "Speed",
|
||||
"Starting…": "Starting…",
|
||||
"Tested and works on this computer": "Tested and works on this computer",
|
||||
"The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works",
|
||||
"The graphics card is set up when the next photo is made.": "The graphics card is set up when the next photo is made.",
|
||||
"The model is kept on this computer only. LightCraft never uploads or shares it.": "The model is kept on this computer only. LightCraft never uploads or shares it.",
|
||||
"The processor does the work.": "The processor does the work.",
|
||||
"There is no model file to install": "There is no model file to install",
|
||||
"This build cannot run denoise models.": "This build cannot run denoise models.",
|
||||
"This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)",
|
||||
"This file cannot be used yet": "This file cannot be used yet",
|
||||
"Tick the box to accept the model's terms first": "Tick the box to accept the model's terms first",
|
||||
"Try again": "Try again",
|
||||
"Try the graphics card again": "Try the graphics card again",
|
||||
"Turn off": "Turn off",
|
||||
"Unknown licence": "Unknown licence",
|
||||
"Use": "Use",
|
||||
"Waiting to start…": "Waiting to start…",
|
||||
"Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.",
|
||||
"cached picture": "cached picture",
|
||||
"cached pictures": "cached pictures",
|
||||
"commercial use allowed": "commercial use allowed",
|
||||
"denoiseModel.accept": "denoiseModel.accept",
|
||||
"in use": "in use",
|
||||
"installed": "installed",
|
||||
"makes AI Denoise available": "makes AI Denoise available",
|
||||
"non-commercial use only": "non-commercial use only",
|
||||
"photo has its denoised picture.": "photo has its denoised picture.",
|
||||
"photo waiting": "photo waiting",
|
||||
"photos have their denoised picture.": "photos have their denoised picture.",
|
||||
"photos waiting": "photos waiting",
|
||||
"starts using it for AI Denoise": "starts using it for AI Denoise",
|
||||
"terms unclear": "terms unclear",
|
||||
"the graphics card": "the graphics card",
|
||||
"the graphics card is not used": "the graphics card is not used",
|
||||
"Cancel Denoise": "Cancel Denoise",
|
||||
"Cancel Denoise Model Download": "Cancel Denoise Model Download",
|
||||
"Choose Denoise Model": "Choose Denoise Model",
|
||||
"Clear Denoise Cache": "Clear Denoise Cache",
|
||||
"Denoise Model Downloads": "Denoise Model Downloads",
|
||||
"Denoise Photos": "Denoise Photos",
|
||||
"Denoise Pump": "Denoise Pump",
|
||||
"Denoise Status": "Denoise Status",
|
||||
"Download Denoise Model": "Download Denoise Model",
|
||||
"Remove Denoise Model": "Remove Denoise Model",
|
||||
"Test Denoise Model": "Test Denoise Model",
|
||||
"AI Denoise is enabled on the requesting photo": "AI Denoise is enabled on the requesting photo",
|
||||
"After installation, AI Denoise will turn on for the requesting photo.": "After installation, AI Denoise will turn on for the requesting photo.",
|
||||
"Cancel pending action": "Cancel pending action",
|
||||
"Install a denoise model to enable AI Denoise on this photo": "Install a denoise model to enable AI Denoise on this photo",
|
||||
"Pending photo action cancelled": "Pending photo action cancelled",
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,5 +187,24 @@
|
|||
"Moved; {} photo(s) relinked": "移動しました。{}枚の写真の参照先を更新しました",
|
||||
"{} label (click more labels to show any of them)": "{}ラベル(ほかのラベルもクリックすると、いずれかに一致する写真を表示)",
|
||||
", {} star{}": "、星{}つ{:.0}",
|
||||
", {} label": "、{}ラベル"
|
||||
", {} label": "、{}ラベル",
|
||||
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.",
|
||||
"{:.1} MB": "{:.1} MB",
|
||||
"Running on the processor: {}.": "Running on the processor: {}.",
|
||||
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.",
|
||||
"Denoising {name}: {done} of {total} tiles{}": "Denoising {name}: {done} of {total} tiles{}",
|
||||
"{size} GB": "{size} GB",
|
||||
"{} of {} from {host}": "{} of {} from {host}",
|
||||
"{} KB": "{} KB",
|
||||
"{} MB": "{} MB",
|
||||
"Making the denoised picture… {}%": "Making the denoised picture… {}%",
|
||||
"{name} · {terms}": "{name} · {terms}",
|
||||
"{n} {noun}": "{n} {noun}",
|
||||
"Trained on: {provenance}": "Trained on: {provenance}",
|
||||
" · {queued} waiting": " · {queued} waiting",
|
||||
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.",
|
||||
"In line for AI Denoise ({ahead} ahead)…": "In line for AI Denoise ({ahead} ahead)…",
|
||||
"Running on {}.": "Running on {}.",
|
||||
"{file} · {size}": "{file} · {size}",
|
||||
"• {assumption}": "• {assumption}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1642,5 +1642,94 @@
|
|||
"How to install the model": "モデルのインストール方法",
|
||||
"Install the SAM 3 Model": "SAM 3モデルのインストール",
|
||||
"This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "このビルドではまだモデルをダウンロードできません。手動でインストールするには、Meta の facebook/sam3 の model.safetensors、vocab.json、merges.txt を上のフォルダに置いてください。置いた時点で「オブジェクト」と「説明で選択」が使えるようになります。",
|
||||
"Open Log Folder": "ログフォルダーを開く"
|
||||
"Open Log Folder": "ログフォルダーを開く",
|
||||
"AI Denoise": "AI Denoise",
|
||||
"AI Denoise Model": "AI Denoise Model",
|
||||
"AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.",
|
||||
"AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.",
|
||||
"Accept & Download": "Accept & Download",
|
||||
"Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.",
|
||||
"Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.",
|
||||
"Cache": "Cache",
|
||||
"Checking and installing the model…": "Checking and installing the model…",
|
||||
"Checking…": "Checking…",
|
||||
"Clear cache": "Clear cache",
|
||||
"Denoise Models": "Denoise Models",
|
||||
"Denoise Settings": "Denoise Settings",
|
||||
"Denoise all photos with an Amount": "Denoise all photos with an Amount",
|
||||
"Downloaded and checked. Installing…": "Downloaded and checked. Installing…",
|
||||
"Failed its last test": "Failed its last test",
|
||||
"Graphics card": "Graphics card",
|
||||
"I have read these terms and accept them for my own use": "I have read these terms and accept them for my own use",
|
||||
"Inspect Denoise Model": "Inspect Denoise Model",
|
||||
"Install Denoise Model": "Install Denoise Model",
|
||||
"Install from file…": "Install from file…",
|
||||
"Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.",
|
||||
"Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft does not know this model, so it assumed:",
|
||||
"Limit": "Limit",
|
||||
"Make it now": "Make it now",
|
||||
"Make the denoised picture of the photos I am looking at": "Make the denoised picture of the photos I am looking at",
|
||||
"Next up for AI Denoise…": "Next up for AI Denoise…",
|
||||
"Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.",
|
||||
"Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.",
|
||||
"Open page": "Open page",
|
||||
"Open the model's page": "Open the model's page",
|
||||
"Opens the model's own page in your browser, to read about it or get the file yourself.": "Opens the model's own page in your browser, to read about it or get the file yourself.",
|
||||
"Processor": "Processor",
|
||||
"Set up AI Denoise…": "Set up AI Denoise…",
|
||||
"Speed": "Speed",
|
||||
"Starting…": "Starting…",
|
||||
"Tested and works on this computer": "Tested and works on this computer",
|
||||
"The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works",
|
||||
"The graphics card is set up when the next photo is made.": "The graphics card is set up when the next photo is made.",
|
||||
"The model is kept on this computer only. LightCraft never uploads or shares it.": "The model is kept on this computer only. LightCraft never uploads or shares it.",
|
||||
"The processor does the work.": "The processor does the work.",
|
||||
"There is no model file to install": "There is no model file to install",
|
||||
"This build cannot run denoise models.": "This build cannot run denoise models.",
|
||||
"This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)",
|
||||
"This file cannot be used yet": "This file cannot be used yet",
|
||||
"Tick the box to accept the model's terms first": "Tick the box to accept the model's terms first",
|
||||
"Try again": "Try again",
|
||||
"Try the graphics card again": "Try the graphics card again",
|
||||
"Turn off": "Turn off",
|
||||
"Unknown licence": "Unknown licence",
|
||||
"Use": "Use",
|
||||
"Waiting to start…": "Waiting to start…",
|
||||
"Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.",
|
||||
"cached picture": "cached picture",
|
||||
"cached pictures": "cached pictures",
|
||||
"commercial use allowed": "commercial use allowed",
|
||||
"denoiseModel.accept": "denoiseModel.accept",
|
||||
"in use": "in use",
|
||||
"installed": "installed",
|
||||
"makes AI Denoise available": "makes AI Denoise available",
|
||||
"non-commercial use only": "non-commercial use only",
|
||||
"photo has its denoised picture.": "photo has its denoised picture.",
|
||||
"photo waiting": "photo waiting",
|
||||
"photos have their denoised picture.": "photos have their denoised picture.",
|
||||
"photos waiting": "photos waiting",
|
||||
"starts using it for AI Denoise": "starts using it for AI Denoise",
|
||||
"terms unclear": "terms unclear",
|
||||
"the graphics card": "the graphics card",
|
||||
"the graphics card is not used": "the graphics card is not used",
|
||||
"Cancel Denoise": "Cancel Denoise",
|
||||
"Cancel Denoise Model Download": "Cancel Denoise Model Download",
|
||||
"Choose Denoise Model": "Choose Denoise Model",
|
||||
"Clear Denoise Cache": "Clear Denoise Cache",
|
||||
"Denoise Model Downloads": "Denoise Model Downloads",
|
||||
"Denoise Photos": "Denoise Photos",
|
||||
"Denoise Pump": "Denoise Pump",
|
||||
"Denoise Status": "Denoise Status",
|
||||
"Download Denoise Model": "Download Denoise Model",
|
||||
"Remove Denoise Model": "Remove Denoise Model",
|
||||
"Test Denoise Model": "Test Denoise Model",
|
||||
"AI Denoise is enabled on the requesting photo": "AI Denoise is enabled on the requesting photo",
|
||||
"After installation, AI Denoise will turn on for the requesting photo.": "After installation, AI Denoise will turn on for the requesting photo.",
|
||||
"Cancel pending action": "Cancel pending action",
|
||||
"Install a denoise model to enable AI Denoise on this photo": "Install a denoise model to enable AI Denoise on this photo",
|
||||
"Pending photo action cancelled": "Pending photo action cancelled",
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,5 +187,24 @@
|
|||
"Moved; {} photo(s) relinked": "Movida; {} foto(s) vinculada(s) novamente",
|
||||
"{} label (click more labels to show any of them)": "Rótulo {} (clique em mais rótulos para mostrar qualquer um deles)",
|
||||
", {} star{}": ", {} estrela{}",
|
||||
", {} label": ", rótulo {}"
|
||||
", {} label": ", rótulo {}",
|
||||
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.",
|
||||
"{:.1} MB": "{:.1} MB",
|
||||
"Running on the processor: {}.": "Running on the processor: {}.",
|
||||
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.",
|
||||
"Denoising {name}: {done} of {total} tiles{}": "Denoising {name}: {done} of {total} tiles{}",
|
||||
"{size} GB": "{size} GB",
|
||||
"{} of {} from {host}": "{} of {} from {host}",
|
||||
"{} KB": "{} KB",
|
||||
"{} MB": "{} MB",
|
||||
"Making the denoised picture… {}%": "Making the denoised picture… {}%",
|
||||
"{name} · {terms}": "{name} · {terms}",
|
||||
"{n} {noun}": "{n} {noun}",
|
||||
"Trained on: {provenance}": "Trained on: {provenance}",
|
||||
" · {queued} waiting": " · {queued} waiting",
|
||||
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.",
|
||||
"In line for AI Denoise ({ahead} ahead)…": "In line for AI Denoise ({ahead} ahead)…",
|
||||
"Running on {}.": "Running on {}.",
|
||||
"{file} · {size}": "{file} · {size}",
|
||||
"• {assumption}": "• {assumption}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1642,5 +1642,94 @@
|
|||
"≤ at most": "≤ no máximo",
|
||||
"≥ at least": "≥ no mínimo",
|
||||
"日本語": "日本語",
|
||||
"繁體中文(台灣)": "繁體中文(台灣)"
|
||||
"繁體中文(台灣)": "繁體中文(台灣)",
|
||||
"AI Denoise": "AI Denoise",
|
||||
"AI Denoise Model": "AI Denoise Model",
|
||||
"AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.",
|
||||
"AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.",
|
||||
"Accept & Download": "Accept & Download",
|
||||
"Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.",
|
||||
"Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.",
|
||||
"Cache": "Cache",
|
||||
"Checking and installing the model…": "Checking and installing the model…",
|
||||
"Checking…": "Checking…",
|
||||
"Clear cache": "Clear cache",
|
||||
"Denoise Models": "Denoise Models",
|
||||
"Denoise Settings": "Denoise Settings",
|
||||
"Denoise all photos with an Amount": "Denoise all photos with an Amount",
|
||||
"Downloaded and checked. Installing…": "Downloaded and checked. Installing…",
|
||||
"Failed its last test": "Failed its last test",
|
||||
"Graphics card": "Graphics card",
|
||||
"I have read these terms and accept them for my own use": "I have read these terms and accept them for my own use",
|
||||
"Inspect Denoise Model": "Inspect Denoise Model",
|
||||
"Install Denoise Model": "Install Denoise Model",
|
||||
"Install from file…": "Install from file…",
|
||||
"Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.",
|
||||
"Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft does not know this model, so it assumed:",
|
||||
"Limit": "Limit",
|
||||
"Make it now": "Make it now",
|
||||
"Make the denoised picture of the photos I am looking at": "Make the denoised picture of the photos I am looking at",
|
||||
"Next up for AI Denoise…": "Next up for AI Denoise…",
|
||||
"Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.",
|
||||
"Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.",
|
||||
"Open page": "Open page",
|
||||
"Open the model's page": "Open the model's page",
|
||||
"Opens the model's own page in your browser, to read about it or get the file yourself.": "Opens the model's own page in your browser, to read about it or get the file yourself.",
|
||||
"Processor": "Processor",
|
||||
"Set up AI Denoise…": "Set up AI Denoise…",
|
||||
"Speed": "Speed",
|
||||
"Starting…": "Starting…",
|
||||
"Tested and works on this computer": "Tested and works on this computer",
|
||||
"The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works",
|
||||
"The graphics card is set up when the next photo is made.": "The graphics card is set up when the next photo is made.",
|
||||
"The model is kept on this computer only. LightCraft never uploads or shares it.": "The model is kept on this computer only. LightCraft never uploads or shares it.",
|
||||
"The processor does the work.": "The processor does the work.",
|
||||
"There is no model file to install": "There is no model file to install",
|
||||
"This build cannot run denoise models.": "This build cannot run denoise models.",
|
||||
"This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)",
|
||||
"This file cannot be used yet": "This file cannot be used yet",
|
||||
"Tick the box to accept the model's terms first": "Tick the box to accept the model's terms first",
|
||||
"Try again": "Try again",
|
||||
"Try the graphics card again": "Try the graphics card again",
|
||||
"Turn off": "Turn off",
|
||||
"Unknown licence": "Unknown licence",
|
||||
"Use": "Use",
|
||||
"Waiting to start…": "Waiting to start…",
|
||||
"Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.",
|
||||
"cached picture": "cached picture",
|
||||
"cached pictures": "cached pictures",
|
||||
"commercial use allowed": "commercial use allowed",
|
||||
"denoiseModel.accept": "denoiseModel.accept",
|
||||
"in use": "in use",
|
||||
"installed": "installed",
|
||||
"makes AI Denoise available": "makes AI Denoise available",
|
||||
"non-commercial use only": "non-commercial use only",
|
||||
"photo has its denoised picture.": "photo has its denoised picture.",
|
||||
"photo waiting": "photo waiting",
|
||||
"photos have their denoised picture.": "photos have their denoised picture.",
|
||||
"photos waiting": "photos waiting",
|
||||
"starts using it for AI Denoise": "starts using it for AI Denoise",
|
||||
"terms unclear": "terms unclear",
|
||||
"the graphics card": "the graphics card",
|
||||
"the graphics card is not used": "the graphics card is not used",
|
||||
"Cancel Denoise": "Cancel Denoise",
|
||||
"Cancel Denoise Model Download": "Cancel Denoise Model Download",
|
||||
"Choose Denoise Model": "Choose Denoise Model",
|
||||
"Clear Denoise Cache": "Clear Denoise Cache",
|
||||
"Denoise Model Downloads": "Denoise Model Downloads",
|
||||
"Denoise Photos": "Denoise Photos",
|
||||
"Denoise Pump": "Denoise Pump",
|
||||
"Denoise Status": "Denoise Status",
|
||||
"Download Denoise Model": "Download Denoise Model",
|
||||
"Remove Denoise Model": "Remove Denoise Model",
|
||||
"Test Denoise Model": "Test Denoise Model",
|
||||
"AI Denoise is enabled on the requesting photo": "AI Denoise is enabled on the requesting photo",
|
||||
"After installation, AI Denoise will turn on for the requesting photo.": "After installation, AI Denoise will turn on for the requesting photo.",
|
||||
"Cancel pending action": "Cancel pending action",
|
||||
"Install a denoise model to enable AI Denoise on this photo": "Install a denoise model to enable AI Denoise on this photo",
|
||||
"Pending photo action cancelled": "Pending photo action cancelled",
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,5 +187,24 @@
|
|||
"{} — show their photos": "{} — показать фото этого человека",
|
||||
"· {dups} already in the library": "· уже в библиотеке: {dups}",
|
||||
"· {n} photo{}": "· фото: {n}{:.0}",
|
||||
"… and {more} more": "… и ещё {more}"
|
||||
"… and {more} more": "… и ещё {more}",
|
||||
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.",
|
||||
"{:.1} MB": "{:.1} MB",
|
||||
"Running on the processor: {}.": "Running on the processor: {}.",
|
||||
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.",
|
||||
"Denoising {name}: {done} of {total} tiles{}": "Denoising {name}: {done} of {total} tiles{}",
|
||||
"{size} GB": "{size} GB",
|
||||
"{} of {} from {host}": "{} of {} from {host}",
|
||||
"{} KB": "{} KB",
|
||||
"{} MB": "{} MB",
|
||||
"Making the denoised picture… {}%": "Making the denoised picture… {}%",
|
||||
"{name} · {terms}": "{name} · {terms}",
|
||||
"{n} {noun}": "{n} {noun}",
|
||||
"Trained on: {provenance}": "Trained on: {provenance}",
|
||||
" · {queued} waiting": " · {queued} waiting",
|
||||
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.",
|
||||
"In line for AI Denoise ({ahead} ahead)…": "In line for AI Denoise ({ahead} ahead)…",
|
||||
"Running on {}.": "Running on {}.",
|
||||
"{file} · {size}": "{file} · {size}",
|
||||
"• {assumption}": "• {assumption}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1312,5 +1312,95 @@
|
|||
"of the following rules:": "из следующих условий:",
|
||||
"saved in": "сохранено в",
|
||||
"this photo": "это фото",
|
||||
"− harder border · + softer border": "− жёстче край · + мягче край"
|
||||
"− harder border · + softer border": "− жёстче край · + мягче край",
|
||||
"AI Denoise": "AI Denoise",
|
||||
"AI Denoise Model": "AI Denoise Model",
|
||||
"AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.",
|
||||
"AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.",
|
||||
"Accept & Download": "Accept & Download",
|
||||
"Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.",
|
||||
"Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.",
|
||||
"Cache": "Cache",
|
||||
"Checking and installing the model…": "Checking and installing the model…",
|
||||
"Checking…": "Checking…",
|
||||
"Clear cache": "Clear cache",
|
||||
"Denoise Models": "Denoise Models",
|
||||
"Denoise Settings": "Denoise Settings",
|
||||
"Denoise all photos with an Amount": "Denoise all photos with an Amount",
|
||||
"Downloaded and checked. Installing…": "Downloaded and checked. Installing…",
|
||||
"Failed its last test": "Failed its last test",
|
||||
"Graphics card": "Graphics card",
|
||||
"I have read these terms and accept them for my own use": "I have read these terms and accept them for my own use",
|
||||
"Inspect Denoise Model": "Inspect Denoise Model",
|
||||
"Install Denoise Model": "Install Denoise Model",
|
||||
"Install from file…": "Install from file…",
|
||||
"Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.",
|
||||
"Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft does not know this model, so it assumed:",
|
||||
"Limit": "Limit",
|
||||
"Make it now": "Make it now",
|
||||
"Make the denoised picture of the photos I am looking at": "Make the denoised picture of the photos I am looking at",
|
||||
"Models": "Models",
|
||||
"Next up for AI Denoise…": "Next up for AI Denoise…",
|
||||
"Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.",
|
||||
"Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.",
|
||||
"Open page": "Open page",
|
||||
"Open the model's page": "Open the model's page",
|
||||
"Opens the model's own page in your browser, to read about it or get the file yourself.": "Opens the model's own page in your browser, to read about it or get the file yourself.",
|
||||
"Processor": "Processor",
|
||||
"Set up AI Denoise…": "Set up AI Denoise…",
|
||||
"Speed": "Speed",
|
||||
"Starting…": "Starting…",
|
||||
"Tested and works on this computer": "Tested and works on this computer",
|
||||
"The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works",
|
||||
"The graphics card is set up when the next photo is made.": "The graphics card is set up when the next photo is made.",
|
||||
"The model is kept on this computer only. LightCraft never uploads or shares it.": "The model is kept on this computer only. LightCraft never uploads or shares it.",
|
||||
"The processor does the work.": "The processor does the work.",
|
||||
"There is no model file to install": "There is no model file to install",
|
||||
"This build cannot run denoise models.": "This build cannot run denoise models.",
|
||||
"This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)",
|
||||
"This file cannot be used yet": "This file cannot be used yet",
|
||||
"Tick the box to accept the model's terms first": "Tick the box to accept the model's terms first",
|
||||
"Try again": "Try again",
|
||||
"Try the graphics card again": "Try the graphics card again",
|
||||
"Turn off": "Turn off",
|
||||
"Unknown licence": "Unknown licence",
|
||||
"Use": "Use",
|
||||
"Waiting to start…": "Waiting to start…",
|
||||
"Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.",
|
||||
"cached picture": "cached picture",
|
||||
"cached pictures": "cached pictures",
|
||||
"commercial use allowed": "commercial use allowed",
|
||||
"denoiseModel.accept": "denoiseModel.accept",
|
||||
"in use": "in use",
|
||||
"installed": "installed",
|
||||
"makes AI Denoise available": "makes AI Denoise available",
|
||||
"non-commercial use only": "non-commercial use only",
|
||||
"photo has its denoised picture.": "photo has its denoised picture.",
|
||||
"photo waiting": "photo waiting",
|
||||
"photos have their denoised picture.": "photos have their denoised picture.",
|
||||
"photos waiting": "photos waiting",
|
||||
"starts using it for AI Denoise": "starts using it for AI Denoise",
|
||||
"terms unclear": "terms unclear",
|
||||
"the graphics card": "the graphics card",
|
||||
"the graphics card is not used": "the graphics card is not used",
|
||||
"Cancel Denoise": "Cancel Denoise",
|
||||
"Cancel Denoise Model Download": "Cancel Denoise Model Download",
|
||||
"Choose Denoise Model": "Choose Denoise Model",
|
||||
"Clear Denoise Cache": "Clear Denoise Cache",
|
||||
"Denoise Model Downloads": "Denoise Model Downloads",
|
||||
"Denoise Photos": "Denoise Photos",
|
||||
"Denoise Pump": "Denoise Pump",
|
||||
"Denoise Status": "Denoise Status",
|
||||
"Download Denoise Model": "Download Denoise Model",
|
||||
"Remove Denoise Model": "Remove Denoise Model",
|
||||
"Test Denoise Model": "Test Denoise Model",
|
||||
"AI Denoise is enabled on the requesting photo": "AI Denoise is enabled on the requesting photo",
|
||||
"After installation, AI Denoise will turn on for the requesting photo.": "After installation, AI Denoise will turn on for the requesting photo.",
|
||||
"Cancel pending action": "Cancel pending action",
|
||||
"Install a denoise model to enable AI Denoise on this photo": "Install a denoise model to enable AI Denoise on this photo",
|
||||
"Pending photo action cancelled": "Pending photo action cancelled",
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,5 +187,24 @@
|
|||
"Moved; {} photo(s) relinked": "已移动;已重新链接 {0} 张照片",
|
||||
"{} label (click more labels to show any of them)": "{0}标签(单击更多标签可显示符合其中任一标签的照片)",
|
||||
", {} star{}": ",{0} 星{1:.0}",
|
||||
", {} label": ",{0}标签"
|
||||
", {} label": ",{0}标签",
|
||||
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.",
|
||||
"{:.1} MB": "{:.1} MB",
|
||||
"Running on the processor: {}.": "Running on the processor: {}.",
|
||||
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.",
|
||||
"Denoising {name}: {done} of {total} tiles{}": "Denoising {name}: {done} of {total} tiles{}",
|
||||
"{size} GB": "{size} GB",
|
||||
"{} of {} from {host}": "{} of {} from {host}",
|
||||
"{} KB": "{} KB",
|
||||
"{} MB": "{} MB",
|
||||
"Making the denoised picture… {}%": "Making the denoised picture… {}%",
|
||||
"{name} · {terms}": "{name} · {terms}",
|
||||
"{n} {noun}": "{n} {noun}",
|
||||
"Trained on: {provenance}": "Trained on: {provenance}",
|
||||
" · {queued} waiting": " · {queued} waiting",
|
||||
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.",
|
||||
"In line for AI Denoise ({ahead} ahead)…": "In line for AI Denoise ({ahead} ahead)…",
|
||||
"Running on {}.": "Running on {}.",
|
||||
"{file} · {size}": "{file} · {size}",
|
||||
"• {assumption}": "• {assumption}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1642,5 +1642,94 @@
|
|||
"How to install the model": "如何安装模型",
|
||||
"Install the SAM 3 Model": "安装 SAM 3 模型",
|
||||
"This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚无法下载模型。如需手动安装,请将 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的文件夹:放入后即可使用“对象”和“描述”。",
|
||||
"Open Log Folder": "打开日志文件夹"
|
||||
"Open Log Folder": "打开日志文件夹",
|
||||
"AI Denoise": "AI Denoise",
|
||||
"AI Denoise Model": "AI Denoise Model",
|
||||
"AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.",
|
||||
"AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.",
|
||||
"Accept & Download": "Accept & Download",
|
||||
"Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.",
|
||||
"Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.",
|
||||
"Cache": "Cache",
|
||||
"Checking and installing the model…": "Checking and installing the model…",
|
||||
"Checking…": "Checking…",
|
||||
"Clear cache": "Clear cache",
|
||||
"Denoise Models": "Denoise Models",
|
||||
"Denoise Settings": "Denoise Settings",
|
||||
"Denoise all photos with an Amount": "Denoise all photos with an Amount",
|
||||
"Downloaded and checked. Installing…": "Downloaded and checked. Installing…",
|
||||
"Failed its last test": "Failed its last test",
|
||||
"Graphics card": "Graphics card",
|
||||
"I have read these terms and accept them for my own use": "I have read these terms and accept them for my own use",
|
||||
"Inspect Denoise Model": "Inspect Denoise Model",
|
||||
"Install Denoise Model": "Install Denoise Model",
|
||||
"Install from file…": "Install from file…",
|
||||
"Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.",
|
||||
"Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft does not know this model, so it assumed:",
|
||||
"Limit": "Limit",
|
||||
"Make it now": "Make it now",
|
||||
"Make the denoised picture of the photos I am looking at": "Make the denoised picture of the photos I am looking at",
|
||||
"Next up for AI Denoise…": "Next up for AI Denoise…",
|
||||
"Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.",
|
||||
"Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.",
|
||||
"Open page": "Open page",
|
||||
"Open the model's page": "Open the model's page",
|
||||
"Opens the model's own page in your browser, to read about it or get the file yourself.": "Opens the model's own page in your browser, to read about it or get the file yourself.",
|
||||
"Processor": "Processor",
|
||||
"Set up AI Denoise…": "Set up AI Denoise…",
|
||||
"Speed": "Speed",
|
||||
"Starting…": "Starting…",
|
||||
"Tested and works on this computer": "Tested and works on this computer",
|
||||
"The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works",
|
||||
"The graphics card is set up when the next photo is made.": "The graphics card is set up when the next photo is made.",
|
||||
"The model is kept on this computer only. LightCraft never uploads or shares it.": "The model is kept on this computer only. LightCraft never uploads or shares it.",
|
||||
"The processor does the work.": "The processor does the work.",
|
||||
"There is no model file to install": "There is no model file to install",
|
||||
"This build cannot run denoise models.": "This build cannot run denoise models.",
|
||||
"This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)",
|
||||
"This file cannot be used yet": "This file cannot be used yet",
|
||||
"Tick the box to accept the model's terms first": "Tick the box to accept the model's terms first",
|
||||
"Try again": "Try again",
|
||||
"Try the graphics card again": "Try the graphics card again",
|
||||
"Turn off": "Turn off",
|
||||
"Unknown licence": "Unknown licence",
|
||||
"Use": "Use",
|
||||
"Waiting to start…": "Waiting to start…",
|
||||
"Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.",
|
||||
"cached picture": "cached picture",
|
||||
"cached pictures": "cached pictures",
|
||||
"commercial use allowed": "commercial use allowed",
|
||||
"denoiseModel.accept": "denoiseModel.accept",
|
||||
"in use": "in use",
|
||||
"installed": "installed",
|
||||
"makes AI Denoise available": "makes AI Denoise available",
|
||||
"non-commercial use only": "non-commercial use only",
|
||||
"photo has its denoised picture.": "photo has its denoised picture.",
|
||||
"photo waiting": "photo waiting",
|
||||
"photos have their denoised picture.": "photos have their denoised picture.",
|
||||
"photos waiting": "photos waiting",
|
||||
"starts using it for AI Denoise": "starts using it for AI Denoise",
|
||||
"terms unclear": "terms unclear",
|
||||
"the graphics card": "the graphics card",
|
||||
"the graphics card is not used": "the graphics card is not used",
|
||||
"Cancel Denoise": "Cancel Denoise",
|
||||
"Cancel Denoise Model Download": "Cancel Denoise Model Download",
|
||||
"Choose Denoise Model": "Choose Denoise Model",
|
||||
"Clear Denoise Cache": "Clear Denoise Cache",
|
||||
"Denoise Model Downloads": "Denoise Model Downloads",
|
||||
"Denoise Photos": "Denoise Photos",
|
||||
"Denoise Pump": "Denoise Pump",
|
||||
"Denoise Status": "Denoise Status",
|
||||
"Download Denoise Model": "Download Denoise Model",
|
||||
"Remove Denoise Model": "Remove Denoise Model",
|
||||
"Test Denoise Model": "Test Denoise Model",
|
||||
"AI Denoise is enabled on the requesting photo": "AI Denoise is enabled on the requesting photo",
|
||||
"After installation, AI Denoise will turn on for the requesting photo.": "After installation, AI Denoise will turn on for the requesting photo.",
|
||||
"Cancel pending action": "Cancel pending action",
|
||||
"Install a denoise model to enable AI Denoise on this photo": "Install a denoise model to enable AI Denoise on this photo",
|
||||
"Pending photo action cancelled": "Pending photo action cancelled",
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,5 +187,24 @@
|
|||
"Moved; {} photo(s) relinked": "已移動;已重新連結 {} 張照片",
|
||||
"{} label (click more labels to show any of them)": "{}標籤(點擊更多標籤可顯示符合其中任一標籤的照片)",
|
||||
", {} star{}": ",{} 顆星{:.0}",
|
||||
", {} label": ",{}標籤"
|
||||
", {} label": ",{}標籤",
|
||||
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.",
|
||||
"{:.1} MB": "{:.1} MB",
|
||||
"Running on the processor: {}.": "Running on the processor: {}.",
|
||||
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.",
|
||||
"Denoising {name}: {done} of {total} tiles{}": "Denoising {name}: {done} of {total} tiles{}",
|
||||
"{size} GB": "{size} GB",
|
||||
"{} of {} from {host}": "{} of {} from {host}",
|
||||
"{} KB": "{} KB",
|
||||
"{} MB": "{} MB",
|
||||
"Making the denoised picture… {}%": "Making the denoised picture… {}%",
|
||||
"{name} · {terms}": "{name} · {terms}",
|
||||
"{n} {noun}": "{n} {noun}",
|
||||
"Trained on: {provenance}": "Trained on: {provenance}",
|
||||
" · {queued} waiting": " · {queued} waiting",
|
||||
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.",
|
||||
"In line for AI Denoise ({ahead} ahead)…": "In line for AI Denoise ({ahead} ahead)…",
|
||||
"Running on {}.": "Running on {}.",
|
||||
"{file} · {size}": "{file} · {size}",
|
||||
"• {assumption}": "• {assumption}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1642,5 +1642,94 @@
|
|||
"How to install the model": "如何安裝模型",
|
||||
"Install the SAM 3 Model": "安裝 SAM 3 模型",
|
||||
"This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚無法下載模型。如需手動安裝,請將 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的資料夾:放入後即可使用「物件」與「描述」。",
|
||||
"Open Log Folder": "開啟記錄資料夾"
|
||||
"Open Log Folder": "開啟記錄資料夾",
|
||||
"AI Denoise": "AI Denoise",
|
||||
"AI Denoise Model": "AI Denoise Model",
|
||||
"AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.",
|
||||
"AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.",
|
||||
"Accept & Download": "Accept & Download",
|
||||
"Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.",
|
||||
"Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.",
|
||||
"Cache": "Cache",
|
||||
"Checking and installing the model…": "Checking and installing the model…",
|
||||
"Checking…": "Checking…",
|
||||
"Clear cache": "Clear cache",
|
||||
"Denoise Models": "Denoise Models",
|
||||
"Denoise Settings": "Denoise Settings",
|
||||
"Denoise all photos with an Amount": "Denoise all photos with an Amount",
|
||||
"Downloaded and checked. Installing…": "Downloaded and checked. Installing…",
|
||||
"Failed its last test": "Failed its last test",
|
||||
"Graphics card": "Graphics card",
|
||||
"I have read these terms and accept them for my own use": "I have read these terms and accept them for my own use",
|
||||
"Inspect Denoise Model": "Inspect Denoise Model",
|
||||
"Install Denoise Model": "Install Denoise Model",
|
||||
"Install from file…": "Install from file…",
|
||||
"Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.",
|
||||
"Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).",
|
||||
"LightCraft does not know this model, so it assumed:": "LightCraft does not know this model, so it assumed:",
|
||||
"Limit": "Limit",
|
||||
"Make it now": "Make it now",
|
||||
"Make the denoised picture of the photos I am looking at": "Make the denoised picture of the photos I am looking at",
|
||||
"Next up for AI Denoise…": "Next up for AI Denoise…",
|
||||
"Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.",
|
||||
"Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.",
|
||||
"Open page": "Open page",
|
||||
"Open the model's page": "Open the model's page",
|
||||
"Opens the model's own page in your browser, to read about it or get the file yourself.": "Opens the model's own page in your browser, to read about it or get the file yourself.",
|
||||
"Processor": "Processor",
|
||||
"Set up AI Denoise…": "Set up AI Denoise…",
|
||||
"Speed": "Speed",
|
||||
"Starting…": "Starting…",
|
||||
"Tested and works on this computer": "Tested and works on this computer",
|
||||
"The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works",
|
||||
"The graphics card is set up when the next photo is made.": "The graphics card is set up when the next photo is made.",
|
||||
"The model is kept on this computer only. LightCraft never uploads or shares it.": "The model is kept on this computer only. LightCraft never uploads or shares it.",
|
||||
"The processor does the work.": "The processor does the work.",
|
||||
"There is no model file to install": "There is no model file to install",
|
||||
"This build cannot run denoise models.": "This build cannot run denoise models.",
|
||||
"This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)",
|
||||
"This file cannot be used yet": "This file cannot be used yet",
|
||||
"Tick the box to accept the model's terms first": "Tick the box to accept the model's terms first",
|
||||
"Try again": "Try again",
|
||||
"Try the graphics card again": "Try the graphics card again",
|
||||
"Turn off": "Turn off",
|
||||
"Unknown licence": "Unknown licence",
|
||||
"Use": "Use",
|
||||
"Waiting to start…": "Waiting to start…",
|
||||
"Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.",
|
||||
"cached picture": "cached picture",
|
||||
"cached pictures": "cached pictures",
|
||||
"commercial use allowed": "commercial use allowed",
|
||||
"denoiseModel.accept": "denoiseModel.accept",
|
||||
"in use": "in use",
|
||||
"installed": "installed",
|
||||
"makes AI Denoise available": "makes AI Denoise available",
|
||||
"non-commercial use only": "non-commercial use only",
|
||||
"photo has its denoised picture.": "photo has its denoised picture.",
|
||||
"photo waiting": "photo waiting",
|
||||
"photos have their denoised picture.": "photos have their denoised picture.",
|
||||
"photos waiting": "photos waiting",
|
||||
"starts using it for AI Denoise": "starts using it for AI Denoise",
|
||||
"terms unclear": "terms unclear",
|
||||
"the graphics card": "the graphics card",
|
||||
"the graphics card is not used": "the graphics card is not used",
|
||||
"Cancel Denoise": "Cancel Denoise",
|
||||
"Cancel Denoise Model Download": "Cancel Denoise Model Download",
|
||||
"Choose Denoise Model": "Choose Denoise Model",
|
||||
"Clear Denoise Cache": "Clear Denoise Cache",
|
||||
"Denoise Model Downloads": "Denoise Model Downloads",
|
||||
"Denoise Photos": "Denoise Photos",
|
||||
"Denoise Pump": "Denoise Pump",
|
||||
"Denoise Status": "Denoise Status",
|
||||
"Download Denoise Model": "Download Denoise Model",
|
||||
"Remove Denoise Model": "Remove Denoise Model",
|
||||
"Test Denoise Model": "Test Denoise Model",
|
||||
"AI Denoise is enabled on the requesting photo": "AI Denoise is enabled on the requesting photo",
|
||||
"After installation, AI Denoise will turn on for the requesting photo.": "After installation, AI Denoise will turn on for the requesting photo.",
|
||||
"Cancel pending action": "Cancel pending action",
|
||||
"Install a denoise model to enable AI Denoise on this photo": "Install a denoise model to enable AI Denoise on this photo",
|
||||
"Pending photo action cancelled": "Pending photo action cancelled",
|
||||
"Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed",
|
||||
"ONNX model": "ONNX model",
|
||||
"commercial use subject to GPL conditions": "commercial use subject to GPL conditions"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ mod tests {
|
|||
h.step();
|
||||
let w = widgets(&mut h);
|
||||
assert!(w.contains("\"denoise:setup\""), "a raw photo with no model is offered the setup: {w}");
|
||||
// one click lands in Settings ▸ AI Denoise, where the model LightCraft knows is listed
|
||||
// One click lands in Settings, with local installation available even without a download offer.
|
||||
let r = h.request("ui.clickWidget", json!({"id": "denoise:setup"}), t);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
|
|
@ -332,7 +332,8 @@ mod tests {
|
|||
assert_eq!(h.app.ui.dialog, Some(Dialog::Settings { tab: "denoise".into() }));
|
||||
h.settle(SETTLE);
|
||||
let listed = h.app.session.execute("denoise.models.list", &json!({})).unwrap();
|
||||
assert!(listed["models"].as_array().unwrap().iter().any(|m| m["id"] == "rawnind-bayer" && m["installed"] == false), "{listed}");
|
||||
assert_eq!(listed["models"].as_array().unwrap().len(), lightcraft_denoise::known::all().len(), "{listed}");
|
||||
assert!(widgets(&mut h).contains("\"denoise:installFile\""));
|
||||
h.app.ui.dialog = None;
|
||||
// a photo that is not raw: no offer
|
||||
h.request(
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ pub struct Services {
|
|||
pub picker: Option<pick::Picker>,
|
||||
/// Show an open dialog for photos; returns paths.
|
||||
pub pick_files: Option<PickFiles>,
|
||||
/// Local ONNX model; a manifest must accompany it.
|
||||
pub pick_denoise_model: Option<PickFiles>,
|
||||
/// Open dialog for preset files (`.lcpreset`, `.xmp`, `.lrtemplate`, `.zip`, `.dng`, Luminar `.lmp` / `.mplumpack`).
|
||||
pub pick_preset_files: Option<PickFiles>,
|
||||
/// Open dialog for a GPS track log (`.gpx`; Photo ▸ Auto-Tag from Tracklog…).
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Resume a denoise request after model setup, bound to its original photo and library.
|
||||
|
||||
use crate::i18n::tr;
|
||||
use crate::{LightcraftApp, state::Dialog};
|
||||
use lightcraft_catalog::{PhotoId, Source};
|
||||
use lightcraft_engine::denoise::PhotoState;
|
||||
|
|
@ -43,7 +44,7 @@ pub(crate) fn intercept(app: &mut LightcraftApp, command: &str, params: &Value)
|
|||
return Some(Err("modelSetup.cancel: use kind denoise".into()));
|
||||
}
|
||||
app.model_setup.denoise = None;
|
||||
app.ui.status = "Pending photo action cancelled".into();
|
||||
app.ui.status = tr("Pending photo action cancelled").into();
|
||||
return Some(Ok(json!({"cancelled": true})));
|
||||
}
|
||||
if command == "denoise.models.downloadCancel" {
|
||||
|
|
@ -85,7 +86,7 @@ pub(crate) fn intercept(app: &mut LightcraftApp, command: &str, params: &Value)
|
|||
app.model_setup.denoise = Some(request);
|
||||
app.model_setup.next_check = 0.0;
|
||||
app.ui.dialog = Some(Dialog::Settings { tab: "denoise".into() });
|
||||
app.ui.status = "Install a denoise model to enable AI Denoise on this photo".into();
|
||||
app.ui.status = tr("Install a denoise model to enable AI Denoise on this photo").into();
|
||||
json!({"setupRequired": true, "photo": id.0})
|
||||
}))
|
||||
}
|
||||
|
|
@ -94,8 +95,8 @@ pub(crate) fn notice(app: &mut LightcraftApp, ui: &mut egui::Ui, kind: &str) {
|
|||
if kind != "denoise" || !app.model_setup.denoise.as_ref().is_some_and(|r| r.valid(app)) {
|
||||
return;
|
||||
}
|
||||
ui.add(egui::Label::new("After installation, AI Denoise will turn on for the requesting photo.").wrap());
|
||||
let r = ui.small_button("Cancel pending action");
|
||||
ui.add(egui::Label::new(tr("After installation, AI Denoise will turn on for the requesting photo.")).wrap());
|
||||
let r = ui.small_button(tr("Cancel pending action"));
|
||||
crate::widgets::register(ui.ctx(), "modelSetup:cancel:denoise", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("modelSetup.cancel", json!({"kind": "denoise"}));
|
||||
|
|
@ -118,7 +119,7 @@ pub(crate) fn pump(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
app.model_setup.next_check = now + 0.25;
|
||||
let Some(request) = app.model_setup.denoise.take() else { return };
|
||||
if !request.valid(app) {
|
||||
app.ui.status = "Pending photo action cancelled because its library or photo changed".into();
|
||||
app.ui.status = tr("Pending photo action cancelled because its library or photo changed").into();
|
||||
return;
|
||||
}
|
||||
if matches!(app.session.denoise_photo_state(request.photo), PhotoState::NoModel | PhotoState::NotApplicable) {
|
||||
|
|
@ -127,7 +128,7 @@ pub(crate) fn pump(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
return;
|
||||
}
|
||||
match app.run("denoise.toggle", json!({"id": request.photo.0, "enabled": true})) {
|
||||
Ok(_) => app.ui.status = "AI Denoise is enabled on the requesting photo".into(),
|
||||
Ok(_) => app.ui.status = tr("AI Denoise is enabled on the requesting photo").into(),
|
||||
Err(error) => app.ui.status = error,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
//! slider mixes in. It is cached data made in the background for the photos being looked at, and never a file in the
|
||||
//! library. A model is fetched only when the user presses Download and accepts its terms.
|
||||
|
||||
use crate::i18n::{tr, tr_format};
|
||||
use egui::RichText;
|
||||
use lightcraft_catalog::PhotoId;
|
||||
use lightcraft_engine::denoise::PhotoState;
|
||||
|
|
@ -110,7 +111,7 @@ fn watch_downloads(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
let (mut keep, mut installed) = (Vec::new(), Vec::new());
|
||||
for id in std::mem::take(&mut app.caches.denoise.dl_watch) {
|
||||
match rows.iter().find(|r| r["id"] == id.as_str()).and_then(|r| r["state"].as_str()) {
|
||||
Some("running" | "done") => keep.push(id),
|
||||
Some("running" | "done" | "installing") => keep.push(id),
|
||||
Some("installed") => installed.push(id),
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -119,7 +120,7 @@ fn watch_downloads(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
for id in installed {
|
||||
let _ = app.session.execute("denoise.models.downloadCancel", &json!({"id": id}));
|
||||
app.caches.denoise.epoch += 1;
|
||||
let text = "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works";
|
||||
let text = tr("The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works");
|
||||
app.ui.status = text.into();
|
||||
app.toast(ctx, text);
|
||||
}
|
||||
|
|
@ -150,7 +151,7 @@ pub fn detail_status(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, am
|
|||
PhotoState::NotApplicable | PhotoState::Ready => {}
|
||||
PhotoState::NoModel => {
|
||||
egui::Frame::NONE.inner_margin(pad).show(ui, |ui| {
|
||||
note(ui, "AI Denoise needs a model: a one-time download, in Settings.", t.text_dim);
|
||||
note(ui, "AI Denoise needs a model. Install one in Settings.", t.text_dim);
|
||||
ui.add_space(2.0);
|
||||
let r = ui.small_button(crate::i18n::tr("Set up AI Denoise…"));
|
||||
register(ui.ctx(), "denoise:setup", r.rect);
|
||||
|
|
@ -180,15 +181,22 @@ pub fn detail_status(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, am
|
|||
}
|
||||
PhotoState::Queued { ahead } => {
|
||||
egui::Frame::NONE.inner_margin(pad).show(ui, |ui| {
|
||||
let text = if ahead == 0 { "Next up for AI Denoise…".to_string() } else { format!("In line for AI Denoise ({ahead} ahead)…") };
|
||||
let text = if ahead == 0 {
|
||||
tr("Next up for AI Denoise…").to_string()
|
||||
} else {
|
||||
tr_format!("In line for AI Denoise ({ahead} ahead)…", ahead = ahead)
|
||||
};
|
||||
note(ui, &text, t.text_dim);
|
||||
});
|
||||
}
|
||||
PhotoState::Running { done, total } => {
|
||||
egui::Frame::NONE.inner_margin(pad).show(ui, |ui| {
|
||||
let frac = if total > 0 { (done as f32 / total as f32).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let text =
|
||||
if total > 0 { format!("Making the denoised picture… {}%", (frac * 100.0).round() as u32) } else { "Starting…".to_string() };
|
||||
let text = if total > 0 {
|
||||
tr_format!("Making the denoised picture… {}%", (frac * 100.0).round() as u32)
|
||||
} else {
|
||||
tr("Starting…").to_string()
|
||||
};
|
||||
let bar = ui.add(egui::ProgressBar::new(frac).desired_width(ui.available_width().min(240.0)).text(RichText::new(text).size(11.0)));
|
||||
register(ui.ctx(), "denoise:progress", bar.rect);
|
||||
});
|
||||
|
|
@ -235,6 +243,24 @@ pub fn settings_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
.map(|v| v["downloads"].as_array().cloned().unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
heading(ui, t, "Models");
|
||||
let r = ui.add_enabled(runtime && app.services.pick_denoise_model.is_some(), egui::Button::new(tr("Install from file…")));
|
||||
register(ui.ctx(), "denoise:installFile", r.rect);
|
||||
if r.clicked()
|
||||
&& let Some(path) = app.services.pick_denoise_model.as_mut().and_then(|f| f().into_iter().next())
|
||||
&& let Ok(info) = app.run("denoise.models.inspect", json!({"path": path}))
|
||||
{
|
||||
app.ui.dialog = Some(crate::state::Dialog::DenoiseModel { info, accepted: false });
|
||||
}
|
||||
if all.is_empty() {
|
||||
hint(ui, t, "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.");
|
||||
}
|
||||
if let Some(job) = downloads.iter().find(|d| d["id"] == "local-install") {
|
||||
match job["state"].as_str() {
|
||||
Some("installing") => hint(ui, t, "Checking and installing the model…"),
|
||||
Some("failed") => hint(ui, t, job["error"].as_str().unwrap_or("Model installation failed")),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for m in &all {
|
||||
let dl = downloads.iter().find(|d| d["id"] == m["id"]);
|
||||
model_row(app, ui, t, m, dl, runtime);
|
||||
|
|
@ -290,8 +316,13 @@ fn work_section(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, list: &V
|
|||
.as_u64()
|
||||
.and_then(|p| app.session.catalog.photo(PhotoId(p)).map(|p| p.file_name.clone()))
|
||||
.unwrap_or_default();
|
||||
let text =
|
||||
format!("Denoising {name}: {done} of {total} tiles{}", if queued > 0 { format!(" · {queued} waiting") } else { String::new() });
|
||||
let text = tr_format!(
|
||||
"Denoising {name}: {done} of {total} tiles{}",
|
||||
if queued > 0 { tr_format!(" · {queued} waiting", queued = queued) } else { String::new() },
|
||||
name = name,
|
||||
done = done,
|
||||
total = total
|
||||
);
|
||||
let bar = ui.add(egui::ProgressBar::new(frac).desired_width(380.0).text(RichText::new(text).size(11.5)));
|
||||
register(ui.ctx(), "denoise:settingsProgress", bar.rect);
|
||||
}
|
||||
|
|
@ -302,13 +333,13 @@ fn work_section(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, list: &V
|
|||
}
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let r = ui.button("Denoise all photos with an Amount");
|
||||
let r = ui.button(tr("Denoise all photos with an Amount"));
|
||||
register(ui.ctx(), "denoise:queueAll", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("denoise.queue", json!({"scope": "withAmount"}));
|
||||
}
|
||||
if running.is_some() || queued > 0 {
|
||||
let r = ui.button("Stop");
|
||||
let r = ui.button(tr("Stop"));
|
||||
register(ui.ctx(), "denoise:stop", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("denoise.cancel", json!({}));
|
||||
|
|
@ -320,7 +351,7 @@ fn work_section(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, list: &V
|
|||
hint(
|
||||
ui,
|
||||
t,
|
||||
&format!(
|
||||
&tr_format!(
|
||||
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.",
|
||||
count(files, "cached picture", "cached pictures"),
|
||||
mb(Some(bytes))
|
||||
|
|
@ -330,16 +361,16 @@ fn work_section(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, list: &V
|
|||
ui.label(RichText::new(crate::i18n::tr("Limit")).color(t.text_label));
|
||||
let current = list["cacheGb"].as_u64().unwrap_or(20) as u32;
|
||||
let mut choice = current;
|
||||
egui::ComboBox::from_id_salt("denoise-cache-limit").selected_text(format!("{current} GB")).show_ui(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt("denoise-cache-limit").selected_text(tr_format!("{size} GB", size = current)).show_ui(ui, |ui| {
|
||||
for gb in CACHE_LIMITS {
|
||||
ui.selectable_value(&mut choice, gb, format!("{gb} GB"));
|
||||
ui.selectable_value(&mut choice, gb, tr_format!("{size} GB", size = gb));
|
||||
}
|
||||
});
|
||||
if choice != current {
|
||||
let _ = app.run("denoise.settings", json!({"cacheGb": choice}));
|
||||
app.caches.denoise.epoch += 1;
|
||||
}
|
||||
let r = ui.button("Clear cache");
|
||||
let r = ui.button(tr("Clear cache"));
|
||||
register(ui.ctx(), "denoise:clear", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("denoise.clear", json!({}));
|
||||
|
|
@ -351,12 +382,12 @@ fn work_section(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, list: &V
|
|||
/// computer's, and only the comparison matters.
|
||||
fn device_line(d: &Value, run_on: &str) -> String {
|
||||
match d["kind"].as_str() {
|
||||
_ if run_on == "cpu" => "The processor does the work.".into(),
|
||||
Some("gpu") => format!("Running on {}.", d["adapter"].as_str().unwrap_or("the graphics card")),
|
||||
_ if run_on == "cpu" => tr("The processor does the work.").into(),
|
||||
Some("gpu") => tr_format!("Running on {}.", d["adapter"].as_str().unwrap_or(tr("the graphics card"))),
|
||||
Some("cpu") => {
|
||||
format!("Running on the processor: {}.", d["reason"].as_str().unwrap_or("the graphics card is not used").trim_end_matches('.'))
|
||||
tr_format!("Running on the processor: {}.", tr(d["reason"].as_str().unwrap_or("the graphics card is not used")).trim_end_matches('.'))
|
||||
}
|
||||
_ => "The graphics card is set up when the next photo is made.".into(),
|
||||
_ => tr("The graphics card is set up when the next photo is made.").into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -375,9 +406,9 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value,
|
|||
ui.label(RichText::new(size).color(t.text_dim));
|
||||
}
|
||||
if selected {
|
||||
ui.label(RichText::new("in use").color(t.accent));
|
||||
ui.label(RichText::new(tr("in use")).color(t.accent));
|
||||
} else if installed {
|
||||
ui.label(RichText::new("installed").color(t.text_dim));
|
||||
ui.label(RichText::new(tr("installed")).color(t.text_dim));
|
||||
}
|
||||
});
|
||||
ui.label(RichText::new(licence_line(m)).font(t.font(11.5)).color(if m["licence"]["commercial"] == "yes" {
|
||||
|
|
@ -387,10 +418,10 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value,
|
|||
}));
|
||||
match m["accepted"]["selfTest"].as_object() {
|
||||
Some(test) if test.get("ok").and_then(Value::as_bool) == Some(true) => {
|
||||
ui.label(RichText::new("Tested and works on this computer").font(t.font(11.5)).color(t.text_dim));
|
||||
ui.label(RichText::new(tr("Tested and works on this computer")).font(t.font(11.5)).color(t.text_dim));
|
||||
}
|
||||
Some(_) => {
|
||||
ui.label(RichText::new("Failed its last test").font(t.font(11.5)).color(t.caution));
|
||||
ui.label(RichText::new(tr("Failed its last test")).font(t.font(11.5)).color(t.caution));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
|
@ -399,15 +430,15 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value,
|
|||
let (got, total) = (d["bytes"].as_u64().unwrap_or(0), d["total"].as_u64().unwrap_or(0));
|
||||
let frac = if total > 0 { (got as f32 / total as f32).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let text = if total > 0 && got >= total {
|
||||
"Checking…".to_string()
|
||||
tr("Checking…").to_string()
|
||||
} else {
|
||||
format!("{} of {} from {host}", mb(Some(got)), mb(Some(total)))
|
||||
tr_format!("{} of {} from {host}", mb(Some(got)), mb(Some(total)), host = host)
|
||||
};
|
||||
let bar = ui.add(egui::ProgressBar::new(frac).desired_width(260.0).text(RichText::new(text).font(t.font(11.5))));
|
||||
register(ui.ctx(), format!("denoise:progress:{id}"), bar.rect);
|
||||
}
|
||||
(Some("done"), _) => {
|
||||
ui.label(RichText::new("Downloaded and checked. Installing…").font(t.font(11.5)).color(t.text_dim));
|
||||
(Some("done" | "installing"), _) => {
|
||||
ui.label(RichText::new(tr("Downloaded and checked. Installing…")).font(t.font(11.5)).color(t.text_dim));
|
||||
}
|
||||
(Some("failed"), Some(d)) => {
|
||||
let why = sentence(d["error"].as_str().unwrap_or("The download failed"));
|
||||
|
|
@ -418,21 +449,21 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value,
|
|||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if installed {
|
||||
let r = ui.button("Remove");
|
||||
let r = ui.button(tr("Remove"));
|
||||
register(ui.ctx(), format!("denoise:remove:{id}"), r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("denoise.models.remove", json!({"id": id}));
|
||||
app.caches.denoise.epoch += 1;
|
||||
}
|
||||
if selected {
|
||||
let r = ui.button("Turn off");
|
||||
let r = ui.button(tr("Turn off"));
|
||||
register(ui.ctx(), format!("denoise:off:{id}"), r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("denoise.models.select", json!({"id": null}));
|
||||
app.caches.denoise.epoch += 1;
|
||||
}
|
||||
} else {
|
||||
let r = ui.button("Use");
|
||||
let r = ui.button(tr("Use"));
|
||||
register(ui.ctx(), format!("denoise:use:{id}"), r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("denoise.models.select", json!({"id": id}));
|
||||
|
|
@ -442,7 +473,7 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value,
|
|||
} else {
|
||||
match dl_state {
|
||||
Some("running") => {
|
||||
let r = ui.button("Cancel");
|
||||
let r = ui.button(tr("Cancel"));
|
||||
register(ui.ctx(), format!("denoise:cancelDownload:{id}"), r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("denoise.models.downloadCancel", json!({"id": id}));
|
||||
|
|
@ -450,12 +481,12 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value,
|
|||
}
|
||||
}
|
||||
// the engine installs it within a moment; nothing to press
|
||||
Some("done") => {}
|
||||
Some("done" | "installing") => {}
|
||||
_ => {
|
||||
if let Some(url) = m["source"].as_str() {
|
||||
let r = ui
|
||||
.button("Open page")
|
||||
.on_hover_text("Opens the model's own page in your browser, to read about it or get the file yourself.");
|
||||
.button(tr("Open page"))
|
||||
.on_hover_text(tr("Opens the model's own page in your browser, to read about it or get the file yourself."));
|
||||
register(ui.ctx(), format!("denoise:get:{id}"), r.rect);
|
||||
if r.clicked() {
|
||||
open_page(app, url);
|
||||
|
|
@ -463,11 +494,12 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value,
|
|||
}
|
||||
if !host.is_empty() {
|
||||
let label = if dl_state == Some("failed") { "Try again" } else { "Download" };
|
||||
let tip = format!(
|
||||
let tip = tr_format!(
|
||||
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.",
|
||||
mb(m["sizeBytes"].as_u64())
|
||||
mb(m["sizeBytes"].as_u64()),
|
||||
host = host
|
||||
);
|
||||
let r = ui.button(label).on_hover_text(tip);
|
||||
let r = ui.button(tr(label)).on_hover_text(tip);
|
||||
register(ui.ctx(), format!("denoise:download:{id}"), r.rect);
|
||||
if r.clicked() {
|
||||
open_download_dialog(app, m);
|
||||
|
|
@ -502,14 +534,17 @@ fn open_download_dialog(app: &mut LightcraftApp, m: &Value) {
|
|||
/// The dialog's OK for a denoise model: start the download and watch it.
|
||||
pub fn install(app: &mut LightcraftApp, info: &Value, accepted: bool) -> Result<Value, String> {
|
||||
if !accepted {
|
||||
return Err("Tick the box to accept the model's terms first".into());
|
||||
return Err(tr("Tick the box to accept the model's terms first").into());
|
||||
}
|
||||
app.caches.denoise.epoch += 1;
|
||||
let id = info["download"].as_str().ok_or("There is no download for this model")?;
|
||||
let r = app.run("denoise.models.download", json!({"id": id, "acknowledged": true}));
|
||||
let (id, r) = if let Some(id) = info["download"].as_str() {
|
||||
(id.to_string(), app.run("denoise.models.download", json!({"id": id, "acknowledged": true})))
|
||||
} else {
|
||||
let path = info["path"].as_str().ok_or_else(|| tr("There is no model file to install").to_string())?;
|
||||
("local-install".into(), app.run("denoise.models.install", json!({"path": path, "acknowledged": true, "background": true})))
|
||||
};
|
||||
if r.is_ok() {
|
||||
app.caches.denoise.dl_watch.push(id.to_string());
|
||||
// the progress is in Settings: stay there
|
||||
app.caches.denoise.dl_watch.push(id);
|
||||
app.ui.dialog = Some(crate::state::Dialog::Settings { tab: "denoise".into() });
|
||||
}
|
||||
r
|
||||
|
|
@ -517,7 +552,7 @@ pub fn install(app: &mut LightcraftApp, info: &Value, accepted: bool) -> Result<
|
|||
|
||||
/// "1 photo" / "3 photos": the count with the right noun.
|
||||
fn count(n: u64, one: &str, many: &str) -> String {
|
||||
format!("{n} {}", if n == 1 { one } else { many })
|
||||
tr_format!("{n} {noun}", n = n, noun = tr(if n == 1 { one } else { many }))
|
||||
}
|
||||
|
||||
pub(super) fn window_pace(app: &LightcraftApp, ctx: &egui::Context, now: f64, watching: bool) -> &'static str {
|
||||
|
|
@ -544,9 +579,9 @@ fn pace_for(focused: bool, minimized: bool, worked: f64, moved: f64, watching: b
|
|||
|
||||
pub(super) fn mb(bytes: Option<u64>) -> String {
|
||||
match bytes {
|
||||
Some(b) if b >= 10_000_000 => format!("{} MB", (b as f64 / 1e6).round() as u64),
|
||||
Some(b) if b >= 1_000_000 => format!("{:.1} MB", b as f64 / 1e6),
|
||||
Some(b) => format!("{} KB", (b as f64 / 1e3).round().max(1.0) as u64),
|
||||
Some(b) if b >= 10_000_000 => tr_format!("{} MB", (b as f64 / 1e6).round() as u64),
|
||||
Some(b) if b >= 1_000_000 => tr_format!("{:.1} MB", b as f64 / 1e6),
|
||||
Some(b) => tr_format!("{} KB", (b as f64 / 1e3).round().max(1.0) as u64),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -565,11 +600,12 @@ pub(super) fn sentence(s: &str) -> String {
|
|||
pub(super) fn licence_line(m: &Value) -> String {
|
||||
let name = m["licence"]["name"].as_str().filter(|s| !s.is_empty()).unwrap_or("Unknown licence");
|
||||
let terms = match m["licence"]["commercial"].as_str() {
|
||||
Some("yes") if name.starts_with("GPL") => "commercial use subject to GPL conditions",
|
||||
Some("yes") => "commercial use allowed",
|
||||
Some("no") => "non-commercial use only",
|
||||
_ => "terms unclear",
|
||||
};
|
||||
format!("{name} · {terms}")
|
||||
tr_format!("{name} · {terms}", name = tr(name), terms = tr(terms))
|
||||
}
|
||||
|
||||
pub(super) fn open_page(app: &mut LightcraftApp, url: &str) {
|
||||
|
|
@ -582,14 +618,15 @@ pub fn model_dialog(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, info
|
|||
let kind = info["kind"].as_str().unwrap_or("unsupported");
|
||||
let file = info["fileName"].as_str().unwrap_or("");
|
||||
if kind == "unsupported" {
|
||||
ui.label(RichText::new("This file cannot be used yet").font(t.semibold(13.5)).color(t.caution));
|
||||
ui.label(RichText::new(tr("This file cannot be used yet")).font(t.semibold(13.5)).color(t.caution));
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
RichText::new(sentence(info["reason"].as_str().unwrap_or("It is not a denoise model LightCraft understands"))).color(t.text_label),
|
||||
RichText::new(sentence(tr(info["reason"].as_str().unwrap_or("It is not a denoise model LightCraft understands"))))
|
||||
.color(t.text_label),
|
||||
)
|
||||
.wrap(),
|
||||
);
|
||||
ui.label(RichText::new(format!("{file} · {}", mb(info["sizeBytes"].as_u64()))).color(t.text_dim));
|
||||
ui.label(RichText::new(tr_format!("{file} · {size}", file = file, size = mb(info["sizeBytes"].as_u64()))).color(t.text_dim));
|
||||
return;
|
||||
}
|
||||
let m = &info["model"];
|
||||
|
|
@ -598,7 +635,7 @@ pub fn model_dialog(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, info
|
|||
ui.label(RichText::new(mb(info["sizeBytes"].as_u64())).color(t.text_dim));
|
||||
});
|
||||
if info["alreadyInstalled"] == true {
|
||||
ui.label(RichText::new("Already installed. Installing again is harmless.").color(t.text_dim));
|
||||
ui.label(RichText::new(tr("Already installed. Remove it before installing a replacement.")).color(t.text_dim));
|
||||
}
|
||||
let download = info["download"].is_string();
|
||||
if download {
|
||||
|
|
@ -606,8 +643,12 @@ pub fn model_dialog(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, info
|
|||
let after = if info["domain"] == "denoise" { "starts using it for AI Denoise" } else { "makes AI Denoise available" };
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
RichText::new(format!("Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}."))
|
||||
.color(t.text_label),
|
||||
RichText::new(tr_format!(
|
||||
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.",
|
||||
host = host,
|
||||
after = tr(after)
|
||||
))
|
||||
.color(t.text_label),
|
||||
)
|
||||
.wrap(),
|
||||
);
|
||||
|
|
@ -620,17 +661,17 @@ pub fn model_dialog(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, info
|
|||
}
|
||||
let provenance = m["provenance"].as_str().unwrap_or("");
|
||||
if !provenance.is_empty() {
|
||||
ui.add(egui::Label::new(RichText::new(format!("Trained on: {provenance}")).color(t.text_dim)).wrap());
|
||||
ui.add(egui::Label::new(RichText::new(tr_format!("Trained on: {provenance}", provenance = provenance)).color(t.text_dim)).wrap());
|
||||
}
|
||||
if kind == "draft" {
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new("LightCraft does not know this model, so it assumed:").color(t.text_label));
|
||||
ui.label(RichText::new(tr("LightCraft does not know this model, so it assumed:")).color(t.text_label));
|
||||
for a in info["assumptions"].as_array().into_iter().flatten().filter_map(Value::as_str) {
|
||||
ui.add(egui::Label::new(RichText::new(format!("• {a}")).font(t.font(12.0)).color(t.text_dim)).wrap());
|
||||
ui.add(egui::Label::new(RichText::new(tr_format!("• {assumption}", assumption = tr(a))).font(t.font(12.0)).color(t.text_dim)).wrap());
|
||||
}
|
||||
}
|
||||
if let Some(url) = m["source"].as_str().or(m["licence"]["url"].as_str()) {
|
||||
let r = ui.link("Open the model's page");
|
||||
let r = ui.link(tr("Open the model's page"));
|
||||
register(ui.ctx(), "denoiseModel:page", r.rect);
|
||||
if r.clicked() {
|
||||
open_page(app, url);
|
||||
|
|
@ -643,5 +684,5 @@ pub fn model_dialog(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, info
|
|||
} else {
|
||||
"The model is kept on this computer only. LightCraft never uploads or shares it."
|
||||
};
|
||||
ui.label(RichText::new(closing).font(t.font(11.5)).color(t.text_dim));
|
||||
ui.label(RichText::new(tr(closing)).font(t.font(11.5)).color(t.text_dim));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
super::denoise::detail_toggle(app, ui, id, on, applicable);
|
||||
control(app, ui, d, "enhance.denoise", applicable && on);
|
||||
super::denoise::detail_status(app, ui, id, if on { d.enhance.denoise } else { 0.0 });
|
||||
sub_title(ui, crate::i18n::tr("Manual Noise Reduction"));
|
||||
sub_title(ui, crate::i18n::tr("Noise Reduction"));
|
||||
for c in ["detail.nrLuminance", "detail.nrDetail", "detail.nrContrast", "detail.nrColor", "detail.nrColorDetail", "detail.nrColorSmoothness"]
|
||||
{
|
||||
control(app, ui, d, c, true);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fn setup(name: &str) -> (LightcraftApp, PathBuf, PhotoId) {
|
|||
(LightcraftApp::new(s, Services::default()), dir, id)
|
||||
}
|
||||
|
||||
fn install_denoise(app: &mut LightcraftApp, dir: &Path) {
|
||||
fn denoise_file(dir: &Path) -> PathBuf {
|
||||
let source = dir.join("source");
|
||||
std::fs::create_dir_all(&source).unwrap();
|
||||
let bytes = lightcraft_denoise::synthetic::smoothing_onnx();
|
||||
|
|
@ -50,7 +50,38 @@ fn install_denoise(app: &mut LightcraftApp, dir: &Path) {
|
|||
gain: Gain::None,
|
||||
};
|
||||
std::fs::write(source.join("denoise-model.json"), serde_json::to_vec(&m).unwrap()).unwrap();
|
||||
app.run("denoise.models.install", json!({"path": source.join("model.onnx").to_string_lossy(), "acknowledged": true})).unwrap();
|
||||
source.join("model.onnx")
|
||||
}
|
||||
|
||||
fn install_denoise(app: &mut LightcraftApp, dir: &Path) {
|
||||
app.run("denoise.models.install", json!({"path": denoise_file(dir), "acknowledged": true})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepting_a_local_model_installs_in_background_and_resumes_the_pending_action() {
|
||||
let (mut app, dir, raw) = setup("local-install");
|
||||
let path = denoise_file(&dir);
|
||||
app.run("denoise.toggle", json!({"id": raw.0, "enabled": true})).unwrap();
|
||||
let info = app.run("denoise.models.inspect", json!({"path": path})).unwrap();
|
||||
assert!(crate::panels::denoise::install(&mut app, &info, false).is_err());
|
||||
assert!(!dir.join("denoise/test-smoothing/model.onnx").exists());
|
||||
let result = crate::panels::denoise::install(&mut app, &info, true).unwrap();
|
||||
assert_eq!(result["started"], "local-install");
|
||||
assert_eq!(app.ui.dialog, Some(Dialog::Settings { tab: "denoise".into() }));
|
||||
let ctx = egui::Context::default();
|
||||
for _ in 0..600 {
|
||||
crate::panels::denoise::pump(&mut app, &ctx);
|
||||
if app.session.denoise_photo_state(raw) != lightcraft_engine::denoise::PhotoState::NoModel {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
resume(&mut app);
|
||||
assert_eq!(amount(&app, raw), 50.0);
|
||||
assert!(dir.join("denoise/test-smoothing/model.onnx").is_file());
|
||||
assert!(path.is_file(), "a user-supplied original is preserved");
|
||||
drop(app);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
fn amount(app: &LightcraftApp, id: PhotoId) -> f64 {
|
||||
|
|
|
|||
|
|
@ -15,22 +15,29 @@ An active slider gesture finishes before the pending action resumes.
|
|||
## Models and terms
|
||||
|
||||
**No model weights are bundled.** Models live in the per-user `denoise-models` folder
|
||||
(override with `LIGHTCRAFT_DENOISE_MODELS`). Settings offers a pinned, verified download
|
||||
of RawNIND UtNet2 for Bayer sensors, or installation of a user-supplied ONNX file with
|
||||
a `denoise-model.json` beside it. Terms must be acknowledged before installation or
|
||||
(override with `LIGHTCRAFT_DENOISE_MODELS`). Settings provides **Install from file…**
|
||||
for a user-supplied ONNX file with a `denoise-model.json` beside it. The default build
|
||||
offers no model downloads. The separate `rawnind-model` Cargo feature enables the
|
||||
pinned RawNIND offer only when a release's model policy permits it.
|
||||
Terms must be acknowledged before installation or
|
||||
download. Downloads use the shared pure-Rust `lightcraft-fetch` client, with resume,
|
||||
progress, cancellation, size limits and SHA-256 verification.
|
||||
|
||||
RawNIND's offered ONNX weights are **GPL-3.0**, as recorded in the pinned model catalog.
|
||||
RawNIND's optional ONNX weights are **GPL-3.0**, as recorded in the pinned model catalog.
|
||||
They are downloaded separately after consent and read by our own Rust executor.
|
||||
Whether releases should offer this download remains a maintainer policy decision;
|
||||
permissively licensed or independently trained weights remain a gap. This is not a
|
||||
claim that optional downloading settles licensing questions. No GPL decoder or neural
|
||||
runtime code is included. Model metadata is in `crates/denoise/src/known.rs`.
|
||||
runtime code is included. Model metadata is in `crates/denoise-core/src/known.rs`.
|
||||
|
||||
Installation checks the manifest, model hash and size, then runs a synthetic self-test:
|
||||
finite output of the promised shape, repeatability, the declared scale and reduced
|
||||
noise. Unsupported operators, malformed files and a failed self-test return errors.
|
||||
The UI performs copying, extraction, hashing, parsing and CPU/GPU self-tests on one
|
||||
background installation worker. `denoise.models.downloads` and `denoise.pump` report
|
||||
`installing`, `installed` or `failed`; an existing model is preserved on a failed replacement.
|
||||
Command clients can request the same asynchronous path with `background: true`.
|
||||
The synchronous command remains available for scripts that require the result immediately.
|
||||
|
||||
The current contract is `bayerToRgb`: normalized, black-subtracted, non-white-balanced
|
||||
RGGB planes `[1, 4, tile, tile]` produce camera RGB `[1, 3, 2*tile, 2*tile]`.
|
||||
|
|
@ -63,10 +70,16 @@ reduction and report their limitation.
|
|||
## Pure-Rust CPU and GPU execution
|
||||
|
||||
The CPU executor reads a bounded subset of ONNX into checked network data. Convolutions
|
||||
use bounded im2col blocks and `faer-core 0.17.1` with its Rust-intrinsic GEMM kernels,
|
||||
without BLAS, generated assembly or an inner thread pool. Tiles share the outer worker
|
||||
use bounded im2col blocks and `faer-core 0.17.1` with its pure-Rust GEMM backend,
|
||||
without BLAS, C dependencies or an inner thread pool. That backend includes
|
||||
architecture-specific assembly in its dependencies. Tiles share the outer worker
|
||||
pool. A scalar implementation checks the optimized operators on synthetic networks.
|
||||
Tract is not a workspace dependency or a product feature.
|
||||
CPU inference and the GPU model module are feature-gated; ordinary engine/web builds
|
||||
do not compile the faer/GEMM backend. The engine depends on the inference crate only through `denoise = ["dep:lightcraft-denoise", …]`;
|
||||
lightweight `lightcraft-denoise-core` metadata and cache formats remain available without inference. Pictures are capped at 100 megapixels; at most
|
||||
four tile calls run together, chosen within the process-wide working-memory budget.
|
||||
One idle CPU workspace is retained, and cache costs reserve room for the blended picture.
|
||||
|
||||
The GPU executor uses our WGSL kernels through wgpu. It checks a real model's output
|
||||
against the CPU and falls back on errors, unsupported networks or a slower device.
|
||||
|
|
@ -97,8 +110,8 @@ Fresh checks can be run using an already installed model and CC0 raw samples:
|
|||
|
||||
```text
|
||||
LC_DENOISE_MODEL=<model.onnx> cargo test --release -p lightcraft-gpu --lib real_model_on_the_gpu_matches_cpu -- --ignored --nocapture
|
||||
LC_DENOISE_MODEL=<model.onnx> LC_DENOISE_RAW=<corpus/raw> cargo test --release -p lightcraft-engine --features denoise --test denoise_cpu -- --ignored --nocapture
|
||||
LC_DENOISE_MODEL=<model.onnx> LC_DENOISE_RAW=<corpus/raw> cargo test --release -p lightcraft-engine --features denoise --test denoise_real -- --ignored --nocapture
|
||||
LC_DENOISE_MODEL=<model.onnx> LC_DENOISE_RAW=<corpus/raw> cargo test --release -p lightcraft-engine --features rawnind-model --test denoise_cpu -- --ignored --nocapture
|
||||
LC_DENOISE_MODEL=<model.onnx> LC_DENOISE_RAW=<corpus/raw> cargo test --release -p lightcraft-engine --features rawnind-model --test denoise_real -- --ignored --nocapture
|
||||
```
|
||||
|
||||
## Commands and limits
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table
|
|||
| C. Views & navigation (VIEW) | 15 | 1 | 1 | 0 | 8/9 (89%) | 4/4 (100%) |
|
||||
| D. Search & filter (FILT) | 11 | 1 | 1 | 0 | 4/4 (100%) | 4/4 (100%) |
|
||||
| E. Metadata (META) | 5 | 1 | 0 | 0 | 2/2 (100%) | 2/2 (100%) |
|
||||
| F. Edit panel — global adjustments (EDIT) | 42 | 1 | 5 | 1 | 28/28 (100%) | 13/14 (93%) |
|
||||
| F. Edit panel — global adjustments (EDIT) | 42 | 2 | 4 | 1 | 28/28 (100%) | 13/14 (93%) |
|
||||
| G. Profiles (PROF) | 7 | 1 | 3 | 0 | 3/4 (75%) | 3/3 (100%) |
|
||||
| H. Crop & rotate (CROP) | 9 | 0 | 0 | 1 | 6/6 (100%) | 3/3 (100%) |
|
||||
| I. Remove / healing (REM) | 7 | 1 | 2 | 2 | 4/4 (100%) | 2/3 (67%) |
|
||||
|
|
@ -79,7 +79,7 @@ Take the first one nobody is working on.
|
|||
Object and Describe masks run SAM 3 in pure Rust (`crates/segment`); the weights (SAM License) are never bundled:
|
||||
the app offers a consented, verified download, but **LightCraft's CDN mirrors are not configured yet** (the
|
||||
built-in list is empty, so today users need their own mirror or a manual install). Subject / Sky / People could
|
||||
use the same model with fixed prompts. Bayer RAW denoise now runs on pure-Rust CPU/GPU with separately installed weights; its opt-in GPL model policy, linear RGB and quality comparison remain open. Super-resolution models remain a maintainer decision.
|
||||
use the same model with fixed prompts; denoise / super-resolution models remain a maintainer decision.
|
||||
7. **HDR** (Q. HDR, LR-EXP-HDR), **video** (R. Video), **Classic output modules** (Map view, Book, Slideshow module,
|
||||
Print, publish): large, well understood, lower priority than 1–5.
|
||||
8. **LR-VIEW-ZOOM** (P0): pinch zoom and two-finger pan now work, with stable image cursors between gesture events.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ pub const TABLE: &[(&str, Class)] = &[
|
|||
("tiff", Class::Layer(0)),
|
||||
("sysmem", Class::Layer(0)),
|
||||
("fetch", Class::Layer(0)),
|
||||
("denoise-core", Class::Layer(1)),
|
||||
("denoise", Class::Layer(1)),
|
||||
("raw", Class::Layer(1)),
|
||||
("codecs", Class::Layer(1)),
|
||||
|
|
@ -69,6 +70,7 @@ pub const TABLE: &[(&str, Class)] = &[
|
|||
pub const INTRA_LAYER_ORDER: &[&[&str]] = &[
|
||||
&["geom", "color", "raster"],
|
||||
&["tiff", "raster"],
|
||||
&["denoise-core", "denoise"],
|
||||
&["meta", "raw"],
|
||||
&["meta", "codecs"],
|
||||
&["meta", "develop"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue