# FishAudio private Coze plugin import package

This directory is for customers who prefer to create private plugins in their own Coze workspace instead of installing the public store listings.

## Files

- `fishaudio-tts.openapi.yaml`: one `generate_tts` text-to-speech tool.
- `fishaudio-media-sync.openapi.yaml`: `create_media_sync` and `get_media_sync_job`.
- `fishaudio-video-dubbing.openapi.yaml`: `create_text_lip_sync` and `get_text_lip_sync_job` for text-to-lip-sync video generation.
- `fishaudio-personal-voice.openapi.yaml`: private `FishAudio 专属语音合成` plugin with `create_personal_voice` and `get_personal_voice_job`.
- `*.ai-plugin.json`: plugin metadata for the left-hand `ai_plugin` field on Coze's `</>` code-creation page. Every OpenAPI YAML has a matching JSON file with the same prefix.
- `fishaudio-tts-icon.png`: FishAudio avatar with a `TTS` badge.
- `fishaudio-media-sync-icon.png`: FishAudio avatar with an `LS` badge for image/video lip sync.
- `fishaudio-video-dubbing-icon.png`: Text-to-lip-sync avatar with a `VD` badge.
- `fishaudio-personal-voice-icon.png`: `FishAudio 专属语音合成` avatar with a `PVS` badge.
- `manifest.json`: the FishAudio delivery-package version, endpoints, and file inventory. **It is not a Coze `ai_plugin` file. Do not paste it into the `ai_plugin` field.**

The package contains two public store plugins and two private-import plugins. `FishAudio TTS 文字转语音` and `FishAudio 图片/视频对口型` are available in the store; `FishAudio 文字对口型` and `FishAudio 专属语音合成` are imported into the customer's own Coze workspace.

## Recommended: use Import

1. Open the resource library and create a plugin.
2. Select a cloud plugin.
3. Select the option to create from an existing service.
4. Click **Import** in the upper-right corner and upload or paste the relevant `*.openapi.yaml`. This route only needs the OpenAPI YAML; it does not ask for `ai_plugin`.
5. Confirm that the plugin URL is `https://fishaudio.org`.
6. Select no authorization. Each customer supplies `Authorization` as a tool input at runtime.
7. Enable the imported tools and run them with the customer's own Fish Audio API key.
8. Publish to the customer's workspace. Private use does not require plugin-store review.

A tool showing a failed debug status immediately after import does not necessarily mean the import failed. It usually means the tool has not yet completed a successful run with valid inputs. Open each tool, run it with valid inputs, and then inspect any specific error.

## If you see both `ai_plugin` and `openapi` editors

You are on the `</>` code-creation page. Both fields are required; do **not** leave `ai_plugin` empty:

1. Left, `ai_plugin (JSON)`: paste the matching `*.ai-plugin.json` file.
2. Right, `openapi (YAML)`: paste the matching `*.openapi.yaml` file.
3. Do not paste `manifest.json` on the left; it is only the FishAudio package inventory.

| Plugin                    | Left: `ai_plugin`                         | Right: `openapi`                        |
| ------------------------- | ----------------------------------------- | --------------------------------------- |
| FishAudio TTS 文字转语音  | `fishaudio-tts.ai-plugin.json`            | `fishaudio-tts.openapi.yaml`            |
| FishAudio 图片/视频对口型 | `fishaudio-media-sync.ai-plugin.json`     | `fishaudio-media-sync.openapi.yaml`     |
| FishAudio 文字对口型      | `fishaudio-video-dubbing.ai-plugin.json`  | `fishaudio-video-dubbing.openapi.yaml`  |
| FishAudio 专属语音合成    | `fishaudio-personal-voice.ai-plugin.json` | `fishaudio-personal-voice.openapi.yaml` |

The matching JSON intentionally uses `auth.type: none`. Each customer still enters their own Fish Audio key at runtime as `Authorization: Bearer ...`; never place a real key in either file.

## Authorization

Every tool expects:

```text
Bearer YOUR_FISH_AUDIO_API_KEY
```

There must be one space after `Bearer`. Never put a real key in the YAML, a screenshot, or a shared workflow template. Create a separate Coze key and rotate or revoke it when needed.

## TTS requirements

The TTS file calls:

```text
POST https://fishaudio.org/api/open/v1/speech/tts
```

Keep `cache` set to `true`. This makes the API return JSON containing `audio_url`. Setting it to `false` returns raw audio bytes, which Coze may reject as an unparseable response.

The `audio_url` is temporary and currently expires after three days. Download or transfer it promptly.

## Media Sync requirements

Save `data.id` from the create tool and pass it to the query tool as `jobId`:

```text
create_media_sync → data.id → wait 15–30 seconds → get_media_sync_job
```

- `processing`: query the same `jobId` again.
- `completed`: read `data.result_url`.
- `failed`: read `data.error_message`.

Do not call `create_media_sync` again when a query times out or the job remains processing. That can create and charge for a duplicate job.

Video and audio URLs must be directly downloadable by the Fish Audio service. Web pages, local paths, and expired signed URLs cannot be used as media inputs.

Video mode bills the shorter video/audio input, rounded up to a whole second. The create response returns `billing_duration_seconds` and `credits_used`; successful-job pricing does not change later, while failed jobs receive a full refund. Set `video_extension: true` only when the video must be extended to the audio duration.

## Text-to-lip-sync requirements

This plugin uses the same combined API as the website Video Dubbing workbench. The service generates TTS audio with the selected voice and then creates the lip-sync job automatically:

```text
create_text_lip_sync → data.id → wait 15–30 seconds → get_text_lip_sync_job
```

- Provide `video_url`, target `text`, and an existing voice `reference_id`; no pre-generated `audio_url` is required.
- Use a new `Idempotency-Key` for every new job. Reuse the original value only when retrying the exact same request.
- `data.audio_url` in the create response is the intermediate TTS audio; read the final video from `data.result_url` in the query response.
- `credits_used` separates `tts`, `lip_sync`, and `total`.
- Querying the same `jobId` does not regenerate speech or charge again. Never recreate a job while it is processing.

## FishAudio 专属语音合成 requirements

This plugin combines persistent private-voice creation with asynchronous TTS:

```text
create_personal_voice → data.id → wait → get_personal_voice_job
```

- Provide `source_audio_url` and the target `text`; a reference transcript is not required.
- The source URL must be directly downloadable by Fish Audio and is limited to 10 MB.
- Use a new `Idempotency-Key` for every new job. Reuse the original value only when retrying the exact same request.
- A successful create response contains the persistent `data.voice_id`, which can be reused with ordinary TTS.
- When the job is `completed`, read `data.audio_url`; the temporary URL currently expires after three days.
- Voice cloning and TTS are charged separately. If cloning succeeds but TTS startup fails, the error contains `clone_completed: true` and `voice_id`; the created voice and clone charge remain.

## Upload local reference audio in a workflow

On the standalone `create_personal_voice` test page, `source_audio_url` is a text URL parameter; it cannot select a local computer file. To let end users upload audio from their computer, use a Coze **workflow**: add an **Audio** input to the Start node and connect it to this tool.

> The Coze upload control may show a 500 MB limit, but this Fish Audio endpoint accepts reference audio up to **10 MB**. Start with a short MP3, WAV, or M4A file, and make sure you have permission to use the voice.

### Phase 1: verify the minimal workflow

1. Create a workflow. In the Start node, add these required inputs:

   | Variable          | Type   | Purpose                                                       |
   | ----------------- | ------ | ------------------------------------------------------------- |
   | `source_audio`    | Audio  | Reference audio selected from the user's computer             |
   | `text`            | String | Text to synthesize                                            |
   | `authorization`   | String | `Bearer ` followed by the user's Fish Audio API key           |
   | `idempotency_key` | String | A new value for each new generation, such as `coze-voice-001` |

2. Add the `create_personal_voice` tool from `FishAudio 专属语音合成` and bind its inputs:

   | Tool input         | Workflow variable |
   | ------------------ | ----------------- |
   | `Authorization`    | `authorization`   |
   | `Idempotency-Key`  | `idempotency_key` |
   | `source_audio_url` | `source_audio`    |
   | `text`             | `text`            |

   Coze passes the Audio variable as a temporary accessible file URL. Do not ask users to enter a separate `source_audio_url` in the Start node.

3. Add an End node that first returns `data.voice_id` and `data.id` (the latter is the `jobId`). The minimal workflow is:

   ```text
   Start (upload Audio + enter text, key, and idempotency value)
     → create_personal_voice
     → End (voice_id + jobId)
   ```

4. Click **Run** and provide:
   - `authorization`: exactly `Bearer <the customer's API key>`;
   - `idempotency_key`: a new value for every new generation;
   - `source_audio`: a local audio file no larger than 10 MB;
   - `text`: a short target text for the first test.

If the tool input in the run log contains an address such as `source_audio_url: https://...byteimg.com/...`, the local-upload-to-temporary-URL conversion succeeded. A later `401 ERR_UNAUTHORIZED` means the API key is invalid, revoked, or incorrectly formatted; it is not an upload failure.

After creation returns both `data.voice_id` and `data.id`, choose one of the following query methods.

### Phase 2: query the generated result

The simplest setup is to wait 15–30 seconds, then manually run `get_personal_voice_job` with the same `Authorization` and the `data.id` from the create response.

For automatic polling, add a Loop node with a 20-iteration limit. Inside the loop, place these nodes in order:

1.  A Python Code node that waits 15 seconds with `await asyncio.sleep(15)`.
2.  `get_personal_voice_job`, with `Authorization` still bound to `authorization` and `jobId` bound to the create response's `data.id`.
3.  A Selector node that routes `completed` or `failed` to Terminate Loop and continues for `processing`.

Bind the Loop output to the last `data` returned by the query tool. Return `audio_url` for `completed` and `error_message` for `failed`. If all 20 iterations remain `processing`, return a timeout message and the original `jobId` for later querying; never create another job.

The wait Code node can use:

```python
import asyncio

async def main(args: Args) -> Output:
    await asyncio.sleep(15)
    return {"waited": True}
```

### Read run logs and diagnose errors

| Symptom                                         | Meaning and action                                                                                                                           |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| The input contains `https://...byteimg.com/...` | Coze uploaded the file and converted the Audio value to a URL successfully.                                                                  |
| `401 ERR_UNAUTHORIZED`                          | The key is invalid or revoked, or the `Bearer ` prefix is missing or malformed. Use a valid key and retry.                                   |
| `413` or file too large                         | Fish Audio limits the file to 10 MB even if the Coze upload control shows a larger limit.                                                    |
| Media download failure or `415`                 | The temporary URL expired, the server cannot download it, or the media type is unsupported. Re-upload a short file in a common audio format. |
| The job remains `processing`                    | Keep querying the same `jobId`; do not create another job.                                                                                   |

Reuse an `Idempotency-Key` only when retrying the **exact same** request. Re-uploading a file changes its temporary URL and therefore requires a new key. `Authorization` and `idempotency_key` are normal workflow inputs and may appear in run logs; never save or share a test set, template, or screenshot containing a real key.

## Pre-publish checklist

- The import creates one TTS tool, two Media Sync tools, two Text-to-Lip-Sync tools, or two FishAudio 专属语音合成 tools as applicable.
- A TTS test returns a non-empty `audio_url`.
- A create test returns a non-empty `data.id`.
- The query tool exposes `status`, `progress`, `result_url`, and `error_message`.
- Text-to-Lip-Sync returns `data.id` and `data.audio_url`, then exposes `data.result_url` when complete.
- FishAudio 专属语音合成 returns `voice_id` and `data.id`, then exposes `audio_url` when complete.
- No real API key is stored in the plugin or a saved example.
