mirror of
https://github.com/travisvn/obsidian-vision-recall.git
synced 2026-07-22 05:38:13 +00:00
Add demo GIF and refine README; improve tag and note generation utilities
This commit is contained in:
parent
a998e8d44e
commit
d00026a760
4 changed files with 53 additions and 32 deletions
36
README.md
36
README.md
|
|
@ -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>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
|
||||||
BIN
demo.gif
Normal file
BIN
demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3 MiB |
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue