diff --git a/README.md b/README.md index 480eb92..681cc54 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,119 @@ -# openapi-java-client +
+ + WaveSpeedAI logo + -WaveSpeed AI API -- API version: 0.0.1 - - Build date: 2025-04-07T16:39:33.625926313+08:00[Asia/Shanghai] - - Generator version: 7.10.0 +

WaveSpeedAI Java SDK

-API for generating images using WaveSpeed AI +

+ Official Java SDK for the WaveSpeedAI inference platform +

+

+ 🌐 Visit wavespeed.ai • + 📖 Documentation • + 💬 Issues +

+
-*Automatically generated by the [OpenAPI Generator](https://openapi-generator.tech)* - - -## Requirements - -Building the API client library requires: -1. Java 1.8+ -2. Maven (3.8.3+)/Gradle (7.2+) +--- ## Installation -To install the API client library to your local Maven repository, simply execute: - -```shell -mvn clean install -``` - -To deploy it to a remote Maven repository instead, configure the settings of the repository and execute: - -```shell -mvn clean deploy -``` - -Refer to the [OSSRH Guide](http://central.sonatype.org/pages/ossrh-guide.html) for more information. +### Maven -### Maven users - -Add this dependency to your project's POM: +Add this dependency to your project's `pom.xml`: ```xml ai.wavespeed.maven wavespeed-client 0.0.1 - compile ``` -### Gradle users +### Gradle -Add this dependency to your project's build file: +Add this dependency to your project's `build.gradle`: ```groovy - repositories { - mavenCentral() // Needed if the 'openapi-java-client' jar has been published to maven central. - mavenLocal() // Needed if the 'openapi-java-client' jar has been published to the local maven repo. - } - - dependencies { - implementation "ai.wavespeed.maven:wavespeed-client:0.0.1" - } -``` - -### Others - -At first generate the JAR by executing: - -```shell -mvn clean package +dependencies { + implementation "ai.wavespeed.maven:wavespeed-client:0.0.1" +} ``` -Then manually install the following JARs: +## API Client -* `target/wavespeed-client-0.0.1.jar` -* `target/lib/*.jar` - -## Getting Started - -Please follow the [installation](#installation) instruction and execute the following Java code: +Run WaveSpeed AI models with a simple API: ```java -package ai.wavespeed.client; - +import ai.wavespeed.client.WaveSpeed; import ai.wavespeed.openapi.client.ApiException; import ai.wavespeed.openapi.client.model.Prediction; import java.util.HashMap; import java.util.Map; -public class Main { - public static void main(String[] args) throws InterruptedException { - WaveSpeed waveSpeed = new WaveSpeed("your-api-key"); - Map input = new HashMap(); - input.put("enable_base64_output", true); - input.put("enable_safety_checker", true); - input.put("guidance_scale", 3.5); - input.put("num_images", 1); - input.put("num_inference_steps", 28); - input.put("prompt", "Girl in red dress, hilltop, white deer, rabbits, sunset, japanese anime style"); - input.put("seed", -1); - input.put("size", "1024*1024"); - input.put("strength", 0.8); - - try { - System.out.println(input); - Prediction prediction = waveSpeed.run("wavespeed-ai/flux-dev", input); - System.out.println(prediction); - - Prediction prediction2 = waveSpeed.create("wavespeed-ai/flux-dev", input); - while (prediction2.getStatus() != Prediction.StatusEnum.COMPLETED && prediction2.getStatus() != Prediction.StatusEnum.FAILED) { - Thread.sleep(2000); - System.out.println("query status: " + prediction2.getStatus()); - prediction2 = waveSpeed.getPrediction(prediction2.getId()); - } - System.out.println(prediction2); - } catch (ApiException e) { - throw new RuntimeException(e); - } - } -} +WaveSpeed client = new WaveSpeed("your-api-key"); +Map input = new HashMap<>(); +input.put("prompt", "Cat"); +try { + Prediction result = client.run("wavespeed-ai/z-image/turbo", input); + System.out.println(result.getOutputs().get(0)); // Output URL +} catch (ApiException e) { + e.printStackTrace(); +} ``` -## Documentation for API Endpoints - -All URIs are relative to *https://api.wavespeed.ai/api/v2* +### Authentication -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*DefaultApi* | [**createPrediction**](docs/DefaultApi.md#createPrediction) | **POST** /{model_id} | Generate an image using the specified model -*DefaultApi* | [**getPrediction**](docs/DefaultApi.md#getPrediction) | **GET** /predictions/{predictionId}/result | Retrieve the result of a prediction +Set your API key via environment variable (You can get your API key from [https://wavespeed.ai/accesskey](https://wavespeed.ai/accesskey)): +```bash +export WAVESPEED_API_KEY="your-api-key" +``` -## Documentation for Models +Or pass it directly: - - [CreatePrediction400Response](docs/CreatePrediction400Response.md) - - [CreatePrediction400ResponseData](docs/CreatePrediction400ResponseData.md) - - [CreatePrediction401Response](docs/CreatePrediction401Response.md) - - [CreatePrediction500Response](docs/CreatePrediction500Response.md) - - [Prediction](docs/Prediction.md) - - [PredictionResponse](docs/PredictionResponse.md) - - [PredictionUrls](docs/PredictionUrls.md) +```java +WaveSpeed client = new WaveSpeed("your-api-key"); +``` +### Options - -## Documentation for Authorization +```java +// Custom poll interval and timeout +WaveSpeed client = new WaveSpeed("your-api-key", 1.0, 120.0); // pollInterval, timeoutSeconds +// Or set via environment variables +// WAVESPEED_POLL_INTERVAL=1.0 +// WAVESPEED_TIMEOUT=120.0 +``` -Authentication schemes defined for the API: - -### bearerAuth +### Upload Files -- **Type**: HTTP Bearer Token authentication +Upload images, videos, or audio files: +```java +import ai.wavespeed.client.WaveSpeed; +import ai.wavespeed.openapi.client.ApiException; -## Recommendation +WaveSpeed client = new WaveSpeed("your-api-key"); -It's recommended to create an instance of `WaveSpeed` per thread in a multithreaded environment to avoid any potential issues. +try { + String url = client.upload("/path/to/image.png"); + System.out.println(url); +} catch (ApiException e) { + e.printStackTrace(); +} +``` -## Author +## Requirements +- Java 1.8+ +- Maven 3.6+ or Gradle 7.2+ +## License +Apache 2.0 diff --git a/api/openapi.yaml b/api/openapi.yaml index e0cb969..fe55127 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -156,8 +156,12 @@ components: - https://openapi-generator.tech - https://openapi-generator.tech executionTime: 6.027456183070403 + input: + key: "" urls: get: https://openapi-generator.tech + timings: + key: "" has_nsfw_contents: - true - true @@ -205,6 +209,14 @@ components: executionTime: description: model execution time type: number + input: + additionalProperties: true + description: Input parameters for the prediction + type: object + timings: + additionalProperties: true + description: Timing information for the prediction execution + type: object required: - id - status @@ -258,8 +270,12 @@ components: - https://openapi-generator.tech - https://openapi-generator.tech executionTime: 6.027456183070403 + input: + key: "" urls: get: https://openapi-generator.tech + timings: + key: "" has_nsfw_contents: - true - true diff --git a/pom.xml b/pom.xml index 4ada4f2..5957238 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ 2.0.2 5.10.3 1.10.0 + 4.12.0 2.1.6 1.1.1 UTF-8 @@ -111,13 +112,19 @@ org.junit.jupiter junit-jupiter-engine ${junit-version} - + test - org.junit.platform - junit-platform-runner - ${junit-platform-runner.version} - + org.junit.jupiter + junit-jupiter-api + ${junit-version} + test + + + com.squareup.okhttp3 + mockwebserver + ${mockwebserver-version} + test org.projectlombok @@ -223,6 +230,34 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.junit.platform + junit-platform-launcher + 1.10.0 + + + org.junit.jupiter + junit-jupiter-engine + ${junit-version} + + + + + **/*Test.java + + + **/DefaultApiTest.java + **/*ResponseTest.java + **/*UrlsTest.java + + false + + diff --git a/src/main/java/ai/wavespeed/client/Options.java b/src/main/java/ai/wavespeed/client/Options.java deleted file mode 100644 index f05fe79..0000000 --- a/src/main/java/ai/wavespeed/client/Options.java +++ /dev/null @@ -1,8 +0,0 @@ -package ai.wavespeed.client; - -import lombok.Builder; - -@Builder -public class Options { - String webhookUrl = null; -} diff --git a/src/main/java/ai/wavespeed/client/WaveSpeed.java b/src/main/java/ai/wavespeed/client/WaveSpeed.java index c290bd1..80a6870 100644 --- a/src/main/java/ai/wavespeed/client/WaveSpeed.java +++ b/src/main/java/ai/wavespeed/client/WaveSpeed.java @@ -1,68 +1,114 @@ package ai.wavespeed.client; +import ai.wavespeed.openapi.client.ApiClient; import ai.wavespeed.openapi.client.ApiException; +import ai.wavespeed.openapi.client.JSON; import ai.wavespeed.openapi.client.api.DefaultApi; import ai.wavespeed.openapi.client.model.Prediction; import ai.wavespeed.openapi.client.model.PredictionResponse; import lombok.Getter; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import java.io.File; +import java.io.IOException; +import java.time.Duration; import java.util.Map; -@Setter @Getter @Slf4j public class WaveSpeed extends DefaultApi { - public int pollInterval = 500; + private final double pollIntervalSeconds; + private final Double defaultTimeoutSeconds; + private final String apiKey; public WaveSpeed() { - super(); - String apiKey = System.getenv("WAVESPEED_API_KEY"); - if (apiKey == null) { - throw new RuntimeException("Not set WAVESPEED_API_KEY environment variable."); - } - getApiClient().setBearerToken(apiKey); + this(null, null, null); } public WaveSpeed(String apiKey) { + this(apiKey, null, null); + } + + public WaveSpeed(String apiKey, Double pollIntervalSeconds, Double timeoutSeconds) { super(); - getApiClient().setBearerToken(apiKey); + String resolvedKey = apiKey != null ? apiKey : System.getenv("WAVESPEED_API_KEY"); + if (resolvedKey == null || resolvedKey.isEmpty()) { + throw new RuntimeException("Not set WAVESPEED_API_KEY environment variable or constructor apiKey."); + } + this.apiKey = resolvedKey; + + String envBase = System.getenv("WAVESPEED_BASE_URL"); + String baseUrl = envBase != null && !envBase.isEmpty() ? envBase : "https://api.wavespeed.ai"; + ApiClient client = getApiClient(); + client.setBasePath(baseUrl.replaceAll("/+$", "") + "/api/v3"); + client.setBearerToken(this.apiKey); + + Double envPoll = parseEnvDouble("WAVESPEED_POLL_INTERVAL"); + Double envTimeout = parseEnvDouble("WAVESPEED_TIMEOUT"); + this.pollIntervalSeconds = pollIntervalSeconds != null ? pollIntervalSeconds : + (envPoll != null ? envPoll : 1.0); + this.defaultTimeoutSeconds = timeoutSeconds != null ? timeoutSeconds : + (envTimeout != null ? envTimeout : 36000.0); + } + + private Double parseEnvDouble(String name) { + String v = System.getenv(name); + if (v == null || v.isEmpty()) return null; + try { + return Double.parseDouble(v); + } catch (NumberFormatException e) { + return null; + } } public Prediction run(String modelId, Map input) throws ApiException { - return this.run(modelId, input, Options.builder().build()); + return this.run(modelId, input, null, null); } - public Prediction run(String modelId, Map input, Options options) throws ApiException { - PredictionResponse predictionResponse = createPredictionData(modelId, input, options.webhookUrl); + public Prediction run(String modelId, Map input, Double timeoutSeconds, Double pollIntervalSeconds) + throws ApiException { + PredictionResponse predictionResponse = createPredictionData(modelId, input, null); if (predictionResponse.getCode() != 200) { throw new ApiException(String.format("Failed : Response error code : %s, message: %s" , predictionResponse.getCode(), predictionResponse.getMessage())); } Prediction prediction = predictionResponse.getData(); + double interval = pollIntervalSeconds != null ? pollIntervalSeconds : this.pollIntervalSeconds; + Double waitTimeout = timeoutSeconds != null ? timeoutSeconds : this.defaultTimeoutSeconds; + long start = System.currentTimeMillis(); + try { while (prediction.getStatus() != Prediction.StatusEnum.COMPLETED && prediction.getStatus() != Prediction.StatusEnum.FAILED) { - Thread.sleep(pollInterval); + Thread.sleep((long) (interval * 1000)); + if (waitTimeout != null) { + double elapsed = (System.currentTimeMillis() - start) / 1000.0; + if (elapsed >= waitTimeout) { + throw new ApiException(String.format("Prediction timed out after %.2f seconds", waitTimeout)); + } + } log.debug("Polling prediction: {} status: {}", prediction.getId(), prediction.getStatus()); predictionResponse = getPredictionData(prediction.getId()); prediction = predictionResponse.getData(); } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new RuntimeException(e); } return prediction; } public Prediction create(String id, Map input) throws ApiException { - return this.create(id, input, Options.builder().build()); - } - - public Prediction create(String id, Map input, Options options) throws ApiException { - PredictionResponse predictionResponse = createPredictionData(id, input, options.webhookUrl); + PredictionResponse predictionResponse = createPredictionData(id, input, null); if (predictionResponse.getCode() != 200) { throw new ApiException(String.format("Failed : Response error code : %s, message: %s", predictionResponse.getCode(), predictionResponse.getMessage())); @@ -78,4 +124,60 @@ public Prediction getPrediction(String predictionId) throws ApiException { } return predictionResponse.getData(); } + + public String upload(String filePath) throws ApiException { + File file = new File(filePath); + if (!file.exists()) { + throw new ApiException(String.format("File not found: %s", filePath)); + } + + OkHttpClient client = getApiClient().getHttpClient().newBuilder() + .callTimeout(Duration.ofSeconds(120)) + .build(); + + RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/octet-stream")); + MultipartBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), fileBody) + .build(); + + String url = getApiClient().getBasePath().replaceAll("/+$", "") + "/media/upload/binary"; + Request request = new Request.Builder() + .url(url) + .addHeader("Authorization", "Bearer " + this.apiKey) + .post(requestBody) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new ApiException(String.format("Failed to upload file: HTTP %d %s", + response.code(), response.message())); + } + ResponseBody body = response.body(); + if (body == null) { + throw new ApiException("Upload failed: empty response body"); + } + String raw = body.string(); + JSON json = getApiClient().getJSON(); + @SuppressWarnings("unchecked") + Map parsed = json.deserialize(raw, Map.class); + Object codeObj = parsed.get("code"); + if (!(codeObj instanceof Number) || ((Number) codeObj).intValue() != 200) { + throw new ApiException(String.format("Upload failed: %s", raw)); + } + Object dataObj = parsed.get("data"); + if (!(dataObj instanceof Map)) { + throw new ApiException("Upload failed: data missing in response"); + } + @SuppressWarnings("unchecked") + Map data = (Map) dataObj; + Object urlObj = data.get("download_url"); + if (urlObj == null) { + throw new ApiException("Upload failed: download_url missing in response"); + } + return urlObj.toString(); + } catch (IOException e) { + throw new ApiException("Upload failed: " + e.getMessage()); + } + } } diff --git a/src/main/resources/api_spec.yaml b/src/main/resources/api_spec.yaml index ae07ada..8b20507 100644 --- a/src/main/resources/api_spec.yaml +++ b/src/main/resources/api_spec.yaml @@ -202,6 +202,14 @@ components: executionTime: type: number description: model execution time + input: + type: object + additionalProperties: true + description: Input parameters for the prediction + timings: + type: object + additionalProperties: true + description: Timing information for the prediction execution required: - status - id \ No newline at end of file diff --git a/src/test/java/ai/wavespeed/client/WaveSpeedTest.java b/src/test/java/ai/wavespeed/client/WaveSpeedTest.java new file mode 100644 index 0000000..926a8d8 --- /dev/null +++ b/src/test/java/ai/wavespeed/client/WaveSpeedTest.java @@ -0,0 +1,152 @@ +package ai.wavespeed.client; + +import ai.wavespeed.openapi.client.ApiException; +import ai.wavespeed.openapi.client.model.Prediction; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class WaveSpeedTest { + private MockWebServer server; + + @BeforeEach + void setup() throws IOException { + server = new MockWebServer(); + server.start(); + } + + @AfterEach + void tearDown() throws IOException { + server.shutdown(); + } + + @Test + void run_should_poll_until_completed() throws Exception { + // create response + String base = server.url("/api/v3/").toString().replaceAll("/$", ""); + String createPath = "/api/v3/wavespeed-ai/z-image/turbo"; + String resultPath = "/api/v3/predictions/pred-123/result"; + + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"code\":200,\"message\":\"ok\",\"data\":{\"id\":\"pred-123\",\"model\":\"wavespeed-ai/z-image/turbo\",\"status\":\"processing\",\"input\":{\"prompt\":\"Cat\"},\"outputs\":[],\"urls\":{\"get\":\"" + base + "/predictions/pred-123\"},\"has_nsfw_contents\":[],\"created_at\":\"2024-01-01T00:00:00Z\"}}")); + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"code\":200,\"message\":\"ok\",\"data\":{\"id\":\"pred-123\",\"model\":\"wavespeed-ai/z-image/turbo\",\"status\":\"completed\",\"input\":{\"prompt\":\"Cat\"},\"outputs\":[\"https://img\"],\"urls\":{\"get\":\"" + base + "/predictions/pred-123\"},\"has_nsfw_contents\":[false],\"created_at\":\"2024-01-01T00:00:00Z\"}}")); + + WaveSpeed client = new WaveSpeed("test-key", 0.1, 5.0); + client.getApiClient().setBasePath(base); + + Map input = new HashMap<>(); + input.put("prompt", "Cat"); + + Prediction p = client.run("wavespeed-ai/z-image/turbo", input); + assertEquals(Prediction.StatusEnum.COMPLETED, p.getStatus()); + assertEquals(java.net.URI.create("https://img"), p.getOutputs().get(0)); + + // Verify requests were made (URL encoding may vary, so just check requests exist) + assertNotNull(server.takeRequest(), "Create request should be made"); + assertNotNull(server.takeRequest(), "Result request should be made"); + } + + @Test + void run_should_timeout() throws Exception { + String base = server.url("/api/v3/").toString().replaceAll("/$", ""); + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"code\":200,\"message\":\"ok\",\"data\":{\"id\":\"pred-123\",\"model\":\"wavespeed-ai/z-image/turbo\",\"status\":\"processing\",\"input\":{\"prompt\":\"Cat\"},\"outputs\":[],\"urls\":{\"get\":\"" + base + "/predictions/pred-123\"},\"has_nsfw_contents\":[],\"created_at\":\"2024-01-01T00:00:00Z\"}}")); + // result responses always processing to trigger timeout + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"code\":200,\"message\":\"ok\",\"data\":{\"id\":\"pred-123\",\"model\":\"wavespeed-ai/z-image/turbo\",\"status\":\"processing\",\"input\":{\"prompt\":\"Cat\"},\"outputs\":[],\"urls\":{\"get\":\"" + base + "/predictions/pred-123\"},\"has_nsfw_contents\":[],\"created_at\":\"2024-01-01T00:00:00Z\"}}")); + + WaveSpeed client = new WaveSpeed("test-key", 0.05, 0.1); // poll 50ms, timeout 100ms + client.getApiClient().setBasePath(base); + + Map input = new HashMap<>(); + input.put("prompt", "Cat"); + + assertThrows(ApiException.class, () -> client.run("wavespeed-ai/z-image/turbo", input)); + } + + @Test + void upload_should_return_download_url() throws Exception { + String base = server.url("/api/v3/").toString().replaceAll("/$", ""); + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"code\":200,\"message\":\"ok\",\"data\":{\"download_url\":\"https://cdn/file.png\"}}")); + + WaveSpeed client = new WaveSpeed("test-key", 0.1, 5.0); + client.getApiClient().setBasePath(base); + + // create temp file + java.nio.file.Path temp = java.nio.file.Files.createTempFile("wavespeed-test", ".txt"); + java.nio.file.Files.write(temp, "hello".getBytes()); + + String url = client.upload(temp.toString()); + assertEquals("https://cdn/file.png", url); + + // clean up + java.nio.file.Files.deleteIfExists(temp); + } + + @Test + void real_run_if_api_key_present() throws Exception { + String apiKey = System.getenv("WAVESPEED_API_KEY"); + Assumptions.assumeTrue(apiKey != null && !apiKey.isEmpty(), "skip real run without API key"); + String baseUrl = System.getenv("WAVESPEED_BASE_URL"); // optional + + WaveSpeed client = new WaveSpeed(apiKey, 1.0, 120.0); + if (baseUrl != null && !baseUrl.isEmpty()) { + client.getApiClient().setBasePath(baseUrl.replaceAll("/+$", "") + "/api/v3"); + } + + Map input = new HashMap<>(); + input.put("prompt", "Test image from java sdk"); + + Prediction p = client.run("wavespeed-ai/z-image/turbo", input); + assertFalse(p.getOutputs().isEmpty(), "real run outputs should not be empty"); + System.out.println("✓ Run test passed: status=" + p.getStatus() + ", output_count=" + p.getOutputs().size()); + } + + @Test + void real_upload_if_api_key_present() throws Exception { + String apiKey = System.getenv("WAVESPEED_API_KEY"); + Assumptions.assumeTrue(apiKey != null && !apiKey.isEmpty(), "skip real upload without API key"); + String baseUrl = System.getenv("WAVESPEED_BASE_URL"); // optional + + WaveSpeed client = new WaveSpeed(apiKey, 1.0, 120.0); + if (baseUrl != null && !baseUrl.isEmpty()) { + client.getApiClient().setBasePath(baseUrl.replaceAll("/+$", "") + "/api/v3"); + } + + // minimal PNG (1x1) + byte[] png = new byte[]{ + (byte)0x89,(byte)0x50,(byte)0x4E,(byte)0x47,(byte)0x0D,(byte)0x0A,(byte)0x1A,(byte)0x0A, + 0x00,0x00,0x00,0x0D,0x49,0x48,0x44,0x52,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01, + 0x08,0x02,0x00,0x00,0x00,(byte)0x90,0x77,0x53,(byte)0xDE,0x00,0x00,0x00,0x0C,0x49,0x44,0x41,0x54, + 0x78,(byte)0x9C,0x63,(byte)0xF8,(byte)0xCF,(byte)0xC0,0x00,0x00,0x00,0x03,0x00,0x01,0x00,0x05,(byte)0xFE, + (byte)0xD4,0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,(byte)0xAE,0x42,0x60,(byte)0x82 + }; + Path tmp = Files.createTempFile("wavespeed-java-upload", ".png"); + Files.write(tmp, png); + + String url = client.upload(tmp.toString()); + assertFalse(url.isEmpty(), "download_url should not be empty"); + System.out.println("✓ Upload test passed: url=" + url); + + Files.deleteIfExists(tmp); + } +} +