---
title: "Frames only (requires a frames spec)"
method: POST
path: "/v1/frames"
---

# Frames only (requires a frames spec)

`POST /v1/frames`

## Request

- Base URL: `https://framefetch.net`
- URL: `https://framefetch.net/v1/frames`
- Auth: HTTP bearer

## Query parameters

- `format` 'md' | 'markdown' | 'srt' | 'vtt'

## Request body

- ExtractRequest
  - `url` string, uri, required
  - `fields` string[]: digest (LLM summary of the transcript) auto-includes transcript and degrades gracefully if none is available. audio_digest (a spoken mp3 briefing synthesized from the digest gist) auto-includes digest (and thus transcript); it returns a time-limited signed URL plus the voice/provider used, is billed per estimated spoken second only when the mp3 is actually produced, and degrades to a warning (never charged, never a hard error) when no TTS provider is configured or the requested language isn't supported by the only configured provider. When translate is also set, the digest is generated directly in that target language so the audio speaks it too — combine audio_digest with translate for a foreign-language briefing from one call. structured (typed JSON understanding of the video: chapters, entities, products_shown, claims, key_moments) auto-includes transcript and also triggers the engine's own keyframe pass over the source video, so it works even on caption-less sources; a single gpt-4o-mini vision call over the transcript plus up to 12 sampled keyframes, run at temperature 0 so the result is deterministic and cacheable; a flat $0.03 per video, charged only when the analysis is actually produced (a download/VLM/parse failure omits the field with a warning and is NOT charged — see StructuredError codes STRUCTURED_UNAVAILABLE, STRUCTURED_PROVIDER_ERROR, STRUCTURED_PARSE_ERROR on VideoStructured below). comments returns up to comments_cap top-level comments per video ({ items: [{ text, author?, like_count?, reply_count? }], total_fetched, cap_applied, sort:"top" }); it is INDEPENDENT of the transcript (no download) and available on YouTube only — Reddit's unauthenticated comment API was deprecated on 2026-05-28 and now returns a hard 403, so Reddit omits the field exactly like TikTok/Instagram/Pinterest, with a warning and never a charge (see GET /v1/platforms and COMMENTS_UNSUPPORTED on VideoComments below). Author is a display handle only (no user ids/urls). Flat $0.0045 per call, charged only when comments were actually produced. comment_sentiment (aggregated audience sentiment: percentages, themes, summary — ONE LLM pass over the comments) auto-includes comments (its input; same YouTube-only availability, Reddit's source having been deprecated) and needs at least 5 fetched comments — fewer, or a model/parse failure, omits it with a warning and is never charged (see CommentSentiment below). Flat $0.006 per call, on top of the comments call it rides on. No competitor offers this rollup. delta (change vs the most-recent prior fetch of the same url: engagement velocity per hour + title/thumbnail-change flags) is never cached and returns hasPrior:false the first time a url is seen. text_overlay (on-screen text via OCR) requires frames to also be requested, with a frames spec. Auto-skipped with a warning above 200 estimated frames.
  - `frames` FrameSpec
    - `mode` 'all' | 'every_n' | 'fps' | 'range'
    - `n` integer
    - `fps` number
    - `from` number
    - `to` number
    - `format` 'jpg' | 'png' | 'webp'
    - `width` integer
  - `verbosity` string
  - `translate` string: Optional ISO-639-1 target language code (e.g. "es", "de", "ja") to also translate the transcript into, surfaced as transcript_translated (with timings preserved when the source transcript had them). Requires "transcript" in fields (or "digest", which pulls transcript in). Billed per audio-minute like transcription — even for caption-sourced transcripts, since the translation is a separate LLM pass. Supported: en, es, de, fr, pt, it, nl, pl, tr, ru, uk, ar, hi, id, vi, th, ja, ko, zh, sv, da, no, fi, cs, ro (25 languages).
  - `voice` 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer' | 'Fritz-PlayAI': Spoken voice for the "audio_digest" field: the six OpenAI gpt-4o-mini-tts voices (alloy, echo, fable, onyx, nova, shimmer — multilingual) plus Groq/PlayAI's "Fritz-PlayAI" (English/Arabic-only fallback, used when no OpenAI key is configured). Defaults to "alloy". Inert unless "audio_digest" is requested; an unlisted value is a typed 400 that echoes this allowlist.
  - `comments_cap` integer: Max number of top-level comments the "comments" field returns (1–200, default 30). Inert unless "comments" (or "comment_sentiment", which auto-includes it) is requested; a value outside 1–200 is a 400 INVALID_PARAMS.
  - `publish` boolean: Opt in to a public, shareable per-video SEO page at /v/<id> (a stable hash of the url). Publishes ONLY public metadata plus a short fair-use transcript excerpt — never the full transcript or a signed frame URL. Pages carry VideoObject structured data and are listed in /sitemap-videos.xml. Best-effort; never affects the API response.

## Response `200`

Result

- ExtractResult
  - `platform` string
  - `url` string
  - `metadata` object
    - `title` string, nullable
    - `uploader` string, nullable
    - `durationSec` number, nullable
    - `uploadDate` string, nullable: normalized YYYY-MM-DD
    - `sourceFps` number, nullable
    - `thumbnail` string, nullable: poster/cover image URL (additive; may be null or omitted for older callers).
  - `insights` object
  - `transcript` object
    - `text` string
    - `source` string
    - `lang` string: Best-effort source language code, when known.
    - `segments` TranscriptSegment[]: Timed transcript segments (Whisper verbose_json or caption cue timings). Additive: omitted entirely (never []) when the source carries no usable timings.
      - `start` number, required
      - `end` number, required
      - `text` string, required
  - `transcript_translated` object: Present only when the caller passed the `translate` param. An LLM translation of the transcript into the requested ISO-639-1 language, billed per audio-minute like transcription.
    - `lang` string: The requested ISO-639-1 target language.
    - `text` string
    - `segments` TranscriptSegment[]: Mirrors the source cue timings (start/end unchanged, text translated); omitted when the source transcript carried no timings — never back-filled from the untranslated transcript.
      - `start` number, required
      - `end` number, required
      - `text` string, required
  - `digest` object: LLM summary of the transcript (present only when the digest field was requested and a transcript was available).
    - `gist` string
    - `topics` string[]
    - `state` object
  - `audio_digest` AudioDigest: A spoken-audio rendering of the video's digest (present only when "audio_digest" was requested and a digest was available to synthesize). Never served from the content-hash cache — its signed URL expires and the voice can vary per request. Billed per estimated spoken second (from the digest text length, ~15 chars/s) ONLY when the mp3 is actually produced; a TTS/upload failure omits this field, adds a warning to the top-level warnings array, and is not charged — the rest of the extraction stays usable. Failure reasons include no TTS provider configured (analogous to the worker-internal TTS_UNAVAILABLE code: neither OPENAI_API_KEY nor GROQ_API_KEY set) and an unsupported language on the only configured provider (TTS_LANG_UNSUPPORTED: Groq/PlayAI only speaks English/Arabic — set an OpenAI key for other languages); neither surfaces as an HTTP error code, only as a warnings[] string.
    - `url` string: Signed URL to the mp3 (24h TTL).
    - `seconds` number: Estimated spoken duration (digest gist chars / 15), the billing basis — not measured from the mp3.
    - `voice` string: The TTS voice actually used (after per-provider fallback/mapping).
    - `provider` 'openai' | 'groq'
    - `lang` string: ISO-639-1 language the audio is spoken in; present only when a translate target drove a target-language digest.
  - `structured` VideoStructured: Typed, structured understanding of a video (present only when "structured" was requested): the transcript plus up to 12 sampled keyframes are run through a vision LLM (gpt-4o-mini, temperature 0) that returns chapters, entities, products shown, claims, and key moments as strict JSON. Deterministic and cacheable — unlike audio_digest it carries no expiring signed URL, so it takes part in the normal content-hash cache. Best-effort like digest: any download/VLM/parse failure omits the whole field, adds a warning to the top-level warnings array, and is NOT charged; the rest of the extraction stays usable. Worker-internal failure codes (never surfaced as an HTTP error, only as a warnings[] string): STRUCTURED_UNAVAILABLE (no vision provider configured — OPENAI_API_KEY unset), STRUCTURED_PROVIDER_ERROR (the provider returned a non-2xx response), STRUCTURED_PARSE_ERROR (the model's output could not be parsed/validated into this schema). Billed a flat $0.03 per video (perStructuredVideoMicros, floor-pinned — see /pricing) charged only when the analysis is actually produced.
    - `chapters` object[]: Timespan segments of the video.
      - `start_sec` number
      - `end_sec` number
      - `title` string
      - `summary` string
    - `entities` object[]: People/orgs/places/products/other the model saw or heard.
      - `name` string
      - `type` 'person' | 'org' | 'place' | 'product' | 'other'
    - `products_shown` object[]: Products/brands the video shows and/or talks about.
      - `name` string
      - `evidence` 'visual' | 'spoken' | 'both': How the product was surfaced: only on-screen, only mentioned, or both.
    - `claims` object[]: Factual claims made in the video, optionally anchored to a timestamp.
      - `text` string
      - `timestamp_sec` number
    - `key_moments` object[]: Notable moments (hooks, reveals, demo steps) at a specific second.
      - `timestamp_sec` number
      - `description` string
    - `keyframe_mode` 'scene' | 'uniform': How the analyzed frames were selected: 'scene' = content-aware cut detection, 'uniform' = even-sampling fallback for static/short clips with too few detected cuts.
  - `comments` VideoComments: Up to cap_applied TOP-LEVEL comments for a video, sorted by the platform's "top" ranking (present only when "comments" was requested). Supported on YouTube only (via yt-dlp's info-json comments). Reddit's own public comment source — the unauthenticated thread JSON API — worked the same way until Reddit deprecated unauthenticated access to it on 2026-05-28; TikTok/Instagram/Pinterest never had a reliable public comment source. Billed a flat $0.0045 per call (independent of comments_cap), charged only when comments were actually produced. Best-effort: any fetch/parse failure, or an unsupported platform, omits the whole field with a warning and is NOT charged — never surfaces as an HTTP error code. Worker-internal failure codes behind those warnings (never an HTTP error, only a warnings[] string): COMMENTS_UNSUPPORTED (the platform has no comment source — drop "comments" from fields for it; see GET /v1/platforms), COMMENTS_PROVIDER_ERROR (the yt-dlp info-json fetch or parse failed).
    - `items` VideoComment[]
      - `text` string, required
      - `author` string: Display handle only — no user id / profile URL (data minimization).
      - `like_count` integer
      - `reply_count` integer: Number of direct replies to this top-level comment.
    - `total_fetched` integer: How many comments actually came back (after the cap).
    - `cap_applied` integer: The effective cap that was applied (default 30, hard max 200 via comments_cap).
    - `sort` 'top'
  - `comment_sentiment` CommentSentiment: Aggregated LLM sentiment over a video's comments (present only when "comment_sentiment" was requested, which auto-includes "comments" as its input): ONE Groq chat call (llama-3.3-70b-versatile, temperature 0, JSON mode) reads up to 100 comments and returns the audience mood AS A WHOLE — not per-comment scoring. Requires at least 5 fetched comments; fewer (or a model/parse failure) omits the whole field with a warning and is NOT charged — never surfaces as an HTTP error code. Billed a flat $0.006 per call, on top of the $0.0045 "comments" call it rides on. No competitor offers this rollup.
    - `positive_pct` number: 0-100; positive_pct + neutral_pct + negative_pct === 100 (normalized).
    - `neutral_pct` number
    - `negative_pct` number
    - `summary` string: 2-3 plain English sentences characterizing the overall audience reaction.
    - `top_themes` string[]: Up to 5 short recurring themes across the comments.
    - `representative` object: One exemplar comment per polarity, when found.
      - `positive` string
      - `negative` string
  - `delta` object: Change since the most-recent prior fetch of the same url (present only when the delta field was requested). First time a url is seen -> { hasPrior: false } and nothing else. A per-hour rate is null when a counter was unavailable or no time had elapsed.
    - `hasPrior` boolean
    - `sinceTs` integer: epoch ms of the prior snapshot this delta is measured against
    - `viewsPerHr` number, nullable
    - `likesPerHr` number, nullable
    - `commentsPerHr` number, nullable
    - `titleChanged` boolean
    - `thumbnailChanged` boolean
  - `frames` object[]
    - `index` integer
    - `url` string
    - `tSec` number
  - `textOverlay` object[]: On-screen text per frame (OCR), same index as frames.
    - `index` integer
    - `text` string
    - `lines` object[]
      - `text` string
      - `confidence` number
      - `bbox` number[]
  - `cost` object
    - `totalMicros` integer
    - `breakdownMicros` object

---

[API](https://skmtc.dev/framefetch/apis/framefetch-api.md) · [All operations](https://skmtc.dev/framefetch/apis/framefetch-api/llms.txt) · [OpenAPI document](https://skmtc.dev/framefetch/apis/framefetch-api/revisions/5584e8eb9bf5?raw)
