# Detect Source: https://docs.perceptron.inc/capabilities/detect api-reference/openapi.json POST /v1/detect Return grounded object detections for a single image from text categories, annotated exemplars, or exhaustive detection. ## Overview The Detect API returns grounded object detections for a single image. Use it when you want a direct detection workflow without building a chat-completions prompt. Requests can include text `categories`, annotated positive `exemplars`, both together, or no targets for exhaustive object detection. You can also include `negative_exemplars`, but only alongside at least one category or positive exemplar. Coordinates for exemplar annotations and returned detections are image pixels. ## Examples ### Detect all objects as boxes Omit `categories` and `exemplars` to ask the model to return the objects it finds in the image. This is useful for exploratory labeling or scene inventory. Box output is the default. ```bash theme={null} curl https://api.perceptron.inc/v1/detect \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -d '{ "media": { "type": "image", "image_url": "https://example.com/warehouse.jpg" }, "config": { "output": "box", "reasoning_effort": "high" } }' ``` Box output example showing detected packages in a delivery van ### Detect target categories as boxes Pass `categories` when you know the labels you want returned. Each detection `mention` is one of the requested categories. ```bash theme={null} curl https://api.perceptron.inc/v1/detect \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -d '{ "media": { "type": "image", "image_url": "https://example.com/warehouse.jpg" }, "categories": ["person", "hard_hat"], "config": { "output": "box", "reasoning_effort": "none" } }' ``` ### Detect one category as points Set `config.output` to `"point"` when you want one pixel point per detection. With exactly one unique category and no exemplars, `/v1/detect` uses a direct point prompt and forces every returned `mention` to that category. ```bash theme={null} curl https://api.perceptron.inc/v1/detect \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -d '{ "media": { "type": "image", "image_url": "https://example.com/warehouse.jpg" }, "categories": ["forklift"], "config": { "output": "point", "reasoning_effort": "none" } }' ``` ### Detect all objects as points You can also request point output without categories or exemplars. The route still uses exhaustive object detection internally, then returns the center point for each detected box or polygon. ```bash theme={null} curl https://api.perceptron.inc/v1/detect \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -d '{ "media": { "type": "image", "image_url": "https://example.com/warehouse.jpg" }, "config": { "output": "point" } }' ``` Point output example showing centroid points for detected packages ### Detect from exemplars as boxes Use `exemplars` when the target is visual or domain-specific. Exemplar annotations use pixel coordinates in the exemplar image. If you omit `categories`, the exemplar annotation `mention` values define the target labels. ```bash theme={null} curl https://api.perceptron.inc/v1/detect \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -d '{ "media": { "type": "image", "image_url": "https://example.com/loading-dock.jpg" }, "exemplars": [ { "media": { "type": "image", "image_url": "data:image/png;base64,..." }, "annotations": [ { "mention": "damaged_box", "point_box": { "top_left": { "x": 124, "y": 80 }, "bottom_right": { "x": 310, "y": 240 } } } ] } ], "config": { "output": "box" } }' ``` ### Detect from exemplars as points For exemplar-guided point output, set `config.output` to `"point"`. Positive exemplars can use either `point` or `point_box` annotations. The route uses the exemplar labels to find matching objects, then returns a `point` for each detection. ```bash theme={null} curl https://api.perceptron.inc/v1/detect \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -d '{ "media": { "type": "image", "image_url": "https://example.com/loading-dock.jpg" }, "categories": ["damaged_box"], "exemplars": [ { "media": { "type": "image", "image_url": "data:image/png;base64,..." }, "annotations": [ { "mention": "damaged_box", "point": { "x": 218, "y": 160 } }, { "mention": "damaged_box", "point_box": { "top_left": { "x": 124, "y": 80 }, "bottom_right": { "x": 310, "y": 240 } } } ] } ], "config": { "output": "point" } }' ``` ## Configure reasoning Use **`config.reasoning_effort`** with `none`, `minimal`, `low`, `medium`, or `high`. The default is `none`; `minimal` currently uses the same reasoning level as `low`. The setting belongs inside `config` on `/v1/detect`, unlike the top-level field on chat completions and Multilook. An unsupported value returns HTTP 400. The effect depends on the detection mode: * **No categories or exemplars:** a value other than `none` enables model reasoning, for either box or point output. The first example uses `high`. Reasoning stays internal; the response contains `detections` and `finish_reason`, without reasoning text. * **Categories with box output, without positive or negative exemplars:** a value other than `none` selects a detection workflow that can inspect additional image regions. It does not enable model reasoning, and the non-`none` tiers do not select different reasoning levels in this mode. * **Category-based point output or any exemplar-guided request:** the field is accepted, but model reasoning remains disabled and the selected tier does not change the detection workflow. For older clients, `config.enable_thinking` remains a deprecated alias: `true` maps to `high`, and `false` maps to `none`. **An explicit `config.reasoning_effort` takes precedence** when both are supplied. For example, `{"reasoning_effort": "none", "enable_thinking": true}` keeps the resolved effort at `none`. Prefer sending only `reasoning_effort`. ## Supported permutations | Request shape | `config.output` | Behavior | | ---------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `media` only | `"box"` or omitted | Exhaustive object detection; returns `point_box` for every parsed object. | | `media` only | `"point"` | Exhaustive object detection; returns `point` values converted from parsed boxes or polygons. | | One or more `categories` entries, no positive exemplars | `"box"` | Category detection; returns `point_box` values. Duplicate categories are deduplicated for prompting. | | One unique `categories` entry, no positive or negative exemplars | `"point"` | Direct point prompt; every returned `mention` is forced to that category. | | Multiple unique `categories` entries | `"point"` | Category detection; returns centroid `point` values from parsed boxes or polygons. | | Positive `exemplars`, with or without `categories` | `"box"` | Exemplar-guided detection; returns `point_box` values. If `categories` is omitted, exemplar `mention` values define the target labels. | | Positive `exemplars`, with or without `categories` | `"point"` | Exemplar-guided detection; returns `point` values. Exemplar annotations may use `point` or `point_box`. | | `negative_exemplars` with categories or positive exemplars | `"box"` or `"point"` | Uses negative images as examples of what not to detect. Negative exemplars do not define labels. | | `negative_exemplars` only | Any | Invalid request; include at least one category or positive exemplar. | ## Example response ### Box response ```json theme={null} { "detections": [ { "mention": "hard_hat", "point_box": { "top_left": { "x": 384.0, "y": 108.0 }, "bottom_right": { "x": 768.0, "y": 540.0 } } } ], "finish_reason": "complete" } ``` ### Point response ```json theme={null} { "detections": [ { "mention": "forklift", "point": { "x": 512.0, "y": 320.0 } } ], "finish_reason": "complete" } ``` ## Notes * `/v1/detect` supports image inputs only. * `config.output` can be `"box"` or `"point"`; the default is `"box"`. The public value is `"box"`, not `"bbox"`. * The response does not include confidence scores. * `finish_reason` is `"complete"` or `"max_tokens"`. * When `categories` is set, every positive exemplar annotation `mention` must exactly match one of those categories. * Exemplar annotations must include exactly one of `point` or `point_box`. * Point outputs may come from parsed points directly, or from the centroid of a parsed box or polygon. * `config.reasoning_effort` does not change the response schema; its effect on detection depends on the request mode described above. ## Limits | Limit | Value | | ------------------------ | ----- | | `categories` entries | 256 | | Positive `exemplars` | 64 | | `negative_exemplars` | 64 | | Annotations per exemplar | 256 | See the API reference below for the complete request and response schemas. # Create chat completion Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/chat-completions/create-chat-completion /api-reference/openapi.json post /v1/chat/completions Generate a response for a text or multimodal conversation, using the OpenAI chat completions request and response format. # Create multilook chat completions Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/chat-completions/create-multilook-chat-completions /api-reference/openapi.json post /v1/chat/completions/multilook Run several independent prompts over one shared, prefilled context. # Detect objects Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/detection/detect-objects /api-reference/openapi.json post /v1/detect Return grounded object detections for a single image from text categories, annotated exemplars, or exhaustive detection. # Delete a file Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/files/delete-a-file /api-reference/openapi.json delete /v1/files/{file_id} Permanently deletes a file. Once deleted, the file can no longer be retrieved, downloaded, or referenced in requests. Returns 404 if no file with the given id exists. # Download a file's contents Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/files/download-a-files-contents /api-reference/openapi.json get /v1/files/{file_id}/content Returns the raw bytes with the file's stored `Content-Type` header. # List files Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/files/list-files /api-reference/openapi.json get /v1/files Returns a paginated list of files belonging to the caller's organization. Cursor pagination via `after`; see the `limit` and `order` parameters for defaults. # Retrieve a file Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/files/retrieve-a-file /api-reference/openapi.json get /v1/files/{file_id} Returns metadata for a single file (size, content type, purpose, etc.). Use the `/v1/files/{file_id}/content` endpoint to download the bytes. # Upload a file Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/files/upload-a-file /api-reference/openapi.json post /v1/files Accepts a single file via multipart upload alongside a `purpose` field that declares how the file will be used. Subject to per-file and per-organization storage limits. # List available models Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/models/list-available-models /api-reference/openapi.json get /v1/models Returns a list of models available in the API. Use the `extended` query parameter to get additional metadata about each model's capabilities and supported features. # Retrieve a model Source: https://docs.perceptron.inc/perceptron-mk1.5/api-reference/models/retrieve-a-model /api-reference/openapi.json get /v1/models/{model_id} Returns a specific model by its ID. Use the `extended` query parameter to get additional metadata about the model's capabilities and supported features. # Prompting reference Source: https://docs.perceptron.inc/perceptron-mk1.5/best-practices/prompting-reference Choose clear instructions, reasoning controls, and output formats for Mk1.5 tasks. State what to inspect, which evidence matters, and how the answer will be used. Put reusable application instructions in a system message and the current question with its media in a user message. Keep text copied from documents or tool results distinct from your application's instructions. ## Task prompts Use these as starting points, then evaluate them on representative inputs. | Task | Example instruction | Output control | | ------------------ | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | Concise caption | “Describe the main objects and their arrangement in one sentence.” | Text | | Detailed caption | “Describe the visible objects, their relationships, and the setting. Separate observations from uncertain interpretations.” | Text, or `box` for grounded mentions | | OCR | “Transcribe all readable text in reading order. Preserve line breaks and mark unreadable spans as unknown.” | Text | | Table extraction | “Extract the table as CSV, preserving column order. Leave unreadable cells empty.” | Text; validate the table locally | | General detection | “Locate each visible object with a separate labeled bounding box.” | `box` | | Category detection | “Locate helmets, vests, and goggles. Use those labels for the matching objects.” | `box`, `point`, or `polygon` | | Visual Q\&A | “Which safety equipment is visible? Cite the regions supporting your answer.” | `box` | | Counting | “Count the visible cars, excluding buses. Place one point on each car you count.” | `point` | | One video event | “Identify the moment the ball passes through the hoop. Return a clip annotation in seconds.” | `clip` | | Repeated events | “Find every interval where the person opens the door. Return one clip annotation per occurrence.” | `clip` | | Temporal evidence | “Does the reference product appear in the video? Return a clip that supports the answer; say if no match is visible.” | `clip` | | Object tracking | “Track the red ball with timed boxes inside one track. Report only observed positions.” | `box`, plus a prompt requesting `` | | Transcription | “Transcribe the speech. Mark unclear words as \[unclear] instead of guessing.” | Text; supply a supported [audio input](/perceptron-mk1.5/capabilities/audio) | | Audio summary | “Summarize the stated decisions and unresolved questions in this recording.” | Text | | Audiovisual Q\&A | “Describe what is visible and what is audible, keeping the two kinds of evidence separate.” | Text; enable the video's soundtrack | For spatial and temporal output, set `vision_config.annotation_format` to the value in the table. These prompts request model-generated annotations; they do not guarantee exhaustive detection, correct counts, or continuous visibility. ## Set the API controls explicitly | Field | Values or shape | Purpose | | ------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | `reasoning_effort` | `none`, `minimal`, `low`, `medium`, `high` | Choose reasoning effort; evaluate quality and latency for your task. | | `vision_config.annotation_format` | `point`, `box`, `polygon`, `clip` | Request spatial geometry or temporal annotations. | | `vision_config.enable_audio_in_video` | Boolean; default `false` | Analyze soundtracks alongside frames for every video in the request. Standalone audio does not need this flag. | | `max_completion_tokens` | Integer within the model's limits | Bound output, including reasoning and the final answer. | | `response_format` | JSON Schema response format | Constrain a final answer to a data structure. | | `regex` | Pattern string at the top level | Constrain a short final answer to a pattern. | | `tools` | Function declarations | Describe functions that your application may execute. | `track` is a container in annotation markup, not an `annotation_format` value. Use `clip` for moments or intervals, and spatial annotations inside tracks for positions over time. Coordinates use the normalized 0–1000 grid; generated annotation timestamps use seconds. See [annotations](/perceptron-mk1.5/concepts/annotations). Use `reasoning_effort` for new requests and leave the older thinking override unset. Reasoning and grounding are separate choices: do not assume all spatial tasks require reasoning off, or that a higher effort guarantees a better answer. See [reasoning](/perceptron-mk1.5/capabilities/thinking) and [tokenization](/perceptron-mk1.5/guides/tokenization) for budgeting. ## Ground a specific question Set `PERCEPTRON_API_KEY`, then send the image, an observable task, and the requested annotation format together: ```bash theme={null} curl --fail-with-body --silent --show-error \ https://api.perceptron.inc/v1/chat/completions \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "model": "perceptron-mk1.5", "messages": [{ "role": "user", "content": [ {"type": "image_url", "image_url": {"url": "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/main/cookbook/_shared/assets/capabilities/detection/ppe_line.webp"}}, {"type": "text", "text": "Locate each visible helmet and safety vest. Label every box with the item type. If an item is not visible, do not invent a box."} ] }], "vision_config": {"annotation_format": "box"}, "reasoning_effort": "high", "max_completion_tokens": 2048 }' ``` Read the answer from `choices[0].message.content` and inspect `finish_reason` before parsing it. Native annotations remain markup within that text; the response does not turn them into a separate detections array. ## Provide examples and identify the target When an object is hard to describe, supply a reference image and, when available, a verified annotation showing the intended region. Keep category labels consistent between demonstrations and the query. Accurate examples are more useful than many uncertain ones; review a model-generated exemplar before reusing it as a reference. Label the role of each input explicitly: for example, “Asset 0 is the reference object; locate matching objects in asset 1.” Request `asset_idx` in annotations and preserve the media ordering when resending history. The attribute is optional, and the model may omit it despite the request; without an explicit or inherited selector, the annotation refers to the last asset passed to the model. A reference image plus a target is useful comparison context; an annotated demonstration additionally shows the desired geometry or answer format. See [multiple assets](/perceptron-mk1.5/guides/multiple-assets). For video, describe a visible event precisely and distinguish a single moment from a duration. Ask for separate clips for repeated events. If you provide a product reference followed by a video, request evidence in the video rather than an annotation on the reference image. Keep input `timestamp_ms` values distinct from generated timestamps in seconds. For denser object tracks, follow [High Fidelity Object Tracking](/perceptron-mk1.5/guides/high-fidelity-object-tracking) to combine model waypoints with optical flow and appearance tracking. ## Choose a final-answer format “Return JSON” is an instruction, not a response constraint. Use a JSON Schema `response_format` when your application requires known fields and types, then validate the completed answer locally. Include an unknown or empty value for information that may not be visible. For document transcription, ask explicitly for reading order, tables, headings, or Markdown rather than relying on an unstated layout convention. Keep native annotation markup and a constrained JSON report as distinct output contracts. Function arguments are another contract: function `strict` does not enforce their schema. A request cannot declare non-empty `tools` together with JSON Schema or regex constrained output. Finish the tool loop, then request a constrained report without tool declarations; completed tool history can remain. See [structured outputs](/perceptron-mk1.5/capabilities/structured-outputs). ## Give tools a clear job Describe what each function does, when it is useful, and the meanings of its arguments. Ask the model to use returned evidence and report missing information honestly. Your application validates names and arguments, checks permissions, executes the function, and returns the result; a prompt does not grant execution permission. Preserve call IDs, completed tool results, and assistant reasoning when continuing a tool conversation. Set limits on rounds, time, tokens, and tool calls, and check completion before using any answer. See [tool calling](/perceptron-mk1.5/guides/tool-calling), [building agents](/perceptron-mk1.5/guides/tool-agents), and [security](/perceptron-mk1.5/best-practices/security). # Security Source: https://docs.perceptron.inc/perceptron-mk1.5/best-practices/security Protect credentials, media, and application actions in a Mk1.5 integration. Keep authorization and data-access decisions in your application. Model output, retrieved text, and media can inform a decision, but should not determine which permissions the caller receives. ## Protect API keys * Store `PERCEPTRON_API_KEY` in a server-side secret manager or environment variable. Keep it out of browser bundles, mobile application packages, public notebooks, source control, and model messages. * Use separate keys for workloads that need independent revocation, and revoke keys that are unused or exposed. Separate keys in one organization still share organization-level request limits. * Redact authorization headers and secrets from logs, error reports, and screenshots. Send requests over HTTPS and keep certificate verification enabled. ## Treat supplied content as data Instructions can appear in an image, an OCR result, a web page, or a tool response. Keep that material separate from trusted system instructions and identify where it came from. For example, ask the model to summarize a retrieved document rather than treating the document's instructions as commands for your application. Prompt wording alone is not a permission boundary. Enforce access checks and tool restrictions in code even if the prompt says to ignore malicious instructions. Return only the passages or media needed for the task, and avoid putting credentials or unrelated private records in the conversation. Treat model output as untrusted when displaying it. Escape text or render only supported annotation elements; do not insert arbitrary generated HTML into a page or execute returned code. Validate coordinates, selectors, timestamps, and data schemas before using them. Reject invalid geometry rather than silently clamping it into a plausible observation. ## Authorize tool execution The model proposes function calls; your application decides whether and how to execute them. * Dispatch only explicitly registered function names. Parse arguments as JSON and validate types, allowed values, required fields, and size limits before dispatch. Function `strict` does not enforce this validation for you. * Check the requesting user's permission for each resource and action. Keep database credentials and service tokens in the tool implementation; do not expose them as model-visible arguments or results. * Use parameterized queries and APIs with structured arguments. Do not concatenate model-generated values into shell commands, SQL, or unrestricted filesystem paths. * If a tool fetches a URL, validate its scheme, destination and redirects, and enforce download-size and timeout limits. A model-selected URL should not grant access to private network services. * Apply the application's confirmation policy before consequential writes, and use operation IDs or idempotency controls where available. A retried model request must not automatically repeat an action that already succeeded. `tool_choice: "none"` is a best-effort generation control, not an execution guard. Stop dispatching calls when the application's permissions or budgets prohibit further work, regardless of what the model returns. See [tool calling](/perceptron-mk1.5/guides/tool-calling) for the request contract and [agent budgets](/perceptron-mk1.5/guides/tool-agents#bound-the-whole-workflow) for bounded execution. ## Control media access The [Files API](/perceptron-mk1.5/guides/files) checks the caller's organization when accessing uploaded files. A file ID is not a substitute for your application's per-user authorization: check that the user may access the media before including it in a request or returning its contents. Use file references or appropriately restricted media URLs for private inputs. A signed URL grants access to whoever holds it until its access conditions expire; keep it out of public examples and logs, limit its scope and lifetime, and allow enough time for the intended request to fetch it. Do not assume that a private bucket makes a signed URL safe to share publicly. Remove unnecessary personal or proprietary data before uploading. Apply access controls to your saved prompts, generated answers, crops, overlays, and batch results as well as to the original media. Protect exported files in your own storage, and use the [delete endpoint](/perceptron-mk1.5/api-reference/files/delete-a-file) when an uploaded file is no longer needed; manage copies in your application separately. # Audio understanding Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/audio Understand speech and sounds, choose audio inputs, and manage usage and limits. Perceptron Mk1.5 accepts audio in chat-completion requests and returns text. Use it to transcribe speech, summarize a recording, or answer questions about audible events. Audio can appear alongside text, images, and videos in the same conversation. The model does not generate audio. | Task | Example instruction | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | Audio understanding | “Describe the audible events in order, then summarize any speech.” | | [Audio Q\&A](/perceptron-mk1.5/capabilities/audio-qa) | “What reason does the speaker give for delaying the delivery?” | | [Audio transcription](/perceptron-mk1.5/capabilities/audio-transcription) | “Transcribe the speech. Mark unclear words as \[unclear] instead of guessing.” | | [Audio clipping](/perceptron-mk1.5/capabilities/audio-clipping) | “Locate the first audible alarm and return its start and end times.” | For a video's soundtrack, use [video with audio](/perceptron-mk1.5/capabilities/video-understanding#analyze-video-soundtracks). Standalone audio inputs do not require `enable_audio_in_video`. ## Understand a local recording Install `perceptron>=0.4.0`, set `PERCEPTRON_API_KEY` as shown in the [quickstart](/perceptron-mk1.5/index), and put a short WAV recording at `./recording.wav`. The `audio()` helper detects WAV, MP3, or FLAC from local file contents and encodes the recording inline. It does not upload the file to the Files API. ```python theme={null} import os from perceptron import Client, audio def print_audio_usage(usage): if usage is None: print("Usage was not included in this response.") return print(f"Input tokens: {usage.prompt_tokens}") print(f"Output tokens: {usage.completion_tokens}") if usage.audio_tokens is None: print("Audio token breakdown was not reported.") else: print(f"Audio tokens, included in input: {usage.audio_tokens}") client = Client( provider="perceptron", api_key=os.environ["PERCEPTRON_API_KEY"], ) audio_part = audio("recording.wav") messages = [{ "role": "user", "content": [ audio_part, {"type": "text", "text": ( "Describe the audible events in order, then summarize any speech. " "Distinguish what you can hear from guesses about its source, " "and say when a sound or word is unclear." )}, ], }] response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="none", max_completion_tokens=2048, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Expected a completed answer, got: {choice.finish_reason}") print(choice.message.content or "") print_audio_usage(response.usage) ``` Check `choices[0].finish_reason`: `stop` indicates a completed answer, while `length` means the output may be incomplete. Choose an output budget appropriate to the requested detail. For word-for-word speech text, use the [transcription guide](/perceptron-mk1.5/capabilities/audio-transcription). On the wire, the helper sends local audio as `input_audio`: `data` is **bare base64**, without a `data:` prefix, and `format` is `wav`, `mp3`, or `flac`. The API accepts these format names case-insensitively. Renaming a file does not convert its encoding. ## Choose an audio input The three input forms all belong in a **user message's content array**. They are not supported as audio content in system, assistant, or tool-result messages. | Content-part type | Use it for | | ----------------- | --------------------------------------------------------------------------------------- | | `input_audio` | A short local recording encoded as bare base64, as above. | | `audio_url` | An HTTP(S) URL accessible to the API, or a base64 data URL. | | `audio_file_id` | An audio file already uploaded through the [Files API](/perceptron-mk1.5/guides/files). | All three accept WAV, MP3, and FLAC. For uploads and data URLs, use `audio/wav`, `audio/mpeg`, and `audio/flac`, respectively. The Files API also accepts aliases such as `audio/x-wav` and `audio/x-flac`. For URL inputs, the actual container determines the format; an incorrect MIME label does not convert unsupported audio. ### Send a remote URL or data URL For a remotely hosted recording, replace `audio_part` in the first example with `audio("https://your-host.example/recording.wav")` before constructing `messages`. Use your actual HTTP(S) URL, accessible to the API; a path such as `/home/me/recording.wav` is a local file, not a remote URL. The SDK passes remote URLs through without downloading the recording itself. The helper also accepts an existing data URL. This fragment uses the same local recording: ```python theme={null} import base64 from pathlib import Path encoded = base64.b64encode(Path("recording.wav").read_bytes()).decode("ascii") audio_part = audio("data:audio/wav;base64," + encoded) ``` Both inline forms count toward the [20 MiB JSON request limit](/perceptron-mk1.5/guides/scaling). Use a remote URL or an uploaded file ID for larger recordings. ### Reference an uploaded audio file Upload the recording through [Files](/perceptron-mk1.5/guides/files), then pass the returned file object to `audio()`. Replace the `audio_part` assignment in the first example with: ```python theme={null} uploaded = client.files.upload("recording.wav") audio_part = audio(uploaded) ``` For a file uploaded earlier, use `audio(file_id="file-...")` with its actual ID. The helper emits an `audio_file_id` content part. The file must belong to the organization making the request. Uploading once saves repeated transfer of the file bytes; the recording still contributes input tokens each time it is analyzed. See the [Files guide](/perceptron-mk1.5/guides/files) for upload limits and deletion. ## Stream the text response Streaming delivers generated text incrementally; it does not turn this endpoint into a live microphone stream. Supply the recording with the request as above. Run this after defining `client`, `messages`, and `print_audio_usage` in the Python example. The code consumes the whole stream, including a possible final usage chunk with an empty `choices` array: ```python theme={null} with client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="none", max_completion_tokens=1024, stream=True, stream_options={"include_usage": True}, ) as stream: for chunk in stream: for choice in chunk.choices: if choice.delta is not None: if choice.delta.tool_calls: raise RuntimeError("Expected a text answer, but received a tool call") print(choice.delta.content or "", end="", flush=True) response = stream.get_final_completion() if not response.complete or response.finish_reason != "stop" or response.tool_calls: raise RuntimeError(f"Incomplete answer: {response.finish_reason}") print() print_audio_usage(response.usage) ``` The SDK assembles the final completion, including usage from a chunk with no choices. It raises on API errors or a stream that ends before its completion marker. If iteration raises or the final completion is unsuccessful, treat displayed text as incomplete. ## Use the command line The SDK installation includes the `perceptron` command. With `PERCEPTRON_API_KEY` set, ask about the same local recording: ```bash theme={null} perceptron question recording.wav \ "Describe the audible events in order, then summarize any speech." \ --provider perceptron --model perceptron-mk1.5 \ --reasoning-effort none --format json ``` For a local video such as `recording.mp4`, explicitly enable its soundtrack: ```bash theme={null} perceptron question recording.mp4 \ "What happens in this video, and what speech or sounds accompany it?" \ --provider perceptron --model perceptron-mk1.5 \ --reasoning-effort high --audio-in-video --format json ``` `--audio-in-video` enables soundtrack processing, including encoded silence; `--no-audio-in-video` explicitly disables it. Omitting both leaves the server default of disabled. These flags do not affect standalone audio inputs. JSON output includes `finish_reason` and reported usage; check completion before relying on the answer. Use the Python API when you need to set an explicit output budget. ## Understand usage and limits `usage.prompt_tokens_details.audio_tokens` reports the audio portion of `prompt_tokens`. It includes standalone recordings and video soundtracks when enabled. **Do not add it to `prompt_tokens` again.** For chat completions, an explicit `0` reports no audio tokens; a missing or null breakdown means that count was not reported. Multilook also uses zero when the upstream breakdown is absent, so its zero does not prove audio was skipped. When several media inputs are present, this aggregate does not identify which individual soundtracks contributed audio. The current **Perceptron Mk1.5** deployment accepts at most **16,384 audio tokens per item**. Audio uses approximately **750 tokens per minute**, making that roughly **21.8 minutes per recording**. Treat this as an estimate: the enforced token count is authoritative, and the limit is model-specific. Video soundtracks use the same per-item audio budget. The per-item limit is separate from the full [context and output limits](/perceptron-mk1.5/models/perceptron-mk1.5#specifications). All recordings, visual inputs, text, history, and requested output must fit the context together. Base64 request size and Files upload limits are separate constraints too. Each standalone audio content part consumes one of the request's 256 media units; follow [asset ordering](/perceptron-mk1.5/guides/multiple-assets) when combining modalities. An over-limit item returns HTTP 400 with `error.code: "audio_token_limit_exceeded"`. Audio is rejected rather than truncated; shorten or split it. The error message reports the active limit, and `error.param` can be null. This rejection occurs before streaming begins, so a streaming request can receive a JSON error instead of an event stream. See [Error messages](/perceptron-mk1.5/guides/error-messages) and [Tokenization](/perceptron-mk1.5/guides/tokenization). ## Ask several questions about the same recording [Multilook](/perceptron-mk1.5/guides/multilook) accepts the same audio parts in shared user context or structured prompts. Use it for independent questions about one recording, such as a summary and a list of stated decisions. Audio usage is reported in the call-level `usage.prompt_tokens_details.audio_tokens`; per-prompt usage contains completion tokens only. Inspect every result: an audio-limit failure can appear in `results[].error` while the Multilook HTTP response is 200. Request-level validation errors still fail the request. For an interactive follow-up conversation or a tool workflow, use ordinary chat completions. # Audio clipping Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/audio-clipping Locate audible events and speech as moments or intervals in a recording. Audio clipping identifies **when** something can be heard. Ask for the onset of a sound, the duration of an audible event, or the interval containing a spoken phrase. The response contains `` annotations in text. Use their timestamps for playback or editing in your application; the API does not return a cut audio file. ## Locate an audible event Install `perceptron>=0.4.0`, set `PERCEPTRON_API_KEY`, and place a short WAV recording at `./recording.wav`. This example asks for the first alarm in your recording. If there is no clear alarm, the prompt asks the model to report that instead of inventing an interval. ```python theme={null} import os from perceptron import Client, audio client = Client( provider="perceptron", api_key=os.environ["PERCEPTRON_API_KEY"], ) audio_part = audio("recording.wav") def locate_event(question): response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ audio_part, {"type": "text", "text": ( question + " Use clip annotations with asset_idx=0 and " "timestamps in explicit seconds from the start of this recording. " "If the event is absent or unclear, say so without inventing timestamps." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, vision_config={"annotation_format": "clip"}, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Incomplete audio clipping answer: {choice.finish_reason}") return choice.message.content or "" print(locate_event( "Find the first audible alarm. Return an interval from when the alarm " "begins until that occurrence stops." )) ``` `vision_config.annotation_format: "clip"` requests temporal annotations for audio as well as video. Standalone audio does not require `enable_audio_in_video`. For MP3, FLAC, remote URLs, or uploaded files, use the corresponding [audio input form](/perceptron-mk1.5/capabilities/audio#choose-an-audio-input). ## Ask for a moment or an interval Reuse `locate_event` to ask for a single boundary instead of a duration: ```python theme={null} print(locate_event( "Find the moment the first alarm begins. Return one timestamp for its onset." )) ``` These annotations illustrate the two formats; they are not measured predictions for your recording: ```html theme={null} ``` A single timestamp is a moment. Two timestamps give the start and end of an interval. For repeated events, ask for a separate interval for each occurrence. A collection can group those intervals under a shared description and asset selector; see [annotation format](/perceptron-mk1.5/concepts/annotations#clips). Define the boundary you need in the prompt: | Task | Example instruction | | -------------------- | ------------------------------------------------------------------------------------------------------- | | Find a spoken phrase | “Locate the interval containing the words ‘the meeting is postponed.’ If they are not audible, say so.” | | Find repeated sounds | “Return a separate interval for each audible burst of applause.” | | Mark a transition | “Return the moment when the background music stops.” | Use [audio transcription](/perceptron-mk1.5/capabilities/audio-transcription) when you need the spoken words, or [audio Q\&A](/perceptron-mk1.5/capabilities/audio-qa) when you need an explanation of their meaning. ## Keep timestamps attached to the recording Times are measured in seconds from the start of the supplied recording. If you create an excerpt starting 120 seconds into a longer recording, a returned time of 2.4 seconds maps to 122.4 seconds in the original. Keep that offset with the excerpt when combining results. `asset_idx="0"` selects the recording in this example. The attribute is optional, even when requested: without an explicit or inherited selector, use the last asset available when the annotation was produced. With several recordings or other media, resolve the [asset selector](/perceptron-mk1.5/guides/multiple-assets#resolve-selectors-before-drawing) before interpreting times. ## Validate before seeking or cutting * Require a completed response. A `length` finish reason or interrupted stream can leave an unfinished annotation. * Check that times are finite, nonnegative, within the selected recording, and ordered for an interval. A moment needs an application-chosen interval before it can become a playable excerpt. * Listen around the proposed boundary before making a precise cut. Requested tags and timestamps are model output to validate, not a guarantee of sample-accurate alignment. Long recordings share the model's input and output budget. See [audio limits](/perceptron-mk1.5/capabilities/audio#understand-usage-and-limits) before sending a recording. For audible events in a video, use [video clipping with sound](/perceptron-mk1.5/capabilities/video-clipping#locate-an-audible-cue) and explicitly enable soundtrack processing. # Audio Q&A Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/audio-qa Ask focused questions about speech and audible events in a recording. Ask a question about a recording, then request the audible evidence that supports the answer. Keep what a speaker says separate from conclusions the recording cannot establish. For example, a speaker's proposed delivery date is not proof that the delivery happened. ## Ask about a spoken decision Install `perceptron>=0.4.0`, set `PERCEPTRON_API_KEY`, and put a short WAV recording of a delivery discussion at `./delivery-discussion.wav`. Use your own recording and adapt the question to its subject. This example asks for the stated reason for a delay and distinguishes a confirmed date from a proposal: ```python theme={null} import os from perceptron import Client, audio client = Client( provider="perceptron", api_key=os.environ["PERCEPTRON_API_KEY"], ) messages = [{ "role": "user", "content": [ audio("delivery-discussion.wav"), {"type": "text", "text": ( "What reason is stated for the delivery delay? What new date, if any, " "do the speakers agree on? Distinguish an agreed date from a suggestion. " "Answer using only the recording. If the reason or date is not stated " "or cannot be heard clearly, say so." )}, ], }] response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="none", max_completion_tokens=1024, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Expected a completed audio answer, got: {choice.finish_reason}") answer = choice.message.content or "" print(answer) ``` The response is text. The `audio()` helper detects WAV, MP3, or FLAC from local file contents and sends the recording inline. See [audio input forms](/perceptron-mk1.5/capabilities/audio#choose-an-audio-input) for MP3, FLAC, URLs, and uploaded file IDs. ## Ask a follow-up with the recording in history Run this after the first example to ask for the words supporting its answer: ```python theme={null} messages.extend([ choice.message.to_dict(), {"role": "user", "content": ( "Recheck the recording and quote the short phrases that support your " "answer about the reason and date. Mark unclear words as [unclear]. " "If your earlier answer is not supported, correct it." )}, ]) follow_up = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="none", max_completion_tokens=1024, ) choice = follow_up.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Expected a completed follow-up, got: {choice.finish_reason}") print(choice.message.content or "") ``` The full `messages` list sends the original audio and previous answer again. The API does not retain this conversation automatically. Text-only follow-ups do not add media assets; this recording remains asset `0` if a later answer uses an annotation. See [asset ordering](/perceptron-mk1.5/guides/multiple-assets) when combining recordings with other media. ## Choose the question and evidence * Ask one specific question, such as what decision was stated or which sound occurred before another, and allow an answer of “not audible” or “not stated.” * Request a short supporting quotation for speech. For non-speech events, ask for a description of the audible evidence rather than an unsupported explanation of its cause. * Use [audio clipping](/perceptron-mk1.5/capabilities/audio-clipping) when the answer needs an interval to review, or [transcription](/perceptron-mk1.5/capabilities/audio-transcription) when you need the full spoken text. * Use [Multilook](/perceptron-mk1.5/guides/multilook) for independent questions over the same recording. Keep dependent follow-ups in a conversation that includes the earlier answer. Check the [audio and context limits](/perceptron-mk1.5/capabilities/audio#understand-usage-and-limits) when carrying recordings through several turns. Each request includes the audio supplied in its history. # Audio transcription Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/audio-transcription Request spoken text with explicit instructions for language, unclear words, and speaker labels. Request a transcript when you need the spoken words rather than a summary. Specify whether to preserve the original language, include repetitions, and mark words that cannot be heard clearly. Review the result against the recording before relying on exact wording. ## Transcribe a local recording Install `perceptron>=0.4.0`, set `PERCEPTRON_API_KEY`, and place a short WAV recording at `./recording.wav`. This example asks for a verbatim transcript in the original language: ```python theme={null} import os from perceptron import Client, audio client = Client( provider="perceptron", api_key=os.environ["PERCEPTRON_API_KEY"], ) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ audio("recording.wav"), {"type": "text", "text": ( "Transcribe the speech verbatim in its original language. " "Preserve spoken repetitions and do not summarize or translate. " "Mark words you cannot hear clearly as [unclear] instead of guessing. " "If there is no intelligible speech, say so. Return only the transcript." )}, ], }], reasoning_effort="none", max_completion_tokens=4096, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Expected a completed transcript, got: {choice.finish_reason}") print(choice.message.content or "") ``` The transcript is returned in `choices[0].message.content`. A `length` finish reason means the output budget was exhausted; treat that transcript as incomplete. A `stop` finish reason indicates completion, not verified transcription accuracy. The `audio()` helper detects the local recording's format and sends it inline. For other supported encodings, remote recordings, or uploaded files, use the [audio input forms](/perceptron-mk1.5/capabilities/audio#choose-an-audio-input). ## Specify the transcript you need | Goal | Example instruction | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Preserve spoken wording | “Transcribe verbatim, including repetitions. Mark unclear words as \[unclear].” | | Produce readable text | “Transcribe the speech with punctuation. Remove filler words without changing the meaning.” | | Separate transcript and translation | “First transcribe in the original language, then provide an English translation under a separate heading.” | | Label speaker changes | “Use Speaker 1, Speaker 2, and so on when voices can be distinguished. Mark uncertain speaker changes rather than guessing.” | These are prompt instructions, not guarantees of word accuracy or speaker separation. Speaker labels describe turns within the recording; they do not establish a person's identity or a stable identity across recordings. Check labels and quotations against the audio when they matter to your application. ## Budget for the full transcript A recording can fit the audio input limit while its transcript exceeds the requested output budget. Allow enough completion tokens for the spoken text, within the model's [context and output limits](/perceptron-mk1.5/models/perceptron-mk1.5#specifications). The example uses `reasoning_effort="none"` and a 4,096-token output budget; adjust that budget for your recording. For a longer recording, split the audio into shorter excerpts before sending requests and retain their start times in your application. If you overlap excerpts to preserve context around a boundary, check for duplicated transcript text when joining results. Audio over the [per-item limit](/perceptron-mk1.5/capabilities/audio#understand-usage-and-limits) is rejected rather than automatically truncated. Use [audio clipping](/perceptron-mk1.5/capabilities/audio-clipping) to ask for an interval containing a phrase or audible event. A clip is a temporal annotation; it does not provide automatic word-by-word alignment for a transcript. Use [audio Q\&A](/perceptron-mk1.5/capabilities/audio-qa) when you only need a specific answer from the recording. # Image captioning Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/image-captioning Generate concise descriptions, detailed captions, and captions with spatial grounding. Use Perceptron Mk1.5 to describe an image for accessibility text, catalog metadata, search, or review. Specify the audience and desired detail in the prompt. A concise caption identifies the main subject; a detailed caption can describe its surroundings and relationships. ## Choose the caption style Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY`. This example sends the same public street image in two independent requests, one for each style. ```python theme={null} import os from perceptron import Client, image client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) image_url = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/caption/suburban_street.webp" ) def caption(prompt, grounded=False): response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image(image_url), {"type": "text", "text": prompt}, ], }], reasoning_effort="high", max_completion_tokens=2048, vision_config={"annotation_format": "box"} if grounded else {}, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Incomplete caption: {choice.finish_reason}") return choice.message.content or "" prompts = { "concise": ( "Write one sentence of alt text identifying the main visible subjects and " "setting. Include only details supported by the image." ), "detailed": ( "Describe this image in one detailed paragraph. Cover the main subjects, " "their positions, the foreground and background, and visible relationships. " "Do not invent events, identities, or details outside the image." ), } for style, prompt in prompts.items(): print(f"{style}: {caption(prompt)}") ``` The output wording will vary. Adjust the instruction rather than treating a style name as an API parameter: `concise` and `detailed` are labels in this example application. ## Ground the caption in regions Request boxes for the subjects mentioned in the description. Run this after the setup above: ```python theme={null} from pathlib import Path grounded_caption = caption( "Describe the main visible subjects and their surroundings. Interleave the " "description with flat point_box annotations for the subjects you mention, " "without collections or tracks. Give each box a descriptive mention and " "asset_idx=0. Use normalized coordinates.", grounded=True, ) print(grounded_caption) Path("response.txt").write_text(grounded_caption, encoding="utf-8") ``` An illustrative grounded phrase looks like: ```html theme={null} The scene includes (100,400) (350,700) beside the road. ``` This is a format illustration, not measured output for the sample image. A `mention` describes the cited region. Boxes use two normalized 0–1000 coordinates: top-left, then bottom-right. Convert using the dimensions of the asset selected by `asset_idx`; do not treat the numbers as source pixels. Use the shared [rendering guide](/perceptron-mk1.5/guides/rendering-annotations) to turn `response.txt` into an overlay. The [annotation reference](/perceptron-mk1.5/concepts/annotations) also covers points, polygons, and collections. For captions comparing several images, request explicit selectors and follow [multiple-asset ordering](/perceptron-mk1.5/guides/multiple-assets). ## Write captions for their destination | Destination | Prompt guidance | | ---------------- | ----------------------------------------------------------------------------------------- | | Alt text | State the important visual information concisely and avoid repeating nearby page text. | | Product metadata | Ask for observable color, shape, material cues, and markings; allow unknown values. | | Visual search | Describe distinguishing objects, actions, and relationships using consistent terminology. | | Human review | Include spatial citations so a reviewer can inspect the evidence behind a description. | For metadata that must be parsed, define a JSON Schema with [structured outputs](/perceptron-mk1.5/capabilities/structured-outputs). Asking for JSON in a caption prompt alone does not enforce a schema. Use [image Q\&A](/perceptron-mk1.5/capabilities/image-qa) when you need an answer to a specific question rather than an overall description. # Image Q&A Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/image-qa Ask questions about an image and ground answers in visible regions. Ask Perceptron Mk1.5 about objects, relationships, activities, or details in an image. Use a specific question for an inspection checklist or product audit, and request spatial annotations when the answer needs visible evidence. ## Ask a grounded question Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY`. This example uses a public photo of a coastal bay and asks the model to cite relevant objects with bounding boxes. ```python theme={null} import os from pathlib import Path from perceptron import Client, image client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) image_url = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp" ) messages = [{ "role": "user", "content": [ image(image_url), {"type": "text", "text": ( "What stands out in this scene? Describe the visible objects and their " "relationships. Cite the objects supporting your answer with flat point_box " "annotations (no collections or tracks), each with a mention and asset_idx=0. Distinguish what is " "visible from any interpretation, and say when a detail is unclear." )}, ], }] response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="high", max_completion_tokens=2048, vision_config={"annotation_format": "box"}, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Incomplete image answer: {choice.finish_reason}") answer = choice.message.content or "" print(answer) Path("response.txt").write_text(answer, encoding="utf-8") ``` The answer is text in `message.content`; any boxes appear as markup within that text. For example, an annotation has this shape: ```html theme={null} (100,150) (300,450) ``` These coordinates illustrate the format, not a prediction for the sample image. They use the normalized 0–1000 grid, with top-left followed by bottom-right. `asset_idx="0"` selects the image supplied in this request. See [annotation format](/perceptron-mk1.5/concepts/annotations) for validation and [rendering annotations](/perceptron-mk1.5/guides/rendering-annotations) to draw the saved answer on the image. For an answer without spatial markup, omit `vision_config` and ask for ordinary prose. To identify a small target by its center, request `annotation_format: "point"`; use `"polygon"` when a boundary is more useful than a rectangle. ## Ask a follow-up question Continue the conversation by retaining the image and previous assistant message. Run this after the example above: ```python theme={null} messages.append(choice.message.to_dict()) messages.append({ "role": "user", "content": ( "Which of those observations is most important to your interpretation? " "Explain briefly and cite its visible evidence with a flat point_box on " "asset_idx=0, without a collection or track wrapper." ), }) follow_up = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="high", max_completion_tokens=1024, vision_config={"annotation_format": "box"}, ) follow_up_choice = follow_up.choices[0] if follow_up_choice.finish_reason != "stop" or follow_up_choice.message.tool_calls: raise RuntimeError("The follow-up answer did not complete") print(follow_up_choice.message.content or "") ``` The image remains asset `0` because the follow-up introduces no new media. When adding reference images, use [multiple assets](/perceptron-mk1.5/guides/multiple-assets) to keep the answer attached to the correct image. Use [Multilook](/perceptron-mk1.5/guides/multilook) for independent questions that can share the same image without depending on each other's answers. ## Make the question useful * Ask for observable evidence: “Which objects block the doorway?” is more specific than “Is this scene okay?” * State the decision criteria and allow an uncertain answer when the needed detail is obscured or too small. * Ask for the geometry your application needs, then validate it before displaying it. A box helps locate evidence; it does not establish that the interpretation is correct. * Use [structured outputs](/perceptron-mk1.5/capabilities/structured-outputs) when the answer must follow a schema, [OCR](/perceptron-mk1.5/capabilities/ocr) for text extraction, and [object detection](/perceptron-mk1.5/capabilities/object-detection) for category-based localization or counts. # Image understanding Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/image-understanding Ask visual questions, extract text, and ground objects in images. Send an image with a natural-language instruction to describe a scene, answer a question, or read visible text. Be specific about the region and the output you need. For OCR, ask the model to preserve reading order and mark unreadable text instead of guessing. The [quickstart](/perceptron-mk1.5/index) shows a complete image question. You can use the same request with prompts such as: | Task | Example instruction | | ----------- | ---------------------------------------------------------------------------------------- | | Captioning | “Describe the scene in two sentences, including the main objects and their arrangement.” | | Visual Q\&A | “Which objects are on the desk? Use only details visible in the image.” | | OCR | “Transcribe the visible labels from top to bottom. Mark unreadable text as unknown.” | | Comparison | “Compare the objects in asset 0 and asset 1. Describe the visible differences.” | For comparisons and demonstrations with reference images, see [multiple assets](/perceptron-mk1.5/guides/multiple-assets). For machine-readable extracted fields, use [structured outputs](/perceptron-mk1.5/capabilities/structured-outputs). ## Explore image tasks | Task | Guide | | --------------------------------------------------- | ------------------------------------------------------------------------------- | | Answer questions with visible evidence | [Image Q\&A](/perceptron-mk1.5/capabilities/image-qa) | | Write concise, detailed, or grounded descriptions | [Image captioning](/perceptron-mk1.5/capabilities/image-captioning) | | Locate and count selected object categories | [Object detection](/perceptron-mk1.5/capabilities/object-detection) | | Transcribe text, extract fields, or preserve tables | [OCR](/perceptron-mk1.5/capabilities/ocr) | | Supply labeled visual demonstrations | [In-context learning](/perceptron-mk1.5/capabilities/in-context-learning-image) | The shared [Detect API](/capabilities/detect) provides a separate detection endpoint with pixel-coordinate results. The task guides above use Mk1.5 chat completions. ## Locate objects Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY`. Choose `vision_config.annotation_format` to request points, boxes, or polygons. This example requests boxes around people: ```python theme={null} import os from perceptron import Client, image client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/detection/ppe_line.webp" ), {"type": "text", "text": "Locate each visible person with a bounding box."}, ], }], max_completion_tokens=2048, vision_config={"annotation_format": "box"}, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete grounding answer: {choice.finish_reason}") print(choice.message.content or "") ``` The answer is markup in `message.content`, not a separate JSON array of detections. A box uses `` with top-left and bottom-right coordinates on a normalized 0–1000 grid. See the [annotation reference](/perceptron-mk1.5/concepts/annotations) before rendering the response. ## Inspect details with tools If a task needs another image, a crop, or information from an external system, your application can provide a function for that operation. The model requests the function; your code validates and executes it, then returns the result. See [building tool agents](/perceptron-mk1.5/guides/tool-agents) for returning image results and mapping crop coordinates back to the original image. # In-context learning (image) Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/in-context-learning-image Use labeled visual examples to guide detection in a new image. In-context learning gives Perceptron Mk1.5 examples of the visual concept you want to find. Supply a reference image, a label, and a box around the reference object, then ask for matching objects in a target image. The examples guide that request; they do not train or permanently update the model. Use one reference for a specific object or several labeled references to distinguish classes. This is useful when a visual example communicates the appearance more clearly than a description alone. ## Detect from labeled examples This example uses reviewed boxes around a cat and a dog in the sample reference images. It runs once with only the dog reference, then with both classes. The target image shows a dog; providing a cat reference does not mean a cat must appear in the answer. Install the `perceptron>=0.4.0` Python package and set `PERCEPTRON_API_KEY` before running: ```python theme={null} import os from html import escape from perceptron import Client, image client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) asset_base = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/in-context-learning/multi" ) cat_reference = { "url": f"{asset_base}/classA.jpg", "label": "cat", "box": (316, 136, 703, 906), } dog_reference = { "url": f"{asset_base}/classB.webp", "label": "dog", "box": (161, 48, 666, 980), } target_url = f"{asset_base}/cat_dog_input.png" def detect_from_examples(examples, target_url): content = [{"type": "text", "text": ( "The following images and box annotations are labeled references. " "Use them to recognize the same classes in the final target image." )}] for asset_idx, example in enumerate(examples): x1, y1, x2, y2 = example["box"] label = escape(example["label"], quote=True) content.extend([ image(example["url"]), {"type": "text", "text": ( f'Reference: ' f"({x1},{y1}) ({x2},{y2}) " )}, ]) target_idx = len(examples) content.extend([ image(target_url), {"type": "text", "text": ( f"This is the target image, asset_idx={target_idx}. " "Find all visible instances of the reference classes here. " "Return flat point_box annotations, without collections, using the reference labels and " f"asset_idx={target_idx}, with normalized 0–1000 coordinates. " "Do not annotate the reference images. Do not assume every class " "is present; say when no matching object is visible." )}, ]) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{"role": "user", "content": content}], reasoning_effort="high", max_completion_tokens=2048, vision_config={"annotation_format": "box"}, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete detection answer: {choice.finish_reason}") return choice.message.content or "" print("One reference:") print(detect_from_examples([dog_reference], target_url)) print("Multiple classes:") print(detect_from_examples([cat_reference, dog_reference], target_url)) ``` In the first call, the dog reference is asset `0` and the target is asset `1`. In the second, the cat is asset `0`, the dog is asset `1`, and the target is asset `2`. Each call has its own supplied conversation, so its numbering starts again at zero. See [multiple assets](/perceptron-mk1.5/guides/multiple-assets) before adapting this to a conversation with earlier media. The answer is annotation markup in `message.content`. Check that each returned box refers to the target asset before drawing it. The [annotation reference](/perceptron-mk1.5/concepts/annotations) explains coordinate conversion, collections, and validation. To draw the boxes, save one call's returned string as `response.txt`, download the target image, and follow [rendering annotations](/perceptron-mk1.5/guides/rendering-annotations). Use `--asset-idx 1 --last-asset-idx 1` for the single-reference answer or `--asset-idx 2 --last-asset-idx 2` for the multiclass answer. Keep the two answers separate because their indices differ. When `asset_idx` is omitted and no selector is inherited, it defaults to the last asset available when that answer was produced, which is the target image in both examples. ## Prepare your own references Replace each reference URL, label, and box together. The sample coordinates belong only to the sample images. Use the normalized 0–1000 grid, with the top-left and bottom-right corners enclosing the object you intend to teach. If you obtain candidate boxes from a previous model response, inspect and correct them before using them as examples. * Choose references with a clear view of the object, including the features that distinguish it from similar objects. * Use consistent class labels across examples and the target request. Multiple views of one class should share a label. * Include confusing alternatives as separately labeled references when distinguishing them matters. * Keep the target image separate from the examples, and request output only for its `asset_idx`. For the same reference-to-query pattern across time, see [in-context learning for video](/perceptron-mk1.5/capabilities/in-context-learning-video). For ordinary detection without examples, see [object detection](/perceptron-mk1.5/capabilities/object-detection). # In-context learning (video) Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/in-context-learning-video Show a reference image and find matching visual evidence in a video. Use a reference image to show Perceptron Mk1.5 what to look for in a video. The request combines the example, an explanation of which features matter, and a query video. This can help with product matching, finding a familiar object, or reviewing footage for a visually defined condition. The example guides the current request. Keep it in the supplied conversation when asking follow-up questions that depend on it. ## Find a reference product on a shelf This example supplies an image of a cereal package, then asks whether a matching package is visible in a shelf video. It asks for temporal evidence and permits an uncertain answer when packaging details cannot be read clearly. Visibility in this recording does not establish a store's current inventory. Install the `perceptron>=0.4.0` Python package and set `PERCEPTRON_API_KEY` before running: ```python theme={null} import os from perceptron import Client, image, video client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) asset_base = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/in-context-learning-video" ) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image(f"{asset_base}/mini_wheats.jpeg"), {"type": "text", "text": ( "Asset 0 is the reference product. Look for a matching cereal " "package using its visible branding, product name, and package " "design. Similar colors alone do not establish a match." )}, video(f"{asset_base}/cereal_short.mp4"), {"type": "text", "text": ( "Asset 1 is the shelf video to inspect. Is a package matching " "the reference visible? Explain the visible evidence and cite " "the matching interval or intervals with clip annotations, " "asset_idx=1, and timestamps in explicit seconds. " "Only cite intervals in the video, not the reference image. " "If no match is visible, say that. If resolution or occlusion " "prevents a confident comparison, explain the uncertainty " "instead of claiming the product is absent." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, vision_config={"annotation_format": "clip"}, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete comparison answer: {choice.finish_reason}") print(choice.message.content or "") ``` The reference image is asset `0`; the query video is asset `1`. Temporal annotations must therefore select asset `1`. This is an illustrative matching interval, not measured output for the sample video: ```html theme={null} ``` Validate the selected asset before using a timestamp in a player. If you add more references or retain earlier media in the conversation, update the selectors according to [multiple-asset ordering](/perceptron-mk1.5/guides/multiple-assets). ## Make the comparison precise * State whether you want the same class, the same product design, or the same individual object. Those are different matching tasks. * Use a reference where the distinguishing features are visible. If it contains several objects, identify the intended one with a reviewed box, as in [image in-context learning](/perceptron-mk1.5/capabilities/in-context-learning-image). * For several reference concepts, label each one and request separate evidence for each. Do not assume every reference has a match in the query video. * Treat a missed or obscured match as uncertainty about the supplied footage. Selected frames do not show every instant of the source video. ## Choose the output you need Request [clip annotations](/perceptron-mk1.5/capabilities/video-clipping) when the result should seek to supporting evidence. Request [video tracks](/perceptron-mk1.5/capabilities/video-tracking) when you need the matching object's position over time; the track belongs to the video asset, not the reference image. If a track has an uncertain interval, [request new model observations around the gap](/perceptron-mk1.5/guides/high-fidelity-object-tracking#recover-a-gap-with-new-model-observations) before continuing local tracking. Keep crop coordinates and source timestamps associated with their own assets when combining the results. # Object detection Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/object-detection Locate named object categories, count visible instances, and render grounded detections. Ask Perceptron Mk1.5 to locate objects through chat completions. Name the categories and request the geometry your application needs: boxes for extents, points for centers, or polygons for boundaries. ## Detect helmets and vests Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY`. This example uses the public PPE image and asks for one box per visible helmet or safety vest. ```python theme={null} import os from pathlib import Path from perceptron import Client, image client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) image_url = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/detection/ppe_line.webp" ) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image(image_url), {"type": "text", "text": ( "Detect visible helmets and safety vests. Return one flat point_box " "annotation per physical item, using mention='helmet' or mention='vest' " "and asset_idx=0. Use normalized coordinates. Do not duplicate an " "item or include objects outside these categories. If none are visible, " "say so without inventing a box. Do not wrap boxes in collections or tracks." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, vision_config={"annotation_format": "box"}, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Incomplete detections: {choice.finish_reason}") annotations = choice.message.content or "" print(annotations) Path("response.txt").write_text(annotations, encoding="utf-8") ``` Detections are annotation markup in `message.content`, not a separate JSON `detections` field. For example: ```html theme={null} (420,100) (570,240) (350,280) (650,760) ``` These values illustrate the format and are not measured detections from this image. Coordinates are normalized to 0–1000, with top-left followed by bottom-right. `asset_idx="0"` identifies the input image. Validate the label, selector, coordinate range, and box ordering before using a detection. Follow [rendering annotations](/perceptron-mk1.5/guides/rendering-annotations) to draw these boxes. For a center-point workflow, change the annotation format to `"point"` and ask for each object's center; use `"polygon"` when an outline better represents the target. ## Return counts as structured data For applications that need numbers, request a constrained count response. Install `jsonschema`, then run this after the client and image setup above: ```python theme={null} import json from jsonschema import validate count_schema = { "type": "object", "properties": { "helmet": {"type": "integer", "minimum": 0}, "vest": {"type": "integer", "minimum": 0}, }, "required": ["helmet", "vest"], "additionalProperties": False, } response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image(image_url), {"type": "text", "text": ( "Count visible helmets and safety vests, counting each physical item " "once. Count the items, not the people. Return zero for a category " "with no visible instances. Return the requested JSON counts." )}, ], }], reasoning_effort="high", max_completion_tokens=1024, response_format={ "type": "json_schema", "json_schema": {"name": "ppe_counts", "strict": True, "schema": count_schema}, }, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError("The count response did not complete") counts = json.loads(choice.message.content or "") validate(instance=counts, schema=count_schema) print(counts) ``` This is a separate request, so its counts can differ from the first request's detections. If totals must match the displayed overlay exactly, count the validated annotations by category in your application. A valid schema ensures the expected structure; it does not establish that every item was detected. Occlusion and small objects can affect counts. ## Define the target precisely Use clear category descriptions and explain borderline cases: “high-visibility safety vest” is more specific than “vest.” For subtle targets, supply a labeled reference image through [in-context image learning](/perceptron-mk1.5/capabilities/in-context-learning-image). With several images, request `asset_idx` on every annotation and select the corresponding dimensions when rendering. For objects moving through a video, use [video tracking](/perceptron-mk1.5/capabilities/video-tracking). Separate detections from independent images do not create persistent object identities. ## Choose the endpoint The examples above select `perceptron-mk1.5` on `/v1/chat/completions` and return normalized annotation markup. The separate [Detect API](/capabilities/detect) provides a managed detection workflow with its own supported model and target-image pixel coordinates. Its request and response formats differ from the chat-completion examples on this page. # OCR Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/ocr Extract product names, prices, and document text as structured data or grounded spans. Perceptron Mk1.5 can read text in images and extract the fields relevant to your application. Ask for a transcription, a product-and-price table, or a constrained JSON record. Request spatial annotations when you need to show where a text span came from. ## Extract product names and prices Install `perceptron>=0.4.0` and `jsonschema`, and set `PERCEPTRON_API_KEY`. This example uses the public grocery-label image. It preserves printed prices as strings so currency symbols and decimal formatting are not lost, and allows `null` when a requested field is unreadable. ```python theme={null} import json import os from jsonschema import validate from perceptron import Client, image client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) image_url = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/ocr/grocery_labels.webp" ) schema = { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "product": {"type": ["string", "null"]}, "price": {"type": ["string", "null"]}, }, "required": ["product", "price"], "additionalProperties": False, }, }, }, "required": ["items"], "additionalProperties": False, } response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image(image_url), {"type": "text", "text": ( "Extract the visible product names and their associated listed prices " "in reading order. Preserve spelling, currency symbols, and decimal " "separators exactly as printed. Use null for a field you cannot read; " "do not guess or pair a product with an unrelated price. Return an " "empty items array if there are no relevant labels." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, response_format={ "type": "json_schema", "json_schema": {"name": "product_prices", "strict": True, "schema": schema}, }, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Incomplete OCR response: {choice.finish_reason}") result = json.loads(choice.message.content or "") validate(instance=result, schema=schema) print(json.dumps(result, indent=2)) ``` An illustrative response shape is: ```json theme={null} { "items": [ {"product": "Example product", "price": "$3.49"}, {"product": "Another product", "price": null} ] } ``` These entries are invented to demonstrate the schema, not a transcription of the sample image. Validate extracted values against the image when accuracy matters. JSON Schema controls structure, not whether the reading or product-price association is correct. See [structured outputs](/perceptron-mk1.5/capabilities/structured-outputs) for other schema and validation patterns. ## Extract a Markdown table For a human-readable result, request Markdown instead of a JSON Schema response. Run this after the client and image setup above: ```python theme={null} response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image(image_url), {"type": "text", "text": ( "Read the visible grocery labels and return a Markdown table with " "Product and Price columns, one row per label in reading order. " "Preserve the printed text and currency formatting. Write [unreadable] " "for unclear text instead of guessing. Do not add code fences." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError("The Markdown extraction did not complete") print(choice.message.content or "") ``` For a document page, change the instruction to “Transcribe the visible text in reading order as Markdown; preserve headings, lists, and tables.” If your application needs HTML, request semantic headings, paragraphs, and tables in the prompt. These formats express document structure rather than exact pixel layout. Use the JSON Schema workflow when downstream code requires a fixed set of fields. ## Locate the text spans Use a separate annotation request when you want boxes around the evidence. This example asks the model to put each transcribed span in its box's `mention` attribute: ```python theme={null} from pathlib import Path response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image(image_url), {"type": "text", "text": ( "Locate the visible product-name and price text spans. Return a " "flat point_box for each span, with the transcribed text as its mention " "and asset_idx=0. Use normalized coordinates, without collections or tracks. Do not invent text " "that is too small or obscured to read." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, vision_config={"annotation_format": "box"}, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError("The grounded OCR answer did not complete") annotations = choice.message.content or "" print(annotations) Path("response.txt").write_text(annotations, encoding="utf-8") ``` Boxes use normalized 0–1000 coordinates, with top-left followed by bottom-right. `asset_idx="0"` selects the supplied image. Follow [rendering annotations](/perceptron-mk1.5/guides/rendering-annotations) to draw `response.txt` over it, and validate attributes and geometry using the [annotation reference](/perceptron-mk1.5/concepts/annotations). OCR markup and constrained JSON are distinct output formats; the API does not automatically attach boxes to the JSON records from the earlier request. ## Improve extraction quality * State the fields and reading order you need. Distinguish a shelf price, a unit price, and a promotional price when several numbers appear near one product. * Preserve identifiers, prices, dates, and other exact text as strings until your application applies its own normalization rules. * Use an image with enough detail for the smallest relevant text. If you supply a crop as well as the original, treat it as another asset and keep the crop-to-source mapping. * For several pages or labels, keep their order and request explicit `asset_idx` on grounded spans. See [multiple assets](/perceptron-mk1.5/guides/multiple-assets). # Structured outputs Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/structured-outputs Choose native annotations, constrained final answers, or function arguments for your application. Use the format that matches how your application will consume the result: | Need | Format | Returned in | | -------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- | | Spatial or temporal grounding | Native point, box, polygon, clip, and track markup | `message.content` | | A final answer with a known data shape | JSON Schema through `response_format` | `message.content` as a JSON string | | A short answer matching a pattern | Top-level `regex` | `message.content` | | A request to execute an application function | Function definitions in `tools` | `message.tool_calls`, with argument strings to parse and validate | Native annotations are documented in [annotation format](/perceptron-mk1.5/concepts/annotations). They are not automatically converted to JSON objects by the API. Function-call schemas are also separate from final-response constraints: `function.strict` is accepted but does not enforce argument validity. `function.arguments` is a string that can contain invalid JSON; parse it and validate it against your function schema before execution. ## Request a JSON Schema response Install `perceptron>=0.4.0` and `jsonschema`, and set `PERCEPTRON_API_KEY`. This example constrains the answer and validates it locally before use: ```python theme={null} import json from jsonschema import validate from perceptron import Client, image client = Client() schema = { "type": "object", "properties": { "description": {"type": "string"}, "visible_objects": {"type": "array", "items": {"type": "string"}}, "unreadable_text_present": {"type": "boolean"}, }, "required": ["description", "visible_objects", "unreadable_text_present"], "additionalProperties": False, } messages = [{ "role": "user", "content": [ image( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp" ), "Describe this scene using the requested JSON fields.", ], }] response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, max_completion_tokens=1024, response_format={ "type": "json_schema", "json_schema": {"name": "scene", "strict": True, "schema": schema}, }, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete structured answer: {choice.finish_reason}") result = json.loads(choice.message.content or "") validate(instance=result, schema=schema) print(result) ``` Define required fields and allowed values explicitly. Use numeric bounds when a field represents a bounded quantity, and include an appropriate unknown or empty value when the image may not contain the requested information. A valid schema constrains the structure; it does not establish that the model's interpretation is correct. ## Define the response with Pydantic Pydantic v2 can generate the JSON Schema and validate the response with the same Python model. Install it with `pip install "pydantic>=2,<3"`, then run this after defining `client` and the image `messages` above: ```python theme={null} from typing import Literal from pydantic import BaseModel, ConfigDict, Field class SceneAnalysis(BaseModel): model_config = ConfigDict(extra="forbid") scene_type: Literal["indoor", "outdoor", "mixed", "unknown"] main_subjects: list[str] = Field(description="Subjects visible in the image; an empty list if none.") time_of_day: Literal["morning", "afternoon", "evening", "night", "unknown"] = Field( description="Use unknown when the image does not establish the time of day." ) response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages + [{ "role": "user", "content": "Analyze the scene using the requested fields. Do not guess the time of day.", }], max_completion_tokens=1024, response_format={ "type": "json_schema", "json_schema": { "name": "scene_analysis", "strict": True, "schema": SceneAnalysis.model_json_schema(), }, }, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError("The model did not return a complete scene analysis") analysis = SceneAnalysis.model_validate_json(choice.message.content or "", strict=True) print(analysis.scene_type) print(analysis.main_subjects) print(analysis.time_of_day) ``` All three fields are required, `extra="forbid"` rejects unexpected fields, and the literal types restrict allowed values. `model_validate_json` raises `ValidationError` for malformed JSON or values that do not match the model. Handle that failure before storing the result or using it in another workflow. ## Use regex for a short answer Run this after the client and image messages above. Pass `regex` directly to the SDK's message API: ```python theme={null} import re response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages + [{"role": "user", "content": "Is a desk visible? Answer yes or no."}], max_completion_tokens=32, regex="yes|no", ) choice = response.choices[0] answer = choice.message.content or "" if choice.finish_reason != "stop" or re.fullmatch(r"yes|no", answer) is None: raise ValueError("The model did not return a complete yes/no answer") print(answer) ``` Use one response constraint per request. Keep regex patterns small and test them against the answer shapes you intend to allow. ## Combine tools with a constrained final answer A request cannot declare non-empty function `tools` together with a `json_schema` response format or `regex`. The SDK raises `BadRequestError` for this combination before sending the request. First complete the [tool loop](/perceptron-mk1.5/guides/tool-calling). Append every tool result to the conversation. Then request the final answer with your response constraint and **omit `tools`**, `tool_choice`, and `parallel_tool_calls` for that request. The completed tool history can remain in `messages`. Save the function-calling example as `inventory.py`. Its `run()` function returns the completed conversation, including the lookup results and assistant answer. Save the following as `inventory_report.py` in the same directory: ```python theme={null} import json from jsonschema import validate from inventory import client, run stock_schema = { "type": "object", "properties": { "quantity": {"type": "integer", "minimum": 0}, "in_stock": {"type": "boolean"}, }, "required": ["quantity", "in_stock"], "additionalProperties": False, } report_schema = { "type": "object", "properties": {"BALL-RED": stock_schema, "BALL-BLUE": stock_schema}, "required": ["BALL-RED", "BALL-BLUE"], "additionalProperties": False, } history = run() history.append({ "role": "user", "content": ( "Turn the inventory lookup results into a JSON report for BALL-RED and BALL-BLUE. " "Use the quantities already retrieved. A product is in stock when its quantity is positive." ), }) response = client.chat.completions.create( model="perceptron-mk1.5", messages=history, max_completion_tokens=1024, response_format={ "type": "json_schema", "json_schema": {"name": "inventory_report", "strict": True, "schema": report_schema}, }, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError("The model did not return a complete inventory report") report = json.loads(choice.message.content or "") validate(instance=report, schema=report_schema) print(json.dumps(report, indent=2)) ``` With `PERCEPTRON_API_KEY` set as in the function-calling guide, install the dependencies and run the report script: ```bash theme={null} pip install "perceptron>=0.4.0" jsonschema python inventory_report.py ``` The script runs the inventory lookup workflow, then makes one additional request for the report. With the example inventory, the report should contain: ```json theme={null} { "BALL-RED": {"quantity": 12, "in_stock": true}, "BALL-BLUE": {"quantity": 0, "in_stock": false} } ``` Always validate function names and arguments in your application before dispatch. A constrained final answer does not retroactively validate a tool call or its result. ## Validate completed streams With `stream=True`, individual `delta.content` chunks can end inside a JSON string or annotation tag. Use `stream.get_final_completion()` to assemble the response, check `response.complete` and the finish reason, then parse and validate the final text. Treat `finish_reason: "length"`, API errors, or a lost connection as incomplete output. The SDK requests usage by default for Perceptron streams. Usage can arrive in a final chunk with `choices: []`; the assembled completion includes it. If you process chunks directly, consume usage independently of choice deltas. The [video tracking stream example](/perceptron-mk1.5/capabilities/video-tracking#stream-a-tracking-answer) demonstrates streaming; replace annotation parsing with JSON or regex validation for constrained responses. # Reasoning Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/thinking Configure reasoning effort and handle reasoning separately from the final answer. Use the top-level `reasoning_effort` field to control reasoning in new integrations. Accepted values are `none`, `minimal`, `low`, `medium`, and `high`. These guides use `high` when enabling reasoning and `none` when disabling it. Evaluate answer quality and latency on your task. `none` requests an answer without reasoning; any other tier enables reasoning without an additional flag. `minimal` currently maps to `low`. Send the exact lowercase values: an unsupported tier is rejected by the SDK before sending. A raw API request with an unsupported tier returns HTTP 400 with `invalid_request_error` and a message naming `reasoning_effort`. ```json theme={null} { "model": "perceptron-mk1.5", "messages": [{"role": "user", "content": "Explain how to check whether two observations are consistent."}], "reasoning_effort": "high", "max_completion_tokens": 2048 } ``` The final answer is in `message.content`. When returned, reasoning is in the separate `message.reasoning_content` extension. In the Perceptron SDK, read the final answer from `response.text` and optional reasoning from `response.reasoning`. Streaming chunks use `delta.reasoning_content` separately from `delta.content`. The field location depends on the endpoint: | Endpoint | Control | Scope | | -------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------- | | `/v1/chat/completions` | `reasoning_effort` | The completion request, including tool-calling turns. | | `/v1/chat/completions/multilook` | `reasoning_effort` | One tier shared by every prompt and completion in the request. | | `/v1/detect` | `config.reasoning_effort` | Detection-mode-specific behavior; see [Detect](/capabilities/detect#configure-reasoning). | ## Choose effort for your task * Use `none` for a baseline without reasoning, including straightforward captions, direct questions, or transcription. * Set `reasoning_effort="high"` when enabling reasoning for comparisons, counting with ambiguous overlaps, or questions that combine several observations. Evaluate the answer against examples with known results. * Measure the added token and latency cost. Higher effort does not guarantee a more accurate answer. Higher tiers can spend more of `max_completion_tokens` on reasoning. A budget of only a few hundred tokens may leave an empty final answer with `finish_reason: "length"`. Start with a larger budget, such as 2,048 tokens in the example below, and adjust to your task within the model's context and output limits. Evaluate spatial and temporal tasks with the same approach: check the returned geometry, labels, timestamps, and asset references. Do not assume that every task benefits from the same effort level. ## Read a completed answer Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY`. This example keeps reasoning separate from the answer and checks that generation completed: ```python theme={null} import os from perceptron import Client, image client = Client( provider="perceptron", api_key=os.environ["PERCEPTRON_API_KEY"], ) messages = [{ "role": "user", "content": [ image( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp" ), {"type": "text", "text": ( "Compare the terrain along the coast and farther inland. Support your answer with visible evidence." )}, ], }] response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="high", max_completion_tokens=2048, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"Expected a completed answer, got: {choice.finish_reason}") reasoning = response.reasoning or "" answer = response.text print("Reasoning:", reasoning) print("Answer:", answer) if response.usage is not None: print("Usage:", response.usage.to_dict()) ``` Reasoning may be absent, in which case `response.reasoning` is `None`. Use `response.text` as the final answer instead of joining the two fields. ## Consume reasoning in a stream Run this after defining `client` and `messages` above. It collects the two fields independently and consumes the entire stream, including a possible final usage chunk with no choices: ```python theme={null} reasoning_parts = [] answer_parts = [] with client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="high", max_completion_tokens=2048, stream=True, stream_options={"include_usage": True}, ) as stream: for chunk in stream: for choice in chunk.choices: delta = choice.delta if delta is not None: if delta.tool_calls: raise RuntimeError("Expected an answer, but received a tool call") reasoning_parts.append(delta.reasoning_content or "") answer_parts.append(delta.content or "") response = stream.get_final_completion() if not response.complete or response.finish_reason != "stop" or response.tool_calls: raise RuntimeError(f"Incomplete answer: {response.finish_reason}") print("Reasoning:", "".join(reasoning_parts)) print("Answer:", "".join(answer_parts)) if response.usage is not None: print("Usage:", response.usage.to_dict()) ``` The SDK assembles reasoning, text, and usage in `get_final_completion()`. It raises on streamed API errors or a stream that ends before its completion marker. If iteration raises or the final completion is unsuccessful, treat the accumulated text as incomplete. A `length` finish reason can mean reasoning used the available budget before the answer was finished. ## Retain reasoning in conversation history For tool conversations, preserve `reasoning_content` on assistant messages when sending the history back. Append `response.message` or `response.message.to_dict()` to history after checking that a message was returned; both retain the reasoning and tool-call fields. The [tool-calling guide](/perceptron-mk1.5/guides/tool-calling) demonstrates this alongside call IDs and tool results. ## Migrating the older flag For chat completions and Multilook, `vision_config.enable_thinking` is a deprecated compatibility control. If present, it overrides whether reasoning is enabled, even when you also send `reasoning_effort`: * `false` keeps reasoning off, including with `reasoning_effort: "high"`. * `true` enables reasoning. An enabled tier still sets the effort; `reasoning_effort: "none"` does not turn reasoning off in this combination and leaves the tier unpinned. Omit the deprecated flag when using `reasoning_effort` so there is one source of control. Detect has different precedence: its explicit `config.reasoning_effort` wins over `config.enable_thinking`. Reasoning text consumes output budget. Leave room for the final answer or tool arguments, and check `finish_reason` before using the result. Increasing effort does not replace validation of annotations, extracted facts, or function arguments. See the [tokenization guide](/perceptron-mk1.5/guides/tokenization) for output budgeting and usage accounting. # Tools Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/tool-calling Connect Perceptron Mk1.5 to your application's data and functions. Perceptron Mk1.5 can request functions that you define. Give the model descriptions and parameter schemas, then let it choose which functions to call while answering a question. Your application executes the calls and sends the results back so the model can continue. Use tools to connect visual understanding to information outside the supplied images or videos: look up a product identified in an image, retrieve an inspection record, or search your own document collection. ## How tool calling works 1. Send a question and function definitions to `/v1/chat/completions`. 2. The model returns `tool_calls` with function names, argument strings, and call IDs. 3. Your application validates and executes each call. 4. Send the assistant message and a `role: "tool"` result for every call back to the model. 5. Repeat until the model returns its answer or your application reaches its budget. One response can request several functions. Each result is paired with its call through `tool_call_id`, so independent functions can run concurrently. The public API returns function requests. Your application provides the implementations, credentials, and execution. Naming a function `search` does not enable a hosted search service. ## Choose the right output | Goal | Use | | -------------------------------------------------- | ------------------------------------------------------------------------ | | Look up data or perform an application action | Function tools and an application execution loop | | Get a final answer matching a JSON Schema or regex | [Structured outputs](/perceptron-mk1.5/capabilities/structured-outputs) | | Locate objects or follow them through a video | [Spatial annotations and tracks](/perceptron-mk1.5/concepts/annotations) | Function parameter schemas describe the expected arguments; they do not constrain decoding. Validate names and arguments before execution, even if a function declares `strict: true`. Tools cannot be combined with JSON-schema output or regex in the same request; the Perceptron SDK rejects this combination before sending it. ## Build a tool workflow Run a complete Python example, handle streaming calls, and learn the supported controls. Add execution budgets, return images, and combine retrieval with visual reasoning. For the model ID and capabilities, see [Perceptron Mk1.5](/perceptron-mk1.5/models/perceptron-mk1.5). Applications selecting models programmatically can request `GET /v1/models?extended=true` and check for `tool_calling` in the model's `capabilities` array. # Video clipping Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/video-clipping Locate an event as a moment or an interval with temporal annotations. Video clipping identifies **when** an event occurs. Ask for a moment when you need one event boundary, or an interval when you need the duration of an action. Visual events use the video frames; audible events require [soundtrack analysis](#locate-an-audible-cue). The response contains temporal annotations; it does not create a new video file. ## Find a basketball event This example asks two different questions about the same shot: when the ball passes through the hoop, and the interval covering the shot attempt. Each request defines what to include so the model can distinguish the event from the surrounding play. Install the `perceptron>=0.4.0` Python package and set `PERCEPTRON_API_KEY` before running: ```python theme={null} import os from perceptron import Client, video client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) video_url = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/video-clipping/mj_shot_short.mp4" ) def locate_event(question, include_soundtrack=False): response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ video(video_url), {"type": "text", "text": ( question + " Use clip annotations with asset_idx=0 and " "timestamps in explicit seconds. If the requested event " "is absent or uncertain, say so instead of inventing a timestamp." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, vision_config={ "annotation_format": "clip", "enable_audio_in_video": include_soundtrack, }, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete clipping answer: {choice.finish_reason}") return choice.message.content or "" print("Moment:") print(locate_event( "Find the moment the ball passes through the hoop. " "Return a single timestamp for that event." )) print("Interval:") print(locate_event( "Find the shot-attempt interval, from the player's shooting motion " "through the ball passing through the hoop. Return a start and end time." )) ``` ## Locate an audible cue The basketball sample also contains audio. Reuse `locate_event` with `include_soundtrack=True` to locate an audible event on the same video timeline: ```python theme={null} print(locate_event( "Find any clear increase in crowd reaction around the shot. " "Use the audio to identify its start and end, and describe separately " "what is visible during that interval. If no clear change is audible, say so.", include_soundtrack=True, )) ``` This sets `vision_config.enable_audio_in_video` alongside `annotation_format: "clip"`; the default remains frames only. Define the cue you want, such as an audible phrase, an alarm, or a change in background sound. Do not treat a visible action as proof that its expected sound occurred. Returned times are estimates, so review the source audio before making a precise cut. Both the frames and soundtrack belong to video asset `0`. [Timestamped frames](/perceptron-mk1.5/capabilities/video-understanding#send-timestamped-frames) alone cannot supply audio evidence. Use the original video or an excerpt that retains its soundtrack when refining an audible boundary, and preserve any offset to the original timeline. See [video soundtracks](/perceptron-mk1.5/capabilities/video-understanding#analyze-video-soundtracks) for input handling and limits. ## Interpret moments and intervals These examples illustrate the markup; the times are not measured predictions for the sample video: ```html theme={null} ``` The first annotation is a moment. The second is an interval with a start and end. Both refer to the video at asset `0`. A moment is an approximate location in time; it does not establish a playable duration. To create a highlight, your application can add a chosen amount of time before and after it, clamped to the video boundaries. For repeated events, request a separate clip for each occurrence. A collection can describe the group with `mention` and supply an inherited asset selector: ```html theme={null} ``` This is also an illustrative format example. See [annotation format](/perceptron-mk1.5/concepts/annotations#clips) for timestamp parsing and asset selector inheritance. ## Use temporal evidence Before seeking, cutting, or displaying a returned clip, validate its asset selector and timestamps. Check that times are finite, within the chosen video's timeline, and ordered for an interval. A response ending with `finish_reason: "length"` may contain unfinished annotations; do not treat it as a complete result. Frame sampling can miss a short event or leave its boundary uncertain. Avoid promising frame-exact cuts from an estimated timestamp. For a closer look, provide frames around the candidate interval using [timestamped video frames](/perceptron-mk1.5/capabilities/video-understanding#send-timestamped-frames), preserving the timeline or recording any excerpt offset. Use [video Q\&A](/perceptron-mk1.5/capabilities/video-qa) when you need an explanation of the event. Use [video tracking](/perceptron-mk1.5/capabilities/video-tracking) when you also need an object's location over time. # Video Q&A Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/video-qa Ask about goals, actions, and outcomes observed in a video. Video Q\&A lets you ask about the actions, sequence, and outcome of an episode. Start with a concrete question, and ask the model to separate visible evidence from any inferred intent. For example, an assembly video may show several actions toward a goal without showing whether the final assembly succeeds. ## Identify a goal and its subgoals The following request asks about a robot-assembly episode. It requests a concise account of the overall goal, the observed subgoals in order, and whether the ending supports a completion claim. Install the `perceptron>=0.4.0` Python package and set `PERCEPTRON_API_KEY` before running: ```python theme={null} import os from perceptron import Client, video client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) video_url = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/video-qa/robot_assembly.mp4" ) messages = [{ "role": "user", "content": [ video(video_url), {"type": "text", "text": ( "Watch this robot-assembly episode. State the likely overall goal, " "then list the observed subgoals in chronological order. " "For each subgoal, describe the visible action that supports it. " "Distinguish an inferred intention from something directly visible. " "Finally, say whether the video shows the overall goal being " "completed, remaining incomplete, or an uncertain outcome." )}, ], }] response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="high", max_completion_tokens=2048, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete video answer: {choice.finish_reason}") answer = choice.message.content or "" print(answer) ``` The answer is natural language. `reasoning_effort` controls reasoning for the request; it does not require you to display a reasoning trace in your application. If you need fixed fields for downstream processing, define a [JSON Schema](/perceptron-mk1.5/capabilities/structured-outputs) for fields such as `goal`, `subgoals`, and `outcome`. ## Ask a follow-up grounded in time A follow-up question can use the first answer while keeping the original video in the supplied history. Run this after the example above to ask for evidence near the end of the episode: ```python theme={null} messages.extend([ {"role": "assistant", "content": answer}, {"role": "user", "content": ( "Recheck the ending of the video. Which visible action most directly " "supports your outcome assessment? Describe it and cite its interval " "with a clip annotation for asset_idx=0, using explicit seconds. " "If the ending is inconclusive, explain what evidence is missing." )}, ]) follow_up = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="high", max_completion_tokens=2048, vision_config={"annotation_format": "clip"}, ) choice = follow_up.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete follow-up answer: {choice.finish_reason}") print(choice.message.content or "") ``` The video remains asset `0`: the assistant answer and follow-up add text but no new media. The service receives the full `messages` list on each request; it does not retain this history automatically. ## Combine audible and visible evidence The robot-assembly sample above has no audio track. For a question that depends on sound, supply a video with audio and set `vision_config.enable_audio_in_video` to `true` on each request that needs it. This example uses a basketball clip with a soundtrack; run it after creating `client` above: ```python theme={null} response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ video( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/video-clipping/mj_shot_short.mp4" ), {"type": "text", "text": ( "What happens during the basketball play, and what does the soundtrack " "add to that account? Separate visible actions from audible speech or " "reactions. Mark unclear speech instead of guessing the words." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, vision_config={"enable_audio_in_video": True}, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete audiovisual answer: {choice.finish_reason}") print(choice.message.content or "") ``` Soundtrack analysis is off by default. A video without an audio stream is still processed visually; an audio decode error is not evidence of silence. For standalone recordings, use the [audio inputs](/perceptron-mk1.5/capabilities/audio). See [video soundtracks](/perceptron-mk1.5/capabilities/video-understanding#analyze-video-soundtracks) for supported video inputs and limits. ## Choose the question and evidence * Ask for observable actions when labeling a task sequence. Use separate language for inferred goals and observed completion. * Narrow the question or supply a relevant excerpt when a long video contains many unrelated activities. Sampled frames can miss brief actions. * Use [video clipping](/perceptron-mk1.5/capabilities/video-clipping) for temporal evidence, or [video tracking](/perceptron-mk1.5/capabilities/video-tracking) when the answer needs an object's changing position. * Use [Multilook](/perceptron-mk1.5/guides/multilook) for independent questions over the same video. Keep dependent follow-ups in a conversation so they include the earlier answer. See [video understanding](/perceptron-mk1.5/capabilities/video-understanding) for supported video inputs and timestamped frames. # Video tracking Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/video-tracking Follow objects through video with timestamped points, boxes, and track tags. Perceptron Mk1.5 can follow an object through a video and return its positions in a ``. Each observation combines a timestamp with spatial geometry. Use this for object trajectories, motion review, or video overlays. Tracking uses the chat completions endpoint. Request the object and track format in your prompt, and use `vision_config.annotation_format: "box"` for bounding boxes. The response contains markup in `message.content`. ## Track an object Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY`. This example uses the public basketball video from the Perceptron cookbook. ```python theme={null} import os from perceptron import Client, video client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) video_url = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/video-clipping/mj_shot_short.mp4" ) request = { "model": "perceptron-mk1.5", "reasoning_effort": "high", "messages": [ { "role": "user", "content": [ video(video_url), { "type": "text", "text": ( "Track the basketball while it is visible. " "Return a track with mention=\"basketball\" and asset_idx=\"0\". " "Use point_box children with timestamps in seconds and " "coordinates normalized to 0–1000. " "Do not invent positions when the ball is occluded or off-screen." ), }, ], } ], "max_completion_tokens": 4096, "vision_config": {"annotation_format": "box"}, } response = client.chat.completions.create(**request) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete tracking answer: {choice.finish_reason}") print(choice.message.content or "") ``` An illustrative track looks like this. These coordinates and timestamps explain the format; they are not recorded output for the sample video. ```html theme={null} (410,520) (450,570) (470,300) (510,350) (580,180) (620,230) ``` When present, `asset_idx="0"` selects the first media asset. It is optional: the model can omit it from the track even though this prompt requests it. Without an explicit or inherited selector, the track refers to the last asset available when it was produced, which is video `0` in this request. For several videos, request explicit selectors and apply the same [last-asset default](/perceptron-mk1.5/guides/multiple-assets#resolve-selectors-before-drawing) when a selector is missing. Each `t` is a time within the selected video; each box gives top-left and bottom-right coordinates. For several objects, ask for separate tracks and describe how to distinguish them. See the [annotation reference](/perceptron-mk1.5/concepts/annotations) for the full grammar. Avoid passing the answer through an ordinary HTML parser: HTML treats `track` as a void element and can discard its grouping. ## Stream a tracking answer SSE chunks are transport boundaries. A chunk may end inside a tag, attribute, timestamp, or coordinate. The simplest reliable consumer accumulates text and validates the finish reason before parsing it. Run this after defining `client` and `request` above: ```python theme={null} with client.chat.completions.create( **request, stream=True, stream_options={"include_usage": True}, ) as stream: for chunk in stream: for choice in chunk.choices: if choice.delta is not None and choice.delta.content: print(choice.delta.content, end="", flush=True) final = stream.get_final_completion() if not final.complete or final.finish_reason != "stop" or final.tool_calls: raise RuntimeError(f"Tracking did not complete: {final.finish_reason}") text = final.text or "" print() print(final.usage) ``` The SDK accumulates the text and usage in `final`, including usage from a trailing chunk with an empty `choices` array. Network and API errors can interrupt the loop; allow them to fail the operation rather than treating the accumulated prefix as a complete answer. A `"length"` finish reason means the output budget was exhausted, even if some waypoints are usable. For live overlays, an incremental annotation parser can emit fully closed child observations while retaining the surrounding track context. Keep these provisional results separate from the final answer. Never append closing tags to make truncated output look complete; retain the completed observations and mark the track incomplete. ## Parse the tracks The SDK parses annotation markup without HTML's special treatment of ``. Run this after the non-streaming request above, or use `final` in place of `response` after streaming: ```python theme={null} annotations = response.annotations(strict=True) for track in annotations.tracks: if not track.complete: raise ValueError("The track markup is incomplete") print(track.mention, len(track.points)) # Flattened boxes carry any selector inherited from their track or collection. for waypoint in annotations.boxes: asset_idx = response.resolve_asset_idx(waypoint) if asset_idx != 0: raise ValueError("Expected a waypoint on the supplied video") print(asset_idx, waypoint.t, waypoint.top_left, waypoint.bottom_right) ``` `strict=True` raises reported parse errors, but it is not a complete schema validator. Retain the raw text and validate annotation syntax and selectors before using parsed results; see [rendering annotations](/perceptron-mk1.5/guides/rendering-annotations). `resolve_asset_idx()` preserves an explicit `0`, applies inherited selectors, and uses the last asset in this response's request when no selector is present. Keep the response associated with that request's media; later turns do not change its asset indices. Validate normalized coordinate ranges, box ordering, and timestamps against the source video before drawing. Parsing does not establish whether an observation is correct or within the video's duration. ## Turn waypoints into an overlay The model supplies observations, which may be sparse. To render them: 1. Resolve an explicit or inherited selector, or default to the last asset available when the track was produced. Confirm the selected asset is a video and use its displayed dimensions. 2. Validate the timestamps and normalized geometry, then sort observations by time. 3. Draw at observed timestamps. If needed, interpolate between nearby observations of the same object. 4. Stop or mark the overlay uncertain across long gaps, occlusion, or a scene cut. Interpolation is a client decision, not another model observation. Linear interpolation can work for short intervals with smooth motion, but it cannot reconstruct a bounce, a sudden turn, or an object passing behind an obstacle. Avoid extending a track before its first observation or after its last one without additional evidence. For denser overlays, follow [High Fidelity Object Tracking](/perceptron-mk1.5/guides/high-fidelity-object-tracking) to combine model waypoints with optical flow and an appearance tracker. Keep model observations and locally estimated positions distinguishable in downstream analysis. ## Improve tracking prompts * Identify the target with visible properties and an initial location, such as “the red car in the left lane at the start.” * State whether you want the whole object, a component, or its center. Choose `box` or `point` to match that task. * Ask for positions only while the object is visible. A missing observation is preferable to an invented trajectory. * Narrow the video interval or number of requested objects if the answer repeatedly reaches the output limit. * Keep timestamps in seconds when authoring annotations. Input `video_frames.timestamp_ms` uses milliseconds; convert explicitly when connecting the two. For event descriptions and temporal clips, see [video understanding](/perceptron-mk1.5/capabilities/video-understanding). For videos mixed with reference images, see [multiple assets](/perceptron-mk1.5/guides/multiple-assets). # Video understanding Source: https://docs.perceptron.inc/perceptron-mk1.5/capabilities/video-understanding Understand video frames and soundtracks, and identify moments or event intervals. Perceptron Mk1.5 can describe actions, answer questions about a clip, and identify when events happen. Send a `video_url` content part, or upload a video through the [Files API](/perceptron-mk1.5/guides/files). Explore the dedicated guides for [video Q\&A](/perceptron-mk1.5/capabilities/video-qa), [moments and intervals](/perceptron-mk1.5/capabilities/video-clipping), [reference-image video search](/perceptron-mk1.5/capabilities/in-context-learning-video), and [object tracking](/perceptron-mk1.5/capabilities/video-tracking). ## Ask about a video Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY`. The `video()` helper accepts a URL, local file, or uploaded file reference. ```python theme={null} import os from perceptron import Client, video client = Client(api_key=os.environ["PERCEPTRON_API_KEY"]) video_url = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/tutorials/isaac_frame_by_frame/surf.mp4" ) messages = [{ "role": "user", "content": [ video(video_url), {"type": "text", "text": "Describe the surfer's actions in chronological order."}, ], }] response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, reasoning_effort="high", max_completion_tokens=2048, ) print(response.choices[0].message.content) ``` For long or visually complex footage, narrow the question to one event or supply the relevant excerpt. Frame sampling can miss brief events; avoid treating a missing observation as proof that an event never occurred. ## Analyze video soundtracks Videos are processed as frames only by default. Set **`vision_config.enable_audio_in_video: true`** when speech, sound effects, or other audio matters to your question. Run this after creating `client` above; this basketball sample contains an audio track: ```python theme={null} soundtrack_video_url = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/video-clipping/mj_shot_short.mp4" ) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ video(soundtrack_video_url), {"type": "text", "text": ( "Describe the visible action and the audible speech or sounds. " "Distinguish what you see from what you hear, and say when speech is unclear." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, vision_config={"enable_audio_in_video": True}, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete audiovisual answer: {choice.finish_reason}") print(choice.message.content or "") ``` The flag applies to every byte-backed video in the request, including remote or data `video_url` parts and uploaded `video_file_id` parts. When omitted or set to `false`, the soundtrack is not processed. It does not enable or disable standalone audio inputs. Preselected `video_frames` contain no soundtrack, so the flag has no effect on those frames. A video with no audio stream, or no soundtrack overlapping the sampled video window, is processed as frames only. A video with an encoded silent audio track still consumes audio tokens when `enable_audio_in_video` is explicitly set to `true`. An audio decode failure or audio-token limit violation still returns an error; enabling sound does not silently discard those failures. Audio uses approximately **750 encoder tokens per minute**, plus timestamp tokens in the prompt. The per-item audio limit is **16,384 encoder tokens**, roughly **21.8 minutes**; video frames, other inputs, and the requested output also need to fit the shared context budget. Check `usage.prompt_tokens_details.audio_tokens` for the audio encoder contribution when returned. See [Audio](/perceptron-mk1.5/capabilities/audio) and [tokenization](/perceptron-mk1.5/guides/tokenization) for formats and budgeting. ## Locate an event in time Use `annotation_format: "clip"` when your application needs a moment or interval. Run this after the setup above: ```python theme={null} response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ video(video_url), {"type": "text", "text": ( "Find the intervals where the surfer is riding a wave. " "Return clip annotations with timestamps in seconds." )}, ], }], reasoning_effort="high", max_completion_tokens=2048, vision_config={"annotation_format": "clip"}, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete clipping answer: {choice.finish_reason}") print(choice.message.content or "") ``` An illustrative interval looks like: ```html theme={null} ``` The example values explain the format; they are not measured output for this video. The first time is the start and the second is the end. A single timestamp identifies a moment. See [annotation format](/perceptron-mk1.5/concepts/annotations#clips). ## Choose a video workflow | Goal | Workflow | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Understand speech or sounds alongside the scene | [Enable video soundtracks](#analyze-video-soundtracks). | | Find when something happens | Request `clip` annotations. | | Follow an object's position | Request [video tracks](/perceptron-mk1.5/capabilities/video-tracking) with timed spatial annotations. | | Produce a denser object track | Combine model waypoints with optical flow and appearance tracking in [High Fidelity Object Tracking](/perceptron-mk1.5/guides/high-fidelity-object-tracking). | | Compare videos or use reference images | Send [multiple assets](/perceptron-mk1.5/guides/multiple-assets) in one conversation. | | Ask several independent questions | Use [Multilook](/perceptron-mk1.5/guides/multilook) to share the context. | ## Send timestamped frames Use `video_frames` when you have already selected frames from a video. Provide at least two frames with the same image dimensions, each with an `image_url` and a non-negative integer `timestamp_ms`. Keep frames in timestamp order; equal timestamps are allowed. URLs can be HTTP(S) URLs or supported image data URLs. The SDK's `video_frames()` helper accepts `(image, timestamp_ms)` pairs and encodes local frames for you. Save frames from 0 and 1.5 seconds of your clip as `frame-000.png` and `frame-1500.png`; change the filenames and timestamps to match your selected frames. Run this after creating `client` above: ```python theme={null} from perceptron import video_frames response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ video_frames([ ("frame-000.png", 0), ("frame-1500.png", 1500), ]), {"type": "text", "text": ( "Describe the change visible between these frames. " "Use clip annotations with timestamps in seconds." )}, ], }], vision_config={"annotation_format": "clip"}, max_completion_tokens=1024, ) if response.finish_reason != "stop": raise RuntimeError(f"Incomplete frame comparison: {response.finish_reason}") print(response.text or "") ``` The entire `video_frames` group is one asset for `asset_idx`. Each supplied frame consumes one media unit, so this example uses one asset index and two of the request's 256 media units. Sending those frames as separate `image_url` content parts would instead create two image assets. See [multiple assets](/perceptron-mk1.5/guides/multiple-assets). Timestamps are preserved, not automatically rebased: `1500` milliseconds corresponds to `1.5 seconds` in annotations. If these frames came from an excerpt starting at 60 seconds and you supplied excerpt-relative timestamps `0` and `1500`, add the 60-second offset when mapping results back to the source. If you supplied source-relative timestamps `60000` and `61500`, the timeline is already 60 and 61.5 seconds; do not add the offset again. Keep this mapping with your media, and check `finish_reason` before using the answer. Selected frames provide sparse observations; they do not establish what happened between them. See [recover a gap with new model observations](/perceptron-mk1.5/guides/high-fidelity-object-tracking#recover-a-gap-with-new-model-observations) when you need more observations in an uncertain tracking interval. # Release notes Source: https://docs.perceptron.inc/perceptron-mk1.5/changelog Perceptron Mk1.5 API integration changes. ## 2026-09-25 — Isaac 0.1 and 0.2 retired `isaac-0.1`, `isaac-0.2-1b`, and `isaac-0.2-2b-preview` are retired from the Perceptron API. Migrate to [`perceptron-mk1.5`](/perceptron-mk1.5/models/perceptron-mk1.5). * **Retired model IDs:** requests for these models return HTTP 404 with error code `model_not_found`. * **Unknown model IDs:** requests for any unknown model ID now return 404 `model_not_found` instead of 400. * **Model discovery:** the retired models no longer appear in `GET /v1/models` or MCP. ## 2026-09-25 — Perceptron Mk1.5 Use the model ID `perceptron-mk1.5` with the chat completions API. * **Audio input:** transcribe, summarize, and ask questions using `input_audio`, `audio_url`, or `audio_file_id` with WAV, MP3, or FLAC. The current per-item limit is 16,384 audio tokens, approximately 21.8 minutes; audio also occupies the shared 36,864-token context. Usage is reported in `usage.prompt_tokens_details.audio_tokens`. See [Audio](/perceptron-mk1.5/capabilities/audio). * **Video soundtracks:** set `vision_config.enable_audio_in_video: true` to analyze audio alongside video frames. This is off by default. See [video soundtracks](/perceptron-mk1.5/capabilities/video-understanding#analyze-video-soundtracks). * **Reasoning effort:** use `none`, `minimal`, `low`, `medium`, or `high` on chat completions and Multilook. `vision_config.enable_thinking` is deprecated. See [Reasoning](/perceptron-mk1.5/capabilities/thinking). * **Function calling:** declare functions, receive tool calls, and return results for the next turn. See [tool calling](/perceptron-mk1.5/capabilities/tool-calling). * **Video tracking:** request `` output with timestamped spatial observations. See [video tracking](/perceptron-mk1.5/capabilities/video-tracking). * **Multiple assets:** the optional `asset_idx` attribute associates annotations with an input across images, videos, and conversation history. See [multiple assets](/perceptron-mk1.5/guides/multiple-assets) for handling both present and omitted selectors. * **Structured output guidance:** distinguish native annotations, constrained final responses, and function arguments. See [structured outputs](/perceptron-mk1.5/capabilities/structured-outputs). * **Hosted MCP:** `question` and `caption` accept `modality: "audio"` and opt-in video soundtracks; all four inference tools accept `reasoning_effort`. * **MCP model default:** `question`, `caption`, `ocr`, and `detect` now default to `perceptron-mk1.5` when `model` is omitted. Pass `model: "perceptron-mk1"` to retain the previous model. See the [MCP guide](/perceptron-mk1.5/guides/mcp) for hosted access and npm bridge compatibility. The [model card](/perceptron-mk1.5/models/perceptron-mk1.5) lists supported inputs, context limits, and pricing. # Annotation format Source: https://docs.perceptron.inc/perceptron-mk1.5/concepts/annotations Read spatial and temporal grounding, including tracks and asset selectors. Perceptron Mk1.5 can ground an answer with points, boxes, polygons, clips, and tracks. These annotations appear as markup inside `choices[0].message.content`, alongside any prose. Streaming responses deliver the same text through `choices[0].delta.content`. Use `vision_config.annotation_format` to request `"point"`, `"box"`, `"polygon"`, or `"clip"` annotations. For video tracking with bounding boxes, choose `"box"` and ask for tracks in the prompt. There is no `"track"` annotation-format value. ## Spatial annotations ```html theme={null} (120,200) (100,150) (300,350) (20,40) (60,40) (60,80) (20,80) ``` | Tag | Body | | ----------- | ---------------------------------------------------- | | `point` | One `(x,y)` coordinate. | | `point_box` | Two coordinates: top-left, then bottom-right. | | `polygon` | An ordered sequence of vertices around the boundary. | Coordinates use a normalized 0–1000 grid. The origin is the top-left corner; x increases to the right and y increases downward. `(500,500)` is the center, independently of image dimensions. Convert to pixels using the dimensions of the selected image or video frame: ```python theme={null} def to_pixels(x, y, width, height): return x * width / 1000, y * height / 1000 ``` A coordinate at `1000` denotes the image boundary. If your drawing library requires a pixel index, clamp the final index to `width - 1` or `height - 1`. Keep the normalized values for storage and later resizing. The [coordinate system guide](/perceptron-mk1.5/concepts/coordinates) covers per-asset dimensions and crop transforms. See [Rendering annotations](/perceptron-mk1.5/guides/rendering-annotations) for a complete flat-box drawing example. ## Attributes | Attribute | Meaning | | ----------- | ------------------------------------------------------------------------------------------------------------------ | | `mention` | A human-readable description of the object or event. It is not a persistent object ID. | | `asset_idx` | Optional zero-based position of a media occurrence among the assets supplied up to that point in the conversation. | | `t` | Time within a temporal asset. Author timestamps with explicit units, such as `"1.5 seconds"`. | Quote attribute values. `asset_idx` answers **which asset**; `t` answers **when within that asset**. An image selector is never a timestamp. `asset_idx` is optional on annotations, collections, and tracks. The model may omit it even when several assets are present or the prompt requests it. Its absence does not make an annotation malformed. For example, a box can appear without a selector: ```html theme={null} (100,150) (300,350) ``` Resolve inherited selectors first. If `asset_idx` is neither explicit nor inherited, the intended asset is **the last asset available when that annotation was produced**. With `N` assets available at that point, the default index is `N - 1`: `0` for one asset, `1` for two assets, and so on. Count supplied media in earlier messages and tool results; later media does not change an earlier annotation's binding. Requesting explicit selectors helps distinguish multiple inputs but does not guarantee their presence. See [multiple assets](/perceptron-mk1.5/guides/multiple-assets) for ordering and resolution rules. ## Collections A collection groups related annotations. Its `mention` describes the group. Children inherit its `asset_idx` unless they specify their own selector; each child can also carry its own `mention`. ```html theme={null} (100,150) (300,350) (500,150) (700,350) ``` Collections can contain different geometry types, clips, and tracks, but cannot contain other collections. They can also group annotations from different assets: put selectors on the children, or override an inherited selector explicitly. ```html theme={null} (100,150) (300,350) (400,300) (600,500) ``` ## Clips A clip identifies a moment or a time interval in a video or audio recording. It is self-closing and has no coordinate body. See [video clipping](/perceptron-mk1.5/capabilities/video-clipping) or [audio clipping](/perceptron-mk1.5/capabilities/audio-clipping) for complete request examples. ```html theme={null} ``` For an interval, the first timestamp is the start and the second is the end. These examples illustrate the format; the values are not measured predictions. ## Tracks A track groups observations of one object within one video asset. The track can carry an object description and optional asset selector; each child provides a timestamp and geometry. Children inherit the track's selector when they do not specify one, and a track can inherit a selector from its enclosing collection. ```html theme={null} (100,150) (180,230) (120,150) (200,230) ``` Use separate tracks for separate objects or videos, even when their descriptions match. A collection can group these tracks. A track must not switch assets between observations. Every track child must have a timestamp and use the same geometry type: all `point`, all `point_box`, or all `polygon`. Tracks cannot contain clips, collections, or other tracks. Track waypoints can be sparse. A track does not promise a box for every source frame, continuous visibility, or a stable identity across separate requests. See [video tracking](/perceptron-mk1.5/capabilities/video-tracking) for a request example and [High Fidelity Object Tracking](/perceptron-mk1.5/guides/high-fidelity-object-tracking) for estimating positions between model waypoints with optical flow and an appearance tracker. ## Parse and validate annotations Treat annotations as a model-output format rather than ordinary HTML. In HTML, `track` is a void element: a browser HTML parser may move its children outside the track and lose their grouping. The response can also contain prose, so the entire answer is not an XML document. A dedicated parser should recognize the supported annotation tags, retain their nesting, and resolve inherited asset selectors before producing application objects. Keep any IDs, pixel coordinates, and interpolation metadata you add separate from the returned markup. Validate parsed results before using them: * Resolve explicit and inherited `asset_idx` values against the assets available at that annotation's position; otherwise use the last available asset's index. Preserve `0`; test for a missing value instead of using a truthiness check. Keep omitted values distinct from invalid or out-of-range selectors. * Check finite coordinates, box ordering, and the expected 0–1000 range. Choose an explicit policy for invalid geometry rather than silently placing it on another asset. * Check that track timestamps belong to the selected video and increase in the order you use for playback. * Accept explicit second units and bare numeric `t` values in generated output. When authoring annotations in prompts or history, use explicit seconds to avoid ambiguity with frame indices. * Preserve completed observations separately from a partial tag or unfinished track. A connection ending does not make partial markup complete. Annotation markup is different from a JSON Schema response and from function-call arguments. See [structured outputs](/perceptron-mk1.5/capabilities/structured-outputs) to choose the appropriate format. # Coordinate system Source: https://docs.perceptron.inc/perceptron-mk1.5/concepts/coordinates Convert normalized annotations to image pixels and preserve their asset and crop context. Mk1.5 chat-completion annotations use a **0–1000 grid** on each image or video frame. The origin is the top-left corner, x increases to the right, and y increases downward. `(500,500)` is the center, whatever the image dimensions. ```text theme={null} (0,0) ───────────────────── (1000,0) │ │ │ (500,500) │ │ │ (0,1000) ────────────────── (1000,1000) ``` Keep normalized geometry together with its optional `asset_idx` and, for video observations, its timestamp. Resolve an omitted selector through inheritance or the last asset available when the annotation was produced. The [annotation reference](/perceptron-mk1.5/concepts/annotations) defines the tags and inheritance rules; [multiple assets](/perceptron-mk1.5/guides/multiple-assets) explains which input each selector identifies. ## Convert between normalized and pixel coordinates Use the dimensions of the selected image as it was supplied to the model: ```python theme={null} def to_pixels(x, y, width, height): return x * width / 1000, y * height / 1000 def to_normalized(pixel_x, pixel_y, width, height): if width <= 0 or height <= 0: raise ValueError("Image dimensions must be positive") return pixel_x * 1000 / width, pixel_y * 1000 / height print(to_pixels(500, 500, 1920, 1080)) # (960.0, 540.0) print(to_normalized(960, 540, 1920, 1080)) # (500.0, 500.0) ``` Apply the conversion to both corners of a box and to every vertex of a polygon. Keep fractional coordinates until the drawing or indexing step. A coordinate of `1000` denotes the image boundary; if a drawing library needs an integer pixel index, clamp the final index to `width - 1` or `height - 1`. For example, the illustrative normalized box `(250,250) (750,750)` becomes `(480,270) (1440,810)` on a 1920×1080 image. On a 640×360 thumbnail of the same image it becomes `(160,90) (480,270)`. ## Choose the correct asset Identify the asset before choosing dimensions. Use `asset_idx` when present, including an inherited value. Otherwise, use the last asset available when the annotation was produced. If the only asset is a 1920×1080 image, a box without a selector uses that image's dimensions, even if a later tool result adds a 512×512 crop. A new box produced after the crop defaults to the crop when its selector is neither explicit nor inherited. Repeating an image in the conversation creates another asset occurrence. For a video track, the asset selector identifies the video and `t` identifies the observation time. Spatial coordinates describe that frame. Keep frame timestamps separate from frame numbers: sampling does not guarantee that one model observation corresponds to one source frame. When rendering a thumbnail, use the size of its displayed image content. If the viewer letterboxes the image, add the content rectangle's offset after scaling; do not scale into the surrounding margins. Keep EXIF orientation and any explicit rotations consistent between the input and the rendered image. ## Map a crop back to the original image A crop returned by a tool is a separate asset. Its coordinates describe the crop, so first convert them to crop pixels and add the crop's offset in the original: ```python theme={null} def crop_point_to_image(x, y, left, top, crop_width, crop_height, image_width, image_height): if min(crop_width, crop_height, image_width, image_height) <= 0: raise ValueError("Image and crop dimensions must be positive") pixel_x = left + x * crop_width / 1000 pixel_y = top + y * crop_height / 1000 return pixel_x * 1000 / image_width, pixel_y * 1000 / image_height # Center of a 400×200 crop beginning at (100,50) in a 1000×500 image. print(crop_point_to_image(500, 500, 100, 50, 400, 200, 1000, 500)) # (300.0, 300.0) in the original image's normalized coordinates. ``` Apply this to every point being mapped, including both box corners. This formula assumes the crop has only been cropped and resized. Rotated or perspective-transformed crops need the corresponding transform. See [High Fidelity Object Tracking](/perceptron-mk1.5/guides/high-fidelity-object-tracking#align-frames-and-coordinates) for keeping video frames, timestamps, and boxes aligned during local tracking. ## Validate before drawing or measuring * Require finite coordinates in the expected 0–1000 range. Keep invalid predictions available for inspection rather than silently hiding them with clamping. * Require ordered box corners: `x1 < x2` and `y1 < y2` for a nonempty box. * Resolve inherited selectors on collections and tracks before selecting a canvas. Preserve selector `0`; it is a valid value. * Compare geometry only after resolving its asset, timestamp, orientation, and coordinate frame. Normalized coordinates alone do not make boxes from different images comparable. Intersection-over-union can be computed in normalized coordinates when both boxes describe the same image and coordinate frame. Converting both through the same axis-aligned resize leaves the ratio unchanged. For a complete response-to-image example, see [Rendering annotations](/perceptron-mk1.5/guides/rendering-annotations). The shared [Detect API](/capabilities/detect) has its own pixel-coordinate response contract; use the contract of the endpoint that produced your result. # Batch processing Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/batch Process independent images with bounded concurrency and a result for each item. For a collection of independent images, run ordinary chat-completion requests through a bounded worker pool. This guide batches work in your application; it does not use a server-side batch endpoint. If several independent questions share one image or video, consider [Multilook](/perceptron-mk1.5/guides/multilook) for that item. If one question needs several media items together, use [multiple assets](/perceptron-mk1.5/guides/multiple-assets) in a single request. ## Process a list of images Install `perceptron>=0.4.0`, set `PERCEPTRON_API_KEY`, and create `images.txt` with one HTTP(S) image URL per line. For example: ```text theme={null} https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/main/cookbook/_shared/assets/capabilities/detection/ppe_line.webp ``` Save this script as `batch_questions.py`. It submits at most four items at a time and writes one JSON record per item. The original line number identifies the input even when requests complete out of order. ```python theme={null} import json import os import sys from concurrent.futures import ThreadPoolExecutor, as_completed from itertools import islice from pathlib import Path from time import perf_counter from perceptron import Client, SDKError, image WORKERS = 4 def process_image(client, line_number, url): started = perf_counter() record = {"line": line_number} try: response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image(url), {"type": "text", "text": "Describe the visible objects in two sentences."}, ], }], max_completion_tokens=512, ) if not response.choices: raise ValueError("The response contained no completion") choice = response.choices[0] record["finish_reason"] = choice.finish_reason record["usage"] = response.usage.to_dict() if response.usage else None if choice.finish_reason != "stop" or choice.message.tool_calls: raise ValueError(f"Incomplete answer: {choice.finish_reason}") record.update(status="ok", text=choice.message.content or "") except (SDKError, ValueError) as error: record.update(status="error", error={ "type": type(error).__name__, "message": str(error), "status_code": getattr(error, "status_code", None), }) record["elapsed_seconds"] = round(perf_counter() - started, 3) return record def run(path): client = Client( api_key=os.environ["PERCEPTRON_API_KEY"], provider="perceptron", timeout=60.0, ) with path.open() as source, ThreadPoolExecutor(max_workers=WORKERS) as pool: items = ((number, line.strip()) for number, line in enumerate(source, 1) if line.strip()) while group := list(islice(items, WORKERS)): futures = [pool.submit(process_image, client, number, url) for number, url in group] for future in as_completed(futures): print(json.dumps(future.result()), flush=True) if __name__ == "__main__": if len(sys.argv) != 2: raise SystemExit("Usage: python batch_questions.py images.txt") run(Path(sys.argv[1])) ``` Run it with: ```bash theme={null} python batch_questions.py images.txt > results.jsonl ``` The script does not retry failed requests automatically. A failure on one item becomes an error record while the other items continue. An answer with `finish_reason: "length"` is recorded as incomplete, not as a successful result. ## Resume and scale deliberately Keep the input file unchanged when using its line numbers to match saved results. For a changing dataset, assign each item a stable application ID instead. Persist completed records as you go, then select only failed or missing items for a later run. Protect the result file: answers and error messages can contain details from your inputs. Use the [error guide](/perceptron-mk1.5/guides/error-messages) to distinguish retryable failures from invalid inputs or exhausted credits. Apply backoff and a total deadline before resubmitting an item. If you adapt the worker to a tool workflow, do not replay completed tool actions when retrying a model request. Four workers bound concurrent requests, but do not enforce requests per minute. Pace submissions across all workers sharing your organization and reduce load when rate limited; see [scaling](/perceptron-mk1.5/guides/scaling). Compare elapsed time, completion rate, errors, and token usage on the same workload before increasing concurrency. For local or repeatedly used media, [upload files once](/perceptron-mk1.5/guides/files) and use file references. File reuse reduces upload traffic, not the model's media-token usage. # Error messages Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/error-messages Diagnose API failures, retry transient requests, and handle incomplete results. Start with the HTTP status and error details. Invalid inputs, credentials, and exhausted credits need a change before you retry. Rate limits, temporary service failures, and connection problems may recover after a wait. ## Read an API error Direct HTTP chat-completion requests return structured errors in this form. For example, an unsupported `tool_choice` produces HTTP 400: ```json theme={null} { "error": { "message": "Only `tool_choice` 'auto' and 'none' are supported; forcing a tool call is not supported yet.", "type": "invalid_request_error", "param": "tool_choice", "code": "unsupported_tool_choice" } } ``` Use `message` to diagnose the problem, `param` to locate an offending field, and `type` or `code` to classify it when available. Those last three fields can be `null`; not every validation failure has a dedicated code. Branch on a documented code rather than matching the wording of a message. A connection failure has no HTTP response, and malformed HTTP requests or intermediary failures may not have this JSON body. Record the response's `x-trace-id` header when present. It helps support locate the request, but may be absent when a request fails before reaching the API. Also retain the UTC time, endpoint, model, HTTP status, and error details. Keep API keys out of logs and support messages. The SDK exposes errors as `SDKError` subclasses with `status_code`, `error_type`, `code`, `param`, `request_id`, and `retry_after`. Some typed-argument checks, such as unsupported `tool_choice`, raise `BadRequestError` before sending HTTP; those errors have no HTTP status or request ID. For a `RateLimitError`, `code` is `"rate_limit"` and the server’s original code is in `details["code"]` when present. Exhausted credits raise `QuotaExceededError`. ## Decide whether to retry | Status or condition | What to check | Next step | | --------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `400` or `422` | Invalid JSON, parameter values, message structure, media, or unsupported options | Correct the request before resending it. Inspect `param` and the response body. | | `401` | Missing, invalid, or revoked API key | Check `Authorization: Bearer ...` and the key supplied through `PERCEPTRON_API_KEY`. | | `403` | Access denied | Check the key's organization and access to the requested resource. | | `404` | Unknown endpoint, model, or file | Check the URL and identifier. Use the current [model ID](/perceptron-mk1.5/models/perceptron-mk1.5) or verify the file still exists. | | `411` on a file upload | Missing `Content-Length` | Use the multipart upload pattern in the [Files guide](/perceptron-mk1.5/guides/files). | | `413` | Upload exceeds a file-size or storage limit | Reduce the upload, or free storage if the error reports exhausted storage quota. | | `429` with `type: "insufficient_quota"` | Credits are exhausted | Check your plan and billing. Repeated requests will not restore credits. | | `429` with `type: "rate_limit_error"` | Too many requests | Reduce concurrency and wait before retrying. | | `500`, `502`, `503`, or `504` | Temporary service or upstream failure | Retry a limited number of times with backoff; investigate persistent failures. | | Connection error or timeout | No complete response was received | Check connectivity and timeouts, then retry only within your application's policy. The original request may already have run. | Organization rate-limit responses include `Retry-After` in seconds. An HTTP 503 with `code: "model_overloaded"` includes `Retry-After: 30`. Other failures may omit that header; use backoff when it is absent. A retry is a new model request and may produce a different answer or additional usage. ## Correct common request problems ### Context and output limits `context_length_exceeded` means the input or requested completion does not fit the model's context. Shorten conversation history, reduce the amount of media, or lower `max_completion_tokens` when the requested output is the problem. Keep enough room for reasoning and the final answer. See the [model card](/perceptron-mk1.5/models/perceptron-mk1.5) for current context and output limits. Exceeding a configured output limit can also produce a validation error without this code. Increasing a client timeout will not fix either request-size problem. A response that starts successfully but ends with `finish_reason: "length"` is a separate case: handle it as [incomplete output](#handle-incomplete-responses-and-streams). ### Images, videos, audio, and files Check that a media URL is reachable and has not expired, that the bytes decode as the declared format, and that a file reference exists in the organization making the request. An image URL that serves an HTML login page is not an image. For uploads, send the actual media bytes with a matching content type; renaming the extension does not convert a file. Use the [Files guide](/perceptron-mk1.5/guides/files) for upload formats and limits, and [video understanding](/perceptron-mk1.5/capabilities/video-understanding) for video URLs and frame inputs. When a request contains many media parts, reduce their number; uploading them first does not remove request-level media limits. ### Audio input and video soundtracks | Error code or message | Correction | | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `audio_token_limit_exceeded` | Shorten or split the audio. The current Mk1.5 limit is 16,384 audio tokens per item, approximately 21.8 minutes. The response message describes the active model's limit; increasing the output budget will not fix it. | | `Invalid input_audio.format: 'ogg'. Expected one of wav, mp3, flac.` | Convert to WAV, MP3, or FLAC and set `input_audio.format` accordingly. Supply bare base64 in `data`, without a data-URL prefix. | | `invalid_audio_format` | Check the bytes served by an audio URL. Renaming an unsupported file or sending an MP4 video as `audio_url` does not convert it to a supported audio container. | | Message contains `does not support audio input` | Select an audio-capable model such as `perceptron-mk1.5` for standalone audio. | | Message contains `does not support audio in video` | Select a model that supports video soundtracks, or disable `vision_config.enable_audio_in_video` for a visual-only request. | | `media_too_large` | Reduce the downloaded media size and check [upload limits](/perceptron-mk1.5/guides/files). Byte limits and audio-token limits are separate. | An over-limit chat request returns HTTP 400 with `type: "invalid_request_error"` and `code: "audio_token_limit_exceeded"`; `param` can be `null`. Audio is rejected rather than silently truncated. This validation happens before generation, so a streaming request can also fail with an ordinary JSON error before any SSE events arrive. No soundtrack, or no soundtrack overlap with the processed video window, falls back to frames only. Decode failures and over-limit soundtracks still return errors. See [Audio](/perceptron-mk1.5/capabilities/audio) for valid content parts and [video soundtracks](/perceptron-mk1.5/capabilities/video-understanding#analyze-video-soundtracks) for the opt-in flag. ### Reasoning effort `reasoning_effort` accepts exactly `none`, `minimal`, `low`, `medium`, and `high`. In a direct HTTP request, an unknown value returns HTTP 400 with `invalid_request_error` and a parse-error message naming the field. The SDK rejects an invalid typed argument before HTTP with `BadRequestError(code="invalid_reasoning_effort")`. Correct the value before retrying. Chat completions and Multilook take this field at the top level; Detect uses `config.reasoning_effort`. A completed HTTP request with reasoning but an empty answer may have exhausted its output budget. Check `finish_reason`; if it is `length`, allow more tokens for reasoning and the answer, or use `none` if reasoning is unnecessary. See [Reasoning](/perceptron-mk1.5/capabilities/thinking). ### Function-calling requests | Code | Correction | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `unsupported_tool_choice` | Use `auto` or `none`; forcing a named function or `required` is unsupported. | | `unsupported_tools_combination` | Complete the tool workflow, then request a [structured final answer](/perceptron-mk1.5/capabilities/structured-outputs#combine-tools-with-a-constrained-final-answer) without tool declarations. | | `unsupported_parameter` | Check whether the selected model and endpoint support the parameter. Use chat completions for function calling. | | `missing_tool_result` | Return one result for every outstanding call before continuing the conversation. | | `invalid_tools` | Inspect the indicated declaration or history entry, including its call ID and arguments. | The [function-calling guide](/perceptron-mk1.5/guides/tool-calling#preserve-the-conversation) shows how to preserve assistant calls, reasoning, and matching tool results. ## Retry a transient inference failure This example uses the Perceptron SDK and adds an application retry policy. It makes up to three attempts and waits no more than 60 seconds between attempts. If the server asks for a longer wait, it returns the error to the caller instead of retrying early. The request timeout applies separately to each attempt. Install `perceptron>=0.4.0`, set `PERCEPTRON_API_KEY`, and save this as `retry_example.py`: ```python theme={null} import math import os import random import time from perceptron import Client, QuotaExceededError, SDKError, TimeoutError, TransportError client = Client( api_key=os.environ["PERCEPTRON_API_KEY"], provider="perceptron", timeout=30.0, ) RETRYABLE_STATUS = {408, 429, 500, 502, 503, 504} def retry_delay(retry_after, attempt): try: delay = float(retry_after) except (TypeError, ValueError): delay = -1 if math.isfinite(delay) and delay >= 0: return delay return 2 ** attempt + random.random() def complete_with_retries(messages): for attempt in range(3): try: return client.chat.completions.create( model="perceptron-mk1.5", messages=messages, max_completion_tokens=256, ) except QuotaExceededError: raise except (TimeoutError, TransportError): if attempt == 2: raise delay = retry_delay(None, attempt) except SDKError as error: if error.status_code not in RETRYABLE_STATUS or attempt == 2: raise delay = retry_delay(error.retry_after, attempt) if delay > 60: raise time.sleep(delay) if __name__ == "__main__": response = complete_with_retries([ {"role": "user", "content": "Give one tip for reviewing a video."}, ]) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError("The request did not return a complete text answer") print(choice.message.content or "") ``` Run `python retry_example.py`. The helper retries only the inference request. It does not automatically retry an incomplete answer, rerun application functions, or restart a whole agent workflow. Use the [workflow budget guidance](/perceptron-mk1.5/guides/tool-agents#bound-the-whole-workflow) when several requests and tool executions share a deadline. ## Handle incomplete responses and streams HTTP 200 means the request was accepted; check the completion as well: * `finish_reason: "stop"` marks a completed answer. * `finish_reason: "tool_calls"` marks a completed model turn asking your application to handle calls. * `finish_reason: "length"` means the output budget was exhausted. Keep any partial text clearly marked as incomplete; do not execute partial arguments or treat truncated JSON or annotations as validated output. A stream can fail after its HTTP headers have been sent. In that case the API sends an SSE data event containing `{"error": {...}}` and closes without `[DONE]`. The Perceptron SDK raises an `SDKError` subclass for that error event, and `IncompleteStreamError` when the stream ends without `[DONE]`. A raw SSE consumer must recognize the error event itself. A dropped connection or stream ending without a finish reason is also incomplete. Keep consuming after the finish chunk so you receive any trailing usage or error event. Do not splice a fresh retry onto an old partial stream. See the [tool-call streaming example](/perceptron-mk1.5/guides/tool-calling#stream-a-tool-call), [tracking stream example](/perceptron-mk1.5/capabilities/video-tracking#stream-a-tracking-answer), and [structured-output validation](/perceptron-mk1.5/capabilities/structured-outputs#validate-completed-streams). ## Handle Multilook partial failures A successful [Multilook request](/perceptron-mk1.5/guides/multilook) can return HTTP 200 while individual entries in `results` contain an `error`. Inspect each result before accessing its `completions`. The prompt error has a `message` and may include `type` and `code`. Keep successful results, and decide whether each failed prompt needs correction or a retry. Resubmit only the prompts you choose to retry, preserving their shared context and any prompt-specific media. Track their original indices in your application: `prompt_index` in the new response refers to the new request. Also check each returned completion's `finish_reason`; truncated output is not necessarily a prompt-level `error`. For example, a prompt-specific audio input can produce `results[].error.code: "audio_token_limit_exceeded"` while another prompt succeeds. Request-level validation, such as an invalid `reasoning_effort`, rejects the whole request before prompt processing. If every prompt fails, the API returns a whole-request error instead of a successful results body. Its status is classified from the first prompt error; unclassified failures return HTTP 502. ## Separate tool failures from API failures A database timeout or failed lookup inside your application is a tool execution failure. Return an explicit error result with the original `tool_call_id` if you want the model to continue; the [tool-agent guide](/perceptron-mk1.5/guides/tool-agents#return-failures-as-results) shows the message shape. Keep the assistant calls and completed tool results when retrying the next model request. Do not restart the whole tool loop blindly: a function that sends a message, creates a record, or changes an external system may already have succeeded. Decide how to reconcile or deduplicate that action in your application before repeating it. # Files Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/files Upload media once and reuse it across requests. Upload media to the Files API, then reference its `file-...` ID in requests. Reusing a file ID avoids uploading the same bytes again. Each occurrence still has its own position in the request's [asset ordering](/perceptron-mk1.5/guides/multiple-assets); file IDs and `asset_idx` serve different purposes. ## Upload and use a local image Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY` as shown in the [quickstart](/perceptron-mk1.5/index). Replace `./image.png` with a local PNG image. The SDK's `files.upload()` sends the bytes to the Files API and returns a `File`; pass that object to `image()` to reference its ID in a completion. ```python Python theme={null} from perceptron import Client, image client = Client(provider="perceptron") uploaded = client.files.upload("./image.png", purpose="vision") response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image(uploaded), {"type": "text", "text": "Describe the scene and its main objects."}, ], }], max_completion_tokens=1024, ) if not response.complete or response.tool_calls: raise RuntimeError(f"Incomplete answer: {response.finish_reason}") print(response.text) print("Reusable file ID:", uploaded.id) ``` ```bash curl theme={null} set -euo pipefail file_id="$( curl --fail-with-body --silent --show-error \ https://api.perceptron.inc/v1/files \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -F 'purpose=vision' \ -F 'file=@./image.png;type=image/png' \ | jq -er '.id' )" jq -n --arg file_id "$file_id" '{ model: "perceptron-mk1.5", messages: [{ role: "user", content: [ {type: "image_file_id", image_file_id: {file_id: $file_id}}, {type: "text", text: "Describe the scene and its main objects."} ] }], max_completion_tokens: 1024 }' | curl --fail-with-body --silent --show-error \ https://api.perceptron.inc/v1/chat/completions \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -H 'Content-Type: application/json' \ --data-binary @- ``` For a file already uploaded, use `image(file_id="file-...")` instead. Calling `image("./image.png")` directly embeds the local image in the completion request; it does not upload to the Files API. The same distinction applies to `video()` and `audio()`. The curl example also needs Bash and `jq`. The upload uses multipart form data; let `curl` set its content type and boundary. The completion response has the same shape as any chat completion: read `choices[0].message.content` and check `choices[0].finish_reason` before treating the answer as complete. ## Upload and use a local video Use `video(uploaded)` to send a `video_file_id` content part. Replace `./video.mp4` with a local video: ```python theme={null} from perceptron import Client, video client = Client(provider="perceptron") uploaded = client.files.upload("./video.mp4", purpose="vision") response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ video(uploaded), {"type": "text", "text": "Describe the main actions in chronological order."}, ], }], max_completion_tokens=1024, ) if not response.complete or response.tool_calls: raise RuntimeError(f"Incomplete answer: {response.finish_reason}") print(response.text) ``` The soundtrack is disabled by default. To analyze it alongside the frames, explicitly pass `vision_config={"enable_audio_in_video": True}` to the completion request. See [Video Q\&A](/perceptron-mk1.5/capabilities/video-qa) for soundtrack behavior. ## Upload and use local audio Audio uploads support these canonical content types: | Format | Content type | Accepted aliases | | ------ | ------------ | --------------------------- | | WAV | `audio/wav` | `audio/x-wav`, `audio/wave` | | MP3 | `audio/mpeg` | `audio/mp3` | | FLAC | `audio/flac` | `audio/x-flac` | Use the content type matching the file's actual bytes. Renaming a file does not convert its format. Replace `./recording.wav` with a local WAV recording: ```python Python theme={null} from perceptron import Client, audio client = Client(provider="perceptron") uploaded = client.files.upload("./recording.wav", purpose="vision") response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ audio(uploaded), {"type": "text", "text": "Transcribe the speech. Mark unclear words instead of guessing."}, ], }], reasoning_effort="none", max_completion_tokens=2048, ) if not response.complete or response.tool_calls: raise RuntimeError(f"Incomplete answer: {response.finish_reason}") print(response.text) ``` ```bash curl theme={null} set -euo pipefail file_id="$( curl --fail-with-body --silent --show-error \ https://api.perceptron.inc/v1/files \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -F 'purpose=vision' \ -F 'file=@./recording.wav;type=audio/wav' \ | jq -er '.id' )" jq -n --arg file_id "$file_id" '{ model: "perceptron-mk1.5", messages: [{ role: "user", content: [ {type: "audio_file_id", audio_file_id: {file_id: $file_id}}, {type: "text", text: "Transcribe the speech. Mark unclear words instead of guessing."} ] }], reasoning_effort: "none", max_completion_tokens: 2048 }' | curl --fail-with-body --silent --show-error \ https://api.perceptron.inc/v1/chat/completions \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -H 'Content-Type: application/json' \ --data-binary @- ``` `purpose=vision` is the accepted Files API purpose for media uploads, including audio. An `audio_file_id` input is analyzed without enabling video soundtracks. For an uploaded video's soundtrack, use `video_file_id` and set `vision_config.enable_audio_in_video: true` on the completion request. A successful upload does not guarantee that the recording fits the model's audio or context limits. Each audio item is limited to 16,384 encoder tokens, approximately 21.8 minutes at 750 tokens per minute; timestamp tokens and other request content also consume context. See [Audio](/perceptron-mk1.5/capabilities/audio) and [tokenization](/perceptron-mk1.5/guides/tokenization) before submitting a long recording. ## Manage uploaded files The SDK also provides `client.files.list()` for one page of files, `client.files.iter()` to iterate across pages, `client.files.retrieve(file_id)` for metadata, and `client.files.download(file_id, path)` to save the bytes locally. Call `client.files.delete(file_id)` when you no longer need a file; subsequent requests cannot use the deleted ID. See [Upload a file](/perceptron-mk1.5/api-reference/files/upload-a-file) for accepted formats and [Delete a file](/perceptron-mk1.5/api-reference/files/delete-a-file) for the deletion endpoint. ## Use presigned media uploads `POST /v1/media/upload-urls` also accepts WAV, MP3, and FLAC with the content types above. This is a separate upload flow from the Files API: 1. Send `files` entries containing `file_name`, `content_type`, and the exact byte `content_length` to `/v1/media/upload-urls`, using your Perceptron API key. 2. Upload the bytes with `PUT` to the returned `upload_url`, using the same `Content-Type` you declared. Do not send your Perceptron API key to the presigned URL. 3. Send the returned `object_key` in an `object_keys` array to `POST /v1/media/download-urls`, using your Perceptron API key. Use the returned `download_url` in an `audio_url` content part while it remains valid. An `object_key` is not a Files API `file-...` ID. Use `/v1/files` when you want to reference the recording with `audio_file_id`. ## Referencing a file in a request Reference an uploaded file in a chat completion by id with an `image_file_id` / `video_file_id` content part, or by passing its `/v1/files/{file_id}/content` URL as a standard `image_url` / `video_url`. All four forms are equivalent. ```json Image (file_id) theme={null} { "type": "image_file_id", "image_file_id": { "file_id": "file-abc123" } } ``` ```json Video (file_id) theme={null} { "type": "video_file_id", "video_file_id": { "file_id": "file-abc123" } } ``` ```json Image (URL) theme={null} { "type": "image_url", "image_url": { "url": "https://api.perceptron.inc/v1/files/file-abc123/content" } } ``` ```json Video (URL) theme={null} { "type": "video_url", "video_url": { "url": "https://api.perceptron.inc/v1/files/file-abc123/content" } } ``` ## Limits Files are capped at **128 MiB** each, with **100 GiB** of total storage per organization. Per-endpoint rate limits are listed on each API reference page. Need higher limits? [Contact us](mailto:support@perceptron.inc). # High Fidelity Object Tracking Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/high-fidelity-object-tracking Turn model waypoints into detailed video tracks with optical flow and appearance tracking. Use Perceptron Mk1.5 to identify an object and return timestamped boxes, then follow the object through the frames between those boxes. This combines the model's understanding of **what to track** with local tracking of **how it moves**. The workflow has two interpolation layers: **optical flow first**, followed by **appearance tracking where flow is weak**. Both use the video's pixels to estimate intermediate positions. A straight line between model boxes cannot capture a bounce or sudden turn. | Stage | What it contributes | | ---------------------------- | -------------------------------------------------------------------------------------- | | Perceptron video request | Sparse `` waypoints that establish the object and anchor boxes. | | Layer 1: optical flow | Motion estimates from visual features between nearby frames. | | Layer 2: appearance tracking | Additional estimates from matching the object's appearance when flow loses confidence. | The two local layers run in your application. They are not extra API parameters or built-in SDK interpolation methods. Start with one object and a short video; extend to multiple tracks once the result is reliable on your footage. ## Get model waypoints Install `perceptron>=0.4.0`, set `PERCEPTRON_API_KEY`, and save a short video containing a basketball as `clip.mp4`. Adapt the target description to your own footage: ```python theme={null} from perceptron import Client, video client = Client(provider="perceptron") response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ video("clip.mp4"), {"type": "text", "text": ( "Track the basketball while it is visible. Return one track with " "mention=\"basketball\" and asset_idx=\"0\". Use point_box children " "with timestamps in seconds and coordinates normalized to 0–1000. " "Include waypoints around changes in direction. Do not invent " "positions when the ball is occluded or off-screen." )}, ], }], vision_config={"annotation_format": "box"}, reasoning_effort="high", max_completion_tokens=4096, ) if not response.complete or response.finish_reason != "stop" or response.tool_calls: raise RuntimeError(f"Incomplete tracking answer: {response.finish_reason}") print(response.text) ``` `video()` encodes a local file inline. For larger or reused videos, [upload once](/perceptron-mk1.5/guides/files) and pass the returned file to `video()`. See [Video tracking](/perceptron-mk1.5/capabilities/video-tracking) for the annotation format and streaming. Treat the model's boxes as anchors. For several objects, keep each `` separate; identical labels do not establish that two tracks are the same object. `asset_idx` is optional: an omitted selector here resolves to the only supplied video, asset `0`. Run this after the request above. It expects one complete box track and produces `anchors`, sorted by time: ```python theme={null} import math from perceptron import BoundingBox tracks = response.annotations(strict=True).tracks if len(tracks) != 1 or not tracks[0].complete: raise ValueError("Expected one complete track; inspect the model answer") anchors = [] for waypoint in tracks[0].points: if not isinstance(waypoint, BoundingBox) or response.resolve_asset_idx(waypoint) != 0: raise ValueError("Expected boxes on the supplied video") t = waypoint.t x1, y1 = waypoint.top_left.x, waypoint.top_left.y x2, y2 = waypoint.bottom_right.x, waypoint.bottom_right.y if t is None or not all(math.isfinite(v) for v in (t, x1, y1, x2, y2)): raise ValueError("Each anchor needs finite coordinates and a timestamp") if t < 0 or not (0 <= x1 < x2 <= 1000 and 0 <= y1 < y2 <= 1000): raise ValueError("Invalid timestamp or normalized box") anchors.append({"t": t, "box": [x1, y1, x2, y2]}) anchors.sort(key=lambda anchor: anchor["t"]) if len(anchors) < 2 or any(a["t"] == b["t"] for a, b in zip(anchors, anchors[1:])): raise ValueError("Interpolation needs at least two distinct anchor times") print(anchors) ``` `strict=True` raises reported parse errors; it is not a complete markup validator. Retain the raw answer and apply the [annotation validation guidance](/perceptron-mk1.5/guides/rendering-annotations). Also check anchor times against the video's duration and inspect whether the boxes follow the intended object before using them. ## Align frames and coordinates Process each pair of adjacent anchors within the same track. Start with short intervals—for example, no more than one second—and split at known occlusions or scene cuts. This interval length is a tuning choice for your application, not an API limit. Decode the actual intervening frames and retain their presentation timestamps. Model timestamps are in seconds; do not derive frame times from a guessed frame rate, especially for variable-frame-rate video. Map each anchor to its decoded frame and skip interpolation if both anchors fall in the same frame. The local trackers operate in pixels. Convert the model's 0–1000 coordinates using the decoded frame's dimensions. For lower processing cost, use one fixed crop covering both anchor boxes with padding, resize that crop consistently for the whole interval, and transform both anchor boxes into its coordinate system. Keep enough surrounding area for the object's path to remain inside the crop. Save the crop and resize transform so you can [map results back to the full frame](/perceptron-mk1.5/concepts/coordinates#map-a-crop-back-to-the-original-image). ## Layer 1: follow local motion Use sparse Lucas–Kanade optical flow as the first pass. Detect feature points inside the starting box, follow them through successive frames, and estimate how their motion changes the box. A practical implementation: 1. Select features inside the object with Shi–Tomasi corner detection. 2. Track those points into the next frame with pyramidal Lucas–Kanade flow. 3. Track them back to the previous frame. Reject points that do not return near their starting position. 4. Fit a robust translation/scale/rotation transform to the surviving points, rejecting outliers. Apply the accumulated transform to the original anchor box corners. 5. Lower confidence when too few points survive, points disagree, or motion and size change implausibly. Run this twice for each interval: forward from the earlier model box and backward from the later one. Reset tracking state at the next anchor pair, so errors do not accumulate through the entire video. OpenCV's [Lucas–Kanade example](https://github.com/opencv/opencv/blob/4.x/samples/python/lk_track.py) shows feature detection and forward/backward verification using `goodFeaturesToTrack()` and `calcOpticalFlowPyrLK()`. You still need to turn feature motion into box estimates and a confidence score. ## Layer 2: recover with appearance If optical flow is missing or weak on any interior frame, run an appearance tracker such as **NanoTrack** over that interval. It matches an object template against nearby frames, providing another source of evidence when feature points are scarce or lost. Initialize one tracker with the earlier model box and another with the later box. Run them in opposite directions, with independent templates and state. Start from the model anchors, rather than using a drifting flow box as the new reference. Keep the endpoints fixed. OpenCV's [TrackerNano](https://docs.opencv.org/4.x/d8/d69/classcv_1_1TrackerNano.html) provides initialization, frame updates, and a tracking score. It requires separate backbone and localization model files, linked from that reference. Tracker scores and thresholds depend on the implementation; calibrate them on representative videos before combining them with optical-flow confidence. Appearance tracking can also drift onto a similar object. A high score alone does not prove identity, and running the second layer does not guarantee every gap can be filled. ## Combine and render the results Use the first and last decoded frame presentation timestamps as `start_time` and `end_time`. At an intermediate frame time `t`, let `u = (t - start_time) / (end_time - start_time)`. Weight the forward prediction by `(1 - u) × forward_confidence` and the backward prediction by `u × backward_confidence`. This favors the nearer anchor while retaining evidence from both directions. Check agreement before accepting a combined box. Intersection over union (IoU) measures the overlap of the two boxes; low overlap should reduce confidence. When predictions agree, blend their centers and sizes. Blending width and height in log space gives a smooth transition in scale. Preserve the original model boxes exactly at the endpoints. Use the same agreement check across the two tracking layers: keep reliable flow, combine agreeing flow and appearance estimates, and accept a conflicting winner only when its confidence is strong enough and clearly higher. Otherwise, leave that frame without a box. Missing reverse evidence should lower confidence too. This **pseudocode** summarizes the orchestration. The tracker and fusion operations are application code, not Perceptron SDK functions: ```text theme={null} for each adjacent anchor pair in one track: if the interval is too long or crosses a known visibility break: keep the anchors and leave the interior unavailable continue frames = decode the interval with its real presentation timestamps flow = track forward and backward from the two anchor boxes if any interior flow estimate is missing or has low confidence: appearance = track forward and backward with independent templates else: appearance = unavailable for each interior frame: combine trustworthy estimates using time, confidence, and box agreement if evidence is ambiguous, store no box for that frame preserve both original anchor boxes ``` Keep each output's timestamp, box, confidence, and origin: `model`, `flow`, `appearance`, or `fused`. These local estimates are not additional model observations. Do not draw a straight-line fallback through rejected frames or extrapolate beyond the first and last anchors. Render using the video's media time and the displayed video's dimensions, including any letterbox offsets. Keep missing intervals missing during playback. This avoids showing a smooth but unsupported path after tracking has failed. ## Recover a gap with new model observations When both layers fail, select a frame near the middle of the uncertain interval and ask Perceptron to locate the object again. A nearby clear reference frame plus its known box can help identify the target. Send the reference and target as separate images, validate the returned target box, and associate it with the target frame's original video timestamp. Those images have their own request-local asset indices; they are not the original video's selector. See [Multiple assets](/perceptron-mk1.5/guides/multiple-assets). If you used crops, map the coordinates back before adding the new anchor. Re-run the shorter intervals around an accepted anchor. If the object is occluded or identity remains uncertain, keep the gap. Start by reviewing a few difficult clips: fast motion, low texture, occlusion, camera cuts, and similar-looking objects. Adjust interval length and confidence gates based on whether boxes stay on the intended object, not merely whether the overlay looks smooth. # MCP Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/mcp Connect an MCP client to Mk1.5 image, video, audio, and reasoning tools. Perceptron's hosted MCP server lets an AI assistant ask questions about media, generate captions, detect objects, and read text. Connect directly over Streamable HTTP for image, video, audio, and reasoning-effort controls. The optional local npm bridge also uploads local files, with the version limits described below. ## Before you begin You need an MCP client and a Perceptron API key. Direct hosted connections do not require Node.js. The local npm bridge requires **Node.js 24 or later**. Get your key from the Perceptron platform Get help and see what others are building ## Connect directly to hosted MCP Add a remote server in a client that supports Streamable HTTP and custom authorization headers: | Setting | Value | | ------------ | -------------------------------- | | Transport | Streamable HTTP | | URL | `https://api.perceptron.inc/mcp` | | Header name | `Authorization` | | Header value | `Bearer YOUR_API_KEY` | Replace `YOUR_API_KEY` with your Perceptron key using the client's secret or private configuration. Do not commit credentials with the project. Your MCP client controls when tools can run and any approval prompts it shows. Connect the server and refresh its tools in your client. The examples in this section are **hosted MCP tool arguments**, not chat-completions request bodies. Hosted calls accept accessible media URLs or supported data URLs; they cannot read a path on your computer. ### Select a model The hosted service defaults to `perceptron-mk1.5` when `model` is omitted. This replaces the previous Mk1 default; pass `"model": "perceptron-mk1"` to keep the previous model. These examples set `"model": "perceptron-mk1.5"` explicitly so the choice remains stable if the default changes again. A previous tool call does not select the model for the next call. The model used by your assistant is also independent of the model used by a Perceptron tool. Model discovery is available through MCP resources at `perceptron://models/{modelId}`. ### Choose a hosted tool | Tool | Inputs | Task controls | | ------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------- | | `question` | `media_url`, `question`; `modality`: image, video, or audio | Optional `output_format`: point, box, polygon, or clip | | `caption` | `media_url`; `modality`: image, video, or audio | Optional `style`: concise (default) or detailed; `output_format` | | `detect` | `media_url`; `modality`: image or video | Optional `classes`: a list of object labels | | `ocr` | `image_url`, `mode`; image-only | `mode`: plain, markdown, or html; optional `prompt` | | `generate_upload_urls` | `files`: file names, MIME types, and byte lengths | Returns URLs for uploading media | | `generate_download_urls` | `object_keys` from the upload response | Returns media URLs to use in an inference tool | For hosted `question`, `caption`, and `detect` calls, `modality` defaults to `image`. Set it explicitly when sending video or audio. Each inference call accepts one media input. All four inference tools accept optional `model`, `max_tokens`, `temperature`, and `reasoning_effort` settings. ### Ask about audio Call `question` with `modality: "audio"`. Replace the example URL with an accessible WAV, MP3, or FLAC recording: ```json theme={null} { "media_url": "https://example.com/recording.wav", "modality": "audio", "model": "perceptron-mk1.5", "question": "Summarize the speech and describe any other audible sounds.", "reasoning_effort": "high", "max_tokens": 2048 } ``` `caption` also accepts audio. `detect` and `ocr` do not accept audio inputs. Omit spatial output formats for an audio-only question. ### Include a video's soundtrack Video soundtracks are disabled by default. To analyze sound together with frames, set `enable_audio_in_video: true` on a hosted `question` or `caption` call with `modality: "video"`: ```json theme={null} { "media_url": "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/main/cookbook/_shared/assets/capabilities/video-clipping/mj_shot_short.mp4", "modality": "video", "model": "perceptron-mk1.5", "question": "Describe what happens and what can be heard in this clip.", "enable_audio_in_video": true, "reasoning_effort": "high", "max_tokens": 2048 } ``` This sample contains a soundtrack; replace its URL with your video as needed. Setting the flag to `true` on an image or audio-only input returns a tool error. The flag is available only on `question` and `caption`. The [audio limits and usage rules](/perceptron-mk1.5/capabilities/audio#understand-usage-and-limits) also apply to recordings and soundtracks sent through MCP. ### Set reasoning effort The four hosted inference tools accept `reasoning_effort`: `none`, `minimal`, `low`, `medium`, or `high`. Any value other than `none` enables reasoning. The older boolean `reasoning` field is deprecated; omit it when using `reasoning_effort`. The JSON result inside an MCP text block uses `content` for the answer and `reasoning` for reasoning when returned. More reasoning consumes output budget and does not guarantee a more accurate answer. See [Reasoning](/perceptron-mk1.5/capabilities/thinking) for guidance on choosing an effort level. ### Upload media for a hosted call For a file on your computer, your application can use the hosted upload helpers: 1. Call `generate_upload_urls` with a `files` array. Each entry contains `file_name`, `content_type`, and `content_length` in bytes. 2. Upload the actual bytes to each returned `upload_url` using HTTP `PUT` and the matching `Content-Type`. Upload files sequentially. 3. Call `generate_download_urls` with the returned `object_key` values in its `object_keys` array, then pass a returned `download_url` as `media_url` or `image_url` to the chosen inference tool. For audio uploads, use `audio/wav` for WAV, `audio/mpeg` for MP3, or `audio/flac` for FLAC. Existing image and video MIME types are `image/jpeg`, `image/png`, `image/webp`, `video/mp4`, and `video/webm`. A MIME type must match the file's actual encoding. The hosted server does not upload a local path automatically. The application's ability to read and upload files depends on the client it uses. ## Use the local npm bridge The currently published bridge, `@perceptron-ai/mcp-server@0.2.0`, supports image and video inputs. It does not support audio inputs, `enable_audio_in_video`, or `reasoning_effort`. Version `0.3.0` with these additions is forthcoming. Use the direct hosted connection above for those features until that version is published. With Node.js 24 or later installed, add a local server using the command `npx` and arguments `-y`, `@perceptron-ai/mcp-server@0.2.0`. Supply `PERCEPTRON_API_KEY` in its environment. For clients that use an `mcpServers` configuration: ```json theme={null} { "mcpServers": { "perceptron": { "command": "npx", "args": ["-y", "@perceptron-ai/mcp-server@0.2.0"], "env": { "PERCEPTRON_API_KEY": "YOUR_API_KEY" } } } } ``` Replace `YOUR_API_KEY` using your client's private configuration. Restart or reconnect the server after changing its environment. The bridge connects to the hosted MCP endpoint above and uses the same hosted model default. ### Call a bridge tool Version `0.2.0` exposes `question`, `caption`, `detect`, and `ocr`, plus a `list_models` helper. Unlike the hosted schema, the bridge requires an explicit `modality` of `"image"` or `"video"` for `question`, `caption`, and `detect`. `ocr` remains image-only and requires `image_url` and `mode`. For example, call `question` with: ```json theme={null} { "media_url": "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp", "modality": "image", "model": "perceptron-mk1.5", "question": "Describe this coastal scene and the visible objects.", "max_tokens": 1024 } ``` The bridge accepts optional generation settings including `max_tokens`, `temperature`, and the older `reasoning` boolean. Use the hosted connection to set `reasoning_effort`. For example, call `ocr` with these arguments, replacing the path with your receipt image: ```json theme={null} { "image_url": "/absolute/path/to/receipt.png", "mode": "markdown", "model": "perceptron-mk1.5", "max_tokens": 2048 } ``` OCR modes are `plain`, `markdown`, and `html`. Choose a mode explicitly. ### Supply a local file The bridge accepts HTTP(S) URLs, supported base64 data URLs, and local image or video paths. Prefer an absolute path: a relative path is resolved from the server process's working directory, which may differ from your project directory. For a local path, the bridge reads the file and uploads its bytes before making the vision request. This keeps the base64 payload out of the assistant's conversation, but the media is still sent to the hosted service. A remote URL must be accessible to that service. ## Handle results For both connection methods, inspect a tool's `isError` flag before treating its content as a successful answer. Results contain text and may include grounded annotations. Use the [chat API](/perceptron-mk1.5/guides/python/request-basics) when you need multi-asset conversation histories, [native tracking](/perceptron-mk1.5/capabilities/video-tracking), or [streaming](/perceptron-mk1.5/capabilities/video-tracking#stream-a-tracking-answer). MCP vision tools are separate from Mk1.5's native [function calling](/perceptron-mk1.5/guides/tool-calling). To give Mk1.5 functions from your own application, declare them in a chat request's `tools` field and execute its returned calls in your application. ## Troubleshoot the connection * **A hosted connection fails:** check that the client supports Streamable HTTP and sends the bearer authorization header to `/mcp`. * **The local bridge does not start:** check `node --version` is 24 or later, `npx` is on the client's executable path, and `PERCEPTRON_API_KEY` is set in the server environment. * **The wrong model answers:** inspect the actual tool arguments and include `model: "perceptron-mk1.5"` in each call. * **Audio or effort settings do not work through the bridge:** use the hosted connection; bridge version `0.2.0` does not expose these controls. * **A local file fails:** hosted calls need a URL or data URL. For the bridge, use an existing absolute path readable by its process, with a file extension matching its media format. * **The service returns an error:** inspect the tool error and use [Error messages](/perceptron-mk1.5/guides/error-messages) to distinguish request problems from transient failures. See the [MCP server repository](https://github.com/perceptron-ai-inc/mcp-server) for package details and client-specific configuration examples. # Multilook Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/multilook Ask independent questions over shared media in one request. Use Multilook when several independent prompts share the same images, video, or audio. The endpoint reuses the shared context within the call and returns one result per prompt. For one question involving several media items, use ordinary chat completions with [multiple assets](/perceptron-mk1.5/guides/multiple-assets) instead. ## Send independent prompts Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY` as shown in the [quickstart](/perceptron-mk1.5/index). ```python Python theme={null} from perceptron import Client, video client = Client(provider="perceptron") response = client.chat.completions.multilook( model="perceptron-mk1.5", context=[{ "role": "user", "content": [video( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/tutorials/isaac_frame_by_frame/surf.mp4" )], }], prompts=[ "Describe the setting in one sentence.", "Describe the surfers' actions in chronological order.", ], n=1, reasoning_effort="high", max_completion_tokens=1024, ) for result in response.results: if not result.ok: error = result.error.message if result.error else "No completions returned" print(f"Prompt {result.prompt_index} failed: {error}") continue for completion in result.completions: if not completion.complete: print(f"Prompt {result.prompt_index} incomplete: {completion.finish_reason}") continue print(result.prompt_index, completion.text) if response.usage is not None: print("Input tokens:", response.usage.prompt_tokens) print("Cached input tokens:", response.usage.cached_tokens) print("Audio input tokens:", response.usage.audio_tokens) ``` ```bash curl theme={null} curl https://api.perceptron.inc/v1/chat/completions/multilook \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "perceptron-mk1.5", "context": [{ "role": "user", "content": [{ "type": "video_url", "video_url": {"url": "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/main/cookbook/_shared/assets/tutorials/isaac_frame_by_frame/surf.mp4"} }] }], "prompts": [ "Describe the setting in one sentence.", "Describe the surfers actions in chronological order." ], "n": 1, "reasoning_effort": "high", "max_completion_tokens": 1024 }' ``` Each entry in `results` identifies its `prompt_index` and contains either `completions` or an `error`. Inspect each result separately; one prompt can fail while another succeeds. Check each completion's `finish_reason` before treating its content as complete. The SDK exposes `completion.text`, `completion.reasoning`, and `completion.complete`. For Multilook, `complete` accepts `stop` and `interrupted`; a `length` finish is incomplete. Request-level failures raise an SDK exception; per-prompt failures remain in `response.results`. ## Set reasoning for the whole request Set the top-level `reasoning_effort` to `none`, `minimal`, `low`, `medium`, or `high`. A tier other than `none` enables reasoning; `minimal` currently maps to `low`. The selected tier applies to **every prompt and all `n` completions** in the request. Per-prompt effort settings are not supported; group prompts into separate requests if they need different tiers. The example uses `high` for both questions. Each completion keeps its final answer in `message.content` and any returned reasoning in `message.reasoning_content`. Reasoning consumes that completion's `max_completion_tokens` budget, so leave room for the answer and check its `finish_reason`. The SDK rejects an unsupported effort value before sending the request. If sent directly to the API, it rejects the whole request with HTTP 400 and a message naming `reasoning_effort`, rather than returning a separate error for each prompt. The deprecated `vision_config.enable_thinking` flag, when supplied, overrides whether reasoning is enabled for every prompt. `false` keeps reasoning off even with `high`; `true` enables reasoning even with `none`, which then leaves the tier unpinned. Omit the old flag when using `reasoning_effort`. See [Reasoning](/perceptron-mk1.5/capabilities/thinking) for output handling and effort selection. ## Audio and video soundtracks Shared user context and structured prompts accept `input_audio`, `audio_url`, and `audio_file_id` parts. For example, put a recording in the shared context and ask separate prompts for a transcript and a summary. See [Audio](/perceptron-mk1.5/capabilities/audio) for the input shapes and supported formats. For videos with soundtracks, set top-level `vision_config: {"enable_audio_in_video": true}`. This opt-in applies across the request's video parts; standalone audio does not need it. Audio token limits still apply, and individual prompts can fail with `audio_token_limit_exceeded`; inspect every result's `error` before using its completions. ## Context and asset indexing Each prompt sees the shared `context` followed by its own content. Prompts do not see each other's inputs or answers. Compute `asset_idx` separately for each context-plus-prompt sequence: shared assets come first, followed by that prompt's media. Put media reused across prompts in `context`. A prompt can also contain its own content parts, but that media is outside the reusable prefix. File references work in both places. For SDK responses, call `completion.annotations()` to parse an answer and `completion.resolve_asset_idx(annotation)` to resolve one of its flattened annotations. The completion retains its own context-plus-prompt asset count, including the last-asset fallback when `asset_idx` is omitted. ## Limits and compatibility * Send up to 16 prompts. `n` can be 1–8, with at most 64 completions per call. `n > 1` requires a positive `temperature`. * Multilook does not stream and does not support `response_format` or `stop`. * It does not support function declarations, `tool_choice`, `parallel_tool_calls`, assistant tool calls, or tool-result history. Use chat completions for a [tool loop](/perceptron-mk1.5/guides/tool-calling). * Keep dependent questions in a multi-turn chat; independent Multilook prompts cannot consume another prompt's answer. The response reports aggregate `usage`. `prompt_tokens_details.cached_tokens` is the subset of prompt tokens served from the shared prefill within that request. See the [Mk1.5 pricing table](/perceptron-mk1.5/models/perceptron-mk1.5#pricing) for the cache-read rate. Reuse within one call does not promise a cache hit on a later request. Aggregate `usage.prompt_tokens_details.audio_tokens` reports audio input across the call and is already part of `prompt_tokens`. Multilook reports zero if the upstream audio breakdown is absent. Per-prompt usage contains completion-token counts, so use the call-level usage once for input accounting. # Multiple assets Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/multiple-assets Ground answers across media and conversation history with asset_idx. Send multiple media parts in a conversation to compare scenes, locate the same object in different inputs, or use a reference image to guide a search. The optional `asset_idx` attribute identifies which input an annotation describes. The model may omit it, including in multi-asset answers and when the prompt asks for it. If no explicit or inherited selector is present, the annotation refers to the last asset available to the model at that point in the conversation. ## How assets are numbered Assets are numbered from zero in their order of appearance across the messages supplied in the request, then within each message's content array. An annotation refers to the media available up to its position in that conversation. Text does not consume an index. Images, videos, and standalone audio share one sequence. | Supplied conversation content | Asset index | | ------------------------------------------- | ------------ | | First user message: reference image | `0` | | First user message: video | `1` | | Assistant answer containing annotation text | No new asset | | Later tool result containing an image | `2` | | Later user message containing another image | `3` | Each media occurrence has a position. Repeating the same URL or uploaded `file_id` in a separate content part introduces another position; file reuse does not make the selector a persistent file identifier. Keep these distinctions in mind: * One `video_url`, `video_file_id`, or `video_frames` group is one asset. Individual frames inside that group are addressed by time. * One `input_audio`, `audio_url`, or `audio_file_id` part is one asset. For an image followed by a standalone recording, the image is asset `0` and the audio is asset `1`. Enabling a video's soundtrack does not add another asset; its frames and audio belong to the same video. * Frames supplied as separate `image_url` parts are separate image assets, even if they came from the same video. * Images returned by functions count in the conversation's asset order. The original image and an image crop returned by a tool are separate assets with their own coordinate systems. * Numbering does not reset at a new user turn. Resend the relevant conversation history when continuing a conversation. * Removing or reordering earlier media changes the mapping. Recompute selectors for the actual request instead of reusing an index from a different history. The request limit is 256 media units. This is a separate count from `asset_idx`: every frame inside a `video_frames` group consumes one media unit, although the group has only one asset index. ## Compare a reference image with a target Install the Perceptron Python SDK and set `PERCEPTRON_API_KEY`. This example uses two public sample images and requests a box in the second image. ```bash theme={null} pip install "perceptron>=0.4.0" export PERCEPTRON_API_KEY="your-api-key" ``` ```python theme={null} import os from perceptron import Client, image client = Client( api_key=os.environ["PERCEPTRON_API_KEY"], provider="perceptron", ) asset_root = ( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/in-context-learning/single/" ) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[ { "role": "user", "content": [ {"type": "text", "text": "Reference object (asset 0):"}, image(asset_root + "cake_mixer_example.webp"), {"type": "text", "text": "Search this image (asset 1):"}, image(asset_root + "find_kitchen_item.webp"), { "type": "text", "text": ( "Find the object in asset 1 that matches the reference in asset 0. " "Return a point_box with asset_idx=\"1\". " "If no matching object is visible, say so without inventing a box." ), }, ], } ], max_completion_tokens=512, vision_config={"annotation_format": "box"}, ) choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Incomplete answer: {choice.finish_reason}") print(choice.message.content or "") ``` The response is text containing annotation markup, not a separate JSON array of boxes. An illustrative annotation is: ```html theme={null} (400,300) (600,650) ``` The coordinates above demonstrate the format; they are not measured output for these sample images. ## Resolve selectors before drawing Select the asset first, then use that asset's dimensions to convert normalized coordinates into pixels. For example, `(500,500)` is `(960,540)` on a 1920×1080 image and `(320,240)` on a 640×480 image. An annotation can inherit its selector from a collection. An explicit child selector overrides the collection's selector: ```html theme={null} (100,150) (300,350) (400,300) (600,500) ``` Here the first box belongs to asset `0` and the second to asset `1`. This override is useful for collections; each `` must stay within one asset. For tracking the same object in two videos, request two tracks with explicit selectors. Their presence is not guaranteed. Resolve an annotation's asset in this order: 1. Use its explicit `asset_idx`, if present. 2. Otherwise, use an inherited selector from its collection or track. 3. If neither is present, use the index of the **last asset available when that annotation was produced**: `number_of_available_assets - 1`. In the reference-plus-target example above, a box without `asset_idx` refers to target image `1`. With only one media asset, the default is `0`. Count media from earlier supplied messages and tool results up to that annotation's position, across all modalities. For example, suppose the user supplies images A (`0`) and B (`1`), and the assistant returns a box without a selector. That box refers to B. If a later tool result supplies image C (`2`), an omitted selector in the next assistant answer refers to C. The earlier box still refers to B; adding C does not change its binding. Store each answer with the asset mapping available when it was produced. Validate explicit and inherited selectors against the asset range available at that annotation's position. An out-of-range or malformed selector is invalid; do not treat it as omitted or replace it with the default. Preserve explicit `0` by testing for a missing value rather than using a truthiness check. In the SDK, `response.annotations(strict=True)` applies inherited selectors to parsed annotations. Use `response.resolve_asset_idx(annotation)` to resolve the remaining omissions using the asset count captured for that answer. Keep that response with its original asset mapping; do not resolve an older annotation using a newer response. Check the selected asset's modality before drawing geometry. If an image is followed by standalone audio, an omitted selector resolves to the audio, not automatically to the last image. Request an explicit selector for image annotations and handle an incompatible or missing visual target instead of drawing on a different asset. ## Multilook uses a separate sequence for each prompt Each Multilook prompt sees the shared context followed by its own content. Asset numbering follows that individual sequence, not the entire batch. | Shared context | Prompt content | Selectors in that result | | ------------------- | ----------------------- | -------------------------- | | One reference image | Prompt A: query image A | Reference `0`, query A `1` | | One reference image | Prompt B: query image B | Reference `0`, query B `1` | Both query images are asset `1` in their respective responses. An omitted selector defaults to the last asset in that prompt's context-plus-prompt sequence, so it also selects asset `1` in both examples. Keep the response's `prompt_index` alongside the asset selector when associating results with inputs. The request-wide media-unit limit still includes the shared context and all prompts. Multilook is for independent prompts. Function calling and tool-result histories belong on `/v1/chat/completions`; see [tool calling](/perceptron-mk1.5/guides/tool-calling). ## Next steps * Read the [annotation format](/perceptron-mk1.5/concepts/annotations) for geometry, collection, and time syntax. * Use [video tracking](/perceptron-mk1.5/capabilities/video-tracking) to follow objects within each video. * Follow [High Fidelity Object Tracking](/perceptron-mk1.5/guides/high-fidelity-object-tracking) to combine a video's model waypoints with local motion and appearance tracking. # Authentication Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/python/auth Configure a Perceptron API key for Python and HTTP requests. Every API request needs a Perceptron API key. Use a key from the organization that owns the files and other resources your application accesses. Get your key from the Perceptron platform Get help and see what others are building ## Set the key in your environment For Bash or Zsh: ```bash theme={null} export PERCEPTRON_API_KEY="your-api-key" ``` For Windows PowerShell: ```powershell theme={null} $env:PERCEPTRON_API_KEY="your-api-key" ``` Start your Python process from that terminal. A notebook or editor that was already running may need its kernel or process restarted to receive the new environment variable. ## Configure the Python client The Perceptron SDK reads `PERCEPTRON_API_KEY`. You can also pass the key explicitly. Select the Perceptron provider for these guides: ```python theme={null} import os from perceptron import Client client = Client( api_key=os.environ["PERCEPTRON_API_KEY"], provider="perceptron", timeout=60.0, ) ``` Reuse this client for requests with the same credentials. Create a separate client when using another organization's key. The key is read when this code runs; changing the environment later does not update an existing client. `timeout` is a client setting, not a model parameter. The SDK does not automatically retry requests; its `retries` setting is accepted for compatibility but does not enable retries. See [Error messages](/perceptron-mk1.5/guides/error-messages) before adding a retry policy. ## Authenticate an HTTP request Direct HTTP requests use the `Authorization` header. For example, list the models: ```bash theme={null} curl --fail-with-body --silent --show-error \ https://api.perceptron.inc/v1/models \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" ``` Do not put the key in the URL, message content, or a media URL. Keep it in a server-side environment or secret store, exclude secret files from source control, and avoid logging authorization headers. Browser and mobile applications should call your backend rather than embed the API key in distributed code. ## Resolve authentication problems | Symptom | Check | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Python raises `KeyError: 'PERCEPTRON_API_KEY'` | Set the variable in the environment of the process running Python. No API request has been made. | | `401` | Supply a valid, active Perceptron key in the bearer header. Check for copied whitespace or a revoked key. | | `403` | Check the organization's access to the requested resource. | | `429` | Check the error type: rate limiting and exhausted credits need different responses. | | A request goes to another provider | Set `provider="perceptron"`, check any configured `base_url` or `PERCEPTRON_BASE_URL`, and pass the Perceptron key to this client. | For error fields, diagnostic headers, and retry behavior, see [Error messages](/perceptron-mk1.5/guides/error-messages). # Python FAQs Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/python/faqs Common questions about Python requests, media, annotations, and async execution. ## Which model ID should I use? Use `model="perceptron-mk1.5"` for the workflows in this version of the documentation. Configure the client with Perceptron's API URL and key as shown in [Python setup](/perceptron-mk1.5/guides/python/getting-started). ## Can I pass a local image, video, or audio path? Yes, wrap the path with `image()`, `video()`, or `audio()` in a message’s content list. The SDK reads the file and encodes it inline; the API never reads a path on your computer. To upload once and reuse the media, call `client.files.upload()` and pass the returned `File` to the matching helper. See [Files](/perceptron-mk1.5/guides/files). A remote URL must return accessible media bytes; a link to an authenticated viewing page is not enough. The [MCP bridge](/perceptron-mk1.5/guides/mcp) has separate local-file handling: it reads and uploads the file before making a vision request. ## How do I ask a follow-up question? Resend the conversation history, including the relevant media and prior assistant messages. The API does not recover earlier turns automatically. Preserve complete assistant messages in your client-side history, including `reasoning_content` and function calls when present. Historical reasoning is passed to the model for requests with tool declarations or tool traffic; the gateway omits it from ordinary non-tool requests. See [Request basics](/perceptron-mk1.5/guides/python/request-basics). ## Why are coordinates in the answer text? Spatial and temporal annotations remain available as markup in `response.text`. Call `response.annotations(strict=True)` to parse boxes, points, polygons, clips, and tracks, then `response.resolve_asset_idx(annotation)` to select the media from that request. Use that asset’s dimensions when [converting to pixels](/perceptron-mk1.5/concepts/coordinates). Parsing does not verify that the prediction is correct. Use [Rendering annotations](/perceptron-mk1.5/guides/rendering-annotations) for a worked example. For video, preserve timestamped observations and object identity as described in [Video tracking](/perceptron-mk1.5/capabilities/video-tracking). ## Can I send several assets together? Yes. Put the media parts in the message's content list and keep their order stable across follow-up requests. The optional `asset_idx` attribute is the zero-based index of a media occurrence available at that point in the conversation; it is not a file ID or video frame index. Numbering continues across messages and turns. If the attribute is neither explicit nor inherited, it defaults to the last asset available at that point. Media added later does not change the target of an earlier annotation. See [Multiple assets](/perceptron-mk1.5/guides/multiple-assets) for the ordering rules. ## How do I make asynchronous requests? Use `AsyncClient` and await the request. This complete text example uses the same API settings as the synchronous client: ```python theme={null} import asyncio import os from perceptron import AsyncClient async def main(): client = AsyncClient( api_key=os.environ["PERCEPTRON_API_KEY"], provider="perceptron", timeout=60.0, ) response = await client.chat.completions.create( model="perceptron-mk1.5", messages=[{"role": "user", "content": "Reply with a brief greeting."}], max_completion_tokens=128, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"No completed text answer: {choice.finish_reason}") print(choice.message.content or "") if __name__ == "__main__": asyncio.run(main()) ``` In a notebook with an event loop already running, use `await main()` instead of `asyncio.run(main())`. Bound concurrent requests as described in [Scaling](/perceptron-mk1.5/guides/scaling). ## Does receiving text mean the response is complete? No. Check the final `finish_reason` before using an answer. A `length` finish means the output was truncated. Streaming consumers must also handle empty-choice usage chunks, nullable deltas, and API errors after streaming has begun. See the [tracking stream example](/perceptron-mk1.5/capabilities/video-tracking#stream-a-tracking-answer) and [Error messages](/perceptron-mk1.5/guides/error-messages). For JSON output, parse and validate only a successfully completed result; see [Structured outputs](/perceptron-mk1.5/capabilities/structured-outputs). ## Does the client execute function calls? Your application validates and executes the returned calls, then sends tool results with the matching call IDs. Neither the chat API nor the Perceptron SDK runs your Python functions automatically. Use the complete [tool-calling loop](/perceptron-mk1.5/guides/tool-calling), including its streaming variant, and [workflow limits](/perceptron-mk1.5/guides/tool-agents). # Get started with Python Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/python/getting-started Set up a Python environment and send a complete Mk1.5 image request. Use the Perceptron Python SDK to send image, video, and audio messages to the hosted API. These guides use SDK 0.4.0 or later. You do not need a local GPU. ## Set up your environment Create a virtual environment, activate it, and install the client: ```bash theme={null} python3 -m venv .venv source .venv/bin/activate python -m pip install --upgrade "perceptron>=0.4.0" export PERCEPTRON_API_KEY="your-api-key" ``` On Windows PowerShell, activate with `.venv\Scripts\Activate.ps1` and set the key with `$env:PERCEPTRON_API_KEY="your-api-key"`. See [Authentication](/perceptron-mk1.5/guides/python/auth) for key configuration and troubleshooting. ## Send an image request Save this as `first_request.py`. It uses a public sample image and checks that the model finished its answer before printing it: ```python theme={null} import os from perceptron import Client, image client = Client( api_key=os.environ["PERCEPTRON_API_KEY"], provider="perceptron", timeout=60.0, ) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp" ), {"type": "text", "text": "Describe this coastal scene and the visible objects."}, ], }], max_completion_tokens=1024, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"No completed text answer: {choice.finish_reason}") print(choice.message.content or "") if response.usage is not None: print(response.usage.to_dict()) ``` Run it from the same terminal: ```bash theme={null} python first_request.py ``` With `provider="perceptron"`, the SDK uses `https://api.perceptron.inc/v1` unless you configure a different `base_url`. It can read `PERCEPTRON_API_KEY` automatically; the example passes the key explicitly. Requests are not retried automatically. Use the bounded retry example in [Error messages](/perceptron-mk1.5/guides/error-messages) when adding retries to your application. ## Use a task helper For a single question, `question()` builds the messages for you. Use `image()`, `video()`, or `audio()` to identify the media type: ```python theme={null} from perceptron import image, question result = question( image( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp" ), "Describe this coastal scene and the visible objects.", provider="perceptron", model="perceptron-mk1.5", max_tokens=1024, ) if not result.complete or result.finish_reason != "stop" or result.tool_calls: raise RuntimeError("No completed text answer") print(result.text) ``` The helper returns a `PerceiveResult`; the message API returns a `ChatCompletion`. Both expose text, reasoning, tool calls, completion status, and asset resolution. The message API exposes typed `Usage` through `response.usage`; task helpers expose usage as a dictionary through `result.usage`. Use the message API when you need explicit conversation history or a [tool loop](/perceptron-mk1.5/guides/tool-calling). ## Build on the example * [Request basics](/perceptron-mk1.5/guides/python/request-basics): reusable functions, message roles, and follow-up questions. * [Files](/perceptron-mk1.5/guides/files): upload local media and reuse its file ID. * [Multiple assets](/perceptron-mk1.5/guides/multiple-assets): include several images or videos and interpret `asset_idx`. * [Structured outputs](/perceptron-mk1.5/capabilities/structured-outputs): validate a JSON result. * [Tool calling](/perceptron-mk1.5/guides/tool-calling): execute application functions and return their results. * [Python FAQs](/perceptron-mk1.5/guides/python/faqs): async requests, annotations, streaming, and common setup problems. # Request basics Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/python/request-basics Build reusable multimodal requests, preserve conversation history, and choose an output format. A chat request is an ordered list of messages. Each message has a role; user content can combine text with image, video, or audio parts. Keep this structure explicit when building reusable Python functions. See [Python setup](/perceptron-mk1.5/guides/python/getting-started) for client installation and configuration. ## Build a reusable image request Install `perceptron>=0.4.0` and set `PERCEPTRON_API_KEY`. This complete example reuses the same client and returns the conversation so a follow-up can include its original image: ```python theme={null} import os from perceptron import Client, image client = Client( api_key=os.environ["PERCEPTRON_API_KEY"], provider="perceptron", timeout=60.0, ) def answer(messages): response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, max_completion_tokens=1024, ) choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError(f"No completed text answer: {choice.finish_reason}") return choice.message.to_dict() def describe(image_url, question): messages = [ {"role": "system", "content": "Answer using visible evidence. State uncertainty."}, { "role": "user", "content": [ image(image_url), {"type": "text", "text": question}, ], }, ] messages.append(answer(messages)) return messages history = describe( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp", "Describe this coastal scene and the visible objects.", ) print(history[-1].get("content") or "") history.append({"role": "user", "content": "Where is the sailboat relative to the shoreline?"}) history.append(answer(history)) print(history[-1].get("content") or "") ``` The API receives the history supplied on each call. A follow-up does not automatically recover the previous image or answer. Append the full assistant message to retain its answer, `reasoning_content`, and any `tool_calls` in your client-side history. For requests with tool declarations, tool calls, or tool results, send `reasoning_content` back with the assistant turn. On ordinary requests without tools or tool traffic, the gateway omits historical `reasoning_content` from the model's input. The previous media and assistant answer still need to be included for a follow-up. ## Choose message roles and content parts | Part | Purpose | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `system` message | Application instructions, such as output expectations and uncertainty handling. | | `user` message | A question, media, and any labeled reference examples. | | `assistant` message | A previous model turn, including its calls or reasoning when present. | | `tool` message | The result of a model-issued function call, with its matching `tool_call_id`. | | `image_url` / `video_url` | Media supplied by URL or a supported data URL. | | `image_file_id` / `video_file_id` | Media previously uploaded through the [Files API](/perceptron-mk1.5/guides/files). | | `video_frames` | A timestamped group of frames; see [video inputs](/perceptron-mk1.5/capabilities/video-understanding#send-timestamped-frames). | | `input_audio` | Bare base64 audio bytes with `format: "wav"`, `"mp3"`, or `"flac"`. | | `audio_url` / `audio_file_id` | Audio supplied by HTTP(S)/data URL or a previously uploaded file; see [Audio](/perceptron-mk1.5/capabilities/audio). | A text part containing a URL is only text. Use the matching media part to make the content available as an image, video, or audio recording. Audio parts belong in user messages. The SDK’s `image()`, `video()`, and `audio()` helpers encode local paths or bytes inline. They also accept URLs, `file_id=...`, or a `File` returned by `client.files.upload()`. They do not upload through the Files API automatically. Order matters for [asset selectors](/perceptron-mk1.5/guides/multiple-assets). User media and tool-returned images share one zero-based sequence that continues across messages and turns. An annotation's `asset_idx` refers to media available at that point in the conversation; media added later cannot change its target. Removing an earlier media occurrence changes subsequent indices. ## Choose how to consume the answer | Desired result | Request and handling | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Text | Give a clear instruction; read `message.content` after checking completion. | | Spatial or temporal annotations | Pass `vision_config={"annotation_format": "box"}`; parse and validate [annotation markup](/perceptron-mk1.5/concepts/annotations). | | A JSON record | Supply a JSON Schema through `response_format` and [validate the result](/perceptron-mk1.5/capabilities/structured-outputs). | | A short pattern-constrained answer | Pass `regex` directly and validate the completed text. | | A function call | Declare `tools`, then use the [caller-executed tool loop](/perceptron-mk1.5/guides/tool-calling). | For example, an ordinary box request adds `vision_config={"annotation_format": "box"}` to `client.chat.completions.create(...)` and asks for the objects to locate. Use [`reasoning_effort`](/perceptron-mk1.5/capabilities/thinking) as a top-level argument when reasoning is needed. Output guidance does not establish that a prediction is correct. Check selectors, geometry, schemas, and task-specific constraints before using the result. Tool declarations and JSON Schema/regex final-answer constraints cannot be combined in the same request; the [structured-report example](/perceptron-mk1.5/capabilities/structured-outputs#combine-tools-with-a-constrained-final-answer) shows the separate final step. ## Inspect a request before sending it Build `messages` as an ordinary Python value so you can inspect roles, content types, ordering, and parameters in a debugger before calling the client. Keep API keys, private URLs, base64 media, and sensitive conversation content out of routine logs. Record the model, completion status, and reported usage for troubleshooting. Bound conversation growth and output with the [tokenization guide](/perceptron-mk1.5/guides/tokenization), and use [Error messages](/perceptron-mk1.5/guides/error-messages) for failures and retries. # Rendering annotations Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/rendering-annotations Parse image boxes with the SDK, resolve their asset selectors, and draw them on the original image. A grounded answer contains annotation text in `message.content`. To draw it, first check that the response completed, parse the requested geometry, and resolve its asset selector to the correct image. See [annotation format](/perceptron-mk1.5/concepts/annotations) for the complete grammar and [coordinates](/perceptron-mk1.5/concepts/coordinates) for scaling and crop transforms. ## Save a completed grounding answer The [image Q\&A](/perceptron-mk1.5/capabilities/image-qa), [captioning](/perceptron-mk1.5/capabilities/image-captioning), [detection](/perceptron-mk1.5/capabilities/object-detection), and [OCR](/perceptron-mk1.5/capabilities/ocr) guides include grounded examples. Request `point_box` elements with labels in `mention` for this walkthrough. You can request explicit `asset_idx` values to help map boxes to images, but the attribute is optional and the model may omit it even when requested. Save the completed output as `response.txt`, and keep the exact input image locally. For example, after a non-streaming request has returned `response`: ```python theme={null} from pathlib import Path choice = response.choices[0] if choice.finish_reason != "stop" or choice.message.tool_calls: raise RuntimeError("Grounding did not return a completed answer") Path("response.txt").write_text(choice.message.content or "", encoding="utf-8") ``` For streaming, accumulate the answer and require successful completion before saving it. Receiving a closing box tag does not establish that the whole response succeeded. ## Draw image boxes Install `perceptron>=0.4.0` and `pillow` and save the following as `render_boxes.py`. Provide the index of the image you want to render and the index of the last asset available when the saved answer was produced. For a request with one image, both indices are `0`: ```bash theme={null} pip install "perceptron>=0.4.0" pillow python render_boxes.py image.png response.txt --asset-idx 0 --last-asset-idx 0 --output annotated.png ``` For a request with two images, render the second image with: ```bash theme={null} python render_boxes.py second-image.png response.txt --asset-idx 1 --last-asset-idx 1 --output annotated.png ``` The parser preserves a missing `asset_idx` as `None`; the renderer resolves it to `--last-asset-idx`. After checking inherited selectors, an omitted selector refers to the **last asset available when the answer was produced**. An explicit selector, including `0`, takes precedence. Count supplied media in message history and tool results up to that answer. Do not include media added later in the conversation when rendering an earlier answer. To render the first image from the two-image request, use `--asset-idx 0 --last-asset-idx 1`; boxes without selectors still belong to asset `1` and are filtered out. The SDK parser handles boxes and inherited collection selectors. This renderer accepts a small, explicit subset: static `point_box` elements, optionally grouped in `collection` elements, with only `mention` and `asset_idx` attributes. Write coordinates as `(x1,y1) (x2,y2)`, quote attribute values, and XML-escape label characters such as `&` and `"`. Tracks, timestamps, and other geometry need a different renderer. `strict=True` raises reported SDK parse errors; it is not a complete syntax or geometry validator. The example checks each annotation fragment with Python's XML parser, rejects leftover annotation markup, and validates coordinates and selectors before drawing. ```python theme={null} import argparse import math import re import xml.etree.ElementTree as ET from pathlib import Path from perceptron import collect_annotations, resolve_asset_idx from PIL import Image, ImageDraw, ImageOps def parse_boxes(text): annotations = collect_annotations(text, strict=True) tag_names = ("point_box", "point", "polygon", "clip", "collection", "track") coordinate = r"\(\s*[0-9]+\s*,\s*[0-9]+\s*\)" box_body = rf"\s*{coordinate}\s+{coordinate}\s*" for segment in annotations.parsed: if segment["kind"] == "text": remaining = segment["text"] tag_start = re.search(r"=0.4.0` and set `PERCEPTRON_API_KEY`. This example prints the usage returned with an image answer and handles missing usage, cache, or audio breakdowns. The same handling applies to audio and video requests. ```python theme={null} import os from perceptron import Client, image client = Client( provider="perceptron", api_key=os.environ["PERCEPTRON_API_KEY"], ) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp" ), {"type": "text", "text": "Describe the scene in two sentences."}, ], }], max_completion_tokens=1024, ) usage = response.usage if usage is None: print("Usage was not included in this response.") else: print(f"Input tokens: {usage.prompt_tokens}") print(f"Output tokens: {usage.completion_tokens}") print(f"Total tokens: {usage.total_tokens}") cached = usage.cached_tokens if cached is not None: print(f"Cached input tokens, included in input: {cached}") audio_tokens = usage.audio_tokens if audio_tokens is not None: print(f"Audio tokens, included in input: {audio_tokens}") choice = response.choices[0] if choice.finish_reason != "stop": raise RuntimeError(f"Answer did not complete: {choice.finish_reason}") print(choice.message.content or "") ``` `prompt_tokens` measures input; `completion_tokens` measures generated output, including reasoning when used. `total_tokens` combines the two. Count the reported output, not only the length of `message.content`: reasoning and function arguments can be returned in separate fields. When `prompt_tokens_details.cached_tokens` is present, it is a subset of `prompt_tokens`. Do not add it again to the total. Cached input still occupies context; its pricing is different. An absent breakdown does not establish a cache hit, and repeating a URL or request does not promise one. The chat-completions audio breakdown is optional too. A missing or `null` `audio_tokens` value means the breakdown was not reported; it is not a measured zero. An explicit zero reports no audio tokens. Multilook instead defaults a missing upstream audio breakdown to zero, so zero alone cannot prove audio was skipped there. For multiple inputs, the aggregate cannot identify which particular video's soundtrack was analyzed. [Multilook](/perceptron-mk1.5/guides/multilook) reports aggregate usage for independent prompts over shared context. Use its aggregate once when accounting for the request; do not add the same aggregate to every prompt result. For streaming requests, send `stream_options={"include_usage": True}`. Usage may arrive after the last content delta in a chunk with `choices: []`. The SDK includes it in the completion returned by `stream.get_final_completion()` after consuming the stream. Read it independently of choice deltas if you handle chunks yourself, and avoid summing cumulative usage snapshots within one response. The [tracking stream example](/perceptron-mk1.5/capabilities/video-tracking#stream-a-tracking-answer) shows this pattern. ## Budget a tool conversation Each model request in a [tool loop](/perceptron-mk1.5/guides/tool-calling) has its own input and output usage. Previous assistant calls and tool results become input on later requests; tool-returned images contribute visual tokens too. Add usage across model requests to measure the whole workflow. Bound the number of rounds and the size of tool results, and reserve capacity for the final answer. Compact long retrieved text to the relevant evidence while keeping call/result pairs intact. If you remove or reorder media in history, update any affected `asset_idx` references. ## Handle an output limit `finish_reason: "length"` means the generated response is incomplete. Do not treat a partial JSON object, track, or function argument string as a completed result. Increase `max_completion_tokens` when both model limits and remaining context allow it. Otherwise reduce the input, shorten the requested answer, or split the task into smaller requests. For tracking, request fewer objects or a narrower interval rather than completing missing markup yourself. Keep usage from incomplete responses when available: they still generated tokens. Review quality alongside usage when changing image resolution, frame sampling, reasoning effort, or output budgets. # Building a tool agent Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/tool-agents Combine function calls, bounded execution, and image results into a useful application workflow. A tool agent is an application that repeats the [function-calling loop](/perceptron-mk1.5/guides/tool-calling): ask the model, execute its calls, return the results, and ask again. Perceptron Mk1.5 can combine those results with images and videos already in the conversation. ## Design tools around useful steps Give each tool a specific job and a short description of when to use it. Prefer inputs your application can validate and results the model can use directly. | Application | Example tools | Workflow | | ----------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Product matching | `search_catalog`, `get_product` | Identify an item in an image, retrieve candidates, and compare specifications or reference images. | | Visual inspection | `get_inspection_record`, `get_reference_image` | Compare a new image with the approved reference and retrieve relevant inspection history. | | Document research | `search_documents`, `read_document` | Search a collection, read relevant passages, and answer with source references. | These are function designs for your application to implement. Include source IDs, titles, and relevant excerpts in retrieval results, and ask the model to cite those IDs. Return an explicit empty result when nothing matches. Separate searching from reading so a broad search does not fill the conversation with entire documents. When a tool returns a long document, extract passages relevant to the question before adding the result. If you use another model request to summarize a document, count that request against the same time and token budgets as the main loop. ## Bound the whole workflow Choose budgets before starting the loop. The values below are illustrative application settings, not API limits. | Budget | Example | Where to enforce it | | --------------------- | --------------------------------- | -------------------------------------------------------- | | Model rounds | 4 | Before each model request | | Total tool calls | 6 | Before dispatch, counting every call in a parallel group | | Concurrent executions | 2 | In your tool runner | | End-to-end time | 90 seconds | Across model calls, tools, and retries together | | Completion tokens | 2,048 per request | `max_completion_tokens` on every model request | | Result size | A few relevant passages or images | Before appending each result | Use a single deadline based on a monotonic clock. Recompute the remaining time before each request and tool execution, including after waiting for a concurrency slot. Set each operation's timeout to at most the remaining time and stop when it reaches zero. A timeout on an individual request alone does not bound a multi-round workflow. Add token usage across every model request to track the workflow's cost. Each new request includes the accumulated history, so input usage can grow even when the latest tool result is short. Stop initiating calls when your budget is spent; reserve room for a final answer if you need one. ## Return failures as results An unknown function, invalid argument, timeout, or empty lookup should produce a clear result for that call. This gives the model a chance to choose another step or explain the limitation. The function-calling example already does this for invalid names and arguments. For example, this is a tool-result message for a call that timed out; replace `call_from_response` with the actual returned ID: ```json theme={null} { "role": "tool", "tool_call_id": "call_from_response", "content": "{\"error\":\"The document lookup timed out. No document was retrieved.\"}" } ``` When a batch exceeds your remaining budget, return a result explaining that limit for each skipped call. Before making another model request, every call in the previous assistant message must have a result. Keep failures separate from successful data so the model does not treat an error as evidence. To request a final answer after the budget is exhausted, keep the tool declarations and set `tool_choice: "none"`. Tell the model to answer from the results already available. If it still returns calls, stop execution and report that the workflow reached its limit; `"none"` is not an execution guard. Run independent calls concurrently when useful, but preserve their IDs and append all results before asking the model to continue. Stable result ordering also keeps image indices predictable. Retry decisions belong in the application; avoid automatically repeating an action that may already have succeeded. ## Return an image from a tool Tool results can contain text and image parts. This lets a catalog lookup return a product photograph or a document lookup return an existing page image. A bare image URL inside a text string is just text; use an `image_url` content part to provide the image to the model. This example compares an observed product with its reference photograph. Save two PNG images as `observed.png` and `reference.png` in your working directory, install `perceptron>=0.4.0`, and set `PERCEPTRON_API_KEY`. Save the script as `compare_product.py` and run `python compare_product.py`. The application declares `get_reference_image(product_id)` and maps the product ID to a local file. The observed image is `asset_idx=0`; the image returned by the tool becomes `asset_idx=1`. The script makes at most two model requests: one to request the reference and one to compare the images. ```python theme={null} import json from pathlib import Path from perceptron import Client, function_tool, image client = Client(timeout=60.0) REFERENCE_IMAGES = {"product-001": Path("reference.png")} TOOLS = [function_tool( "get_reference_image", description="Return the reference photograph for a product ID.", parameters={ "type": "object", "properties": { "product_id": {"type": "string", "enum": ["product-001"]}, }, "required": ["product_id"], "additionalProperties": False, }, )] def execute_tool(call): function = call["function"] if function["name"] != "get_reference_image": raise ValueError("Unknown function.") arguments = json.loads(function["arguments"]) if not isinstance(arguments, dict) or set(arguments) != {"product_id"}: raise ValueError("Expected only product_id in the arguments.") product_id = arguments["product_id"] if not isinstance(product_id, str) or product_id not in REFERENCE_IMAGES: raise ValueError("Unknown product ID.") return { "role": "tool", "tool_call_id": call["id"], "content": [ { "type": "text", "text": f"Reference photograph for {product_id} (asset_idx=1).", }, image(REFERENCE_IMAGES[product_id]), ], } def complete(messages, tool_choice): return client.chat.completions.create( model="perceptron-mk1.5", messages=messages, tools=TOOLS, tool_choice=tool_choice, parallel_tool_calls=False, reasoning_effort="high", max_completion_tokens=2048, ).choices[0] def run(): messages = [{ "role": "user", "content": [ image("observed.png"), { "type": "text", "text": ( "This is an observed product-001 (asset_idx=0). " "Call get_reference_image for product-001, then compare the " "observed item with the reference photograph. Describe visible " "matches and differences. Include asset_idx on any spatial annotations." ), }, ], }] choice = complete(messages, "auto") assistant = choice.message.to_dict() calls = assistant.get("tool_calls") or [] if choice.finish_reason != "tool_calls" or len(calls) != 1: raise RuntimeError("Expected one completed reference-image call.") messages.append(assistant) messages.append(execute_tool(calls[0])) final = complete(messages, "none") if final.finish_reason != "stop" or final.message.tool_calls: raise RuntimeError("The model did not complete its comparison.") print(final.message.content or "") if __name__ == "__main__": run() ``` The second request includes the complete assistant message, including any `reasoning_content`, and the image result with its matching `tool_call_id`. The comparison can now refer to both the user's photograph and the retrieved reference. `tool_choice: "none"` requests a final answer; the script stops if the model asks for another tool instead. The SDK encodes these local images inline. Pass an HTTPS URL to `image(url)` for a remote image, or use `image(file_id="file-...")` for an image already uploaded with the Files API. Tool results support text and images; other media belong in user messages. Images returned by tools become new assets in conversation order. If the user supplied one image and the first tool result adds two images, their indices are `0`, `1`, and `2`. A later annotation such as `...` refers to the second tool-returned image, with coordinates relative to that image's dimensions. Adding those images does not change the bindings of annotations produced before the tool result. Maintain the same ordering when replaying history. Indices refer to occurrences of media, so returning the same image again adds another asset position. Include a short description next to each image, especially when returning several candidates. See [Working with multiple assets](/perceptron-mk1.5/guides/multiple-assets) for allocation rules and [Annotations](/perceptron-mk1.5/concepts/annotations) for rendering coordinates. ## Combine retrieval with visual evidence For a product-matching workflow, send the user's image and ask the model to search your catalog, inspect the returned candidates, and explain the match using both visible features and retrieved specifications. Keep the user's original image and each candidate as separate assets. Ask for `asset_idx` on spatial annotations to help your viewer select the right image. If it is neither explicit nor inherited, the annotation refers to the last asset available when it was produced, including images returned by tools up to that point. For video inspection, keep the video in the conversation while tools retrieve reference material. Ask for timed [track annotations](/perceptron-mk1.5/concepts/annotations#tracks) when the answer needs to identify an object's path, and cite retrieved records separately from visual observations. The application chooses which tool results to expose to the model and which annotations to render for the user. # Function calling Source: https://docs.perceptron.inc/perceptron-mk1.5/guides/tool-calling Execute model-requested functions and continue the conversation, with complete Python and streaming examples. This guide uses the Perceptron Python SDK with Perceptron's public API. The model selects a function and generates its arguments; your Python code runs it and returns the result. ## Run a complete example Install the client and set `PERCEPTRON_API_KEY` to your API key: ```bash theme={null} pip install "perceptron>=0.4.0" export PERCEPTRON_API_KEY="your-api-key" ``` Save the following as `inventory.py` and run `python inventory.py`. The inventory is a local example dataset, so only the model request needs network access. Replace `lookup_inventory` with your own database or service once the loop is working. ```python theme={null} import json from perceptron import Client, function_tool client = Client(timeout=60.0) INVENTORY = { "BALL-RED": {"name": "Red ball", "quantity": 12}, "BALL-BLUE": {"name": "Blue ball", "quantity": 0}, } TOOLS = [function_tool( "lookup_inventory", description="Look up the current stock for one product SKU.", parameters={ "type": "object", "properties": { "sku": {"type": "string", "description": "The exact product SKU."}, }, "required": ["sku"], "additionalProperties": False, }, )] def execute_tool(name, arguments): try: if name != "lookup_inventory": raise ValueError("Unknown tool. Use lookup_inventory.") args = json.loads(arguments) if not isinstance(args, dict) or set(args) != {"sku"}: raise ValueError("Arguments must contain only sku.") if not isinstance(args["sku"], str): raise ValueError("sku must be a string.") item = INVENTORY.get(args["sku"]) return json.dumps({"sku": args["sku"], "found": item is not None, "item": item}) except (TypeError, ValueError) as error: return json.dumps({"error": str(error)}) def complete_turn(messages, tool_choice="auto"): response = client.chat.completions.create( model="perceptron-mk1.5", messages=messages, tools=TOOLS, tool_choice=tool_choice, parallel_tool_calls=True, reasoning_effort="high", max_completion_tokens=2048, ) choice = response.choices[0] message = choice.message.to_dict() return message, choice.finish_reason, response.usage def run(): messages = [{ "role": "user", "content": ( "Use inventory lookup to compare BALL-RED and BALL-BLUE. " "Which is in stock, and how many are available?" ), }] calls_used = 0 for _ in range(4): message, finish_reason, usage = complete_turn(messages) if usage is not None: print(f"Tokens this request: {usage.total_tokens}") calls = message.get("tool_calls") or [] if finish_reason == "stop" and not calls: messages.append(message) return messages if finish_reason != "tool_calls" or not calls: raise RuntimeError(f"Incomplete response: {finish_reason}") if calls_used + len(calls) > 6: raise RuntimeError("Tool call budget exhausted.") messages.append(message) for call in calls: function = call["function"] result = execute_tool(function["name"], function["arguments"]) messages.append({ "role": "tool", "tool_call_id": call["id"], "content": result, }) calls_used += len(calls) raise RuntimeError("Model round budget exhausted.") if __name__ == "__main__": history = run() print(history[-1].get("content") or "") ``` The example allows at most four model requests and six tool executions. It executes calls sequentially for clarity, even when the model requests them in the same turn. A successful answer should report 12 red balls and no blue balls in this example inventory; the wording and number of calls can vary. `run()` returns the completed conversation, including the final assistant answer, so another request can reuse the tool results. ## Preserve the conversation Append the whole assistant message before its tool results. The SDK accepts the returned `response.message` directly in `messages`; use its `to_dict()` method when storing history as dictionaries, as in the example. Both retain `content`, `tool_calls`, and any returned `reasoning_content`. The model's next input does not necessarily include every saved field. Historical `reasoning_content` is forwarded when the request includes function tools or tool-call/result history; it is ignored on ordinary requests without either. When an assistant message contains tool calls, its accompanying prose in `content` is omitted from the next model input. Keep the full message for the tool round trip, and put information needed for subsequent reasoning in the user messages or tool results. Each call needs one result with the matching `tool_call_id` before you continue. Results may be supplied in a different order from the calls. Keep `function.arguments` as the returned string in assistant history, even after parsing it for execution. Continue sending the same function declarations on subsequent requests, including when you want a final answer. For a [constrained JSON or regex answer](/perceptron-mk1.5/capabilities/structured-outputs#combine-tools-with-a-constrained-final-answer), complete the tool loop first, then omit the tool declarations and options from that separate request. Assistant text and tool arguments can be absent or empty while a response is streaming. ## Stream a tool call Replace `complete_turn` in the example with this version. The execution loop stays the same. The SDK assembles calls by `index` and accumulates argument fragments. The example waits for the complete stream and checks the result before returning anything that can be executed. ```python theme={null} def complete_turn(messages, tool_choice="auto"): with client.chat.completions.create( model="perceptron-mk1.5", messages=messages, tools=TOOLS, tool_choice=tool_choice, parallel_tool_calls=True, reasoning_effort="high", max_completion_tokens=2048, stream=True, stream_options={"include_usage": True}, ) as stream: for chunk in stream: for choice in chunk.choices: print(choice.delta.content or "", end="", flush=True) response = stream.get_final_completion() if not response.complete or response.finish_reason not in {"stop", "tool_calls"}: raise RuntimeError(f"Incomplete stream: {response.finish_reason}") calls = response.tool_calls or [] ids = [call.id for call in calls] if calls and (not all(ids) or len(set(ids)) != len(ids)): raise RuntimeError("Missing or duplicate tool call IDs.") return response.message.to_dict(), response.finish_reason, response.usage ``` The SDK requests usage by default for Perceptron streams; the example sets `stream_options.include_usage: true` explicitly. Usage can arrive in a separate trailing chunk with `choices: []`. `get_final_completion()` consumes any remaining chunks and returns the assembled message and usage. If you read chunks yourself, consume usage independently of choice deltas and keep reading after a finish reason. `finish_reason: "tool_calls"` means a model turn is complete and ready for your application to handle. `"stop"` is a completed answer. Treat `"length"`, a missing finish reason, or an API error as an incomplete turn; do not execute accumulated arguments. The SDK raises an `SDKError` subclass for API or transport failures, and `IncompleteStreamError` when the stream ends without `[DONE]`. Its error may carry a `.partial` completion for diagnostics; do not execute those partial calls. A raw SSE client must also handle `{"error": ...}` events, which end the stream without `[DONE]`. ## Supported controls The API behaviors below apply when the request declares non-empty `tools`. The SDK validates controls before sending a request: unsupported `tool_choice` values and incompatible tool/output combinations raise `BadRequestError` locally. At the HTTP API layer, validly shaped `tool_choice` and `parallel_tool_calls` values are ignored when `tools` is omitted or empty. | Control | Behavior | | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `tool_choice: "auto"` | Default. The model chooses whether to call a function. | | `tool_choice: "none"` | Best effort request for an answer without calls. Your application must still handle returned calls. | | `tool_choice: "required"` or a named function | Unsupported; the SDK raises `BadRequestError`. Direct API requests with non-empty `tools` receive HTTP 400. | | `parallel_tool_calls` | Defaults to `true`. Always accept an array of calls, including when set to `false`; prior parallel history can preserve this behavior. | | Function `strict: true` | Accepted, but argument-schema enforcement is not implemented. Validate arguments yourself. | | `n` | Chat completions supports `1`. Several tool calls can still appear within that one completion. | | JSON-schema `response_format` or `regex` with tools | Unsupported in the same request. | | Multilook | Does not support tool declarations, tool calls, or tool-result history. Use chat completions. | For a machine-readable final report, finish the tool workflow and make a separate [structured-output request](/perceptron-mk1.5/capabilities/structured-outputs) without tool declarations. Completed tool history can remain in the conversation. For execution budgets, image results, and retrieval patterns, continue to [Building a tool agent](/perceptron-mk1.5/guides/tool-agents). # Quickstart Source: https://docs.perceptron.inc/perceptron-mk1.5/index Get started with Perceptron Mk1.5 for image, video, and audio understanding, tracking, and tool calling. Perceptron Mk1.5 understands images, video, and audio, grounds answers in space and time, and calls functions that your application provides. Use the same chat completions API for questions, transcription, structured extraction, object tracking, and tool-assisted workflows. Get your key from the Perceptron platform Get help and see what others are building ## Make your first request Install the Perceptron Python SDK and set your API key: ```bash theme={null} pip install "perceptron>=0.4.0" export PERCEPTRON_API_KEY="your-api-key" ``` ```python Python theme={null} import os from perceptron import Client, image client = Client( api_key=os.environ["PERCEPTRON_API_KEY"], provider="perceptron", ) response = client.chat.completions.create( model="perceptron-mk1.5", messages=[{ "role": "user", "content": [ image( "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/" "main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp" ), {"type": "text", "text": "Describe this coastal scene and the visible objects."}, ], }], max_completion_tokens=1024, ) print(response.choices[0].message.content) ``` ```bash curl theme={null} curl https://api.perceptron.inc/v1/chat/completions \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "perceptron-mk1.5", "messages": [{ "role": "user", "content": [ {"type": "image_url", "image_url": {"url": "https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp"}}, {"type": "text", "text": "Describe this coastal scene and the visible objects."} ] }], "max_completion_tokens": 1024 }' ``` Use the SDK’s `image()`, `video()`, and `audio()` helpers for local paths, bytes, URLs, or uploaded file references. Local paths and bytes are encoded inline; use [Files](/perceptron-mk1.5/guides/files) to upload once and reuse a file ID. See [Python setup](/perceptron-mk1.5/guides/python/getting-started) for a complete integration. ## Explore the capabilities Ask questions, read text, and locate objects. Describe actions and locate events in time. Follow objects with timestamped geometry inside track tags. Connect the model to functions executed by your application. Summarize recordings, ask questions, transcribe speech, and locate events in time. Choose reasoning effort and budget for the final answer. Compare media and map annotations with asset\_idx. Constrain final answers with JSON Schema or regex. See the [model card](/perceptron-mk1.5/models/perceptron-mk1.5) for limits and pricing. For production integrations, read [Tokenization](/perceptron-mk1.5/guides/tokenization), [Error messages](/perceptron-mk1.5/guides/error-messages), and [Scaling](/perceptron-mk1.5/guides/scaling). # Models overview Source: https://docs.perceptron.inc/perceptron-mk1.5/models/index Choose a model and discover its API capabilities. Use **Perceptron Mk1.5** for new integrations that need audio understanding, video soundtracks, configurable reasoning, tool calling, visual grounding across multiple assets, or native video tracking. | Model | Inputs | Function calling | Model guide | | ------------------ | -------------------------- | ---------------- | ------------------------------------------------------------- | | `perceptron-mk1.5` | Text, images, video, audio | Yes | [Perceptron Mk1.5](/perceptron-mk1.5/models/perceptron-mk1.5) | | `perceptron-mk1` | Text, images, video | No | [Perceptron Mk1](/perceptron-mk1/models/perceptron-mk1) | The Isaac 0.1 and 0.2 models have been retired from the Perceptron API; see the [release notes](/perceptron-mk1.5/changelog). Model availability can change; query the API for the current catalog and per-model limits: ```python Python theme={null} from perceptron import Client client = Client(provider="perceptron") models = client.models.list(extended=True) for model in models: print(model.id, model.modalities, model.capabilities) model = client.models.retrieve("perceptron-mk1.5", extended=True) print("Tool calling:", model.supports("tool_calling")) print("Context limit:", model.max_context_tokens) print("Output limit:", model.max_output_tokens) ``` ```bash curl theme={null} curl 'https://api.perceptron.inc/v1/models?extended=true' \ -H "Authorization: Bearer $PERCEPTRON_API_KEY" ``` The Python example uses `perceptron>=0.4.0` and reads `PERCEPTRON_API_KEY` from the environment; see the [quickstart](/perceptron-mk1.5/index). `extended=True` includes capabilities, modalities, and token limits. Without it, the SDK returns basic model metadata. Inspect each model's `capabilities` before sending optional features such as `tool_calling`. Treat capability names as an extensible list so newly added capabilities do not break your client; the SDK preserves new capability names. # Perceptron Mk1.5 Source: https://docs.perceptron.inc/perceptron-mk1.5/models/perceptron-mk1.5 Image, video, and audio understanding with visual grounding, reasoning, and function calling. Perceptron Mk1.5 accepts text, images, video, and audio. It generates text, spatial and temporal annotations, structured responses, and function calls for your application to execute. ## Specifications | Property | Value | | --------------------- | --------------------------------------------------------------------------------------- | | Model ID | `perceptron-mk1.5` | | Context window | 36,864 tokens | | Maximum output | 8,192 tokens | | Input modalities | Text, images, video, audio | | Audio formats | WAV, MP3, FLAC | | Audio limit | 16,384 audio tokens per item; about 21.8 minutes at approximately 750 tokens per minute | | Reasoning | Supported; configure with `reasoning_effort` | | Function calling | Supported on chat completions | | Constrained responses | JSON Schema and regex | | Multilook | Supported for independent prompts over shared context | Audio shares the context window with the rest of the input and the output budget. The audio limit is specific to the current Mk1.5 model; a request can reach the context limit before reaching the per-item audio limit. Video soundtracks require `vision_config.enable_audio_in_video: true`. Responses contain text and structured content; the model does not generate audio. See [Audio](/perceptron-mk1.5/capabilities/audio) for input forms and limits. ## Pricing | Token category | Price per million tokens | | -------------- | ------------------------ | | Input | \$0.15 | | Output | \$1.50 | | Cached input | \$0.0375 | [Multilook](/perceptron-mk1.5/guides/multilook) reuses a shared context within one request. Cached tokens are a subset of prompt tokens; do not add them to the prompt-token total a second time. Use the [tokenization guide](/perceptron-mk1.5/guides/tokenization) to budget media, reasoning, and conversation history, and the [scaling guide](/perceptron-mk1.5/guides/scaling) to manage throughput. ## Choose the output for your task | Task | API controls | Guide | | ------------------------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Describe an image or answer a question | Natural-language prompt | [Image understanding](/perceptron-mk1.5/capabilities/image-understanding) | | Summarize a recording or describe sounds | `input_audio`, `audio_url`, or `audio_file_id` | [Audio understanding](/perceptron-mk1.5/capabilities/audio) | | Ask questions about a recording | Audio input and a question | [Audio Q\&A](/perceptron-mk1.5/capabilities/audio-qa) | | Transcribe speech | Audio input and a transcription instruction | [Audio transcription](/perceptron-mk1.5/capabilities/audio-transcription) | | Locate audible events or speech in time | Audio input and `vision_config.annotation_format: "clip"` | [Audio clipping](/perceptron-mk1.5/capabilities/audio-clipping) | | Combine video frames with their soundtrack | `vision_config.enable_audio_in_video: true` | [Video soundtracks](/perceptron-mk1.5/capabilities/video-understanding#analyze-video-soundtracks) | | Locate objects | `vision_config.annotation_format`: `point`, `box`, or `polygon` | [Annotation format](/perceptron-mk1.5/concepts/annotations) | | Locate events in video | `vision_config.annotation_format`: `clip` | [Video understanding](/perceptron-mk1.5/capabilities/video-understanding) | | Track an object | Spatial annotation format plus a prompt requesting `` | [Video tracking](/perceptron-mk1.5/capabilities/video-tracking) | | Return a constrained final answer | `response_format` with JSON Schema, or `regex` | [Structured outputs](/perceptron-mk1.5/capabilities/structured-outputs) | | Ask your application to run a function | `tools` and `tool_choice: "auto"` | [Tool calling](/perceptron-mk1.5/capabilities/tool-calling) | `track` is a container in generated markup, not an `annotation_format` value. The optional `asset_idx` attribute identifies a media occurrence among the assets available at that point in the conversation; it is not a persistent file identifier. If no explicit or inherited selector is present, it defaults to the last asset available when the annotation was produced. See the [asset resolution rules](/perceptron-mk1.5/guides/multiple-assets#resolve-selectors-before-drawing). ## Integration notes * Use `n: 1` for chat completions. Multilook has separate sampling controls. * Inspect `finish_reason` before using an answer. `length` means the output may be incomplete; tool calls require a caller-managed round trip. * Use [`reasoning_effort`](/perceptron-mk1.5/capabilities/thinking) for new reasoning integrations. * Function tools cannot be declared in the same request as JSON Schema or regex constrained output. Request a constrained final answer after the tool loop.