implement robust event-driven file watching system

This commit is contained in:
buenosairesam
2025-08-25 04:03:56 -03:00
parent cdab5191e4
commit dd3e9769a5
16 changed files with 11808 additions and 421 deletions

View File

@@ -835,4 +835,5 @@ If you tell me your stack (language/framework + how you install deps), Ill ta
it's docker compose not docker-compose it's docker compose not docker-compose
- use debug logs not prints

View File

@@ -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&#45;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&#45;&gt;planned --> <!-- implemented&#45;&gt;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&#45;&gt;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&#45;&gt;webcam_adapter --> <!-- webcam&#45;&gt;webcam_adapter -->
<g id="edge2" class="edge"> <g id="edge3" class="edge">
<title>webcam&#45;&gt;webcam_adapter</title> <title>webcam&#45;&gt;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&#45;&gt;rtmp_adapter --> <!-- rtmp&#45;&gt;rtmp_adapter -->
<g id="edge3" class="edge"> <g id="edge4" class="edge">
<title>rtmp&#45;&gt;rtmp_adapter</title> <title>rtmp&#45;&gt;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&#45;&gt;file_adapter --> <!-- files&#45;&gt;file_adapter -->
<g id="edge18" class="edge"> <g id="edge28" class="edge">
<title>files&#45;&gt;file_adapter</title> <title>files&#45;&gt;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&#45;&gt;webcam_adapter --> <!-- base_adapter&#45;&gt;webcam_adapter -->
<g id="edge28" class="edge"> <g id="edge38" class="edge">
<title>base_adapter&#45;&gt;webcam_adapter</title> <title>base_adapter&#45;&gt;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&#45;&gt;rtmp_adapter --> <!-- base_adapter&#45;&gt;rtmp_adapter -->
<g id="edge29" class="edge"> <g id="edge39" class="edge">
<title>base_adapter&#45;&gt;rtmp_adapter</title> <title>base_adapter&#45;&gt;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&#45;&gt;file_adapter --> <!-- base_adapter&#45;&gt;file_adapter -->
<g id="edge30" class="edge"> <g id="edge40" class="edge">
<title>base_adapter&#45;&gt;file_adapter</title> <title>base_adapter&#45;&gt;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&#45;&gt;django --> <!-- webcam_adapter&#45;&gt;django -->
<g id="edge4" class="edge"> <g id="edge5" class="edge">
<title>webcam_adapter&#45;&gt;django</title> <title>webcam_adapter&#45;&gt;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&#45;&gt;django --> <!-- rtmp_adapter&#45;&gt;django -->
<g id="edge5" class="edge"> <g id="edge6" class="edge">
<title>rtmp_adapter&#45;&gt;django</title> <title>rtmp_adapter&#45;&gt;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&#45;&gt;django --> <!-- file_adapter&#45;&gt;django -->
<g id="edge19" class="edge"> <g id="edge29" class="edge">
<title>file_adapter&#45;&gt;django</title> <title>file_adapter&#45;&gt;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&#45;&gt;postgres --> <!-- django&#45;&gt;postgres -->
<g id="edge6" class="edge"> <g id="edge7" class="edge">
<title>django&#45;&gt;postgres</title> <title>django&#45;&gt;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 &amp; Broker</text> <text text-anchor="middle" x="657" y="-1180.41" font-family="Arial" font-size="10.00" fill="#ffffff">Cache &amp; Broker</text>
</g> </g>
<!-- django&#45;&gt;redis --> <!-- django&#45;&gt;redis -->
<g id="edge7" class="edge"> <g id="edge8" class="edge">
<title>django&#45;&gt;redis</title> <title>django&#45;&gt;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&#45;served)</text> <text text-anchor="middle" x="981" y="-1180.41" font-family="Arial" font-size="10.00" fill="#ffffff">(nginx&#45;served)</text>
</g> </g>
<!-- django&#45;&gt;local_storage --> <!-- django&#45;&gt;local_storage -->
<g id="edge8" class="edge"> <g id="edge9" class="edge">
<title>django&#45;&gt;local_storage</title> <title>django&#45;&gt;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&#45;&gt;gcs_storage --> <!-- django&#45;&gt;gcs_storage -->
<g id="edge9" class="edge"> <g id="edge10" class="edge">
<title>django&#45;&gt;gcs_storage</title> <title>django&#45;&gt;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&#45;&gt;angular --> <!-- django&#45;&gt;angular -->
<g id="edge15" class="edge"> <g id="edge25" class="edge">
<title>django&#45;&gt;angular</title> <title>django&#45;&gt;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&#45;&gt;logo_worker --> <!-- redis&#45;&gt;logo_worker -->
<g id="edge10" class="edge"> <g id="edge19" class="edge">
<title>redis&#45;&gt;logo_worker</title> <title>redis&#45;&gt;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&#45;&gt;visual_worker --> <!-- redis&#45;&gt;visual_worker -->
<g id="edge20" class="edge"> <g id="edge30" class="edge">
<title>redis&#45;&gt;visual_worker</title> <title>redis&#45;&gt;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&#45;&gt;audio_worker --> <!-- redis&#45;&gt;audio_worker -->
<g id="edge21" class="edge"> <g id="edge31" class="edge">
<title>redis&#45;&gt;audio_worker</title> <title>redis&#45;&gt;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&#45;&gt;text_worker --> <!-- redis&#45;&gt;text_worker -->
<g id="edge22" class="edge"> <g id="edge32" class="edge">
<title>redis&#45;&gt;text_worker</title> <title>redis&#45;&gt;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&#45;&gt;angular --> <!-- nginx&#45;&gt;angular -->
<g id="edge17" class="edge"> <g id="edge27" class="edge">
<title>nginx&#45;&gt;angular</title> <title>nginx&#45;&gt;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&#45;&gt;local_strategy --> <!-- base_strategy&#45;&gt;local_strategy -->
<g id="edge31" class="edge"> <g id="edge41" class="edge">
<title>base_strategy&#45;&gt;local_strategy</title> <title>base_strategy&#45;&gt;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&#45;&gt;lan_strategy --> <!-- base_strategy&#45;&gt;lan_strategy -->
<g id="edge32" class="edge"> <g id="edge42" class="edge">
<title>base_strategy&#45;&gt;lan_strategy</title> <title>base_strategy&#45;&gt;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&#45;&gt;cloud_strategy --> <!-- base_strategy&#45;&gt;cloud_strategy -->
<g id="edge33" class="edge"> <g id="edge43" class="edge">
<title>base_strategy&#45;&gt;cloud_strategy</title> <title>base_strategy&#45;&gt;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&#45;&gt;clip_adapter --> <!-- local_strategy&#45;&gt;clip_adapter -->
<g id="edge12" class="edge"> <g id="edge21" class="edge">
<title>local_strategy&#45;&gt;clip_adapter</title> <title>local_strategy&#45;&gt;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&#45;&gt;gcp_vision --> <!-- local_strategy&#45;&gt;gcp_vision -->
<g id="edge13" class="edge"> <g id="edge22" class="edge">
<title>local_strategy&#45;&gt;gcp_vision</title> <title>local_strategy&#45;&gt;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&#45;&gt;yolo_adapter --> <!-- lan_strategy&#45;&gt;yolo_adapter -->
<g id="edge27" class="edge"> <g id="edge37" class="edge">
<title>lan_strategy&#45;&gt;yolo_adapter</title> <title>lan_strategy&#45;&gt;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&#45;to&#45;Text API</text> <text text-anchor="middle" x="1655" y="-98.41" font-family="Arial" font-size="10.00" fill="#212529">Speech&#45;to&#45;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&#45;&gt;speech_api --> <!-- cloud_strategy&#45;&gt;speech_api -->
<g id="edge26" class="edge"> <g id="edge36" class="edge">
<title>cloud_strategy&#45;&gt;speech_api</title> <title>cloud_strategy&#45;&gt;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&#45;&gt;file_watcher_source -->
<g id="edge47" class="edge">
<title>base_event_source&#45;&gt;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&#45;&gt;cloud_storage_source -->
<g id="edge48" class="edge">
<title>base_event_source&#45;&gt;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&#45;&gt;webhook_source -->
<g id="edge49" class="edge">
<title>base_event_source&#45;&gt;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&#45;based)</text>
</g>
<!-- file_watcher_source&#45;&gt;event_source_manager -->
<g id="edge13" class="edge">
<title>file_watcher_source&#45;&gt;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&#45;&gt;event_source_manager -->
<g id="edge14" class="edge">
<title>cloud_storage_source&#45;&gt;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&#45;&gt;event_source_manager -->
<g id="edge15" class="edge">
<title>webhook_source&#45;&gt;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&#45;&gt;redis_events -->
<g id="edge16" class="edge">
<title>event_source_manager&#45;&gt;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&#45;&gt;event_processor -->
<g id="edge17" class="edge">
<title>redis_events&#45;&gt;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&#45;&gt;logo_worker -->
<g id="edge18" class="edge">
<title>event_processor&#45;&gt;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&#45;&gt;django -->
<g id="edge24" class="edge">
<title>logo_worker&#45;&gt;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&#45;&gt;local_strategy --> <!-- logo_worker&#45;&gt;local_strategy -->
<g id="edge11" class="edge"> <g id="edge20" class="edge">
<title>logo_worker&#45;&gt;local_strategy</title> <title>logo_worker&#45;&gt;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&#45;&gt;lan_strategy --> <!-- visual_worker&#45;&gt;lan_strategy -->
<g id="edge23" class="edge"> <g id="edge33" class="edge">
<title>visual_worker&#45;&gt;lan_strategy</title> <title>visual_worker&#45;&gt;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&#45;&gt;cloud_strategy --> <!-- audio_worker&#45;&gt;cloud_strategy -->
<g id="edge24" class="edge"> <g id="edge34" class="edge">
<title>audio_worker&#45;&gt;cloud_strategy</title> <title>audio_worker&#45;&gt;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&#45;&gt;cloud_strategy --> <!-- text_worker&#45;&gt;cloud_strategy -->
<g id="edge25" class="edge"> <g id="edge35" class="edge">
<title>text_worker&#45;&gt;cloud_strategy</title> <title>text_worker&#45;&gt;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&#45;&gt;clip_adapter --> <!-- base_ai&#45;&gt;clip_adapter -->
<g id="edge34" class="edge"> <g id="edge44" class="edge">
<title>base_ai&#45;&gt;clip_adapter</title> <title>base_ai&#45;&gt;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&#45;&gt;gcp_vision --> <!-- base_ai&#45;&gt;gcp_vision -->
<g id="edge35" class="edge"> <g id="edge45" class="edge">
<title>base_ai&#45;&gt;gcp_vision</title> <title>base_ai&#45;&gt;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&#45;&gt;yolo_adapter --> <!-- base_ai&#45;&gt;yolo_adapter -->
<g id="edge36" class="edge"> <g id="edge46" class="edge">
<title>base_ai&#45;&gt;yolo_adapter</title> <title>base_ai&#45;&gt;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&#45;&gt;vision_api --> <!-- gcp_vision&#45;&gt;vision_api -->
<g id="edge14" class="edge"> <g id="edge23" class="edge">
<title>gcp_vision&#45;&gt;vision_api</title> <title>gcp_vision&#45;&gt;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&#45;&gt;file_watcher_source -->
<g id="edge11" class="edge">
<title>local_storage&#45;&gt;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&#45;&gt;cloud_storage_source -->
<g id="edge12" class="edge">
<title>gcs_storage&#45;&gt;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&#45;&gt;hls_player --> <!-- angular&#45;&gt;hls_player -->
<g id="edge16" class="edge"> <g id="edge26" class="edge">
<title>angular&#45;&gt;hls_player</title> <title>angular&#45;&gt;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

View File

@@ -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];
} }

File diff suppressed because it is too large Load Diff

View 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.

View File

@@ -0,0 +1,46 @@
Heres 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>
```

View 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}

View File

@@ -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)

View File

@@ -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
# ============================================================================= # =============================================================================

View 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()

View 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'
}

View File

@@ -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)

View File

@@ -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}'))

View 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

View File

@@ -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:

View File

@@ -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;
} }
} }
} }