Skip to content

Commit bd6c473

Browse files
author
Mikhail Samin's Claude
committed
Add file-absolute start_seconds/end_seconds to enterprise matches
recognize_enterprise now reports where each song plays in the file as start_seconds / end_seconds (seconds, file-absolute). These are computed from the chunk offset the response otherwise carries only at the chunk level, so the position is no longer lost when chunks are flattened. The endpoint is now asked for accurate offsets by default, so the values are precise. The raw start_offset / end_offset remain as the fragment-relative milliseconds.
1 parent 2f7270e commit bd6c473

8 files changed

Lines changed: 103 additions & 7 deletions

File tree

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,14 @@ Valid `returnMetadata` values: `apple_music`, `spotify`, `deezer`,
140140

141141
For longer audio files, use `audd.recognizeEnterprise(source,
142142
opts)` — it returns `List<EnterpriseMatch>`. Each `EnterpriseMatch`
143-
carries the same core tags plus `score()`, `startOffset()`, `endOffset()`,
144-
`isrc()`, `upc()`. Access to `isrc`, `upc`, and `score` requires a Startup
145-
plan or higher — [contact us](mailto:api@audd.io) for enterprise features.
143+
carries the same core tags plus `score()`, `startSeconds()`, `endSeconds()`,
144+
`startOffset()`, `endOffset()`, `isrc()`, `upc()`. `startSeconds()` and
145+
`endSeconds()` are where the song plays in your file, in seconds — precise
146+
because accurate offsets are requested by default (pass
147+
`accurateOffsets(false)` to opt out). Behind them, `startOffset()` and
148+
`endOffset()` are the raw fragment-relative milliseconds. Access to `isrc`,
149+
`upc`, and `score` requires a Startup plan or higher — [contact
150+
us](mailto:api@audd.io) for enterprise features.
146151

147152
### Reading additional metadata
148153

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>io.audd</groupId>
88
<artifactId>audd</artifactId>
9-
<version>1.5.13</version>
9+
<version>1.5.14</version>
1010
<packaging>jar</packaging>
1111

1212
<name>AudD Java SDK</name>

src/main/java/io/audd/AsyncAudD.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,10 @@ public CompletableFuture<List<EnterpriseMatch>> recognizeEnterprise(Object sourc
152152
for (JsonNode chunkNode : result) {
153153
try {
154154
EnterpriseChunkResult chunk = MAPPER.treeToValue(chunkNode, EnterpriseChunkResult.class);
155-
if (chunk != null && chunk.songs() != null) out.addAll(chunk.songs());
155+
if (chunk != null && chunk.songs() != null) {
156+
AudD.applyChunkPositions(chunk);
157+
out.addAll(chunk.songs());
158+
}
156159
} catch (Exception e) {
157160
throw new io.audd.errors.AudDSerializationError("Failed to decode EnterpriseChunkResult", resp.rawText());
158161
}

src/main/java/io/audd/AudD.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ public List<EnterpriseMatch> recognizeEnterprise(Object source, EnterpriseOption
209209
try {
210210
EnterpriseChunkResult chunk = MAPPER.treeToValue(chunkNode, EnterpriseChunkResult.class);
211211
if (chunk != null && chunk.songs() != null) {
212+
applyChunkPositions(chunk);
212213
out.addAll(chunk.songs());
213214
}
214215
} catch (Exception e) {
@@ -299,6 +300,46 @@ static SourcePreparer.Prepared prepareSource(Object source) {
299300
}
300301
}
301302

303+
/**
304+
* Parse an enterprise chunk offset ({@code "SS"}, {@code "MM:SS"},
305+
* {@code "HH:MM:SS"}, or a plain number) into seconds. Returns null when the
306+
* value is null/empty/unparseable. Never throws.
307+
*/
308+
static Double offsetToSeconds(String o) {
309+
if (o == null) return null;
310+
String s = o.trim();
311+
if (s.isEmpty()) return null;
312+
try {
313+
String[] parts = s.split(":");
314+
if (parts.length == 1) {
315+
return Double.parseDouble(parts[0].trim());
316+
}
317+
double total = 0;
318+
for (String part : parts) {
319+
total = total * 60 + Double.parseDouble(part.trim());
320+
}
321+
return total;
322+
} catch (NumberFormatException e) {
323+
return null;
324+
}
325+
}
326+
327+
/**
328+
* Anchor each song in a chunk to its position in the user's file. The chunk
329+
* offset is the fragment's start in the file; per-song start/end offsets are
330+
* milliseconds within the fragment. Sets startSeconds/endSeconds when the
331+
* chunk offset parses; leaves them null otherwise.
332+
*/
333+
static void applyChunkPositions(EnterpriseChunkResult chunk) {
334+
Double base = offsetToSeconds(chunk.offset());
335+
if (base == null) return;
336+
for (EnterpriseMatch song : chunk.songs()) {
337+
if (song == null) continue;
338+
song.startSeconds = base + (song.startOffset == null ? 0 : song.startOffset) / 1000.0;
339+
song.endSeconds = base + (song.endOffset == null ? 0 : song.endOffset) / 1000.0;
340+
}
341+
}
342+
302343
static Map<String, String> buildEnterpriseFields(EnterpriseOptions opts) {
303344
Map<String, String> fields = new HashMap<>();
304345
if (opts == null) return fields;

src/main/java/io/audd/EnterpriseOptions.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ public static final class Builder {
5858
private Integer limit;
5959
private Integer skipFirstSeconds;
6060
private Boolean useTimecode;
61-
private Boolean accurateOffsets;
61+
// Accurate offsets are on by default so startSeconds/endSeconds land on
62+
// exact positions in the user's file. Pass accurateOffsets(false) to opt out.
63+
private Boolean accurateOffsets = Boolean.TRUE;
6264
private Long timeoutMs;
6365
private Map<String, String> extraParameters;
6466

src/main/java/io/audd/internal/UserAgent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
/** SDK identifier sent on every request. */
44
public final class UserAgent {
5-
public static final String SDK_VERSION = "1.5.13";
5+
public static final String SDK_VERSION = "1.5.14";
66

77
private UserAgent() {}
88

src/main/java/io/audd/models/EnterpriseMatch.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ public final class EnterpriseMatch extends ForwardCompatible {
2323
@JsonProperty("start_offset") public Integer startOffset;
2424
@JsonProperty("end_offset") public Integer endOffset;
2525

26+
/**
27+
* Where this song starts in the user's file, in seconds. Computed from the
28+
* chunk offset plus {@link #startOffset}; null when the chunk offset can't
29+
* be parsed. Not a wire field.
30+
*/
31+
@JsonIgnore public Double startSeconds;
32+
/**
33+
* Where this song ends in the user's file, in seconds. Computed from the
34+
* chunk offset plus {@link #endOffset}; null when the chunk offset can't
35+
* be parsed. Not a wire field.
36+
*/
37+
@JsonIgnore public Double endSeconds;
38+
2639
public Integer score() { return score; }
2740
public String timecode() { return timecode; }
2841
public String artist() { return artist; }
@@ -35,6 +48,8 @@ public final class EnterpriseMatch extends ForwardCompatible {
3548
public String songLink() { return songLink; }
3649
public Integer startOffset() { return startOffset; }
3750
public Integer endOffset() { return endOffset; }
51+
public Double startSeconds() { return startSeconds; }
52+
public Double endSeconds() { return endSeconds; }
3853

3954
@JsonIgnore
4055
public String thumbnailUrl() {

src/test/java/io/audd/AudDTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,36 @@ void recognizeEnterprise_flatList() throws Exception {
151151
assertThat(req.getBody().readUtf8()).contains("limit").contains("1");
152152
}
153153

154+
@Test
155+
void recognizeEnterprise_anchorsSongsToFilePositions() throws Exception {
156+
server.enqueue(new MockResponse().setBody(
157+
"{\"status\":\"success\",\"result\":["
158+
+ "{\"songs\":[{\"score\":81,\"artist\":\"A\",\"title\":\"B\","
159+
+ "\"start_offset\":4200,\"end_offset\":11800}],\"offset\":\"00:01:00\"},"
160+
+ "{\"songs\":[{\"score\":70,\"artist\":\"C\",\"title\":\"D\","
161+
+ "\"start_offset\":1000,\"end_offset\":2000}]}]}"));
162+
163+
List<EnterpriseMatch> matches = audd.recognizeEnterprise("https://example.com/audio.mp3",
164+
EnterpriseOptions.builder().limit(1).build());
165+
assertThat(matches).hasSize(2);
166+
// Chunk offset "00:01:00" = 60s; 60 + 4200/1000 = 64.2, 60 + 11800/1000 = 71.8.
167+
assertThat(matches.get(0).startSeconds()).isEqualTo(64.2);
168+
assertThat(matches.get(0).endSeconds()).isEqualTo(71.8);
169+
// Chunk with absent offset -> positions stay null.
170+
assertThat(matches.get(1).startSeconds()).isNull();
171+
assertThat(matches.get(1).endSeconds()).isNull();
172+
}
173+
174+
@Test
175+
void recognizeEnterprise_defaultRequestSendsAccurateOffsets() throws Exception {
176+
server.enqueue(new MockResponse().setBody("{\"status\":\"success\",\"result\":[]}"));
177+
178+
audd.recognizeEnterprise("https://example.com/audio.mp3");
179+
180+
RecordedRequest req = server.takeRequest();
181+
assertThat(req.getBody().readUtf8()).contains("accurate_offsets").contains("true");
182+
}
183+
154184
@Test
155185
void close_isIdempotent() {
156186
audd.close();

0 commit comments

Comments
 (0)