> ## Documentation Index
> Fetch the complete documentation index at: https://docs.camb.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Stories

> Turn a text or Word document into narrated audio using the Python or TypeScript SDK

## Overview

Upload a `.txt` or `.docx` manuscript and produce **narrated audio**: full narration, an optional dialogue-only track, and optionally a time-coded transcript. Pick the narrator voice and source language, then run an asynchronous job and fetch URLs when it completes.

### Prerequisites

<Steps>
  <Step title="Create an account">
    Sign up at [CAMB.AI Studio](https://studio.camb.ai) if you haven't already.
  </Step>

  <Step title="Get your API key">
    Go to **Settings → API Keys** in Studio and copy your key. See [Authentication](/getting-started/authentication) for details.
  </Step>

  <Step title="Install the SDK">
    <CodeGroup>
      ```bash Python theme={null}
      pip install camb-sdk
      ```

      ```bash TypeScript theme={null}
      npm install @camb-ai/sdk
      ```
    </CodeGroup>

    Skip this step if you're using the [direct API](/tutorials/direct-api).
  </Step>

  <Step title="Set your API key to use in your code">
    ```bash theme={null}
    export CAMB_API_KEY="your_api_key_here"
    ```
  </Step>
</Steps>

***

## Code

<CodeGroup>
  ```python Python theme={null}
  import os
  import time
  from camb.client import CambAI
  from camb.types.language_enums import Languages

  client = CambAI(api_key=os.getenv("CAMB_API_KEY"))

  def narrate_story():
      # Replace with your story file (.txt or .docx)
      story_path = "story.txt"

      # Step 1: Submit the story job (multipart upload)
      response = client.story.create_story(
          file=open(story_path, "rb"),
          source_language=Languages.EN_US,
          title="My Story",
          description="Sample narrated story",
          narrator_voice_id=147320,
      )

      # Usually returns task_id for async processing (see Note below)
      task_id = getattr(response, "task_id", None)
      if not task_id:
          print("No task_id on response; if you received run_id directly, skip polling and call get_story_run_info.")
          return

      print(f"Story task created: {task_id}")

      # Step 2: Poll until complete
      max_attempts = 60
      attempt = 0
      while attempt < max_attempts:
          status = client.story.get_story_status(task_id=task_id)
          print(f"Status: {status.status}")

          if status.status == "SUCCESS":
              # Step 3: Full narration, dialogue track, optional transcript
              result = client.story.get_story_run_info(
                  status.run_id,
                  include_transcript=True,
              )
              print(f"Audio URL: {result.get('audio_url')}")
              print(f"Dialogue URL: {result.get('dialogue_url')}")
              transcript = result.get("transcript")
              if transcript:
                  for row in transcript[:2]:
                      print(f"[{row.get('start')}-{row.get('end')}ms] {row.get('speaker')}: {row.get('text')}")
              break
          elif status.status == "ERROR":
              print(f"Story failed: {status.exception_reason}")
              break

          time.sleep(5)
          attempt += 1

  narrate_story()
  ```

  ```javascript TypeScript theme={null}
  import { CambClient, CambApi } from '@camb-ai/sdk';
  import * as fs from 'fs';

  const client = new CambClient({
      apiKey: process.env.CAMB_API_KEY
  });

  async function narrateStory() {
      const storyPath = 'story.txt'; // Replace with your .txt or .docx

      if (!fs.existsSync(storyPath)) {
          console.error('Story file not found:', storyPath);
          return;
      }

      // Step 1: Submit the story job (multipart upload)
      const response = await client.story.createStory({
          file: fs.createReadStream(storyPath),
          source_language: CambApi.Languages.EN_US,
          title: 'My Story',
          description: 'Sample narrated story',
          narrator_voice_id: 147320
      });

      // Usually returns task_id for async processing (see Note below)
      const taskId = 'task_id' in response && response.task_id ? response.task_id : null;
      if (!taskId) {
          console.error('No task_id on response; if you received run_id directly, skip polling and call getStoryRunInfo.');
          return;
      }

      console.log(`Story task created: ${taskId}`);

      // Step 2: Poll until complete
      const maxAttempts = 60;
      let attempt = 0;

      while (attempt < maxAttempts) {
          const status = await client.story.getStoryStatus({
              task_id: taskId
          });

          console.log(`Status: ${status.status}`);

          if (status.status === 'SUCCESS') {
              // Step 3: Full narration, dialogue track, optional transcript
              const result = await client.story.getStoryRunInfo({
                  run_id: status.run_id,
                  include_transcript: true
              });

              console.log(`Audio URL: ${result['audio_url']}`);
              console.log(`Dialogue URL: ${result['dialogue_url']}`);
              const transcript = result['transcript'];
              if (Array.isArray(transcript)) {
                  for (const row of transcript.slice(0, 2)) {
                      const r = row;
                      console.log(
                          `[${r['start']}-${r['end']}ms] ${r['speaker']}: ${r['text']}`
                      );
                  }
              }
              break;
          } else if (status.status === 'ERROR') {
              console.error(`Story failed: ${status.exception_reason}`);
              break;
          }

          await new Promise(resolve => setTimeout(resolve, 5000));
          attempt++;
      }

      if (attempt >= maxAttempts) {
          console.error('Timeout waiting for story completion.');
      }
  }

  narrateStory();
  ```
</CodeGroup>

<Note>
  `create_story` / `createStory` may return either a **`task_id`** (poll as above) or, in some flows, a **`run_id`** immediately. If you already have `run_id`, call `get_story_run_info` / `getStoryRunInfo` without polling.
</Note>

***

## Parameters

### Required

| Parameter         | Type             | Description                                                                                                                                                 |
| ----------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `file`            | file stream      | `.txt` or `.docx` content to narrate                                                                                                                        |
| `source_language` | `Languages` enum | Language of the manuscript (e.g. `Languages.EN_US`). The raw API also accepts locale-tag strings like `"en-us"`; numeric IDs still work but are deprecated. |

### Optional

| Parameter             | Type       | Description                                                                                                                                   |
| --------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `title`               | string     | Story title                                                                                                                                   |
| `description`         | string     | Short description                                                                                                                             |
| `narrator_voice_id`   | integer    | Voice ID for the narrator (use [list voices](/api-reference/endpoint/list-voices) or pick one from [Text to Speech](/tutorials/tts-with-sdk)) |
| `folder_id`           | integer    | Folder for organizing the run                                                                                                                 |
| `chosen_dictionaries` | integer\[] | Custom pronunciation dictionaries                                                                                                             |
| `run_id`              | integer    | Optional tracing / linkage                                                                                                                    |

***

## Tips

* **Formats:** Use `.txt` or `.docx` as described in the [Create Story](/api-reference/endpoint/create-story) reference.
* **Languages:** Use the SDK `Languages` enum (e.g. `Languages.EN_US`) for `source_language`. The raw API also accepts locale-tag strings like `"en-us"`. See [Get source languages](/api-reference/endpoint/get-source-languages) for the full list.
* **URLs:** Download links from `audio_url` and `dialogue_url` **expire after 24 hours**—fetch and store files promptly.
* **Polling:** Cap your loop (for example 60 attempts × 5 seconds) and handle timeouts separately from `ERROR` status.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Create Story API" icon="book" href="/api-reference/endpoint/create-story">
    Multipart upload and story parameters.
  </Card>

  <Card title="Get Story Status" icon="clock" href="/api-reference/endpoint/get-story-status">
    Poll with `task_id`.
  </Card>

  <Card title="Get Story Run Info" icon="headphones" href="/api-reference/endpoint/get-story-run-info">
    `audio_url`, `dialogue_url`, and optional transcript.
  </Card>

  <Card title="Text to Speech" icon="audio-lines" href="/tutorials/tts-with-sdk">
    Generate speech and explore voices for `narrator_voice_id`.
  </Card>
</CardGroup>
