{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/framefetch-net/json-schema/framefetch-net-extract-result-schema.json",
  "title": "ExtractResult",
  "x-generated": "2026-10-30",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/framefetch-net-openapi.json#/components/schemas/ExtractResult",
  "type": "object",
  "properties": {
    "platform": {
      "type": "string"
    },
    "url": {
      "type": "string"
    },
    "metadata": {
      "type": "object",
      "properties": {
        "title": {
          "type": [
            "string",
            "null"
          ]
        },
        "uploader": {
          "type": [
            "string",
            "null"
          ]
        },
        "durationSec": {
          "type": [
            "number",
            "null"
          ]
        },
        "uploadDate": {
          "type": [
            "string",
            "null"
          ],
          "description": "normalized YYYY-MM-DD"
        },
        "sourceFps": {
          "type": [
            "number",
            "null"
          ]
        },
        "thumbnail": {
          "type": [
            "string",
            "null"
          ],
          "description": "poster/cover image URL (additive; may be null or omitted for older callers)."
        }
      }
    },
    "insights": {
      "type": "object"
    },
    "transcript": {
      "type": "object",
      "properties": {
        "text": {
          "type": "string"
        },
        "source": {
          "type": "string"
        },
        "lang": {
          "type": "string",
          "description": "Best-effort source language code, when known."
        },
        "segments": {
          "type": "array",
          "description": "Timed transcript segments (Whisper verbose_json or caption cue timings). Additive: omitted entirely (never []) when the source carries no usable timings.",
          "items": {
            "$ref": "#/$defs/TranscriptSegment"
          }
        }
      }
    },
    "transcript_translated": {
      "type": "object",
      "description": "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.",
      "properties": {
        "lang": {
          "type": "string",
          "description": "The requested ISO-639-1 target language."
        },
        "text": {
          "type": "string"
        },
        "segments": {
          "type": "array",
          "description": "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.",
          "items": {
            "$ref": "#/$defs/TranscriptSegment"
          }
        }
      }
    },
    "digest": {
      "type": "object",
      "description": "LLM summary of the transcript (present only when the digest field was requested and a transcript was available).",
      "properties": {
        "gist": {
          "type": "string"
        },
        "topics": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "state": {
          "type": "object"
        }
      }
    },
    "audio_digest": {
      "$ref": "#/$defs/AudioDigest"
    },
    "structured": {
      "$ref": "#/$defs/VideoStructured"
    },
    "comments": {
      "$ref": "#/$defs/VideoComments"
    },
    "comment_sentiment": {
      "$ref": "#/$defs/CommentSentiment"
    },
    "delta": {
      "type": "object",
      "description": "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.",
      "properties": {
        "hasPrior": {
          "type": "boolean"
        },
        "sinceTs": {
          "type": "integer",
          "description": "epoch ms of the prior snapshot this delta is measured against"
        },
        "viewsPerHr": {
          "type": [
            "number",
            "null"
          ]
        },
        "likesPerHr": {
          "type": [
            "number",
            "null"
          ]
        },
        "commentsPerHr": {
          "type": [
            "number",
            "null"
          ]
        },
        "titleChanged": {
          "type": "boolean"
        },
        "thumbnailChanged": {
          "type": "boolean"
        }
      }
    },
    "frames": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "url": {
            "type": "string"
          },
          "tSec": {
            "type": "number"
          }
        }
      }
    },
    "textOverlay": {
      "type": "array",
      "description": "On-screen text per frame (OCR), same index as frames.",
      "items": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "text": {
            "type": "string"
          },
          "lines": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                },
                "confidence": {
                  "type": "number"
                },
                "bbox": {
                  "type": "array",
                  "items": {
                    "type": "number"
                  },
                  "minItems": 4,
                  "maxItems": 4
                }
              }
            }
          }
        }
      }
    },
    "cost": {
      "type": "object",
      "properties": {
        "totalMicros": {
          "type": "integer"
        },
        "breakdownMicros": {
          "type": "object"
        }
      }
    }
  },
  "$defs": {
    "AudioDigest": {
      "type": "object",
      "description": "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.",
      "properties": {
        "url": {
          "type": "string",
          "description": "Signed URL to the mp3 (24h TTL)."
        },
        "seconds": {
          "type": "number",
          "description": "Estimated spoken duration (digest gist chars / 15), the billing basis — not measured from the mp3."
        },
        "voice": {
          "type": "string",
          "description": "The TTS voice actually used (after per-provider fallback/mapping)."
        },
        "provider": {
          "type": "string",
          "enum": [
            "openai",
            "groq"
          ]
        },
        "lang": {
          "type": "string",
          "description": "ISO-639-1 language the audio is spoken in; present only when a translate target drove a target-language digest."
        }
      }
    },
    "CommentSentiment": {
      "type": "object",
      "description": "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.",
      "properties": {
        "positive_pct": {
          "type": "number",
          "description": "0-100; positive_pct + neutral_pct + negative_pct === 100 (normalized)."
        },
        "neutral_pct": {
          "type": "number"
        },
        "negative_pct": {
          "type": "number"
        },
        "summary": {
          "type": "string",
          "description": "2-3 plain English sentences characterizing the overall audience reaction."
        },
        "top_themes": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Up to 5 short recurring themes across the comments."
        },
        "representative": {
          "type": "object",
          "description": "One exemplar comment per polarity, when found.",
          "properties": {
            "positive": {
              "type": "string"
            },
            "negative": {
              "type": "string"
            }
          }
        }
      }
    },
    "TranscriptSegment": {
      "type": "object",
      "description": "One timed transcript segment: start/end offsets in seconds (floats, relative to the video start) and the segment's text.",
      "required": [
        "start",
        "end",
        "text"
      ],
      "properties": {
        "start": {
          "type": "number"
        },
        "end": {
          "type": "number"
        },
        "text": {
          "type": "string"
        }
      }
    },
    "VideoComment": {
      "type": "object",
      "description": "One top-level comment. Data-minimized by design: only the comment text, the author's DISPLAY handle (never a user id, profile URL, or avatar), the like count, and the number of direct replies — all optional except text, omitted when the source doesn't provide them.",
      "required": [
        "text"
      ],
      "properties": {
        "text": {
          "type": "string"
        },
        "author": {
          "type": "string",
          "description": "Display handle only — no user id / profile URL (data minimization)."
        },
        "like_count": {
          "type": "integer"
        },
        "reply_count": {
          "type": "integer",
          "description": "Number of direct replies to this top-level comment."
        }
      }
    },
    "VideoComments": {
      "type": "object",
      "description": "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).",
      "properties": {
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/VideoComment"
          }
        },
        "total_fetched": {
          "type": "integer",
          "description": "How many comments actually came back (after the cap)."
        },
        "cap_applied": {
          "type": "integer",
          "description": "The effective cap that was applied (default 30, hard max 200 via comments_cap)."
        },
        "sort": {
          "type": "string",
          "enum": [
            "top"
          ]
        }
      }
    },
    "VideoStructured": {
      "type": "object",
      "description": "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.",
      "properties": {
        "chapters": {
          "type": "array",
          "description": "Timespan segments of the video.",
          "items": {
            "type": "object",
            "properties": {
              "start_sec": {
                "type": "number"
              },
              "end_sec": {
                "type": "number"
              },
              "title": {
                "type": "string"
              },
              "summary": {
                "type": "string"
              }
            }
          }
        },
        "entities": {
          "type": "array",
          "description": "People/orgs/places/products/other the model saw or heard.",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "person",
                  "org",
                  "place",
                  "product",
                  "other"
                ]
              }
            }
          }
        },
        "products_shown": {
          "type": "array",
          "description": "Products/brands the video shows and/or talks about.",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "evidence": {
                "type": "string",
                "enum": [
                  "visual",
                  "spoken",
                  "both"
                ],
                "description": "How the product was surfaced: only on-screen, only mentioned, or both."
              }
            }
          }
        },
        "claims": {
          "type": "array",
          "description": "Factual claims made in the video, optionally anchored to a timestamp.",
          "items": {
            "type": "object",
            "properties": {
              "text": {
                "type": "string"
              },
              "timestamp_sec": {
                "type": "number"
              }
            }
          }
        },
        "key_moments": {
          "type": "array",
          "description": "Notable moments (hooks, reveals, demo steps) at a specific second.",
          "items": {
            "type": "object",
            "properties": {
              "timestamp_sec": {
                "type": "number"
              },
              "description": {
                "type": "string"
              }
            }
          }
        },
        "keyframe_mode": {
          "type": "string",
          "enum": [
            "scene",
            "uniform"
          ],
          "description": "How the analyzed frames were selected: 'scene' = content-aware cut detection, 'uniform' = even-sampling fallback for static/short clips with too few detected cuts."
        }
      }
    }
  }
}