Skip to content
Open
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,16 @@ runware config path # Print config file path

Config is stored at `~/.runware/config.yaml`.

### Upload
### Media

```shell
runware upload <file|url> # Upload an image asset; prints imageUUID (and taskUUID) for use in run params
runware media upload <file|url> # Upload media; prints mediaUUID (and mediaURL) for use in run params
runware media delete <mediaUUID> # Permanently delete stored media by its UUID
```

Accepts a local file path, public URL, or data URI. The returned imageUUID can be passed to parameters like `inputs.seedImage=<imageUUID>` on the `run` command. The taskUUID can be used with `runware result`.
Accepts a local file path, public URL, or data URI for upload. The returned mediaUUID can be passed to parameters like `inputs.seedImage=<mediaUUID>` on the `run` command. The taskUUID can be used with `runware result`.

The older `runware upload` command is deprecated; use `runware media upload` instead.

### Result

Expand Down
2 changes: 1 addition & 1 deletion docs/runware.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ Use of Runware services is subject to our Terms of Service (https://runware.ai/t
* [runware auth](runware_auth.md) - Manage authentication
* [runware completion](runware_completion.md) - Generate shell completion scripts
* [runware config](runware_config.md) - Manage CLI configuration
* [runware media](runware_media.md) - Store and delete media in your Runware account
* [runware model](runware_model.md) - Manage and search models
* [runware ping](runware_ping.md) - Check API connectivity
* [runware preset](runware_preset.md) - Manage named presets
* [runware result](runware_result.md) - Wait for and display the result of a task by taskUUID
* [runware run](runware_run.md) - Run an inference request against any Runware model
* [runware upload](runware_upload.md) - Upload an asset and return its UUID
* [runware version](runware_version.md) - Print version information

25 changes: 25 additions & 0 deletions docs/runware_media.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## runware media

Store and delete media in your Runware account

### Options

```
-h, --help help for media
```

### Options inherited from parent commands

```
--debug Show full debug output
-F, --format string CLI output format: table, json, yaml (default "table")
--transport string Transport protocol: ws (WebSocket) or http (REST) (default "ws")
-v, --verbose Show request/response details
```

### SEE ALSO

* [runware](runware.md) - CLI tool for the Runware API
* [runware media delete](runware_media_delete.md) - Permanently delete stored media by its UUID
* [runware media upload](runware_media_upload.md) - Upload media and return its UUID

39 changes: 39 additions & 0 deletions docs/runware_media_delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## runware media delete

Permanently delete stored media by its UUID

### Synopsis

Permanently remove media previously uploaded to your Runware account,
identified by its mediaUUID. This cannot be undone.

```
runware media delete <mediaUUID> [flags]
```

### Examples

```
# delete a stored asset by its UUID
runware media delete 5f1d2c3b-8a4e-4c2a-9f1a-2b3c4d5e6f70
```

### Options

```
-h, --help help for delete
```

### Options inherited from parent commands

```
--debug Show full debug output
-F, --format string CLI output format: table, json, yaml (default "table")
--transport string Transport protocol: ws (WebSocket) or http (REST) (default "ws")
-v, --verbose Show request/response details
```

### SEE ALSO

* [runware media](runware_media.md) - Store and delete media in your Runware account

23 changes: 11 additions & 12 deletions docs/runware_upload.md → docs/runware_media_upload.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
## runware upload
## runware media upload

Upload an asset and return its UUID
Upload media and return its UUID

### Synopsis

Upload an image to the Runware platform for use as input in other tasks
Upload media to the Runware platform for use as input in other tasks
(e.g. image-to-image, upscaling, background removal).

The argument may be a local file path, a publicly accessible URL, or a data URI.
Local files are read and uploaded; URLs and data URIs are forwarded as-is. The
command prints the uploaded imageUUID (and taskUUID), which can be passed to
image parameters such as inputs.seedImage on the run command.
command prints the stored mediaUUID and mediaURL, which can be passed to media
parameters such as inputs.seedImage on the run command.

Supported file types: JPEG, JPG, PNG, WEBP, BMP, GIF. Video and audio upload is
not yet supported by the API.
Accepts any media type: images, video, audio, 3D models, and more.

```
runware upload <file|url> [flags]
runware media upload <file|url> [flags]
```

### Examples

```
# upload a local image and print its UUID
runware upload ./photo.jpg
runware media upload ./photo.jpg

# upload a remote image by URL
runware upload https://example.com/photo.jpg
runware media upload https://example.com/photo.jpg

# upload and use the UUID directly in a run command
runware run runware:100@1 positivePrompt="Same scene at night" width=1024 height=1024 \
inputs.seedImage=$(runware upload ./photo.jpg -F json | jq -r '.imageUUID')
inputs.seedImage=$(runware media upload ./photo.jpg -F json | jq -r '.mediaUUID')
```

### Options
Expand All @@ -50,5 +49,5 @@ runware upload <file|url> [flags]

### SEE ALSO

* [runware](runware.md) - CLI tool for the Runware API
* [runware media](runware_media.md) - Store and delete media in your Runware account

41 changes: 41 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ func (c *Client) AccountDetails(ctx context.Context) (*AccountResult, error) {
// UploadImage uploads an image to the Runware platform via the imageUpload task
// and returns the stored asset's UUID. image may be a publicly accessible URL, a
// data URI, or a base64-encoded image, per the API contract.
//
// Superseded by MediaStorage, which handles any media type and supports deletion;
// retained for the deprecated "upload" command.
Comment on lines +89 to +91
func (c *Client) UploadImage(ctx context.Context, image string) (*ImageUploadResult, error) {
if image == "" {
return nil, fmt.Errorf("image is required")
Expand Down Expand Up @@ -116,6 +119,44 @@ func (c *Client) UploadImage(ctx context.Context, image string) (*ImageUploadRes
return &result, nil
}

// MediaStorage stores or removes media via the mediaStorage task. operation is
// "upload" or "delete"; media is the asset (publicly accessible URL, data URI, or
// base64 string) for upload, or the mediaUUID to remove for delete. Upload returns
// the stored asset's MediaUUID and MediaURL; delete returns just the MediaUUID.
func (c *Client) MediaStorage(ctx context.Context, operation, media string) (*MediaStorageResult, error) {
if operation == "" {
return nil, fmt.Errorf("operation is required")
}
if media == "" {
return nil, fmt.Errorf("media is required")
}

tasks := []any{
&MediaStorageRequest{
TaskType: taskTypeMediaStorage,
TaskUUID: uuid.New(),
Operation: operation,
Media: media,
},
}

data, err := c.transport.Send(ctx, tasks)
if err != nil {
return nil, err
}

if len(data) == 0 {
return nil, fmt.Errorf("empty response from mediaStorage")
}

var result MediaStorageResult
if err := json.Unmarshal(data[0], &result); err != nil {
return nil, fmt.Errorf("failed to parse mediaStorage response: %w", err)
}

return &result, nil
}

// submit sends a single arbitrary task payload and returns all raw JSON responses.
// It is the low-level wire call used by Run after system fields have been injected.
func (c *Client) submit(ctx context.Context, payload map[string]any) ([]json.RawMessage, error) {
Expand Down
7 changes: 7 additions & 0 deletions internal/api/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ const (
taskTypeGetResponse TaskType = "getResponse"
taskTypeAccountManagement TaskType = "accountManagement"
taskTypeImageUpload TaskType = "imageUpload"
taskTypeMediaStorage TaskType = "mediaStorage"
)

// Media storage operations, passed to Client.MediaStorage.
const (
MediaOperationUpload = "upload"
MediaOperationDelete = "delete"
)

// DeliveryMethod specifies how task results are delivered.
Expand Down
135 changes: 135 additions & 0 deletions internal/api/media_storage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package api

import (
"context"
"encoding/json"
"log/slog"
"testing"

"github.com/google/uuid"
)

// TestMediaStorage_UploadSuccess: a valid upload response is parsed and the
// request carries the mediaStorage task type, the upload operation, and media.
func TestMediaStorage_UploadSuccess(t *testing.T) {
mediaUUID := uuid.New()
mediaURL := "https://im.runware.ai/asset.png"
mock := &mockTransport{
responses: []mockResponse{
{data: []json.RawMessage{rawJSON(t, map[string]any{
fieldTaskType: "mediaStorage",
fieldTaskUUID: uuid.New().String(),
"operation": MediaOperationUpload,
"mediaUUID": mediaUUID.String(),
"mediaURL": mediaURL,
})}},
},
}

c := NewClient(mock, slog.Default())
result, err := c.MediaStorage(context.Background(), MediaOperationUpload, "data:image/png;base64,AAAA")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.MediaUUID != mediaUUID {
t.Errorf("MediaUUID = %v, want %v", result.MediaUUID, mediaUUID)
}
if result.MediaURL != mediaURL {
t.Errorf("MediaURL = %q, want %q", result.MediaURL, mediaURL)
}

if len(mock.captured) != 1 || len(mock.captured[0]) != 1 {
t.Fatalf("expected one submitted task, got %#v", mock.captured)
}
req, ok := mock.captured[0][0].(*MediaStorageRequest)
if !ok {
t.Fatalf("submitted task is not *MediaStorageRequest: %T", mock.captured[0][0])
}
if req.TaskType != taskTypeMediaStorage {
t.Errorf("TaskType = %q, want %q", req.TaskType, taskTypeMediaStorage)
}
if req.Operation != MediaOperationUpload {
t.Errorf("Operation = %q, want %q", req.Operation, MediaOperationUpload)
}
if req.Media != "data:image/png;base64,AAAA" {
t.Errorf("Media = %q, want the supplied value", req.Media)
}
}

// TestMediaStorage_DeleteSuccess: a delete response (no mediaURL) is parsed and
// the request carries the delete operation with the target UUID as media.
func TestMediaStorage_DeleteSuccess(t *testing.T) {
mediaUUID := uuid.New()
mock := &mockTransport{
responses: []mockResponse{
{data: []json.RawMessage{rawJSON(t, map[string]any{
fieldTaskType: "mediaStorage",
fieldTaskUUID: uuid.New().String(),
"operation": MediaOperationDelete,
"mediaUUID": mediaUUID.String(),
})}},
},
}

c := NewClient(mock, slog.Default())
result, err := c.MediaStorage(context.Background(), MediaOperationDelete, mediaUUID.String())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.MediaUUID != mediaUUID {
t.Errorf("MediaUUID = %v, want %v", result.MediaUUID, mediaUUID)
}
if result.MediaURL != "" {
t.Errorf("MediaURL = %q, want empty for delete", result.MediaURL)
}

req, ok := mock.captured[0][0].(*MediaStorageRequest)
if !ok {
t.Fatalf("submitted task is not *MediaStorageRequest: %T", mock.captured[0][0])
}
if req.Operation != MediaOperationDelete {
t.Errorf("Operation = %q, want %q", req.Operation, MediaOperationDelete)
}
if req.Media != mediaUUID.String() {
t.Errorf("Media = %q, want the target UUID", req.Media)
}
}

// TestMediaStorage_EmptyOperation: an empty operation is rejected before any
// transport call.
func TestMediaStorage_EmptyOperation(t *testing.T) {
mock := &mockTransport{}
c := NewClient(mock, slog.Default())
if _, err := c.MediaStorage(context.Background(), "", "data:image/png;base64,AAAA"); err == nil {
t.Fatal("expected error for empty operation, got nil")
}
if mock.callCount != 0 {
t.Errorf("expected no transport calls, got %d", mock.callCount)
}
}

// TestMediaStorage_EmptyMedia: an empty media value is rejected before any
// transport call.
func TestMediaStorage_EmptyMedia(t *testing.T) {
mock := &mockTransport{}
c := NewClient(mock, slog.Default())
if _, err := c.MediaStorage(context.Background(), MediaOperationUpload, ""); err == nil {
t.Fatal("expected error for empty media, got nil")
}
if mock.callCount != 0 {
t.Errorf("expected no transport calls, got %d", mock.callCount)
}
}

// TestMediaStorage_EmptyResponse: an empty data slice returns an error.
func TestMediaStorage_EmptyResponse(t *testing.T) {
mock := &mockTransport{
responses: []mockResponse{
{data: []json.RawMessage{}},
},
}
c := NewClient(mock, slog.Default())
if _, err := c.MediaStorage(context.Background(), MediaOperationDelete, uuid.New().String()); err == nil {
t.Fatal("expected error for empty response, got nil")
}
}
10 changes: 5 additions & 5 deletions internal/api/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func TestClientRun_SyncSuccess(t *testing.T) {
func TestClientRun_AsyncSuccess(t *testing.T) {
srv := inferenceSchemaServer(t, requestSchemaWithTaskType("videoInference", "async"))

submitAck := rawJSON(t, map[string]any{"taskUUID": "some-uuid"})
submitAck := rawJSON(t, map[string]any{fieldTaskUUID: "some-uuid"})
pollResult := successItem(t, map[string]any{"videoURL": "https://example.com/vid.mp4"})

mock := &mockTransport{
Expand Down Expand Up @@ -270,7 +270,7 @@ func TestClientRun_OnProgressCalled(t *testing.T) {

mock := &mockTransport{
responses: []mockResponse{
{data: []json.RawMessage{rawJSON(t, map[string]any{"taskUUID": "x"})}}, // submit
{data: []json.RawMessage{rawJSON(t, map[string]any{fieldTaskUUID: "x"})}}, // submit
{data: []json.RawMessage{processingItem(t, 30)}},
{data: []json.RawMessage{processingItem(t, 70)}},
{data: []json.RawMessage{successItem(t, nil)}},
Expand Down Expand Up @@ -438,7 +438,7 @@ func TestClientRun_UserProvidedTaskUUID(t *testing.T) {
if err := json.Unmarshal(taskBytes, &payload); err != nil {
t.Fatalf("unmarshal captured task: %v", err)
}
got, ok := payload["taskUUID"]
got, ok := payload[fieldTaskUUID]
if !ok {
t.Fatal("taskUUID not present in submitted payload")
}
Expand All @@ -461,8 +461,8 @@ func TestClientRun_UserProvidedTaskUUID_Invalid(t *testing.T) {
if err == nil {
t.Fatal("expected error for invalid taskUUID, got nil")
}
if !strings.Contains(err.Error(), "taskUUID") {
t.Errorf("expected error to mention %q, got: %v", "taskUUID", err)
if !strings.Contains(err.Error(), fieldTaskUUID) {
t.Errorf("expected error to mention %q, got: %v", fieldTaskUUID, err)
}
if mock.callCount != 0 {
t.Errorf("expected 0 transport calls, got %d", mock.callCount)
Expand Down
Loading