Skip to content
Closed
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
f99dba7
Exception report 2
waiafur May 11, 2026
b9df9e6
Merge into fork
waiafur May 11, 2026
4326e3d
Merge pull request #3 from embedded-dev-research/main
waiafur May 12, 2026
420b279
Merge branch 'main' into main
waiafur May 12, 2026
e498fd2
fix: auto-prepare bundled OpenVINO model
allnes May 14, 2026
41f9229
small fix
waiafur May 14, 2026
dc2de68
yolo26 first implementation
waiafur May 14, 2026
b7e62a8
Merge pull request #7 from embedded-dev-research/main
waiafur May 14, 2026
74b0efc
CI attempt 1
waiafur May 15, 2026
4bac0a8
CI attempt 2
waiafur May 15, 2026
aeb2077
CI attempt 3
waiafur May 15, 2026
d261449
CI attempt 4
waiafur May 15, 2026
49914e6
CI attempt 5
waiafur May 15, 2026
720eb6e
CI attempt 6
waiafur May 15, 2026
6c62c62
CI attempt 7
waiafur May 15, 2026
610fb3d
context fix + Koin integration
waiafur May 17, 2026
6d159a6
Merge pull request #8 from embedded-dev-research/main
waiafur May 17, 2026
74338fe
Merge pull request #9 from embedded-dev-research/main
waiafur May 17, 2026
31538f8
test fix
waiafur May 17, 2026
910af33
conflicts with UI redesign resolved
waiafur May 17, 2026
743972c
Update YOLO download script
May 17, 2026
947edfa
ktlint fix
waiafur May 17, 2026
c51ea51
feat: auto-select YOLO model based on device specs
May 17, 2026
d4f11c0
delete finalize and aiModule split
waiafur May 17, 2026
8c73d0e
fix: add coroutines support and proper resource cleanup
May 17, 2026
ac2a7bf
Merge branch 'main' into patch-2
waiafur May 17, 2026
492c985
Merge pull request #11 from Salekh-A/patch-2
waiafur May 17, 2026
5a9c6cf
review fixes
waiafur May 17, 2026
d031cca
CI fix
waiafur May 17, 2026
030f80f
CI fix 2
waiafur May 17, 2026
783156a
test: add YOLO models copy verification test
May 18, 2026
e4c0bdf
quick test build fix
waiafur May 18, 2026
6472dc1
refactor: move AiModule to app module, remove FileSystemProvider, use…
May 18, 2026
919abfa
fix: update aiModule import path and test
May 18, 2026
93319cb
refactor: move AiModule to app layer, use Context directly
May 18, 2026
d598271
fix: update imports for aiModule
May 18, 2026
1c1e4eb
Merge branch 'main' into patch-2
waiafur May 19, 2026
a96167a
Merge pull request #13 from Salekh-A/patch-2
waiafur May 19, 2026
7fa8dbd
small fix
waiafur May 19, 2026
9b9c6a2
small fix
waiafur May 19, 2026
fa106fb
Revert "small fix"
waiafur May 19, 2026
30885db
Reapply "small fix"
waiafur May 19, 2026
7ccce77
Revert "Reapply "small fix""
waiafur May 19, 2026
869a178
Reapply "Reapply "small fix""
waiafur May 19, 2026
33ce1dc
Revert "Reapply "small fix""
waiafur May 19, 2026
b415845
Reapply "small fix"
waiafur May 19, 2026
9d143ca
Revert "small fix"
waiafur May 19, 2026
2c942a0
Revert "small fix"
waiafur May 19, 2026
65b98d3
Revert "Merge pull request #13 from Salekh-A/patch-2"
waiafur May 19, 2026
d449a55
Revert "Merge branch 'main' into patch-2"
waiafur May 19, 2026
14bddcd
revert succesful
waiafur May 19, 2026
b501ea4
delete app/AIModule
waiafur May 19, 2026
2278c48
FileSystemProvider to Context change in engine
waiafur May 19, 2026
7d84be0
merge with fork
waiafur May 19, 2026
962133d
.gitignore fix
waiafur May 19, 2026
0487421
test fix + version choosing implemented
waiafur May 19, 2026
b57c682
Merge pull request #16 from embedded-dev-research/main
waiafur May 19, 2026
a9a175e
quick fix
waiafur May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@
.cxx
local.properties
.idea/
/ai/libs/
/ai/src/main/jniLibs/arm64-v8a/
/ai/src/main/assets/
77 changes: 77 additions & 0 deletions ai/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,34 @@ android {

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
ndk {
abiFilters.add("arm64-v8a")
}
}

sourceSets {
getByName("main") {
jniLibs.srcDirs("src/main/jniLibs")
assets.srcDirs("src/main/assets", "$buildDir/generated/yolo26/assets")
}
getByName("androidTest") {
assets.srcDirs("src/androidTest/assets")
}
}
tasks.whenTaskAdded {
if (name.contains("merge") && name.contains("NativeLibs")) {
doLast {
println("=== Merged dirs for $name:")
fileTree("$buildDir/intermediates")
.filter { it.name.endsWith(".so") }
.forEach { println(it.parentFile.absolutePath) }
}
}
}
packaging {
jniLibs {
useLegacyPackaging = true
}
}

buildTypes {
Expand All @@ -32,6 +60,42 @@ android {
}
}

val prepareYolo26Model =
tasks.register<Exec>("prepareYolo26Model") {
val outputAssetsDir = layout.buildDirectory.dir("generated/yolo26/assets")
val workDir = layout.buildDirectory.dir("yolo26")
val script = layout.projectDirectory.file("scripts/prepare_yolo26_model.py")
val python =
if (System.getProperty("os.name").lowercase().contains("windows")) {
providers.environmentVariable("PYTHON").orElse("python")
} else {
providers.environmentVariable("PYTHON").orElse("python3")
}

inputs.file(script)
outputs.dir(outputAssetsDir)

commandLine(
python.get(),
script.asFile.absolutePath,
"--output-assets-dir",
outputAssetsDir.get().asFile.absolutePath,
"--work-dir",
workDir.get().asFile.absolutePath,
)
}

tasks
.matching {
it.name == "preBuild" || (it.name.startsWith("merge") && it.name.endsWith("Assets"))
}.configureEach {
dependsOn(prepareYolo26Model)
}

tasks.matching { it.name == "mergeDebugAndroidTestAssets" }.configureEach {
dependsOn("copyYoloToTestAssets")
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
Expand All @@ -42,8 +106,21 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation(project(":domain"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation(libs.koin.android)
}

tasks.whenTaskAdded {
if (name.contains("mergeDebugNativeLibs") || name.contains("mergeReleaseNativeLibs")) {
doLast {
copy {
from("$rootDir/ai/src/main/jniLibs/arm64-v8a/openvino-2026.2.0")
into("$buildDir/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/openvino-2026.2.0")
}
}
}
}
481 changes: 316 additions & 165 deletions ai/gradle.lockfile

Large diffs are not rendered by default.

137 changes: 137 additions & 0 deletions ai/scripts/prepare_yolo26_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""Prepare YOLO OpenVINO IR assets for the Android AI module."""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path

# Список моделей для подготовки
MODELS = [
{
"name": "yolo26n",
"pt": "yolo26n.pt",
"dir": "yolo26n_openvino_model",
"files": ("yolo26n.xml", "yolo26n.bin", "metadata.yaml"),
"export_args": {"end2end": True}
},
{
"name": "yolov10n",
"pt": "yolov10n.pt",
"dir": "yolov10n_openvino_model",
"files": ("yolov10n.xml", "yolov10n.bin", "metadata.yaml"),
"export_args": {}
}
]

def run(command: list[str], cwd: Path | None = None) -> None:
subprocess.run(command, cwd=cwd, check=True)

def venv_python(venv_dir: Path) -> Path:
if os.name == "nt":
return venv_dir / "Scripts" / "python.exe"
return venv_dir / "bin" / "python"

def ensure_venv(venv_dir: Path) -> Path:
python = venv_python(venv_dir)
if not python.exists():
run([sys.executable, "-m", "venv", str(venv_dir)])
run([str(python), "-m", "pip", "install", "--upgrade", "pip"])
return python

def ensure_python_packages(python: Path) -> None:
check_code = "import ultralytics, openvino"
result = subprocess.run([str(python), "-c", check_code], check=False)
if result.returncode == 0:
return
run([str(python), "-m", "pip", "install", "-U", "ultralytics", "openvino"])

def export_model(python: Path, work_dir: Path, model: dict) -> Path:
"""Экспорт модели в OpenVINO"""

# Формируем аргументы экспорта
export_args_str = ", ".join([f"{k}={v}" for k, v in model["export_args"].items()])
if export_args_str:
export_args_str = ", " + export_args_str

export_code = f"""
from ultralytics import YOLO
model = YOLO("{model['pt']}")
model.export(format="openvino", imgsz=640, batch=1, dynamic=False{export_args_str})
"""
run([str(python), "-c", export_code], cwd=work_dir)

model_dir = work_dir / model["dir"]
missing = [name for name in model["files"] if not (model_dir / name).exists()]
if missing:
raise FileNotFoundError(f"Ultralytics export did not produce expected files for {model['name']}: {missing}")
return model_dir

def write_coco_names(python: Path, work_dir: Path, output_file: Path) -> None:
"""Записываем coco.names (используем первую модель для получения имён)"""
first_model = MODELS[0]
names_code = f"""
from pathlib import Path
from ultralytics import YOLO
model = YOLO("{first_model['pt']}")
names = model.names
Path(r"{output_file}").write_text(
"\\n".join(names[i] for i in range(len(names))),
encoding="utf-8",
)
"""
run([str(python), "-c", names_code], cwd=work_dir)

def verify_openvino_model(python: Path, model_xml: Path, model_name: str) -> None:
verify_code = f"""
from openvino import Core
core = Core()
model = core.read_model(r"{model_xml}")
compiled = core.compile_model(model, "CPU")
print("Prepared OpenVINO model for {model_name}")
for inp in compiled.inputs:
print("input:", inp.shape, inp.element_type)
for out in compiled.outputs:
print("output:", out.shape, out.element_type)
"""
run([str(python), "-c", verify_code])

def copy_assets(model_dir: Path, output_assets_dir: Path, model: dict) -> None:
output_model_dir = output_assets_dir / "models" / model["dir"]
output_model_dir.mkdir(parents=True, exist_ok=True)
for name in model["files"]:
shutil.copy2(model_dir / name, output_model_dir / name)

def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--output-assets-dir", required=True, type=Path)
parser.add_argument("--work-dir", required=True, type=Path)
return parser.parse_args()

def main() -> None:
args = parse_args()
args.output_assets_dir.mkdir(parents=True, exist_ok=True)
args.work_dir.mkdir(parents=True,exist_ok=True)

python = ensure_venv(args.work_dir / ".venv")
ensure_python_packages(python)

# Записываем coco.names один раз
write_coco_names(python, args.work_dir, args.output_assets_dir / "coco.names")

# Подготавливаем каждую модель
for model in MODELS:
print(f"\n{'='*50}")
print(f"Preparing {model['name']}...")
print(f"{'='*50}")

model_dir = export_model(python, args.work_dir, model)
copy_assets(model_dir, args.output_assets_dir, model)
verify_openvino_model(python, args.output_assets_dir / "models" / model["dir"] / f"{model['name']}.xml", model['name'])

print(f"[OK] {model['name']} done!")

if __name__ == "__main__":
main()
22 changes: 0 additions & 22 deletions ai/src/androidTest/java/com/itlab/ai/ExampleInstrumentedTest.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.itlab.ai

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.itlab.domain.app.FileSystemProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import java.io.InputStream

@RunWith(AndroidJUnit4::class)
class OpenVinoAiLayerInstrumentedTest {
private val engines = mutableListOf<OpenVinoEngine>()

@After
fun tearDown() {
engines.forEach { it.release() }
engines.clear()
}

private fun createEngine(modelPath: String = ""): OpenVinoEngine {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val fileSystem = TestFileSystemProvider(context)
return OpenVinoEngine(fileSystem, modelPath).also { engines.add(it) }
}

@Test
fun summarize_returnsTrimmedSummary() =
runBlocking {
val engine = createEngine()
engine.initialize() // Явно инициализируем
val service = OpenVinoNoteAiService(engine, ResultProcessor())
val result = service.summarize(" Summary text ")
assertEquals("Summary text", result)
}

@Test
fun tagTXT_normalizesCaseAndSeparators() =
runBlocking {
val engine = createEngine()
engine.initialize()
val service = OpenVinoNoteAiService(engine, ResultProcessor())
val result = service.tagTXT(" Kotlin, Notes\nAI ")
assertEquals(setOf("kotlin", "notes", "ai"), result)
}

@Test
fun tagIMGs_aggregatesAndDeduplicatesTags() =
runBlocking {
val processor = ResultProcessor()
val result = processor.normalizeTags(("Cat, Pet, pet, animal, CAT"))
assertEquals(setOf("cat", "pet", "animal"), result)
}

@Test
fun initialize_returnsFalseWhenModelIsMissing() =
runBlocking {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val missingModel = File(context.filesDir, "missing-model.xml")
val engine = createEngine(missingModel.absolutePath)

val initialized =
withContext(Dispatchers.Default) {
engine.initialize()
}

assertFalse(initialized)
}

@Test
fun initialize_loadsBundledYoloModel() =
runBlocking {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val copiedModel = File(context.filesDir, "models/yolo26n_openvino_model/yolo26n.xml")
val engine = createEngine()

val initialized =
withContext(Dispatchers.Default) {
engine.initialize()
}

assertTrue(copiedModel.exists())
assertTrue(initialized)
assertTrue(engine.isReady())
}

private class TestFileSystemProvider(
private val context: android.content.Context,
) : FileSystemProvider {
override fun openAsset(path: String): InputStream = context.assets.open(path)

override fun listAssets(path: String): Array<String> = context.assets.list(path) ?: emptyArray()

override fun getFilesDir(): File = context.filesDir

override fun getTotalRamMB(): Long = 1024
}
}
5 changes: 5 additions & 0 deletions ai/src/main/assets/plugins.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ie>
<plugins>
<plugin name="CPU" location="libopenvino_arm_cpu_plugin.so" />
</plugins>
</ie>
Loading
Loading