Add demo GIF and refine README; improve tag and note generation utilities

This commit is contained in:
Travis V 2025-02-07 16:11:28 -05:00
parent a998e8d44e
commit d00026a760
4 changed files with 53 additions and 32 deletions

View file

@ -1,4 +1,4 @@
# Vision Recall — Screenshot Knowledge Base for Obsidian # Vision Recall — Screenshot Knowledge Base for Obsidian
<p align="center"> <p align="center">
<a href="https://github.com/travisvn/obsidian-vision-recall"> <a href="https://github.com/travisvn/obsidian-vision-recall">
@ -15,13 +15,13 @@
<h1 align="center">Screenshot anything you want to recall later. Let AI handle the rest.</h1> <h1 align="center">Screenshot anything you want to recall later. Let AI handle the rest.</h1>
![Obsidian Vision Recall Plugin Settings Screenshot](https://ud8v76kv5b.ufs.sh/f/1lHAtGEcemsOz28jKAJ0nP31wvubfTVZEFXeiGl29Rjtczhq) ![Obsidian Vision Recall Plugin Settings Screenshot](https://ud8v76kv5b.ufs.sh/f/1lHAtGEcemsOz28jKAJ0nP31wvubfTVZEFXeiGl29Rjtczhq)
## Features ## Features
- **📸 Automatic Screenshot Intake:** - **📸 Automatic Screenshot Intake:**
- 📂 **Intake Folder Monitoring:** Automatically process screenshots as soon as they are added to your designated intake folder. - 📂 **Intake Folder Monitoring:** Automatically process screenshots as soon as they are added to your designated intake folder.
- ⏱️ **Periodic & Auto-Processing:** Options for periodic intake folder checks or instant processing upon file creation. - ⏱️ **Periodic & Auto-Processing:** Options for periodic intake folder checks or instant processing upon file creation.
- 🔗 **Deep Link Intake:** Capture screenshots directly via Obsidian deep links (for advanced workflows). - 🔗 **Deep Link Intake:** Capture screenshots directly via Obsidian deep links (for advanced workflows).
- 📋 **Clipboard Upload:** Quickly process screenshots directly from your clipboard. - 📋 **Clipboard Upload:** Quickly process screenshots directly from your clipboard.
@ -29,24 +29,24 @@
- **🧠 AI-Powered Screenshot Analysis:** - **🧠 AI-Powered Screenshot Analysis:**
- 👓 **Optical Character Recognition (OCR):** Extract text from images with Tesseract OCR. - 👓 **Optical Character Recognition (OCR):** Extract text from images with Tesseract OCR.
- 👁️ **Vision LLM Integration:** Leverage powerful Vision Language Models (like OpenAI's GPT-4o or Ollama models) to analyze image content and understand the context. - 👁️ **Vision LLM Integration:** Leverage powerful Vision Language Models (like OpenAI's GPT-4o or Ollama models) to analyze image content and understand the context.
- 📝 **Intelligent Note Generation:** Automatically generate insightful Obsidian notes summarizing the key information from your screenshots, combining OCR text and vision analysis. - 📝 **Intelligent Note Generation:** Automatically generate insightful Obsidian notes summarizing the key information from your screenshots, combining OCR text and vision analysis.
- 🏷️ **Smart Tagging:** AI-suggested tags automatically categorize your screenshots, making them easily searchable and linkable. - 🏷️ **Smart Tagging:** AI-suggested tags automatically categorize your screenshots, making them easily searchable and linkable.
- **🗂️ Organize & Recall Your Visual Knowledge:** - **🗂️ Organize & Recall Your Visual Knowledge:**
- 🖼️ **Gallery & List Views:** Browse and manage your screenshots in visually appealing gallery or detailed list views within Obsidian. - 🖼️ **Gallery & List Views:** Browse and manage your screenshots in visually appealing gallery or detailed list views within Obsidian.
- 🔍 **Powerful Filtering & Search:** Quickly find screenshots using text search, tag filters, and date range filters. - 🔍 **Powerful Filtering & Search:** Quickly find screenshots using text search, tag filters, and date range filters.
- 🏷️ **Tag Management:** Easily edit and manage tags for your screenshots to refine your knowledge organization. - 🏷️ **Tag Management:** Easily edit and manage tags for your screenshots to refine your knowledge organization.
- 🔗 **Obsidian Note Linking:** Seamlessly link back to your original screenshots from generated notes and vice versa. - 🔗 **Obsidian Note Linking:** Seamlessly link back to your original screenshots from generated notes and vice versa.
- 📊 **Metadata Rich:** View and edit detailed metadata for each screenshot, including OCR text, vision analysis, generated notes, and extracted tags. - 📊 **Metadata Rich:** View and edit detailed metadata for each screenshot, including OCR text, vision analysis, generated notes, and extracted tags.
- **⚙️ Customizable & User-Friendly:** - **⚙️ Customizable & User-Friendly:**
- 🎛️ **Comprehensive Settings:** Fine-tune LLM providers, storage folders, output note settings, and more through a detailed settings tab. - 🎛️ **Comprehensive Settings:** Fine-tune LLM providers, storage folders, output note settings, and more through a detailed settings tab.
- 🚦 **Processing Queue Management:** Monitor and control screenshot processing with a built-in queue, allowing you to pause, resume, and stop processing as needed. - 🚦 **Processing Queue Management:** Monitor and control screenshot processing with a built-in queue, allowing you to pause, resume, and stop processing as needed.
- 📊 **Status Bar Integration:** Quickly access processing queue status and controls from the Obsidian status bar. - 📊 **Status Bar Integration:** Quickly access processing queue status and controls from the Obsidian status bar.
- 🧑‍💻 **Debug Mode:** Detailed logging for troubleshooting and development. - 🧑‍💻 **Debug Mode:** Detailed logging for troubleshooting and development.
## Installation ## Installation
@ -72,3 +72,5 @@
- **Opening Notes:** Click on a screenshot in the Vision Recall View to open its generated Obsidian note, containing summaries, OCR text, vision analysis, and tags. - **Opening Notes:** Click on a screenshot in the Vision Recall View to open its generated Obsidian note, containing summaries, OCR text, vision analysis, and tags.
- **Editing Metadata:** Edit tags and other metadata directly from the Vision Recall View. - **Editing Metadata:** Edit tags and other metadata directly from the Vision Recall View.
- **Managing Processing Queue:** Use the Processing Queue modal (accessible from the status bar button or commands) to monitor and control screenshot processing. - **Managing Processing Queue:** Use the Processing Queue modal (accessible from the status bar button or commands) to monitor and control screenshot processing.
![Vision Recall Obsidian Plugin Demo](./demo.gif)

BIN
demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 MiB

View file

@ -10,7 +10,6 @@ export const TagsSchema = z.object({
// export const tagsJsonSchema = TagsSchema.toJSON(); // export const tagsJsonSchema = TagsSchema.toJSON();
export const tagsJsonSchema = zodToJsonSchema(TagsSchema); export const tagsJsonSchema = zodToJsonSchema(TagsSchema);
export function formatTags(tags: string[]): string[] { export function formatTags(tags: string[]): string[] {
return tags.map((tag) => { return tags.map((tag) => {
let formattedTag = tag.replace(/ /g, '_'); let formattedTag = tag.replace(/ /g, '_');
@ -38,4 +37,21 @@ export function tagsToCommaString(tags: string[]): string {
export function tagsFromCommaString(tags: string): string[] { export function tagsFromCommaString(tags: string): string[] {
return tags.split(',').map(tag => tag.trim()); return tags.split(',').map(tag => tag.trim());
}
export function sanitizeObsidianTag(tag: string): string | null {
// Replace spaces with underscores to maintain readability
tag = tag.replace(/\s+/g, '_');
// Remove all disallowed characters (keep letters, numbers, _, -, /)
tag = tag.replace(/[^a-zA-Z0-9_\/-]/g, '');
// Ensure there's at least one non-numeric character
if (!/[a-zA-Z_\/-]/.test(tag)) {
return null; // Return null if there's no valid non-numeric character
}
// Convert to lowercase to ensure case insensitivity
// return tag.toLowerCase();
return tag;
} }

View file

@ -4,7 +4,7 @@ import { VisionRecallPluginSettings } from '@/types/settings-types';
import { DEFAULT_TAGS_AND_TITLE, TagsAndTitle, VISION_LLM_PROMPT, callLLMAPI, llmSuggestTagsAndTitle } from '@/services/llm-service'; import { DEFAULT_TAGS_AND_TITLE, TagsAndTitle, VISION_LLM_PROMPT, callLLMAPI, llmSuggestTagsAndTitle } from '@/services/llm-service';
import VisionRecallPlugin from '@/main'; import VisionRecallPlugin from '@/main';
import { checkOCRText } from '@/lib/ocr-validation'; import { checkOCRText } from '@/lib/ocr-validation';
import { formatTags, tagsToCommaString } from '@/lib/tag-utils'; import { formatTags, sanitizeObsidianTag, tagsToCommaString } from '@/lib/tag-utils';
import { useQueueStore } from '@/stores/queueStore'; import { useQueueStore } from '@/stores/queueStore';
import { PluginLogger } from '@/lib/Logger'; import { PluginLogger } from '@/lib/Logger';
import { base64EncodeImage } from '@/lib/encode'; import { base64EncodeImage } from '@/lib/encode';
@ -252,6 +252,9 @@ export class ScreenshotProcessor {
this.progressManager.updateProgress('Saving results...', 20); this.progressManager.updateProgress('Saving results...', 20);
if (this.progressManager.isStoppedByUser()) return false; if (this.progressManager.isStoppedByUser()) return false;
const tagPath = sanitizeObsidianTag(paths.uniqueName);
const linkingTag = `#${this.settings.tagPrefix}/${tagPath}`;
const noteInfo = await this.createObsidianNote( const noteInfo = await this.createObsidianNote(
imageFile, imageFile,
paths.newScreenshotPath, paths.newScreenshotPath,
@ -261,7 +264,8 @@ export class ScreenshotProcessor {
tagsAndTitle, tagsAndTitle,
formattedTags, formattedTags,
entryId, entryId,
paths.uniqueName paths.uniqueName,
linkingTag
); );
if (!noteInfo || this.progressManager.isStoppedByUser()) return false; if (!noteInfo || this.progressManager.isStoppedByUser()) return false;
@ -276,7 +280,8 @@ export class ScreenshotProcessor {
tagsAndTitle, tagsAndTitle,
formattedTags, formattedTags,
noteInfo, noteInfo,
entryId entryId,
linkingTag
); );
if (this.progressManager.isStoppedByUser()) return false; if (this.progressManager.isStoppedByUser()) return false;
@ -414,14 +419,14 @@ export class ScreenshotProcessor {
try { try {
const outputNotesFolder = normalizePath(await this.plugin.getFolderFromSettingsKey('outputNotesFolderPath')); const outputNotesFolder = normalizePath(await this.plugin.getFolderFromSettingsKey('outputNotesFolderPath'));
const noteTitle = tagsAndTitle.title || imageFile.basename; const noteTitle = sanitizeFilename(tagsAndTitle.title || imageFile.basename);
let noteFileName = `${noteTitle} Notes.md`; let noteFileName = `${noteTitle} Notes`;
let notePath = normalizePath(`${outputNotesFolder}/${noteFileName}`); let notePath = normalizePath(`${outputNotesFolder}/${noteFileName}.md`);
let counter = 1; let counter = 1;
while (this.app.vault.getAbstractFileByPath(notePath) && counter < 100) { while (this.app.vault.getAbstractFileByPath(notePath) && counter < 100) {
noteFileName = `${noteTitle} Notes (${counter}).md`; noteFileName = `${noteTitle} Notes (${counter})`;
notePath = normalizePath(`${outputNotesFolder}/${noteFileName}`); notePath = normalizePath(`${outputNotesFolder}/${noteFileName}.md`);
counter++; counter++;
} }
@ -447,7 +452,8 @@ export class ScreenshotProcessor {
tagsAndTitle: TagsAndTitle, tagsAndTitle: TagsAndTitle,
formattedTags: string, formattedTags: string,
id: string, id: string,
uniqueName: string uniqueName: string,
linkingTag: string
): Promise<{ notePath: string; noteTitle: string } | null> { ): Promise<{ notePath: string; noteTitle: string } | null> {
try { try {
const outputNotesFolder = normalizePath(await this.plugin.getFolderFromSettingsKey('outputNotesFolderPath')); const outputNotesFolder = normalizePath(await this.plugin.getFolderFromSettingsKey('outputNotesFolderPath'));
@ -488,8 +494,6 @@ export class ScreenshotProcessor {
// Main note content // Main note content
const noteContent = `# Notes from Screenshot: ${noteTitleBase}\n\n${generatedNotes}`; const noteContent = `# Notes from Screenshot: ${noteTitleBase}\n\n${generatedNotes}`;
const linkingTag = `#${this.settings.tagPrefix}/${uniqueName}`;
const metadataContent = `\n\n---\n*Screenshot Filename:* [[${newScreenshotPath}]]\n*${ocrTitle}*:\n\`\`\`\n${truncatedOcrText}...\n\`\`\`\n*${visionTitle}*:\n\`\`\`\n${truncatedVisionLLMResponse}...\n\`\`\`\n\n*Tags:* ${formattedTags}\n\n${linkingTag}\n`; const metadataContent = `\n\n---\n*Screenshot Filename:* [[${newScreenshotPath}]]\n*${ocrTitle}*:\n\`\`\`\n${truncatedOcrText}...\n\`\`\`\n*${visionTitle}*:\n\`\`\`\n${truncatedVisionLLMResponse}...\n\`\`\`\n\n*Tags:* ${formattedTags}\n\n${linkingTag}\n`;
const finalNoteContent = this.settings.includeMetadataInNote const finalNoteContent = this.settings.includeMetadataInNote
@ -522,7 +526,8 @@ export class ScreenshotProcessor {
tagsAndTitle: TagsAndTitle, tagsAndTitle: TagsAndTitle,
formattedTags: string, formattedTags: string,
noteInfo: { notePath: string; noteTitle: string }, noteInfo: { notePath: string; noteTitle: string },
entryId: string entryId: string,
linkingTag: string
): Promise<void> { ): Promise<void> {
try { try {
@ -534,8 +539,6 @@ export class ScreenshotProcessor {
const metadataFilename = `${uniqueName}.json`; const metadataFilename = `${uniqueName}.json`;
const metadataPath = normalizePath(`${await this.plugin.getFolderFromSettingsKey('screenshotStorageFolderPath')}/${metadataFilename}`); const metadataPath = normalizePath(`${await this.plugin.getFolderFromSettingsKey('screenshotStorageFolderPath')}/${metadataFilename}`);
const linkingTag = `#${this.settings.tagPrefix}/${uniqueName}`;
const metadata = { const metadata = {
id: entryId, id: entryId,
originalFilename: imageFile.name, originalFilename: imageFile.name,