<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Janos Tolgyesi</title>
    <description>The latest articles on DEV Community by Janos Tolgyesi (@mrtj).</description>
    <link>https://dev.to/mrtj</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F500112%2F83c78d1f-c65f-43e0-872b-cba066067fb6.png</url>
      <title>DEV Community: Janos Tolgyesi</title>
      <link>https://dev.to/mrtj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrtj"/>
    <language>en</language>
    <item>
      <title>Mind the layers: a three-layer model for document AI</title>
      <dc:creator>Janos Tolgyesi</dc:creator>
      <pubDate>Wed, 30 Sep 2026 08:28:19 +0000</pubDate>
      <link>https://dev.to/mrtj/mind-the-layers-a-three-layer-model-for-document-ai-3653</link>
      <guid>https://dev.to/mrtj/mind-the-layers-a-three-layer-model-for-document-ai-3653</guid>
      <description>&lt;p&gt;In case you missed the previous episode: I argued that there is no single universal schema for what a document &lt;em&gt;means&lt;/em&gt;, because what matters about a document is relative to the question you are asking. Knowledge is structured by use, the categories we extract are tools rather than universal truths, and the design move that follows is to &lt;em&gt;equip the inquiry&lt;/em&gt; rather than model the document. That was the stance. This post is the architecture.&lt;/p&gt;

&lt;p&gt;This is the second of a three-part series. &lt;a href="https://dev.to/mrtj/pragmatism-for-document-ai-4hjk"&gt;Part one&lt;/a&gt; argued the case; part three will get into implementation. Here, in the middle, I want to make it concrete with a model I have come to rely on: the knowledge you pull out of a document lives on &lt;em&gt;three layers&lt;/em&gt;, and the layers differ in how much of the work you can reuse from one document or task to the next. Then I will argue the single rule that keeps such a system debuggable when it breaks in production, which it will: &lt;strong&gt;never skip a layer&lt;/strong&gt;. The model is the tidy part; the rule is the part that earns its keep at three in the morning. Let's dive deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three layers
&lt;/h2&gt;

&lt;p&gt;Here is the model in one sentence: the knowledge you extract from a document sits on three layers, and each layer has both a different &lt;em&gt;reuse profile&lt;/em&gt; (how much of it carries over to the next document or task) and a different &lt;em&gt;epistemic status&lt;/em&gt; (the kind of question it answers). I will take each layer in three beats: what it is, how reusable it is, and what kind of question it settles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 is the document's intrinsic structure&lt;/strong&gt;: pages, blocks, tables, reading order, sections, signatures, the geometry of the thing on the page. This layer is &lt;em&gt;fully reusable&lt;/em&gt;, because every document is, first and before anything else, a document: a contract, an invoice, a vaccination certificate, and a novel are all pages with blocks and a reading order. Its epistemic status is &lt;em&gt;perception&lt;/em&gt;: it answers "what is physically on the page?" It is domain-free and, increasingly, engine-detectable by general-purpose &lt;a href="https://en.wikipedia.org/wiki/Document_layout_analysis" rel="noopener noreferrer"&gt;document layout analysis&lt;/a&gt;. Build it once, reuse it everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 is the domain's entities and relations&lt;/strong&gt;: the parties, dates, amounts, issuing authorities, and cross-references that a domain keeps caring about across documents. This layer is &lt;em&gt;partially reusable&lt;/em&gt;. A sparse &lt;a href="https://en.wikipedia.org/wiki/Upper_ontology" rel="noopener noreferrer"&gt;upper ontology&lt;/a&gt; generalizes (most legal documents have parties and effective dates), and each domain extends it with its own. Its epistemic status is &lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Symbol_grounding_problem" rel="noopener noreferrer"&gt;grounding&lt;/a&gt;&lt;/em&gt;: it answers "what is this document about, in domain terms?" It is the layer that turns a block of text at coordinates on page four into "the governing-law clause naming the State of New York."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 is workflow-specific knowledge&lt;/strong&gt;: "is this a duplicate payment?", "is this clause enforceable?", "summarize this filing for the board." This layer is &lt;em&gt;not reusable, and it should not be&lt;/em&gt;. Its epistemic status is &lt;em&gt;inference&lt;/em&gt;: it answers "what does this particular workflow need to conclude?" This is part one's "equip the inquiry" layer, wearing its architectural hat. Each workflow builds the slice of knowledge its question needs, and retires it when the question changes.&lt;/p&gt;

&lt;p&gt;None of this is specific to legal or financial documents. Run the same model over a novel and the layers still fall out cleanly: chapters, paragraphs, and dialogue at Layer 1; characters, places, and events at Layer 2; a question like "how does John change across the book?" at Layer 3. I will come back to that.&lt;/p&gt;

&lt;p&gt;The following diagram shows the model, and the shortcut it warns against.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hr0h6bkrse7fi81fhpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hr0h6bkrse7fi81fhpj.png" alt="Flowchart: a raw document flows down through Layer 1 (intrinsic structure, perception), Layer 2 (domain entities and relations, grounding) and Layer 3 (workflow knowledge, inference) to the workflow answer, while a dashed arrow skips straight from the document to the answer, labelled: errors compound, failure undiagnosable." width="736" height="1023"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Diagram by the author&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hold on to the three epistemic statuses, because they turn out to be the point: &lt;em&gt;perception&lt;/em&gt;, &lt;em&gt;grounding&lt;/em&gt;, and &lt;em&gt;inference&lt;/em&gt; are three different kinds of question, and a system that keeps them apart is a system that can tell you which one it got wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shapes of Layer 2
&lt;/h2&gt;

&lt;p&gt;Layers 1 and 3 are easy to picture. Layer 2 is the interesting one, because it looks quite different from one domain to the next, and it is where most of the design judgment goes. Three contrasting examples make the point.&lt;/p&gt;

&lt;p&gt;The first is the humble invoice, the same example that closed part one. An invoice has a &lt;em&gt;rich, stable&lt;/em&gt; Layer-2 vocabulary: issuer, recipient, line items, amounts, tax, dates, a reference number. This is the reusable core I promised to give a home to, and here it is: capture that vocabulary once and reuse it across every workflow that touches invoices. For the invoice, Layer 2 is thick, and it is mostly a matter of naming the entities that are reliably there.&lt;/p&gt;

&lt;p&gt;The second is a legal contract, and it behaves differently. Its &lt;em&gt;stable&lt;/em&gt; vocabulary is comparatively &lt;em&gt;thin&lt;/em&gt;: parties, governing law, a few effective dates. The real Layer-2 work is not naming entities; it is two harder jobs. The first is &lt;em&gt;reference resolution&lt;/em&gt;. A contract cites statutes, articles of law, other clauses, and external agreements, and resolving each citation to a stable, canonical identifier (the task known in NLP as &lt;a href="https://en.wikipedia.org/wiki/Entity_linking" rel="noopener noreferrer"&gt;entity linking&lt;/a&gt;) is what lets an agent &lt;em&gt;reason about&lt;/em&gt; the reference rather than merely follow the link: is the cited provision apt, is it still in force, has it been abrogated? Resolving a reference to a canonical identity is the precondition for reasoning about it, because the identifier is a key into a knowledge base where facts about the reference live, not just a pointer to its text. The second is &lt;em&gt;defined-term binding&lt;/em&gt;. Contracts define their own terms ("Interest Rate" means what section 1.1 says it means, not what a dictionary says), so binding a defined term to the contract's own definitions section, a within-document &lt;a href="https://en.wikipedia.org/wiki/Coreference" rel="noopener noreferrer"&gt;coreference&lt;/a&gt; between the term and the clause that defines it, is a first-class grounding task. Bind "Interest Rate" to its everyday sense and you have made a &lt;em&gt;grounding error&lt;/em&gt;, quietly, and everything Layer 3 concludes from it will be wrong in a way that looks perfectly reasonable.&lt;/p&gt;

&lt;p&gt;The third is not a legal or financial document at all, and that is the point of including it. As a thought experiment, take a novel. Its Layer 2 is a cast of &lt;em&gt;characters&lt;/em&gt;, the &lt;em&gt;places&lt;/em&gt; they move through, the &lt;em&gt;events&lt;/em&gt; that occur, and the &lt;em&gt;timeline&lt;/em&gt; that orders them. The characteristic Layer-2 work here is neither a fixed vocabulary nor citation resolution but &lt;em&gt;entity tracking&lt;/em&gt; across a long narrative: knowing that the "he" on page three hundred is the boy introduced on page five (coreference again), and reconstructing a chronology from a story told out of order. Put a Layer-3 question to it, "how does John's character develop across the book?", and the discipline is unchanged: get the structure and the entities in place before you reason about the arc. The model does not seem to mind the domain. It seems to work.&lt;/p&gt;

&lt;p&gt;The point is not that one document is harder than another. It is that Layer 2 is real everywhere, but its &lt;em&gt;thickness varies by domain&lt;/em&gt;: for the invoice it is mostly vocabulary, for the contract it is mostly resolution and binding, for the novel it is tracking a cast of entities and a timeline across a long text. The machinery that does the contract job well (detecting references, resolving them, binding defined terms) is a field note of its own, which I will write separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Layer 2 sparse, Layer 3 disposable
&lt;/h2&gt;

&lt;p&gt;There is a standing temptation to let Layer 2 grow. A workflow needs some derived fact, computing it once in the shared layer feels efficient, and so a workflow convenience quietly migrates downward into Layer 2. Resist it. Every workflow-specific convenience you bake into Layer 2 erodes the one property that made it worth sharing: its reusability. The discipline is a two-sided rule: keep Layer 2 &lt;em&gt;sparse and stable&lt;/em&gt;, and keep Layer 3 &lt;em&gt;rich and disposable&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A concrete case. A due-diligence review and a litigation-risk review run over the same contracts, and they overlap: both care about which obligations survive termination. The instinct is to compute "surviving obligations" once, push it down into Layer 2, and let both share it. Resist it: "surviving obligations" is not a stable domain fact but a task-shaped judgment, and the two reviews define it differently. What belongs in Layer 2 is the raw material they agree on (the termination clause, the obligations it names); the interpretation stays in each review's Layer 3, even though both interpret the same clause. That overlap is not duplication to factor away; hoisting the judgment into Layer 2 would bake one review's assumptions into the shared layer and couple the two. The test: a fact that is task-independent and stable belongs in Layer 2; a judgment whose definition depends on the question stays in Layer 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never skip a layer
&lt;/h2&gt;

&lt;p&gt;Now the rule the whole post is named for. Never go straight from the raw document to the language model. The shortcut is seductive: drop the whole PDF, or a text dump of it, into the prompt, ask your Layer-3 question, and let a capable model sort out the rest. In a demo it works beautifully. In production it fails, and worse, it fails in a way you cannot debug.&lt;/p&gt;

&lt;p&gt;The reason is that the three layers are three distinct places where a mistake can happen, and the shortcut collapses them into one. Consider the chain, kept deliberately generic: a table cell is misread (a &lt;em&gt;perception&lt;/em&gt; error), so an amount is attached to the wrong party (a &lt;em&gt;grounding&lt;/em&gt; error), so the workflow reaches a wrong contractual conclusion (an &lt;em&gt;inference&lt;/em&gt; error). That errors cascade and compound down a chain of dependent stages is not a new observation: it is a long-studied failure mode of language-processing pipelines (&lt;a href="https://aclanthology.org/W06-1673/" rel="noopener noreferrer"&gt;Finkel, Manning, and Ng, 2006&lt;/a&gt;), whose own remedy was to reduce the propagation with joint inference. The layered response makes a different and more modest bet. When you build the layers explicitly, each of those is a separate, inspectable step. When you throw everything into one prompt, all three failure modes happen inside the same opaque call, and when the answer comes back wrong you have no way to ask the only question that matters: &lt;em&gt;which layer failed?&lt;/em&gt; Was the number misread, or read correctly and misattributed, or read and attributed correctly but reasoned about wrongly? One prompt cannot tell you. Three layers can.&lt;/p&gt;

&lt;p&gt;That is the payoff, and it is worth stating plainly, because it is the entire reason to pay the tax of building the layers at all: &lt;strong&gt;explicit layers make a failure localizable to one layer.&lt;/strong&gt; A layered system fails in a &lt;em&gt;place&lt;/em&gt;; a skipped-layer system just fails. And what fails in a place can be &lt;em&gt;tested&lt;/em&gt; in a place: each layer takes its own golden dataset, which is far less noisy than one end-to-end metric whose definition of success is hard to pin down (a topic I will take up in its own post). Diagnosability is not a nice-to-have you add later; it is a property you either designed in or gave away on day one. If this sounds like a familiar engineering discipline, it is: &lt;a href="https://en.wikipedia.org/wiki/Separation_of_concerns" rel="noopener noreferrer"&gt;separation of concerns&lt;/a&gt; applied to document knowledge, the same reason strict layering keeps systems like the &lt;a href="https://en.wikipedia.org/wiki/OSI_model" rel="noopener noreferrer"&gt;OSI stack&lt;/a&gt; tractable, where each layer builds on the one below without reaching around it.&lt;/p&gt;

&lt;p&gt;Going back to the source is not the same as skipping a layer. A Layer-3 task will often need a detail the generalist Layer 2 never captured, and the right move is to reach back and re-read the exact span the lower layers point to: the full text of clause 4.2, or the chapter where the two characters first meet. That is using the layers to navigate back to grounded evidence, not bypassing them. What the rule forbids is the ungrounded shortcut, asking the final question of the raw document with no layers built at all.&lt;/p&gt;

&lt;p&gt;There is a precondition hiding in here, which I will only gesture at now. Layering pays off only if the lower layers hand the upper layers something &lt;em&gt;stable&lt;/em&gt; to point at. If Layer 1's identifiers churn every time you re-extract the document (re-run the extraction pipeline after, say, an OCR or model upgrade), then Layer 2's groundings and Layer 3's conclusions dangle, and the whole structure collapses back into re-extract-everything. Stable identity across re-extraction is what makes layering hold together, and it is the subject of part three.&lt;/p&gt;

&lt;h2&gt;
  
  
  A familiar split
&lt;/h2&gt;

&lt;p&gt;If some of this feels familiar, it may be because you have met it under other names. In a talk I gave on document AI at AWS, I split the pipeline into &lt;em&gt;extraction&lt;/em&gt;, &lt;em&gt;knowledge&lt;/em&gt;, and &lt;em&gt;generation&lt;/em&gt;. Those are the same three layers wearing different labels: extraction is Layer 1, knowledge is Layer 2, generation is Layer 3. I find the layer framing more useful now precisely because it makes the never-skip rule obvious, and the rule is what tells you the split is worth keeping even in an era when a single large model could, in principle, do all three jobs in one call. Being &lt;em&gt;able&lt;/em&gt; to skip the layers is exactly why you need a rule not to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to carry forward
&lt;/h2&gt;

&lt;p&gt;In summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three layers, three reuse profiles.&lt;/strong&gt; Intrinsic structure is fully reusable, domain entities are partially reusable, and workflow knowledge is not reusable at all (and that last one is a feature, not a gap).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each layer answers a different kind of question:&lt;/strong&gt; perception, grounding, inference. Keeping them separate is what lets a system tell you which one it got wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never skip a layer.&lt;/strong&gt; The raw-document-to-model shortcut compounds errors into one undiagnosable failure; explicit layers buy you the ability to say where it broke.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next time you are tempted to pipe a document straight into a prompt and ask the hard question, add the two intermediate stops: extract the structure, ground the entities, and only then infer. It costs more to build and it pays for itself the first time something goes wrong and you can point at the layer that did it.&lt;/p&gt;

&lt;p&gt;The rule leans on a precondition I have only hinted at: the lower layers have to give the upper layers something stable to annotate. In part three I will build that out: a document object model that survives re-extraction, so identity does not churn underneath your groundings and conclusions. Stay tuned.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Janos Tolgyesi is an engineer building document AI systems, currently in legal AI, and an &lt;a href="https://aws.amazon.com/developer/community/community-builders/" rel="noopener noreferrer"&gt;AWS Community Builder&lt;/a&gt; since 2020. Field notes on knowledge management, agents, and context engineering in document AI: &lt;a href="https://mrtj.pro/" rel="noopener noreferrer"&gt;mrtj.pro&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Pragmatism for document AI</title>
      <dc:creator>Janos Tolgyesi</dc:creator>
      <pubDate>Wed, 23 Sep 2026 15:04:44 +0000</pubDate>
      <link>https://dev.to/mrtj/pragmatism-for-document-ai-4hjk</link>
      <guid>https://dev.to/mrtj/pragmatism-for-document-ai-4hjk</guid>
      <description>&lt;p&gt;When a new document type lands on your desk (a contract, an invoice, a vaccination certificate), the first engineering instinct is almost always the same: design &lt;em&gt;the&lt;/em&gt; schema. One data model per document type, rich enough to capture everything important, built once and reused across every workflow that will ever touch that document. It feels like the responsible thing to do. It is also, I have come to think, one of the most expensive mistakes in document AI.&lt;/p&gt;

&lt;p&gt;This is the first of a three-part series on how I have learned to think about knowledge in document AI systems. Before the architecture (part two) and the implementation details (part three), I want to start with the stance, because the stance is what makes the rest coherent. That stance is borrowed, almost wholesale, from a hundred-year-old American philosophical tradition called &lt;a href="https://plato.stanford.edu/entries/pragmatism/" rel="noopener noreferrer"&gt;&lt;em&gt;pragmatism&lt;/em&gt;&lt;/a&gt;. Bear with the philosophy: it pays for itself quickly.&lt;/p&gt;

&lt;p&gt;Let me state the claim up front, and state it carefully. It is not that structure never generalizes; for some documents a surprising amount of it does. It is that no single schema captures everything that matters about a document, because what matters is relative to the question you are asking. Knowledge is structured by use, and the categories we extract are tools rather than universal truths. Let's dive deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The universal-schema instinct
&lt;/h2&gt;

&lt;p&gt;The instinct has a respectable pedigree. It goes back to Aristotle's &lt;a href="http://classics.mit.edu/Aristotle/categories.html" rel="noopener noreferrer"&gt;&lt;em&gt;Categories&lt;/em&gt;&lt;/a&gt;, which set out to enumerate the fundamental kinds of things once and for all: substance, quantity, quality, relation, and so on. Every entity falls under a category, categories have structure, and reasoning becomes tractable. Computer science inherited the ambition directly. When Tom Gruber gave the field its canonical definition in 1993, an &lt;a href="https://tomgruber.org/writing/ontolingua-kaj-1993.pdf" rel="noopener noreferrer"&gt;&lt;em&gt;ontology&lt;/em&gt;&lt;/a&gt; became "an explicit specification of a conceptualization": a formal vocabulary of classes, relations, and constraints that a machine can reason over. The word was not borrowed by accident: computer science took over philosophy's ambition of naming, once and for all, the kinds of things a domain contains.&lt;/p&gt;

&lt;p&gt;For part of the problem, this works beautifully. A document, considered as an artifact, really does have stable structure: pages, blocks, tables, reading order, sections, signatures. That layer generalizes across contracts, certificates, and transcripts, because every one of them is, first, a document. Build it once, reuse it everywhere.&lt;/p&gt;

&lt;p&gt;The instinct rarely stops at the artifact, though. Having modeled the document's structure once, it reaches inward for the same universality: one schema for what the document &lt;em&gt;means&lt;/em&gt;, rich enough that every future workflow can read what it needs off a single representation. That is the move I want to question. Some of what is inside genuinely generalizes, but the slice of meaning any given task actually needs does not settle into one shared form: how you carve up a document's meaning depends on what you are trying to do with it.&lt;/p&gt;

&lt;p&gt;You might reasonably push back here, and the strongest counterexample is the humble invoice. An invoice is so regular that a single generic schema (issuer and recipient, line items, amounts, tax, dates, a reference number) really does apply to almost all of them. That is a genuine, reusable schema, and I do not want to wave it away: capturing that shared core once, rather than reinventing it per project, is exactly the right move. We will give it a home in the next post, as the middle layer of a three-layer model.&lt;/p&gt;

&lt;p&gt;But notice what that reusable core is and is not. It is a stable vocabulary of the entities an invoice tends to contain; it is not a schema that captures everything that matters, because what matters still depends on the question. Ask whether an invoice is a duplicate payment, whether its tax treatment is correct, or whether it is fraudulent, and the decomposition that actually helps diverges each time: which fields you trust, what counts as a match, what "correct" even means. That task-shaped knowledge is the layer the universal instinct cannot pre-model, and the one the next post argues you should build only when a task demands it. A contract makes the point more starkly still: its useful "joints" are completely different when you are checking enforceability, computing tax exposure, or screening for fraud. There is no view-from-nowhere decomposition that captures everything important, because importance is relative to a question. The pragmatist way to put it: decomposition is downstream of use.&lt;/p&gt;

&lt;p&gt;Wittgenstein gave us the sharpest tool for seeing why. Concepts like "contract" are not natural kinds with a shared essence; they are &lt;a href="https://en.wikipedia.org/wiki/Family_resemblance" rel="noopener noreferrer"&gt;&lt;em&gt;family resemblance&lt;/em&gt;&lt;/a&gt; concepts (&lt;em&gt;Philosophical Investigations&lt;/em&gt;, sections 66 to 67), whose members overlap and criss-cross without a single defining feature. Their boundaries shift with jurisdiction and purpose. Trying to write the one definition that captures all contracts is chasing an essence that is not there. Mistaking a meaning problem for a structure problem is the root error, and it is an easy error to make precisely because our tools are so good at the structure part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concepts are tools, not mirrors
&lt;/h2&gt;

&lt;p&gt;A category is just a concept put to work: the slice of meaning we decide is worth extracting. So if knowledge is structured by use, what is such a category actually for? Here the pragmatists are blunt. For &lt;a href="https://plato.stanford.edu/entries/dewey/" rel="noopener noreferrer"&gt;John Dewey&lt;/a&gt;, a concept is not a picture of reality; it is an instrument for resolving a problematic situation. Peirce got there first with his &lt;a href="https://courses.media.mit.edu/2004spring/mas966/Peirce%201878%20Make%20Ideas%20Clear.pdf" rel="noopener noreferrer"&gt;&lt;em&gt;pragmatic maxim&lt;/em&gt;&lt;/a&gt; of 1878: the meaning of a concept is the sum of the practical consequences of holding it. A category earns its place if it makes some downstream workflow succeed, and the same domain can legitimately warrant different categories for different tasks. A field that helps no inquiry is decoration, however correct it looks on paper.&lt;/p&gt;

&lt;p&gt;That is the whole argument, and it points to a single design move: stop trying to model the document, and start trying to equip the inquiry. For any document type there is no one correct ontology, only the ontology that makes a specific downstream question answerable. Expect to hold more than one at a time. Expect to retire them when the question changes. This sounds permissive, but it is the opposite. "Structured by use" is not "anything goes", it is "justified by use": every category has to earn its keep against a real workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  We already do this, on the evaluation side
&lt;/h2&gt;

&lt;p&gt;Here is the part I find quietly reassuring. Machine learning already practices pragmatism; it just practices it downstream. We do not ask whether a model's outputs correspond to reality. We ask whether they hold up across distributions, support good decisions, and stay stable under perturbation. That is Peirce's maxim almost verbatim, meaning cashed out in practical consequences, operationalized as a held-out test set. &lt;a href="https://www.gutenberg.org/files/5116/5116-h/5116-h.htm" rel="noopener noreferrer"&gt;William James&lt;/a&gt; would recognize it at once: truth as what proves itself in practice.&lt;/p&gt;

&lt;p&gt;So the field is pragmatist about how it &lt;em&gt;judges&lt;/em&gt; models, and rationalist about how it &lt;em&gt;designs schemas&lt;/em&gt;, still reaching for the one universal conceptualization that will capture everything. The fix is not more philosophy. It is to bring the stance we already trust for evaluation upstream, to the point where we decide what to extract in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here?
&lt;/h2&gt;

&lt;p&gt;In summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge is structured by use.&lt;/strong&gt; A document has no privileged decomposition; its joints depend on the question you are asking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concepts are tools.&lt;/strong&gt; A category is good if it helps a downstream workflow, so equip the inquiry rather than model the document.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concretely, the next time you reach for the one universal schema of a document type, stop and ask which task it serves: build the schema for that question, expect to hold more than one, and let each category earn its place against a real workflow. None of this argues against structure. The document-as-artifact layer is real, shared, and worth getting right once. The argument is about where universality stops and use begins, and about being honest that most of the interesting knowledge lives on the "use" side.&lt;/p&gt;

&lt;p&gt;In the next part of this series I will make that concrete with a three-layer model that puts each kind of knowledge where it belongs, and a rule that keeps the whole thing diagnosable: never skip a layer. Stay tuned.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Remote View your Computer Vision Models Running on AWS Panorama</title>
      <dc:creator>Janos Tolgyesi</dc:creator>
      <pubDate>Mon, 09 May 2022 15:41:22 +0000</pubDate>
      <link>https://dev.to/mrtj/remote-view-your-computer-vision-models-running-on-aws-panorama-2imp</link>
      <guid>https://dev.to/mrtj/remote-view-your-computer-vision-models-running-on-aws-panorama-2imp</guid>
      <description>&lt;p&gt;Real-time smart video analytics application development and edge device deployment is a tricks task. In the last few years, the industry players built platforms to support this activity. Notable examples include &lt;a href="https://www.nvidia.com/en-us/autonomous-machines/intelligent-video-analytics-platform/" rel="noopener noreferrer"&gt;NVIDIA Metropolis&lt;/a&gt;, &lt;a href="https://www.intel.com/content/www/us/en/developer/tools/openvino-toolkit/overview.html" rel="noopener noreferrer"&gt;Intel’s OpenVINO&lt;/a&gt;, and &lt;a href="https://aws.amazon.com/panorama/" rel="noopener noreferrer"&gt;AWS Panorama&lt;/a&gt;. While these solutions make some aspects of the video-analytics application development more straightforward, there are still many issues to deal with before deploying a video analytics application in production. This post introduces Telescope, the first in a &lt;a href="https://github.com/Neosperience/backpack" rel="noopener noreferrer"&gt;series of open-source tools&lt;/a&gt; to make developing AWS Panorama application simpler.&lt;/p&gt;

&lt;p&gt;AWS Panorama is a machine learning appliance and software framework that allows you to deploy video analytics applications on edge. For a thorough introduction and a step-by-step tutorial on deploying a Panorama application, refer to &lt;em&gt;&lt;a href="https://towardsdatascience.com/deploy-an-object-detector-model-at-the-edge-on-aws-panorama-9b80ea1dd03a" rel="noopener noreferrer"&gt;Deploy an Object-Detector Model at the Edge on AWS Panorama&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;AWS Panorama framework eases many aspects of video analytics application development, including camera connection management, video decoding, frame extraction, model optimization and loading, display output management, over-the-air deployment of your application, and other features. Nevertheless, some tasks are still challenging, including diagnostic tasks when a model does not work as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Telescope
&lt;/h2&gt;

&lt;p&gt;The only available way to get visual feedback on the correct functionality of a Panorama application is to physically connect a display to the HDMI port of the appliance. The display will show the output video stream of a single application deployed on the device. However, physically accessing the appliance is not always feasible. Telescope allows to re-stream the output video of any Panorama application to an external service, for example, to &lt;a href="https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/what-is-kinesis-video.html" rel="noopener noreferrer"&gt;AWS Kinesis Video Streams&lt;/a&gt;. This feature can be very convenient for monitoring an application remotely.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Telescope work?
&lt;/h2&gt;

&lt;p&gt;Telescope instantiates a &lt;a href="https://gstreamer.freedesktop.org/documentation/application-development/introduction/basics.html" rel="noopener noreferrer"&gt;GStreamer pipeline&lt;/a&gt; with an &lt;a href="https://gstreamer.freedesktop.org/documentation/app/appsrc.html" rel="noopener noreferrer"&gt;&lt;code&gt;appsrc&lt;/code&gt;&lt;/a&gt; element at the head. An &lt;a href="https://docs.opencv.org/4.5.5/dd/d43/tutorial_py_video_display.html" rel="noopener noreferrer"&gt;OpenCV&lt;/a&gt; &lt;a href="https://docs.opencv.org/4.5.5/dd/d43/tutorial_py_video_display.html" rel="noopener noreferrer"&gt;&lt;code&gt;VideoWriter&lt;/code&gt;&lt;/a&gt; is configured to write to the &lt;code&gt;appsrc&lt;/code&gt;: instead of saving the consecutive frames to a video file, it streams to the output sink. When opening the &lt;code&gt;VideoWriter&lt;/code&gt; instance, the user should specify the frame width and height and the frame rate of the output stream. You can manually set these parameters or let Telescope infer them from the input dimensions and the frequency you feed new frames to it. If using this auto-configuration feature, some frames (by default 100) will be discarded at the beginning of the streaming because Telescope will use them to calculate statistics of the frame rate and measure the frame dimensions. This is the “warmup” of Telescope. If you send frames with different sizes, Telescope will resize the input, but this comes with a performance penalty. You are also expected to send new frames to Telescope with the frequency specified in the frame-per-second parameter. If frames are sent at a different frequency, the video fragments get out of sync in Kinesis Video Streams, and you won’t be able to replay the video smoothly.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ptokgyxahjd4syvwvly.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ptokgyxahjd4syvwvly.jpeg" alt="Cogwheels of a complicated mechanism" width="800" height="533"&gt;&lt;/a&gt;Photo by &lt;a href="https://unsplash.com/@joshredd" rel="noopener noreferrer"&gt;Josh Redd&lt;/a&gt; on &lt;a href="https://unsplash.com/" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Telescope&lt;/code&gt; is an abstract base class that handles the state machine of the GStreamer pipeline. The concrete implementation KVSTelescope sends frames to &lt;a href="https://aws.amazon.com/kinesis/video-streams/" rel="noopener noreferrer"&gt;Amazon Kinesis Video Streams&lt;/a&gt; service. It is easy to extend Telescope to support other services, especially if there is already a GStreamer plugin for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Telescope into a Panorama application
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configuring the Docker container
&lt;/h3&gt;

&lt;p&gt;Telescope depends on custom compiled external libraries. All these libraries must be compiled and configured correctly in your application’s docker container to make &lt;code&gt;Telescope&lt;/code&gt; work. These libraries include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GStreamer 1.0 installed with standard plugins pack, libav, tools, and development libraries;&lt;/li&gt;
&lt;li&gt;OpenCV 4.2.0, compiled with GStreamer support and Python bindings;&lt;/li&gt;
&lt;li&gt;numpy (it is typically installed by the base docker image of your Panorama application).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the case you want to use &lt;code&gt;KVSTelescope&lt;/code&gt;, the &lt;code&gt;Telescope&lt;/code&gt; implementation that streams the video to Amazon Kinesis Video Streams, it will need also the following libraries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon Kinesis Video Streams (KVS) Producer SDK compiled with GStreamer plugin support;&lt;/li&gt;
&lt;li&gt;Environment variable &lt;code&gt;GST_PLUGIN_PATH&lt;/code&gt; configured to point to the directory where the compiled binaries of KVS Producer SDK GStreamer plugin is placed;&lt;/li&gt;
&lt;li&gt;Environment variable &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt; including the third-party open-source dependencies compiled by KVS Producer SDK;&lt;/li&gt;
&lt;li&gt;boto3 (it is typically installed by the base docker image of your Panorama application).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the source code repository of Telescope, a &lt;a href="https://github.com/Neosperience/backpack/blob/master/examples/Dockerfile" rel="noopener noreferrer"&gt;sample Dockerfile&lt;/a&gt; is provided in the examples folder that shows how to install these libraries and Telescope in any container correctly. In most cases, it is required just to copy the relevant sections from the sample to your application’s Dockerfile. Please note that the first time you compile the docker container, it might take up to one hour to correctly compile all libraries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up AWS IAM privileges for KVSTelescope
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;KVSTelescope&lt;/code&gt; uses the Amazon Kinesis Video Streams (KVS) Producer library, wrapped in a GStreamer sink element, to send the processed frames to the KVS service. KVS Producer needs AWS credentials. It can use the credentials associated with the Panorama Application Role, but you must explicitly configure it. &lt;code&gt;KVSTelescope&lt;/code&gt; needs privileges to execute the following actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kinesisvideo:DescribeStream
kinesisvideo:GetStreamingEndpoint
kinesisvideo:PutMedia
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If KVSTelescope needs to automatically create the Kinesis Video Stream the first time it is used, you should also include the &lt;code&gt;kinesisvideo:CreateStream&lt;/code&gt; action. An example policy allowing KVSTelescope to write data to Kinesis Video Streams could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesisvideo:DescribeStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesisvideo:CreateStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesisvideo:GetDataEndpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesisvideo:PutMedia"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting up credentials
&lt;/h3&gt;

&lt;p&gt;There are two types of AWS security credentials: static and temporary. The former never expire and in case of a leak, you should invalidate them manually and reconfigure the applications. For this reason, their use is strongly discouraged in a production environment. Examples of static AWS credentials include the IAM user’s Access Key Id and Secret Access Key pair.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqrtceqvziyxy34tnreq6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqrtceqvziyxy34tnreq6.jpeg" alt="A photo of a keychain" width="800" height="533"&gt;&lt;/a&gt;Photo by &lt;a href="https://unsplash.com/@mangelinka" rel="noopener noreferrer"&gt;Angela Merenkova&lt;/a&gt; on &lt;a href="https://unsplash.com/" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Temporary credentials expire after a predefined period. In case of a leak, they can be used only within their expiration time, typically in the order of several hours. Temporary credentials can be renewed before they expire so their lifespan can be extended, or they can be exchanged for new ones with a later expiration time. This process needs additional coordination from the application using this type of credentials. Examples of temporary credentials include the AWS Access Key Id, the Secret Access Key, and the Session Token.&lt;/p&gt;

&lt;p&gt;We offer different options to provide credentials with &lt;code&gt;KVSCredentialsHandler&lt;/code&gt; subclasses provided in the &lt;code&gt;kvs&lt;/code&gt; module. If you want to use static credentials for testing purposes, &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html" rel="noopener noreferrer"&gt;create an IAM user&lt;/a&gt; in your AWS account and &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_manage-attach-detach.html" rel="noopener noreferrer"&gt;attach a policy&lt;/a&gt; like the one above to the user. You should configure this user to have programmatic access to AWS resources and get the user’s &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html" rel="noopener noreferrer"&gt;AWS Access Key and Secret Key pair&lt;/a&gt;: create a &lt;code&gt;KVSInlineCredentialsHandler&lt;/code&gt; or &lt;code&gt;KVSEnvironmentCredentialsHandler&lt;/code&gt; instance in the application’s code to pass these credentials to KVS Producer Plugin directly in the GStreamer pipeline definition or as environment variables. However, as these credentials do not expire, using this configuration in a production environment is not recommended. Even in a development and testing environment, you should take the appropriate security measures to protect these credentials: never hardcode them in the source code. Instead, use AWS Secret Manager or a similar service to provide these parameters to your application.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;KVSTelescope&lt;/code&gt; can also use the &lt;a href="https://docs.aws.amazon.com/panorama/latest/dev/permissions-application.html" rel="noopener noreferrer"&gt;Panorama Application Role&lt;/a&gt; to pass the application's credentials to KVS Producer. These credentials are temporary, meaning that they expire within a couple of hours, and the user should renew them before expiration. The Producer library expects temporary credentials in a text file. &lt;code&gt;KVSFileCredentialsHandler&lt;/code&gt; manages the renewal of the credentials and periodically updates the text file with the new credentials. If you want to use this method, attach a policy similar to the example above to your Application Role. Remember to always test your Panorama application — KVS integration if it still works after the &lt;code&gt;KVSFileCredentialsHandler&lt;/code&gt; refreshed the credentials. Let your application run for several hours and periodically check if it continues to stream the video to KVS. You will also find diagnostic information in the CloudWatch logs of your application when the credentials were renewed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment variables
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;KvsTelescope&lt;/code&gt; needs two environment variables to have GStreamer find the KVS Producer plugin. The name of these variables is &lt;code&gt;GST_PLUGIN_PATH&lt;/code&gt; and &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt;. They point to the folder of KVS Producer GStreamer plugin and its 3rd party dependencies. In the sample Dockerfile provided, the correct values of these variables are written to a small configuration file named &lt;code&gt;/panorama/.env&lt;/code&gt; in the container. You should pass the path of this file to &lt;code&gt;KvsTelescope&lt;/code&gt; or otherwise ensure that these environment variables contain the correct value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage example
&lt;/h2&gt;

&lt;p&gt;When initializing a &lt;code&gt;KVSTelescope&lt;/code&gt; instance, you should pass the AWS region name where your stream is created, the name of the stream, and a credentials handler instance. If you want to configure the frame rate and the dimensions of the frames manually, you should also set them here. When both parameters are specified, Telescope skips the warmup period and sends the first frame directly to KVS. When you are ready to send frames, call the &lt;code&gt;start_streaming&lt;/code&gt; method which opens the GStreamer pipeline. After this method is called, you are expected to send new frames to the stream calling the &lt;code&gt;put&lt;/code&gt; method periodically, with the frequency specified in the frame rate or inferred by &lt;code&gt;KvsTelescope&lt;/code&gt;. You can stop and restart streaming on the same &lt;code&gt;KvsTelescope&lt;/code&gt; instance.&lt;/p&gt;

&lt;p&gt;The following example uses the temporary credentials of the IAM Application Role assumed by your Panorama application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;panoramasdk&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;backpack.kvs&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KVSTelescope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KVSFileCredentialsHandler&lt;/span&gt;

&lt;span class="c1"&gt;# You might want to read these values from 
# Panorama application parameters
&lt;/span&gt;&lt;span class="n"&gt;stream_region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;stream_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;panorama-video&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# The example Dockerfile writes static 
# configuration variables to this file
# If you change the .env file path in the 
# Dockerfile, you should change it also here
&lt;/span&gt;&lt;span class="n"&gt;DOTENV_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/panorama/.env&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;panoramasdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# ...
&lt;/span&gt;        &lt;span class="n"&gt;credentials_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KVSFileCredentialsHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;telescope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KVSTelescope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;stream_region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;stream_region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;stream_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;stream_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;credentials_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;credentials_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;dotenv_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DOTENV_PATH&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# This call opens the streaming pipeline:
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;telescope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_streaming&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# called from video processing loop:
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_streams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;streams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;video_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

            &lt;span class="c1"&gt;# Process the stream, for example with:
&lt;/span&gt;            &lt;span class="c1"&gt;# self.process_media(stream)
&lt;/span&gt;
            &lt;span class="c1"&gt;# TODO: eventually multiplex streams 
&lt;/span&gt;            &lt;span class="c1"&gt;# to a single frame
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;telescope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything works well, you can watch the restreamed video on the &lt;a href="https://console.aws.amazon.com/kinesisvideo/home" rel="noopener noreferrer"&gt;Kinesis Video Streams page&lt;/a&gt; of the AWS console. Certainly, you can modify the image before sending it to Telescope: draw annotations on it based on the inference result of the deep-learning model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Annotations
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Annotations&lt;/em&gt; and &lt;em&gt;annotation drivers&lt;/em&gt; provide a unified way to draw annotations on different rendering backends. Currently, two annotation drivers are implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PanoramaMediaAnnotationDriver&lt;/code&gt; allows you to draw on panoramasdk.media object, and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OpenCVImageAnnotationDriver&lt;/code&gt; allows you to draw on an OpenCV image (numpy array) object.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkx808nr4cpf2pfceoky.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkx808nr4cpf2pfceoky.jpeg" alt="A photo of colorful pencils" width="799" height="559"&gt;&lt;/a&gt;Photo by &lt;a href="https://unsplash.com/@lucasgwendt" rel="noopener noreferrer"&gt;Lucas George Wendt&lt;/a&gt; on &lt;a href="https://unsplash.com/" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The library can draw two types of annotations: labels and boxes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using annotations
&lt;/h3&gt;

&lt;p&gt;Depending on the available backends, you can create one or more annotation driver instances at the beginning of the video frame processing loop. During the process of a single frame, you are expected to collect all annotations to be drawn on the frame in a python collection (for example, in a &lt;code&gt;list&lt;/code&gt;). When the processing is finished, you call the &lt;code&gt;render&lt;/code&gt; method on any number of drivers, passing the same collection of annotations. All coordinates used in annotation are normalized to the &lt;code&gt;[0; 1)&lt;/code&gt; range.&lt;/p&gt;

&lt;p&gt;Example usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;panoramasdk&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;backpack.annotation&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LabelAnnotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RectAnnotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimestampAnnotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OpenCVImageAnnotationDriver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;PanoramaMediaAnnotationDriver&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;panoramasdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# self.telescope = ... 
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;panorama_driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PanoramaMediaAnnotationDriver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cv2_driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenCVImageAnnotationDriver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# called from video processing loop:
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_streams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;streams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;video_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;annotations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="nc"&gt;TimestampAnnotation&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="nc"&gt;RectAnnotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;point1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                    &lt;span class="n"&gt;point2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nc"&gt;LabelAnnotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hello World!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;panorama_driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# TODO: eventually multiplex streams to a 
&lt;/span&gt;            &lt;span class="c1"&gt;# single frame
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cv2_driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;# self.telescope.put(stream.image)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Postface
&lt;/h2&gt;

&lt;p&gt;Even if Telescope can be a helpful tool, its usage might raise two concerns that you should consider carefully. We discourage using Telescope in a production environment: it is a development aid or and a debugging tool.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvtzdgtuep77n7dxenbw0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvtzdgtuep77n7dxenbw0.jpeg" alt="A photo of a mountain path with a table saying danger" width="800" height="450"&gt;&lt;/a&gt;Photo by &lt;a href="https://unsplash.com/@greg_rosenke" rel="noopener noreferrer"&gt;Greg Rosenke&lt;/a&gt; on &lt;a href="https://unsplash.com/" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The first concern is of technical nature. Currently, the application code in a Panorama app does not have direct access to the onboard GPU; thus, all video encoding done by Telescope run on the device’s CPU. This behavior could slow down the appliance: streaming a single output stream with Telescope could require anything between 10–30% of the CPU capacity of the device.&lt;/p&gt;

&lt;p&gt;The second concern is related to data protection. The Panorama appliance is designed to protect the video streams being processed. It has two ethernet interfaces to separate the network of the video cameras (typically a closed-circuit local area network) from the device’s Internet access. Using Telescope, you relay the video stream from the protected, closed-circuit camera network to the public Internet. You should carefully examine the data protection requirements of your application and the camera network before using Telescope. Moreover, you are responsible for keeping private all AWS credentials used by Telescope, as required by &lt;a href="https://aws.amazon.com/compliance/shared-responsibility-model/" rel="noopener noreferrer"&gt;AWS Shared Responsibility Model&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Telescope&lt;/code&gt; is part of &lt;code&gt;Backpack&lt;/code&gt;, a broader set of tools that aims to help software development on AWS Panorama. Other components will be presented in a future post, so keep tuned and follow us to get updated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Backpack&lt;/code&gt; is &lt;a href="https://github.com/Neosperience/backpack" rel="noopener noreferrer"&gt;open source and available on GitHub&lt;/a&gt;. You can build and run the &lt;a href="https://github.com/Neosperience/backpack/blob/master/examples/Dockerfile" rel="noopener noreferrer"&gt;example docker container&lt;/a&gt; on an ARM64 based system. For example, suppose you’ve already set up the &lt;a href="https://github.com/aws-samples/aws-panorama-samples/blob/main/docs/EnvironmentSetup.md" rel="noopener noreferrer"&gt;Panorama Test Utility&lt;/a&gt; on a t4g type EC2 instance. In this case, you can build and launch a shell in this container with the following commands on the EC2 instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/Neosperience/backpack.git
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backpack/examples
&lt;span class="nv"&gt;$ &lt;/span&gt;docker build &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; my_backpack_example:1.0
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-it&lt;/span&gt; my_backpack_example:1.0 bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container can be built and executed also on a MacBook Pro with the new M1 ARM-based chip.&lt;/p&gt;

&lt;p&gt;The backpack library is extensively documented with docstrings. You can read the &lt;a href="https://s3.eu-west-1.amazonaws.com/github-ci.experiments.neosperience.com/Neosperience/backpack/docs/index.html" rel="noopener noreferrer"&gt;detailed API docs online&lt;/a&gt; or using the &lt;code&gt;python3&lt;/code&gt; interpreter in the container shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@my_backpack_example:/# python3
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; import backpack.telescope, backpack.kvs, backpack.annotation
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;backpack.telescope&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;backpack.kvs&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;backpack.annotation&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also try other &lt;code&gt;Telescope&lt;/code&gt; implementations found in Backpack. For example, &lt;code&gt;RTSPTelescope&lt;/code&gt; allows you to host an RTSP server right in the application container, and connect directly to your Panorama appliance with an RTSP client to monitor your application.&lt;/p&gt;

&lt;p&gt;Let me know how you use &lt;code&gt;Telescope&lt;/code&gt;, which new features you would like to see and stay tuned for future posts about the other components of &lt;code&gt;Backpack&lt;/code&gt;.&lt;/p&gt;





&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;Janos Tolgyesi is an AWS Community Builder working as Machine Learning Solution Architect at Neosperience. He has worked with ML technologies for five years and with AWS infrastructure for eight years. He loves building things, let it be a &lt;a href="https://www.neosperience.com/solutions/people-analytics/" rel="noopener noreferrer"&gt;video analytics application on the edge&lt;/a&gt; or a &lt;a href="https://www.neosperience.com/solutions/user-insight/" rel="noopener noreferrer"&gt;user profiler based on clickstream events&lt;/a&gt;. You can find me here, on &lt;a href="https://dev.to/mrtj"&gt;dev.to&lt;/a&gt;, on &lt;a href="https://twitter.com/jtolgyesi" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, &lt;a href="https://towardsdatascience.com/@janos.tolgyesi" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, and &lt;a href="http://linkedin.com/in/janostolgyesi" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The open-source project of Backpack was supported by &lt;a href="https://www.neosperience.com/" rel="noopener noreferrer"&gt;Neosperience&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I would like to express my special thanks to &lt;br&gt;
&lt;a href="https://twitter.com/bianchiluca" rel="noopener noreferrer"&gt;Luca Bianchi&lt;/a&gt; for proofreading this article.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>computervision</category>
    </item>
  </channel>
</rss>
