Release 0.30.0: dedupe plaud-category, add plaud-industry (#65)

Follow-up to #61: suppress plaud-category when it duplicates plaud-template, capture Plaud industry_category as a separate plaud-industry property. Closes #61.
This commit is contained in:
Charles Kelsoe 2026-07-09 19:43:46 -04:00 committed by GitHub
parent 0e0db57fd4
commit 05f0ea67e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 146 additions and 13 deletions

View file

@ -4,6 +4,16 @@ All notable changes to Plaud Importer will be documented in this file.
## [Unreleased]
## [0.30.0] - 2026-07-09
### Added
- **New `plaud-industry` frontmatter property.** Plaud carries a topical/industry classification alongside each summary, separate from the summary category. When present it is now written as its own `plaud-industry` property instead of being folded into `plaud-category`, so the two never mix. It is left off for recordings where Plaud provides no industry value. A matching `{{industry}}` token is available in the extra-frontmatter value field.
### Fixed
- **The `plaud-category` property no longer duplicates `plaud-template`.** Plaud's current summary format stores the template (summary-type) name in the same field the plugin reads for the category, so both properties came out with the identical value, for example both showing "Deep Summary Transcript". The plugin now omits `plaud-category` when its value would only repeat `plaud-template`. If Plaud ever provides a category that genuinely differs from the template, it is written as before.
## [0.29.0] - 2026-07-09
### Fixed

View file

@ -1282,8 +1282,9 @@ describe('formatFrontmatter', () => {
makeSummary({
headline: 'Q2 Planning',
category: 'ai-meeting',
industry: 'Technology',
language: 'en',
template: 'ai-meeting',
template: 'Meeting Minutes',
model: 'azure-sweden-central-gpt-5',
noteId: 'note-abc',
summaryId: 'sum-xyz',
@ -1293,17 +1294,60 @@ describe('formatFrontmatter', () => {
// yamlScalar leaves `Q2 Planning` unquoted because the pattern
// allows letters / digits / spaces. `azure-sweden-central-gpt-5`
// is also pattern-safe (hyphens allowed). `"3"` gets quoted only
// because it leads with a digit.
// because it leads with a digit. category differs from template here,
// so it is not deduped (see the dedup test below).
expect(fm).toContain('plaud-headline: Q2 Planning');
expect(fm).toContain('plaud-category: ai-meeting');
expect(fm).toContain('plaud-industry: Technology');
expect(fm).toContain('plaud-language: en');
expect(fm).toContain('plaud-template: ai-meeting');
expect(fm).toContain('plaud-template: Meeting Minutes');
expect(fm).toContain('plaud-model: azure-sweden-central-gpt-5');
expect(fm).toContain('plaud-note-id: note-abc');
expect(fm).toContain('plaud-summary-id: sum-xyz');
expect(fm).toContain('plaud-summary-version: "3"');
});
it('omits plaud-category when it duplicates plaud-template (issue #61)', () => {
const fm = formatFrontmatter(
makeRecording(),
[],
makeSummary({
category: 'Deep Summary Transcript',
template: 'Deep Summary Transcript',
}),
);
// Plaud's category field now mirrors the template name; the duplicate
// category line is suppressed while the template line stays.
expect(fm).not.toMatch(/plaud-category:/);
expect(fm).toContain('plaud-template: Deep Summary Transcript');
});
it('keeps plaud-category when it differs from plaud-template', () => {
const fm = formatFrontmatter(
makeRecording(),
[],
makeSummary({ category: 'Legal', template: 'Deep Summary Transcript' }),
);
expect(fm).toContain('plaud-category: Legal');
expect(fm).toContain('plaud-template: Deep Summary Transcript');
});
it('emits plaud-industry from the industry field, separate from category', () => {
const fm = formatFrontmatter(
makeRecording(),
[],
makeSummary({
category: 'Deep Summary Transcript',
industry: 'Healthcare',
template: 'Deep Summary Transcript',
}),
);
// category duplicates template and is dropped, but the real topical
// classification survives as its own property.
expect(fm).not.toMatch(/plaud-category:/);
expect(fm).toContain('plaud-industry: Healthcare');
});
it('emits partial extras and omits the rest', () => {
const fm = formatFrontmatter(
makeRecording(),

View file

@ -71,6 +71,7 @@ describe('findSummaryMetadata (issue #61 — newer summary path)', () => {
model: 'gpt-5.5',
headline: 'Example headline for the recording',
category: 'Adaptive Summary',
industry: 'redacted-industry',
summaryId: '20260708183602-v2@redacted',
});
});
@ -87,6 +88,45 @@ describe('findSummaryMetadata (issue #61 — newer summary path)', () => {
);
});
it('resolves industry_category separately from category, even when category mirrors the template name', () => {
const distinct = {
data: {
content_list: [
{
data_type: 'auto_sum_note',
extra: { used_template: { template_name: 'Deep Summary Transcript' } },
},
],
extra_data: {
model: 'gpt-5.5',
aiContentHeader: {
category: 'Deep Summary Transcript',
industry_category: 'Healthcare',
},
},
},
};
const md = findSummaryMetadata(distinct, endpoint);
expect(md.template).toBe('Deep Summary Transcript');
expect(md.category).toBe('Deep Summary Transcript');
expect(md.industry).toBe('Healthcare');
});
it('omits industry when aiContentHeader has no industry_category', () => {
const noIndustry = {
data: {
content_list: [
{ data_type: 'auto_sum_note', extra: { summ_type: 'meeting' } },
],
extra_data: {
model: 'gpt-5.5',
aiContentHeader: { headline: 'H', category: 'Meeting' },
},
},
};
expect(findSummaryMetadata(noIndustry, endpoint).industry).toBeUndefined();
});
it('does not bleed category into a nested per-question category when the header lacks a direct one', () => {
const noDirectCategory = {
data: {

View file

@ -325,6 +325,7 @@ const FOLDER_INSERT_TOKEN: readonly [string, string] = ["Folder", "{{plaud-folde
const CONTENT_INSERT_TOKENS: ReadonlyArray<readonly [string, string]> = [
["Duration", "{{duration}}"],
["Category", "{{category}}"],
["Industry", "{{industry}}"],
["Headline", "{{headline}}"],
["Language", "{{language}}"],
["Summary template", "{{template}}"],
@ -343,6 +344,7 @@ const CUSTOM_FRONTMATTER_TOKENS: ReadonlyArray<readonly [string, string]> = [
["{{plaud-folder}}", "the recording's Plaud folder name"],
["{{duration}}", "the recording length, for example 30m"],
["{{category}}", "the summary's category (empty with no AI summary)"],
["{{industry}}", "the summary's industry or topic (empty with no AI summary)"],
["{{headline}}", "the summary's one-line headline"],
["{{YYYY}} {{MM}} {{DD}} {{Q}} {{WW}}", "the date set, same as the other fields"],
];

View file

@ -1,7 +1,7 @@
{
"id": "plaud-importer",
"name": "Plaud Importer",
"version": "0.29.0",
"version": "0.30.0",
"minAppVersion": "1.11.4",
"description": "Import meeting summaries, transcripts, and attachments from Plaud.AI into your vault.",
"author": "Charles Kelsoe (ckelsoe)",

View file

@ -1220,6 +1220,7 @@ export const RESERVED_FRONTMATTER_KEYS: ReadonlySet<string> = new Set([
'plaud-version-ms',
'plaud-headline',
'plaud-category',
'plaud-industry',
'plaud-language',
'plaud-template',
'plaud-model',
@ -1245,6 +1246,7 @@ export interface CustomFrontmatterContext {
readonly folderName: string;
readonly durationSeconds: number;
readonly category: string;
readonly industry: string;
readonly headline: string;
readonly language: string;
readonly template: string;
@ -1265,6 +1267,7 @@ export function customFrontmatterContext(
? Math.max(0, Math.floor(recording.durationSeconds))
: 0,
category: summary?.category ?? '',
industry: summary?.industry ?? '',
headline: summary?.headline ?? '',
language: summary?.language ?? '',
template: summary?.template ?? '',
@ -1288,6 +1291,7 @@ export function expandCustomFrontmatterValue(
const content: Record<string, string> = {
duration: formatDurationHoursMinutes(ctx.durationSeconds),
category: ctx.category,
industry: ctx.industry,
headline: ctx.headline,
language: ctx.language,
template: ctx.template,
@ -1328,6 +1332,7 @@ export const TEMPLATE_PREVIEW_CUSTOM_CONTEXT: CustomFrontmatterContext = {
folderName: TEMPLATE_PREVIEW_FOLDER,
durationSeconds: 1830,
category: 'Meeting',
industry: 'Technology',
headline: 'Weekly team sync recap',
language: 'en',
template: 'Meeting notes',
@ -1445,9 +1450,20 @@ export function formatFrontmatter(
// Plaud shape that drops one of these fields. Add new known extras
// here without changing any other call site.
if (summary) {
// Plaud's `category` field mirrors the template/summary-type name in the
// current API shape (issue #61), so emitting both yields an identical
// duplicate. Suppress plaud-category when it exactly equals the template;
// a genuinely distinct category (or a future Plaud divergence) still comes
// through. Plaud's real topical classification lives in `industry` and is
// emitted separately as plaud-industry, never mixed into category.
const category =
summary.category !== undefined && summary.category === summary.template
? undefined
: summary.category;
const extras: ReadonlyArray<readonly [string, string | undefined]> = [
['plaud-headline', summary.headline],
['plaud-category', summary.category],
['plaud-category', category],
['plaud-industry', summary.industry],
['plaud-language', summary.language],
['plaud-template', summary.template],
['plaud-model', summary.model],

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-plaud-importer",
"version": "0.29.0",
"version": "0.30.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-plaud-importer",
"version": "0.29.0",
"version": "0.30.0",
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.39.2",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-plaud-importer",
"version": "0.29.0",
"version": "0.30.0",
"description": "Import meeting summaries, transcripts, and attachments from Plaud.AI into an Obsidian vault.",
"main": "main.js",
"scripts": {

View file

@ -1791,13 +1791,21 @@ export function findAiKeywords(
/**
* The optional {@link Summary} fields Plaud carries as metadata around a
* generated summary. Doubles as the checklist the bundle fetch uses to
* detect shape drift: a summary present with every one of these unresolved
* is the drift signal (issue #61).
* generated summary. The subset that must resolve on every AI summary is the
* shape-drift checklist, tracked separately in {@link DRIFT_SIGNAL_FIELDS};
* `language` and `industry` are deliberately excluded there because Plaud
* legitimately omits them, so their absence is never treated as drift (issue
* #61).
*/
export type SummaryMetadata = Pick<
Summary,
'headline' | 'category' | 'language' | 'template' | 'model' | 'summaryId'
| 'headline'
| 'category'
| 'industry'
| 'language'
| 'template'
| 'model'
| 'summaryId'
>;
// Fields expected on every AI-generated summary. When a summary IS present,
@ -1963,12 +1971,17 @@ export function findSummaryMetadata(
const model = resolveString(modelScope, ['model'], 0);
const headline = resolveString(headerScope, ['headline'], 0);
const category = resolveString(headerScope, ['category'], 0);
// `industry_category` is Plaud's real topical classification, kept separate
// from `category` (which now mirrors the template name). Same direct-only
// scope as `category` so it can't bleed into an unrelated nested key.
const industry = resolveString(headerScope, ['industry_category'], 0);
const summaryId = resolveString(idScope, ['summary_id'], 0);
const language = resolveString(modelScope, ['language'], 0);
return {
...(headline !== undefined && { headline }),
...(category !== undefined && { category }),
...(industry !== undefined && { industry }),
...(language !== undefined && { language }),
...(template !== undefined && { template }),
...(model !== undefined && { model }),

View file

@ -273,6 +273,13 @@ export interface Summary {
*/
readonly headline?: string;
readonly category?: string;
/**
* Plaud's `industry_category`: a topical/industry classification distinct
* from `category` (which in the current Plaud shape mirrors the template
* name). Surfaced as its own `plaud-industry` property rather than folded
* into `category` so the two never mix. Best-effort like the rest here.
*/
readonly industry?: string;
readonly language?: string;
readonly template?: string;
readonly model?: string;

View file

@ -52,5 +52,6 @@
"0.27.1": "1.11.4",
"0.27.2": "1.11.4",
"0.28.0": "1.11.4",
"0.29.0": "1.11.4"
"0.29.0": "1.11.4",
"0.30.0": "1.11.4"
}