first commit
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# `client/python-audio-service.ts` — `PythonAudioService`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`PythonAudioService` is the typed HTTP client TypeScript and browser callers use to talk to the Python-tier audio fingerprinting service. It wraps `fetch`, handles multipart `FormData` uploads, propagates `AbortController`-based timeouts, and translates the FastAPI error envelope (`{detail: string}`) into well-typed JavaScript `Error` instances with informative messages. The class is the only runtime export of the `@suaveplan/audio-fingerprint/client` subpath, and it is also re-exported from the package root.
|
||||
|
||||
The client deliberately depends on nothing beyond the platform `fetch`, `FormData`, and `AbortController` APIs. Browsers (Chrome, Firefox, Safari 14+, Edge) and modern server runtimes (Node 18+, Bun, Deno) all expose these, so the same code runs unchanged in every consumer tier. There is no axios, no isomorphic-fetch polyfill, and no transitive dependency closure to audit.
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable `baseUrl` (default `http://localhost:8000`) and `timeout` in ms (default `30000`).
|
||||
- `healthCheck()` — `GET /`, returns the typed `HealthCheckResponse`.
|
||||
- `fingerprint(req)` — `POST /fingerprint` with `file` plus optional `peak_threshold` / `fan_value`.
|
||||
- `match(req)` — `POST /match` with `query_file`, `reference_file`, plus optional `peak_threshold` / `fan_value` / `min_matches`.
|
||||
- `AbortController`-based timeouts; on expiry the client rejects with `Error("Request timeout after Nms: <endpoint>")`.
|
||||
- Error parsing: HTTP-non-OK responses are parsed as JSON; if the body is `{detail: "..."}`, that string becomes the `Error.message`. Otherwise the client falls back to `HTTP <status>: <statusText>`.
|
||||
- Non-`Error` thrown values from `fetch` are normalised to `Error("Unknown error occurred: <string>")`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { PythonAudioService } from "@suaveplan/audio-fingerprint/client";
|
||||
|
||||
const client = new PythonAudioService();
|
||||
const health = await client.healthCheck();
|
||||
// { status: "ok", service: "audio-fingerprinting" }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { PythonAudioService } from "@suaveplan/audio-fingerprint";
|
||||
import type {
|
||||
FingerprintResponse,
|
||||
MatchResponse,
|
||||
} from "@suaveplan/audio-fingerprint/types";
|
||||
|
||||
const client = new PythonAudioService({
|
||||
baseUrl: "https://audio-fp.internal.example.com",
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
async function identify(query: File, reference: File): Promise<MatchResponse> {
|
||||
try {
|
||||
const queryFingerprint: FingerprintResponse = await client.fingerprint({
|
||||
file: query,
|
||||
peak_threshold: 0.55,
|
||||
fan_value: 18,
|
||||
});
|
||||
// ... persist queryFingerprint to corpus if needed
|
||||
return await client.match({
|
||||
query_file: query,
|
||||
reference_file: reference,
|
||||
peak_threshold: 0.55,
|
||||
fan_value: 18,
|
||||
min_matches: 25,
|
||||
});
|
||||
} catch (error) {
|
||||
// The thrown message is either the FastAPI `detail`, a status-text
|
||||
// fallback, or the timeout / unknown-error envelope.
|
||||
console.error("identification failed:", (error as Error).message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor
|
||||
|
||||
```ts
|
||||
constructor(config?: AudioFingerprintConfig)
|
||||
```
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `config.baseUrl` | `string` | `"http://localhost:8000"` | Base URL of the FastAPI service. |
|
||||
| `config.timeout` | `number` | `30000` | Per-request timeout in milliseconds. |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Signature | Throws |
|
||||
|---|---|---|
|
||||
| `healthCheck` | `() => Promise<HealthCheckResponse>` | `Error` on HTTP error, timeout, or network failure. |
|
||||
| `fingerprint` | `(req: FingerprintRequest) => Promise<FingerprintResponse>` | `Error` on HTTP error, timeout, or network failure. |
|
||||
| `match` | `(req: MatchRequest) => Promise<MatchResponse>` | `Error` on HTTP error, timeout, or network failure. |
|
||||
|
||||
### Error envelope
|
||||
|
||||
| Cause | `Error.message` |
|
||||
|---|---|
|
||||
| Timeout | `Request timeout after <Nms>: <endpoint>` |
|
||||
| HTTP non-OK with parseable `{detail: string}` body | `<detail>` |
|
||||
| HTTP non-OK with non-JSON or `detail`-missing body | `HTTP <status>: <statusText>` |
|
||||
| `fetch` throws a non-`Error` value | `Unknown error occurred: <stringified value>` |
|
||||
| `fetch` throws a non-Abort `Error` | the original error is re-raised verbatim |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The timeout is implemented with a `setTimeout` that calls `controller.abort()`; the timer is cleared both on success and on failure (try/catch + redundant `clearTimeout` after the `await`). This avoids leaking timers in long-lived browsers tabs.
|
||||
- `AbortError` is detected by `error.name === "AbortError"` rather than by `instanceof DOMException` to remain runtime-agnostic (DOMException is browser-only).
|
||||
- Optional form fields are only appended when the caller supplies them; the FastAPI defaults remain authoritative.
|
||||
- The error-body JSON parse is wrapped in a try/catch with no rethrow inside the catch — a parse failure should NOT mask the original HTTP error, only fall through to the status-text fallback.
|
||||
- Type-only imports from `../types/audio-fingerprint.types.js` are used so the client carries zero runtime cost from the type definitions.
|
||||
- The client has no peer dependency on any framework; consumers wrap it in their own React/Vue/Svelte hooks as appropriate. The sibling `@suaveplan/react-audio-fingerprint` package's `python-backend` subpath is the canonical React adapter.
|
||||
Reference in New Issue
Block a user