Functions/GenXdev.AI.Queries/Export-ImageDatabase.ps1
################################################################################ <# .SYNOPSIS Initializes and populates the SQLite database by discovering images directly. .DESCRIPTION Creates a SQLite database with optimized schema for fast image searching based on metadata including keywords, people, objects, scenes, and descriptions. The function always deletes any existing database file and creates a fresh one, discovers images using Find-Image from specified directories or configured image directories, and populates the database directly without requiring a metadata JSON file. Finally, it creates indexes for optimal performance. .PARAMETER InputObject Accepts search results from a Find-Image call to regenerate the view. .PARAMETER DatabaseFilePath Path to the SQLite database file. If not specified, uses the default location under Storage\allimages.meta.db. .PARAMETER ImageDirectories Array of directory paths to search for images. If not specified, uses the configured image directories from Get-AIImageCollection. .PARAMETER EmbedImages Embed images directly into the database. .EXAMPLE Export-ImageDatabase -DatabaseFilePath "C:\Custom\Path\images.db" -ImageDirectories @("C:\Photos", "D:\Images") -EmbedImages .EXAMPLE indexcachedimages #> function Export-ImageDatabase { [CmdletBinding()] [Alias("indexcachedimages", "Inititalize-ImageDatabase", "Recreate-ImageIndex")] param( [Parameter( Position = 0, Mandatory = $false, ValueFromPipeline = $true, HelpMessage = ("Accepts search results from a Find-Image " + "call to regenerate the view.") )] [object[]] $InputObject, ############################################################################### [Parameter( Position = 0, Mandatory = $false, HelpMessage = "The path to the image database file. If not specified, a default path is used." )] [string] $DatabaseFilePath, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Array of directory paths to search for images" )] [ValidateNotNullOrEmpty()] [Alias("imagespath", "directories", "imgdirs", "imagedirectory")] [string[]] $ImageDirectories, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = ( "Array of directory path-like search strings to filter images by " + "path (SQL LIKE patterns, e.g. '%\\2024\\%')" ) )] [string[]] $PathLike = @(), ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Language for descriptions and keywords." )] [ValidateSet( "Afrikaans", "Akan", "Albanian", "Amharic", "Arabic", "Armenian", "Azerbaijani", "Basque", "Belarusian", "Bemba", "Bengali", "Bihari", "Bork, bork, bork!", "Bosnian", "Breton", "Bulgarian", "Cambodian", "Catalan", "Cherokee", "Chichewa", "Chinese (Simplified)", "Chinese (Traditional)", "Corsican", "Croatian", "Czech", "Danish", "Dutch", "Elmer Fudd", "English", "Esperanto", "Estonian", "Ewe", "Faroese", "Filipino", "Finnish", "French", "Frisian", "Ga", "Galician", "Georgian", "German", "Greek", "Guarani", "Gujarati", "Hacker", "Haitian Creole", "Hausa", "Hawaiian", "Hebrew", "Hindi", "Hungarian", "Icelandic", "Igbo", "Indonesian", "Interlingua", "Irish", "Italian", "Japanese", "Javanese", "Kannada", "Kazakh", "Kinyarwanda", "Kirundi", "Klingon", "Kongo", "Korean", "Krio (Sierra Leone)", "Kurdish", "Kurdish (Soranî)", "Kyrgyz", "Laothian", "Latin", "Latvian", "Lingala", "Lithuanian", "Lozi", "Luganda", "Luo", "Macedonian", "Malagasy", "Malay", "Malayalam", "Maltese", "Maori", "Marathi", "Mauritian Creole", "Moldavian", "Mongolian", "Montenegrin", "Nepali", "Nigerian Pidgin", "Northern Sotho", "Norwegian", "Norwegian (Nynorsk)", "Occitan", "Oriya", "Oromo", "Pashto", "Persian", "Pirate", "Polish", "Portuguese (Brazil)", "Portuguese (Portugal)", "Punjabi", "Quechua", "Romanian", "Romansh", "Runyakitara", "Russian", "Scots Gaelic", "Serbian", "Serbo-Croatian", "Sesotho", "Setswana", "Seychellois Creole", "Shona", "Sindhi", "Sinhalese", "Slovak", "Slovenian", "Somali", "Spanish", "Spanish (Latin American)", "Sundanese", "Swahili", "Swedish", "Tajik", "Tamil", "Tatar", "Telugu", "Thai", "Tigrinya", "Tonga", "Tshiluba", "Tumbuka", "Turkish", "Turkmen", "Twi", "Uighur", "Ukrainian", "Urdu", "Uzbek", "Vietnamese", "Welsh", "Wolof", "Xhosa", "Yiddish", "Yoruba", "Zulu" )] [string] $Language, ####################################################################### [parameter( Mandatory = $false, HelpMessage = ("The directory containing face images organized by " + "person folders. If not specified, uses the " + "configured faces directory preference.") )] [string] $FacesDirectory, ####################################################################### [Parameter( Mandatory = $false, HelpMessage = "Embed images as base64." )] [switch] $EmbedImages, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Force rebuild of the image index database." )] [switch] $ForceIndexRebuild, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Switch to disable fallback behavior." )] [switch] $NoFallback, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = "Switch to skip database initialization and rebuilding." )] [switch] $NeverRebuild ############################################################################### ) begin { # determine database file path if not provided $params = GenXdev.Helpers\Copy-IdenticalParamValues ` -BoundParameters $PSBoundParameters ` -FunctionName "GenXdev.AI\Get-ImageDatabasePath" ` -DefaultValues ( Microsoft.PowerShell.Utility\Get-Variable -Scope Local -ErrorAction SilentlyContinue ) $DatabaseFilePath = GenXdev.AI\Get-ImageDatabasePath @params -NeverRebuild # retrieve configured image directories if not provided $ImageDirectories = GenXdev.AI\Get-AIImageCollection -ImageDirectories $ImageDirectories # output that the image index database is being recreated Microsoft.PowerShell.Utility\Write-Host @" Recreating image index database Path = $DatabaseFilePath") Image directories = $(($ImageDirectories -join ", ")) "@ -ForegroundColor Cyan # output the directories being used for image discovery Microsoft.PowerShell.Utility\Write-Verbose ( "Directories:`r`n$(($ImageDirectories | Microsoft.PowerShell.Utility\ConvertTo-Json))" ) # output whether image embedding is enabled or disabled if ($EmbedImages) { Microsoft.PowerShell.Utility\Write-Verbose ( "Image embedding: ENABLED - Images will be stored as binary data in database" ) } else { Microsoft.PowerShell.Utility\Write-Verbose ( "Image embedding: DISABLED - Only file paths will be stored" ) } # define schema version constant $SCHEMA_VERSION = "1.0.0.3" # initialize info object for tracking found results $Info = @{ FoundResults = $false TotalImages = 0 } if ([String]::IsNullOrWhiteSpace($DatabaseFilePath)) { Microsoft.PowerShell.Utility\Write-Error ( "Failed to retrieve database file path." ) return } try { [System.Data.SQLite.SQLiteConnection]::Shutdown() } catch { Microsoft.PowerShell.Utility\Write-Verbose ( "Failed to shutdown SQLite connection: $($_.Exception.Message)" ) } Microsoft.PowerShell.Utility\Write-Verbose ( "Using image database: $DatabaseFilePath" ) $DatabaseFilePath = GenXdev.FileSystem\Expand-Path ( $DatabaseFilePath ) -CreateDirectory -DeleteExistingFile -ErrorAction SilentlyContinue # if the database file exists, move it to backup if ([IO.File]::Exists($DatabaseFilePath)) { # try to move the file to backup, swap if move fails if (-not (GenXdev.FileSystem\Move-ItemWithTracking $DatabaseFilePath $DatabaseBackupFilePath -Force)) { $tmp = $DatabaseFilePath $DatabaseFilePath = $DatabaseBackupFilePath $DatabaseBackupFilePath = $tmp } else { # move the *-journal file as wel $journalFilePath = "$DatabaseFilePath-journal" if ([IO.File]::Exists($journalFilePath)) { if (-not (GenXdev.FileSystem\Move-ItemWithTracking $journalFilePath "$DatabaseBackupFilePath-journal" -Force)) { Microsoft.PowerShell.Utility\Write-Warning ( "Failed to move journal file: $journalFilePath" ) # move renamed file back if (-not (GenXdev.FileSystem\Move-ItemWithTracking -Path $DatabaseBackupFilePath -Destination $DatabaseFilePath -Force -ErrorAction SilentlyContinue)) { Microsoft.PowerShell.Utility\Write-Warning ( "Failed to restore original database file: $DatabaseFilePath" ) } else { Microsoft.PowerShell.Utility\Write-Verbose ( "Restored original database file: $DatabaseFilePath" ) } $tmp = $DatabaseFilePath $DatabaseFilePath = $DatabaseBackupFilePath $DatabaseBackupFilePath = $tmp } } } } # output verbose information about database and metadata paths Microsoft.PowerShell.Utility\Write-Verbose ( "Database path: $DatabaseFilePath" ) Microsoft.PowerShell.Utility\Write-Verbose ( "Metadata path: $(($ImageDirectories | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10))" ) # define table creation scripts for database schema $createImagesTable = @" CREATE TABLE IF NOT EXISTS Images ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL, image_data BLOB, has_explicit_content BOOLEAN DEFAULT 0, has_nudity BOOLEAN DEFAULT 0, short_description TEXT, long_description TEXT, picture_type TEXT, overall_mood_of_image TEXT, style_type TEXT, description_keywords TEXT, people_count INTEGER DEFAULT 0, people_faces TEXT, people_json TEXT, objects_count INTEGER DEFAULT 0, objects_list TEXT, objects_json TEXT, object_counts TEXT, scene_label TEXT, scene_confidence REAL, scene_confidence_percentage REAL, scene_processed_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); "@ # create keywords lookup table for fast searching $createKeywordsTable = @" CREATE TABLE IF NOT EXISTS ImageKeywords ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_id INTEGER NOT NULL, keyword TEXT NOT NULL, FOREIGN KEY (image_id) REFERENCES Images(id) ON DELETE CASCADE ); "@ # create people lookup table $createPeopleTable = @" CREATE TABLE IF NOT EXISTS ImagePeople ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_id INTEGER NOT NULL, person_name TEXT NOT NULL, FOREIGN KEY (image_id) REFERENCES Images(id) ON DELETE CASCADE ); "@ # create objects lookup table $createObjectsTable = @" CREATE TABLE IF NOT EXISTS ImageObjects ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_id INTEGER NOT NULL, object_name TEXT NOT NULL, object_count INTEGER DEFAULT 1, FOREIGN KEY (image_id) REFERENCES Images(id) ON DELETE CASCADE ); "@ # create scenes lookup table $createScenesTable = @" CREATE TABLE IF NOT EXISTS ImageScenes ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_id INTEGER NOT NULL, scene_name TEXT NOT NULL, confidence REAL, FOREIGN KEY (image_id) REFERENCES Images(id) ON DELETE CASCADE ); "@ # create schema version table $createSchemaVersionTable = @" CREATE TABLE IF NOT EXISTS ImageSchemaVersion ( id INTEGER PRIMARY KEY CHECK (id = 1), version TEXT NOT NULL ); "@ # create indexes for super fast searching with NO TABLE SCANS $createIndexes = @" -- =================================================================== -- PRIMARY SINGLE-COLUMN INDEXES (Most frequently used filters) -- =================================================================== -- Path index for unique lookups and sorting (most common operation) CREATE INDEX IF NOT EXISTS idx_images_path ON Images(path); -- Content safety filters (very frequently used together) CREATE INDEX IF NOT EXISTS idx_images_nudity ON Images(has_nudity); CREATE INDEX IF NOT EXISTS idx_images_explicit ON Images(has_explicit_content); -- Core categorical filters CREATE INDEX IF NOT EXISTS idx_images_picture_type ON Images(picture_type); CREATE INDEX IF NOT EXISTS idx_images_mood ON Images(overall_mood_of_image); CREATE INDEX IF NOT EXISTS idx_images_style ON Images(style_type); CREATE INDEX IF NOT EXISTS idx_images_scene_label ON Images(scene_label); -- Add regular indexes for text search columns (for LIKE/GLOB) CREATE INDEX IF NOT EXISTS idx_images_short_description ON Images(short_description); CREATE INDEX IF NOT EXISTS idx_images_long_description ON Images(long_description); CREATE INDEX IF NOT EXISTS idx_images_description_keywords ON Images(description_keywords); -- Count filters for performance CREATE INDEX IF NOT EXISTS idx_images_people_count ON Images(people_count); CREATE INDEX IF NOT EXISTS idx_images_objects_count ON Images(objects_count); -- =================================================================== -- COMPOSITE INDEXES FOR COMMON FILTER COMBINATIONS -- =================================================================== -- Safety + Type combinations (very common search pattern) CREATE INDEX IF NOT EXISTS idx_images_safety_type ON Images(has_nudity, has_explicit_content, picture_type); CREATE INDEX IF NOT EXISTS idx_images_safety_mood ON Images(has_nudity, has_explicit_content, overall_mood_of_image); CREATE INDEX IF NOT EXISTS idx_images_safety_style ON Images(has_nudity, has_explicit_content, style_type); -- Type + Mood + Style combinations (aesthetic searches) CREATE INDEX IF NOT EXISTS idx_images_aesthetic ON Images(picture_type, overall_mood_of_image, style_type); -- Count-based composite indexes (for filtering by presence of people/objects) CREATE INDEX IF NOT EXISTS idx_images_counts_safety ON Images(people_count, objects_count, has_nudity, has_explicit_content); -- Scene confidence optimization CREATE INDEX IF NOT EXISTS idx_images_scene_confidence ON Images(scene_label, scene_confidence); -- =================================================================== -- COVERING INDEXES TO AVOID TABLE LOOKUPS -- =================================================================== -- Covering index for basic search results (includes most commonly selected columns) CREATE INDEX IF NOT EXISTS idx_images_covering_basic ON Images( path, has_nudity, has_explicit_content, picture_type, overall_mood_of_image, style_type, people_count, objects_count, scene_label, scene_confidence, short_description, long_description, description_keywords ); -- =================================================================== -- LOOKUP TABLE INDEXES FOR JOINS (Optimized FOR exact matches first) -- =================================================================== -- Keywords table optimization - exact matches first, then prefix matches CREATE INDEX IF NOT EXISTS idx_keywords_exact ON ImageKeywords(keyword, image_id); CREATE INDEX IF NOT EXISTS idx_keywords_prefix ON ImageKeywords(keyword COLLATE NOCASE, image_id); CREATE INDEX IF NOT EXISTS idx_keywords_reverse ON ImageKeywords(image_id, keyword); -- People table optimization - exact matches first, then prefix matches CREATE INDEX IF NOT EXISTS idx_people_exact ON ImagePeople(person_name, image_id); CREATE INDEX IF NOT EXISTS idx_people_prefix ON ImagePeople(person_name COLLATE NOCASE, image_id); CREATE INDEX IF NOT EXISTS idx_people_reverse ON ImagePeople(image_id, person_name); -- Objects table optimization - exact matches first, then prefix matches CREATE INDEX IF NOT EXISTS idx_objects_exact ON ImageObjects(object_name, image_id, object_count); CREATE INDEX IF NOT EXISTS idx_objects_prefix ON ImageObjects(object_name COLLATE NOCASE, image_id, object_count); CREATE INDEX IF NOT EXISTS idx_objects_reverse ON ImageObjects(image_id, object_name, object_count); -- Scenes table optimization - exact matches first, then prefix matches CREATE INDEX IF NOT EXISTS idx_scenes_exact ON ImageScenes(scene_name, image_id, confidence); CREATE INDEX IF NOT EXISTS idx_scenes_prefix ON ImageScenes(scene_name COLLATE NOCASE, image_id, confidence); CREATE INDEX IF NOT EXISTS idx_scenes_reverse ON ImageScenes(image_id, scene_name, confidence); CREATE INDEX IF NOT EXISTS idx_scenes_confidence ON ImageScenes(confidence, scene_name); -- =================================================================== -- PARTIAL INDEXES FOR SELECTIVE FILTERING (NO TABLE SCANS) -- =================================================================== -- Only index images with nudity (for faster nudity searches) CREATE INDEX IF NOT EXISTS idx_images_has_nudity ON Images(path, picture_type, overall_mood_of_image) WHERE has_nudity = 1; -- Only index images with explicit content CREATE INDEX IF NOT EXISTS idx_images_has_explicit ON Images(path, picture_type, overall_mood_of_image) WHERE has_explicit_content = 1; -- Only index family-safe images (no nudity, no explicit content) CREATE INDEX IF NOT EXISTS idx_images_family_safe ON Images(path, picture_type, overall_mood_of_image, style_type) WHERE has_nudity = 0 AND has_explicit_content = 0; -- Only index images with people (people_count > 0) CREATE INDEX IF NOT EXISTS idx_images_with_people ON Images(path, people_count, picture_type) WHERE people_count > 0; -- Only index images with objects (objects_count > 0) CREATE INDEX IF NOT EXISTS idx_images_with_objects ON Images(path, objects_count, picture_type) WHERE objects_count > 0; -- =================================================================== -- PREFIX SEARCH OPTIMIZATION (AVOID LEADING WILDCARD TABLE SCANS) -- =================================================================== -- For efficient prefix searches without leading wildcards CREATE INDEX IF NOT EXISTS idx_keywords_prefix_only ON ImageKeywords( CASE WHEN keyword GLOB '[A-Za-z]*' THEN substr(keyword, 1, 3) END, keyword, image_id ) WHERE keyword GLOB '[A-Za-z]*'; CREATE INDEX IF NOT EXISTS idx_people_prefix_only ON ImagePeople( CASE WHEN person_name GLOB '[A-Za-z]*' THEN substr(person_name, 1, 3) END, person_name, image_id ) WHERE person_name GLOB '[A-Za-z]*'; CREATE INDEX IF NOT EXISTS idx_objects_prefix_only ON ImageObjects( CASE WHEN object_name GLOB '[A-Za-z]*' THEN substr(object_name, 1, 3) END, object_name, image_id ) WHERE object_name GLOB '[A-Za-z]*'; CREATE INDEX IF NOT EXISTS idx_scenes_prefix_only ON ImageScenes( CASE WHEN scene_name GLOB '[A-Za-z]*' THEN substr(scene_name, 1, 3) END, scene_name, image_id ) WHERE scene_name GLOB '[A-Za-z]*'; -- =================================================================== -- CASE-INSENSITIVE SEARCH OPTIMIZATION (COLLATION BASED) -- =================================================================== -- Main table case-insensitive searches using collation (more efficient than lower()) CREATE INDEX IF NOT EXISTS idx_images_type_nocase ON Images(picture_type COLLATE NOCASE); CREATE INDEX IF NOT EXISTS idx_images_mood_nocase ON Images(overall_mood_of_image COLLATE NOCASE); CREATE INDEX IF NOT EXISTS idx_images_style_nocase ON Images(style_type COLLATE NOCASE); CREATE INDEX IF NOT EXISTS idx_images_scene_nocase ON Images(scene_label COLLATE NOCASE); -- =================================================================== -- SPECIALIZED INDEXES FOR COMPLEX QUERIES -- =================================================================== -- Multi-table join optimization with covering columns CREATE INDEX IF NOT EXISTS idx_images_join_optimize ON Images( id, path, has_nudity, has_explicit_content, picture_type, overall_mood_of_image, style_type, people_count, objects_count ); -- Range queries on confidence scores CREATE INDEX IF NOT EXISTS idx_images_scene_confidence_range ON Images(scene_confidence, scene_label, path) WHERE scene_confidence > 0; "@ } process { # start stopwatch for timing the process $totalTime = [System.Diagnostics.Stopwatch]::StartNew() # always delete existing database file to ensure clean rebuild if (Microsoft.PowerShell.Management\Test-Path $DatabaseFilePath) { Microsoft.PowerShell.Utility\Write-Verbose ( "Deleting existing database file for clean rebuild..." ) Microsoft.PowerShell.Management\Remove-Item $DatabaseFilePath -Force } # create new database Microsoft.PowerShell.Utility\Write-Verbose "Creating new database..." GenXdev.Data\New-SQLiteDatabase -DatabaseFilePath $DatabaseFilePath # create tables (without indexes initially for faster inserts) Microsoft.PowerShell.Utility\Write-Verbose "Creating database tables..." $createTablesQueries = @( $createImagesTable, $createKeywordsTable, $createPeopleTable, $createObjectsTable, $createScenesTable, $createSchemaVersionTable ) GenXdev.Data\Invoke-SQLiteQuery -DatabaseFilePath $DatabaseFilePath -Queries $createTablesQueries # get images using Find-Image for direct integration Microsoft.PowerShell.Utility\Write-Verbose "Discovering images using Find-Image..." # create transaction for batch operations $params = GenXdev.Helpers\Copy-IdenticalParamValues ` -BoundParameters $PSBoundParameters ` -FunctionName "GenXdev.Data\Get-SQLiteTransaction" ` -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable -Scope Local -ErrorAction SilentlyContinue) $transaction = GenXdev.Data\Get-SQLiteTransaction @params # prepare image insertion query $insertQuery = if ($EmbedImages) { "INSERT INTO Images (path, image_data, has_explicit_content, has_nudity, short_description, long_description, picture_type, overall_mood_of_image, style_type, description_keywords, people_count, people_faces, people_json, objects_count, objects_list, objects_json, object_counts, scene_label, scene_confidence, scene_confidence_percentage, scene_processed_at) VALUES (@path, @image_data, @explicit, @nudity, @short_desc, @long_desc, @pic_type, @mood, @style, @desc_keywords, @people_count, @people_faces, @people_json, @objects_count, @objects_list, @objects_json, @object_counts, @scene_label, @scene_confidence, @scene_conf_pct, @scene_processed)" } else { "INSERT INTO Images (path, has_explicit_content, has_nudity, short_description, long_description, picture_type, overall_mood_of_image, style_type, description_keywords, people_count, people_faces, people_json, objects_count, objects_list, objects_json, object_counts, scene_label, scene_confidence, scene_confidence_percentage, scene_processed_at) VALUES (@path, @explicit, @nudity, @short_desc, @long_desc, @pic_type, @mood, @style, @desc_keywords, @people_count, @people_faces, @people_json, @objects_count, @objects_list, @objects_json, @object_counts, @scene_label, @scene_confidence, @scene_conf_pct, @scene_processed)" } function ImportImages { param($Info) # set found results to true when images are found $Info.FoundResults = $true # copy identical parameter values for Find-Image $findImageParams = GenXdev.Helpers\Copy-IdenticalParamValues ` -BoundParameters $PSBoundParameters ` -FunctionName "GenXdev.AI\Find-Image" ` -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable -Scope Local -ErrorAction SilentlyContinue) # set image directories for Find-Image $findImageParams.ImageDirectories = GenXdev.AI\Get-AIImageCollection -ImageDirectories $ImageDirectories # prepare lookup table inserts [System.Collections.Generic.List[String]] $lookupQueries = [System.Collections.Generic.List[String]]::new() [System.Collections.Generic.List[System.Collections.Hashtable]] $lookupParams = [System.Collections.Generic.List[System.Collections.Hashtable]]::new() function insertImage { param($image, $Info) process { $lookupQueries.Clear() $lookupParams.Clear() $Info.FoundResults = $true try { # convert image object to json and back for deep copy $image = $image | Microsoft.PowerShell.Utility\ConvertTo-Json -depth 20 | Microsoft.PowerShell.Utility\ConvertFrom-Json $Info.TotalImages++ # build parameters for main image record insert $imageParams = @{ "path" = $image.path "explicit" = if ($image.description -and $image.description.has_explicit_content) { 1 } else { 0 } "nudity" = if ($image.description -and $image.description.has_nudity) { 1 } else { 0 } "short_desc" = if ($image.description) { $image.description.short_description } else { "" } "long_desc" = if ($image.description) { $image.description.long_description } else { "" } "pic_type" = if ($image.description) { $image.description.picture_type } else { "" } "mood" = if ($image.description) { $image.description.overall_mood_of_image } else { "" } "style" = if ($image.description) { $image.description.style_type } else { "" } "desc_keywords" = if ($image.description -and $image.description.keywords) { if ($image.description.keywords.Count -gt 0) { $image.description.keywords | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 20 } else { "[]" } } else { "[]" } "people_count" = if ($image.people) { $image.people.count } else { 0 } "people_faces" = if ($image.people -and $image.people.faces) { if ($image.people.faces.Count -gt 0) { $image.people.faces | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 20 } else { "[]" } } else { "[]" } "people_json" = if ($image.people) { $image.people | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 20 } else { '{}' } "objects_count" = if ($image.objects) { $image.objects.count } else { 0 } "objects_list" = if ($image.objects -and $image.objects.objects) { if ($image.objects.objects.Count -gt 0) { $image.objects.objects | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 20 } else { "[]" } } else { "[]" } "objects_json" = if ($image.objects) { $image.objects | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 20 } else { '{}' } "object_counts" = if ($image.objects -and $image.objects.object_counts) { $image.objects.object_counts | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 20 } else { "{}" } "scene_label" = if ($image.scenes) { $image.scenes.label } else { "" } "scene_confidence" = if ($image.scenes) { $image.scenes.confidence } else { 0 } "scene_conf_pct" = if ($image.scenes) { $image.scenes.confidence_percentage } else { 0 } "scene_processed" = if ($image.scenes) { $image.scenes.processed_at } else { "" } } # add image data if embedding is enabled if ($EmbedImages) { try { # check if image file exists before reading if (Microsoft.PowerShell.Management\Test-Path $image.path -PathType Leaf) { Microsoft.PowerShell.Utility\Write-Verbose ( "Reading image data from: $($image.path)" ) $imageBytes = [System.IO.File]::ReadAllBytes($image.path) $imageParams["image_data"] = $imageBytes Microsoft.PowerShell.Utility\Write-Verbose ( "Embedded $($imageBytes.Length) bytes for image: $($image.path)" ) } else { Microsoft.PowerShell.Utility\Write-Warning ( "Image file not found for embedding: $($image.path)" ) $imageParams["image_data"] = $null } } catch { Microsoft.PowerShell.Utility\Write-Warning ( "Failed to read image data for embedding: $($image.path) - $($_.Exception.Message)" ) $imageParams["image_data"] = $null } } # execute the insert and get the new ID $queries = @($insertQuery, "SELECT last_insert_rowid() AS newId;") $result = GenXdev.Data\Invoke-SQLiteQuery -Transaction $transaction -Queries $queries -SqlParameters @($imageParams) $imageId = $result | Microsoft.PowerShell.Utility\Select-Object -ExpandProperty newId $lookupQueries.Clear() $lookupParams.Clear() # insert keywords if present if ($image.keywords -and $image.keywords.Count -gt 0) { foreach ($keyword in $image.keywords) { $lookupQueries.Add("INSERT INTO ImageKeywords (image_id, keyword) VALUES (@image_id, @keyword)") $lookupParams.Add(@{ "image_id" = $imageId; "keyword" = $keyword }) } } # insert people (parse faces string if needed) if ($image.people -and $image.people.faces -and $image.people.faces -ne "" -and $image.people.faces.Count -gt 0) { $people = if ($image.people.faces -is [array]) { $image.people.faces } else { $image.people.faces -split ',' | Microsoft.PowerShell.Core\ForEach-Object { $_.Trim() } | Microsoft.PowerShell.Core\Where-Object { $_ -ne "" } } foreach ($person in $people) { $lookupQueries.Add("INSERT INTO ImagePeople (image_id, person_name) VALUES (@image_id, @person_name)") $lookupParams.Add(@{ "image_id" = $imageId; "person_name" = $person }) } } # insert objects (parse objects string if needed) if ($image.objects -and $image.objects.objects -and $image.objects.objects -ne "" -and $image.objects.objects.Count -gt 0) { $objects = if ($image.objects.objects -is [array]) { $image.objects.objects } else { $image.objects.objects -split ',' | Microsoft.PowerShell.Core\ForEach-Object { $_.Trim() } | Microsoft.PowerShell.Core\Where-Object { $_ -ne "" } } foreach ($obj in $objects) { $lookupQueries.Add("INSERT INTO ImageObjects (image_id, object_name) VALUES (@image_id, @object_name)") $lookupParams.Add(@{ "image_id" = $imageId; "object_name" = $obj }) } } # insert scenes if present if ($image.scenes -and (-not [string]::IsNullOrWhiteSpace($image.scenes.label))) { $lookupQueries.Add("INSERT INTO ImageScenes (image_id, scene_name, confidence) VALUES (@image_id, @scene_name, @confidence)") $lookupParams.Add(@{ "image_id" = $imageId; "scene_name" = $image.scenes.label; "confidence" = $image.scenes.confidence }) } # execute all lookup inserts in a single batch if ($lookupQueries.Count -gt 0) { GenXdev.Data\Invoke-SQLiteQuery -Transaction $transaction -Queries $lookupQueries -SqlParameters $lookupParams Microsoft.PowerShell.Utility\Write-Verbose ( "Inserted lookup data for image ID $imageId" ) } else { Microsoft.PowerShell.Utility\Write-Verbose ( "No lookup data to insert for image ID $imageId" ) } # progress update every 100 images if ($Info.TotalImages % 100 -eq 0) { Microsoft.PowerShell.Utility\Write-Verbose ( "Processed $($Info.TotalImages) images..." ) } # output the image object if PassThru is enabled if ($PassThru) { $image | Microsoft.PowerShell.Utility\Write-Output } } catch { Microsoft.PowerShell.Utility\Write-Warning ( "Failed to insert image data for $($image.path): $($_.Exception.Message)" ) } } } # process input object if provided if ($InputObject -and $InputObject.Count -gt 0) { Microsoft.PowerShell.Utility\Write-Verbose ( "Processing input object with $($InputObject.Count) images..." ) $InputObject | Microsoft.PowerShell.Core\ForEach-Object { $Info.FoundResults = $true insertImage $PSItem $Info } } else { GenXdev.AI\Find-Image @findImageParams | Microsoft.PowerShell.Core\ForEach-Object { $Info.FoundResults = $true insertImage $PSItem $Info } } } try { try { ImportImages $Info # if no results found, update all image metadata and retry if (-not $Info.FoundResults) { $params = GenXdev.Helpers\Copy-IdenticalParamValues ` -BoundParameters $PSBoundParameters ` -FunctionName "GenXdev.AI\Update-AllImageMetaData" ` -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable -Scope Local -ErrorAction SilentlyContinue) $null = GenXdev.AI\Update-AllImageMetaData @params ImportImages $Info } # commit the transaction $transaction.Commit() Microsoft.PowerShell.Utility\Write-Verbose ( "Transaction committed successfully with $($Info.TotalImages)) images" ) } catch { # rollback on error $transaction.Rollback() Microsoft.PowerShell.Utility\Write-Warning ( "Transaction rolled back due to error: $($_.Exception.Message)" ) throw $_ } # create indexes for optimal performance (outside transaction for better performance) Microsoft.PowerShell.Utility\Write-Verbose ( "Creating indexes for optimal performance..." ) GenXdev.Data\Invoke-SQLiteQuery -DatabaseFilePath $DatabaseFilePath -Queries $createIndexes # Run ANALYZE to update query optimizer statistics for best performance Microsoft.PowerShell.Utility\Write-Verbose ( "Running ANALYZE to optimize query planning..." ) GenXdev.Data\Invoke-SQLiteQuery -DatabaseFilePath $DatabaseFilePath -Queries "ANALYZE;" # insert schema version Microsoft.PowerShell.Utility\Write-Verbose ( "Setting schema version to $SCHEMA_VERSION..." ) GenXdev.Data\Invoke-SQLiteQuery -DatabaseFilePath $DatabaseFilePath -Queries "INSERT OR REPLACE INTO ImageSchemaVersion (id, version) VALUES (1, @version)" -SqlParameters @{ "version" = $SCHEMA_VERSION } # output image database stats if PassThru is not enabled if (-not $PassThru) { $params = GenXdev.Helpers\Copy-IdenticalParamValues ` -BoundParameters $PSBoundParameters ` -FunctionName "GenXdev.AI\Get-ImageDatabaseStats" ` -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable -Scope Local -ErrorAction SilentlyContinue) GenXdev.AI\Get-ImageDatabaseStats @params | Microsoft.PowerShell.Utility\Write-Output } # stop the stopwatch and output completion message $totalTime.Stop() Microsoft.PowerShell.Utility\Write-Verbose ( "Image database initialization completed successfully with $($Info.TotalImages) records in $($totalTime.Elapsed.ToString('mm\:ss'))" ) } finally { # always close the connection if ($transaction -and $transaction.Connection) { $transaction.Connection.Close() } } } end { } } ################################################################################ |