Goal: to summarize what’s in the various groups - which have text, pdf, images - and present it in a kind of “magazine” way so that purely text records have a brief summary, images have a title and a thumbnail.
I tried using MCP, it can’t (says I would need to export the images locally, use APIs, other tools, etc). So I believe I really can’t - but who knows?
It would be nice to have a kind of a summarized, with little images, view of what’s in these older databases.
AppleScript. The following script was created by DEVONthink’s script assistant using Claude Opus 5.5 and simply pasting your post.
-- Magazine-style overview of selected DEVONthink groups
-- Text records get a summary or excerpt, images and PDFs get a thumbnail.
-- Select one or more groups (or individual documents), then run.
property maxItemsPerGroup : 80 -- cap cards per section to keep the page usable
property maxDepth : 6 -- how deep to descend into subgroups
property excerptLength : 450 -- characters for fast excerpts
property aiInputLength : 6000 -- characters sent to the AI per record
property gSections : {}
on run
set my gSections to {}
tell application id "DNtp"
try
set theSelection to selected records
if theSelection is {} then error "Please select one or more groups (or documents) first."
set modeChoice to button returned of (display dialog "Create a magazine-style overview of the selection." & return & return & "Text summaries:" buttons {"Cancel", "AI Summary (slow)", "Excerpt (fast)"} default button 3 cancel button 1 with title "Magazine Overview")
set useAI to (modeChoice starts with "AI")
if useAI and (current chat engine is missing value) then
display dialog "No AI chat engine is configured in DEVONthink's settings. Excerpts will be used instead." buttons {"OK"} default button 1 with title "Magazine Overview"
set useAI to false
end if
-- Gather sections: one per group (recursively), plus loose selected documents
set looseDocs to {}
repeat with aRecord in theSelection
set recType to record type of aRecord
if recType is group then
my collectSections(aRecord, 1)
else if recType is not smart group then
set end of looseDocs to aRecord
end if
end repeat
if looseDocs is not {} then
set end of my gSections to {sectionTitle:"Selected Documents", sectionItems:looseDocs, totalCount:(count of looseDocs)}
end if
if (count of my gSections) is 0 then error "No documents were found in the selection."
set totalCards to 0
repeat with aSection in my gSections
set totalCards to totalCards + (count of (sectionItems of aSection))
end repeat
show progress indicator "Building magazine overview…" steps totalCards with cancel button
set htmlParts to {}
set end of htmlParts to my htmlHeader()
set dbName to name of database of (item 1 of theSelection)
set end of htmlParts to "<h1>" & my htmlEscape(dbName) & "</h1>"
set end of htmlParts to "<div class=\"subtitle\">An overview of " & totalCards & " documents in " & (count of my gSections) & " sections · compiled " & my formatDate(current date) & "</div>"
set wasCancelled to false
repeat with aSection in my gSections
if wasCancelled then exit repeat
set theItems to sectionItems of aSection
set textCount to 0
set pdfCount to 0
set imageCount to 0
set otherCount to 0
set cardParts to {}
repeat with anItem in theItems
if cancelled progress then
set wasCancelled to true
exit repeat
end if
step progress indicator (name of anItem as string)
set cardResult to my buildCard(anItem, useAI)
set end of cardParts to cardHTML of cardResult
set cardCategory to category of cardResult
if cardCategory is "text" then
set textCount to textCount + 1
else if cardCategory is "pdf" then
set pdfCount to pdfCount + 1
else if cardCategory is "image" then
set imageCount to imageCount + 1
else
set otherCount to otherCount + 1
end if
end repeat
set end of htmlParts to "<h2>" & my htmlEscape(sectionTitle of aSection) & "</h2>"
set end of htmlParts to "<div class=\"stats\">" & (totalCount of aSection) & " documents · " & textCount & " text · " & pdfCount & " PDF · " & imageCount & " images · " & otherCount & " other</div>"
set end of htmlParts to "<div class=\"grid\">" & my joinList(cardParts, "") & "</div>"
if (totalCount of aSection) > (count of theItems) then
set end of htmlParts to "<div class=\"more\">… and " & ((totalCount of aSection) - (count of theItems)) & " more documents not shown.</div>"
end if
end repeat
set end of htmlParts to "</body></html>"
hide progress indicator
set htmlSource to my joinList(htmlParts, return)
set destGroup to incoming group of database of (item 1 of theSelection)
if destGroup is missing value then set destGroup to root of database of (item 1 of theSelection)
set newRecord to create record with {name:"Magazine Overview - " & dbName & " - " & my formatDate(current date), type:HTML, source:htmlSource} in destGroup
if newRecord is not missing value then
open window for record newRecord
if wasCancelled then display dialog "Cancelled - a partial overview was created." buttons {"OK"} default button 1 with title "Magazine Overview"
else
error "The overview document could not be created."
end if
on error errMsg number errNum
try
hide progress indicator
end try
if errNum is not -128 then display alert "Magazine Overview" message errMsg as warning
end try
end tell
end run
-- Recursively collect one section per group containing documents
on collectSections(theGroup, depthLevel)
tell application id "DNtp"
set docList to {}
set subGroups to {}
set theChildren to children of theGroup
repeat with aChild in theChildren
set childType to record type of aChild
if childType is group then
set end of subGroups to aChild
else if childType is not smart group then
set end of docList to aChild
end if
end repeat
set totalDocs to count of docList
if totalDocs > (my maxItemsPerGroup) then set docList to items 1 thru (my maxItemsPerGroup) of docList
if totalDocs > 0 then
set groupLocation to location with name of theGroup
if groupLocation is missing value then set groupLocation to name of theGroup
set sectionLabel to (name of database of theGroup) & " ▸ " & groupLocation
set end of my gSections to {sectionTitle:sectionLabel, sectionItems:docList, totalCount:totalDocs}
end if
if depthLevel < (my maxDepth) then
repeat with aSub in subGroups
my collectSections(aSub, depthLevel + 1)
end repeat
end if
end tell
end collectSections
-- Build a single card; returns {cardHTML:..., category:...}
on buildCard(theRecord, useAI)
tell application id "DNtp"
set recName to name of theRecord
set recType to record type of theRecord
set recLink to reference URL of theRecord
set recKind to kind of theRecord
if recKind is missing value then set recKind to ""
set recDate to creation date of theRecord
set recPath to path of theRecord
set thumbSrc to ""
set summaryText to ""
set metaExtra to ""
set cardCategory to "other"
if recType is picture then
set cardCategory to "image"
if recPath is not missing value and recPath is not "" then set thumbSrc to my fileURL(recPath)
try
set recDims to dimensions of theRecord
if (count of recDims) is 2 then set metaExtra to ((item 1 of recDims) as integer as string) & "×" & ((item 2 of recDims) as integer as string) & " px"
end try
else if recType is PDF document then
set cardCategory to "pdf"
-- WebKit renders the first page of a PDF inside an img tag
if recPath is not missing value and recPath is not "" then set thumbSrc to my fileURL(recPath)
try
set metaExtra to ((page count of theRecord) as string) & " pages"
end try
set summaryText to my summarizeRecord(theRecord, useAI)
if summaryText is "" then set summaryText to "(No text layer - possibly a scan.)"
else if recType is in {txt, RTF, RTFD, formatted note, markdown, HTML, webarchive, email, XML} then
set cardCategory to "text"
try
set metaExtra to ((word count of theRecord) as string) & " words"
end try
set summaryText to my summarizeRecord(theRecord, useAI)
if summaryText is "" then set summaryText to "(Empty document.)"
else if recType is bookmark then
set recURL to URL of theRecord
if recURL is not missing value then set summaryText to recURL
else if recType is multimedia then
try
set recDuration to duration of theRecord
set metaExtra to ((round (recDuration / 60) rounding down) as string) & " min " & (((recDuration as integer) mod 60) as string) & " s"
end try
else if recType is sheet then
try
set recColumns to columns of theRecord
if recColumns is not missing value then set summaryText to "Columns: " & my joinList(recColumns, ", ")
end try
end if
end tell
-- Assemble card HTML
set metaLine to my htmlEscape(recKind)
if recDate is not missing value then set metaLine to metaLine & " · " & my formatDate(recDate)
if metaExtra is not "" then set metaLine to metaLine & " · " & my htmlEscape(metaExtra)
set cardHTML to "<div class=\"card " & cardCategory & "\">"
if thumbSrc is not "" then
set cardHTML to cardHTML & "<a href=\"" & recLink & "\"><img src=\"" & thumbSrc & "\" loading=\"lazy\" onerror=\"this.style.display='none'\"></a>"
end if
set cardHTML to cardHTML & "<div class=\"body\"><h3><a href=\"" & recLink & "\">" & my htmlEscape(recName) & "</a></h3>"
set cardHTML to cardHTML & "<div class=\"meta\">" & metaLine & "</div>"
if summaryText is not "" then
set cardHTML to cardHTML & "<div class=\"summary\">" & my replaceText(my htmlEscape(summaryText), linefeed, "<br>") & "</div>"
end if
set cardHTML to cardHTML & "</div></div>"
return {cardHTML:cardHTML, category:cardCategory}
end buildCard
-- Returns an AI summary or a plain excerpt
on summarizeRecord(theRecord, useAI)
tell application id "DNtp"
set recText to ""
try
set recText to plain text of theRecord
end try
if recText is missing value then set recText to ""
end tell
if recText is "" then return ""
if useAI then
try
set aiInput to my truncateText(recText, my aiInputLength)
tell application id "DNtp"
set aiResult to summarize text aiInput as text summary
end tell
if aiResult is not missing value and aiResult is not "" then return aiResult
end try
end if
-- Fallback / fast mode: cleaned excerpt
set cleanText to my replaceText(recText, return, " ")
set cleanText to my replaceText(cleanText, linefeed, " ")
set cleanText to my replaceText(cleanText, tab, " ")
repeat 4 times
set cleanText to my replaceText(cleanText, " ", " ")
end repeat
if (length of cleanText) > (my excerptLength) then
return (my truncateText(cleanText, my excerptLength)) & "…"
end if
return cleanText
end summarizeRecord
on truncateText(theString, maxLength)
if (length of theString) > maxLength then
return characters 1 thru maxLength of theString
end if
return theString
end truncateText
on fileURL(posixPath)
set encodedPath to my replaceText(posixPath, "%", "%25")
set encodedPath to my replaceText(encodedPath, " ", "%20")
set encodedPath to my replaceText(encodedPath, "#", "%23")
set encodedPath to my replaceText(encodedPath, "?", "%3F")
set encodedPath to my replaceText(encodedPath, "\"", "%22")
set encodedPath to my replaceText(encodedPath, "'", "%27")
set encodedPath to my replaceText(encodedPath, "<", "%3C")
set encodedPath to my replaceText(encodedPath, ">", "%3E")
return "file://" & encodedPath
end fileURL
on htmlEscape(theString)
set theString to theString as string
set theString to my replaceText(theString, "&", "&")
set theString to my replaceText(theString, "<", "<")
set theString to my replaceText(theString, ">", ">")
set theString to my replaceText(theString, "\"", """)
return theString
end htmlEscape
on replaceText(theString, findString, replaceString)
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to findString
set textParts to text items of theString
set AppleScript's text item delimiters to replaceString
set theString to textParts as string
set AppleScript's text item delimiters to oldDelims
return theString
end replaceText
on joinList(theList, theDelimiter)
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to theDelimiter
set joinedText to theList as string
set AppleScript's text item delimiters to oldDelims
return joinedText
end joinList
on formatDate(theDate)
try
return short date string of theDate
on error
return ""
end try
end formatDate
on htmlHeader()
set cssText to "body{font-family:Georgia,'Times New Roman',serif;background:#f4f1ea;color:#222;margin:0;padding:30px 40px;}" & ¬
"h1{font-size:2.6em;border-bottom:4px double #333;padding-bottom:8px;margin-bottom:4px;}" & ¬
".subtitle{font-style:italic;color:#666;margin-bottom:30px;}" & ¬
"h2{font-family:'Helvetica Neue',Arial,sans-serif;font-size:1.3em;margin-top:44px;border-bottom:1px solid #999;padding-bottom:4px;}" & ¬
".stats{font-family:'Helvetica Neue',Arial,sans-serif;font-size:0.8em;color:#777;margin-bottom:14px;}" & ¬
".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:20px;}" & ¬
".card{background:#fff;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.15);overflow:hidden;display:flex;flex-direction:column;}" & ¬
".card img{width:100%;height:180px;object-fit:cover;background:#ddd;display:block;}" & ¬
".card.pdf img{object-fit:contain;object-position:top;background:#eee;}" & ¬
".body{padding:12px 14px;}" & ¬
".card h3{font-size:1.05em;margin:0 0 6px 0;line-height:1.25;}" & ¬
".card h3 a{color:#111;text-decoration:none;}" & ¬
".card h3 a:hover{text-decoration:underline;}" & ¬
".meta{font-family:'Helvetica Neue',Arial,sans-serif;font-size:0.72em;color:#888;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;}" & ¬
".summary{font-size:0.9em;line-height:1.45;color:#333;}" & ¬
".card.text{border-top:4px solid #8a5a44;}" & ¬
".card.image{border-top:4px solid #44708a;}" & ¬
".card.pdf{border-top:4px solid #a33;}" & ¬
".card.other{border-top:4px solid #888;}" & ¬
".more{font-style:italic;color:#888;margin-top:10px;}"
return "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Magazine Overview</title><style>" & cssText & "</style></head><body>"
end htmlHeader
Alternatively a skill for DEVONthink, see below. Again created by simply pasting your post and using Claude Opus 5.5, this time in Settings > AI > Library.
That’s almost shocking. I’m fiddling around with this task for some hours now and haven’t even understood it as the AI has. OTOH, I’m learning a lot on the way, so the time was well spend.
As to the code: Some comments might not have harmed. And perhaps thumbnails (QuickLook?) for those documents for which them are possible (yes, that was not in the requirements).
And I’d of course have used something like list.join(theDelimiter), theString.replace(findString, replaceString), encodeURIComponent, theString.truncate(maxLength) etc instead of verbose functions Perhaps even `cleanText = recText.replaceAll(/[\n\t\r]/g,’ ').replaceAll(" “,” "). But I guess an AI doesn’t mind writing such code over and over again. Or even check the length for a truncation condition twice… Ah, well. It’s fast.
Learning how to quickly and efficiently use AI is also time well spent, IMHO. In the end it’s just another tool, like a compiler or editor is. And times are changing or would anyone still prefer to do everything using punched card programming or assembler?
P.S: And I did a lot of assembler when I was young(er)
Applescript AI Summary - this one didn’t generate what I expected. The result was exactly the same as Excerpt, and it took the same time to run, and no summary…
The new Skill.
This was interesting. I ran the following prompt “using skill magazine create a magazine for the contents of record “:1978 TEST Randonization” in database TESTING”
The md had a neat summary, but I guess I have to adjust the skill (?) because I really want something different (more at the bottom).
List everything inside a group, showing a thumbnail and a 1-2 line description of the metadata of every record inside, including groups. If the record is an image or a pdf without a text layer, analyze the image and provide a 1-3 line summary of what the image or pdf is. If the record has text, summarize it.
I am sure this is horribly deficient, but it is a start.
List everything inside “1978 TEST Randonization” in the TESTING database., showing a thumbnail and a 1-2 line description of the metadata of every record inside, including groups. If the record is an image or a pdf without a text layer, analyze the image and provide a 1-3 line summary of what the image or pdf is. If the record has text, summarize it.
The results:
“1978 TEST Randonization”contains 123 records: 116 images, 5 PDFs, and 2 AppleScript records. It has no child groups.
I can’t embed actual thumbnails in a complete catalog with the available tools: I can inspect an image, but can’t export its image data for use in an HTML or Markdown list. The 116 images would also make a very long catalog.
Would you like me to provide the inventory here in chat, with linked titles and descriptions based on available title and metadata, or create a report record without embedded thumbnails?
Which provider & model do you use in DEVONthink? See Settings > AI > Chat. By default the skill uses Claude Sonnet 5.5, this can be changed in Settings > AI > Library. To use it, simply type /magazine in the chat assistant, the groups should be selected first. It will create a document in your database (just like the script)