Metadata vocabularies

Metadata properties are identified by an exact vocabulary string and an exact vocabulary-local property string. Do not rewrite either string during import. Use a standard namespace when a standard already defines the concept; use a stable application-owned namespace only for genuinely application-specific data.

Add and read metadata

A metadata assertion attaches a typed value to a production, asset, representation, resource, or activity under an exact vocabulary and property. Every surface can:

  • append a value, so a property can hold several ordered values;

  • remove every value of a property from a target;

  • read every assertion on a target; and

  • find every assertion that uses an exact vocabulary and property.

The Rust storage API can additionally replace every ordered value of a property atomically. The example adds a language-tagged title to an asset and reads it back from both directions:

static const char *const IPTC_VIDEO_METADATA_HUB =
    "https://iptc.org/std/videometadatahub/recommendation/"
    "iptc-vmhub-1.7-schema.json";

static pp_error_code_t add_title(pp_production_t *production,
                                 const pp_uuid_t *asset_id,
                                 pp_error_t **error) {
  const pp_object_ref_t target = {PP_OBJECT_ASSET, *asset_id};
  pp_metadata_input_t *title = NULL;
  pp_transaction_t *transaction = NULL;
  pp_metadata_set_t *on_asset = NULL;
  pp_metadata_set_t *everywhere = NULL;

  pp_error_code_t status =
      pp_metadata_input_create_string("Interview", "en-US", &title, error);
  if (status == PP_OK) {
    status = pp_production_begin_transaction(production, &transaction, error);
  }
  if (status == PP_OK) {
    status = pp_transaction_add_metadata_value(
        transaction, &target, IPTC_VIDEO_METADATA_HUB, "title", title, error);
  }
  if (status == PP_OK) {
    status = pp_transaction_commit(transaction, error);
  }
  if (status == PP_OK) {
    status = pp_production_metadata(production, &target, &on_asset, error);
  }
  if (status == PP_OK) {
    status = pp_production_find_metadata(production, IPTC_VIDEO_METADATA_HUB,
                                         "title", &everywhere, error);
  }
  for (uint64_t index = 0;
       status == PP_OK && index < pp_metadata_set_count(on_asset); ++index) {
    pp_object_ref_t owner;
    const char *vocabulary = NULL;
    const char *property = NULL;
    const pp_metadata_value_t *value = NULL;
    const char *text = NULL;
    const char *language = NULL;
    status = pp_metadata_set_get(on_asset, index, &owner, &vocabulary,
                                 &property, &value, error);
    if (status == PP_OK &&
        pp_metadata_value_kind(value) == PP_METADATA_LANG_STRING) {
      status = pp_metadata_value_get_string(value, &text, &language, error);
      if (status == PP_OK) {
        printf("%s: %s [%s]\n", property, text, language);
      }
    }
  }

  pp_metadata_set_release(everywhere);
  pp_metadata_set_release(on_asset);
  pp_transaction_release(transaction);
  pp_metadata_input_release(title);
  return status;
}
void add_title(postproject::Production &production,
               const postproject::Uuid &asset_id) {
  const postproject::ObjectRef target{postproject::ObjectKind::asset, asset_id};

  auto transaction = production.beginTransaction();
  transaction.addMetadataValue(
      target,
      "https://iptc.org/std/videometadatahub/recommendation/"
      "iptc-vmhub-1.7-schema.json",
      "title", postproject::MetadataInput::languageString("Interview", "en-US"));
  transaction.commit();
  // The wrapper does not read metadata yet; use pp_production_metadata().
}
def add_title(production: Production, asset_id: AssetId) -> None:
    title = MetadataProperty(
        "https://iptc.org/std/videometadatahub/recommendation/iptc-vmhub-1.7-schema.json",
        "title",
    )
    with production.transaction() as transaction:
        transaction.add_metadata(
            asset_id, title, MetadataLanguageString("Interview", "en-US")
        )

    for assertion in production.metadata[asset_id]:
        print(f"{assertion.property.property}: {assertion.value}")
    assert len(production.metadata_by_property[title]) == 1
fn add_title(production: &mut SqliteProduction, asset_id: AssetId) -> Result<()> {
    let title = MetadataProperty::new(
        VocabularyId::new(
            "https://iptc.org/std/videometadatahub/recommendation/iptc-vmhub-1.7-schema.json",
        )?,
        PropertyId::new("title")?,
    );
    let target = ObjectRef::Asset(asset_id);
    {
        let mut transaction = production.begin_transaction()?;
        transaction.add_metadata_value(
            target,
            &title,
            &MetadataValue::language_string("Interview", "en-US")?,
        )?;
        transaction.commit()?;
    }

    let on_asset = production.metadata(target)?;
    let everywhere = production.query_by_metadata_property(&title)?;
    assert_eq!(on_asset.len(), 1);
    assert_eq!(everywhere.len(), 1);
    Ok(())
}
IPTC_VMHUB=https://iptc.org/std/videometadatahub/recommendation/iptc-vmhub-1.7-schema.json
postproject metadata add-text production.pproj asset "$ASSET_ID" \
  "$IPTC_VMHUB" title "Interview" --language en-US
postproject metadata list production.pproj asset "$ASSET_ID"
postproject metadata find production.pproj "$IPTC_VMHUB" title

All writes belong to an explicit production transaction. A failed operation or rollback leaves no partial assertions.

Typed values

Values are typed rather than stringly encoded: plain and language-tagged text, signed and unsigned 64-bit integers, exact decimals and rationals, booleans, timestamps, URIs, opaque bytes, typed object references, and recursively nested ordered lists and named-field structures. Every surface preserves every value kind on read. In C, recursive input handles copy their children, so callers can release intermediate list and structure values immediately after construction.

CLI input and inspection

The demonstrator can add text or any recursively typed value, list values, find a property, and remove all values of a property. A typed value uses the same tagged JSON shape emitted by --json output. For example, contact.json may contain:

{
  "type": "struct",
  "fields": [
    {"name": "name", "value": {"type": "string", "value": "Camera department"}},
    {"name": "confidence", "value": {"type": "decimal", "coefficient": "995", "scale": 3}}
  ]
}

Write it and inspect it with:

postproject metadata add production.pproj asset "$ASSET_ID" \
  https://example.com/vocabulary contact contact.json

postproject metadata add-text production.pproj asset "$ASSET_ID" \
  https://iptc.org/std/videometadatahub/recommendation/iptc-vmhub-1.7-schema.json \
  title "Interview" \
  --language en-US

postproject --json metadata list \
  production.pproj asset "$ASSET_ID"

postproject --json metadata find production.pproj \
  https://iptc.org/std/videometadatahub/recommendation/iptc-vmhub-1.7-schema.json \
  title

postproject metadata remove production.pproj asset "$ASSET_ID" \
  https://iptc.org/std/videometadatahub/recommendation/iptc-vmhub-1.7-schema.json \
  title

JSON output is explicitly tagged with value types. Decimal coefficients are strings so JSON consumers do not lose precision. Binary values use hexadecimal text. Lists and structured fields are recursive and ordered.

Technical inspection

The Rust media crate defines a MediaInspector adapter boundary and a bounded FfprobeInspector subprocess implementation. Successful results are ordinary typed assertions under https://postproject.org/ns/technical-media/1 with property inspection; no FFmpeg type or dependency enters postproject-core. Raw embedded tag keys and values are represented as ordered key/value structures so unfamiliar tags do not need to become schema fields.

The CLI reaches this adapter with media add --inspect and attaches successful assertions to the imported representation in the same transaction. The direct inspection operation is not currently exposed through C, C++, or Python; those surfaces can read the resulting assertion through their existing metadata traversal APIs.

media resolve --verify reuses a single stored inspection as partial identity evidence when scoring relocated file candidates. The candidate remains ambiguous if another credible match exists. Use --ffprobe PATH to select the inspector executable; an unavailable or failed inspector leaves the other resolver evidence unchanged.

Subprocess output is limited to 8 MiB per stream, execution defaults to a 30-second deadline, JSON and numeric values are parsed without floating point, and stderr diagnostics are truncated. Missing ffprobe, non-zero exit, timeout, oversized output, and malformed JSON are distinguishable outcomes.

Optional Rust registry

The core registry supplies a small set of advisory definitions for IPTC Video Metadata Hub JSON, Dublin Core, XMP Basic, EBUCore, and PostProject-owned metadata. A property hint can describe accepted value kinds, cardinality, labels, descriptions, and known mapping aliases. validate_values applies those rules only when an application explicitly calls it.

Registry lookup uses exact identifiers. An absent vocabulary or property is not an error, and persistence never invokes the registry automatically. This keeps unknown and application-specific metadata fully round-trippable.

Availability

The typed domain model, optional vocabulary registry, and SQLite persistence back every surface. C, Python, Rust, and the CLI read and write every value kind. The C++ wrapper writes every value kind but does not yet wrap metadata reads; C++ integrations call the C read functions directly. Activity metadata is writable after the activity is created in the same or an earlier transaction.

See standards boundaries and the mapping matrix for the intended relationship to IPTC Video Metadata Hub, EBUCore, XMP, and other standards.