diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e476299bd..38eba2ee1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,11 +11,11 @@ env: jobs: build: + name: ${{ matrix.os }} strategy: matrix: os: [macos-latest, windows-latest] fail-fast: false - name: ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5e44f66bd..3cf8bccb9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,32 +1,35 @@ -name: Lint - -on: - push: - branches: ['main'] - pull_request: - -env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - lint: - runs-on: ubuntu-latest - steps: - # refer: https://v2.tauri.app/start/prerequisites/#linux - - run: | - sudo apt update - sudo apt install libwebkit2gtk-4.1-dev \ - build-essential \ - curl \ - wget \ - file \ - libxdo-dev \ - libssl-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev - - uses: actions/checkout@v6 - - uses: Swatinem/rust-cache@v2 - - uses: reviewdog/action-setup@v1 - - run: cargo check -q --message-format=short | reviewdog -f cargo-check -reporter=github-check - - run: cargo clippy -q --message-format=short | reviewdog -f clippy -reporter=github-check - - uses: bnjbvr/cargo-machete@main +name: Lint + +on: + push: + branches: ['main'] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + lint: + name: Lint + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: ubuntu-latest + steps: + # refer: https://v2.tauri.app/start/prerequisites/#linux + - run: | + sudo apt update + sudo apt install libwebkit2gtk-4.1-dev \ + build-essential \ + curl \ + wget \ + file \ + libxdo-dev \ + libssl-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev + - uses: actions/checkout@v6 + - uses: Swatinem/rust-cache@v2 + - uses: reviewdog/action-setup@v1 + - run: cargo check -q --message-format=short | reviewdog -f cargo-check -reporter=github-check + - run: cargo clippy -q --message-format=short | reviewdog -f clippy -reporter=github-check + - uses: bnjbvr/cargo-machete@main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..000a55fb9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: Test + +on: + push: + branches: ['main'] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Integration tests + runs-on: ubuntu-latest + steps: + # refer: https://v2.tauri.app/start/prerequisites/#linux + - run: | + sudo apt update + sudo apt install libwebkit2gtk-4.1-dev \ + build-essential \ + curl \ + wget \ + file \ + libxdo-dev \ + libssl-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + fonts-noto-cjk + - uses: actions/checkout@v6 + - uses: Swatinem/rust-cache@v2 + - run: cargo test --no-run + - run: cargo test --workspace --tests diff --git a/koharu-ml/src/lama/fft/cpu.rs b/koharu-ml/src/lama/fft/cpu.rs index 170d9629a..7388f2ea8 100644 --- a/koharu-ml/src/lama/fft/cpu.rs +++ b/koharu-ml/src/lama/fft/cpu.rs @@ -104,16 +104,21 @@ pub fn irfft2(storage: &CpuStorage, layout: &Layout, width: usize) -> Result<(Cp let base = x * 2; *c = Complex32::new(row[base], row[base + 1]); } - for x in 1..w_half { - let mirror = width - x; - dst_row[mirror] = dst_row[x].conj(); - } } - - for row in buffer.chunks_exact_mut(width) { - ifft_w.process(row); + // Reconstruct the full complex spectrum using the conjugate symmetry + // property of real-valued signals: F(-ky, -kx) = conj(F(ky, kx)). + for ky in 0..height { + let mirror_ky = if ky == 0 { 0 } else { height - ky }; + let row_start = ky * width; + let mirror_row_start = mirror_ky * width; + for kx in 1..w_half { + let mirror_kx = width - kx; + buffer[row_start + mirror_kx] = buffer[mirror_row_start + kx].conj(); + } } + // Apply the inverse transform in the reverse order of the forward pass: + // first undo the height transform, then undo the width transform. for x in 0..width { for (dst, src) in col_buffer .iter_mut() @@ -132,6 +137,10 @@ pub fn irfft2(storage: &CpuStorage, layout: &Layout, width: usize) -> Result<(Cp } } + for row in buffer.chunks_exact_mut(width) { + ifft_w.process(row); + } + let out_plane = &mut dst[bc * plane_out_stride..(bc + 1) * plane_out_stride]; for (out, val) in out_plane.iter_mut().zip(buffer.iter()) { *out = val.re; diff --git a/koharu-ml/tests/comic_text_detector.rs b/koharu-ml/tests/comic_text_detector.rs index 07336fe0c..22088c556 100644 --- a/koharu-ml/tests/comic_text_detector.rs +++ b/koharu-ml/tests/comic_text_detector.rs @@ -1,11 +1,10 @@ use std::path::Path; -use koharu_ml::{comic_text_detector::ComicTextDetector, device}; +use koharu_ml::comic_text_detector::ComicTextDetector; #[tokio::test] async fn comic_text_detector() -> anyhow::Result<()> { - let device = device(true)?; - let model = ComicTextDetector::load(device).await?; + let model = ComicTextDetector::load(false).await?; let img = image::open(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/1.jpg"))?; let (boxes, mask) = model.inference(&img)?; diff --git a/koharu-ml/tests/fixtures/dialog.jpg b/koharu-ml/tests/fixtures/dialog.jpg new file mode 100644 index 000000000..a6cd00253 Binary files /dev/null and b/koharu-ml/tests/fixtures/dialog.jpg differ diff --git a/koharu-ml/tests/fixtures/image.jpg b/koharu-ml/tests/fixtures/image.jpg new file mode 100644 index 000000000..79159e949 Binary files /dev/null and b/koharu-ml/tests/fixtures/image.jpg differ diff --git a/koharu-ml/tests/fixtures/mask.png b/koharu-ml/tests/fixtures/mask.png new file mode 100644 index 000000000..4acce5e61 Binary files /dev/null and b/koharu-ml/tests/fixtures/mask.png differ diff --git a/koharu-ml/tests/inpaint.rs b/koharu-ml/tests/inpaint.rs new file mode 100644 index 000000000..5186a1206 --- /dev/null +++ b/koharu-ml/tests/inpaint.rs @@ -0,0 +1,36 @@ +use std::path::Path; + +use koharu_ml::lama::Lama; +use image::GenericImageView; + +#[tokio::test] +async fn lama_inpainting_updates_masked_region() -> anyhow::Result<()> { + let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); + + let lama = Lama::load(false).await?; + let base = image::open(fixtures.join("image.jpg"))?; + let mask = image::open(fixtures.join("mask.png"))?; + + let output = lama.inference(&base, &mask)?; + + assert_eq!(output.dimensions(), base.dimensions()); + + let mask = mask.to_luma8(); + let base = base.to_rgb8(); + let output = output.to_rgb8(); + + let mut changed = false; + for ((mask_px, base_px), out_px) in mask + .pixels() + .zip(base.pixels()) + .zip(output.pixels()) + { + if mask_px.0[0] > 0 && base_px.0 != out_px.0 { + changed = true; + break; + } + } + + assert!(changed, "inpainting should change at least one masked pixel"); + Ok(()) +} diff --git a/koharu-ml/tests/llm.rs b/koharu-ml/tests/llm.rs new file mode 100644 index 000000000..b85f27e97 --- /dev/null +++ b/koharu-ml/tests/llm.rs @@ -0,0 +1,29 @@ +use koharu_ml::llm::{GenerateOptions, Llm, ModelId}; +use strum::IntoEnumIterator; + +#[tokio::test] +async fn llm_generates_text_for_all_models() -> anyhow::Result<()> { + let prompt = "こんにちは、テストです。"; + + for model in ModelId::iter() { + let mut llm = Llm::load(model, false).await?; + let opts = GenerateOptions { + max_tokens: 32, + temperature: 0.0, + top_k: None, + top_p: None, + seed: 1, + split_prompt: false, + repeat_penalty: 1.0, + repeat_last_n: 64, + }; + + let generated = llm.generate(prompt, &opts)?; + assert!( + !generated.trim().is_empty(), + "model {model:?} should return some text" + ); + } + + Ok(()) +} diff --git a/koharu-ml/tests/ocr.rs b/koharu-ml/tests/ocr.rs new file mode 100644 index 000000000..c4f39ebb3 --- /dev/null +++ b/koharu-ml/tests/ocr.rs @@ -0,0 +1,20 @@ +use std::path::Path; + +use koharu_ml::manga_ocr::MangaOcr; + +#[tokio::test] +async fn manga_ocr_reads_dialog_image() -> anyhow::Result<()> { + let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); + let image = image::open(fixtures.join("dialog.jpg"))?; + + let ocr = MangaOcr::load(false).await?; + let results = ocr.inference(&[image])?; + + assert_eq!(results.len(), 1); + assert!( + !results[0].trim().is_empty(), + "OCR result should contain text" + ); + + Ok(()) +}