Why this is an architecture problem
When people hear "AI against disinformation" they think of models. In practice, most of the hard decisions in a system like GuaIA were about everything around the models: how formats are routed, how long each step may take, how results are combined and what a person sees at the end.
This article is a set of design principles, not a description of GuaIA's internals. They are the ones I reach for after working on the system as a co-architect, where the constraints were four content types (text, images, video and audio), a response in under four seconds, and Python and NestJS services that had to behave as one product.
1. Put every content type behind the same contract
Each modality needs its own analysis, but the rest of the system should not care. If the API layer has to know how a video is analyzed, every new capability becomes a change in five places.
A small, explicit contract keeps that boundary clean:
type Evidence = {
signal: string // e.g. "manipulated-audio", "known-false-claim"
confidence: number // 0..1
detail?: string // human-readable explanation
}
type AnalysisResult = {
modality: 'text' | 'image' | 'video' | 'audio'
status: 'ok' | 'timeout' | 'error'
evidence: Evidence[]
}
interface Analyzer {
readonly modality: AnalysisResult['modality']
analyze(content: Buffer, signal: AbortSignal): Promise<AnalysisResult>
}This snippet is illustrative, not production code. The point is the shape: every analyzer returns evidence with a confidence, and failure is a value (timeout, error), not an exception that takes the whole request down.
2. Treat latency as a budget you spend
"Under four seconds" is only achievable if you decide in advance who gets how much of it. I think about it as a budget: a slice for ingestion, a slice for the slowest analyzer, a slice for combining results.
Two consequences follow. Independent analyzers should run in parallel, and each one needs its own deadline, so one slow path cannot consume the budget of the others:
const withDeadline = (analyzer: Analyzer, content: Buffer, ms: number) =>
analyzer.analyze(content, AbortSignal.timeout(ms)).catch(
(): AnalysisResult => ({ modality: analyzer.modality, status: 'timeout', evidence: [] })
)
const results = await Promise.all(analyzers.map(a => withDeadline(a, content, 2500)))A partial answer that arrives on time is usually more useful than a complete one that does not.
3. Return evidence, not verdicts
For a system that touches public debate, a bare "fake" or "real" label is the wrong output. What helps a reviewer is the evidence: which signals fired, how confident each one is and what is missing because an analyzer timed out.
That also keeps the system honest about uncertainty. A result with low-confidence evidence from two modalities should read differently from one with high-confidence evidence from a single modality, and your response format should make that visible.
4. Keep a human at the end of the loop
GuaIA is designed as a support tool: the final decision on any measure belongs to a person. That principle has engineering consequences. You need stable identifiers for every analysis, an audit trail of what the system said and when, and an interface that supports review rather than automating a conclusion.
5. Separate the data you store from the data you analyze
A detector ends up touching several databases: reference material, past analyses, operational metadata. I prefer to hide each of them behind the service that owns it, so the analysis path can be tested without the whole data estate and each store can change on its own schedule.
Takeaways
- One contract for every modality keeps the system extensible.
- Latency is a budget: parallelize, set deadlines per analyzer, accept partial results.
- Return evidence and confidence, never a bare verdict.
- Design the human review step first. The automation fits around it.


