Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
14d8c34
refactor: rename transcribedText field to transcript
braden-w Nov 29, 2025
2139609
feat(db): add Recording schema versioning and migration from V6 to V7
braden-w Nov 29, 2025
e40b60b
refactor(db): use explicit type for RecordingV7 to avoid arktype re-e…
braden-w Nov 29, 2025
601362d
refactor(recordings): follow TransformationStep pattern for migrating…
braden-w Nov 29, 2025
0ba678f
refactor(recordings): use typeof RecordingV7.infer for consistency
braden-w Nov 29, 2025
109ce96
refactor(db): use .modify() for Dexie migration and Recording validat…
braden-w Nov 29, 2025
b8b22c3
refactor: Recording type consolidation
braden-w Nov 29, 2025
6458e9b
refactor(db): use .modify() for V4 migration, keep clear/bulkAdd for V5
braden-w Nov 29, 2025
e0418f0
refactor(db): simplify parseMarkdownToRecording to defer to Recording…
braden-w Nov 29, 2025
14936c4
refactor(db): return Result from parseMarkdownToRecording
braden-w Nov 29, 2025
0a644ec
docs(db): improve JSDoc for Recording validator
braden-w Nov 29, 2025
575dff0
refactor(db): freeze Dexie schema types as explicit snapshots
braden-w Nov 29, 2025
7d3096e
refactor(db): derive V5/V6 Dexie schemas from arktype validators
braden-w Nov 29, 2025
ec34e15
fix(db): hardcode version numbers in Dexie migrations
braden-w Nov 29, 2025
cf9d5e2
docs: add article on freezing schema types and improve writing guidel…
braden-w Nov 29, 2025
e7b10a6
refactor(db): inline migration logic in TransformationStep validator
braden-w Nov 30, 2025
a3988d0
refactor(db): derive Recording type from migrating validator
braden-w Nov 30, 2025
e0b0ca5
refactor(db): simplify transformation schema exports and migration logic
braden-w Nov 30, 2025
ad3db33
docs: update freezing schema types and migrate on read
braden-w Nov 30, 2025
a9ca697
docs: update migration articles to use Recording example
braden-w Dec 1, 2025
67617e4
Merge pull request #1050 from EpicenterHQ/feat/recording-migration-v7
braden-w Dec 1, 2025
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
4 changes: 2 additions & 2 deletions apps/whispering/src/lib/components/MigrationDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
'This is a medium-length recording with a bit more content to transcribe and process.',
`This is a longer recording transcript. It contains multiple sentences and paragraphs of content. ${Array(10).fill('Lorem ipsum dolor sit amet, consectetur adipiscing elit.').join(' ')}`,
];
const transcribedText = textLengths[index % textLengths.length];
const transcript = textLengths[index % textLengths.length];

const id = nanoid();
const now = new Date().toISOString();
Expand All @@ -91,7 +91,7 @@
timestamp: timestampStr,
createdAt: now,
updatedAt: now,
transcribedText,
transcript,
transcriptionStatus,
serializedAudio: _generateMockAudio(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* ```svelte
* <TranscriptDialog
* recordingId={recording.id}
* transcribedText={recording.transcribedText}
* transcript={recording.transcript}
* rows={1}
* />
* ```
Expand All @@ -21,29 +21,29 @@
/** The ID of the recording whose transcript is being displayed */
recordingId,
/** The transcript content to display */
transcribedText,
transcript,
/** Number of rows for the preview textarea (default: 2) */
rows = 2,
/** Whether the dialog trigger is disabled */
disabled = false,
}: {
recordingId: string;
transcribedText: string;
transcript: string;
rows?: number;
disabled?: boolean;
} = $props();

const id = getRecordingTransitionId({
recordingId,
propertyName: 'transcribedText',
propertyName: 'transcript',
});
</script>

<TextPreviewDialog
{id}
title="Transcript"
label="transcript"
text={transcribedText}
text={transcript}
{rows}
{disabled}
/>
8 changes: 5 additions & 3 deletions apps/whispering/src/lib/query/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { nanoid } from 'nanoid/non-secure';
import { Err, Ok } from 'wellcrafted/result';
import { fromTaggedError, WhisperingErr } from '$lib/result';
import { DbServiceErr } from '$lib/services/db';
import { CURRENT_RECORDING_VERSION } from '$lib/services/db/models';
import { settings } from '$lib/stores/settings.svelte';
import * as transformClipboardWindow from '../../routes/transform-clipboard/transformClipboardWindow.tauri';
import { rpc } from './';
Expand Down Expand Up @@ -623,12 +624,13 @@ async function processRecordingPipeline({

const recording = {
id: newRecordingId,
version: CURRENT_RECORDING_VERSION,
title: '',
subtitle: '',
timestamp: now,
createdAt: now,
updatedAt: now,
transcribedText: '',
transcript: '',
transcriptionStatus: 'UNPROCESSED',
} as const;

Expand Down Expand Up @@ -661,7 +663,7 @@ async function processRecordingPipeline({
description: 'Your recording is being transcribed...',
});

const { data: transcribedText, error: transcribeError } =
const { data: transcript, error: transcribeError } =
await transcription.transcribeRecording.execute(recording);

if (transcribeError) {
Expand All @@ -681,7 +683,7 @@ async function processRecordingPipeline({
sound.playSoundIfEnabled.execute('transcriptionComplete');

await delivery.deliverTranscriptionResult.execute({
text: transcribedText,
text: transcript,
toastId: transcribeToastId,
});

Expand Down
6 changes: 3 additions & 3 deletions apps/whispering/src/lib/query/transcription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const transcription = {
},
});
}
const { data: transcribedText, error: transcribeError } =
const { data: transcript, error: transcribeError } =
await transcribeBlob(audioBlob);
if (transcribeError) {
const { error: setRecordingTranscribingError } =
Expand All @@ -77,7 +77,7 @@ export const transcription = {
const { error: setRecordingTranscribedTextError } =
await db.recordings.update.execute({
...recording,
transcribedText,
transcript,
transcriptionStatus: 'DONE',
});
if (setRecordingTranscribedTextError) {
Expand All @@ -91,7 +91,7 @@ export const transcription = {
},
});
}
return Ok(transcribedText);
return Ok(transcript);
},
}),

Expand Down
2 changes: 1 addition & 1 deletion apps/whispering/src/lib/query/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const transformer = {

const { data: transformationRun, error: transformationRunError } =
await runTransformation({
input: recording.transcribedText,
input: recording.transcript,
transformation,
recordingId,
});
Expand Down
89 changes: 42 additions & 47 deletions apps/whispering/src/lib/services/db/file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,52 @@ import {
} from '@tauri-apps/plugin-fs';
import { type } from 'arktype';
import matter from 'gray-matter';
import { Ok, tryAsync } from 'wellcrafted/result';
import { Err, Ok, tryAsync, type Result } from 'wellcrafted/result';
import { getExtensionFromMimeType } from '$lib/constants/mime';
import { PATHS } from '$lib/constants/paths';
import * as services from '$lib/services';
import type { Recording } from './models';
import { Transformation, TransformationRun } from './models';
import { Recording, Transformation, TransformationRun } from './models';
import type { DbService } from './types';
import { DbServiceErr } from './types';

/**
* Schema validator for Recording front matter (everything except transcribedText)
*/
const RecordingFrontMatter = type({
id: 'string',
title: 'string',
subtitle: 'string',
timestamp: 'string',
createdAt: 'string',
updatedAt: 'string',
transcriptionStatus: '"UNPROCESSED" | "TRANSCRIBING" | "DONE" | "FAILED"',
});

type RecordingFrontMatter = typeof RecordingFrontMatter.infer;

/**
* Convert Recording to markdown format (frontmatter + body)
*
* Storage format:
* - Frontmatter: All metadata fields (id, title, subtitle, etc.) but NOT version
* - Body: The transcript text
*
* Version is excluded from frontmatter because desktop files don't need versioning;
* they always store transcript in the body (never had transcribedText in frontmatter).
* When reading, we use the Recording validator which handles any schema version.
*/
function recordingToMarkdown(recording: Recording): string {
const { transcribedText, ...frontMatter } = recording;
return matter.stringify(transcribedText ?? '', frontMatter);
const { transcript, version: _version, ...frontMatter } = recording;
return matter.stringify(transcript ?? '', frontMatter);
}

/**
* Convert markdown file (YAML frontmatter + body) to Recording
* Parse markdown file content into a Recording using the migrating validator.
*
* Desktop files are treated as V6 schema (no version field stored, body = transcript).
* The Recording validator defaults version to 6, then migrates V6 → V7 automatically.
*/
function markdownToRecording({
frontMatter,
body,
}: {
frontMatter: RecordingFrontMatter;
body: string;
}): Recording {
return {
function parseMarkdownToRecording(
content: string,
): Result<Recording, { summary: string }> {
const { data: frontMatter, content: body } = matter(content);

// Recording validator accepts V6 or V7 input and always outputs V7
const result = Recording({
...frontMatter,
transcribedText: body,
};
transcribedText: body, // V6 schema: body maps to transcribedText, version defaults to 6
transcript: body, // V7 schema: body maps to transcript
});

if (result instanceof type.errors) {
return Err({ summary: result.summary });
}
return Ok(result);
}

/**
Expand Down Expand Up @@ -101,17 +101,14 @@ export function createFileSystemDb(): DbService {
// Use Rust command to read all markdown files at once
const contents = await readMarkdownFiles(recordingsPath);

// Parse all files
// Parse all files using the Recording validator (handles V6→V7 migration)
const recordings = contents.map((content) => {
const { data, content: body } = matter(content);

// Validate the front matter schema
const frontMatter = RecordingFrontMatter(data);
if (frontMatter instanceof type.errors) {
const { data, error } = parseMarkdownToRecording(content);
if (error) {
console.error('Invalid recording:', error.summary);
return null; // Skip invalid recording, don't crash the app
}

return markdownToRecording({ frontMatter, body });
return data;
});

// Filter out any null entries and sort by timestamp (newest first)
Expand Down Expand Up @@ -180,17 +177,15 @@ export function createFileSystemDb(): DbService {
if (!fileExists) return null;

const content = await readTextFile(mdPath);
const { data, content: body } = matter(content);

// Validate the front matter schema
const frontMatter = RecordingFrontMatter(data);
if (frontMatter instanceof type.errors) {
throw new Error(
`Invalid recording front matter: ${frontMatter.summary}`,
);
// Parse using the Recording validator (handles V6→V7 migration)
const { data, error } = parseMarkdownToRecording(content);
if (error) {
const { summary } = error;
throw new Error(`Invalid recording: ${summary}`);
}

return markdownToRecording({ frontMatter, body });
return data;
},
catch: (error) =>
DbServiceErr({
Expand Down
1 change: 1 addition & 0 deletions apps/whispering/src/lib/services/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
TransformationStepRun,
} from './models';
export {
CURRENT_RECORDING_VERSION,
generateDefaultTransformation,
generateDefaultTransformationStep,
TRANSFORMATION_STEP_TYPES,
Expand Down
10 changes: 6 additions & 4 deletions apps/whispering/src/lib/services/db/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// Recordings
export type {
export {
CURRENT_RECORDING_VERSION,
Recording,
} from './recordings';
export type {
RecordingStoredInIndexedDB,
RecordingsDbSchemaV1,
RecordingsDbSchemaV2,
RecordingsDbSchemaV3,
RecordingsDbSchemaV4,
RecordingsDbSchemaV5,
RecordingsDbSchemaV6,
SerializedAudio,
} from './recordings';
// Transformation Runs
Expand All @@ -21,8 +25,7 @@ export {
TransformationStepRunRunning,
} from './transformation-runs';
export type {
TransformationStepV1,
TransformationStepV2,
TransformationStep,
TransformationV1,
TransformationV2,
} from './transformations';
Expand All @@ -33,5 +36,4 @@ export {
TRANSFORMATION_STEP_TYPES,
TRANSFORMATION_STEP_TYPES_TO_LABELS,
Transformation,
TransformationStep,
} from './transformations';
Loading
Loading