Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 16 additions & 4 deletions examples/audiobook-curator/src/evidence.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { lstat, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { lstat, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { basename, dirname, join, resolve } from 'node:path';

import type { JsonObject, JsonValue } from '@agent-bundle/runtime';
Expand Down Expand Up @@ -156,6 +155,19 @@ const pythonMatcher = (python: string, process: MediaProcess): AcousticMatcher =
}
};

// Evidence staging holds decoded audio, and os.tmpdir() is commonly a
// RAM-backed tmpfs on Linux; media work must stay on regular disk. Stage
// beside the requested receipt when one exists, otherwise beside the source
// media, mirroring the sibling modules' same-directory staging convention.
const evidenceWorkDir = async (
input: Readonly<{ file: string; receipt?: string }>,
prefix: string,
): Promise<string> => {
const root = dirname(resolve(input.receipt ?? input.file));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate the receipt into acoustic-identify staging

When acoustic-identify is given a receipt path, the per-candidate object passed to sampleMatch omits input.receipt, so this fallback always chooses the source-media directory. If the receipt was placed on a disk because the source is on tmpfs, low on space, or read-only, identification still stages on—or fails against—the source filesystem, defeating the purpose of this change; forward the receipt path into each sampleMatch call.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed on main (merge af1c185).

await mkdir(root, { recursive: true });
return mkdtemp(join(root, prefix));
};

const productUrl = (region: AudibleRegion, asin: string): string => {
const groups = new URLSearchParams({ response_groups: 'contributors,media,product_desc,product_extended_attrs,sample' });
return `https://${audibleHosts[region]}/1.0/catalog/products/${encodeURIComponent(asin)}?${groups.toString()}`;
Expand Down Expand Up @@ -184,7 +196,7 @@ const sampleMatch = async (
if (sampleUrl === undefined || sampleUrl === '') throw new CuratorError('Audible candidate has no sample URL');
const bytes = await requestWithAttempts(http, sampleUrl, attempts, { binary: true, signal: dependencies.signal });
if (!Buffer.isBuffer(bytes)) throw new CuratorError('Audible sample response is not binary.');
const work = await mkdtemp(join(tmpdir(), 'audiobook-curator-acoustic-'));
const work = await evidenceWorkDir(input, '.audiobook-curator-acoustic-');
const sample = join(work, 'sample.mp3');
try {
await writeFile(sample, bytes, { mode: 0o600 });
Expand Down Expand Up @@ -327,7 +339,7 @@ export const verifyWithWhisper = async (
const windowSeconds = Math.max(1, input.windowSeconds ?? 35);
const minimumChars = Math.max(1, input.minimumChars ?? 80);
const process = dependencies.process ?? runMediaProcess;
const work = await mkdtemp(join(tmpdir(), 'audiobook-curator-whisper-'));
const work = await evidenceWorkDir(input, '.audiobook-curator-whisper-');
const windows: WhisperWindow[] = [];
try {
for (const [offset, fraction] of whisperSamplingFractions(maximumWindows).entries()) {
Expand Down
37 changes: 35 additions & 2 deletions examples/audiobook-curator/tests/evidence-parity.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { dirname, join } from 'node:path';

import { afterEach, describe, expect, it } from '@rstest/core';

Expand Down Expand Up @@ -85,6 +85,39 @@ describe('optional identity evidence parity', () => {
expect(receipt).toMatchObject({ exitCode: 0, identified: { asin: 'MATCH' }, verifiedRecording: true });
});

it('stages evidence work directories on regular disk, never under os.tmpdir()', async () => {
const root = await mkdtemp(join(tmpdir(), 'curator-evidence-disk-'));
roots.push(root);
const file = join(root, 'library', 'book.m4b');
const receiptPath = join(root, 'receipts', 'acoustic.json');
await mkdir(dirname(file), { recursive: true });
await mkdir(dirname(receiptPath), { recursive: true });
await writeFile(file, 'book');
const sampleDirs: string[] = [];
const matcher: AcousticMatcher = async (_source, sample) => {
sampleDirs.push(dirname(sample));
return { found: true };
};
const http: CuratorHttpClient = async () => Buffer.from('sample');

await verifyAudibleSample(
{ asin: 'ASIN', file, receipt: receiptPath, sampleUrl: 'https://sample.example/book.mp3' },
{ http, matcher },
);
await verifyAudibleSample(
{ asin: 'ASIN', file, sampleUrl: 'https://sample.example/book.mp3' },
{ http, matcher },
);

const [besideReceipt, besideFile] = sampleDirs;
expect(besideReceipt!.startsWith(join(dirname(receiptPath), '.audiobook-curator-acoustic-'))).toBe(true);
expect(besideFile!.startsWith(join(dirname(file), '.audiobook-curator-acoustic-'))).toBe(true);
for (const workDir of sampleDirs) expect(workDir.startsWith(join(tmpdir(), 'audiobook-curator-'))).toBe(false);
// Staging is cleaned up even though it lives beside durable outputs.
expect((await readdir(dirname(receiptPath))).filter((entry) => entry.startsWith('.audiobook-curator-'))).toEqual([]);
expect((await readdir(dirname(file))).filter((entry) => entry.startsWith('.audiobook-curator-'))).toEqual([]);
});

it('extracts distributed PCM windows and returns review evidence without internal deadlines', async () => {
const root = await mkdtemp(join(tmpdir(), 'curator-whisper-'));
roots.push(root);
Expand Down
Loading