implement robust event-driven file watching system
This commit is contained in:
@@ -836,3 +836,4 @@ If you tell me your stack (language/framework + how you install deps), I’ll ta
|
|||||||
|
|
||||||
|
|
||||||
it's docker compose not docker-compose
|
it's docker compose not docker-compose
|
||||||
|
- use debug logs not prints
|
||||||
@@ -4,486 +4,642 @@
|
|||||||
<!-- Generated by graphviz version 2.42.4 (0)
|
<!-- Generated by graphviz version 2.42.4 (0)
|
||||||
-->
|
-->
|
||||||
<!-- Title: MediaAnalyzer Pages: 1 -->
|
<!-- Title: MediaAnalyzer Pages: 1 -->
|
||||||
<svg width="1589pt" height="735pt"
|
<svg width="648pt" height="720pt"
|
||||||
viewBox="0.00 0.00 1589.00 735.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
viewBox="0.00 0.00 647.84 720.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 731)">
|
<g id="graph0" class="graph" transform="scale(0.34 0.34) rotate(0) translate(4 2084.29)">
|
||||||
<title>MediaAnalyzer</title>
|
<title>MediaAnalyzer</title>
|
||||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-731 1585,-731 1585,4 -4,4"/>
|
<polygon fill="white" stroke="transparent" points="-4,4 -4,-2084.29 1875,-2084.29 1875,4 -4,4"/>
|
||||||
<g id="clust1" class="cluster">
|
<g id="clust1" class="cluster">
|
||||||
<title>cluster_legend</title>
|
<title>cluster_legend</title>
|
||||||
<polygon fill="lightgray" stroke="lightgray" points="8,-573 8,-719 108,-719 108,-573 8,-573"/>
|
<polygon fill="lightgray" stroke="lightgray" points="8,-1674.04 8,-2057.88 236,-2057.88 236,-1674.04 8,-1674.04"/>
|
||||||
<text text-anchor="middle" x="58" y="-705.4" font-family="Arial" font-size="12.00">Legend</text>
|
<text text-anchor="middle" x="122" y="-2044.28" font-family="Arial" font-size="12.00">Legend</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust2" class="cluster">
|
<g id="clust2" class="cluster">
|
||||||
<title>cluster_sources</title>
|
<title>cluster_sources</title>
|
||||||
<polygon fill="#e3f2fd" stroke="#e3f2fd" points="116,-646 116,-719 358,-719 358,-646 116,-646"/>
|
<polygon fill="#1976d2" stroke="#1976d2" points="244,-1853.35 244,-2057.88 494,-2057.88 494,-1853.35 244,-1853.35"/>
|
||||||
<text text-anchor="middle" x="237" y="-705.4" font-family="Arial" font-size="12.00">Video Sources</text>
|
<text text-anchor="middle" x="369" y="-2044.28" font-family="Arial" font-size="12.00">Video Sources</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust3" class="cluster">
|
<g id="clust3" class="cluster">
|
||||||
<title>cluster_adapters</title>
|
<title>cluster_adapters</title>
|
||||||
<polygon fill="#f3e5f5" stroke="#f3e5f5" points="366,-573 366,-719 648,-719 648,-573 366,-573"/>
|
<polygon fill="#8e24aa" stroke="#8e24aa" points="502,-1674.04 502,-2057.88 791,-2057.88 791,-1674.04 502,-1674.04"/>
|
||||||
<text text-anchor="middle" x="507" y="-705.4" font-family="Arial" font-size="12.00">Source Adapters Pattern</text>
|
<text text-anchor="middle" x="646.5" y="-2044.28" font-family="Arial" font-size="12.00">Source Adapters Pattern</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust4" class="cluster">
|
<g id="clust4" class="cluster">
|
||||||
<title>cluster_core</title>
|
<title>cluster_core</title>
|
||||||
<polygon fill="#fff3e0" stroke="#fff3e0" points="463,-364.5 463,-554 653,-554 653,-364.5 463,-364.5"/>
|
<polygon fill="#1976d2" stroke="#1976d2" points="606,-1115.09 606,-1626.41 800,-1626.41 800,-1115.09 606,-1115.09"/>
|
||||||
<text text-anchor="middle" x="558" y="-540.4" font-family="Arial" font-size="12.00">Core Platform</text>
|
<text text-anchor="middle" x="703" y="-1612.81" font-family="Arial" font-size="12.00">Core Platform</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust5" class="cluster">
|
<g id="clust5" class="cluster">
|
||||||
<title>cluster_execution</title>
|
<title>cluster_execution</title>
|
||||||
<polygon fill="#e8f5e8" stroke="#e8f5e8" points="983,-173 983,-340.5 1264,-340.5 1264,-173 983,-173"/>
|
<polygon fill="#8e24aa" stroke="#8e24aa" points="1574,-549.14 1574,-1012.83 1863,-1012.83 1863,-549.14 1574,-549.14"/>
|
||||||
<text text-anchor="middle" x="1123.5" y="-326.9" font-family="Arial" font-size="12.00">Execution Strategies Pattern</text>
|
<text text-anchor="middle" x="1718.5" y="-999.23" font-family="Arial" font-size="12.00">Execution Strategies Pattern</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust6" class="cluster">
|
<g id="clust6" class="cluster">
|
||||||
<title>cluster_workers</title>
|
<title>cluster_event_sources</title>
|
||||||
<polygon fill="#e3f2fd" stroke="#e3f2fd" points="426,-265 426,-343 849,-343 849,-265 426,-265"/>
|
<polygon fill="#8e24aa" stroke="#8e24aa" points="1134,-808.3 1134,-1319.62 1566,-1319.62 1566,-808.3 1134,-808.3"/>
|
||||||
<text text-anchor="middle" x="637.5" y="-329.4" font-family="Arial" font-size="12.00">Celery Workers (Queue Segregation)</text>
|
<text text-anchor="middle" x="1350" y="-1306.02" font-family="Arial" font-size="12.00">Event Source Pattern</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust7" class="cluster">
|
<g id="clust7" class="cluster">
|
||||||
<title>cluster_ai_adapters</title>
|
<title>cluster_events</title>
|
||||||
<polygon fill="#fce4ec" stroke="#fce4ec" points="1272,-100 1272,-246 1573,-246 1573,-100 1272,-100"/>
|
<polygon fill="#1976d2" stroke="#1976d2" points="1100,-22.41 1100,-753.67 1249,-753.67 1249,-22.41 1100,-22.41"/>
|
||||||
<text text-anchor="middle" x="1422.5" y="-232.4" font-family="Arial" font-size="12.00">Analysis Adapters Pattern</text>
|
<text text-anchor="middle" x="1174.5" y="-740.07" font-family="Arial" font-size="12.00">Event-Driven Processing</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust8" class="cluster">
|
<g id="clust8" class="cluster">
|
||||||
<title>cluster_storage</title>
|
<title>cluster_workers</title>
|
||||||
<polygon fill="#f1f8e9" stroke="#f1f8e9" points="661,-364.5 661,-437.5 849,-437.5 849,-364.5 661,-364.5"/>
|
<polygon fill="#1976d2" stroke="#1976d2" points="365,-801.3 365,-1019.83 800,-1019.83 800,-801.3 365,-801.3"/>
|
||||||
<text text-anchor="middle" x="755" y="-423.9" font-family="Arial" font-size="12.00">Media Storage</text>
|
<text text-anchor="middle" x="582.5" y="-1006.23" font-family="Arial" font-size="12.00">Celery Workers</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust9" class="cluster">
|
<g id="clust9" class="cluster">
|
||||||
<title>cluster_frontend</title>
|
<title>cluster_ai_adapters</title>
|
||||||
<polygon fill="#e8eaf6" stroke="#e8eaf6" points="857,-267.5 857,-440 975,-440 975,-267.5 857,-267.5"/>
|
<polygon fill="#8e24aa" stroke="#8e24aa" points="1257,-322.2 1257,-753.67 1566,-753.67 1566,-322.2 1257,-322.2"/>
|
||||||
<text text-anchor="middle" x="916" y="-426.4" font-family="Arial" font-size="12.00">Frontend</text>
|
<text text-anchor="middle" x="1411.5" y="-740.07" font-family="Arial" font-size="12.00">Analysis Adapters Pattern</text>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust10" class="cluster">
|
<g id="clust10" class="cluster">
|
||||||
|
<title>cluster_storage</title>
|
||||||
|
<polygon fill="#1976d2" stroke="#1976d2" points="934,-1115.09 934,-1319.62 1126,-1319.62 1126,-1115.09 934,-1115.09"/>
|
||||||
|
<text text-anchor="middle" x="1030" y="-1306.02" font-family="Arial" font-size="12.00">Media Storage</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust11" class="cluster">
|
||||||
|
<title>cluster_frontend</title>
|
||||||
|
<polygon fill="#1976d2" stroke="#1976d2" points="808,-808.3 808,-1326.62 926,-1326.62 926,-808.3 808,-808.3"/>
|
||||||
|
<text text-anchor="middle" x="867" y="-1313.02" font-family="Arial" font-size="12.00">Frontend</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust12" class="cluster">
|
||||||
<title>cluster_cloud</title>
|
<title>cluster_cloud</title>
|
||||||
<polygon fill="#e0f2f1" stroke="#e0f2f1" points="1146,-8 1146,-81 1373,-81 1373,-8 1146,-8"/>
|
<polygon fill="#1976d2" stroke="#1976d2" points="1483,-22.41 1483,-226.94 1714,-226.94 1714,-22.41 1483,-22.41"/>
|
||||||
<text text-anchor="middle" x="1259.5" y="-67.4" font-family="Arial" font-size="12.00">GCP Services</text>
|
<text text-anchor="middle" x="1598.5" y="-213.34" font-family="Arial" font-size="12.00">GCP Services</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- implemented -->
|
<!-- implemented -->
|
||||||
<g id="node1" class="node">
|
<g id="node1" class="node">
|
||||||
<title>implemented</title>
|
<title>implemented</title>
|
||||||
<polygon fill="#d4edda" stroke="transparent" points="100,-690 16,-690 16,-654 100,-654 100,-690"/>
|
<polygon fill="#28a745" stroke="transparent" points="228.5,-1944.41 139.5,-1944.41 139.5,-1908.41 228.5,-1908.41 228.5,-1944.41"/>
|
||||||
<text text-anchor="middle" x="58" y="-669.5" font-family="Arial" font-size="10.00">✓ Implemented</text>
|
<text text-anchor="middle" x="184" y="-1923.91" font-family="Arial" font-size="10.00" fill="#ffffff">✅ Implemented</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- planned -->
|
<!-- planned -->
|
||||||
<g id="node2" class="node">
|
<g id="node2" class="node">
|
||||||
<title>planned</title>
|
<title>planned</title>
|
||||||
<polygon fill="#fff3cd" stroke="transparent" points="88.5,-617 27.5,-617 27.5,-581 88.5,-581 88.5,-617"/>
|
<polygon fill="#ffc107" stroke="transparent" points="228,-1764.41 160,-1764.41 160,-1728.41 228,-1728.41 228,-1764.41"/>
|
||||||
<text text-anchor="middle" x="58" y="-596.5" font-family="Arial" font-size="10.00">○ Planned</text>
|
<text text-anchor="middle" x="194" y="-1743.91" font-family="Arial" font-size="10.00" fill="#212529">⏳ Planned</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- implemented->planned -->
|
<!-- implemented->planned -->
|
||||||
<!-- webcam -->
|
<!-- code_pattern -->
|
||||||
<g id="node3" class="node">
|
<g id="node3" class="node">
|
||||||
|
<title>code_pattern</title>
|
||||||
|
<polygon fill="#8e24aa" stroke="transparent" points="117,-1944.41 27,-1944.41 27,-1908.41 117,-1908.41 117,-1944.41"/>
|
||||||
|
<text text-anchor="middle" x="72" y="-1923.91" font-family="Arial" font-size="10.00" fill="#ffffff">📐 Code Pattern</text>
|
||||||
|
</g>
|
||||||
|
<!-- service_arch -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>service_arch</title>
|
||||||
|
<polygon fill="#1976d2" stroke="transparent" points="137.5,-1764.41 16.5,-1764.41 16.5,-1728.41 137.5,-1728.41 137.5,-1764.41"/>
|
||||||
|
<text text-anchor="middle" x="77" y="-1743.91" font-family="Arial" font-size="10.00" fill="#ffffff">⚡ Service/Architecture</text>
|
||||||
|
</g>
|
||||||
|
<!-- code_pattern->service_arch -->
|
||||||
|
<!-- webcam -->
|
||||||
|
<g id="node5" class="node">
|
||||||
<title>webcam</title>
|
<title>webcam</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="265.5,-690 210.5,-690 210.5,-654 265.5,-654 265.5,-690"/>
|
<polygon fill="#28a745" stroke="black" points="307.5,-1944.41 252.5,-1944.41 252.5,-1908.41 307.5,-1908.41 307.5,-1944.41"/>
|
||||||
<text text-anchor="middle" x="238" y="-669.5" font-family="Arial" font-size="10.00">Webcam</text>
|
<text text-anchor="middle" x="280" y="-1923.91" font-family="Arial" font-size="10.00" fill="#ffffff">Webcam</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- webcam_adapter -->
|
<!-- webcam_adapter -->
|
||||||
<g id="node7" class="node">
|
<g id="node9" class="node">
|
||||||
<title>webcam_adapter</title>
|
<title>webcam_adapter</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="555.5,-617 466.5,-617 466.5,-581 555.5,-581 555.5,-617"/>
|
<polygon fill="#28a745" stroke="black" points="598.5,-1764.41 509.5,-1764.41 509.5,-1728.41 598.5,-1728.41 598.5,-1764.41"/>
|
||||||
<text text-anchor="middle" x="511" y="-596.5" font-family="Arial" font-size="10.00">WebcamAdapter</text>
|
<text text-anchor="middle" x="554" y="-1743.91" font-family="Arial" font-size="10.00" fill="#ffffff">WebcamAdapter</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- webcam->webcam_adapter -->
|
<!-- webcam->webcam_adapter -->
|
||||||
<g id="edge2" class="edge">
|
<g id="edge3" class="edge">
|
||||||
<title>webcam->webcam_adapter</title>
|
<title>webcam->webcam_adapter</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M259.82,-654C264.61,-650.92 269.81,-648.05 275,-646 348.21,-617.09 375.16,-634.32 456.19,-616.91"/>
|
<path fill="none" stroke="#2e7d32" d="M301.09,-1908.09C325.95,-1888.02 368.83,-1854.44 408,-1828.91 441.69,-1806.96 481.66,-1784.91 511.26,-1769.28"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="457.37,-620.23 466.35,-614.61 455.83,-613.41 457.37,-620.23"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="513.25,-1772.19 520.48,-1764.45 510,-1765.99 513.25,-1772.19"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- rtmp -->
|
<!-- rtmp -->
|
||||||
<g id="node4" class="node">
|
<g id="node6" class="node">
|
||||||
<title>rtmp</title>
|
<title>rtmp</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="192,-690 124,-690 124,-654 192,-654 192,-690"/>
|
<polygon fill="#28a745" stroke="black" points="486,-1944.41 418,-1944.41 418,-1908.41 486,-1908.41 486,-1944.41"/>
|
||||||
<text text-anchor="middle" x="158" y="-669.5" font-family="Arial" font-size="10.00">RTMP/OBS</text>
|
<text text-anchor="middle" x="452" y="-1923.91" font-family="Arial" font-size="10.00" fill="#ffffff">RTMP/OBS</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- rtmp_adapter -->
|
<!-- rtmp_adapter -->
|
||||||
<g id="node8" class="node">
|
<g id="node10" class="node">
|
||||||
<title>rtmp_adapter</title>
|
<title>rtmp_adapter</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="448,-617 374,-617 374,-581 448,-581 448,-617"/>
|
<polygon fill="#28a745" stroke="black" points="783,-1764.41 709,-1764.41 709,-1728.41 783,-1728.41 783,-1764.41"/>
|
||||||
<text text-anchor="middle" x="411" y="-596.5" font-family="Arial" font-size="10.00">RtmpAdapter</text>
|
<text text-anchor="middle" x="746" y="-1743.91" font-family="Arial" font-size="10.00" fill="#ffffff">RtmpAdapter</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- rtmp->rtmp_adapter -->
|
<!-- rtmp->rtmp_adapter -->
|
||||||
<g id="edge3" class="edge">
|
<g id="edge4" class="edge">
|
||||||
<title>rtmp->rtmp_adapter</title>
|
<title>rtmp->rtmp_adapter</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M184.66,-653.8C189.93,-650.89 195.53,-648.13 201,-646 254.94,-625.01 320.58,-612.57 363.96,-606.02"/>
|
<path fill="none" stroke="#2e7d32" d="M480.19,-1908.35C533.64,-1875.98 649.69,-1805.72 709.19,-1769.7"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="364.56,-609.47 373.95,-604.56 363.55,-602.55 364.56,-609.47"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="711.15,-1772.6 717.9,-1764.43 707.53,-1766.61 711.15,-1772.6"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- files -->
|
<!-- files -->
|
||||||
<g id="node5" class="node">
|
<g id="node7" class="node">
|
||||||
<title>files</title>
|
<title>files</title>
|
||||||
<polygon fill="#fff3cd" stroke="black" points="350,-690 284,-690 284,-654 350,-654 350,-690"/>
|
<polygon fill="#ffc107" stroke="black" points="396,-1944.41 330,-1944.41 330,-1908.41 396,-1908.41 396,-1944.41"/>
|
||||||
<text text-anchor="middle" x="317" y="-669.5" font-family="Arial" font-size="10.00">File Upload</text>
|
<text text-anchor="middle" x="363" y="-1923.91" font-family="Arial" font-size="10.00" fill="#212529">File Upload</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- file_adapter -->
|
<!-- file_adapter -->
|
||||||
<g id="node9" class="node">
|
<g id="node11" class="node">
|
||||||
<title>file_adapter</title>
|
<title>file_adapter</title>
|
||||||
<polygon fill="#fff3cd" stroke="black" points="640,-617 574,-617 574,-581 640,-581 640,-617"/>
|
<polygon fill="#ffc107" stroke="black" points="687,-1764.41 621,-1764.41 621,-1728.41 687,-1728.41 687,-1764.41"/>
|
||||||
<text text-anchor="middle" x="607" y="-596.5" font-family="Arial" font-size="10.00">FileAdapter</text>
|
<text text-anchor="middle" x="654" y="-1743.91" font-family="Arial" font-size="10.00" fill="#212529">FileAdapter</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- files->file_adapter -->
|
<!-- files->file_adapter -->
|
||||||
<g id="edge18" class="edge">
|
<g id="edge28" class="edge">
|
||||||
<title>files->file_adapter</title>
|
<title>files->file_adapter</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M344.36,-653.75C350.02,-650.77 356.07,-648 362,-646 447.95,-617.05 476.34,-640.28 564,-617 564.1,-616.97 564.2,-616.95 564.31,-616.92"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M385.67,-1908.1C412.36,-1888.04 458.34,-1854.47 500,-1828.91 536.69,-1806.41 580.16,-1783.75 611.55,-1768.05"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="565.39,-620.25 573.92,-613.96 563.34,-613.55 565.39,-620.25"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="613.49,-1771 620.88,-1763.41 610.37,-1764.73 613.49,-1771"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_adapter -->
|
<!-- base_adapter -->
|
||||||
<g id="node6" class="node">
|
<g id="node8" class="node">
|
||||||
<title>base_adapter</title>
|
<title>base_adapter</title>
|
||||||
<polygon fill="#e1bee7" stroke="black" points="563,-690 459,-690 459,-654 563,-654 563,-690"/>
|
<polygon fill="#ce93d8" stroke="black" points="706,-1944.41 602,-1944.41 602,-1908.41 706,-1908.41 706,-1944.41"/>
|
||||||
<text text-anchor="middle" x="511" y="-675" font-family="Arial" font-size="10.00">BaseSourceAdapter</text>
|
<text text-anchor="middle" x="654" y="-1929.41" font-family="Arial" font-size="10.00">BaseSourceAdapter</text>
|
||||||
<text text-anchor="middle" x="511" y="-664" font-family="Arial" font-size="10.00">(Abstract)</text>
|
<text text-anchor="middle" x="654" y="-1918.41" font-family="Arial" font-size="10.00">(Abstract)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_adapter->webcam_adapter -->
|
<!-- base_adapter->webcam_adapter -->
|
||||||
<g id="edge28" class="edge">
|
<g id="edge38" class="edge">
|
||||||
<title>base_adapter->webcam_adapter</title>
|
<title>base_adapter->webcam_adapter</title>
|
||||||
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M511,-653.81C511,-645.79 511,-636.05 511,-627.07"/>
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M644.41,-1908.35C626.87,-1877.11 589.49,-1810.58 568.71,-1773.59"/>
|
||||||
<polygon fill="gray" stroke="gray" points="514.5,-627.03 511,-617.03 507.5,-627.03 514.5,-627.03"/>
|
<polygon fill="gray" stroke="gray" points="571.51,-1771.43 563.56,-1764.43 565.41,-1774.86 571.51,-1771.43"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_adapter->rtmp_adapter -->
|
<!-- base_adapter->rtmp_adapter -->
|
||||||
<g id="edge29" class="edge">
|
<g id="edge39" class="edge">
|
||||||
<title>base_adapter->rtmp_adapter</title>
|
<title>base_adapter->rtmp_adapter</title>
|
||||||
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M487.05,-653.99C474.03,-644.75 457.75,-633.19 443.59,-623.14"/>
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M662.82,-1908.35C678.96,-1877.11 713.35,-1810.58 732.47,-1773.59"/>
|
||||||
<polygon fill="gray" stroke="gray" points="445.28,-620.05 435.1,-617.11 441.23,-625.76 445.28,-620.05"/>
|
<polygon fill="gray" stroke="gray" points="735.72,-1774.92 737.21,-1764.43 729.5,-1771.71 735.72,-1774.92"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_adapter->file_adapter -->
|
<!-- base_adapter->file_adapter -->
|
||||||
<g id="edge30" class="edge">
|
<g id="edge40" class="edge">
|
||||||
<title>base_adapter->file_adapter</title>
|
<title>base_adapter->file_adapter</title>
|
||||||
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M534.24,-653.81C546.74,-644.57 562.32,-633.04 575.86,-623.03"/>
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M654,-1908.35C654,-1877.38 654,-1811.71 654,-1774.54"/>
|
||||||
<polygon fill="gray" stroke="gray" points="578.02,-625.79 583.98,-617.03 573.85,-620.16 578.02,-625.79"/>
|
<polygon fill="gray" stroke="gray" points="657.5,-1774.43 654,-1764.43 650.5,-1774.43 657.5,-1774.43"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- django -->
|
<!-- django -->
|
||||||
<g id="node10" class="node">
|
<g id="node12" class="node">
|
||||||
<title>django</title>
|
<title>django</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="544.5,-525 477.5,-525 477.5,-484 544.5,-484 544.5,-525"/>
|
<polygon fill="#28a745" stroke="black" points="687.5,-1507.91 620.5,-1507.91 620.5,-1466.91 687.5,-1466.91 687.5,-1507.91"/>
|
||||||
<text text-anchor="middle" x="511" y="-513" font-family="Arial" font-size="10.00">Django API</text>
|
<text text-anchor="middle" x="654" y="-1495.91" font-family="Arial" font-size="10.00" fill="#ffffff">Django API</text>
|
||||||
<text text-anchor="middle" x="511" y="-502" font-family="Arial" font-size="10.00">+ Channels</text>
|
<text text-anchor="middle" x="654" y="-1484.91" font-family="Arial" font-size="10.00" fill="#ffffff">+ Channels</text>
|
||||||
<text text-anchor="middle" x="511" y="-491" font-family="Arial" font-size="10.00">:8000</text>
|
<text text-anchor="middle" x="654" y="-1473.91" font-family="Arial" font-size="10.00" fill="#ffffff">:8000</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- webcam_adapter->django -->
|
<!-- webcam_adapter->django -->
|
||||||
<g id="edge4" class="edge">
|
<g id="edge5" class="edge">
|
||||||
<title>webcam_adapter->django</title>
|
<title>webcam_adapter->django</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M511,-580.6C511,-567.88 511,-550.32 511,-535.37"/>
|
<path fill="none" stroke="#2e7d32" d="M560.72,-1728.15C577.54,-1684.91 621.65,-1571.55 642.62,-1517.67"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="514.5,-535.06 511,-525.06 507.5,-535.06 514.5,-535.06"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="646,-1518.62 646.37,-1508.03 639.48,-1516.08 646,-1518.62"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- rtmp_adapter->django -->
|
<!-- rtmp_adapter->django -->
|
||||||
<g id="edge5" class="edge">
|
<g id="edge6" class="edge">
|
||||||
<title>rtmp_adapter->django</title>
|
<title>rtmp_adapter->django</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M429.81,-580.6C444.63,-566.9 465.53,-547.56 482.41,-531.94"/>
|
<path fill="none" stroke="#2e7d32" d="M739.82,-1728.15C724.34,-1684.91 683.76,-1571.55 664.47,-1517.67"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="484.89,-534.42 489.85,-525.06 480.13,-529.28 484.89,-534.42"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="667.69,-1516.26 661.02,-1508.03 661.1,-1518.62 667.69,-1516.26"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- file_adapter->django -->
|
<!-- file_adapter->django -->
|
||||||
<g id="edge19" class="edge">
|
<g id="edge29" class="edge">
|
||||||
<title>file_adapter->django</title>
|
<title>file_adapter->django</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M584.95,-580.95C575.17,-573.07 563.71,-563.4 554,-554 547.11,-547.33 540.05,-539.73 533.69,-532.56"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M654,-1728.15C654,-1685.09 654,-1572.49 654,-1518.35"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="536.31,-530.24 527.1,-525 531.03,-534.84 536.31,-530.24"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="657.5,-1518.03 654,-1508.03 650.5,-1518.03 657.5,-1518.03"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- postgres -->
|
<!-- postgres -->
|
||||||
<g id="node11" class="node">
|
<g id="node13" class="node">
|
||||||
<title>postgres</title>
|
<title>postgres</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="541,-408.5 471,-408.5 471,-372.5 541,-372.5 541,-408.5"/>
|
<polygon fill="#28a745" stroke="black" points="792,-1206.41 722,-1206.41 722,-1170.41 792,-1170.41 792,-1206.41"/>
|
||||||
<text text-anchor="middle" x="506" y="-393.5" font-family="Arial" font-size="10.00">PostgreSQL</text>
|
<text text-anchor="middle" x="757" y="-1191.41" font-family="Arial" font-size="10.00" fill="#ffffff">PostgreSQL</text>
|
||||||
<text text-anchor="middle" x="506" y="-382.5" font-family="Arial" font-size="10.00">Database</text>
|
<text text-anchor="middle" x="757" y="-1180.41" font-family="Arial" font-size="10.00" fill="#ffffff">Database</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- django->postgres -->
|
<!-- django->postgres -->
|
||||||
<g id="edge6" class="edge">
|
<g id="edge7" class="edge">
|
||||||
<title>django->postgres</title>
|
<title>django->postgres</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M510.13,-483.99C509.32,-465.93 508.12,-438.89 507.21,-418.71"/>
|
<path fill="none" stroke="#2e7d32" d="M660.92,-1466.47C678.92,-1414.57 727.22,-1275.28 747.66,-1216.34"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="510.71,-418.42 506.76,-408.59 503.71,-418.73 510.71,-418.42"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="751.06,-1217.23 751.03,-1206.63 744.45,-1214.93 751.06,-1217.23"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- redis -->
|
<!-- redis -->
|
||||||
<g id="node12" class="node">
|
<g id="node14" class="node">
|
||||||
<title>redis</title>
|
<title>redis</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="645,-408.5 559,-408.5 559,-372.5 645,-372.5 645,-408.5"/>
|
<polygon fill="#28a745" stroke="black" points="700,-1206.41 614,-1206.41 614,-1170.41 700,-1170.41 700,-1206.41"/>
|
||||||
<text text-anchor="middle" x="602" y="-393.5" font-family="Arial" font-size="10.00">Redis</text>
|
<text text-anchor="middle" x="657" y="-1191.41" font-family="Arial" font-size="10.00" fill="#ffffff">Redis</text>
|
||||||
<text text-anchor="middle" x="602" y="-382.5" font-family="Arial" font-size="10.00">Cache & Broker</text>
|
<text text-anchor="middle" x="657" y="-1180.41" font-family="Arial" font-size="10.00" fill="#ffffff">Cache & Broker</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- django->redis -->
|
<!-- django->redis -->
|
||||||
<g id="edge7" class="edge">
|
<g id="edge8" class="edge">
|
||||||
<title>django->redis</title>
|
<title>django->redis</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M526.85,-483.99C542.07,-465.26 565.12,-436.89 581.68,-416.51"/>
|
<path fill="none" stroke="#2e7d32" d="M654.2,-1466.47C654.72,-1414.67 656.13,-1275.85 656.72,-1216.71"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="584.53,-418.55 588.12,-408.59 579.09,-414.14 584.53,-418.55"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="660.22,-1216.67 656.83,-1206.63 653.23,-1216.59 660.22,-1216.67"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- local_storage -->
|
<!-- local_storage -->
|
||||||
<g id="node26" class="node">
|
<g id="node35" class="node">
|
||||||
<title>local_storage</title>
|
<title>local_storage</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="747,-408.5 669,-408.5 669,-372.5 747,-372.5 747,-408.5"/>
|
<polygon fill="#28a745" stroke="black" points="1020,-1206.41 942,-1206.41 942,-1170.41 1020,-1170.41 1020,-1206.41"/>
|
||||||
<text text-anchor="middle" x="708" y="-393.5" font-family="Arial" font-size="10.00">Local Files</text>
|
<text text-anchor="middle" x="981" y="-1191.41" font-family="Arial" font-size="10.00" fill="#ffffff">Local Files</text>
|
||||||
<text text-anchor="middle" x="708" y="-382.5" font-family="Arial" font-size="10.00">(nginx-served)</text>
|
<text text-anchor="middle" x="981" y="-1180.41" font-family="Arial" font-size="10.00" fill="#ffffff">(nginx-served)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- django->local_storage -->
|
<!-- django->local_storage -->
|
||||||
<g id="edge8" class="edge">
|
<g id="edge9" class="edge">
|
||||||
<title>django->local_storage</title>
|
<title>django->local_storage</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M544.77,-488.1C547.89,-486.71 551,-485.32 554,-484 599.56,-463.94 615.42,-467.36 657,-440 667.16,-433.31 677.13,-424.5 685.52,-416.21"/>
|
<path fill="none" stroke="#2e7d32" d="M678.88,-1466.81C704.44,-1446.47 745.15,-1413.48 779,-1383.41 848.91,-1321.3 863.66,-1302.8 931,-1237.91 939.1,-1230.11 947.91,-1221.58 955.85,-1213.87"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="688.25,-418.43 692.74,-408.84 683.24,-413.53 688.25,-418.43"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="958.71,-1215.98 963.44,-1206.5 953.83,-1210.96 958.71,-1215.98"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- gcs_storage -->
|
<!-- gcs_storage -->
|
||||||
<g id="node27" class="node">
|
<g id="node36" class="node">
|
||||||
<title>gcs_storage</title>
|
<title>gcs_storage</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="841,-408.5 765,-408.5 765,-372.5 841,-372.5 841,-408.5"/>
|
<polygon fill="#ffc107" stroke="black" points="1118,-1206.41 1042,-1206.41 1042,-1170.41 1118,-1170.41 1118,-1206.41"/>
|
||||||
<text text-anchor="middle" x="803" y="-393.5" font-family="Arial" font-size="10.00">Google Cloud</text>
|
<text text-anchor="middle" x="1080" y="-1191.41" font-family="Arial" font-size="10.00" fill="#212529">Google Cloud</text>
|
||||||
<text text-anchor="middle" x="803" y="-382.5" font-family="Arial" font-size="10.00">Storage</text>
|
<text text-anchor="middle" x="1080" y="-1180.41" font-family="Arial" font-size="10.00" fill="#212529">Storage</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- django->gcs_storage -->
|
<!-- django->gcs_storage -->
|
||||||
<g id="edge9" class="edge">
|
<g id="edge10" class="edge">
|
||||||
<title>django->gcs_storage</title>
|
<title>django->gcs_storage</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M544.53,-487.48C547.7,-486.21 550.89,-485.03 554,-484 641.23,-455.13 675.72,-484.7 756,-440 766.51,-434.15 776.14,-425.2 783.92,-416.57"/>
|
<path fill="none" stroke="#2e7d32" d="M687.53,-1467.36C755.28,-1428.23 912.13,-1334.34 1031,-1237.91 1039.92,-1230.68 1049.05,-1222.05 1056.96,-1214.09"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="786.68,-418.72 790.5,-408.84 781.35,-414.18 786.68,-418.72"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1059.95,-1216.03 1064.42,-1206.43 1054.94,-1211.15 1059.95,-1216.03"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- angular -->
|
<!-- angular -->
|
||||||
<g id="node28" class="node">
|
<g id="node37" class="node">
|
||||||
<title>angular</title>
|
<title>angular</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="951,-411 865,-411 865,-370 951,-370 951,-411"/>
|
<polygon fill="#28a745" stroke="black" points="902,-1208.91 816,-1208.91 816,-1167.91 902,-1167.91 902,-1208.91"/>
|
||||||
<text text-anchor="middle" x="908" y="-399" font-family="Arial" font-size="10.00">Angular 17 SPA</text>
|
<text text-anchor="middle" x="859" y="-1196.91" font-family="Arial" font-size="10.00" fill="#ffffff">Angular 17 SPA</text>
|
||||||
<text text-anchor="middle" x="908" y="-388" font-family="Arial" font-size="10.00">+ WebSocket</text>
|
<text text-anchor="middle" x="859" y="-1185.91" font-family="Arial" font-size="10.00" fill="#ffffff">+ WebSocket</text>
|
||||||
<text text-anchor="middle" x="908" y="-377" font-family="Arial" font-size="10.00">:4200</text>
|
<text text-anchor="middle" x="859" y="-1174.91" font-family="Arial" font-size="10.00" fill="#ffffff">:4200</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- django->angular -->
|
<!-- django->angular -->
|
||||||
<g id="edge15" class="edge">
|
<g id="edge25" class="edge">
|
||||||
<title>django->angular</title>
|
<title>django->angular</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M544.9,-486.94C547.93,-485.82 550.99,-484.82 554,-484 627.31,-464.06 648.93,-477.71 724,-466 763.01,-459.91 772.27,-455.69 811,-448 829.64,-444.3 835.93,-448.36 853,-440 863.97,-434.63 874.43,-426.48 883.24,-418.38"/>
|
<path fill="none" stroke="#2e7d32" d="M667.48,-1466.69C683.88,-1442.73 712.42,-1401.07 737,-1365.41 773.19,-1312.93 815.63,-1251.8 839.72,-1217.14"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="885.76,-420.81 890.52,-411.35 880.89,-415.78 885.76,-420.81"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="842.59,-1219.13 845.43,-1208.93 836.85,-1215.14 842.59,-1219.13"/>
|
||||||
<text text-anchor="middle" x="831.5" y="-459.6" font-family="Arial" font-size="8.00">WebSocket</text>
|
<text text-anchor="middle" x="757.5" y="-1377.01" font-family="Arial" font-size="8.00">WebSocket</text>
|
||||||
<text text-anchor="middle" x="831.5" y="-450.6" font-family="Arial" font-size="8.00">API</text>
|
<text text-anchor="middle" x="757.5" y="-1368.01" font-family="Arial" font-size="8.00">API</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- logo_worker -->
|
<!-- logo_worker -->
|
||||||
<g id="node18" class="node">
|
<g id="node27" class="node">
|
||||||
<title>logo_worker</title>
|
<title>logo_worker</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="840.5,-314 757.5,-314 757.5,-273 840.5,-273 840.5,-314"/>
|
<polygon fill="#28a745" stroke="black" points="456.5,-901.91 373.5,-901.91 373.5,-860.91 456.5,-860.91 456.5,-901.91"/>
|
||||||
<text text-anchor="middle" x="799" y="-302" font-family="Arial" font-size="10.00">Logo Detection</text>
|
<text text-anchor="middle" x="415" y="-889.91" font-family="Arial" font-size="10.00" fill="#ffffff">Logo Detection</text>
|
||||||
<text text-anchor="middle" x="799" y="-291" font-family="Arial" font-size="10.00">Worker</text>
|
<text text-anchor="middle" x="415" y="-878.91" font-family="Arial" font-size="10.00" fill="#ffffff">Worker</text>
|
||||||
<text text-anchor="middle" x="799" y="-280" font-family="Arial" font-size="10.00">(logo_queue)</text>
|
<text text-anchor="middle" x="415" y="-867.91" font-family="Arial" font-size="10.00" fill="#ffffff">(logo_queue)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- redis->logo_worker -->
|
<!-- redis->logo_worker -->
|
||||||
<g id="edge10" class="edge">
|
<g id="edge19" class="edge">
|
||||||
<title>redis->logo_worker</title>
|
<title>redis->logo_worker</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M636.79,-372.44C643.41,-369.56 650.35,-366.76 657,-364.5 696.75,-350.97 712.15,-363.13 749,-343 758.87,-337.61 768.17,-329.73 776.01,-321.89"/>
|
<path fill="none" stroke="#2e7d32" d="M643.3,-1170.14C603.28,-1119.71 486.35,-972.34 437.19,-910.38"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="779,-323.82 783.33,-314.15 773.91,-319.01 779,-323.82"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="439.67,-907.88 430.71,-902.22 434.19,-912.23 439.67,-907.88"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- visual_worker -->
|
<!-- visual_worker -->
|
||||||
<g id="node19" class="node">
|
<g id="node28" class="node">
|
||||||
<title>visual_worker</title>
|
<title>visual_worker</title>
|
||||||
<polygon fill="#fff3cd" stroke="black" points="739.5,-314 648.5,-314 648.5,-273 739.5,-273 739.5,-314"/>
|
<polygon fill="#ffc107" stroke="black" points="569.5,-901.91 478.5,-901.91 478.5,-860.91 569.5,-860.91 569.5,-901.91"/>
|
||||||
<text text-anchor="middle" x="694" y="-302" font-family="Arial" font-size="10.00">Visual Properties</text>
|
<text text-anchor="middle" x="524" y="-889.91" font-family="Arial" font-size="10.00" fill="#212529">Visual Properties</text>
|
||||||
<text text-anchor="middle" x="694" y="-291" font-family="Arial" font-size="10.00">Worker</text>
|
<text text-anchor="middle" x="524" y="-878.91" font-family="Arial" font-size="10.00" fill="#212529">Worker</text>
|
||||||
<text text-anchor="middle" x="694" y="-280" font-family="Arial" font-size="10.00">(visual_queue)</text>
|
<text text-anchor="middle" x="524" y="-867.91" font-family="Arial" font-size="10.00" fill="#212529">(visual_queue)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- redis->visual_worker -->
|
<!-- redis->visual_worker -->
|
||||||
<g id="edge20" class="edge">
|
<g id="edge30" class="edge">
|
||||||
<title>redis->visual_worker</title>
|
<title>redis->visual_worker</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M618.87,-372.08C632.52,-357.99 651.96,-337.91 667.65,-321.71"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M649.47,-1170.14C627.61,-1120.02 564,-974.15 536.69,-911.53"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="670.57,-323.73 675.01,-314.11 665.54,-318.86 670.57,-323.73"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="539.84,-909.99 532.64,-902.22 533.42,-912.78 539.84,-909.99"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- audio_worker -->
|
<!-- audio_worker -->
|
||||||
<g id="node20" class="node">
|
<g id="node29" class="node">
|
||||||
<title>audio_worker</title>
|
<title>audio_worker</title>
|
||||||
<polygon fill="#fff3cd" stroke="black" points="521.5,-314 434.5,-314 434.5,-273 521.5,-273 521.5,-314"/>
|
<polygon fill="#ffc107" stroke="black" points="791.5,-901.91 704.5,-901.91 704.5,-860.91 791.5,-860.91 791.5,-901.91"/>
|
||||||
<text text-anchor="middle" x="478" y="-302" font-family="Arial" font-size="10.00">Audio Transcript</text>
|
<text text-anchor="middle" x="748" y="-889.91" font-family="Arial" font-size="10.00" fill="#212529">Audio Transcript</text>
|
||||||
<text text-anchor="middle" x="478" y="-291" font-family="Arial" font-size="10.00">Worker</text>
|
<text text-anchor="middle" x="748" y="-878.91" font-family="Arial" font-size="10.00" fill="#212529">Worker</text>
|
||||||
<text text-anchor="middle" x="478" y="-280" font-family="Arial" font-size="10.00">(audio_queue)</text>
|
<text text-anchor="middle" x="748" y="-867.91" font-family="Arial" font-size="10.00" fill="#212529">(audio_queue)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- redis->audio_worker -->
|
<!-- redis->audio_worker -->
|
||||||
<g id="edge21" class="edge">
|
<g id="edge31" class="edge">
|
||||||
<title>redis->audio_worker</title>
|
<title>redis->audio_worker</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M573.63,-372.4C560.34,-364.02 544.5,-353.49 531,-343 522.43,-336.34 513.56,-328.61 505.59,-321.3"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M662.15,-1170.14C677.08,-1120.13 720.45,-974.75 739.2,-911.91"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="507.67,-318.45 497.96,-314.19 502.89,-323.57 507.67,-318.45"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="742.59,-912.8 742.09,-902.22 735.88,-910.8 742.59,-912.8"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- text_worker -->
|
<!-- text_worker -->
|
||||||
<g id="node21" class="node">
|
<g id="node30" class="node">
|
||||||
<title>text_worker</title>
|
<title>text_worker</title>
|
||||||
<polygon fill="#fff3cd" stroke="black" points="630,-314 540,-314 540,-273 630,-273 630,-314"/>
|
<polygon fill="#ffc107" stroke="black" points="682,-901.91 592,-901.91 592,-860.91 682,-860.91 682,-901.91"/>
|
||||||
<text text-anchor="middle" x="585" y="-302" font-family="Arial" font-size="10.00">Text Recognition</text>
|
<text text-anchor="middle" x="637" y="-889.91" font-family="Arial" font-size="10.00" fill="#212529">Text Recognition</text>
|
||||||
<text text-anchor="middle" x="585" y="-291" font-family="Arial" font-size="10.00">Worker</text>
|
<text text-anchor="middle" x="637" y="-878.91" font-family="Arial" font-size="10.00" fill="#212529">Worker</text>
|
||||||
<text text-anchor="middle" x="585" y="-280" font-family="Arial" font-size="10.00">(text_queue)</text>
|
<text text-anchor="middle" x="637" y="-867.91" font-family="Arial" font-size="10.00" fill="#212529">(text_queue)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- redis->text_worker -->
|
<!-- redis->text_worker -->
|
||||||
<g id="edge22" class="edge">
|
<g id="edge32" class="edge">
|
||||||
<title>redis->text_worker</title>
|
<title>redis->text_worker</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M598.88,-372.08C596.5,-358.76 593.16,-340.08 590.35,-324.39"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M655.87,-1170.14C652.59,-1120.23 643.09,-975.35 638.96,-912.3"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="593.72,-323.34 588.51,-314.11 586.83,-324.57 593.72,-323.34"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="642.45,-911.97 638.3,-902.22 635.46,-912.43 642.45,-911.97"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- nginx -->
|
<!-- nginx -->
|
||||||
<g id="node13" class="node">
|
<g id="node15" class="node">
|
||||||
<title>nginx</title>
|
<title>nginx</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="645,-525 563,-525 563,-484 645,-484 645,-525"/>
|
<polygon fill="#28a745" stroke="black" points="792,-1507.91 710,-1507.91 710,-1466.91 792,-1466.91 792,-1507.91"/>
|
||||||
<text text-anchor="middle" x="604" y="-513" font-family="Arial" font-size="10.00">NGINX</text>
|
<text text-anchor="middle" x="751" y="-1495.91" font-family="Arial" font-size="10.00" fill="#ffffff">NGINX</text>
|
||||||
<text text-anchor="middle" x="604" y="-502" font-family="Arial" font-size="10.00">Reverse Proxy</text>
|
<text text-anchor="middle" x="751" y="-1484.91" font-family="Arial" font-size="10.00" fill="#ffffff">Reverse Proxy</text>
|
||||||
<text text-anchor="middle" x="604" y="-491" font-family="Arial" font-size="10.00">:80</text>
|
<text text-anchor="middle" x="751" y="-1473.91" font-family="Arial" font-size="10.00" fill="#ffffff">:80</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- nginx->angular -->
|
<!-- nginx->angular -->
|
||||||
<g id="edge17" class="edge">
|
<g id="edge27" class="edge">
|
||||||
<title>nginx->angular</title>
|
<title>nginx->angular</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M645.14,-501.31C707.29,-497.34 821.55,-487.34 856,-466 873.37,-455.24 886.48,-436.29 895.19,-420.19"/>
|
<path fill="none" stroke="#2e7d32" d="M758.25,-1466.47C776.85,-1415.32 826.31,-1279.3 848.26,-1218.96"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="898.39,-421.63 899.81,-411.13 892.15,-418.45 898.39,-421.63"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="851.65,-1219.87 851.78,-1209.28 845.07,-1217.48 851.65,-1219.87"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_strategy -->
|
<!-- base_strategy -->
|
||||||
<g id="node14" class="node">
|
<g id="node16" class="node">
|
||||||
<title>base_strategy</title>
|
<title>base_strategy</title>
|
||||||
<polygon fill="#c8e6c9" stroke="black" points="1183.5,-311.5 1064.5,-311.5 1064.5,-275.5 1183.5,-275.5 1183.5,-311.5"/>
|
<polygon fill="#ce93d8" stroke="black" points="1777.5,-899.41 1658.5,-899.41 1658.5,-863.41 1777.5,-863.41 1777.5,-899.41"/>
|
||||||
<text text-anchor="middle" x="1124" y="-296.5" font-family="Arial" font-size="10.00">BaseExecutionStrategy</text>
|
<text text-anchor="middle" x="1718" y="-884.41" font-family="Arial" font-size="10.00">BaseExecutionStrategy</text>
|
||||||
<text text-anchor="middle" x="1124" y="-285.5" font-family="Arial" font-size="10.00">(Abstract)</text>
|
<text text-anchor="middle" x="1718" y="-873.41" font-family="Arial" font-size="10.00">(Abstract)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- local_strategy -->
|
<!-- local_strategy -->
|
||||||
<g id="node15" class="node">
|
<g id="node17" class="node">
|
||||||
<title>local_strategy</title>
|
<title>local_strategy</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="1255.5,-217 1178.5,-217 1178.5,-181 1255.5,-181 1255.5,-217"/>
|
<polygon fill="#28a745" stroke="black" points="1659.5,-640.41 1582.5,-640.41 1582.5,-604.41 1659.5,-604.41 1659.5,-640.41"/>
|
||||||
<text text-anchor="middle" x="1217" y="-196.5" font-family="Arial" font-size="10.00">LocalStrategy</text>
|
<text text-anchor="middle" x="1621" y="-619.91" font-family="Arial" font-size="10.00" fill="#ffffff">LocalStrategy</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_strategy->local_strategy -->
|
<!-- base_strategy->local_strategy -->
|
||||||
<g id="edge31" class="edge">
|
<g id="edge41" class="edge">
|
||||||
<title>base_strategy->local_strategy</title>
|
<title>base_strategy->local_strategy</title>
|
||||||
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1141.49,-275.1C1155.87,-260.8 1176.41,-240.37 1192.44,-224.43"/>
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1711.48,-863.15C1694.88,-819.17 1650.91,-702.65 1631.02,-649.96"/>
|
||||||
<polygon fill="gray" stroke="gray" points="1195.28,-226.54 1199.9,-217.01 1190.34,-221.58 1195.28,-226.54"/>
|
<polygon fill="gray" stroke="gray" points="1634.29,-648.72 1627.49,-640.6 1627.74,-651.19 1634.29,-648.72"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- lan_strategy -->
|
<!-- lan_strategy -->
|
||||||
<g id="node16" class="node">
|
<g id="node18" class="node">
|
||||||
<title>lan_strategy</title>
|
<title>lan_strategy</title>
|
||||||
<polygon fill="#fff3cd" stroke="black" points="1160.5,-217 1087.5,-217 1087.5,-181 1160.5,-181 1160.5,-217"/>
|
<polygon fill="#ffc107" stroke="black" points="1754.5,-640.41 1681.5,-640.41 1681.5,-604.41 1754.5,-604.41 1754.5,-640.41"/>
|
||||||
<text text-anchor="middle" x="1124" y="-196.5" font-family="Arial" font-size="10.00">LANStrategy</text>
|
<text text-anchor="middle" x="1718" y="-619.91" font-family="Arial" font-size="10.00" fill="#212529">LANStrategy</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_strategy->lan_strategy -->
|
<!-- base_strategy->lan_strategy -->
|
||||||
<g id="edge32" class="edge">
|
<g id="edge42" class="edge">
|
||||||
<title>base_strategy->lan_strategy</title>
|
<title>base_strategy->lan_strategy</title>
|
||||||
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1124,-275.1C1124,-261.58 1124,-242.58 1124,-227.07"/>
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1718,-863.15C1718,-819.35 1718,-703.62 1718,-650.62"/>
|
||||||
<polygon fill="gray" stroke="gray" points="1127.5,-227.01 1124,-217.01 1120.5,-227.01 1127.5,-227.01"/>
|
<polygon fill="gray" stroke="gray" points="1721.5,-650.6 1718,-640.6 1714.5,-650.6 1721.5,-650.6"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- cloud_strategy -->
|
<!-- cloud_strategy -->
|
||||||
<g id="node17" class="node">
|
<g id="node19" class="node">
|
||||||
<title>cloud_strategy</title>
|
<title>cloud_strategy</title>
|
||||||
<polygon fill="#fff3cd" stroke="black" points="1069.5,-217 990.5,-217 990.5,-181 1069.5,-181 1069.5,-217"/>
|
<polygon fill="#ffc107" stroke="black" points="1855.5,-640.41 1776.5,-640.41 1776.5,-604.41 1855.5,-604.41 1855.5,-640.41"/>
|
||||||
<text text-anchor="middle" x="1030" y="-196.5" font-family="Arial" font-size="10.00">CloudStrategy</text>
|
<text text-anchor="middle" x="1816" y="-619.91" font-family="Arial" font-size="10.00" fill="#212529">CloudStrategy</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_strategy->cloud_strategy -->
|
<!-- base_strategy->cloud_strategy -->
|
||||||
<g id="edge33" class="edge">
|
<g id="edge43" class="edge">
|
||||||
<title>base_strategy->cloud_strategy</title>
|
<title>base_strategy->cloud_strategy</title>
|
||||||
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1106.32,-275.1C1091.79,-260.8 1071.02,-240.37 1054.82,-224.43"/>
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1724.58,-863.15C1741.35,-819.17 1785.79,-702.65 1805.88,-649.96"/>
|
||||||
<polygon fill="gray" stroke="gray" points="1056.87,-221.53 1047.28,-217.01 1051.96,-226.52 1056.87,-221.53"/>
|
<polygon fill="gray" stroke="gray" points="1809.15,-651.19 1809.45,-640.6 1802.61,-648.7 1809.15,-651.19"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- clip_adapter -->
|
<!-- clip_adapter -->
|
||||||
<g id="node23" class="node">
|
<g id="node32" class="node">
|
||||||
<title>clip_adapter</title>
|
<title>clip_adapter</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="1565,-144 1493,-144 1493,-108 1565,-108 1565,-144"/>
|
<polygon fill="#28a745" stroke="black" points="1337,-413.41 1265,-413.41 1265,-377.41 1337,-377.41 1337,-413.41"/>
|
||||||
<text text-anchor="middle" x="1529" y="-129" font-family="Arial" font-size="10.00">CLIPAdapter</text>
|
<text text-anchor="middle" x="1301" y="-398.41" font-family="Arial" font-size="10.00" fill="#ffffff">CLIPAdapter</text>
|
||||||
<text text-anchor="middle" x="1529" y="-118" font-family="Arial" font-size="10.00">(Local)</text>
|
<text text-anchor="middle" x="1301" y="-387.41" font-family="Arial" font-size="10.00" fill="#ffffff">(Local)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- local_strategy->clip_adapter -->
|
<!-- local_strategy->clip_adapter -->
|
||||||
<g id="edge12" class="edge">
|
<g id="edge21" class="edge">
|
||||||
<title>local_strategy->clip_adapter</title>
|
<title>local_strategy->clip_adapter</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M1247.91,-180.89C1254.39,-177.86 1261.3,-175.03 1268,-173 1357.7,-145.78 1388.13,-166.29 1483.16,-143.84"/>
|
<path fill="none" stroke="#2e7d32" d="M1596.69,-604.32C1539.85,-564.36 1398.25,-464.8 1333.84,-419.5"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="1484,-147.24 1492.88,-141.46 1482.33,-140.44 1484,-147.24"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1335.47,-416.37 1325.28,-413.48 1331.44,-422.1 1335.47,-416.37"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- gcp_vision -->
|
<!-- gcp_vision -->
|
||||||
<g id="node24" class="node">
|
<g id="node33" class="node">
|
||||||
<title>gcp_vision</title>
|
<title>gcp_vision</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="1474.5,-144 1375.5,-144 1375.5,-108 1474.5,-108 1474.5,-144"/>
|
<polygon fill="#ffc107" stroke="black" points="1458.5,-413.41 1359.5,-413.41 1359.5,-377.41 1458.5,-377.41 1458.5,-413.41"/>
|
||||||
<text text-anchor="middle" x="1425" y="-129" font-family="Arial" font-size="10.00">GCPVisionAdapter</text>
|
<text text-anchor="middle" x="1409" y="-398.41" font-family="Arial" font-size="10.00" fill="#212529">GCPVisionAdapter</text>
|
||||||
<text text-anchor="middle" x="1425" y="-118" font-family="Arial" font-size="10.00">(Cloud)</text>
|
<text text-anchor="middle" x="1409" y="-387.41" font-family="Arial" font-size="10.00" fill="#212529">(Cloud)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- local_strategy->gcp_vision -->
|
<!-- local_strategy->gcp_vision -->
|
||||||
<g id="edge13" class="edge">
|
<g id="edge22" class="edge">
|
||||||
<title>local_strategy->gcp_vision</title>
|
<title>local_strategy->gcp_vision</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M1250.51,-180.96C1256.3,-178.19 1262.29,-175.44 1268,-173 1307.45,-156.15 1321.06,-156.16 1365.67,-144.29"/>
|
<path fill="none" stroke="#2e7d32" d="M1608.7,-604.39C1590.04,-578.99 1552.92,-530.11 1517,-492.41 1491.88,-466.05 1460.52,-438.72 1438.07,-419.99"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="1366.62,-147.66 1375.35,-141.66 1364.79,-140.9 1366.62,-147.66"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1440.17,-417.18 1430.23,-413.5 1435.7,-422.57 1440.17,-417.18"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- yolo_adapter -->
|
<!-- yolo_adapter -->
|
||||||
<g id="node25" class="node">
|
<g id="node34" class="node">
|
||||||
<title>yolo_adapter</title>
|
<title>yolo_adapter</title>
|
||||||
<polygon fill="#fff3cd" stroke="black" points="1357.5,-144 1280.5,-144 1280.5,-108 1357.5,-108 1357.5,-144"/>
|
<polygon fill="#ffc107" stroke="black" points="1557.5,-413.41 1480.5,-413.41 1480.5,-377.41 1557.5,-377.41 1557.5,-413.41"/>
|
||||||
<text text-anchor="middle" x="1319" y="-129" font-family="Arial" font-size="10.00">YOLOAdapter</text>
|
<text text-anchor="middle" x="1519" y="-398.41" font-family="Arial" font-size="10.00" fill="#212529">YOLOAdapter</text>
|
||||||
<text text-anchor="middle" x="1319" y="-118" font-family="Arial" font-size="10.00">(Planned)</text>
|
<text text-anchor="middle" x="1519" y="-387.41" font-family="Arial" font-size="10.00" fill="#212529">(Planned)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- lan_strategy->yolo_adapter -->
|
<!-- lan_strategy->yolo_adapter -->
|
||||||
<g id="edge27" class="edge">
|
<g id="edge37" class="edge">
|
||||||
<title>lan_strategy->yolo_adapter</title>
|
<title>lan_strategy->yolo_adapter</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M1153.27,-180.98C1158.74,-178.12 1164.47,-175.33 1170,-173 1202.73,-159.22 1241.04,-147.48 1270.61,-139.31"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M1702.72,-604.13C1667.69,-564.53 1581.5,-467.08 1540.87,-421.14"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="1271.56,-142.68 1280.29,-136.68 1269.72,-135.93 1271.56,-142.68"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="1543.31,-418.62 1534.06,-413.45 1538.07,-423.26 1543.31,-418.62"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- speech_api -->
|
<!-- speech_api -->
|
||||||
<g id="node31" class="node">
|
<g id="node40" class="node">
|
||||||
<title>speech_api</title>
|
<title>speech_api</title>
|
||||||
<polygon fill="#fff3cd" stroke="black" points="1255.5,-52 1154.5,-52 1154.5,-16 1255.5,-16 1255.5,-52"/>
|
<polygon fill="#ffc107" stroke="black" points="1705.5,-113.41 1604.5,-113.41 1604.5,-77.41 1705.5,-77.41 1705.5,-113.41"/>
|
||||||
<text text-anchor="middle" x="1205" y="-37" font-family="Arial" font-size="10.00">Speech-to-Text API</text>
|
<text text-anchor="middle" x="1655" y="-98.41" font-family="Arial" font-size="10.00" fill="#212529">Speech-to-Text API</text>
|
||||||
<text text-anchor="middle" x="1205" y="-26" font-family="Arial" font-size="10.00">(Audio Transcript)</text>
|
<text text-anchor="middle" x="1655" y="-87.41" font-family="Arial" font-size="10.00" fill="#212529">(Audio Transcript)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- cloud_strategy->speech_api -->
|
<!-- cloud_strategy->speech_api -->
|
||||||
<g id="edge26" class="edge">
|
<g id="edge36" class="edge">
|
||||||
<title>cloud_strategy->speech_api</title>
|
<title>cloud_strategy->speech_api</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M1048.4,-180.86C1079.64,-151.76 1143.17,-92.59 1178.97,-59.25"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M1810.73,-604.24C1787.25,-527.67 1691.98,-216.99 1663.24,-123.29"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="1181.65,-61.53 1186.59,-52.15 1176.88,-56.41 1181.65,-61.53"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="1666.5,-121.99 1660.23,-113.45 1659.81,-124.04 1666.5,-121.99"/>
|
||||||
|
</g>
|
||||||
|
<!-- base_event_source -->
|
||||||
|
<g id="node20" class="node">
|
||||||
|
<title>base_event_source</title>
|
||||||
|
<polygon fill="#ce93d8" stroke="black" points="1412,-1206.41 1300,-1206.41 1300,-1170.41 1412,-1170.41 1412,-1206.41"/>
|
||||||
|
<text text-anchor="middle" x="1356" y="-1191.41" font-family="Arial" font-size="10.00">SegmentEventSource</text>
|
||||||
|
<text text-anchor="middle" x="1356" y="-1180.41" font-family="Arial" font-size="10.00">(Abstract)</text>
|
||||||
|
</g>
|
||||||
|
<!-- file_watcher_source -->
|
||||||
|
<g id="node21" class="node">
|
||||||
|
<title>file_watcher_source</title>
|
||||||
|
<polygon fill="#28a745" stroke="black" points="1267.5,-899.41 1142.5,-899.41 1142.5,-863.41 1267.5,-863.41 1267.5,-899.41"/>
|
||||||
|
<text text-anchor="middle" x="1205" y="-884.41" font-family="Arial" font-size="10.00" fill="#ffffff">FileWatcherEventSource</text>
|
||||||
|
<text text-anchor="middle" x="1205" y="-873.41" font-family="Arial" font-size="10.00" fill="#ffffff">(Local/LAN)</text>
|
||||||
|
</g>
|
||||||
|
<!-- base_event_source->file_watcher_source -->
|
||||||
|
<g id="edge47" class="edge">
|
||||||
|
<title>base_event_source->file_watcher_source</title>
|
||||||
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1347.45,-1170.14C1322.22,-1119.18 1247.99,-969.25 1217.89,-908.46"/>
|
||||||
|
<polygon fill="gray" stroke="gray" points="1221.03,-906.91 1213.46,-899.5 1214.76,-910.01 1221.03,-906.91"/>
|
||||||
|
</g>
|
||||||
|
<!-- cloud_storage_source -->
|
||||||
|
<g id="node22" class="node">
|
||||||
|
<title>cloud_storage_source</title>
|
||||||
|
<polygon fill="#ffc107" stroke="black" points="1422,-899.41 1290,-899.41 1290,-863.41 1422,-863.41 1422,-899.41"/>
|
||||||
|
<text text-anchor="middle" x="1356" y="-884.41" font-family="Arial" font-size="10.00" fill="#212529">CloudStorageEventSource</text>
|
||||||
|
<text text-anchor="middle" x="1356" y="-873.41" font-family="Arial" font-size="10.00" fill="#212529">(GCS Pub/Sub)</text>
|
||||||
|
</g>
|
||||||
|
<!-- base_event_source->cloud_storage_source -->
|
||||||
|
<g id="edge48" class="edge">
|
||||||
|
<title>base_event_source->cloud_storage_source</title>
|
||||||
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1356,-1170.14C1356,-1119.5 1356,-971.09 1356,-909.59"/>
|
||||||
|
<polygon fill="gray" stroke="gray" points="1359.5,-909.5 1356,-899.5 1352.5,-909.5 1359.5,-909.5"/>
|
||||||
|
</g>
|
||||||
|
<!-- webhook_source -->
|
||||||
|
<g id="node23" class="node">
|
||||||
|
<title>webhook_source</title>
|
||||||
|
<polygon fill="#ffc107" stroke="black" points="1557.5,-899.41 1444.5,-899.41 1444.5,-863.41 1557.5,-863.41 1557.5,-899.41"/>
|
||||||
|
<text text-anchor="middle" x="1501" y="-884.41" font-family="Arial" font-size="10.00" fill="#212529">WebhookEventSource</text>
|
||||||
|
<text text-anchor="middle" x="1501" y="-873.41" font-family="Arial" font-size="10.00" fill="#212529">(External)</text>
|
||||||
|
</g>
|
||||||
|
<!-- base_event_source->webhook_source -->
|
||||||
|
<g id="edge49" class="edge">
|
||||||
|
<title>base_event_source->webhook_source</title>
|
||||||
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1364.21,-1170.14C1388.39,-1119.29 1459.43,-969.86 1488.44,-908.83"/>
|
||||||
|
<polygon fill="gray" stroke="gray" points="1491.75,-910.03 1492.88,-899.5 1485.42,-907.03 1491.75,-910.03"/>
|
||||||
|
</g>
|
||||||
|
<!-- event_source_manager -->
|
||||||
|
<g id="node24" class="node">
|
||||||
|
<title>event_source_manager</title>
|
||||||
|
<polygon fill="#28a745" stroke="black" points="1241,-640.41 1131,-640.41 1131,-604.41 1241,-604.41 1241,-640.41"/>
|
||||||
|
<text text-anchor="middle" x="1186" y="-625.41" font-family="Arial" font-size="10.00" fill="#ffffff">EventSourceManager</text>
|
||||||
|
<text text-anchor="middle" x="1186" y="-614.41" font-family="Arial" font-size="10.00" fill="#ffffff">(Environment-based)</text>
|
||||||
|
</g>
|
||||||
|
<!-- file_watcher_source->event_source_manager -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>file_watcher_source->event_source_manager</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M1203.72,-863.15C1200.49,-819.35 1191.93,-703.62 1188.01,-650.62"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1191.5,-650.32 1187.27,-640.6 1184.52,-650.83 1191.5,-650.32"/>
|
||||||
|
</g>
|
||||||
|
<!-- cloud_storage_source->event_source_manager -->
|
||||||
|
<g id="edge14" class="edge">
|
||||||
|
<title>cloud_storage_source->event_source_manager</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M1344.58,-863.15C1315.31,-818.89 1237.45,-701.19 1202.91,-648.98"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1205.8,-647.01 1197.37,-640.6 1199.97,-650.87 1205.8,-647.01"/>
|
||||||
|
</g>
|
||||||
|
<!-- webhook_source->event_source_manager -->
|
||||||
|
<g id="edge15" class="edge">
|
||||||
|
<title>webhook_source->event_source_manager</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M1479.84,-863.15C1424.92,-818.35 1277.74,-698.27 1214.99,-647.07"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1217.03,-644.21 1207.07,-640.6 1212.6,-649.63 1217.03,-644.21"/>
|
||||||
|
</g>
|
||||||
|
<!-- redis_events -->
|
||||||
|
<g id="node25" class="node">
|
||||||
|
<title>redis_events</title>
|
||||||
|
<polygon fill="#28a745" stroke="black" points="1231,-413.41 1129,-413.41 1129,-377.41 1231,-377.41 1231,-413.41"/>
|
||||||
|
<text text-anchor="middle" x="1180" y="-398.41" font-family="Arial" font-size="10.00" fill="#ffffff">Redis Event Queue</text>
|
||||||
|
<text text-anchor="middle" x="1180" y="-387.41" font-family="Arial" font-size="10.00" fill="#ffffff">(segment_events)</text>
|
||||||
|
</g>
|
||||||
|
<!-- event_source_manager->redis_events -->
|
||||||
|
<g id="edge16" class="edge">
|
||||||
|
<title>event_source_manager->redis_events</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M1185.54,-604.13C1184.5,-565.18 1181.97,-470.29 1180.72,-423.47"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1184.22,-423.35 1180.45,-413.45 1177.22,-423.54 1184.22,-423.35"/>
|
||||||
|
<text text-anchor="middle" x="1200" y="-504.01" font-family="Arial" font-size="8.00">publishes</text>
|
||||||
|
<text text-anchor="middle" x="1200" y="-495.01" font-family="Arial" font-size="8.00">events</text>
|
||||||
|
</g>
|
||||||
|
<!-- event_processor -->
|
||||||
|
<g id="node26" class="node">
|
||||||
|
<title>event_processor</title>
|
||||||
|
<polygon fill="#28a745" stroke="black" points="1215,-113.41 1121,-113.41 1121,-77.41 1215,-77.41 1215,-113.41"/>
|
||||||
|
<text text-anchor="middle" x="1168" y="-98.41" font-family="Arial" font-size="10.00" fill="#ffffff">Event Processor</text>
|
||||||
|
<text text-anchor="middle" x="1168" y="-87.41" font-family="Arial" font-size="10.00" fill="#ffffff">(triggers analysis)</text>
|
||||||
|
</g>
|
||||||
|
<!-- redis_events->event_processor -->
|
||||||
|
<g id="edge17" class="edge">
|
||||||
|
<title>redis_events->event_processor</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M1179.3,-377.1C1177.31,-327.52 1171.55,-184.65 1169.11,-124.08"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1172.6,-123.62 1168.7,-113.77 1165.6,-123.9 1172.6,-123.62"/>
|
||||||
|
<text text-anchor="middle" x="1193.5" y="-277.01" font-family="Arial" font-size="8.00">consumes</text>
|
||||||
|
<text text-anchor="middle" x="1193.5" y="-268.01" font-family="Arial" font-size="8.00">events</text>
|
||||||
|
</g>
|
||||||
|
<!-- event_processor->logo_worker -->
|
||||||
|
<g id="edge18" class="edge">
|
||||||
|
<title>event_processor->logo_worker</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M1151.39,-113.71C1054.28,-214.82 560.34,-729.09 440.99,-853.36"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="438.39,-851.01 433.99,-860.64 443.44,-855.85 438.39,-851.01"/>
|
||||||
|
<text text-anchor="middle" x="801" y="-504.01" font-family="Arial" font-size="8.00">triggers</text>
|
||||||
|
<text text-anchor="middle" x="801" y="-495.01" font-family="Arial" font-size="8.00">analysis</text>
|
||||||
|
</g>
|
||||||
|
<!-- logo_worker->django -->
|
||||||
|
<g id="edge24" class="edge">
|
||||||
|
<title>logo_worker->django</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M422.76,-902.02C457.7,-990.34 600.65,-1351.58 642.46,-1457.25"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="639.25,-1458.65 646.18,-1466.66 645.76,-1456.07 639.25,-1458.65"/>
|
||||||
|
<text text-anchor="middle" x="568" y="-1191.01" font-family="Arial" font-size="8.00">analysis</text>
|
||||||
|
<text text-anchor="middle" x="568" y="-1182.01" font-family="Arial" font-size="8.00">results</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- logo_worker->local_strategy -->
|
<!-- logo_worker->local_strategy -->
|
||||||
<g id="edge11" class="edge">
|
<g id="edge20" class="edge">
|
||||||
<title>logo_worker->local_strategy</title>
|
<title>logo_worker->local_strategy</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M832.13,-272.96C838.84,-269.78 845.99,-266.9 853,-265 921.1,-246.49 1105.48,-274.6 1170,-246 1180.6,-241.3 1190.19,-233.09 1197.89,-224.9"/>
|
<path fill="none" stroke="#2e7d32" d="M448.15,-860.91C454.57,-857.81 461.36,-854.95 468,-852.91 943.1,-707.13 1131.75,-901.86 1571,-669.41 1581.38,-663.92 1591.24,-655.75 1599.43,-647.81"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="1200.72,-226.99 1204.68,-217.16 1195.46,-222.37 1200.72,-226.99"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1601.93,-650.25 1606.44,-640.66 1596.93,-645.35 1601.93,-650.25"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- visual_worker->lan_strategy -->
|
<!-- visual_worker->lan_strategy -->
|
||||||
<g id="edge23" class="edge">
|
<g id="edge33" class="edge">
|
||||||
<title>visual_worker->lan_strategy</title>
|
<title>visual_worker->lan_strategy</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M727.77,-272.95C734.61,-269.77 741.88,-266.9 749,-265 819.98,-246.08 1012.14,-276.42 1079,-246 1089.33,-241.3 1098.54,-233.09 1105.9,-224.9"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M559.74,-860.82C566.63,-857.72 573.91,-854.89 581,-852.91 918.99,-758.53 1024.46,-854.14 1367,-777.91 1507.03,-746.75 1549.36,-745.46 1671,-669.41 1680.29,-663.6 1689.23,-655.68 1696.76,-648.05"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="1108.64,-227.08 1112.37,-217.16 1103.27,-222.59 1108.64,-227.08"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="1699.47,-650.28 1703.79,-640.6 1694.38,-645.47 1699.47,-650.28"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- audio_worker->cloud_strategy -->
|
<!-- audio_worker->cloud_strategy -->
|
||||||
<g id="edge24" class="edge">
|
<g id="edge34" class="edge">
|
||||||
<title>audio_worker->cloud_strategy</title>
|
<title>audio_worker->cloud_strategy</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M511.46,-272.93C517.79,-269.87 524.47,-267.05 531,-265 689.92,-215.13 888.89,-203.49 980.12,-200.8"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M782.76,-860.78C789.32,-857.73 796.25,-854.92 803,-852.91 1081.29,-770.06 1168.88,-842.38 1452,-777.91 1595.97,-745.13 1639.22,-745.1 1766,-669.41 1775.72,-663.61 1785.18,-655.69 1793.21,-648.06"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="980.28,-204.29 990.18,-200.52 980.09,-197.29 980.28,-204.29"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="1796.09,-650.14 1800.72,-640.61 1791.16,-645.17 1796.09,-650.14"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- text_worker->cloud_strategy -->
|
<!-- text_worker->cloud_strategy -->
|
||||||
<g id="edge25" class="edge">
|
<g id="edge35" class="edge">
|
||||||
<title>text_worker->cloud_strategy</title>
|
<title>text_worker->cloud_strategy</title>
|
||||||
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M620.44,-272.86C626.82,-269.89 633.5,-267.11 640,-265 758.46,-226.48 904.94,-209.91 980.19,-203.53"/>
|
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M672.75,-860.87C679.64,-857.77 686.92,-854.92 694,-852.91 1003.5,-765.04 1098.77,-842.27 1414,-777.91 1574.4,-745.17 1624.04,-750.95 1766,-669.41 1775.82,-663.77 1785.31,-655.9 1793.33,-648.25"/>
|
||||||
<polygon fill="#f57f17" stroke="#f57f17" points="980.65,-207 990.33,-202.69 980.08,-200.02 980.65,-207"/>
|
<polygon fill="#f57f17" stroke="#f57f17" points="1796.21,-650.31 1800.82,-640.77 1791.27,-645.36 1796.21,-650.31"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_ai -->
|
<!-- base_ai -->
|
||||||
<g id="node22" class="node">
|
<g id="node31" class="node">
|
||||||
<title>base_ai</title>
|
<title>base_ai</title>
|
||||||
<polygon fill="#f8bbd9" stroke="black" points="1471,-217 1379,-217 1379,-181 1471,-181 1471,-217"/>
|
<polygon fill="#ce93d8" stroke="black" points="1455,-640.41 1363,-640.41 1363,-604.41 1455,-604.41 1455,-640.41"/>
|
||||||
<text text-anchor="middle" x="1425" y="-202" font-family="Arial" font-size="10.00">DetectionAdapter</text>
|
<text text-anchor="middle" x="1409" y="-625.41" font-family="Arial" font-size="10.00">DetectionAdapter</text>
|
||||||
<text text-anchor="middle" x="1425" y="-191" font-family="Arial" font-size="10.00">(Abstract)</text>
|
<text text-anchor="middle" x="1409" y="-614.41" font-family="Arial" font-size="10.00">(Abstract)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_ai->clip_adapter -->
|
<!-- base_ai->clip_adapter -->
|
||||||
<g id="edge34" class="edge">
|
<g id="edge44" class="edge">
|
||||||
<title>base_ai->clip_adapter</title>
|
<title>base_ai->clip_adapter</title>
|
||||||
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1449.91,-180.99C1463.57,-171.67 1480.7,-159.97 1495.52,-149.86"/>
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1400.7,-604.13C1381.89,-564.94 1335.89,-469.09 1313.56,-422.58"/>
|
||||||
<polygon fill="gray" stroke="gray" points="1497.65,-152.64 1503.93,-144.11 1493.7,-146.86 1497.65,-152.64"/>
|
<polygon fill="gray" stroke="gray" points="1316.66,-420.95 1309.18,-413.45 1310.35,-423.98 1316.66,-420.95"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_ai->gcp_vision -->
|
<!-- base_ai->gcp_vision -->
|
||||||
<g id="edge35" class="edge">
|
<g id="edge45" class="edge">
|
||||||
<title>base_ai->gcp_vision</title>
|
<title>base_ai->gcp_vision</title>
|
||||||
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1425,-180.81C1425,-172.79 1425,-163.05 1425,-154.07"/>
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1409,-604.13C1409,-565.18 1409,-470.29 1409,-423.47"/>
|
||||||
<polygon fill="gray" stroke="gray" points="1428.5,-154.03 1425,-144.03 1421.5,-154.03 1428.5,-154.03"/>
|
<polygon fill="gray" stroke="gray" points="1412.5,-423.45 1409,-413.45 1405.5,-423.45 1412.5,-423.45"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- base_ai->yolo_adapter -->
|
<!-- base_ai->yolo_adapter -->
|
||||||
<g id="edge36" class="edge">
|
<g id="edge46" class="edge">
|
||||||
<title>base_ai->yolo_adapter</title>
|
<title>base_ai->yolo_adapter</title>
|
||||||
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1399.61,-180.99C1385.68,-171.67 1368.23,-159.97 1353.13,-149.86"/>
|
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1417.45,-604.13C1436.61,-564.94 1483.47,-469.09 1506.21,-422.58"/>
|
||||||
<polygon fill="gray" stroke="gray" points="1354.81,-146.77 1344.55,-144.11 1350.91,-152.59 1354.81,-146.77"/>
|
<polygon fill="gray" stroke="gray" points="1509.43,-423.97 1510.67,-413.45 1503.14,-420.89 1509.43,-423.97"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- vision_api -->
|
<!-- vision_api -->
|
||||||
<g id="node30" class="node">
|
<g id="node39" class="node">
|
||||||
<title>vision_api</title>
|
<title>vision_api</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="1364.5,-52 1273.5,-52 1273.5,-16 1364.5,-16 1364.5,-52"/>
|
<polygon fill="#ffc107" stroke="black" points="1582.5,-113.41 1491.5,-113.41 1491.5,-77.41 1582.5,-77.41 1582.5,-113.41"/>
|
||||||
<text text-anchor="middle" x="1319" y="-37" font-family="Arial" font-size="10.00">Cloud Vision API</text>
|
<text text-anchor="middle" x="1537" y="-98.41" font-family="Arial" font-size="10.00" fill="#212529">Cloud Vision API</text>
|
||||||
<text text-anchor="middle" x="1319" y="-26" font-family="Arial" font-size="10.00">(Logo Detection)</text>
|
<text text-anchor="middle" x="1537" y="-87.41" font-family="Arial" font-size="10.00" fill="#212529">(Logo Detection)</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- gcp_vision->vision_api -->
|
<!-- gcp_vision->vision_api -->
|
||||||
<g id="edge14" class="edge">
|
<g id="edge23" class="edge">
|
||||||
<title>gcp_vision->vision_api</title>
|
<title>gcp_vision->vision_api</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M1404.56,-107.65C1388.28,-93.83 1365.32,-74.33 1347.28,-59.01"/>
|
<path fill="none" stroke="#2e7d32" d="M1416.44,-377.1C1437.87,-327.21 1499.87,-182.87 1525.6,-122.96"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="1349.26,-56.1 1339.38,-52.3 1344.73,-61.44 1349.26,-56.1"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1528.81,-124.34 1529.54,-113.77 1522.38,-121.58 1528.81,-124.34"/>
|
||||||
|
</g>
|
||||||
|
<!-- local_storage->file_watcher_source -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>local_storage->file_watcher_source</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M987.69,-1170.31C998.05,-1144.8 1019.48,-1095.78 1045,-1058.41 1085.53,-999.06 1144.91,-939.06 1178.77,-906.73"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1181.55,-908.92 1186.4,-899.5 1176.73,-903.84 1181.55,-908.92"/>
|
||||||
|
<text text-anchor="middle" x="1071.5" y="-1070.01" font-family="Arial" font-size="8.00">monitors</text>
|
||||||
|
<text text-anchor="middle" x="1071.5" y="-1061.01" font-family="Arial" font-size="8.00">HLS segments</text>
|
||||||
|
</g>
|
||||||
|
<!-- gcs_storage->cloud_storage_source -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>gcs_storage->cloud_storage_source</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M1095.63,-1170.14C1142.12,-1118.77 1279.66,-966.77 1333.77,-906.98"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1336.43,-909.26 1340.54,-899.5 1331.24,-904.56 1336.43,-909.26"/>
|
||||||
|
<text text-anchor="middle" x="1207" y="-1070.01" font-family="Arial" font-size="8.00">storage</text>
|
||||||
|
<text text-anchor="middle" x="1207" y="-1061.01" font-family="Arial" font-size="8.00">events</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- hls_player -->
|
<!-- hls_player -->
|
||||||
<g id="node29" class="node">
|
<g id="node38" class="node">
|
||||||
<title>hls_player</title>
|
<title>hls_player</title>
|
||||||
<polygon fill="#d4edda" stroke="black" points="966.5,-311.5 865.5,-311.5 865.5,-275.5 966.5,-275.5 966.5,-311.5"/>
|
<polygon fill="#28a745" stroke="black" points="917.5,-899.41 816.5,-899.41 816.5,-863.41 917.5,-863.41 917.5,-899.41"/>
|
||||||
<text text-anchor="middle" x="916" y="-296.5" font-family="Arial" font-size="10.00">HLS.js Player</text>
|
<text text-anchor="middle" x="867" y="-884.41" font-family="Arial" font-size="10.00" fill="#ffffff">HLS.js Player</text>
|
||||||
<text text-anchor="middle" x="916" y="-285.5" font-family="Arial" font-size="10.00">+ Canvas Overlays</text>
|
<text text-anchor="middle" x="867" y="-873.41" font-family="Arial" font-size="10.00" fill="#ffffff">+ Canvas Overlays</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- angular->hls_player -->
|
<!-- angular->hls_player -->
|
||||||
<g id="edge16" class="edge">
|
<g id="edge26" class="edge">
|
||||||
<title>angular->hls_player</title>
|
<title>angular->hls_player</title>
|
||||||
<path fill="none" stroke="#2e7d32" d="M909.66,-369.82C910.83,-355.86 912.42,-336.96 913.72,-321.59"/>
|
<path fill="none" stroke="#2e7d32" d="M859.51,-1167.87C860.89,-1115.27 864.69,-970.56 866.28,-909.82"/>
|
||||||
<polygon fill="#2e7d32" stroke="#2e7d32" points="917.21,-321.87 914.56,-311.61 910.23,-321.28 917.21,-321.87"/>
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="869.79,-909.59 866.55,-899.5 862.79,-909.4 869.79,-909.59"/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 39 KiB |
@@ -5,7 +5,9 @@ digraph MediaAnalyzer {
|
|||||||
fontname="Arial";
|
fontname="Arial";
|
||||||
fontsize=12;
|
fontsize=12;
|
||||||
ratio=fill;
|
ratio=fill;
|
||||||
size="12,16!";
|
size="9,10!";
|
||||||
|
ranksep=0.3;
|
||||||
|
nodesep=0.3;
|
||||||
|
|
||||||
// Node defaults
|
// Node defaults
|
||||||
node [fontname="Arial", fontsize=10, shape=box, style=filled];
|
node [fontname="Arial", fontsize=10, shape=box, style=filled];
|
||||||
@@ -18,111 +20,137 @@ digraph MediaAnalyzer {
|
|||||||
color=lightgray;
|
color=lightgray;
|
||||||
node [shape=plaintext, style=filled];
|
node [shape=plaintext, style=filled];
|
||||||
|
|
||||||
implemented [label="✓ Implemented", fillcolor="#d4edda", color="#155724"];
|
implemented [label="✅ Implemented", fillcolor="#28a745", color="#ffffff", fontcolor="#ffffff"];
|
||||||
planned [label="○ Planned", fillcolor="#fff3cd", color="#856404"];
|
planned [label="⏳ Planned", fillcolor="#ffc107", color="#212529", fontcolor="#212529"];
|
||||||
|
code_pattern [label="📐 Code Pattern", fillcolor="#8e24aa", color="#ffffff", fontcolor="#ffffff"];
|
||||||
|
service_arch [label="⚡ Service/Architecture", fillcolor="#1976d2", color="#ffffff", fontcolor="#ffffff"];
|
||||||
|
|
||||||
implemented -> planned [style=invis];
|
implemented -> planned [style=invis];
|
||||||
|
code_pattern -> service_arch [style=invis];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input Sources
|
// Input Sources
|
||||||
subgraph cluster_sources {
|
subgraph cluster_sources {
|
||||||
label="Video Sources";
|
label="Video Sources";
|
||||||
style=filled;
|
style=filled;
|
||||||
color="#e3f2fd";
|
color="#1976d2";
|
||||||
|
|
||||||
webcam [label="Webcam", fillcolor="#d4edda"];
|
webcam [label="Webcam", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
rtmp [label="RTMP/OBS", fillcolor="#d4edda"];
|
rtmp [label="RTMP/OBS", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
files [label="File Upload", fillcolor="#fff3cd"];
|
files [label="File Upload", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source Adapters (Design Pattern)
|
// Source Adapters (Design Pattern)
|
||||||
subgraph cluster_adapters {
|
subgraph cluster_adapters {
|
||||||
label="Source Adapters Pattern";
|
label="Source Adapters Pattern";
|
||||||
style=filled;
|
style=filled;
|
||||||
color="#f3e5f5";
|
color="#8e24aa";
|
||||||
|
|
||||||
base_adapter [label="BaseSourceAdapter\n(Abstract)", fillcolor="#e1bee7"];
|
base_adapter [label="BaseSourceAdapter\n(Abstract)", fillcolor="#ce93d8"];
|
||||||
webcam_adapter [label="WebcamAdapter", fillcolor="#d4edda"];
|
webcam_adapter [label="WebcamAdapter", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
rtmp_adapter [label="RtmpAdapter", fillcolor="#d4edda"];
|
rtmp_adapter [label="RtmpAdapter", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
file_adapter [label="FileAdapter", fillcolor="#fff3cd"];
|
file_adapter [label="FileAdapter", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core Services
|
// Core Services
|
||||||
subgraph cluster_core {
|
subgraph cluster_core {
|
||||||
label="Core Platform";
|
label="Core Platform";
|
||||||
style=filled;
|
style=filled;
|
||||||
color="#fff3e0";
|
color="#1976d2";
|
||||||
|
|
||||||
django [label="Django API\n+ Channels\n:8000", fillcolor="#d4edda"];
|
django [label="Django API\n+ Channels\n:8000", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
postgres [label="PostgreSQL\nDatabase", fillcolor="#d4edda"];
|
postgres [label="PostgreSQL\nDatabase", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
redis [label="Redis\nCache & Broker", fillcolor="#d4edda"];
|
redis [label="Redis\nCache & Broker", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
nginx [label="NGINX\nReverse Proxy\n:80", fillcolor="#d4edda"];
|
nginx [label="NGINX\nReverse Proxy\n:80", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execution Strategies (Design Pattern)
|
// Execution Strategies (Design Pattern)
|
||||||
subgraph cluster_execution {
|
subgraph cluster_execution {
|
||||||
label="Execution Strategies Pattern";
|
label="Execution Strategies Pattern";
|
||||||
style=filled;
|
style=filled;
|
||||||
color="#e8f5e8";
|
color="#8e24aa";
|
||||||
|
|
||||||
base_strategy [label="BaseExecutionStrategy\n(Abstract)", fillcolor="#c8e6c9"];
|
base_strategy [label="BaseExecutionStrategy\n(Abstract)", fillcolor="#ce93d8"];
|
||||||
local_strategy [label="LocalStrategy", fillcolor="#d4edda"];
|
local_strategy [label="LocalStrategy", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
lan_strategy [label="LANStrategy", fillcolor="#fff3cd"];
|
lan_strategy [label="LANStrategy", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
cloud_strategy [label="CloudStrategy", fillcolor="#fff3cd"];
|
cloud_strategy [label="CloudStrategy", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analysis Workers (Queue Segregation)
|
// Event Source Pattern (NEW)
|
||||||
subgraph cluster_workers {
|
subgraph cluster_event_sources {
|
||||||
label="Celery Workers (Queue Segregation)";
|
label="Event Source Pattern";
|
||||||
style=filled;
|
style=filled;
|
||||||
color="#e3f2fd";
|
color="#8e24aa";
|
||||||
|
|
||||||
logo_worker [label="Logo Detection\nWorker\n(logo_queue)", fillcolor="#d4edda"];
|
base_event_source [label="SegmentEventSource\n(Abstract)", fillcolor="#ce93d8"];
|
||||||
visual_worker [label="Visual Properties\nWorker\n(visual_queue)", fillcolor="#fff3cd"];
|
file_watcher_source [label="FileWatcherEventSource\n(Local/LAN)", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
audio_worker [label="Audio Transcript\nWorker\n(audio_queue)", fillcolor="#fff3cd"];
|
cloud_storage_source [label="CloudStorageEventSource\n(GCS Pub/Sub)", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
text_worker [label="Text Recognition\nWorker\n(text_queue)", fillcolor="#fff3cd"];
|
webhook_source [label="WebhookEventSource\n(External)", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Driven Processing Pipeline
|
||||||
|
subgraph cluster_events {
|
||||||
|
label="Event-Driven Processing";
|
||||||
|
style=filled;
|
||||||
|
color="#1976d2";
|
||||||
|
|
||||||
|
event_source_manager [label="EventSourceManager\n(Environment-based)", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
|
redis_events [label="Redis Event Queue\n(segment_events)", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
|
event_processor [label="Event Processor\n(triggers analysis)", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analysis Workers
|
||||||
|
subgraph cluster_workers {
|
||||||
|
label="Celery Workers";
|
||||||
|
style=filled;
|
||||||
|
color="#1976d2";
|
||||||
|
|
||||||
|
logo_worker [label="Logo Detection\nWorker\n(logo_queue)", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
|
visual_worker [label="Visual Properties\nWorker\n(visual_queue)", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
|
audio_worker [label="Audio Transcript\nWorker\n(audio_queue)", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
|
text_worker [label="Text Recognition\nWorker\n(text_queue)", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI Adapters (Design Pattern)
|
// AI Adapters (Design Pattern)
|
||||||
subgraph cluster_ai_adapters {
|
subgraph cluster_ai_adapters {
|
||||||
label="Analysis Adapters Pattern";
|
label="Analysis Adapters Pattern";
|
||||||
style=filled;
|
style=filled;
|
||||||
color="#fce4ec";
|
color="#8e24aa";
|
||||||
|
|
||||||
base_ai [label="DetectionAdapter\n(Abstract)", fillcolor="#f8bbd9"];
|
base_ai [label="DetectionAdapter\n(Abstract)", fillcolor="#ce93d8"];
|
||||||
clip_adapter [label="CLIPAdapter\n(Local)", fillcolor="#d4edda"];
|
clip_adapter [label="CLIPAdapter\n(Local)", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
gcp_vision [label="GCPVisionAdapter\n(Cloud)", fillcolor="#d4edda"];
|
gcp_vision [label="GCPVisionAdapter\n(Cloud)", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
yolo_adapter [label="YOLOAdapter\n(Planned)", fillcolor="#fff3cd"];
|
yolo_adapter [label="YOLOAdapter\n(Planned)", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage Options
|
// Storage Options
|
||||||
subgraph cluster_storage {
|
subgraph cluster_storage {
|
||||||
label="Media Storage";
|
label="Media Storage";
|
||||||
style=filled;
|
style=filled;
|
||||||
color="#f1f8e9";
|
color="#1976d2";
|
||||||
|
|
||||||
local_storage [label="Local Files\n(nginx-served)", fillcolor="#d4edda"];
|
local_storage [label="Local Files\n(nginx-served)", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
gcs_storage [label="Google Cloud\nStorage", fillcolor="#d4edda"];
|
gcs_storage [label="Google Cloud\nStorage", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frontend
|
// Frontend
|
||||||
subgraph cluster_frontend {
|
subgraph cluster_frontend {
|
||||||
label="Frontend";
|
label="Frontend";
|
||||||
style=filled;
|
style=filled;
|
||||||
color="#e8eaf6";
|
color="#1976d2";
|
||||||
|
|
||||||
angular [label="Angular 17 SPA\n+ WebSocket\n:4200", fillcolor="#d4edda"];
|
angular [label="Angular 17 SPA\n+ WebSocket\n:4200", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
hls_player [label="HLS.js Player\n+ Canvas Overlays", fillcolor="#d4edda"];
|
hls_player [label="HLS.js Player\n+ Canvas Overlays", fillcolor="#28a745", fontcolor="#ffffff"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cloud Services
|
// Cloud Services
|
||||||
subgraph cluster_cloud {
|
subgraph cluster_cloud {
|
||||||
label="GCP Services";
|
label="GCP Services";
|
||||||
style=filled;
|
style=filled;
|
||||||
color="#e0f2f1";
|
color="#1976d2";
|
||||||
|
|
||||||
vision_api [label="Cloud Vision API\n(Logo Detection)", fillcolor="#d4edda"];
|
vision_api [label="Cloud Vision API\n(Logo Detection)", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
speech_api [label="Speech-to-Text API\n(Audio Transcript)", fillcolor="#fff3cd"];
|
speech_api [label="Speech-to-Text API\n(Audio Transcript)", fillcolor="#ffc107", fontcolor="#212529"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connections - Current Implementation (solid)
|
// Connections - Current Implementation (solid)
|
||||||
@@ -137,12 +165,26 @@ digraph MediaAnalyzer {
|
|||||||
django -> local_storage [color="#2e7d32"];
|
django -> local_storage [color="#2e7d32"];
|
||||||
django -> gcs_storage [color="#2e7d32"];
|
django -> gcs_storage [color="#2e7d32"];
|
||||||
|
|
||||||
|
// Event source pattern connections
|
||||||
|
local_storage -> file_watcher_source [label="monitors\nHLS segments", color="#2e7d32"];
|
||||||
|
gcs_storage -> cloud_storage_source [label="storage\nevents", color="#2e7d32"];
|
||||||
|
file_watcher_source -> event_source_manager [color="#2e7d32"];
|
||||||
|
cloud_storage_source -> event_source_manager [color="#2e7d32"];
|
||||||
|
webhook_source -> event_source_manager [color="#2e7d32"];
|
||||||
|
|
||||||
|
// Event-driven processing flow
|
||||||
|
event_source_manager -> redis_events [label="publishes\nevents", color="#2e7d32"];
|
||||||
|
redis_events -> event_processor [label="consumes\nevents", color="#2e7d32"];
|
||||||
|
event_processor -> logo_worker [label="triggers\nanalysis", color="#2e7d32"];
|
||||||
|
|
||||||
redis -> logo_worker [color="#2e7d32"];
|
redis -> logo_worker [color="#2e7d32"];
|
||||||
logo_worker -> local_strategy [color="#2e7d32"];
|
logo_worker -> local_strategy [color="#2e7d32"];
|
||||||
local_strategy -> clip_adapter [color="#2e7d32"];
|
local_strategy -> clip_adapter [color="#2e7d32"];
|
||||||
local_strategy -> gcp_vision [color="#2e7d32"];
|
local_strategy -> gcp_vision [color="#2e7d32"];
|
||||||
gcp_vision -> vision_api [color="#2e7d32"];
|
gcp_vision -> vision_api [color="#2e7d32"];
|
||||||
|
|
||||||
|
// WebSocket real-time updates
|
||||||
|
logo_worker -> django [label="analysis\nresults", color="#2e7d32"];
|
||||||
django -> angular [label="WebSocket\nAPI", color="#2e7d32"];
|
django -> angular [label="WebSocket\nAPI", color="#2e7d32"];
|
||||||
angular -> hls_player [color="#2e7d32"];
|
angular -> hls_player [color="#2e7d32"];
|
||||||
nginx -> angular [color="#2e7d32"];
|
nginx -> angular [color="#2e7d32"];
|
||||||
@@ -174,4 +216,9 @@ digraph MediaAnalyzer {
|
|||||||
base_ai -> clip_adapter [style=dotted, color=gray];
|
base_ai -> clip_adapter [style=dotted, color=gray];
|
||||||
base_ai -> gcp_vision [style=dotted, color=gray];
|
base_ai -> gcp_vision [style=dotted, color=gray];
|
||||||
base_ai -> yolo_adapter [style=dotted, color=gray];
|
base_ai -> yolo_adapter [style=dotted, color=gray];
|
||||||
|
|
||||||
|
// Event source inheritance (NEW)
|
||||||
|
base_event_source -> file_watcher_source [style=dotted, color=gray];
|
||||||
|
base_event_source -> cloud_storage_source [style=dotted, color=gray];
|
||||||
|
base_event_source -> webhook_source [style=dotted, color=gray];
|
||||||
}
|
}
|
||||||
10059
def/file_watching_event_refactor_context
Normal file
10059
def/file_watching_event_refactor_context
Normal file
File diff suppressed because it is too large
Load Diff
161
def/improvements/moving_file_watcher_events
Normal file
161
def/improvements/moving_file_watcher_events
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
Two issues here:
|
||||||
|
|
||||||
|
1. Container isolation problem - File-watcher creates temp directory in its container, but
|
||||||
|
celery-logo can't access it (different containers)
|
||||||
|
2. Strategy logging spam - "Configured execution strategy: local" logs on every task
|
||||||
|
|
||||||
|
Let me fix both:
|
||||||
|
|
||||||
|
Fix 1: Use shared volume instead of container-local temp directory
|
||||||
|
|
||||||
|
● Update(media-analyzer/backend/streaming/file_watcher.py)
|
||||||
|
⎿ User rejected update to media-analyzer/backend/streaming/file_watcher.py
|
||||||
|
18 self.processed_files = set()
|
||||||
|
19 self.analyzer = VideoAnalyzer()
|
||||||
|
20
|
||||||
|
21 - # Create a persistent directory for analysis segments
|
||||||
|
21 + # Create a persistent directory for analysis segments in shared volume
|
||||||
|
22 self.analysis_dir = self.media_dir / 'analysis_segments'
|
||||||
|
23 try:
|
||||||
|
24 - self.analysis_dir.mkdir(exist_ok=True)
|
||||||
|
24 + self.analysis_dir.mkdir(exist_ok=True, mode=0o755)
|
||||||
|
25 + logger.info(f"File watcher: Using analysis directory: {self.analysis_dir}")
|
||||||
|
26 except PermissionError:
|
||||||
|
27 - logger.warning(f"File watcher: Cannot create {self.analysis_dir}, using temp directory")
|
||||||
|
27 + logger.warning(f"File watcher: Cannot create {self.analysis_dir}, using shared temp directory")
|
||||||
|
28 + # Use temp directory within the shared volume so celery can access it
|
||||||
|
29 import tempfile
|
||||||
|
30 - self.analysis_dir = Path(tempfile.mkdtemp(prefix='analysis_segments_'))
|
||||||
|
30 + self.analysis_dir = Path(tempfile.mkdtemp(dir=self.media_dir, prefix='analysis_'))
|
||||||
|
31 logger.info(f"File watcher: Using temporary analysis directory: {self.analysis_dir}")
|
||||||
|
32
|
||||||
|
33 # Track copied files for cleanup
|
||||||
|
|
||||||
|
> how this archicheture desing would scale when moving to k8s or hybrid deployments
|
||||||
|
|
||||||
|
● Excellent question! The current architecture has scaling challenges that need addressing for K8s/hybrid deployments:
|
||||||
|
|
||||||
|
Current Architecture Limitations
|
||||||
|
|
||||||
|
Single Point of Failure:
|
||||||
|
File-watcher (1 instance) → Temp Directory → Multiple Celery Workers
|
||||||
|
- Only one file-watcher instance can run
|
||||||
|
- Temp directory is container-local (doesn't work across pods)
|
||||||
|
- No coordination between file-watchers if scaled
|
||||||
|
|
||||||
|
K8s-Ready Architecture Design
|
||||||
|
|
||||||
|
Option 1: Event-Driven with Message Queues
|
||||||
|
|
||||||
|
RTMP/HLS → File Events → Redis Streams → Celery Tasks → Analysis Results
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
- File events go directly to Redis/RabbitMQ instead of filesystem watching
|
||||||
|
- Multiple file-watcher pods can process different streams
|
||||||
|
- Celery workers scale independently across nodes
|
||||||
|
- No shared filesystem dependency
|
||||||
|
|
||||||
|
Option 2: Object Storage + Event Notifications
|
||||||
|
|
||||||
|
RTMP → Cloud Storage → Storage Events → Lambda/K8s Jobs → Analysis
|
||||||
|
|
||||||
|
GCP Implementation:
|
||||||
|
# segments uploaded to GCS trigger Cloud Functions
|
||||||
|
# Cloud Functions push tasks to Cloud Tasks/Pub/Sub
|
||||||
|
# Celery workers pull from Pub/Sub instead of file watching
|
||||||
|
|
||||||
|
Option 3: StatefulSet + Persistent Volumes
|
||||||
|
|
||||||
|
File-watcher StatefulSet → PVC → Celery Deployment
|
||||||
|
|
||||||
|
Current Architecture Enhanced:
|
||||||
|
- File-watcher as StatefulSet with persistent volume
|
||||||
|
- Analysis segments on shared PVC (ReadWriteMany)
|
||||||
|
- Multiple celery pods access same PVC
|
||||||
|
|
||||||
|
Hybrid Deployment Patterns
|
||||||
|
|
||||||
|
Local Development
|
||||||
|
|
||||||
|
docker compose up # Current setup works fine
|
||||||
|
|
||||||
|
Single VM K8s (k3s/kind)
|
||||||
|
|
||||||
|
# Use hostPath volumes
|
||||||
|
volumes:
|
||||||
|
- name: media-storage
|
||||||
|
hostPath:
|
||||||
|
path: /opt/media-analyzer/segments
|
||||||
|
|
||||||
|
Cloud K8s (GKE/EKS)
|
||||||
|
|
||||||
|
# Use cloud storage + events
|
||||||
|
env:
|
||||||
|
- name: USE_CLOUD_STORAGE
|
||||||
|
value: "true"
|
||||||
|
- name: GCS_BUCKET
|
||||||
|
value: "media-analyzer-segments"
|
||||||
|
|
||||||
|
Recommended Migration Path
|
||||||
|
|
||||||
|
Phase 1: Make Current Architecture K8s-Compatible
|
||||||
|
|
||||||
|
# Add environment-aware file watching
|
||||||
|
if os.getenv('KUBERNETES_SERVICE_HOST'):
|
||||||
|
# Use shared PVC path
|
||||||
|
self.analysis_dir = Path('/shared-storage/analysis_segments')
|
||||||
|
else:
|
||||||
|
# Use local docker volume
|
||||||
|
self.analysis_dir = self.media_dir / 'analysis_segments'
|
||||||
|
|
||||||
|
Phase 2: Event-Driven Refactor
|
||||||
|
|
||||||
|
# Replace file system events with message events
|
||||||
|
class VideoSegmentProcessor:
|
||||||
|
def handle_new_segment(self, segment_event):
|
||||||
|
# segment_event contains: bucket, path, stream_key
|
||||||
|
# No file copying needed - process from object storage
|
||||||
|
|
||||||
|
Phase 3: Full Cloud-Native
|
||||||
|
|
||||||
|
# Kubernetes CronJob for cleanup instead of in-process cleanup
|
||||||
|
# Horizontal Pod Autoscaler based on queue depth
|
||||||
|
# Cloud storage for persistence
|
||||||
|
# Cloud events for coordination
|
||||||
|
|
||||||
|
Performance & Scaling Considerations
|
||||||
|
|
||||||
|
Current Bottlenecks:
|
||||||
|
- Single file-watcher instance
|
||||||
|
- File copying overhead
|
||||||
|
- Shared filesystem dependency
|
||||||
|
|
||||||
|
K8s Solutions:
|
||||||
|
- HPA: Scale celery workers based on Redis queue length
|
||||||
|
- Multiple file-watchers: One per stream or region
|
||||||
|
- Node affinity: Co-locate file-watcher with RTMP pods
|
||||||
|
- Resource quotas: Prevent memory leaks from affecting cluster
|
||||||
|
|
||||||
|
Configuration Strategy
|
||||||
|
|
||||||
|
# Environment-aware configuration
|
||||||
|
class ScalingConfig:
|
||||||
|
def __init__(self):
|
||||||
|
self.deployment_type = self.detect_environment()
|
||||||
|
|
||||||
|
def detect_environment(self):
|
||||||
|
if os.getenv('KUBERNETES_SERVICE_HOST'):
|
||||||
|
if os.getenv('GCP_PROJECT_ID'):
|
||||||
|
return 'gke'
|
||||||
|
return 'k8s'
|
||||||
|
return 'docker'
|
||||||
|
|
||||||
|
def get_storage_backend(self):
|
||||||
|
return {
|
||||||
|
'docker': LocalFileSystem(),
|
||||||
|
'k8s': SharedVolume(),
|
||||||
|
'gke': CloudStorage()
|
||||||
|
}[self.deployment_type]
|
||||||
|
|
||||||
|
The current architecture works well for Docker Compose but needs the event-driven refactor for true K8s scalability. The hybrid approach lets you migrate incrementally while maintaining backward
|
||||||
|
compatibility.
|
||||||
46
def/rewrite_commit_history_strategy
Normal file
46
def/rewrite_commit_history_strategy
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
Here’s the **concise, command-first** workflow we agreed on (using the **rename/no-force-push** path):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) Find base commit (where feature diverged from main)
|
||||||
|
BASE=$(git merge-base main feature)
|
||||||
|
|
||||||
|
# 2) Create clean branch from base
|
||||||
|
git checkout -b feature-clean "$BASE"
|
||||||
|
|
||||||
|
# 3) Cherry-pick/group commits (example: group A+B, keep C; group D+E+F, keep G)
|
||||||
|
git cherry-pick -n A
|
||||||
|
git cherry-pick -n B
|
||||||
|
git commit -m "Feat: X — initial implementation (A+B)"
|
||||||
|
git cherry-pick C
|
||||||
|
git cherry-pick -n D
|
||||||
|
git cherry-pick -n E
|
||||||
|
git cherry-pick -n F
|
||||||
|
git commit -m "Feat: X — refinements (D+E+F)"
|
||||||
|
git cherry-pick G
|
||||||
|
# (Or: pick everything then `git rebase -i "$BASE"` to squash/fixup selected ones.)
|
||||||
|
|
||||||
|
# 4) Verify equivalence
|
||||||
|
git range-diff feature...feature-clean
|
||||||
|
# (Optional sanity: git diff feature..feature-clean)
|
||||||
|
|
||||||
|
# 5) Publish clean branch & preserve old main
|
||||||
|
git push -u origin feature-clean
|
||||||
|
git push origin main:main-old-2025-08-24
|
||||||
|
```
|
||||||
|
|
||||||
|
**Then (platform UI):**
|
||||||
|
|
||||||
|
1. Set **default branch** to `feature-clean`.
|
||||||
|
2. Rename `feature-clean` → `main`.
|
||||||
|
3. (If not done in step 5) rename old `main` → `main-old-2025-08-24`.
|
||||||
|
4. Reapply branch protections/CI rules on the new `main`.
|
||||||
|
|
||||||
|
**Teammates (already cloned) quick sync:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch --all --prune
|
||||||
|
git switch main || git checkout -b main origin/main
|
||||||
|
git reset --hard origin/main
|
||||||
|
# For feature branches based on old main:
|
||||||
|
git rebase --rebase-merges --onto origin/main origin/main-old-2025-08-24 <my-branch>
|
||||||
|
```
|
||||||
159
media-analyzer/backend/ai_processing/event_tasks.py
Normal file
159
media-analyzer/backend/ai_processing/event_tasks.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from celery import shared_task
|
||||||
|
from streaming.segment_events import SegmentEventConsumer
|
||||||
|
from .analysis_engine import AnalysisEngine
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@shared_task(bind=True, max_retries=3)
|
||||||
|
def process_segment_from_event(self):
|
||||||
|
"""
|
||||||
|
Celery task that consumes segment events from Redis and processes them.
|
||||||
|
This replaces the file-watcher copying approach with an event-driven model.
|
||||||
|
"""
|
||||||
|
consumer = SegmentEventConsumer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Consume next segment event (non-blocking with short timeout)
|
||||||
|
event = consumer.consume_segment_event(timeout=1)
|
||||||
|
|
||||||
|
if not event:
|
||||||
|
# No events available, task completes normally
|
||||||
|
return {'status': 'no_events', 'processed': 0}
|
||||||
|
|
||||||
|
segment_path = event['segment_path']
|
||||||
|
stream_key = event['stream_key']
|
||||||
|
session_id = event.get('session_id')
|
||||||
|
|
||||||
|
logger.info(f"Processing segment event: {segment_path} (stream: {stream_key})")
|
||||||
|
|
||||||
|
# Check if segment file still exists (nginx might have rotated it)
|
||||||
|
if not Path(segment_path).exists():
|
||||||
|
logger.warning(f"Segment file no longer exists: {segment_path} - skipping")
|
||||||
|
return {'status': 'file_missing', 'segment_path': segment_path}
|
||||||
|
|
||||||
|
# Initialize analysis engine and configure for logo detection
|
||||||
|
analysis_engine = AnalysisEngine()
|
||||||
|
|
||||||
|
# Configure logo detection provider (using existing config)
|
||||||
|
from .config_manager import config_manager
|
||||||
|
if not config_manager.has_capability('logo_detection'):
|
||||||
|
logger.error("No logo detection provider configured")
|
||||||
|
return {'status': 'error', 'error': 'No logo detection provider configured'}
|
||||||
|
|
||||||
|
logo_config = config_manager.get_provider_config('logo_detection')
|
||||||
|
analysis_engine.configure_providers({'logo_detection': logo_config})
|
||||||
|
|
||||||
|
# Extract frame from segment
|
||||||
|
frame = analysis_engine.extract_frame_from_segment(segment_path)
|
||||||
|
if not frame:
|
||||||
|
logger.error(f"Failed to extract frame from {segment_path}")
|
||||||
|
return {'status': 'error', 'error': 'Failed to extract frame from segment'}
|
||||||
|
|
||||||
|
# Analyze frame for logo detection
|
||||||
|
results = analysis_engine.analyze_frame(
|
||||||
|
image=frame,
|
||||||
|
requested_analysis=['logo_detection'],
|
||||||
|
confidence_threshold=0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
logo_detections = results.get('logos', [])
|
||||||
|
logger.info(f"Completed analysis for {segment_path}: {len(logo_detections)} logo detections")
|
||||||
|
|
||||||
|
# Store results in database
|
||||||
|
from .models import VideoAnalysis, DetectionResult
|
||||||
|
from channels.layers import get_channel_layer
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
|
||||||
|
analysis = VideoAnalysis.objects.create(
|
||||||
|
stream_key=stream_key,
|
||||||
|
session_id=session_id,
|
||||||
|
segment_path=segment_path,
|
||||||
|
processing_time=1.5, # Approximate processing time
|
||||||
|
analysis_type='logo_detection',
|
||||||
|
frame_timestamp=0.0 # First frame of segment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create detection records and prepare for WebSocket
|
||||||
|
detections = []
|
||||||
|
for logo in logo_detections:
|
||||||
|
detection = DetectionResult.objects.create(
|
||||||
|
analysis=analysis,
|
||||||
|
label=logo['label'],
|
||||||
|
confidence=logo['confidence'],
|
||||||
|
bbox_x=logo['bbox']['x'],
|
||||||
|
bbox_y=logo['bbox']['y'],
|
||||||
|
bbox_width=logo['bbox']['width'],
|
||||||
|
bbox_height=logo['bbox']['height'],
|
||||||
|
detection_type='logo'
|
||||||
|
)
|
||||||
|
detections.append(detection.to_dict())
|
||||||
|
|
||||||
|
# Send results via WebSocket (always send, even with 0 detections)
|
||||||
|
channel_layer = get_channel_layer()
|
||||||
|
websocket_group = f"stream_{stream_key}"
|
||||||
|
logger.info(f"Sending websocket update to group: {websocket_group} - detections: {len(detections)}")
|
||||||
|
async_to_sync(channel_layer.group_send)(
|
||||||
|
websocket_group,
|
||||||
|
{
|
||||||
|
"type": "analysis_update",
|
||||||
|
"analysis": analysis.to_dict()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log successful detection
|
||||||
|
if logo_detections:
|
||||||
|
logger.info(f"Logo detections found: {[d.get('label', 'Unknown') for d in logo_detections]}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'segment_path': segment_path,
|
||||||
|
'stream_key': stream_key,
|
||||||
|
'detections': len(logo_detections),
|
||||||
|
'analysis_id': str(analysis.id),
|
||||||
|
'brands': [d['label'] for d in detections] if detections else []
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing segment event: {e}")
|
||||||
|
|
||||||
|
# Retry with exponential backoff
|
||||||
|
if self.request.retries < self.max_retries:
|
||||||
|
countdown = 2 ** self.request.retries
|
||||||
|
logger.info(f"Retrying in {countdown} seconds (attempt {self.request.retries + 1})")
|
||||||
|
raise self.retry(countdown=countdown)
|
||||||
|
|
||||||
|
return {'status': 'error', 'error': str(e)}
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def start_event_processor():
|
||||||
|
"""
|
||||||
|
Background task that continuously processes segment events.
|
||||||
|
This replaces the file-watcher process.
|
||||||
|
"""
|
||||||
|
consumer = SegmentEventConsumer()
|
||||||
|
processed_count = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Process events in batches
|
||||||
|
while processed_count < 50: # Process up to 50 events per task
|
||||||
|
event = consumer.consume_segment_event(timeout=2)
|
||||||
|
|
||||||
|
if not event:
|
||||||
|
break # No more events
|
||||||
|
|
||||||
|
# Trigger individual processing task
|
||||||
|
process_segment_from_event.delay()
|
||||||
|
processed_count += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'completed',
|
||||||
|
'processed_count': processed_count,
|
||||||
|
'queue_length': consumer.redis_client.llen(consumer.event_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in event processor: {e}")
|
||||||
|
return {'status': 'error', 'error': str(e), 'processed_count': processed_count}
|
||||||
@@ -6,115 +6,13 @@ from .analysis_engine import AnalysisEngine
|
|||||||
from .models import VideoAnalysis, DetectionResult, VisualAnalysis, ProcessingQueue, AnalysisProvider
|
from .models import VideoAnalysis, DetectionResult, VisualAnalysis, ProcessingQueue, AnalysisProvider
|
||||||
from .config_manager import config_manager
|
from .config_manager import config_manager
|
||||||
|
|
||||||
|
# Import event_tasks to ensure Celery autodiscovery finds them
|
||||||
|
from . import event_tasks
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
channel_layer = get_channel_layer()
|
channel_layer = get_channel_layer()
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True, queue='logo_detection')
|
|
||||||
def analyze_logo_detection(self, stream_key, segment_path, session_id=None):
|
|
||||||
"""Dedicated task for logo detection analysis"""
|
|
||||||
queue_item = None
|
|
||||||
try:
|
|
||||||
# Update queue status
|
|
||||||
queue_item = ProcessingQueue.objects.filter(
|
|
||||||
stream_key=stream_key,
|
|
||||||
segment_path=segment_path,
|
|
||||||
status='pending'
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if queue_item:
|
|
||||||
queue_item.status = 'processing'
|
|
||||||
queue_item.save()
|
|
||||||
|
|
||||||
# Check if logo detection is configured
|
|
||||||
if not config_manager.has_capability('logo_detection'):
|
|
||||||
logger.error("No logo detection provider configured")
|
|
||||||
if queue_item:
|
|
||||||
queue_item.status = 'failed'
|
|
||||||
queue_item.error_message = 'No logo detection provider configured'
|
|
||||||
queue_item.save()
|
|
||||||
return {"error": "No logo detection provider configured"}
|
|
||||||
|
|
||||||
# Initialize analysis engine with cached config
|
|
||||||
engine = AnalysisEngine()
|
|
||||||
logo_config = config_manager.get_provider_config('logo_detection')
|
|
||||||
engine.configure_providers({'logo_detection': logo_config})
|
|
||||||
|
|
||||||
# Extract and analyze frame
|
|
||||||
frame = engine.extract_frame_from_segment(segment_path)
|
|
||||||
if not frame:
|
|
||||||
logger.error(f"Failed to extract frame from {segment_path}")
|
|
||||||
if queue_item:
|
|
||||||
queue_item.status = 'failed'
|
|
||||||
queue_item.error_message = 'Failed to extract frame from segment'
|
|
||||||
queue_item.save()
|
|
||||||
return {"error": "Failed to extract frame"}
|
|
||||||
|
|
||||||
# Analyze for logos only - use configured threshold
|
|
||||||
from django.conf import settings
|
|
||||||
confidence = settings.LOGO_DETECTION_CONFIG['confidence_threshold']
|
|
||||||
analysis_results = engine.analyze_frame(frame, ['logo_detection'], confidence_threshold=confidence)
|
|
||||||
|
|
||||||
# Store results
|
|
||||||
provider_info = config_manager.get_provider_by_type(logo_config['provider_type'])
|
|
||||||
provider = AnalysisProvider.objects.get(id=provider_info['id'])
|
|
||||||
|
|
||||||
analysis = VideoAnalysis.objects.create(
|
|
||||||
stream_key=stream_key,
|
|
||||||
session_id=session_id,
|
|
||||||
segment_path=segment_path,
|
|
||||||
provider=provider,
|
|
||||||
analysis_type='logo_detection',
|
|
||||||
frame_timestamp=0.0,
|
|
||||||
confidence_threshold=confidence
|
|
||||||
)
|
|
||||||
|
|
||||||
detections = []
|
|
||||||
if 'logos' in analysis_results:
|
|
||||||
for logo in analysis_results['logos']:
|
|
||||||
detection = DetectionResult.objects.create(
|
|
||||||
analysis=analysis,
|
|
||||||
label=logo['label'],
|
|
||||||
confidence=logo['confidence'],
|
|
||||||
bbox_x=logo['bbox']['x'],
|
|
||||||
bbox_y=logo['bbox']['y'],
|
|
||||||
bbox_width=logo['bbox']['width'],
|
|
||||||
bbox_height=logo['bbox']['height'],
|
|
||||||
detection_type='logo'
|
|
||||||
)
|
|
||||||
detections.append(detection.to_dict())
|
|
||||||
|
|
||||||
# Send results via WebSocket (always send, even with 0 detections)
|
|
||||||
websocket_group = f"stream_{stream_key}"
|
|
||||||
logger.info(f"Sending websocket update to group: {websocket_group} - detections: {len(detections)}")
|
|
||||||
async_to_sync(channel_layer.group_send)(
|
|
||||||
websocket_group,
|
|
||||||
{
|
|
||||||
"type": "analysis_update",
|
|
||||||
"analysis": analysis.to_dict()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update queue status
|
|
||||||
if queue_item:
|
|
||||||
queue_item.status = 'completed'
|
|
||||||
queue_item.save()
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"detections": len(detections),
|
|
||||||
"analysis_id": str(analysis.id),
|
|
||||||
"brands": [d['label'] for d in detections] if detections else []
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Logo detection failed for {segment_path}: {e}")
|
|
||||||
if queue_item:
|
|
||||||
queue_item.status = 'failed'
|
|
||||||
queue_item.error_message = str(e)
|
|
||||||
queue_item.save()
|
|
||||||
raise self.retry(countdown=60, max_retries=3)
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True, queue='visual_analysis')
|
@shared_task(bind=True, queue='visual_analysis')
|
||||||
def analyze_visual_properties(self, stream_key, segment_path, session_id=None):
|
def analyze_visual_properties(self, stream_key, segment_path, session_id=None):
|
||||||
@@ -202,8 +100,8 @@ def process_video_segment(self, stream_key, segment_path, session_id=None):
|
|||||||
# Dispatch to specialized queues based on available capabilities
|
# Dispatch to specialized queues based on available capabilities
|
||||||
active_capabilities = config_manager.get_active_capabilities()
|
active_capabilities = config_manager.get_active_capabilities()
|
||||||
|
|
||||||
if 'logo_detection' in active_capabilities:
|
# Logo detection now handled by event-driven system in event_tasks.py
|
||||||
analyze_logo_detection.delay(stream_key, segment_path, session_id)
|
# Events are published by file-watcher and consumed by process_segment_from_event
|
||||||
|
|
||||||
# Visual analysis disabled for performance - only logo detection
|
# Visual analysis disabled for performance - only logo detection
|
||||||
# analyze_visual_properties.delay(stream_key, segment_path)
|
# analyze_visual_properties.delay(stream_key, segment_path)
|
||||||
|
|||||||
@@ -101,9 +101,10 @@ CELERY_TIMEZONE = 'UTC'
|
|||||||
|
|
||||||
# Celery queue routing
|
# Celery queue routing
|
||||||
CELERY_TASK_ROUTES = {
|
CELERY_TASK_ROUTES = {
|
||||||
'ai_processing.tasks.analyze_logo_detection': {'queue': 'logo_detection'},
|
|
||||||
'ai_processing.tasks.analyze_visual_properties': {'queue': 'visual_analysis'},
|
'ai_processing.tasks.analyze_visual_properties': {'queue': 'visual_analysis'},
|
||||||
'ai_processing.tasks.reload_analysis_config': {'queue': 'config_management'},
|
'ai_processing.tasks.reload_analysis_config': {'queue': 'config_management'},
|
||||||
|
'ai_processing.event_tasks.process_segment_from_event': {'queue': 'logo_detection'},
|
||||||
|
'ai_processing.event_tasks.start_event_processor': {'queue': 'default'},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Define queues with different priorities
|
# Define queues with different priorities
|
||||||
@@ -249,6 +250,11 @@ LOGO_DETECTION_CONFIG = {
|
|||||||
'use_cloud_vision': USE_CLOUD_VISION,
|
'use_cloud_vision': USE_CLOUD_VISION,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Event Source Configuration
|
||||||
|
SEGMENT_EVENT_SOURCE = os.getenv('SEGMENT_EVENT_SOURCE', 'filewatcher').lower()
|
||||||
|
FILE_WATCHER_POLL_INTERVAL = float(os.getenv('FILE_WATCHER_POLL_INTERVAL', '1.0'))
|
||||||
|
WEBHOOK_PORT = int(os.getenv('WEBHOOK_PORT', '8001'))
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Kubernetes and Container Configuration
|
# Kubernetes and Container Configuration
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -275,6 +281,53 @@ if os.getenv('KUBERNETES_SERVICE_HOST'):
|
|||||||
f"https://{os.getenv('INGRESS_HOST', 'localhost')}",
|
f"https://{os.getenv('INGRESS_HOST', 'localhost')}",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Logging Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'verbose': {
|
||||||
|
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'verbose',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'streaming.file_watcher': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO', # Changed from DEBUG
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
'streaming.event_sources': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
'streaming.event_source_manager': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
'ai_processing': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'root': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Production Security Settings
|
# Production Security Settings
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
225
media-analyzer/backend/streaming/event_source_manager.py
Normal file
225
media-analyzer/backend/streaming/event_source_manager.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
"""
|
||||||
|
Event source manager for dynamic selection and management of segment event sources.
|
||||||
|
Handles environment-based switching between file watchers, cloud storage, etc.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any, Type
|
||||||
|
from django.conf import settings
|
||||||
|
from streaming.event_sources import (
|
||||||
|
SegmentEventSource,
|
||||||
|
FileWatcherEventSource,
|
||||||
|
CloudStorageEventSource,
|
||||||
|
WebhookEventSource
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EventSourceManager:
|
||||||
|
"""
|
||||||
|
Manages event sources based on environment configuration.
|
||||||
|
Provides a single interface for starting/stopping segment monitoring.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Available event source implementations
|
||||||
|
EVENT_SOURCE_CLASSES = {
|
||||||
|
'filewatcher': FileWatcherEventSource,
|
||||||
|
'filesystem': FileWatcherEventSource, # Alias
|
||||||
|
'cloud': CloudStorageEventSource,
|
||||||
|
'gcs': CloudStorageEventSource, # Alias
|
||||||
|
'gcp': CloudStorageEventSource, # Alias
|
||||||
|
'webhook': WebhookEventSource,
|
||||||
|
'http': WebhookEventSource, # Alias
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, source_type: Optional[str] = None):
|
||||||
|
self.source_type = source_type or self._get_configured_source_type()
|
||||||
|
self.current_source: Optional[SegmentEventSource] = None
|
||||||
|
self._initialize_source()
|
||||||
|
|
||||||
|
def _get_configured_source_type(self) -> str:
|
||||||
|
"""Get event source type from environment configuration"""
|
||||||
|
# Check environment variable first
|
||||||
|
env_source = os.getenv('SEGMENT_EVENT_SOURCE', '').lower()
|
||||||
|
if env_source in self.EVENT_SOURCE_CLASSES:
|
||||||
|
return env_source
|
||||||
|
|
||||||
|
# Check Django settings
|
||||||
|
settings_source = getattr(settings, 'SEGMENT_EVENT_SOURCE', '').lower()
|
||||||
|
if settings_source in self.EVENT_SOURCE_CLASSES:
|
||||||
|
return settings_source
|
||||||
|
|
||||||
|
# Default to file watcher for local development
|
||||||
|
return 'filewatcher'
|
||||||
|
|
||||||
|
def _initialize_source(self) -> None:
|
||||||
|
"""Initialize the configured event source"""
|
||||||
|
try:
|
||||||
|
source_class = self.EVENT_SOURCE_CLASSES.get(self.source_type)
|
||||||
|
if not source_class:
|
||||||
|
available = ', '.join(self.EVENT_SOURCE_CLASSES.keys())
|
||||||
|
raise ValueError(f"Unknown event source type: {self.source_type}. Available: {available}")
|
||||||
|
|
||||||
|
# Initialize with appropriate parameters based on source type
|
||||||
|
if self.source_type in ['filewatcher', 'filesystem']:
|
||||||
|
media_dir = getattr(settings, 'MEDIA_ROOT', None)
|
||||||
|
poll_interval = float(os.getenv('FILE_WATCHER_POLL_INTERVAL', 1.0))
|
||||||
|
self.current_source = source_class(media_dir=media_dir, poll_interval=poll_interval)
|
||||||
|
|
||||||
|
elif self.source_type in ['cloud', 'gcs', 'gcp']:
|
||||||
|
bucket_name = os.getenv('GCS_BUCKET_NAME', 'media-segments')
|
||||||
|
self.current_source = source_class(bucket_name=bucket_name)
|
||||||
|
|
||||||
|
elif self.source_type in ['webhook', 'http']:
|
||||||
|
webhook_port = int(os.getenv('WEBHOOK_PORT', 8001))
|
||||||
|
self.current_source = source_class(webhook_port=webhook_port)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Fallback - initialize with no parameters
|
||||||
|
self.current_source = source_class()
|
||||||
|
|
||||||
|
logger.info(f"EventSourceManager: Initialized {self.source_type} event source")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"EventSourceManager: Failed to initialize {self.source_type} source: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def start_monitoring(self) -> bool:
|
||||||
|
"""Start segment monitoring with the configured event source"""
|
||||||
|
try:
|
||||||
|
if not self.current_source:
|
||||||
|
logger.error("EventSourceManager: No event source initialized")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.current_source.start_monitoring()
|
||||||
|
logger.info(f"EventSourceManager: Started monitoring with {self.source_type} source")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"EventSourceManager: Failed to start monitoring: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_monitoring(self) -> bool:
|
||||||
|
"""Stop segment monitoring"""
|
||||||
|
try:
|
||||||
|
if not self.current_source:
|
||||||
|
logger.warning("EventSourceManager: No event source to stop")
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.current_source.stop_monitoring()
|
||||||
|
logger.info(f"EventSourceManager: Stopped monitoring with {self.source_type} source")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"EventSourceManager: Failed to stop monitoring: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get current event source status and information"""
|
||||||
|
if not self.current_source:
|
||||||
|
return {
|
||||||
|
'configured_type': self.source_type,
|
||||||
|
'initialized': False,
|
||||||
|
'error': 'Event source not initialized'
|
||||||
|
}
|
||||||
|
|
||||||
|
source_info = self.current_source.get_source_info()
|
||||||
|
return {
|
||||||
|
'configured_type': self.source_type,
|
||||||
|
'initialized': True,
|
||||||
|
'source_info': source_info,
|
||||||
|
'available_types': list(self.EVENT_SOURCE_CLASSES.keys())
|
||||||
|
}
|
||||||
|
|
||||||
|
def switch_source(self, new_source_type: str) -> bool:
|
||||||
|
"""Switch to a different event source type"""
|
||||||
|
try:
|
||||||
|
if new_source_type not in self.EVENT_SOURCE_CLASSES:
|
||||||
|
available = ', '.join(self.EVENT_SOURCE_CLASSES.keys())
|
||||||
|
logger.error(f"EventSourceManager: Invalid source type {new_source_type}. Available: {available}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Stop current source
|
||||||
|
was_monitoring = False
|
||||||
|
if self.current_source:
|
||||||
|
try:
|
||||||
|
current_info = self.current_source.get_source_info()
|
||||||
|
was_monitoring = current_info.get('status') == 'active'
|
||||||
|
self.stop_monitoring()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"EventSourceManager: Error stopping current source: {e}")
|
||||||
|
|
||||||
|
# Switch to new source
|
||||||
|
old_source_type = self.source_type
|
||||||
|
self.source_type = new_source_type
|
||||||
|
self._initialize_source()
|
||||||
|
|
||||||
|
# Resume monitoring if it was active
|
||||||
|
if was_monitoring:
|
||||||
|
self.start_monitoring()
|
||||||
|
|
||||||
|
logger.info(f"EventSourceManager: Switched from {old_source_type} to {new_source_type}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"EventSourceManager: Failed to switch to {new_source_type}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def emit_manual_event(self, segment_path: str, stream_key: str,
|
||||||
|
session_id: Optional[str] = None) -> bool:
|
||||||
|
"""Manually emit a segment event (for testing/debugging)"""
|
||||||
|
try:
|
||||||
|
if not self.current_source:
|
||||||
|
logger.error("EventSourceManager: No event source available for manual event")
|
||||||
|
return False
|
||||||
|
|
||||||
|
success = self.current_source.emit_segment_event(
|
||||||
|
segment_path=segment_path,
|
||||||
|
stream_key=stream_key,
|
||||||
|
session_id=session_id,
|
||||||
|
metadata={'manual': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"EventSourceManager: Manual event emitted for {segment_path}")
|
||||||
|
else:
|
||||||
|
logger.error(f"EventSourceManager: Failed to emit manual event for {segment_path}")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"EventSourceManager: Error emitting manual event: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Global event source manager instance
|
||||||
|
_global_event_source_manager: Optional[EventSourceManager] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_event_source_manager() -> EventSourceManager:
|
||||||
|
"""Get or create the global event source manager instance"""
|
||||||
|
global _global_event_source_manager
|
||||||
|
|
||||||
|
if _global_event_source_manager is None:
|
||||||
|
_global_event_source_manager = EventSourceManager()
|
||||||
|
|
||||||
|
return _global_event_source_manager
|
||||||
|
|
||||||
|
|
||||||
|
def start_segment_monitoring() -> bool:
|
||||||
|
"""Convenience function to start segment monitoring"""
|
||||||
|
manager = get_event_source_manager()
|
||||||
|
return manager.start_monitoring()
|
||||||
|
|
||||||
|
|
||||||
|
def stop_segment_monitoring() -> bool:
|
||||||
|
"""Convenience function to stop segment monitoring"""
|
||||||
|
manager = get_event_source_manager()
|
||||||
|
return manager.stop_monitoring()
|
||||||
|
|
||||||
|
|
||||||
|
def get_monitoring_status() -> Dict[str, Any]:
|
||||||
|
"""Convenience function to get monitoring status"""
|
||||||
|
manager = get_event_source_manager()
|
||||||
|
return manager.get_status()
|
||||||
310
media-analyzer/backend/streaming/event_sources.py
Normal file
310
media-analyzer/backend/streaming/event_sources.py
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
"""
|
||||||
|
Event source abstraction for segment monitoring.
|
||||||
|
Supports file system watchers, cloud storage events, and other sources.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SegmentEventSource(ABC):
|
||||||
|
"""Abstract base class for segment event sources"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.publisher = None
|
||||||
|
self._setup_publisher()
|
||||||
|
|
||||||
|
def _setup_publisher(self):
|
||||||
|
"""Initialize the event publisher"""
|
||||||
|
try:
|
||||||
|
from streaming.segment_events import SegmentEventPublisher
|
||||||
|
self.publisher = SegmentEventPublisher()
|
||||||
|
logger.info(f"Initialized {self.__class__.__name__} event source")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to setup event publisher: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def start_monitoring(self) -> None:
|
||||||
|
"""Start monitoring for new segments - implementation specific"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def stop_monitoring(self) -> None:
|
||||||
|
"""Stop monitoring - implementation specific"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def emit_segment_event(self, segment_path: str, stream_key: str,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
|
metadata: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Common event emission logic for all sources.
|
||||||
|
This ensures consistent event format regardless of source.
|
||||||
|
"""
|
||||||
|
if not self.publisher:
|
||||||
|
logger.error("Event publisher not initialized")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Add source metadata
|
||||||
|
if metadata is None:
|
||||||
|
metadata = {}
|
||||||
|
metadata['source'] = self.__class__.__name__
|
||||||
|
|
||||||
|
success = self.publisher.publish_segment_event(
|
||||||
|
segment_path=segment_path,
|
||||||
|
stream_key=stream_key,
|
||||||
|
session_id=session_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.debug(f"Event emitted by {self.__class__.__name__}: {segment_path}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to emit event from {self.__class__.__name__}: {segment_path}")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error emitting event from {self.__class__.__name__}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_source_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return information about this event source"""
|
||||||
|
return {
|
||||||
|
'name': self.__class__.__name__,
|
||||||
|
'type': 'unknown',
|
||||||
|
'status': 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FileWatcherEventSource(SegmentEventSource):
|
||||||
|
"""File system watcher event source for local development"""
|
||||||
|
|
||||||
|
def __init__(self, media_dir: Optional[str] = None, poll_interval: float = 1.0):
|
||||||
|
super().__init__()
|
||||||
|
self.media_dir = Path(media_dir or settings.MEDIA_ROOT)
|
||||||
|
self.poll_interval = poll_interval
|
||||||
|
self.processed_files = set()
|
||||||
|
self._monitoring = False
|
||||||
|
self._monitor_thread = None
|
||||||
|
self._last_cleanup = time.time()
|
||||||
|
self._cleanup_interval = 300 # Clean processed_files every 5 minutes
|
||||||
|
|
||||||
|
def get_stream_key_from_active_stream(self) -> Optional[tuple]:
|
||||||
|
"""Get active stream info from database with connection management"""
|
||||||
|
try:
|
||||||
|
from django.db import connection
|
||||||
|
from streaming.models import VideoStream, StreamStatus
|
||||||
|
|
||||||
|
# Ensure fresh connection
|
||||||
|
connection.ensure_connection()
|
||||||
|
|
||||||
|
active_stream = VideoStream.objects.filter(status=StreamStatus.ACTIVE).first()
|
||||||
|
if active_stream:
|
||||||
|
return active_stream.stream_key, getattr(active_stream, 'session_id', None)
|
||||||
|
return None, None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"FileWatcher: Error getting active stream: {e}")
|
||||||
|
# Close potentially broken connection
|
||||||
|
from django.db import connection
|
||||||
|
connection.close()
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def process_new_segment(self, file_path: Path) -> None:
|
||||||
|
"""Process a new HLS segment file by emitting event"""
|
||||||
|
try:
|
||||||
|
stream_key, session_id = self.get_stream_key_from_active_stream()
|
||||||
|
if not stream_key:
|
||||||
|
logger.warning(f"FileWatcher: No active stream found, skipping {file_path.name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug(f"FileWatcher: Processing new segment {file_path.name} (stream: {stream_key})")
|
||||||
|
|
||||||
|
success = self.emit_segment_event(
|
||||||
|
segment_path=str(file_path),
|
||||||
|
stream_key=stream_key,
|
||||||
|
session_id=session_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.debug(f"FileWatcher: Emitted event for {file_path.name}")
|
||||||
|
else:
|
||||||
|
logger.error(f"FileWatcher: Failed to emit event for {file_path.name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"FileWatcher: Error processing {file_path}: {e}")
|
||||||
|
|
||||||
|
def _cleanup_processed_files(self) -> None:
|
||||||
|
"""Clean up processed_files set to prevent memory leak"""
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self._last_cleanup < self._cleanup_interval:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Only keep files that still exist on disk
|
||||||
|
existing_files = set()
|
||||||
|
if self.media_dir.exists():
|
||||||
|
for ts_file in self.media_dir.glob("*.ts"):
|
||||||
|
if ts_file.is_file():
|
||||||
|
existing_files.add(ts_file)
|
||||||
|
|
||||||
|
# Remove deleted files from processed set
|
||||||
|
old_count = len(self.processed_files)
|
||||||
|
self.processed_files &= existing_files
|
||||||
|
new_count = len(self.processed_files)
|
||||||
|
|
||||||
|
if old_count != new_count:
|
||||||
|
logger.debug(f"FileWatcher: Cleaned up {old_count - new_count} processed file entries")
|
||||||
|
|
||||||
|
self._last_cleanup = current_time
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"FileWatcher: Error cleaning up processed files: {e}")
|
||||||
|
|
||||||
|
def scan_for_new_files(self) -> None:
|
||||||
|
"""Scan for new .ts files in the media directory"""
|
||||||
|
try:
|
||||||
|
if not self.media_dir.exists():
|
||||||
|
logger.debug(f"FileWatcher: Media directory {self.media_dir} does not exist")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_files = set()
|
||||||
|
for ts_file in self.media_dir.glob("*.ts"):
|
||||||
|
if ts_file.is_file():
|
||||||
|
current_files.add(ts_file)
|
||||||
|
|
||||||
|
# Find new files
|
||||||
|
new_files = current_files - self.processed_files
|
||||||
|
|
||||||
|
if new_files:
|
||||||
|
logger.debug(f"FileWatcher: Found {len(new_files)} new files to process")
|
||||||
|
|
||||||
|
for new_file in new_files:
|
||||||
|
self.process_new_segment(new_file)
|
||||||
|
self.processed_files.add(new_file)
|
||||||
|
|
||||||
|
# Periodic cleanup to prevent memory leak
|
||||||
|
self._cleanup_processed_files()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"FileWatcher: Error scanning directory: {e}")
|
||||||
|
|
||||||
|
def _monitor_loop(self) -> None:
|
||||||
|
"""Main monitoring loop running in thread"""
|
||||||
|
logger.info(f"FileWatcher: Started monitoring {self.media_dir}")
|
||||||
|
|
||||||
|
# Initial scan for existing files
|
||||||
|
self.scan_for_new_files()
|
||||||
|
|
||||||
|
while self._monitoring:
|
||||||
|
try:
|
||||||
|
self.scan_for_new_files()
|
||||||
|
time.sleep(self.poll_interval)
|
||||||
|
except Exception as e:
|
||||||
|
if self._monitoring: # Only log if still supposed to be running
|
||||||
|
logger.error(f"FileWatcher: Error in monitor loop: {e}")
|
||||||
|
time.sleep(self.poll_interval)
|
||||||
|
|
||||||
|
def start_monitoring(self) -> None:
|
||||||
|
"""Start file system monitoring in background thread"""
|
||||||
|
if self._monitoring:
|
||||||
|
logger.warning("FileWatcher: Already monitoring")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._monitoring = True
|
||||||
|
self._monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||||
|
self._monitor_thread.start()
|
||||||
|
logger.info(f"FileWatcher: Started monitoring thread for {self.media_dir}")
|
||||||
|
|
||||||
|
def stop_monitoring(self) -> None:
|
||||||
|
"""Stop file system monitoring"""
|
||||||
|
if not self._monitoring:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._monitoring = False
|
||||||
|
if self._monitor_thread and self._monitor_thread.is_alive():
|
||||||
|
self._monitor_thread.join(timeout=2.0)
|
||||||
|
|
||||||
|
# Clear processed files to free memory
|
||||||
|
self.processed_files.clear()
|
||||||
|
logger.info("FileWatcher: Stopped monitoring")
|
||||||
|
|
||||||
|
def get_source_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return file watcher source information"""
|
||||||
|
return {
|
||||||
|
'name': 'FileWatcherEventSource',
|
||||||
|
'type': 'filesystem',
|
||||||
|
'status': 'active' if self._monitoring else 'stopped',
|
||||||
|
'media_dir': str(self.media_dir),
|
||||||
|
'poll_interval': self.poll_interval,
|
||||||
|
'processed_files': len(self.processed_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CloudStorageEventSource(SegmentEventSource):
|
||||||
|
"""Cloud storage event source for production deployments"""
|
||||||
|
|
||||||
|
def __init__(self, bucket_name: Optional[str] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.bucket_name = bucket_name or os.getenv('GCS_BUCKET_NAME', 'media-segments')
|
||||||
|
self._monitoring = False
|
||||||
|
|
||||||
|
def start_monitoring(self) -> None:
|
||||||
|
"""Start cloud storage event monitoring"""
|
||||||
|
# TODO: Implement GCS Pub/Sub or webhook receiver
|
||||||
|
logger.info(f"CloudStorage: Would start monitoring bucket {self.bucket_name}")
|
||||||
|
logger.warning("CloudStorage: Not yet implemented - placeholder for future cloud deployment")
|
||||||
|
self._monitoring = True
|
||||||
|
|
||||||
|
def stop_monitoring(self) -> None:
|
||||||
|
"""Stop cloud storage event monitoring"""
|
||||||
|
logger.info("CloudStorage: Stopping monitoring")
|
||||||
|
self._monitoring = False
|
||||||
|
|
||||||
|
def get_source_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return cloud storage source information"""
|
||||||
|
return {
|
||||||
|
'name': 'CloudStorageEventSource',
|
||||||
|
'type': 'cloud_storage',
|
||||||
|
'status': 'active' if self._monitoring else 'stopped',
|
||||||
|
'bucket_name': self.bucket_name,
|
||||||
|
'implementation': 'placeholder'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookEventSource(SegmentEventSource):
|
||||||
|
"""Webhook receiver event source for external integrations"""
|
||||||
|
|
||||||
|
def __init__(self, webhook_port: int = 8001):
|
||||||
|
super().__init__()
|
||||||
|
self.webhook_port = webhook_port
|
||||||
|
self._monitoring = False
|
||||||
|
|
||||||
|
def start_monitoring(self) -> None:
|
||||||
|
"""Start webhook server"""
|
||||||
|
# TODO: Implement webhook HTTP server
|
||||||
|
logger.info(f"Webhook: Would start server on port {self.webhook_port}")
|
||||||
|
logger.warning("Webhook: Not yet implemented - placeholder for future integrations")
|
||||||
|
self._monitoring = True
|
||||||
|
|
||||||
|
def stop_monitoring(self) -> None:
|
||||||
|
"""Stop webhook server"""
|
||||||
|
logger.info("Webhook: Stopping server")
|
||||||
|
self._monitoring = False
|
||||||
|
|
||||||
|
def get_source_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return webhook source information"""
|
||||||
|
return {
|
||||||
|
'name': 'WebhookEventSource',
|
||||||
|
'type': 'webhook',
|
||||||
|
'status': 'active' if self._monitoring else 'stopped',
|
||||||
|
'webhook_port': self.webhook_port,
|
||||||
|
'implementation': 'placeholder'
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ import time
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from ai_processing.processors.video_analyzer import VideoAnalyzer
|
|
||||||
from .models import VideoStream, StreamStatus
|
from .models import VideoStream, StreamStatus
|
||||||
|
from .segment_events import SegmentEventPublisher
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ class HLSFileWatcher:
|
|||||||
self.media_dir = Path(media_dir or settings.MEDIA_ROOT)
|
self.media_dir = Path(media_dir or settings.MEDIA_ROOT)
|
||||||
self.poll_interval = poll_interval
|
self.poll_interval = poll_interval
|
||||||
self.processed_files = set()
|
self.processed_files = set()
|
||||||
self.analyzer = VideoAnalyzer()
|
self.event_publisher = SegmentEventPublisher()
|
||||||
|
|
||||||
def get_stream_key_from_filename(self, filename):
|
def get_stream_key_from_filename(self, filename):
|
||||||
"""Extract stream_key from filename: 'stream_key-segment_number.ts' -> 'stream_key'"""
|
"""Extract stream_key from filename: 'stream_key-segment_number.ts' -> 'stream_key'"""
|
||||||
@@ -27,18 +27,30 @@ class HLSFileWatcher:
|
|||||||
return stream_key if stream_key else None
|
return stream_key if stream_key else None
|
||||||
|
|
||||||
def process_new_segment(self, file_path):
|
def process_new_segment(self, file_path):
|
||||||
"""Process a new HLS segment file"""
|
"""Process a new HLS segment file by publishing event"""
|
||||||
try:
|
try:
|
||||||
# Determine the active stream from the database
|
# Determine the active stream from the database
|
||||||
active_stream = VideoStream.objects.filter(status=StreamStatus.ACTIVE).first()
|
active_stream = VideoStream.objects.filter(status=StreamStatus.ACTIVE).first()
|
||||||
if not active_stream:
|
if not active_stream:
|
||||||
logger.warning(f"File watcher: No active stream found, skipping segment {file_path.name}")
|
logger.warning(f"File watcher: No active stream found, skipping segment {file_path.name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
stream_key = active_stream.stream_key
|
stream_key = active_stream.stream_key
|
||||||
|
session_id = getattr(active_stream, 'session_id', None)
|
||||||
logger.info(f"File watcher: Processing new segment {file_path.name} (stream: {stream_key})")
|
logger.info(f"File watcher: Processing new segment {file_path.name} (stream: {stream_key})")
|
||||||
# Queue for analysis
|
|
||||||
self.analyzer.queue_segment_analysis(stream_key, str(file_path))
|
# Publish event to Redis instead of copying file
|
||||||
logger.info(f"File watcher: Queued segment for analysis: {file_path.name}")
|
success = self.event_publisher.publish_segment_event(
|
||||||
|
segment_path=str(file_path),
|
||||||
|
stream_key=stream_key,
|
||||||
|
session_id=session_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"File watcher: Published segment event for {file_path.name}")
|
||||||
|
else:
|
||||||
|
logger.error(f"File watcher: Failed to publish event for {file_path.name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"File watcher: Error processing {file_path}: {e}")
|
logger.error(f"File watcher: Error processing {file_path}: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
@@ -48,6 +60,7 @@ class HLSFileWatcher:
|
|||||||
"""Scan for new .ts files in the media directory"""
|
"""Scan for new .ts files in the media directory"""
|
||||||
try:
|
try:
|
||||||
if not self.media_dir.exists():
|
if not self.media_dir.exists():
|
||||||
|
logger.debug(f"File watcher: Media directory {self.media_dir} does not exist")
|
||||||
return
|
return
|
||||||
|
|
||||||
current_files = set()
|
current_files = set()
|
||||||
@@ -55,18 +68,31 @@ class HLSFileWatcher:
|
|||||||
if ts_file.is_file():
|
if ts_file.is_file():
|
||||||
current_files.add(ts_file)
|
current_files.add(ts_file)
|
||||||
|
|
||||||
|
logger.debug(f"File watcher: Found {len(current_files)} total .ts files, {len(self.processed_files)} already processed")
|
||||||
|
|
||||||
# Find new files
|
# Find new files
|
||||||
new_files = current_files - self.processed_files
|
new_files = current_files - self.processed_files
|
||||||
|
|
||||||
|
if new_files:
|
||||||
|
logger.info(f"File watcher: Found {len(new_files)} new files to process")
|
||||||
|
|
||||||
for new_file in new_files:
|
for new_file in new_files:
|
||||||
self.process_new_segment(new_file)
|
self.process_new_segment(new_file)
|
||||||
self.processed_files.add(new_file)
|
self.processed_files.add(new_file)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"File watcher: Error scanning directory: {e}")
|
logger.error(f"File watcher: Error scanning directory: {e}")
|
||||||
|
logger.debug(f"File watcher: Scan exception details: {e}")
|
||||||
|
|
||||||
def start_watching(self):
|
def start_watching(self):
|
||||||
"""Start the file watching loop"""
|
"""Start the file watching loop"""
|
||||||
|
logger.debug(f"File watcher: Starting to watch {self.media_dir}")
|
||||||
|
logger.debug(f"File watcher: Directory exists: {self.media_dir.exists()}")
|
||||||
|
|
||||||
|
if self.media_dir.exists():
|
||||||
|
existing_files = list(self.media_dir.glob("*.ts"))
|
||||||
|
logger.debug(f"File watcher: Found {len(existing_files)} existing .ts files")
|
||||||
|
|
||||||
logger.info(f"File watcher: Starting to watch {self.media_dir}")
|
logger.info(f"File watcher: Starting to watch {self.media_dir}")
|
||||||
|
|
||||||
# Initial scan to catch existing files
|
# Initial scan to catch existing files
|
||||||
@@ -81,4 +107,5 @@ class HLSFileWatcher:
|
|||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"File watcher: Unexpected error: {e}")
|
logger.error(f"File watcher: Unexpected error: {e}")
|
||||||
|
logger.debug(f"File watcher: Exception traceback: {e}")
|
||||||
time.sleep(self.poll_interval)
|
time.sleep(self.poll_interval)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""
|
||||||
|
Django management command to start the configured event source.
|
||||||
|
Supports pluggable event sources: file watcher, cloud storage, webhook, etc.
|
||||||
|
"""
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from streaming.event_source_manager import get_event_source_manager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Start the configured segment event source (file watcher, cloud events, etc.)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--source-type',
|
||||||
|
type=str,
|
||||||
|
help='Override event source type (filewatcher, cloud, webhook)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--status',
|
||||||
|
action='store_true',
|
||||||
|
help='Show current event source status and exit'
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# Just show status if requested
|
||||||
|
if options['status']:
|
||||||
|
self.show_status()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Initialize event source manager
|
||||||
|
try:
|
||||||
|
if options['source_type']:
|
||||||
|
from streaming.event_source_manager import EventSourceManager
|
||||||
|
manager = EventSourceManager(source_type=options['source_type'])
|
||||||
|
else:
|
||||||
|
manager = get_event_source_manager()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'Failed to initialize event source: {e}'))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Show initial status
|
||||||
|
status = manager.get_status()
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f"Initialized event source: {status['configured_type']}"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Setup signal handlers for graceful shutdown
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
self.stdout.write(self.style.WARNING('Received shutdown signal, stopping event source...'))
|
||||||
|
try:
|
||||||
|
manager.stop_monitoring()
|
||||||
|
self.stdout.write(self.style.SUCCESS('Event source stopped successfully'))
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'Error stopping event source: {e}'))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
# Start monitoring
|
||||||
|
try:
|
||||||
|
success = manager.start_monitoring()
|
||||||
|
if not success:
|
||||||
|
self.stdout.write(self.style.ERROR('Failed to start event source monitoring'))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f"Event source monitoring started successfully with {status['configured_type']}"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Keep the command running
|
||||||
|
signal.pause()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'Error starting event source: {e}'))
|
||||||
|
try:
|
||||||
|
manager.stop_monitoring()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def show_status(self):
|
||||||
|
"""Show current event source status"""
|
||||||
|
try:
|
||||||
|
manager = get_event_source_manager()
|
||||||
|
status = manager.get_status()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.HTTP_INFO('=== Event Source Status ==='))
|
||||||
|
self.stdout.write(f"Configured Type: {status['configured_type']}")
|
||||||
|
self.stdout.write(f"Initialized: {status['initialized']}")
|
||||||
|
|
||||||
|
if status['initialized']:
|
||||||
|
source_info = status['source_info']
|
||||||
|
self.stdout.write(f"Source Name: {source_info['name']}")
|
||||||
|
self.stdout.write(f"Source Type: {source_info['type']}")
|
||||||
|
self.stdout.write(f"Status: {source_info['status']}")
|
||||||
|
|
||||||
|
# Show source-specific info
|
||||||
|
if 'media_dir' in source_info:
|
||||||
|
self.stdout.write(f"Media Directory: {source_info['media_dir']}")
|
||||||
|
self.stdout.write(f"Poll Interval: {source_info['poll_interval']}s")
|
||||||
|
self.stdout.write(f"Processed Files: {source_info['processed_files']}")
|
||||||
|
elif 'bucket_name' in source_info:
|
||||||
|
self.stdout.write(f"Bucket Name: {source_info['bucket_name']}")
|
||||||
|
elif 'webhook_port' in source_info:
|
||||||
|
self.stdout.write(f"Webhook Port: {source_info['webhook_port']}")
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.ERROR(f"Error: {status.get('error', 'Unknown error')}"))
|
||||||
|
|
||||||
|
self.stdout.write(f"Available Types: {', '.join(status['available_types'])}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'Error getting status: {e}'))
|
||||||
94
media-analyzer/backend/streaming/segment_events.py
Normal file
94
media-analyzer/backend/streaming/segment_events.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from django.conf import settings
|
||||||
|
import redis
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SegmentEventPublisher:
|
||||||
|
"""Publishes segment events to Redis for processing by Celery workers"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.redis_client = redis.Redis(
|
||||||
|
host=settings.REDIS_HOST,
|
||||||
|
port=settings.REDIS_PORT,
|
||||||
|
decode_responses=True
|
||||||
|
)
|
||||||
|
self.event_key = 'media_analyzer:segment_events'
|
||||||
|
|
||||||
|
def publish_segment_event(self, segment_path: str, stream_key: str, session_id: Optional[str] = None):
|
||||||
|
"""Publish a new segment event to Redis and trigger processing"""
|
||||||
|
try:
|
||||||
|
event = {
|
||||||
|
'segment_path': segment_path,
|
||||||
|
'stream_key': stream_key,
|
||||||
|
'session_id': session_id,
|
||||||
|
'timestamp': time.time(),
|
||||||
|
'event_type': 'new_segment'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Push event to Redis list (FIFO queue)
|
||||||
|
result = self.redis_client.lpush(self.event_key, json.dumps(event))
|
||||||
|
logger.debug(f"Published segment event: {segment_path} (queue length: {result})")
|
||||||
|
|
||||||
|
# Trigger event processing task
|
||||||
|
try:
|
||||||
|
from ai_processing.event_tasks import process_segment_from_event
|
||||||
|
process_segment_from_event.delay()
|
||||||
|
logger.debug(f"Triggered event processing for {segment_path}")
|
||||||
|
except Exception as task_error:
|
||||||
|
logger.warning(f"Failed to trigger event processing task: {task_error}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to publish segment event for {segment_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_queue_length(self) -> int:
|
||||||
|
"""Get current number of pending segment events"""
|
||||||
|
try:
|
||||||
|
return self.redis_client.llen(self.event_key)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get queue length: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class SegmentEventConsumer:
|
||||||
|
"""Consumes segment events from Redis for processing"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.redis_client = redis.Redis(
|
||||||
|
host=settings.REDIS_HOST,
|
||||||
|
port=settings.REDIS_PORT,
|
||||||
|
decode_responses=True
|
||||||
|
)
|
||||||
|
self.event_key = 'media_analyzer:segment_events'
|
||||||
|
|
||||||
|
def consume_segment_event(self, timeout: int = 1) -> Optional[dict]:
|
||||||
|
"""Consume next segment event from Redis (blocking)"""
|
||||||
|
try:
|
||||||
|
# BRPOP blocks until event available or timeout
|
||||||
|
result = self.redis_client.brpop(self.event_key, timeout=timeout)
|
||||||
|
if result:
|
||||||
|
_, event_json = result
|
||||||
|
event = json.loads(event_json)
|
||||||
|
logger.debug(f"Consumed segment event: {event['segment_path']}")
|
||||||
|
return event
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to consume segment event: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def peek_next_event(self) -> Optional[dict]:
|
||||||
|
"""Peek at next event without consuming it"""
|
||||||
|
try:
|
||||||
|
event_json = self.redis_client.lindex(self.event_key, -1) # Last item (FIFO)
|
||||||
|
if event_json:
|
||||||
|
return json.loads(event_json)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to peek at next event: {e}")
|
||||||
|
return None
|
||||||
@@ -112,8 +112,40 @@ services:
|
|||||||
condition: service_started
|
condition: service_started
|
||||||
command: ./entrypoint-celery.sh --queues=default,config_management --hostname=default-worker@%h
|
command: ./entrypoint-celery.sh --queues=default,config_management --hostname=default-worker@%h
|
||||||
|
|
||||||
# File Watcher - Monitor HLS segments
|
# Event Source - Pluggable segment monitoring (file watcher, cloud events, etc)
|
||||||
file-watcher:
|
event-source:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
target: development
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
- ./media:/app/media
|
||||||
|
environment:
|
||||||
|
- DEBUG=1
|
||||||
|
- DB_HOST=postgres
|
||||||
|
- DB_NAME=media_analyzer
|
||||||
|
- DB_USER=media_user
|
||||||
|
- DB_PASSWORD=media_pass
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- MEDIA_ROOT=/app/media
|
||||||
|
# Event source configuration
|
||||||
|
- SEGMENT_EVENT_SOURCE=filewatcher # Options: filewatcher, cloud, webhook
|
||||||
|
- FILE_WATCHER_POLL_INTERVAL=1.0
|
||||||
|
- WEBHOOK_PORT=8001
|
||||||
|
# Cloud configuration (for future use)
|
||||||
|
- GCS_BUCKET_NAME=media-analyzer-segments
|
||||||
|
- USE_CLOUD_STORAGE=false
|
||||||
|
- USE_CLOUD_VISION=false
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
command: python manage.py start_event_source
|
||||||
|
|
||||||
|
# Event Processor - Consumes segment events from Redis
|
||||||
|
event-processor:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
target: development
|
target: development
|
||||||
@@ -134,7 +166,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
command: python manage.py watch_hls_files --poll-interval=2
|
command: celery -A media_analyzer worker -l info -Q default -c 2 --hostname=event-processor@%h
|
||||||
|
|
||||||
# Angular Frontend with NGINX (unified approach)
|
# Angular Frontend with NGINX (unified approach)
|
||||||
frontend:
|
frontend:
|
||||||
@@ -177,8 +209,8 @@ services:
|
|||||||
- "0.0.0.0:1935:1935" # RTMP port - bind to all interfaces
|
- "0.0.0.0:1935:1935" # RTMP port - bind to all interfaces
|
||||||
- "0.0.0.0:8081:80" # HTTP port for HLS - bind to all interfaces
|
- "0.0.0.0:8081:80" # HTTP port for HLS - bind to all interfaces
|
||||||
volumes:
|
volumes:
|
||||||
# Persist HLS segments and playlists to host media directory
|
# Persist HLS segments and playlists to host media directory - FIXED: same path as event-source
|
||||||
- ./media:/var/www/media
|
- ./media:/app/media
|
||||||
# Custom nginx.conf for RTMP/HLS configuration
|
# Custom nginx.conf for RTMP/HLS configuration
|
||||||
- ./docker/nginx.conf:/etc/nginx/nginx.conf
|
- ./docker/nginx.conf:/etc/nginx/nginx.conf
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ rtmp {
|
|||||||
|
|
||||||
# Turn on HLS
|
# Turn on HLS
|
||||||
hls on;
|
hls on;
|
||||||
hls_path /var/www/media;
|
hls_path /app/media;
|
||||||
hls_fragment 3;
|
hls_fragment 3;
|
||||||
hls_playlist_length 60;
|
hls_playlist_length 60;
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ http {
|
|||||||
add_header Content-Type application/vnd.apple.mpegurl;
|
add_header Content-Type application/vnd.apple.mpegurl;
|
||||||
add_header Access-Control-Allow-Origin *;
|
add_header Access-Control-Allow-Origin *;
|
||||||
add_header Cache-Control no-cache;
|
add_header Cache-Control no-cache;
|
||||||
root /var/www/media;
|
root /app/media;
|
||||||
}
|
}
|
||||||
|
|
||||||
# HLS segment files
|
# HLS segment files
|
||||||
@@ -48,12 +48,12 @@ http {
|
|||||||
add_header Content-Type video/mp2t;
|
add_header Content-Type video/mp2t;
|
||||||
add_header Access-Control-Allow-Origin *;
|
add_header Access-Control-Allow-Origin *;
|
||||||
add_header Cache-Control no-cache;
|
add_header Cache-Control no-cache;
|
||||||
root /var/www/media;
|
root /app/media;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fallback for other requests
|
# Fallback for other requests
|
||||||
location / {
|
location / {
|
||||||
root /var/www/media;
|
root /app/media;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user