# origin-asset

Rust SDK for the Origin Asset Gateway service.

[![Crates.io](https://img.shields.io/crates/v/origin-asset.svg)](https://crates.io/crates/origin-asset)
[![docs.rs](https://docs.rs/origin-asset/badge.svg)](https://docs.rs/origin-asset)
[![License](https://img.shields.io/crates/l/origin-asset.svg)](LICENSE-MIT)

## Installation

```toml
[dependencies]
origin-asset = "0.11.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

## Quick Start

```rust
use origin_asset::AssetClient;

#[tokio::main]
async fn main() -> origin_asset::Result<()> {
    let client = AssetClient::new("your-api-key");

    let image = client.generate_image("a fire sword", None).await?;
    println!("URL: {:?}", image.output_url);

    let healthy = client.health().await?;
    println!("Healthy: {healthy}");

    Ok(())
}
```

## Configuration

### Default endpoint

`AssetClient::new("your-api-key")` uses `https://asset.origingame.dev`.

### Builder

```rust
let client = origin_asset::AssetClient::builder("your-api-key")
    .base_url("https://my-asset-gateway.example.com")
    .http_client(custom_reqwest_client)
    .build();
```

## Supported Operations

- `generate`
- `generate_image`
- `generate_video`
- `generate_audio`
- `generate_tts`
- `generate_music`
- `generate_model3d`
- `generate_character`
- `generate_prop`
- `generate_sprite`
- `generate_world`
- `generate_text`
- `generate_image_async` and the other `generate_*_async` helpers
- `batch`
- `batch_async`
- `process`
- `process_async`
- `process3d`
- `process3d_async`
- `voice_clone`
- `voice_clone_async`
- `voice_design`
- `voice_list`
- `voice_delete`
- `upload_file`
- `list_assets`
- `delete_asset`
- `library_search`
- `library_ingest`
- `library_ingest_sync`
- `library_ingest_status`
- `library_ingest_wait`
- `library_packs`
- `library_packs_install`
- `library_packs_delete`
- `library_packs_refresh`
- `library_add`
- `library_get`
- `library_delete`
- `library_catalog_jobs`
- `jobs`
- `job_status`
- `job_wait_sse`
- `providers`
- `health`

## Async Submit + Wait

```rust,no_run
use origin_asset::{AssetClient, AsyncSubmitOptions, ImageOptions};

#[tokio::main]
async fn main() -> origin_asset::Result<()> {
    let client = AssetClient::new("your-api-key");

    let ack = client
        .generate_image_async(
            "cinematic sci-fi skyline",
            Some(ImageOptions {
                size: Some("1536x1024".into()),
                ..Default::default()
            }),
            Some(AsyncSubmitOptions {
                idempotency_key: Some("skyline-001".into()),
                async_mode: true,
            }),
        )
        .await?;

    let job = client
        .job_wait_sse(&ack.job_id, |event| {
            println!("{} {}", event.kind, event.status);
        })
        .await?;

    println!("Output: {:?}", job.output_url);
    Ok(())
}
```

Rust uses separate `_async` methods because it has no keyword arguments. These methods are equivalent to TypeScript calls like `forge.image(prompt, { async: true })` and send `Prefer: respond-async` plus `Idempotency-Key` when provided.

## Example

```rust
use origin_asset::{
    AssetClient, AssetType, AudioOptions, BatchGenerateRequest, CharacterOptions, ImageOptions,
    Model3dOptions, Process3dRequest, PropOptions, TextOptions, TtsOptions, WorldOptions,
};
use serde_json::json;

let client = AssetClient::new("your-api-key");

let image = client.generate_image("a medieval castle", Some(ImageOptions {
    size: Some("1792x1024".into()),
    model: Some("gpt-image-1".into()),
    transparent: Some(true),
    ..Default::default()
})).await?;

let speech = client.generate_tts("Hello world!", Some(TtsOptions {
    voice: Some("alloy".into()),
    ..Default::default()
})).await?;

let sfx = client.generate_audio("explosion boom", Some(AudioOptions {
    duration: Some(3),
    ..Default::default()
})).await?;

let model = client.generate_model3d("stylized oak tree", Some(Model3dOptions {
    format: Some("glb".into()),
    polycount: Some(20000),
    pbr: Some(true),
    hd_texture: Some(true),
    ai_model: Some("v3.1-20260211".into()),
    ..Default::default()
})).await?;

let character = client.generate_character("warrior", Some(CharacterOptions {
    format: Some("fbx".into()),
    polycount: Some(30000),
    pbr: Some(true),
    hd_texture: Some(true),
    pose_mode: Some("t-pose".into()),
    ..Default::default()
})).await?;

let prop = client.generate_prop("treasure chest", Some(PropOptions {
    format: Some("glb".into()),
    pbr: Some(true),
    ..Default::default()
})).await?;

let world = client.generate_world("misty sci-fi canyon", Some(WorldOptions {
    display_name: Some("Canyon Outpost".into()),
    ..Default::default()
})).await?;

let text = client.generate_text("Write a short item description", Some(TextOptions {
    max_tokens: Some(120),
    ..Default::default()
})).await?;

let batch = client.batch(&BatchGenerateRequest {
    asset_type: AssetType::Image,
    prompts: vec!["red potion".into(), "blue potion".into()],
    shared: Default::default(),
    compose: None,
}).await?;

let remesh = client.process3d(&Process3dRequest {
    task_id: Some("tripo-task-id".into()),
    operation: "remesh".into(),
    params: json!({ "polycount": 20000, "format": "glb" }),
    ..Default::default()
}).await?;

let jobs = client.jobs(Some("completed"), Some(10)).await?;
```

## Uploads, Voice, and Library

```rust,no_run
use origin_asset::{
    AssetClient, IngestSource, LibraryAddRequest, LibraryIngestRequest, LibraryPackInstallRequest,
    LibraryPackSource, LibrarySearchMode, LibrarySearchQuery, VoiceCloneRequest, VoiceType,
};

let client = AssetClient::new("your-api-key");

let upload = client.upload_file("./reference.png").await?;
let assets = client.list_assets().await?;

let cloned_voice = client.voice_clone(&VoiceCloneRequest {
    sample_data_url: None,
    audio_base64: Some("...".into()),
    audio_mime: Some("audio/mpeg".into()),
    text: None,
    preview_text: Some("Welcome back.".into()),
    style: None,
    save_as: None,
    name: Some("hero".into()),
    target_model: None,
}).await?;
let voices = client.voice_list(Some(VoiceType::Vc), None, None).await?;

let item = client.library_add(&LibraryAddRequest {
    asset_type: "image".into(),
    name: "Reference".into(),
    file_url: upload.url,
    ..Default::default()
}).await?;
let search = client.library_search(&LibrarySearchQuery {
    q: Some("reference".into()),
    r#type: Some("image".into()),
    mode: Some(LibrarySearchMode::Hybrid),
    limit: Some(10),
    ..Default::default()
}).await?;
let loaded = client.library_get(&item.id).await?;

let ingest = client.library_ingest(&LibraryIngestRequest {
    source: IngestSource::Manual,
    asset_type: "image".into(),
    file_url: Some("https://example.com/reference.png".into()),
    file_data_b64: None,
    thumbnail_url: None,
    name: Some("Reference".into()),
    user_tags: vec!["reference".into()],
    user_metadata: serde_json::Value::Null,
    pack_id: None,
    pack_asset_key: None,
    source_job_id: None,
}).await?;
let ingested = client.library_ingest_wait(&ingest.ingest_job_id, 1000).await?;

let pack = client.library_packs_install(&LibraryPackInstallRequest {
    source: LibraryPackSource::Builtin,
    id: Some("hello-world".into()),
    version: None,
    url: None,
    tarball_b64: None,
}).await?;
let packs = client.library_packs().await?;
```

## Error Handling

All methods return `origin_asset::Result<T>`, which uses `OriginError`:

```rust
use origin_asset::OriginError;

match origin_asset::AssetClient::new("key").generate_image("test", None).await {
    Ok(response) => println!("Success: {:?}", response.output_url),
    Err(OriginError::Api { status, message, .. }) => {
        eprintln!("API error ({}): {}", status, message);
    }
    Err(OriginError::Http(e)) => eprintln!("Network error: {e}"),
    Err(e) => eprintln!("Other error: {e}"),
}
```

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.
