Skip to content

DEV: Remove AI Auto image caption feature #33794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LinkTo } from "@ember/routing";
import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n";

export default class AutoImageCaptionSetting extends Component {
export default class AiPreferences extends Component {
static shouldRender(args, context) {
return context.siteSettings.discourse_ai_enabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,6 @@ export default class PreferencesAiController extends Controller {

get booleanSettings() {
return [
{
key: "auto_image_caption",
label: "discourse_ai.ai_helper.image_caption.automatic_caption_setting",
settingName: "auto-image-caption",
checked: this.model.user_option.auto_image_caption,
isIncluded: (() => {
const aiHelperEnabledFeatures =
this.siteSettings.ai_helper_enabled_features.split("|");

return (
this.model?.user_allowed_ai_auto_image_captions &&
aiHelperEnabledFeatures.includes("image_caption") &&
this.siteSettings.ai_helper_enabled
);
})(),
},
{
key: "ai_search_discoveries",
label: "discourse_ai.discobot_discoveries.user_setting",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { ajax } from "discourse/lib/ajax";
import { extractError, popupAjaxError } from "discourse/lib/ajax-error";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { apiInitializer } from "discourse/lib/api";
import {
getUploadMarkdown,
IMAGE_MARKDOWN_REGEX,
isImage,
} from "discourse/lib/uploads";
import { i18n } from "discourse-i18n";

export default apiInitializer("1.25.0", (api) => {
export default apiInitializer((api) => {
const buttonAttrs = {
label: i18n("discourse_ai.ai_helper.image_caption.button_label"),
icon: "discourse-sparkles",
Expand All @@ -24,8 +19,6 @@ export default apiInitializer("1.25.0", (api) => {
return;
}

api.addSaveableUserOptionField("auto_image_caption");

api.addComposerImageWrapperButton(
buttonAttrs.label,
buttonAttrs.class,
Expand Down Expand Up @@ -86,164 +79,4 @@ export default apiInitializer("1.25.0", (api) => {
}
}
);

// Checks if image is small (≤ 0.1 MP)
function isSmallImage(width, height) {
const megapixels = (width * height) / 1000000;
return megapixels <= 0.1;
}

function needsImprovedCaption(caption) {
return caption.length < 20 || caption.split(" ").length === 1;
}

function getUploadUrlFromMarkdown(markdown) {
const regex = /\(upload:\/\/([^)]+)\)/;
const match = markdown.match(regex);
return match ? `upload://${match[1]}` : null;
}

async function fetchImageCaption(imageUrl, urlType) {
try {
const response = await ajax(`/discourse-ai/ai-helper/caption_image`, {
method: "POST",
data: {
image_url: imageUrl,
image_url_type: urlType,
},
});
return response.caption;
} catch (error) {
toasts.error({
class: "ai-image-caption-error-toast",
duration: "short",
data: {
message: extractError(error),
},
});
}
}

const autoCaptionAllowedGroups =
settings?.ai_auto_image_caption_allowed_groups
.split("|")
.map((id) => parseInt(id, 10));
const currentUserGroups = currentUser.groups.map((g) => g.id);

if (
!currentUserGroups.some((groupId) =>
autoCaptionAllowedGroups.includes(groupId)
)
) {
return;
}

const toasts = api.container.lookup("service:toasts");
// Automatically caption uploaded images
api.addComposerUploadMarkdownResolver(async (upload) => {
const autoCaptionEnabled = currentUser.get(
"user_option.auto_image_caption"
);

if (
!autoCaptionEnabled ||
!isImage(upload.url) ||
!needsImprovedCaption(upload.original_filename) ||
isSmallImage(upload.width, upload.height)
) {
return getUploadMarkdown(upload);
}

const caption = await fetchImageCaption(upload.url, "long_url");
if (!caption) {
return getUploadMarkdown(upload);
}
return `![${caption}|${upload.thumbnail_width}x${upload.thumbnail_height}](${upload.short_url})`;
});

// Conditionally show dialog to auto image caption
api.composerBeforeSave(() => {
return new Promise((resolve, reject) => {
const dialog = api.container.lookup("service:dialog");
const composer = api.container.lookup("service:composer");
const localePrefix =
"discourse_ai.ai_helper.image_caption.automatic_caption_dialog";
const autoCaptionEnabled = currentUser.get(
"user_option.auto_image_caption"
);

const imageUploads = composer.model.reply.match(IMAGE_MARKDOWN_REGEX);
const hasImageUploads = imageUploads?.length > 0;

if (!hasImageUploads) {
resolve();
}

const imagesToCaption = imageUploads.filter((image) => {
const caption = image
.substring(image.indexOf("[") + 1, image.indexOf("]"))
.split("|")[0];
// We don't check if the image is small to show the prompt here
// because the width/height are the thumbnail sizes so the mp count
// is incorrect. It doesn't matter because the auto caption won't
// happen anyways if its small because that uses the actual upload dimensions
return needsImprovedCaption(caption);
});

const needsBetterCaptions = imagesToCaption?.length > 0;

const keyValueStore = api.container.lookup("service:key-value-store");
const imageCaptionPopup = api.container.lookup(
"service:imageCaptionPopup"
);
const autoCaptionPromptKey = "ai-auto-caption-seen";
const seenAutoCaptionPrompt = keyValueStore.getItem(autoCaptionPromptKey);

if (autoCaptionEnabled || !needsBetterCaptions || seenAutoCaptionPrompt) {
return resolve();
}

keyValueStore.setItem(autoCaptionPromptKey, true);

dialog.confirm({
message: i18n(`${localePrefix}.prompt`),
confirmButtonLabel: `${localePrefix}.confirm`,
cancelButtonLabel: `${localePrefix}.cancel`,
class: "ai-image-caption-prompt-dialog",

didConfirm: async () => {
try {
currentUser.set("user_option.auto_image_caption", true);
await currentUser.save(["auto_image_caption"]);

imagesToCaption.forEach(async (imageMarkdown) => {
const uploadUrl = getUploadUrlFromMarkdown(imageMarkdown);
imageCaptionPopup.showAutoCaptionLoader = true;
const caption = await fetchImageCaption(uploadUrl, "short_url");

// Find and replace the caption in the reply
const regex = new RegExp(
`(!\\[)[^|]+(\\|[^\\]]+\\]\\(${uploadUrl}\\))`
);
const newReply = composer.model.reply.replace(
regex,
`$1${caption}$2`
);
composer.model.set("reply", newReply);
imageCaptionPopup.showAutoCaptionLoader = false;
resolve();
});
} catch (error) {
// Reject the promise if an error occurs
// Show an error saying unable to generate captions
reject(error);
}
},
didCancel: () => {
// Don't enable auto captions and continue with the save
resolve();
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default class ImageCaptionPopup extends Service {
@tracked newCaption = null;
@tracked loading = false;
@tracked popupTrigger = null;
@tracked showAutoCaptionLoader = false;
@tracked _request = null;

updateCaption() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -672,14 +672,6 @@
}
}

.auto-image-caption-loader {
margin-left: 2rem;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--primary-high);
}

// AI Helper Options List
.ai-helper-options {
margin: 0;
Expand Down
6 changes: 0 additions & 6 deletions plugins/discourse-ai/config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -718,12 +718,6 @@ en:
generating: "Generating caption..."
credits: "Captioned by AI"
save_caption: "Save"
automatic_caption_setting: "Enable auto caption"
automatic_caption_loading: "Captioning images..."
automatic_caption_dialog:
prompt: "This post contains non-captioned images. Would you like to enable automatic captions on image uploads? (This can be changed in your preferences later)"
confirm: "Enable"
cancel: "Don't ask again"
no_content_error: "Add content first to perform AI actions on it"

reviewables:
Expand Down
1 change: 0 additions & 1 deletion plugins/discourse-ai/config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ en:
ai_helper_enabled_features: "Select the features to enable in the AI helper."
post_ai_helper_allowed_groups: "User groups allowed to access AI Helper features in posts"
ai_helper_image_caption_model: "Select the model to use for generating image captions"
ai_auto_image_caption_allowed_groups: "Users on these groups can toggle automatic image captioning."

ai_embeddings_selected_model: "Use the selected model for generating embeddings."
ai_embeddings_generate_for_pms: "Generate embeddings for personal messages."
Expand Down
8 changes: 0 additions & 8 deletions plugins/discourse-ai/config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,6 @@ discourse_ai:
type: enum
enum: "DiscourseAi::Configuration::LlmVisionEnumerator"
hidden: true
ai_auto_image_caption_allowed_groups:
client: true
type: group_list
list_type: compact
default: "10" # 10: @trust_level_0
allow_any: false
refresh: true
area: "ai-features/ai_helper"
ai_helper_model_allowed_seeded_models:
default: ""
hidden: true
Expand Down
25 changes: 0 additions & 25 deletions plugins/discourse-ai/lib/ai_helper/entry_point.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,31 +48,6 @@ def inject_into(plugin)
root: false,
)
end

plugin.add_to_serializer(:current_user, :user_allowed_ai_auto_image_captions) do
scope.user.in_any_groups?(SiteSetting.ai_auto_image_caption_allowed_groups_map)
end

UserUpdater::OPTION_ATTR.push(:auto_image_caption)
plugin.add_to_serializer(
:user_option,
:auto_image_caption,
include_condition: -> do
SiteSetting.ai_helper_enabled &&
SiteSetting.ai_helper_enabled_features.include?("image_caption") &&
scope.user.in_any_groups?(SiteSetting.ai_auto_image_caption_allowed_groups_map)
end,
) { object.auto_image_caption }

plugin.add_to_serializer(
:current_user_option,
:auto_image_caption,
include_condition: -> do
SiteSetting.ai_helper_enabled &&
SiteSetting.ai_helper_enabled_features.include?("image_caption") &&
scope.user.in_any_groups?(SiteSetting.ai_auto_image_caption_allowed_groups_map)
end,
) { object.auto_image_caption }
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,4 @@
)
end
end

it "will include auto_image_caption field in the user_option if image caption is enabled" do
assign_fake_provider_to(:ai_helper_model)
assign_fake_provider_to(:ai_helper_image_caption_model)
SiteSetting.ai_helper_enabled = true
SiteSetting.ai_helper_enabled_features = "image_caption"
SiteSetting.ai_auto_image_caption_allowed_groups = "10" # tl0
serializer = CurrentUserSerializer.new(english_user, scope: Guardian.new(english_user))

expect(serializer.user_option.auto_image_caption).to eq(false)
end
end
13 changes: 0 additions & 13 deletions plugins/discourse-ai/spec/models/user_option_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,9 @@

before do
enable_current_plugin

assign_fake_provider_to(:ai_helper_model)
assign_fake_provider_to(:ai_helper_image_caption_model)
SiteSetting.ai_helper_enabled = true
SiteSetting.ai_helper_enabled_features = "image_caption"
SiteSetting.ai_auto_image_caption_allowed_groups = "10" # tl0

SiteSetting.ai_bot_enabled = true
end

describe "#auto_image_caption" do
it "is present" do
expect(described_class.new.auto_image_caption).to eq(false)
end
end

describe "#ai_search_discoveries" do
before do
SiteSetting.ai_bot_discover_persona = ai_persona.id
Expand Down
Loading
Loading