The export format

Every project saved with “Save to Computer…” is a tt_export file: one plain-text file holding the whole project — passages, sentences and templates. This page describes what is in that file, what will not change about it, and how to read it with your own tools.

What is in the file

A saved project is ndjson: UTF-8 text, one JSON object per line, with no brackets or commas holding the whole file together. Read it one line at a time — each line stands on its own, so a large project can be streamed rather than loaded whole.

The first line names the project. Every line after it is one document, labelled with the collection it belongs to: a template (meta), a passage, or a sentence.

{"doctype":"tt_export","version":2,"name":"Florble","description":"Fieldwork, spring","exported_at":1787350000.0}
{"collection":"meta","doc":{"id":"template_1787253162080_Default template","data":{"kind":"template","name":"Default template","tracks":{"word_tracks":[{"name":"IPA","type":"T2IPA"},{"name":"Gloss","type":"Text"}],"sentence_tracks":[{"name":"Translation"}]}}},"internal":{"…":"…"}}
{"collection":"passages","doc":{"id":"nY1oxUoyOi9dxjF","data":{"name":"P1","word_tracks":[{"name":"IPA","type":"T2IPA"},{"name":"Gloss","type":"Text"}],"sentence_tracks":[{"name":"Translation"}],"track_template":"template_1787253162080_Default template"}},"internal":{"…":"…"}}
{"collection":"sentences","doc":{"id":"nY1oxUoyOi9dxjF-0000000000","data":{"words":[{"IPA":"ɲɔnwɔ","Gloss":"dog"}],"sentences":{"Translation":"the dog"},"grammatical":true}},"internal":{"…":"…"}}

Two things about that shape matter most:

Read doc; ignore internal. Every document line carries both. doc is the external view — the one this page describes and the schema constrains. internal is the app's own working state, kept in the file so that loading the project back restores it exactly as it was. It is deliberately undocumented, it may change shape at any time without the version changing, and nothing outside the app should read it.

A sentence knows its passage by its id. A sentence's id is its passage's id, then a hyphen, then a counter — nY1oxUoyOi9dxjF-0000000000 belongs to passage nY1oxUoyOi9dxjF. That prefix is the one link between collections, and it is stable.

Inside doc.data

data holds the linguistic content, arranged for reading rather than for the app's convenience:

  • Word tracks are keyed by track name. words is one object per word: {"IPA": "ɲɔnwɔ", "Gloss": "dog"}. Sentence-level tracks work the same way, in sentences.
  • T2IPA tracks are already IPA. A track entered in tipa notation (see section 4.4 of the manual) appears in the file as the rendered IPA — ɲɔnwɔ, not \textltailn On^{w}O.
  • Judgments are plain booleans: grammatical and infelicitous, matching the checkboxes in the app.
  • A passage carries its own track configuration in word_tracks and sentence_tracks, plus track_template: the id of the template it was created from, which is how you join a passage back to the meta collection.
  • Every string is NFD-normalized Unicode, so a character and its combining marks are written the same way no matter which keyboard typed them. This matters the moment you compare or sort strings: normalize your search terms the same way before comparing (unicodedata.normalize('NFD', s) in Python, s.normalize('NFD') in JavaScript).

Examples

jq recipes, to copy or adapt. IPA and Translation are track names from the example above — substitute the ones your project uses:

# every IPA line in the project, one sentence per line
jq -r 'select(.collection=="sentences") | [.doc.data.words[].IPA] | join(" ")' 'My Project.json'

# sentences with their translations, as TSV for a spreadsheet
jq -r 'select(.collection=="sentences") | [([.doc.data.words[].IPA] | join(" ")), .doc.data.sentences.Translation] | @tsv' 'My Project.json' > sentences.tsv

# the passages, with the id each sentence id is prefixed by
jq -r 'select(.collection=="passages") | [.doc.id, .doc.data.name] | @tsv' 'My Project.json'

The same walk with the Python reader below:

import tt_export

export = tt_export.load('My Project.json')
print(export.header.name, len(export.passages), 'passages')

for passage in export.passages:
    print(passage.data['name'])
    for sentence in export.sentences_for(passage):
        words = [w.get('IPA', '') for w in sentence.data.get('words', [])]
        print('   ', ' '.join(words), '--', sentence.data['sentences'].get('Translation', ''))

If you write your own reader

Four rules:

  1. Read doc, never internal. The internal half of a document line carries no promises.
  2. Ignore lines and fields you do not recognize. Fields may be added without a version bump, and a future release may add a collection that does not exist today. Skipping the unfamiliar is what makes a reader survive them. A change that would break a reader that does this — a field removed, or one whose meaning changes — comes with a higher version number, which is on the first line of every file.
  3. Treat rev (and imported.rev) as opaque tokens. They can be compared for equality and nothing else — no ordering, no structure, no meaning outside the database they came from.
  4. Treat creator and modifier as approximate, unverified provenance. They are opaque ids, useful for telling two contributors apart and for knowing roughly who to ask about a strange entry. They are not identity anyone vouches for — a file can be edited by anybody who has it.

Older files

Files whose first line begins {"v":1, were written by an earlier version of TT, before this format existed. They are a different thing — a database replication dump — and they do not follow anything on this page. TT still loads them, and re-saving the loaded project converts it to the current format. Note that those older files did not include templates, so templates cannot be recovered from one.

Schema reference

Everything a file may contain, from the schema itself:

tt_export lineversion 2
objectstringnumberbooleanarrayexact valuerequired— everything else is optional; click a row to open it
Line 1identifies the file and the project it holds
headerobjectheader
·doctyperequiredconst"tt_export"
·versionrequiredinteger
·namerequiredstring

Project name.

·descriptionrequiredstring

Project description.

·exported_atnumber

Seconds since the Unix epoch, at export time. Optional.

·project_idstring

The project this file came from. Written by a server export; absent from a file the browser wrote, which has no id to give.

·update_seqstring

An opaque cursor for the moment this snapshot was read. Hand it back to a change feed and nothing between the two is missed. Server exports only.

Every other lineone document: a template, a passage, or a sentence
documentobjectdocument
·collectionrequiredenum"meta""passages""sentences"

meta holds project-level documents such as track templates; passages and sentences hold the linguistic data. A sentence's id is prefixed by its passage's id.

docrequiredobjectenvelope
·idrequiredstring^[^_]

Document id, unique within its collection. Sentence ids are '<passage id>-<10-digit counter>'.

·revstring

Opaque revision token of the source database at export time. Comparable for equality only; carries no ordering or structure.

·created_datenumber

Seconds since the Unix epoch.

·modified_datenumber

Seconds since the Unix epoch.

·creatorstring

Opaque user id: unverified approximate provenance, not identity the system vouches for.

·modifierstring

Opaque user id; see creator.

importedobject

Present when this document arrived in its project via a file import. creator/modifier upstream of an import are file-supplied and unverifiable.

·datarequiredobject

The external view of the document's payload: every string NFD-normalized Unicode, with per-collection shapes below. Every field the writer emits here is documented in this schema — undocumented internal fields live only in internal until deliberately promoted. Derived at export and ignored on import — a hand-edited data never enters a database.

·internalrequiredobject

The app's own document state, 1:1 with its database, and the only thing the app reads on import. Its shape is deliberately UNSPECIFIED: it may change without a version bump and may drift toward doc over time. Nothing outside the app may depend on it — everything meant for reading is in doc.

when collection = "passages"
doc.dataobjectpassageData
·namestring
·descriptionstring
word_tracksarrayof wordTrackConfig
sentence_tracksarrayof sentenceTrackConfig
·track_templatestring

The id of the meta template this passage was created from — the join key to the meta collection's template documents.

when collection = "sentences"
doc.dataobjectsentenceData
·wordsrequiredarrayof object

One object per word: track name -> NFD display value.

·sentencesrequiredobject

Sentence-level track values, keyed by track name — e.g. this sentence's English translation and its French translation.

·grammaticalboolean

The grammaticality judgment for this sentence.

·infelicitousboolean

The felicity judgment for this sentence.

when collection = "meta"
doc.dataobjectmetaData

Project-level documents. Track templates carry kind/name/tracks; a meta document of no known kind exports an empty data shell (its content is in internal only).

·kindconst"template"
·namestring
tracksobject
signature
signatureobjectsignature

The last line of a signed file, vouching for every byte above it. Written by a server that holds a signing key; a file the browser wrote has none, and an unsigned file is perfectly valid. Verifying needs only the public key named by kid.

·doctyperequiredconst"tt_export_sig"
·algrequiredstring

The construction. Ed25519-SHA256 means: SHA-256 over every byte above this line, then Ed25519 over the canonical JSON of {alg, kid, project_id, exported_at, sha256} prefixed with "tt_export_sig.v1\n". The claims are signed too, so alg and kid cannot be rewritten on a file that still verifies.

·kidrequiredstring

Which public key verifies this, so keys can rotate without old files becoming unverifiable.

·project_idstring
·exported_atnumber

Epoch seconds, fractional — the same value the header carries.

·sha256requiredstring^[0-9a-f]{64}$

Lowercase hex SHA-256 of every byte above this line.

·sigrequiredstring

base64url, unpadded, of the Ed25519 signature.

Rendered from https://www.twisted-tongues.com/schemas/tt-export-v2.json, the schema itself.