Skip to main content

TACIT

Theory-Anchored Coding with Interpretive Transparency.

A Streamlit application that codes interview transcripts against a pluggable theoretical framework, keeps interpretive authority with the researcher, and reports the statistics a reader will actually ask for — including inter-rater reliability computed from genuine double-blind double coding.

Language models can run entirely on your own machine, so transcripts need not leave it.

License: MIT Python 3.9+ Tests DOI


What makes this different

Most qualitative tools treat a language model as an autocoder: feed it a transcript, get codes back, accept them. This one refuses to work that way, for four reasons.

The framework comes from literature, not from the model's memory. A coding framework is the validity foundation of the whole study. Asking a model to recall the dimensions of a theory produces output that is plausible and partly invented, including citations to work that does not exist. Here you retrieve literature from OpenAlex, the model drafts dimensions from the retrieved abstracts, and a hallucination guard rejects any citation that was not actually retrieved. You then approve the framework dimension by dimension, under your own name.

Interpretive authority stays with the researcher. Model coding is a draft. Every code can be confirmed, revised or deleted, and you can add segments the model missed. Every change is written to an audit trail, and the model's original judgement is retained — so "how often did the model need correcting?" becomes a reportable number rather than an unknown.

Anything computable is computed, not estimated. Reliability uses a sampling frame that includes unmarked units, so recall is meaningful — without them, recall is identically 1 and the number is meaningless. When the assumptions of a chi-square test fail, the tool refuses to print a p-value rather than inviting a claim that cannot survive scrutiny.

Your data does not have to leave your machine. Interview transcripts are human-subject data. Whether they may be sent to a third-party service is decided by your ethics approval, and approvals written before generative AI generally do not cover it. Point TACIT at a local model server and nothing goes over the network. It also helps reproducibility: cloud models get withdrawn, and the weights behind a version string change silently, whereas a local model has a fixed weight file you can name in a paper.


Try it

Manual (English) docs/TACIT_User-Manual_en.pdf
Manual (中文) docs/TACIT_User-Manual_zh.pdf
Demo corpora 80 witness transcripts from U.S. congressional hearings on AI governance (public domain) · 24 synthetic English interviews with reference codings · 6 Traditional Chinese

The synthetic interviews ship with a reference coding for every one of them, so you can open every tab and see real results with no API key and no local model. The hearing transcripts are real speech you can check against its source. See Demonstration data.


Evaluating TACIT without a model

You do not need a model to evaluate this software. There are three levels, and the first two need nothing installed beyond the requirements.

Level 1 — no model, no key, our data. Install, launch, and in the sidebar tick the shipped analyses under analyses/. Every analysis tab then works on real coded material: cross-tabulations, co-occurrence, the cross-case matrix, the audit trail in the coding-review tab, the reliability report, and the Excel/Gioia exports. This exercises everything except the calls to the model itself. The shipped codings are read-only — editing one saves a new file and records what it was derived from, so the reference material cannot be overwritten by accident.

Level 2 — no model, no key, your own transcripts. Open the reliability tab and set Build the frame from to Transcripts I upload here. Upload your own .docx files; TACIT splits them into speaking units, draws a blinded sample from a seed you choose, and issues one coding sheet per coder. Take the completed sheets back and you get percentage agreement, Cohen's κ, PABAK, Gwet's AC1 and Krippendorff's α, per code and pooled, with the disagreement list.

That is not the end of it. Turn these codings into an analysis sends a coder's completed sheet into the workspace as ordinary records, and every analysis tab then works on them: crosstabs by case descriptor with the chi-square guard, co-occurrence, the cross-case matrix, lexicon discovery, the Excel and Gioia exports — and theme grouping, which clusters the codes by co-occurrence or by label similarity and leaves the naming to you. A complete two-coder thematic analysis, from transcript to data structure, with no model involved at any point and nothing leaving the machine.

Level 3 — with a model. Either a free Google Gemini key (https://aistudio.google.com/apikey, no payment details required) or a local Ollama model. See Choosing a model provider.

One thing about the free Gemini tier is worth knowing before you plan a run, because it is easy to misread as a broken key. The daily request quota is per project and per model, not per key, so issuing a second key in the same project changes nothing. The limit differs sharply between models: as of September 2026 gemini-3.6-flash allowed 20 requests per day, which will not finish a single transcript, while gemini-3.5-flash-lite completed a 12-transcript windowed run (149 requests) with room to spare. A 429 reply names the quota it hit — look for GenerateRequestsPerDayPerProjectPerModel and the limit: value — and TACIT reports quota errors separately from a busy server, because waiting helps with the second and not with the first.


Install

Requires Python 3.9+.

git clone https://github.com/NYCU-FUN-TECH-LAB/TACIT.git
cd TACIT

Then double-click the launcher for your platform:

Platform Launcher
Windows launch_windows.bat
macOS launch_mac.command
Linux launch_linux.sh

The launcher creates an isolated environment, installs dependencies, picks a free port and opens a browser. First run takes 3–10 minutes.

Prefer to do it manually:

python -m venv .venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -r requirements.txt
streamlit run src/app.py

Install from PyPI

TACIT is also published as a Python package:

pip install tacit-qda
tacit-qda

tacit-qda starts the interface with the current directory as the workspace: frameworks, analyses and lexicons are read from and written there, and the four shipped frameworks are copied into frameworks/ on first use. Arguments after the command go to streamlit run (for example tacit-qda --server.port 8600). The demonstration corpora and benchmark scripts are in this repository, not in the package. pip install "tacit-qda[zh]" adds the Traditional Chinese segmenter.

The engines can also be used from a script:

import tacit_qda                      # puts the TACIT modules on the import path
import tacit_framework as F
F.FRAMEWORK_DIR = tacit_qda.bundled_frameworks_dir()
F.activate_by_id("ri_stilgoe_2013")

Choosing a model provider

Pick the provider at the top of the sidebar. Everything downstream — coding, theme induction, framework drafting — uses whichever you chose.

Provider Needs a key Data leaves your machine Extra Python package
Ollama no no none
OpenAI-compatible endpoint (LM Studio, llama.cpp server, vLLM, LocalAI) no, if it is on localhost no, if it is on localhost none
Google Gemini yes yes google-genai (installed with the requirements)

All three providers work straight after installation. The Google SDK is part of requirements.txt, so a machine without Ollama or any other local model server can use Gemini as soon as TACIT is installed. The local providers use only the Python standard library.

The older google-generativeai package still works and is detected automatically, but Google has ended support for it; the sidebar will say so and point at the replacement.

Local setup in three commands

ollama serve
ollama pull llama3.1:8b-instruct-q4_K_M
streamlit run src/app.py

Then choose Ollama (local) in the sidebar, set the address to http://localhost:11434, and press Test connection.

One thing you must get right: the context window

Ollama's default context window is 4,096 tokens. One interview does not fit. A server that runs out of context truncates the input silently and still returns well-formed JSON — so your coding looks complete while half the transcript was never read. This is the most dangerous failure mode in local deployment precisely because nothing goes wrong on screen.

TACIT therefore sets the context window explicitly (default 32,768), exposes it in the sidebar, estimates the prompt length before sending, and refuses to send a prompt that would not fit. Raise it for long transcripts; it costs memory, roughly 0.5–2 GB per 32k tokens depending on the model.

Batch and scripted runs

export TACIT_PROVIDER=ollama
export TACIT_MODEL=llama3.1:8b-instruct-q4_K_M
export TACIT_BASE_URL=http://localhost:11434
export TACIT_NUM_CTX=32768

The sidebar picks these up as defaults.

What gets recorded

Every analysis record stores the full endpoint descriptor, not just the model name — ollama/llama3.1:8b-instruct-q4_K_M@http://localhost:11434 rather than llama3.1:8b. The same short name means different quantisations on different machines, and a record that only names the model cannot be reproduced.


Pluggable frameworks

Three frameworks ship with the tool, and they are deliberately unalike — each one exercises a different part of the framework contract:

Framework Dimensions Polarity Field What it tests
ri_stilgoe_2013 Anticipation, Reflexivity, Engagement, Responsiveness yes, P/N → 8 codes Responsible innovation the default
utaut_venkatesh_2003 Performance expectancy, Effort expectancy, Social influence, Facilitating conditions none → 4 codes Technology acceptance a framework with no polarity model
esg_disclosure_probe Targets and baselines, Measurement and boundary, Governance and accountability, Stakeholder engagement yes, S/A → 8 codes Sustainability disclosure polarity values that are not P/N; a non-interview corpus

None of them is hard-coded. Dimensions, polarity values, descriptor fields and interface labels live in a JSON file, and the analysis engines, prompts and schema all derive from whichever framework is active. Switching to UTAUT collapses codes from ANT-P/ANT-N to PE, and the polarity sub-tab disappears because that analysis is meaningless without a polarity model.

The third framework guards against a specific failure: code that derives the dimensions from the active framework but still wrote the two polarity values as the literals "P" and "N" in the polarity-balance statistics and in the lexicon coder — which no test caught, because the only other framework that shipped had no polarity model at all and skipped those code paths entirely. esg_disclosure_probe names its poles S (substantiated) and A (asserted), so it does exercise them. tests/test_frameworks_shipped.py runs every framework in frameworks/ through the full analysis pipeline for exactly this reason: a pluggability claim is only worth as much as the least similar framework you have actually run.

Any deductive coding scheme is in scope — template analysis, framework analysis, theory-driven content analysis. Write a JSON file, or build one from retrieved literature in the Framework builder tab.


Demonstration data

Two corpora ship, and they are different kinds of thing. Do not mix them up.

Congressional hearings Synthetic interviews Synthetic, Chinese
Folder demo_data/hearings/ demo_data/en/ demo_data/zh/
What it is real testimony, public record fictional, written by a language model fictional
Transcripts 80 witnesses, 22 hearings (24 form the paper's subset) 24 6
Sectors industry, academia, government, civil society the same four, six each 4
Reference codings none 24, in analyses/ —
Use it for open coding, screenshots, anything you want a reader to be able to verify statistics, agreement against a fixed standard, regression tests CKIP segmentation, language routing

The hearings are U.S. congressional hearings on artificial-intelligence governance, March 2023 to June 2025, retrieved from govinfo.gov and in the public domain under 17 U.S.C. § 105. The record was split at witness level, one file per speaker; prepared statements reprinted in the record were left out because they are written documents, not testimony. manifest.json gives the package identifier, hearing title, date, chamber and source URL for every witness, the hand-assigned sector, and the three witnesses excluded with the reason for each.

python src/make_hearing_corpus.py    # re-fetches from govinfo and rebuilds the corpus

The synthetic interviews are fictional. Transcripts and reference codings alike were written by a language model (Claude) against the responsible-innovation codebook, so every quotation is by construction a clean substring of a passage that exemplifies its code. That is what makes them a fixed comparison standard, and it is also why agreement measured against them is an upper bound — expect lower figures on real transcripts. They must not be cited as empirical data. Read demo_data/en/00_ABOUT_THIS_DATA.txt before using them.

python src/make_demo_data.py         # rebuilds the .docx files and reference codings

Why twenty-four and not six

Six transcripts are enough to walk through the interface and not enough for a single inferential statistic to run. In a 2×4 crosstab of institution type against polarity, expected cell counts land around one or two, the chi-square assumptions fail, and the tool correctly refuses to print a p-value. A newcomer then opens the cross-analysis tab and sees a column of insufficient-data notices — the part of the tool most worth examining shows nothing at all.

With 24 respondents the corpus produces:

Quantity Value
Coded segments 148, of which 36 (24%) are coded more than once
Codes assigned 184 (one per researcher judgement)
Analysis units 181 distinct segment × dimension × polarity
Reliability sampling frame 299 units, 174 of them unmarked
Institution type × polarity χ²(3) = 10.04, p = .018, V = .236, min. expected 18.1
Institution type × dimension χ²(9) = 7.19, p = .617, V = .115, assumptions met
Institution type × full code set assumptions not met, p value withheld

Those three rows are all deliberate: one significant, one null with assumptions met, one withheld. A demonstration corpus in which every test came out significant would be demonstrating the corpus, not the method.

The two code counts differ because they answer different questions. Three segments carry two judgements on the same dimension and polarity, with different rationales; the record keeps both, because a rationale is the researcher's reasoning and not an annotation. The analysis layer counts each segment once per dimension and polarity, because counting one passage twice under the same code would inflate its co-occurrence and Jaccard weights. Both numbers are correct; any report has to say which one it is using.

About the reference codings

The codings in analyses/ were written alongside the transcripts. They are not the output of any particular model run and should not be read as evidence of how well any model performs. They exist so every tab can be opened without an API key.

Using them as a comparison set for reliability is a legitimate use: run the coder yourself, then compare your run against this fixed, deliberately designed scheme.


Handling real interview data

Keep confidential transcripts outside this folder. .gitignore only excludes files that are not already tracked; it is a safety net, not a guarantee. A single git add -f defeats it.

project/
├── tacit/            ← this repository
└── research-data/    ← your transcripts and analyses, never committed

If your ethics approval does not permit sending transcripts to a third-party service, use a local provider and verify the address is on localhost. The sidebar states which case you are in.


What's in each tab

Tab Purpose
Run analysis Code transcripts against the active framework. The only step that uses model capacity
Data & descriptors Edit respondent attributes; these drive every later comparison
Code review Confirm, revise, delete or add codes. Full audit trail
Cross-analysis Crosstabs, co-occurrence, cross-case matrix, polarity balance
Theme structure Two-stage inductive theme induction with a model, or model-free grouping of the codes by co-occurrence or label similarity which you name yourself; Gioia data structure figure with SVG export
Lexicon induction Term discovery, log-odds feature induction, dictionary baseline for auditing the model
Reliability Sampling frame from records or from transcripts you upload, double-blind coding sheets, κ / PABAK / AC1 / Krippendorff α, confusion matrices, model precision and recall, and human codings turned back into analysable records
Export Excel, Word, JSON
Framework builder Retrieve literature from OpenAlex, draft dimensions, approve them individually

Reproducing the numbers in the paper

Every figure quoted in the SoftwareX article comes from one of the scripts below, and each writes its raw output under bench_out/, which is kept in this repository. Run them from the project root with the virtual environment active.

What it produces Command
Demonstration corpus and reference codings (Table 7) python src/make_demo_data.py then python run_tests.py demo
Model comparison: segments, codes, κ against the reference codings, wall time (Table 6) python src/bench_models.py --provider ollama --models llama3:8b
The same for a cloud model python src/bench_models.py --provider gemini --models gemini-3.5-flash-lite --api-key <key>
Agreement against the reference codings on the 299-unit frame (Section 3) python src/bench_agreement.py bench_out/local_8b/records/llama3_8b/en
Single-pass against windowed yield (Section 2.4) python src/bench_yield.py --model llama3:8b
Open coding, one run on the twelve-transcript subset (Table 8) python src/bench_open_coding.py --corpus hearings --model llama3:8b --per-sector 3 --tag 12_run1
The same with a cloud model python src/bench_open_coding.py --corpus hearings --provider gemini --model gemini-3.5-flash-lite --per-sector 3 --tag 12_run1
Table 8 itself: median and range over the archived runs, no model needed python src/bench_open_summary.py
Figure 2, computed from the shipped context formula python paper/make_fig2.py
Figure 6, the three-level data structure python paper/make_fig6.py

Two caveats about exactness.

The cloud row cannot be reproduced identically. Weights behind a stable version string change and older versions are withdrawn, which is one of the arguments the article makes. The archived outputs under bench_out/cloud/ are therefore the record of what that run did; re-running gives a comparable but not identical result.

Runs vary between samples, by a lot. On the same twelve transcripts, in the same order, at the same temperature, the cloud model returned 89, 143 and 156 codes and the local 8B model 11, 27 and 108. One open-coding run is therefore not a reportable number. Give each run its own --tag (the archived ones are 12_run1, 12_run2, 12_run3) so it lands in its own directory; bench_open_summary.py then reports the median and range, and leaves out runs made by a different version of the code, saying which and why.


Testing

python run_tests.py

Seventeen suites, none requiring network access, an API key or a model server. The OpenAlex client is tested against offline fixtures, the provider abstraction against fake transports, and the interface end-to-end with Streamlit's AppTest in both languages and several frameworks.

tests/test_frameworks_shipped.py enumerates the real frameworks/ directory rather than a hard-coded list, and pushes every framework it finds through the whole pipeline — schema, prompt, cross-tabs, co-occurrence, polarity balance, lexicon, review, themes, reliability sampling and interface labels. Adding a framework file adds it to the test run automatically. This suite exists because the pluggability claim had been tested only against frameworks that happened to resemble the default one.

Run one suite:

python run_tests.py demo

Optional: Traditional Chinese segmentation

pip install ckip-transformers

Installed automatically by the launchers. Without it the lexicon module falls back to an n-gram heuristic with no linguistic knowledge of Chinese, and some entries will be wrong word boundaries. Semantic coding does not pass through segmentation and is unaffected.


Citation

If this tool contributes to published work, please cite it. See CITATION.cff; machine-readable metadata is in codemeta.json.

Every release is archived on Zenodo. Version 1.0.0 is 10.5281/zenodo.22882169; 10.5281/zenodo.22882168 always resolves to the latest version.

The built-in frameworks rest on:

  • Stilgoe, J., Owen, R., & Macnaghten, P. (2013). Developing a framework for responsible innovation. Research Policy, 42(9), 1568–1580. https://doi.org/10.1016/j.respol.2013.05.008
  • Venkatesh, V., Morris, M. G., Davis, G. B., & Davis, F. D. (2003). User acceptance of information technology: Toward a unified view. MIS Quarterly, 27(3), 425–478. https://doi.org/10.2307/30036540

Statistical methods:

  • Monroe, B. L., Colaresi, M. P., & Quinn, K. M. (2008). Fightin' words. Political Analysis, 16(4), 372–403.
  • Gwet, K. L. (2008). Computing inter-rater reliability and its variance in the presence of high agreement. British Journal of Mathematical and Statistical Psychology, 61(1), 29–48.
  • Gioia, D. A., Corley, K. G., & Hamilton, A. L. (2013). Seeking qualitative rigor in inductive research. Organizational Research Methods, 16(1), 15–31.

Limitations

  • Model coding is a draft, not ground truth. Unreviewed coding should not be written up as verified.
  • Small local models produce noticeably fewer segments and follow the output format less reliably than large ones. If a model keeps failing to return parseable JSON, it is usually too small or is a base rather than an instruction-tuned model.
  • Theme induction under-covers dimensions, and which dimension it drops varies by model. Running the shipped 24-transcript corpus through four local models gave four different answers: qwen2.5:7b covered 1 of the 4 dimensions, llama3:8b missed anticipation, mistral:7b missed reflexivity, gemma3:12b missed responsiveness — even though the corpus contains 43–49 codes in every dimension by construction. Do not read a missing dimension in the data structure figure as a finding. The Theme structure tab now says so explicitly when a dimension receives no themes; check the first-order concepts before concluding anything, and prefer the largest model you can run.
  • Lexicon quality depends on the segmenter; without CKIP some Chinese entries will be wrong word boundaries.
  • Many statistics are meaningless at small n. The tool withholds figures whose assumptions fail, but it cannot tell you whether twenty-four respondents support your conclusion.
  • Framework construction depends on OpenAlex coverage. Niche fields, non-English literature and very recent work may not be retrievable.
  • This tool will not make a study rigorous. It makes what you did traceable, reportable and checkable.

License

MIT — see LICENSE.txt.

Release files for tacit-qda 1.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tacit-qda 1.1.1
File Size Uploaded
tacit_qda-1.1.1.tar.gz 529.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tacit-qda 1.1.1
File Interpreter ABI Platform
tacit_qda-1.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 938.1 kB

Release files / tacit_qda-1.1.1.tar.gz

Download URL tacit_qda-1.1.1.tar.gz
Size 529.0 kB
Tags Source
SHA-256 checksum
How to use checksums
46d586ec57a8dce0a8cd9048976ab0d804130c028f14a5432ffb8abb2554e91d
BLAKE2b-256 checksum
How to use checksums
e0375f41415c92e9324ef0291c9eac45542689fde89b90b2b30da2cee1864304
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release files / tacit_qda-1.1.1-py3-none-any.whl

Download URL tacit_qda-1.1.1-py3-none-any.whl
Size 409.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
91df88bfc60021eea808c0e4602148897dd7c1fe204cfead8320c62b30c65f5a
BLAKE2b-256 checksum
How to use checksums
554bd9b2d40d39e39abaf463d9463b1ca6d2804f72596cbe6e0e4ff15744e1c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page