Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright IBM Corp. 2025 - 2025
* SPDX-License-Identifier: Apache-2.0
*/
package com.ibm.watsonx.ai.textprocessing;
package com.ibm.watsonx.ai.core;

/**
* Enum representing supported languages with their corresponding ISO 639 language codes.
Expand Down Expand Up @@ -85,4 +85,5 @@ public enum Language {
public String isoCode() {
return isoCode;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright IBM Corp. 2025 - 2025
* SPDX-License-Identifier: Apache-2.0
*/
package com.ibm.watsonx.ai.core.http;

import static java.util.Objects.isNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
* Utility for constructing multipart/form-data HTTP request bodies.
* <p>
* <b>Example usage:</b>
*
* <pre>{@code
* HTTPRequestMultipartBody body = HTTPRequestMultipartBody.builder()
* .addPart("model", "openai/whisper-tiny")
* .addPart("language", "it")
* .addInputStream("file", is)
* .build();
*
* HttpRequest request = HttpRequest.newBuilder()
* .uri(URI.create(endpoint))
* .header("Content-Type", body.getContentType())
* .POST(BodyPublishers.ofByteArray(body.getBody()))
* .build();
* }</pre>
*
*/
public final class HttpRequestMultipartBody {

private static final String BOUNDARY = "----watsonx-ai-sdk";

private final byte[] bytes;

private HttpRequestMultipartBody(byte[] bytes) {
this.bytes = bytes;
}

public String boundary() {
return BOUNDARY;
}

public String contentType() {
return "multipart/form-data; boundary=" + BOUNDARY;
}

public byte[] body() {
return bytes;
}

public static Builder builder() {
return new Builder();
}

public static final class Builder {
private static final String CRLF = "\r\n";
private final List<Part> parts = new ArrayList<>();

private record Part(String fieldName, String fileName, String contentType, byte[] content) {}

public Builder addPart(String name, String value) {
if (isNull(name) || isNull(value))
return this;

parts.add(new Part(name, null, "text/plain; charset=UTF-8",
value.getBytes(StandardCharsets.UTF_8)));
return this;
}

public Builder addInputStream(String name, InputStream is) {
try {
parts.add(new Part(name, null, "application/octet-stream", is.readAllBytes()));
return this;
} catch (IOException e) {
throw new RuntimeException(e);
}
}


public HttpRequestMultipartBody build() {
if (parts.isEmpty()) {
throw new IllegalStateException("Cannot build multipart body with no parts");
}

try (var out = new ByteArrayOutputStream()) {
for (Part part : parts) {
writeBoundary(out, BOUNDARY);
writePart(out, part);
}
out.write(("--" + BOUNDARY + "--" + CRLF).getBytes(StandardCharsets.UTF_8));
return new HttpRequestMultipartBody(out.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private void writeBoundary(OutputStream out, String boundary) throws IOException {
out.write(("--" + boundary + CRLF).getBytes(StandardCharsets.UTF_8));
}

private void writePart(OutputStream out, Part part) throws IOException {
out.write(("Content-Disposition: form-data; name=\"" + part.fieldName() + "\"").getBytes(StandardCharsets.UTF_8));
if (part.fileName() != null) {
out.write(("; filename=\"" + part.fileName() + "\"").getBytes(StandardCharsets.UTF_8));
}
out.write((CRLF).getBytes(StandardCharsets.UTF_8));
out.write(("Content-Type: " + part.contentType() + CRLF + CRLF).getBytes(StandardCharsets.UTF_8));
out.write(part.content());
out.write(CRLF.getBytes(StandardCharsets.UTF_8));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.ibm.watsonx.ai.timeseries.TimeSeriesService;
import com.ibm.watsonx.ai.tokenization.TokenizationService;
import com.ibm.watsonx.ai.tool.ToolService;
import com.ibm.watsonx.ai.transcription.TranscriptionRestClient;

/**
* This class provides common functionality and shared configuration used across various service-specific clients (e.g., {@code ChatService},
Expand All @@ -42,6 +43,7 @@
* @see FoundationModelService
* @see ToolService
* @see DetectionService
* @see TranscriptionRestClient
*/
public abstract class WatsonxService {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
import java.util.Map;
import java.util.stream.Stream;
import com.ibm.watsonx.ai.WatsonxParameters;
import com.ibm.watsonx.ai.core.Language;
import com.ibm.watsonx.ai.textprocessing.CosReference;
import com.ibm.watsonx.ai.textprocessing.Language;
import com.ibm.watsonx.ai.textprocessing.OcrMode;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
import java.util.Map;
import java.util.stream.Stream;
import com.ibm.watsonx.ai.WatsonxParameters;
import com.ibm.watsonx.ai.core.Language;
import com.ibm.watsonx.ai.textprocessing.CosReference;
import com.ibm.watsonx.ai.textprocessing.Language;
import com.ibm.watsonx.ai.textprocessing.OcrMode;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright IBM Corp. 2025 - 2025
* SPDX-License-Identifier: Apache-2.0
*/
package com.ibm.watsonx.ai.transcription;

import static com.ibm.watsonx.ai.core.Json.fromJson;
import static java.util.Objects.nonNull;
import static java.util.Objects.requireNonNull;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import com.ibm.watsonx.ai.core.factory.HttpClientFactory;
import com.ibm.watsonx.ai.core.http.HttpRequestMultipartBody;
import com.ibm.watsonx.ai.core.http.SyncHttpClient;
import com.ibm.watsonx.ai.core.http.interceptors.LoggerInterceptor.LogMode;

/**
* Default implementation of the {@link TranscriptionRestClient} abstract class.
*/
final class DefaultRestClient extends TranscriptionRestClient {

private final SyncHttpClient syncHttpClient;

DefaultRestClient(Builder builder) {
super(builder);
requireNonNull(authenticator, "authenticator is mandatory");
syncHttpClient = HttpClientFactory.createSync(authenticator, httpClient, LogMode.of(logRequests, logResponses));
}

@Override
public TranscriptionResult transcribe(TranscriptionRequest request) {

var multiPartRequest = HttpRequestMultipartBody.builder()
.addPart("model", request.modelId())
.addPart("language", request.language())
.addPart("project_id", request.projectId())
.addPart("space_id", request.spaceId())
.addInputStream("file", request.inputStream())
.build();

var httpRequest = HttpRequest.newBuilder(URI.create(baseUrl + "/ml/v1/audio/transcriptions?version=%s".formatted(version)))
.header("Content-Type", multiPartRequest.contentType())
.header("Accept", "application/json")
.POST(BodyPublishers.ofByteArray(multiPartRequest.body()))
.timeout(timeout);

if (nonNull(request.transactionId()))
httpRequest.header(TRANSACTION_ID_HEADER, request.transactionId());

try {

var httpReponse = syncHttpClient.send(httpRequest.build(), BodyHandlers.ofString());
return fromJson(httpReponse.body(), TranscriptionResult.class);

} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}

/**
* Returns a new {@link Builder} instance.
*/
static Builder builder() {
return new Builder();
}

/**
* Builder class for constructing {@link DefaultRestClient} instances with configurable parameters.
*/
public final static class Builder extends TranscriptionRestClient.Builder {

private Builder() {}

/**
* Builds a {@link DefaultRestClient} instance using the configured parameters.
*
* @return a new instance of {@link DefaultRestClient}
*/
public DefaultRestClient build() {
return new DefaultRestClient(this);
}
}
}
Loading