@h-ear/core 1.0.0-dev.202604132134 → 1.0.0-dev.202604132221
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -6
- package/dist/chunker.d.ts +3 -41
- package/dist/chunker.d.ts.map +1 -1
- package/dist/chunker.js +4 -129
- package/dist/chunker.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -86,16 +86,12 @@ const config: ServerConfig = {
|
|
|
86
86
|
| `apiUrl(config, path)` | Resolve full URL for a given API path |
|
|
87
87
|
| `ENVIRONMENTS` | Base URL map per environment |
|
|
88
88
|
| `HEAR_API` | API constants (formats, limits, noop callback URL) |
|
|
89
|
-
| `
|
|
90
|
-
|
|
91
|
-
## Large File Chunking
|
|
92
|
-
|
|
93
|
-
`splitAudioFile()` splits audio over 25 MB into 120-second overlapping chunks using `ffmpeg`. `mergeChunkResults()` deduplicates and merges the results. Requires `ffmpeg` + `ffprobe` on PATH — optional, only needed for local files over 25 MB.
|
|
89
|
+
| `getAudioDuration(filePath)` | Audio duration detection via ffprobe (returns 0 when unavailable) |
|
|
94
90
|
|
|
95
91
|
## Requirements
|
|
96
92
|
|
|
97
93
|
- Node.js >= 18
|
|
98
|
-
-
|
|
94
|
+
- ffprobe (optional — duration detection for upload routing)
|
|
99
95
|
|
|
100
96
|
## License
|
|
101
97
|
|
package/dist/chunker.d.ts
CHANGED
|
@@ -1,46 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Audio
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Overlap: 30s between chunks so boundary events are not lost.
|
|
2
|
+
* Audio duration detection via ffprobe.
|
|
3
|
+
* The ffmpeg-based splitAudioFile/mergeChunkResults chunker was removed —
|
|
4
|
+
* replaced by byte-slice upload in api-client.ts (submitClassifyChunked).
|
|
6
5
|
*/
|
|
7
|
-
import type { ClassifyResult } from './types.js';
|
|
8
|
-
/** Chunk duration (seconds). Keeps each chunk under API 25MB limit. */
|
|
9
|
-
export declare const CHUNK_DURATION_SEC = 120;
|
|
10
|
-
/** Chunk overlap (seconds). Preserves temporal connectivity at chunk boundaries. */
|
|
11
|
-
export declare const CHUNK_OVERLAP_SEC = 30;
|
|
12
|
-
/** Minimum trailing chunk duration (seconds). Chunks shorter than this are merged into the previous chunk. */
|
|
13
|
-
export declare const MIN_TRAILING_CHUNK_SEC = 5;
|
|
14
|
-
export interface ChunkInfo {
|
|
15
|
-
path: string;
|
|
16
|
-
startTime: number;
|
|
17
|
-
duration: number;
|
|
18
|
-
index: number;
|
|
19
|
-
}
|
|
20
|
-
/** Check if ffmpeg is available */
|
|
21
|
-
export declare function hasFFmpeg(): boolean;
|
|
22
6
|
/** Get audio duration in seconds using ffprobe */
|
|
23
7
|
export declare function getAudioDuration(filePath: string): number;
|
|
24
|
-
/**
|
|
25
|
-
* Split audio file into overlapping chunks using ffmpeg.
|
|
26
|
-
* Returns array of chunk file paths with their time offsets.
|
|
27
|
-
*/
|
|
28
|
-
export declare function splitAudioFile(filePath: string, totalDuration: number, options?: {
|
|
29
|
-
chunkDurationSec?: number;
|
|
30
|
-
overlapSec?: number;
|
|
31
|
-
}): ChunkInfo[];
|
|
32
|
-
/** Clean up chunk temp files */
|
|
33
|
-
export declare function cleanupChunks(chunks: ChunkInfo[]): void;
|
|
34
|
-
/**
|
|
35
|
-
* Merge classification results from multiple chunks into a unified result.
|
|
36
|
-
* Uses TIME-CHOP: each chunk owns a non-overlapping time window.
|
|
37
|
-
* Events outside the owned window are discarded — the overlap served its ML context purpose.
|
|
38
|
-
*
|
|
39
|
-
* Chunk N owns [N*step, (N+1)*step) where step = chunkDuration - overlap.
|
|
40
|
-
* Last chunk owns [start, totalDuration].
|
|
41
|
-
*/
|
|
42
|
-
export declare function mergeChunkResults(chunkResults: Array<{
|
|
43
|
-
chunk: ChunkInfo;
|
|
44
|
-
result: ClassifyResult;
|
|
45
|
-
}>, _originalFileName: string, totalDuration: number): ClassifyResult;
|
|
46
8
|
//# sourceMappingURL=chunker.d.ts.map
|
package/dist/chunker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,kDAAkD;AAClD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUzD"}
|
package/dist/chunker.js
CHANGED
|
@@ -1,29 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Audio
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Overlap: 30s between chunks so boundary events are not lost.
|
|
2
|
+
* Audio duration detection via ffprobe.
|
|
3
|
+
* The ffmpeg-based splitAudioFile/mergeChunkResults chunker was removed —
|
|
4
|
+
* replaced by byte-slice upload in api-client.ts (submitClassifyChunked).
|
|
6
5
|
*/
|
|
7
|
-
import { execSync
|
|
8
|
-
import { existsSync, mkdirSync, unlinkSync, rmdirSync } from 'fs';
|
|
9
|
-
import { join, basename } from 'path';
|
|
10
|
-
import { tmpdir } from 'os';
|
|
11
|
-
/** Chunk duration (seconds). Keeps each chunk under API 25MB limit. */
|
|
12
|
-
export const CHUNK_DURATION_SEC = 120;
|
|
13
|
-
/** Chunk overlap (seconds). Preserves temporal connectivity at chunk boundaries. */
|
|
14
|
-
export const CHUNK_OVERLAP_SEC = 30;
|
|
15
|
-
/** Minimum trailing chunk duration (seconds). Chunks shorter than this are merged into the previous chunk. */
|
|
16
|
-
export const MIN_TRAILING_CHUNK_SEC = 5;
|
|
17
|
-
/** Check if ffmpeg is available */
|
|
18
|
-
export function hasFFmpeg() {
|
|
19
|
-
try {
|
|
20
|
-
execSync('ffmpeg -version', { stdio: 'pipe', timeout: 5000 });
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
6
|
+
import { execSync } from 'child_process';
|
|
27
7
|
/** Get audio duration in seconds using ffprobe */
|
|
28
8
|
export function getAudioDuration(filePath) {
|
|
29
9
|
try {
|
|
@@ -34,109 +14,4 @@ export function getAudioDuration(filePath) {
|
|
|
34
14
|
return 0;
|
|
35
15
|
}
|
|
36
16
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Split audio file into overlapping chunks using ffmpeg.
|
|
39
|
-
* Returns array of chunk file paths with their time offsets.
|
|
40
|
-
*/
|
|
41
|
-
export function splitAudioFile(filePath, totalDuration, options) {
|
|
42
|
-
const chunkDuration = options?.chunkDurationSec ?? CHUNK_DURATION_SEC;
|
|
43
|
-
const overlap = options?.overlapSec ?? CHUNK_OVERLAP_SEC;
|
|
44
|
-
const chunkDir = join(tmpdir(), `h-ear-chunks-${Date.now()}`);
|
|
45
|
-
mkdirSync(chunkDir, { recursive: true });
|
|
46
|
-
const chunks = [];
|
|
47
|
-
const name = basename(filePath, '.mp3').replace(/\.[^.]+$/, '');
|
|
48
|
-
let startTime = 0;
|
|
49
|
-
let index = 0;
|
|
50
|
-
const step = chunkDuration - overlap;
|
|
51
|
-
while (startTime < totalDuration) {
|
|
52
|
-
const chunkFile = join(chunkDir, `${name}_chunk${index}.mp3`);
|
|
53
|
-
let duration = Math.min(chunkDuration, totalDuration - startTime);
|
|
54
|
-
// Peek: if the next step would leave a trailing chunk shorter than MIN_TRAILING_CHUNK_SEC,
|
|
55
|
-
// extend this chunk to eat the remainder and finish.
|
|
56
|
-
const nextStart = startTime + step;
|
|
57
|
-
const remaining = totalDuration - nextStart;
|
|
58
|
-
if (remaining > 0 && remaining < MIN_TRAILING_CHUNK_SEC) {
|
|
59
|
-
duration = totalDuration - startTime;
|
|
60
|
-
}
|
|
61
|
-
const result = spawnSync('ffmpeg', [
|
|
62
|
-
'-y',
|
|
63
|
-
'-ss', String(startTime),
|
|
64
|
-
'-i', filePath,
|
|
65
|
-
'-t', String(duration),
|
|
66
|
-
'-acodec', 'libmp3lame',
|
|
67
|
-
'-q:a', '4', // Good quality MP3 (~128kbps)
|
|
68
|
-
chunkFile,
|
|
69
|
-
], {
|
|
70
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
71
|
-
encoding: 'utf-8',
|
|
72
|
-
timeout: 30000,
|
|
73
|
-
});
|
|
74
|
-
if (result.status !== 0 || !existsSync(chunkFile)) {
|
|
75
|
-
process.stderr.write(`[h-ear-core] ffmpeg chunk ${index} failed: ${(result.stderr || '').split('\n').pop()}\n`);
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
chunks.push({ path: chunkFile, startTime, duration, index });
|
|
79
|
-
// If we ate the trailing remainder, we're done
|
|
80
|
-
if (duration >= totalDuration - startTime)
|
|
81
|
-
break;
|
|
82
|
-
startTime += step;
|
|
83
|
-
index++;
|
|
84
|
-
}
|
|
85
|
-
return chunks;
|
|
86
|
-
}
|
|
87
|
-
/** Clean up chunk temp files */
|
|
88
|
-
export function cleanupChunks(chunks) {
|
|
89
|
-
if (chunks.length === 0)
|
|
90
|
-
return;
|
|
91
|
-
const chunkDir = join(chunks[0].path, '..');
|
|
92
|
-
for (const chunk of chunks) {
|
|
93
|
-
try {
|
|
94
|
-
unlinkSync(chunk.path);
|
|
95
|
-
}
|
|
96
|
-
catch { /* ignore */ }
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
rmdirSync(chunkDir);
|
|
100
|
-
}
|
|
101
|
-
catch { /* ignore */ }
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Merge classification results from multiple chunks into a unified result.
|
|
105
|
-
* Uses TIME-CHOP: each chunk owns a non-overlapping time window.
|
|
106
|
-
* Events outside the owned window are discarded — the overlap served its ML context purpose.
|
|
107
|
-
*
|
|
108
|
-
* Chunk N owns [N*step, (N+1)*step) where step = chunkDuration - overlap.
|
|
109
|
-
* Last chunk owns [start, totalDuration].
|
|
110
|
-
*/
|
|
111
|
-
export function mergeChunkResults(chunkResults, _originalFileName, totalDuration) {
|
|
112
|
-
const allClassifications = [];
|
|
113
|
-
for (let i = 0; i < chunkResults.length; i++) {
|
|
114
|
-
const { chunk, result } = chunkResults[i];
|
|
115
|
-
if (!result.classifications)
|
|
116
|
-
continue;
|
|
117
|
-
const isLast = i === chunkResults.length - 1;
|
|
118
|
-
const ownedStart = chunk.startTime;
|
|
119
|
-
const ownedEnd = isLast ? totalDuration : chunkResults[i + 1].chunk.startTime;
|
|
120
|
-
for (const cls of result.classifications) {
|
|
121
|
-
const absoluteStart = (cls.startTime || 0) + chunk.startTime;
|
|
122
|
-
const absoluteEnd = (cls.endTime || 0) + chunk.startTime;
|
|
123
|
-
// Time-chop: only keep events within this chunk's owned window
|
|
124
|
-
if (absoluteStart >= ownedStart && absoluteStart < ownedEnd) {
|
|
125
|
-
allClassifications.push({
|
|
126
|
-
...cls,
|
|
127
|
-
startTime: absoluteStart,
|
|
128
|
-
endTime: absoluteEnd,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
allClassifications.sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
|
|
134
|
-
return {
|
|
135
|
-
requestId: `merged-${Date.now()}`,
|
|
136
|
-
duration: totalDuration,
|
|
137
|
-
processingTimeMs: 0,
|
|
138
|
-
eventCount: allClassifications.length,
|
|
139
|
-
classifications: allClassifications,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
17
|
//# sourceMappingURL=chunker.js.map
|
package/dist/chunker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunker.js","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"chunker.js","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,kDAAkD;AAClD,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC7C,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,CACnB,+DAA+D,QAAQ,GAAG,EAC1E,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CACzE,CAAC;QACF,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,CAAC,CAAC;IACb,CAAC;AACL,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ export { resolveConfigFromEnv, apiUrl } from './config.js';
|
|
|
7
7
|
export type { ServerConfig } from './config.js';
|
|
8
8
|
export { HEAR_API, ENVIRONMENTS } from './constants.js';
|
|
9
9
|
export type { HearEnvironment } from './constants.js';
|
|
10
|
-
export {
|
|
11
|
-
export type { ChunkInfo } from './chunker.js';
|
|
10
|
+
export { getAudioDuration } from './chunker.js';
|
|
12
11
|
export type { ClassifyRequest, ClassifyResult, Classification, NoiseEvent, AsyncAccepted, ChunkResult, ClientCapturedMetadata, ClassifyOptions, BatchFile, BatchRequest, BatchAccepted, AudioClass, ClassesResult, HealthResult, UsageResult, JobSummary, JobsResult, JobResult, JobNoiseEvent, JobEventsResult, JobAudioResult, JobWaveformResult, WebhookRegister, WebhookResult, EnterpriseWebhookCreate, EnterpriseWebhook, EnterpriseWebhookCreateResult, EnterpriseWebhookListResult, EnterpriseWebhookUpdate, PingResult, WebhookDelivery, WebhookDeliveriesResult, ApiErrorBody, } from './types.js';
|
|
13
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC9D,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3D,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGtD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC9D,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3D,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,YAAY,EACR,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EACvF,sBAAsB,EAAE,eAAe,EACvC,SAAS,EAAE,YAAY,EAAE,aAAa,EACtC,UAAU,EAAE,aAAa,EACzB,YAAY,EACZ,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAC9C,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,iBAAiB,EACjE,eAAe,EAAE,aAAa,EAC9B,uBAAuB,EAAE,iBAAiB,EAAE,6BAA6B,EACzE,2BAA2B,EAAE,uBAAuB,EACpD,UAAU,EAAE,eAAe,EAAE,uBAAuB,EACpD,YAAY,GACf,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,6 @@ export { HearApiClient, HearApiError } from './api-client.js';
|
|
|
7
7
|
export { resolveConfigFromEnv, apiUrl } from './config.js';
|
|
8
8
|
// Constants
|
|
9
9
|
export { HEAR_API, ENVIRONMENTS } from './constants.js';
|
|
10
|
-
// Chunker
|
|
11
|
-
export {
|
|
10
|
+
// Chunker (duration detection only — ffmpeg chunking removed, replaced by byte-slice upload)
|
|
11
|
+
export { getAudioDuration } from './chunker.js';
|
|
12
12
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,aAAa;AACb,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG9D,SAAS;AACT,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG3D,YAAY;AACZ,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGxD,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,aAAa;AACb,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG9D,SAAS;AACT,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG3D,YAAY;AACZ,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGxD,6FAA6F;AAC7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ear/core",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.202604132221",
|
|
4
4
|
"description": "Shared API client for H-ear World audio classification — used by @h-ear/mcp-server and @h-ear/openclaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,8 +55,7 @@
|
|
|
55
55
|
"node": ">=18"
|
|
56
56
|
},
|
|
57
57
|
"systemDependencies": {
|
|
58
|
-
"
|
|
59
|
-
"ffprobe": "optional — integration test duration detection only (getAudioDuration). Not used by MCP tools."
|
|
58
|
+
"ffprobe": "optional — duration detection (getAudioDuration) for upload routing. Returns 0 when unavailable."
|
|
60
59
|
},
|
|
61
60
|
"files": [
|
|
62
61
|
"dist",
|