diff --git a/.claude/skills/lrgeniustagai-dev/SKILL.md b/.claude/skills/lrgeniustagai-dev/SKILL.md new file mode 100644 index 0000000..7cc4d96 --- /dev/null +++ b/.claude/skills/lrgeniustagai-dev/SKILL.md @@ -0,0 +1,243 @@ +--- +name: lrgeniustagai-dev +description: Develop and maintain LrGeniusTagAI, a Lightroom Classic plugin (Lua, Lightroom SDK 11+) that sends photos to AI vision models (Gemini, ChatGPT, Ollama, LM Studio) to generate titles, captions, alt text and keywords. Use when adding an AI provider or model, adding a plugin preference or dialog field, changing prompts or the JSON response schema, adding translations, fixing Lightroom SDK issues, or preparing a release. +--- + +# LrGeniusTagAI development + +A Lightroom Classic plugin written in Lua against the Lightroom Classic SDK. All +source lives in `LrGeniusTagAI.lrdevplugin/`. There is no build step, no +package manager, and no test runner: the folder is loaded directly by Lightroom's +Plug-in Manager and zipped as-is for releases. + +## Runtime constraints (read first) + +- **Lua 5.1 semantics** inside Lightroom. No `goto`, no integer division `//`, + no `table.unpack` (use `unpack`), no bit operators. `string.gsub` patterns are + Lua patterns, not regex. +- **Only the Lightroom SDK is available.** No LuaSocket, no OS libs, no `os.execute`. + HTTP goes through `LrHttp`, files through `LrFileUtils`/`LrPathUtils`, JSON + through the bundled `JSON.lua` (`JSON:encode` / `JSON:decode`). +- **Blocking work must run in an async task.** Menu entry files (`AnalyzeImageTask.lua`, + `KeywordConfigTask.lua`) wrap everything in + `LrTasks.startAsyncTask(function() LrFunctionContext.callWithContext(...) end)`. + `LrHttp`, `LrExportSession`, `LrTasks.sleep`, and modal dialogs need this context. +- **Catalog writes need a write-access block.** Public metadata (title, caption, + alt text, keywords) uses `catalog:withWriteAccessDo(name, fn)`. Plugin-private + fields (`aiModel`, `aiLastRun`, `photoContext`) use + `catalog:withPrivateWriteAccessDo(fn)` + `photo:setPropertyForPlugin(_PLUGIN, id, value)`. + Never nest these blocks. +- **The plugin cannot be executed outside Lightroom.** Verify changes by + reloading the plugin in *File → Plug-in Manager* (select the plugin, click + *Reload Plug-in*) and reading the log file (see Debugging). + +## Architecture + +Load order and globals are defined in `Init.lua`. It imports every `Lr*` +namespace into `_G`, requires every module, creates the global `prefs` +(`LrPrefs.prefsForPlugin()`) and `log` (`LrLogger 'LrGeniusTagAI'`), and +fills missing preferences with values from `Defaults.lua`. Because of this, +**modules are plain global tables**, not `require`-returned locals: + +```lua +MyModule = {} +MyModule.__index = MyModule +function MyModule:new() local o = setmetatable({}, MyModule) ... return o end +``` + +| File | Role | +|------|------| +| `Info.lua` | Plugin manifest: version (`Info.MAJOR/MINOR/REVISION`), SDK version, menu items, metadata provider | +| `Init.lua` | Global imports, module loading, preference defaults, background update check | +| `Defaults.lua` | Model list, API base URLs, pricing tables, prompt defaults, keyword categories, top-level keyword names | +| `AiModelAPI.lua` | Facade: picks the provider from the `prefs.ai` prefix and builds prompt/system instruction | +| `GeminiAPI.lua`, `ChatGptAPI.lua`, `OllamaAPI.lua`, `LmStudioAPI.lua` | One provider each; same public contract (below) | +| `ResponseStructure.lua` | Builds the provider-specific JSON schema for structured output | +| `AnalyzeImageTask.lua` | Menu entry point: export temp JPEG → call AI → validate → write metadata | +| `AnalyzeImageProvider.lua` | Dialogs for the task: preflight, photo context, validation, token/cost summary; recursive keyword writer | +| `PluginInfo.lua` + `PluginInfoDialogSections.lua` | Plug-in Manager settings UI (bindings to `prefs`) | +| `PromptConfigProvider.lua`, `KeywordConfigProvider.lua` | Sub-dialogs for prompt presets and keyword categories | +| `AIMetadataProvider.lua` | Custom per-photo metadata fields (`aiLastRun`, `aiModel`, `photoContext`) | +| `ErrorHandler.lua` | `ErrorHandler.handleError(msg, details)`: logs and shows a modal with a "Generate report" button | +| `UpdateCheck.lua` | Compares `Info` version to the latest GitHub release tag | +| `Util.lua` | Helpers: base64 photo encoding, table dump, log-file paths, keyword table flatten/rebuild | +| `JSON.lua`, `inspect.lua` | Vendored third-party libs, do not edit | +| `TranslatedStrings_*.txt` | Localization tables (en, de, fr) | + +### Analysis flow + +1. `AnalyzeImageTask.lua` gets `catalog:getTargetPhotos()`, shows the preflight + dialog, opens an `LrProgressScope`. +2. For each photo: export a temporary JPEG via `LrExportSession` using + `prefs.exportSize` (long edge px) and `prefs.exportQuality`. +3. Collect metadata (`gps`, `keywordTagsForExport`, folder names, optional + free-text context from the photo-context dialog). +4. `AiModelAPI:new():analyzeImage(path, metadata)` dispatches to the provider. +5. Provider returns `success, resultTable, inputTokens, outputTokens`. + `resultTable` keys are the **localized** field names + (`LOC "$$$/lrc-ai-assistant/Defaults/ResponseStructure/ImageTitle=Image title"` etc.) + plus `keywords` (flat array or nested category table). +6. Optional validation dialog, then metadata is written, keywords are created + recursively under an optional per-provider top keyword, temp file deleted, + `aiModel`/`aiLastRun` saved. +7. Per-photo failure returns a cause string: `"fatal"` stops the batch, + `"canceled"` stops silently, `"non-fatal"` continues and is listed at the end. + +### Provider contract + +Every provider class must implement: + +```lua +Provider:new() -- returns nil (after ErrorHandler) if not configured +Provider:analyzeImage(filePath, metadata) + -- returns: success(boolean), result(table or error string), inputTokens, outputTokens + -- must call AiModelAPI.generatePromptFromConfiguration() for the task text + -- must call AiModelAPI.addKeywordHierarchyToSystemInstruction() for the system prompt + -- must append GPS / keywords / context / folder names exactly like GeminiAPI:analyzeImage + -- must strip ```json fences (Defaults.geminiKeywordsGarbageAtStart/End) and apply prefs.replaceSS +Provider:doRequest(filePath, task, systemInstruction, generationConfig) + -- returns: success, rawText, inputTokens, outputTokens +``` + +Local providers (Ollama, LM Studio) additionally expose +`Provider.getLocalVisionModels()` returning `{ {title=..., value='-'} }` +which `Defaults.getAvailableAiModels()` appends to the popup list. + +Provider selection is **prefix-based on `prefs.ai`** in three places that must +stay in sync: `AiModelAPI:new()`, `ResponseStructure:new()`, and the model +`value` strings in `Defaults.lua` (`gemini-*`, `gpt-*`, `ollama-*`, `lmstudio-*`). + +## Common tasks + +### Add a cloud model to an existing provider +1. Add `{ title = "...", value = "" }` to `aiModels` in `Defaults.lua`. +2. Add `Defaults.baseUrls['']`. +3. Add `Defaults.pricing[''].input/.output` as USD per token + (`price_per_million / 1000000`). Missing pricing breaks the cost dialog. +4. If the model needs different request params (e.g. gpt-5 forces + `temperature = 1` and `reasoning_effort`), branch on the id prefix inside + the provider's `doRequest`. + +### Add a new provider +1. Create `NewProviderAPI.lua` implementing the contract above; mirror + `GeminiAPI.lua` (cloud, API key) or `OllamaAPI.lua` (local, model discovery). +2. Add `require "NewProviderAPI"` to `Init.lua` next to the other provider + requires (after `Defaults`, before `ResponseStructure`). +3. Add a prefix branch in `AiModelAPI:new()` and `ResponseStructure:new()`; + decide whether the schema uses Gemini-style (`OBJECT`/`STRING` upper case, + `response_schema`), OpenAI-style (`json_schema` + `strict` + `required` + + `additionalProperties=false`), or Ollama-style (`format` = bare schema). +4. Add `Defaults.TopKeyword` and the preference defaults (API key or + base URL) in `Init.lua`. +5. Add UI fields in `PluginInfoDialogSections.sectionsForTopOfDialog` bound to + the new prefs, and a row in the README provider table. + +### Add a preference +1. Default it in `Init.lua` (`if prefs.x == nil then prefs.x = ... end`). +2. Bind it in `PluginInfoDialogSections.lua` (`bind 'x'`; `startDialog` copies + `prefs` into the property table, `endDialog` copies back). +3. If it affects the request, read it in the providers or `AiModelAPI`. +4. If it should appear in the perf CSV, extend the header and the write line in + `AnalyzeImageTask.lua` together. + +### Change generated fields or the response schema +- Field names are localized keys; the same `LOC` string is used to **build** the + schema (`ResponseStructure.lua`) and to **read** the result + (`AnalyzeImageTask.lua`). Change both, and add the key to every + `TranslatedStrings_*.txt` (see the localization rule below). +- Keyword categories come from `prefs.keywordCategories` falling back to + `Defaults.defaultKeywordCategories`. Nested tables produce nested schema objects. + +### Add or change UI strings / translations + +**Rule: whenever you add, change, or remove a user-visible text, update every +existing `TranslatedStrings_*.txt` file in the same change. Do this +automatically, without being asked.** Discover the files with +`ls LrGeniusTagAI.lrdevplugin/TranslatedStrings_*.txt` (currently `en`, `de`, +`fr`) so new languages are covered too. A `LOC` key that is missing from a +translation file silently falls back to the English default, so omissions are +not caught by Lightroom. + +- Every user-visible string uses `LOC "$$$/lrc-ai-assistant//=English default"`. + The `$$$/lrc-ai-assistant/` prefix is historical; keep it, do not rename to the plugin name. +- Placeholders are `^1`, `^2`, passed as extra args: `LOC("$$$/.../caption=Photo ^1/^2", a, b)`. +- File format: one quoted line per key, + `"$$$/lrc-ai-assistant//=Übersetzung"`. Keep each file sorted by key. + `TranslatedStrings_en.txt` mirrors the English defaults exactly. +- Provide real translations for `de` and `fr`. Keep German wording consistent + with existing entries (e.g. "Zusatzmodul-Manager" for Plug-in Manager). Never + leave the English text as a placeholder in a non-English file. +- When you **rename or delete** a key, remove or rename it in all files so no + orphaned entries remain. +- When you **edit an English default** inside a `LOC` call, also review the + translated wording for that key; the key stays the same, only the text changes. +- Before finishing, verify completeness: + ```bash + cd LrGeniusTagAI.lrdevplugin + grep -oh '\$\$\$/lrc-ai-assistant/[A-Za-z0-9_/]*' *.lua | sort -u > /tmp/keys.txt + for f in TranslatedStrings_*.txt; do + echo "== $f" + grep -oh '\$\$\$/lrc-ai-assistant/[A-Za-z0-9_/]*' "$f" | sort -u | comm -23 /tmp/keys.txt - + done + ``` + Any key printed under a file name is missing from that file. +- Translations are only picked up after a plugin reload. + +### Error handling and logging +- User-facing failures: `ErrorHandler.handleError('Short title', 'Details ' .. Util.dumpTable(headers))`. + It logs and shows a modal; do not also call `LrDialogs.showError` for the same error. +- Provider `doRequest` should return `false, '', 0, 0` after handling the + error; `analyzeImage` propagates. The sentinel `'RATE_LIMIT_EXHAUSTED'` is + treated as fatal by the task loop. +- Use `log:trace` for flow details and `log:error` for failures. Avoid tracing + full base64 bodies or model lists in a loop (bloats the log). +- `LrHttp.post(url, body, headers, 'POST', 720)`: the last arg is the timeout in + seconds; keep it large for slow local models. + +## Debugging + +- Log file (always enabled): + - macOS, Lightroom 14+: `~/Library/Logs/Adobe/Lightroom/LrClassicLogs/LrGeniusTagAI.log` + - Windows, Lightroom 14+: `%LOCALAPPDATA%\Adobe\Lightroom\Logs\LrClassicLogs\LrGeniusTagAI.log` + - Older versions: `~/Documents/LrClassicLogs/LrGeniusTagAI.log` +- The error dialog's *Generate report* button copies logs to the Desktop + (`Util.copyLogfilesToDesktop`). +- Enable *performance logging* in the settings to get `perflog.csv` on the Desktop + (semicolon separated; one row per photo). +- Local providers: verify the service first with + `curl http://localhost:11434/api/tags` (Ollama) or + `curl http://localhost:1234/api/v0/models` (LM Studio); only models reporting + the `vision` capability are listed. +- Lua diagnostics globals for the editor (`LOC`, `_PLUGIN`, `MAC_ENV`) are + declared in `.vscode/settings.json`; add new Lightroom globals there rather + than suppressing warnings inline. + +## Release + +1. Bump `Info.MAJOR` / `Info.MINOR` / `Info.REVISION` in `Info.lua`. + `UpdateCheck` compares `"v" .. major.minor.revision` against the latest + GitHub release tag, so the tag **must** be `vX.Y.Z` matching `Info.lua`. +2. Commit, then run the *Create Release* workflow (`.github/workflows/release.yml`, + `workflow_dispatch`) with the tag as input. It zips + `LrGeniusTagAI.lrdevplugin/` and attaches `LrGeniusTagAI.lrdevplugin.zip` + with auto-generated release notes. +3. The upstream distribution repository is `LrGenius/LrGeniusTagAI`; this repo + is a development fork. Update checks and README download links point upstream. + +## Gotchas + +- `prefs.ai` is the single source of truth for provider *and* model. Never + store the provider separately. +- Gemini returns fenced JSON despite `response_mime_type`; always strip fences. +- ChatGPT strict schemas require every property in `required` and + `additionalProperties = false` at every nesting level. +- gpt-5 models reject `temperature` other than 1; the plugin forces it. +- Keyword results may be a flat array or a nested category table depending on + `prefs.useKeywordHierarchy`; `AnalyzeImageProvider.addKeywordRecursively` + and `Util.extractAllKeywords` handle both. Keep new code table-shape agnostic. +- Dialog property tables are copies of `prefs`; changes take effect only after + `endDialog` copies them back, so a task started while the dialog is open uses + old values. +- Do not edit `JSON.lua` or `inspect.lua`; they are vendored. +- Every new `LOC` string must land in all `TranslatedStrings_*.txt` files in the + same change. Treat a missing translation as an incomplete task. diff --git a/LrGeniusTagAI.lrdevplugin/AnalyzeImageProvider.lua b/LrGeniusTagAI.lrdevplugin/AnalyzeImageProvider.lua index e5dbf49..b1928d6 100644 --- a/LrGeniusTagAI.lrdevplugin/AnalyzeImageProvider.lua +++ b/LrGeniusTagAI.lrdevplugin/AnalyzeImageProvider.lua @@ -30,7 +30,44 @@ function AnalyzeImageProvider.addKeywordRecursively(photo, keywordSubTable, pare end -function AnalyzeImageProvider.showValidationDialog(ctx, keywords, title, caption, altText) +-- Renders a small temporary JPEG of the photo for use in f:picture (which shows images at native size). +-- Returns success, path. Caller must delete the file after the dialog closes. +function AnalyzeImageProvider.renderPreview(photo, longEdge) + local exportSettings = { + LR_export_destinationType = 'specificFolder', + LR_export_destinationPathPrefix = LrPathUtils.getStandardFilePath('temp'), + LR_export_useSubfolder = false, + LR_format = 'JPEG', + LR_jpeg_quality = 60, + LR_minimizeEmbeddedMetadata = true, + LR_outputSharpeningOn = false, + LR_size_doConstrain = true, + LR_size_maxHeight = longEdge, + LR_size_resizeType = 'longEdge', + LR_size_units = 'pixels', + LR_collisionHandling = 'rename', + LR_includeVideoFiles = false, + LR_removeLocationMetadata = true, + LR_embeddedMetadataOption = "copyrightOnly", + } + + local exportSession = LrExportSession({ + photosToExport = { photo }, + exportSettings = exportSettings + }) + + for _, rendition in exportSession:renditions() do + local success, path = rendition:waitForRender() + if success then + return true, path + end + log:error("Preview rendering failed: " .. tostring(path)) + end + return false, "" +end + +-- photo is optional; when given, a preview is shown left of the result fields. +function AnalyzeImageProvider.showValidationDialog(ctx, keywords, title, caption, altText, photo) local f = LrView.osFactory() local bind = LrView.bind local share = LrView.share @@ -75,8 +112,29 @@ function AnalyzeImageProvider.showValidationDialog(ctx, keywords, title, caption table.insert(keywordRows, f:row(row)) end - local dialogView = f:column { - bind_to_object = propertyTable, + -- Preview of the photo, shown left of the result fields. + local previewPath = "" + local previewRendered = false + local previewColumn = nil + if photo ~= nil then + previewRendered, previewPath = AnalyzeImageProvider.renderPreview(photo, 400) + if previewRendered then + previewColumn = f:column { + spacing = f:control_spacing(), + f:picture { + value = previewPath, + frame_width = 0, + }, + f:static_text { + title = photo:getFormattedMetadata('fileName'), + alignment = 'center', + size = 'small', + }, + } + end + end + + local resultFields = f:column { f:row { margin_vertical = 10, f:checkbox { @@ -152,12 +210,31 @@ function AnalyzeImageProvider.showValidationDialog(ctx, keywords, title, caption }, } + local dialogView + if previewColumn ~= nil then + dialogView = f:row { + bind_to_object = propertyTable, + spacing = f:dialog_spacing(), + previewColumn, + resultFields, + } + else + dialogView = f:column { + bind_to_object = propertyTable, + resultFields, + } + end + + -- Buttons: OK saves the (edited) results, "Skip" leaves this photo untouched and + -- continues with the next one (result == "other"), Cancel aborts the whole batch. local result = LrDialogs.presentModalDialog({ title = LOC "$$$/lrc-ai-assistant/AnalyzeImageTask/ReviewWindowTitle=Review results", - -- otherVerb = LOC "$$$/lrc-ai-assistant/AnalyzeImageTask/discard=Discard", + otherVerb = LOC "$$$/lrc-ai-assistant/AnalyzeImageTask/SkipPhoto=Skip", contents = dialogView, }) + if previewRendered then LrFileUtils.delete(previewPath) end + local validatedKeywords = {} if propertyTable.saveKeywords then validatedKeywords = Util.rebuildTableFromKeywords(keywords, propertyTable.keywordsVal, propertyTable.keywordsSel) @@ -290,39 +367,7 @@ function AnalyzeImageProvider.showPhotoContextDialog(photo) end propertyTable.photoContextData = PhotoContextData - local tempDir = LrPathUtils.getStandardFilePath('temp') - local exportSettings = { - LR_export_destinationType = 'specificFolder', - LR_export_destinationPathPrefix = tempDir, - LR_export_useSubfolder = false, - LR_format = 'JPEG', - LR_jpeg_quality = 60, - LR_minimizeEmbeddedMetadata = true, - LR_outputSharpeningOn = false, - LR_size_doConstrain = true, - LR_size_maxHeight = 460, - LR_size_resizeType = 'longEdge', - LR_size_units = 'pixels', - LR_collisionHandling = 'rename', - LR_includeVideoFiles = false, - LR_removeLocationMetadata = true, - LR_embeddedMetadataOption = "copyrightOnly", - } - - local exportSession = LrExportSession({ - photosToExport = { photo }, - exportSettings = exportSettings - }) - - local photoPath = "" - local renderSuccess = false - for _, rendition in exportSession:renditions() do - local success, path = rendition:waitForRender() - if success then - photoPath = path - renderSuccess = success - end - end + local renderSuccess, photoPath = AnalyzeImageProvider.renderPreview(photo, 460) local dialogView = f:column { bind_to_object = propertyTable, diff --git a/LrGeniusTagAI.lrdevplugin/AnalyzeImageTask.lua b/LrGeniusTagAI.lrdevplugin/AnalyzeImageTask.lua index c0efbca..149e852 100644 --- a/LrGeniusTagAI.lrdevplugin/AnalyzeImageTask.lua +++ b/LrGeniusTagAI.lrdevplugin/AnalyzeImageTask.lua @@ -108,10 +108,15 @@ local function exportAndAnalyzePhoto(photo, ctx, progressScope) saveTitle, title, saveCaption, caption, saveAltText, altText, - SkipReview = AnalyzeImageProvider.showValidationDialog(ctx, keywords, title, caption, altText) + SkipReview = AnalyzeImageProvider.showValidationDialog(ctx, keywords, title, caption, altText, photo) if validationResult == "ok" then log:trace("User confirmed AI results.") + elseif validationResult == "other" then + -- Skip: write nothing to this photo, but keep the batch running. + log:trace("User skipped AI results for " .. photoName .. ".") + LrFileUtils.delete(path) + return false, inputTokens, outputTokens, "skipped", "Skipped by user in validation dialog." elseif validationResult == "cancel" then return false, inputTokens, outputTokens, "canceled", "Canceled by user in validation dialog." end @@ -210,6 +215,7 @@ LrTasks.startAsyncTask(function() local totalPhotos = #selectedPhotos local totalFailed = 0 + local totalSkipped = 0 local errorMessages = {} local totalSuccess = 0 local totalInputTokens = 0 @@ -227,7 +233,11 @@ LrTasks.startAsyncTask(function() if outputTokens ~= nil then totalOutputTokens = totalOutputTokens + outputTokens end - if not success then + if not success and cause == "skipped" then + -- Skipped by the user in the review dialog: not an error, continue with next photo. + totalSkipped = totalSkipped + 1 + log:trace("Skipped photo: " .. photo:getFormattedMetadata('fileName')) + elseif not success then totalFailed = totalFailed + 1 errorMessages[photo:getFormattedMetadata('fileName')] = errorMessage log:error("Unsuccessful photo analysis: " .. photo:getFormattedMetadata('fileName')) @@ -256,7 +266,8 @@ LrTasks.startAsyncTask(function() progressScope:done() local stopTimeBatch = LrDate.currentTime() - log:trace("Analyzing " .. totalPhotos .. " with " .. prefs.ai .. " took " .. (stopTimeBatch - startTimeBatch) .. " seconds.") + log:trace("Analyzing " .. totalPhotos .. " with " .. prefs.ai .. " took " .. (stopTimeBatch - startTimeBatch) .. " seconds. " + .. "Success: " .. totalSuccess .. ", skipped: " .. totalSkipped .. ", failed: " .. totalFailed) if prefs.perfLogging and PerfLogFile ~= nil then PerfLogFile:close() diff --git a/LrGeniusTagAI.lrdevplugin/Defaults.lua b/LrGeniusTagAI.lrdevplugin/Defaults.lua index 76cf79c..aee460b 100644 --- a/LrGeniusTagAI.lrdevplugin/Defaults.lua +++ b/LrGeniusTagAI.lrdevplugin/Defaults.lua @@ -43,6 +43,9 @@ local aiModels = { { title = "Google Gemini Pro 2.5", value = "gemini-2.5-pro" }, { title = "Google Gemini Flash 3.1 Lite", value = "gemini-3.1-flash-lite" }, { title = "Google Gemini Flash 3.5", value = "gemini-3.5-flash" }, + { title = "Google Gemini Flash 3.6", value = "gemini-3.6-flash" }, + { title = "Google Gemini Flash 3.7", value = "gemini-3.7-flash" }, + { title = "Google Gemini Flash 3.8", value = "gemini-3.8-flash" }, { title = "Google Gemini Pro 3.1", value = "gemini-3.1-pro-preview" }, { title = "ChatGPT 5.4 Nano", value = "gpt-5.4-nano" }, { title = "ChatGPT 5.4 Mini", value = "gpt-5.4-mini" }, @@ -86,6 +89,9 @@ Defaults.baseUrls['gemini-2.5-flash'] = 'https://generativelanguage.googleapis.c Defaults.baseUrls['gemini-2.5-pro'] = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key=' Defaults.baseUrls['gemini-3.1-flash-lite'] = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite:generateContent?key=' Defaults.baseUrls['gemini-3.5-flash'] = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent?key=' +Defaults.baseUrls['gemini-3.6-flash'] = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent?key=' +Defaults.baseUrls['gemini-3.7-flash'] = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.7-flash:generateContent?key=' +Defaults.baseUrls['gemini-3.8-flash'] = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.8-flash:generateContent?key=' Defaults.baseUrls['gemini-3.1-pro-preview'] = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview:generateContent?key=' @@ -125,6 +131,17 @@ Defaults.pricing["gemini-3.5-flash"].output= 9 / 1000000 Defaults.pricing["gemini-3.1-flash-lite"] = {} Defaults.pricing["gemini-3.1-flash-lite"].input = 0.25 / 1000000 Defaults.pricing["gemini-3.1-flash-lite"].output= 1.5 / 1000000 +-- Gemini 3.6/3.7/3.8 Flash: launch pricing valid through 2026-12-31 (source: ai.google.dev/gemini-api/docs/pricing). +-- From 2027-01-01 Google lists 1.50 input / 7.50 output per 1M tokens. +Defaults.pricing["gemini-3.6-flash"] = {} +Defaults.pricing["gemini-3.6-flash"].input = 0.75 / 1000000 +Defaults.pricing["gemini-3.6-flash"].output= 3.75 / 1000000 +Defaults.pricing["gemini-3.7-flash"] = {} +Defaults.pricing["gemini-3.7-flash"].input = 0.75 / 1000000 +Defaults.pricing["gemini-3.7-flash"].output= 3.75 / 1000000 +Defaults.pricing["gemini-3.8-flash"] = {} +Defaults.pricing["gemini-3.8-flash"].input = 0.75 / 1000000 +Defaults.pricing["gemini-3.8-flash"].output= 3.75 / 1000000 Defaults.pricing["gpt-5.5"] = {} Defaults.pricing["gpt-5.5"].input = 5 / 1000000 diff --git a/LrGeniusTagAI.lrdevplugin/Init.lua b/LrGeniusTagAI.lrdevplugin/Init.lua index 30a8b52..787ca19 100644 --- a/LrGeniusTagAI.lrdevplugin/Init.lua +++ b/LrGeniusTagAI.lrdevplugin/Init.lua @@ -177,6 +177,10 @@ if _G.prefs.periodicalUpdateCheck == nil then _G.prefs.periodicalUpdateCheck = false end +if _G.prefs.onlyReportNewVersions == nil then + _G.prefs.onlyReportNewVersions = false +end + if _G.prefs.submitFolderName == nil then _G.prefs.submitFolderName = false end @@ -187,7 +191,8 @@ end if prefs.periodicalUpdateCheck then LrTasks.startAsyncTask(function() - -- Check for updates in the background - UpdateCheck.checkForNewVersionInBackground() + -- Check for updates in the background. + -- If "Only report new versions" is enabled, stay silent when already up to date. + UpdateCheck.checkForNewVersionInBackground(prefs.onlyReportNewVersions) end) end \ No newline at end of file diff --git a/LrGeniusTagAI.lrdevplugin/PluginInfoDialogSections.lua b/LrGeniusTagAI.lrdevplugin/PluginInfoDialogSections.lua index db8fc8c..50b304c 100644 --- a/LrGeniusTagAI.lrdevplugin/PluginInfoDialogSections.lua +++ b/LrGeniusTagAI.lrdevplugin/PluginInfoDialogSections.lua @@ -61,6 +61,7 @@ function PluginInfoDialogSections.startDialog(propertyTable) propertyTable.licenseKey = prefs.licenseKey propertyTable.periodicalUpdateCheck = prefs.periodicalUpdateCheck + propertyTable.onlyReportNewVersions = prefs.onlyReportNewVersions propertyTable.submitFolderName = prefs.submitFolderName @@ -124,6 +125,17 @@ function PluginInfoDialogSections.sectionsForBottomOfDialog(f, propertyTable) alignment = 'right', }, }, + f:row { + f:checkbox { + value = bind 'onlyReportNewVersions', + enabled = bind 'periodicalUpdateCheck', + }, + f:static_text { + title = LOC "$$$/lrc-ai-assistant/PluginInfoDialogSections/onlyReportNewVersions=Only report new versions", + alignment = 'right', + enabled = bind 'periodicalUpdateCheck', + }, + }, f:row { f:push_button { title = LOC "$$$/lrc-ai-assistant/PluginInfoDialogSections/UpdateCheck=Check for updates", @@ -524,6 +536,7 @@ function PluginInfoDialogSections.endDialog(propertyTable) prefs.perfLogging = propertyTable.perfLogging prefs.periodicalUpdateCheck = propertyTable.periodicalUpdateCheck + prefs.onlyReportNewVersions = propertyTable.onlyReportNewVersions prefs.submitFolderName = propertyTable.submitFolderName diff --git a/LrGeniusTagAI.lrdevplugin/TranslatedStrings_de.txt b/LrGeniusTagAI.lrdevplugin/TranslatedStrings_de.txt index 995538b..9bfe1f9 100644 --- a/LrGeniusTagAI.lrdevplugin/TranslatedStrings_de.txt +++ b/LrGeniusTagAI.lrdevplugin/TranslatedStrings_de.txt @@ -23,7 +23,7 @@ "$$$/lrc-ai-assistant/ChatGptAPI/NoAPIkey=Kein ChatGPT API key eingetragen im Zusatzmodul-Manager." "$$$/lrc-ai-assistant/ChatGptAPI/gpsAddon=Dieses Foto wurde an den folgenden Koordinaten aufgenommen: " "$$$/lrc-ai-assistant/ChatGptAPI/keywordAddon=Einige Stichwörter sind:" -"$$$/lrc-ai-assistant/AnalyzeImageTask/SkipFromHere=Die folgenden ohne Überprüfung speichern." +"$$$/lrc-ai-assistant/AnalyzeImageTask/SkipFromHere=Die folgenden Bilder ohne Überprüfung speichern." "$$$/lrc-ai-assistant/AnalyzeImageTask/UsedTokenDialog/GeneratedCosts=Erzeugte Kosten" "$$$/lrc-ai-assistant/AnalyzeImageTask/UsedTokenDialog/Title=Verursachte Kosten" "$$$/lrc-ai-assistant/AnalyzeImageTask/UsedTokenDialog/TotalCosts=Kosten insgesamt" @@ -38,6 +38,7 @@ "$$$/lrc-ai-assistant/AnalyzeImageTask/saveTitleCaption=Mit KI generierte Bildbeschreibung und Bildtitel speichern" "$$$/lrc-ai-assistant/AnalyzeImageTask/saveTopKeyword=KI-generierte Stichwörter speichern." "$$$/lrc-ai-assistant/AnalyzeImageTask/ReviewWindowTitle=Überprüfen der Ergebnisse" +"$$$/lrc-ai-assistant/AnalyzeImageTask/SkipPhoto=Überspringen" "$$$/lrc-ai-assistant/Info/Title=Fotos mit KI analysieren" "$$$/lrc-ai-assistant/Info/PluginName=Lightroom KI Assistent" "$$$/lrc-ai-assistant/PluginInfoDialogSections/GoogleApiKey=Google API Schlüssel" @@ -92,6 +93,7 @@ "$$$/lrc-ai-assistant/PluginInfoDialogSections/enableDebugLogging=Aktiviere Log-Dateien" "$$$/lrc-ai-assistant/PluginInfoDialogSections/enablePerfLogging=Aktiviere Leistungs Log-Dateien" "$$$/lrc-ai-assistant/PluginInfoDialogSections/periodUpdateCheck=Periodisch nach Updates suchen" +"$$$/lrc-ai-assistant/PluginInfoDialogSections/onlyReportNewVersions=Nur neue Versionen melden" "$$$/lrc-ai-assistant/PluginInfoDialogSections/folderNames=Ordnernamen" "$$$/lrc-ai-assistant/AnalyzeImageTask/aiBehavior=KI Verhalten" "$$$/lrc-ai-assistant/AnalyzeImageTask/beCoherent=Kohärent" @@ -114,5 +116,4 @@ "$$$/lrc-ai-assistant/AnalyzeImageTask/SaveKeywords=Übernehme Stichwörter" "$$$/lrc-ai-assistant/AnalyzeImageTask/SaveTitle=Übernehme Titel" "$$$/lrc-ai-assistant/AnalyzeImageTask/SaveCaption=Übernehme Beschreibung" -"$$$/lrc-ai-assistant/AnalyzeImageTask/SaveAltText=Übernehme Alt-Text" -"$$$/lrc-ai-assistant/AnalyzeImageTask/SkipFromHere=Überspringe Überprüfung bei den folgenden." \ No newline at end of file +"$$$/lrc-ai-assistant/AnalyzeImageTask/SaveAltText=Übernehme Alt-Text" \ No newline at end of file diff --git a/LrGeniusTagAI.lrdevplugin/TranslatedStrings_en.txt b/LrGeniusTagAI.lrdevplugin/TranslatedStrings_en.txt index 40198b5..78a36d1 100644 --- a/LrGeniusTagAI.lrdevplugin/TranslatedStrings_en.txt +++ b/LrGeniusTagAI.lrdevplugin/TranslatedStrings_en.txt @@ -23,7 +23,7 @@ "$$$/lrc-ai-assistant/ChatGptAPI/NoAPIkey=No ChatGPT API key configured in Add-Ons manager." "$$$/lrc-ai-assistant/ChatGptAPI/gpsAddon=This photo was taken at the following coordinates: " "$$$/lrc-ai-assistant/ChatGptAPI/keywordAddon=Some keywords are:" -"$$$/lrc-ai-assistant/AnalyzeImageTask/SkipFromHere=Save following without reviewing." +"$$$/lrc-ai-assistant/AnalyzeImageTask/SkipFromHere=Save following Photos without reviewing." "$$$/lrc-ai-assistant/AnalyzeImageTask/UsedTokenDialog/GeneratedCosts=Generated costs" "$$$/lrc-ai-assistant/AnalyzeImageTask/UsedTokenDialog/Title=Generation costs" "$$$/lrc-ai-assistant/AnalyzeImageTask/UsedTokenDialog/TotalCosts=Total costs:" @@ -38,6 +38,7 @@ "$$$/lrc-ai-assistant/AnalyzeImageTask/rateLimit=Quota exhausted, set up pay as you go at Google, or wait for some hours." "$$$/lrc-ai-assistant/AnalyzeImageTask/saveTitleCaption=Save AI generated title and caption" "$$$/lrc-ai-assistant/AnalyzeImageTask/ReviewWindowTitle=Review the results" +"$$$/lrc-ai-assistant/AnalyzeImageTask/SkipPhoto=Skip" "$$$/lrc-ai-assistant/Info/AnalyzeImageTask/Title=Analyze photos with AI" "$$$/lrc-ai-assistant/Info/PluginName=Lightroom AI assistant" "$$$/lrc-ai-assistant/PluginInfoDialogSections/GoogleApiKey=Google API key" @@ -95,6 +96,7 @@ "$$$/lrc-ai-assistant/PluginInfoDialogSections/enableDebugLogging=Enable debug logging" "$$$/lrc-ai-assistant/PluginInfoDialogSections/enablePerfLogging=Enable performance logging" "$$$/lrc-ai-assistant/PluginInfoDialogSections/periodUpdateCheck=Periodically check for Updates" +"$$$/lrc-ai-assistant/PluginInfoDialogSections/onlyReportNewVersions=Only report new versions" "$$$/lrc-ai-assistant/PluginInfoDialogSections/folderNames=Folder names" "$$$/lrc-ai-assistant/AnalyzeImageTask/aiBehavior=AI behavior" "$$$/lrc-ai-assistant/AnalyzeImageTask/beCoherent=Be coherent" @@ -118,9 +120,7 @@ "$$$/lrc-ai-assistant/ErrorHandler/gatherLogs=Generate report" "$$$/lrc-ai-assistant/AnalyzeImageTask/discard=Discard" "$$$/lrc-ai-assistant/PluginInfoDialogSections/validation=Enable validation" - "$$$/lrc-ai-assistant/AnalyzeImageTask/SaveKeywords=Save keywords" "$$$/lrc-ai-assistant/AnalyzeImageTask/SaveTitle=Save title" "$$$/lrc-ai-assistant/AnalyzeImageTask/SaveCaption=Save caption" -"$$$/lrc-ai-assistant/AnalyzeImageTask/SaveAltText=Save alt text" -"$$$/lrc-ai-assistant/AnalyzeImageTask/SkipFromHere=Save following without reviewing." \ No newline at end of file +"$$$/lrc-ai-assistant/AnalyzeImageTask/SaveAltText=Save alt text" \ No newline at end of file diff --git a/LrGeniusTagAI.lrdevplugin/TranslatedStrings_fr.txt b/LrGeniusTagAI.lrdevplugin/TranslatedStrings_fr.txt index 134b6d2..c7f5792 100644 --- a/LrGeniusTagAI.lrdevplugin/TranslatedStrings_fr.txt +++ b/LrGeniusTagAI.lrdevplugin/TranslatedStrings_fr.txt @@ -38,6 +38,7 @@ "$$$/lrc-ai-assistant/AnalyzeImageTask/rateLimit=Quota épuisé, configurez le paiement à l'utilisation chez Google ou attendez quelques heures." "$$$/lrc-ai-assistant/AnalyzeImageTask/saveTitleCaption=Enregistrer le titre et la légende générés par l'IA" "$$$/lrc-ai-assistant/AnalyzeImageTask/ReviewWindowTitle=Examiner les résultats" +"$$$/lrc-ai-assistant/AnalyzeImageTask/SkipPhoto=Ignorer" "$$$/lrc-ai-assistant/Info/Title=Analyser les photos avec l'IA" "$$$/lrc-ai-assistant/Info/PluginName=Assistant IA pour Lightroom" "$$$/lrc-ai-assistant/PluginInfoDialogSections/GoogleApiKey=Clé API Google" @@ -93,6 +94,7 @@ "$$$/lrc-ai-assistant/PluginInfoDialogSections/enableDebugLogging=Activer la journalisation" "$$$/lrc-ai-assistant/PluginInfoDialogSections/enablePerfLogging=Activer la journalisation des performances" "$$$/lrc-ai-assistant/PluginInfoDialogSections/periodUpdateCheck=Vérifiez régulièrement les mises à jour" +"$$$/lrc-ai-assistant/PluginInfoDialogSections/onlyReportNewVersions=Signaler uniquement les nouvelles versions" "$$$/lrc-ai-assistant/PluginInfoDialogSections/folderNames=Noms de dossiers" "$$$/lrc-ai-assistant/AnalyzeImageTask/aiBehavior=Comportement de l'IA" "$$$/lrc-ai-assistant/AnalyzeImageTask/beCoherent=Soyez cohérent" diff --git a/LrGeniusTagAI.lrdevplugin/UpdateCheck.lua b/LrGeniusTagAI.lrdevplugin/UpdateCheck.lua index 34359f5..aaf28ac 100644 --- a/LrGeniusTagAI.lrdevplugin/UpdateCheck.lua +++ b/LrGeniusTagAI.lrdevplugin/UpdateCheck.lua @@ -6,7 +6,8 @@ UpdateCheck.releaseTagName = "v" .. tostring(Info.MAJOR) .. "." .. tostring(Info UpdateCheck.updateCheckUrl = "https://api.github.com/repos/LrGenius/LrGeniusTagAI/releases/latest" UpdateCheck.latestReleaseUrl = "https://github.com/LrGenius/LrGeniusTagAI/releases/latest" -function UpdateCheck.checkForNewVersion() +-- silentIfCurrent: when true, no message is shown if the installed version is already the latest. +function UpdateCheck.checkForNewVersion(silentIfCurrent) local response, headers = LrHttp.get(UpdateCheck.updateCheckUrl) if headers.status == 200 then @@ -15,8 +16,10 @@ function UpdateCheck.checkForNewVersion() if decoded ~= nil then if decoded.tag_name ~= UpdateCheck.releaseTagName then LrHttp.openUrlInBrowser(UpdateCheck.latestReleaseUrl) + elseif not silentIfCurrent then + LrDialogs.message(LOC "$$$/lrc-ai-assistant/UpdateCheck/onCurrentVersion=You're running the current version of LrGeniusTagAI", UpdateCheck.releaseTagName) else - LrDialogs.message("You're on the latest plugin version: " .. UpdateCheck.releaseTagName) + log:trace("Update check: already on latest version " .. UpdateCheck.releaseTagName .. ", message suppressed.") end end else @@ -31,6 +34,6 @@ function UpdateCheck.checkForNewVersion() return nil end -function UpdateCheck.checkForNewVersionInBackground() - return UpdateCheck.checkForNewVersion() +function UpdateCheck.checkForNewVersionInBackground(silentIfCurrent) + return UpdateCheck.checkForNewVersion(silentIfCurrent) end \ No newline at end of file diff --git a/README.md b/README.md index 06e22fd..6b43bae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +This is a Developement-Fork of a Lightroom Classic plugin that uses LLMs to generate tags and describe images. + +**Please use https://github.com/LrGenius/LrGeniusTagAI for Downloads.** + +--- + # LrGeniusTagAI [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/W7X2240HF4) @@ -101,7 +107,7 @@ All settings live under **File → Plug-in Manager → LrGeniusTagAI**: | Provider | Models | Requires | |------------|------------------------------------------------------|-----------------| -| Gemini | 2.5 Flash-Lite/Flash/Pro, 3.1/3.5 Flash-Lite/Flash/Pro| Google API key | +| Gemini | 2.5 Flash-Lite/Flash/Pro, 3.1/3.5 Flash-Lite/Flash/Pro, 3.6/3.7/3.8 Flash | Google API key | | ChatGPT | 5.4 Nano/Mini, 5.4, 5.5 | OpenAI API key | | Ollama | any locally installed vision model | Ollama running locally | | LM Studio | any locally installed vision model | LM Studio running locally |