sources/webrtc-reviewed/stack/webrtc.drs

Текстовый файл из учебного комплекта, не свидетельство установленного runtime. В этом snapshot удалены credentials; исходная и очищенная SHA256 указаны в source manifest. Вернуться к учебнику · Исходный файл

   1 Options name: 'web-rtc-komaroff'
   2 
   3 NATS_USER = ENV.fetch('WEBRTC_NATS_USER')
   4 NATS_PASS = ENV.fetch('WEBRTC_NATS_PASSWORD')
   5 NATS_URL  = "nats://[credentials-omitted]@web-rtc-komaroff_audio_nats:4222"
   6 
   7 USER_ID = "user123"
   8 NATS_EVENTS_SUBJECT = "nats.events."
   9 NATS_AGENT_SUBJECT = "nats.agent."
  10 NATS_AGENT_HISTORY_SUBJECT = "nats.agent.history."
  11 NATS_LLM_SUBJECT = "nats.llm."
  12 ASR_IN_PREFIX = "inference.whisper.stream."
  13 ASR_OUT_PREFIX = "inference.whisper.text."
  14 
  15 HOST = 'webrtc-komaroff.*'
  16 VOICE_CHAT_HOST = 'voice-chat.*'
  17 POSTGRES_HOST = 'web-rtc-komaroff_src_postgres'
  18 POSTGRES_PORT = '5432'
  19 POSTGRES_DB = 'mcp'
  20 POSTGRES_USER = ENV.fetch('WEBRTC_POSTGRES_USER')
  21 POSTGRES_PASSWORD = ENV.fetch('WEBRTC_POSTGRES_PASSWORD')
  22 DATABASE_URL = "postgresql://#{POSTGRES_USER}:#{POSTGRES_PASSWORD}@#{POSTGRES_HOST}:#{POSTGRES_PORT}/#{POSTGRES_DB}?sslmode=disable"
  23 NATS_TOOLS_PREFIX = 'nats.tools.'
  24 NATS_REQUEST_TIMEOUT = '600'
  25 AGENT_MAX_STEPS = '10'
  26 MAX_STEPS = '10'
  27 TIMEOUT_SECONDS = '600'
  28 WORKFLOW_RUNTIME = ENV.fetch('WORKFLOW_RUNTIME', 'ruby_node')
  29 NATS_WORKFLOW_RUN_SUBJECT =
  30   case WORKFLOW_RUNTIME
  31   when 'ruby'
  32     'nats.workflow.run.ruby'
  33   when 'ruby_node'
  34     'nats.workflow.run.ruby.node'
  35   else
  36     'nats.workflow.run.python'
  37   end
  38 NATS_WORKFLOW_HEALTH_SUBJECT =
  39   case WORKFLOW_RUNTIME
  40   when 'ruby'
  41     'nats.workflow.health.ruby'
  42   when 'ruby_node'
  43     'nats.workflow.health.ruby.node'
  44   else
  45     'nats.workflow.health.python'
  46   end
  47 webrtc_registry = ->(img){ "builder-registry.builder.giscloud.ru/gis/webrtc-komaroff/#{img}/main" }
  48 registry =->(image) { "builder-registry.builder.giscloud.ru/#{image}" }
  49 
  50 WHISPER_STACK_REVISION = '2026-04-22-whisper-sidecars-simplified-1'
  51 WHISPER_FILE_JOB_STORAGE_DIR = 'локальный источник;
  52 WHISPER_FILE_JOB_LOCK_URL = 'http://web-rtc-komaroff_whisper_file_job_lock:8091'
  53 
  54 WHISPER_FILE_JOB_ENV = {
  55   NATS_URL: NATS_URL,
  56   NATS_EVENTS_SUBJECT: NATS_EVENTS_SUBJECT,
  57   ASR_FILE_EVENT_PREFIX: 'inference.whisper.file.event.',
  58   ASR_FILE_SESSION_EVENT_PREFIX: 'inference.whisper.file.session.',
  59   LINTO_HTTP_URL: 'http://llm-models_linto_stt_whisper_http:80/transcribe',
  60   LINTO_HTTP_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_linto:7000',
  61   LINTO_HTTP_HOST: '',
  62   LINTO_HTTP_LANGUAGE: 'ru',
  63   FILE_JOB_STORAGE_DIR: WHISPER_FILE_JOB_STORAGE_DIR,
  64   FILE_CHUNK_SECONDS: '300',
  65   FILE_CHUNK_OVERLAP_SECONDS: '15',
  66   FILE_CHUNK_RETRY_LIMIT: '2',
  67   FILE_TEXT_SOFT_BREAK_S: '0.8',
  68   FILE_TEXT_HARD_BREAK_S: '1.8',
  69   FILE_JOB_LOCK_URL: WHISPER_FILE_JOB_LOCK_URL,
  70   FILE_JOB_LOCK_TIMEOUT_S: '5',
  71   FILE_JOB_LOCK_HEARTBEAT_S: '30',
  72   WHISPER_STACK_REVISION: WHISPER_STACK_REVISION
  73 }
  74 
  75 WHISPER_WEBUI_ENV = {
  76   WEBUI_ENABLED: '1',
  77   WEBUI_FILE_TRANSCRIBE_ENABLED: '1',
  78   WEBUI_FILE_MAX_UPLOAD_MB: '1024',
  79   WEBUI_NATS_WS_URL: '/ws',
  80   WEBUI_NATS_USER: NATS_USER,
  81   WEBUI_NATS_PASS: NATS_PASS,
  82   WEBUI_NATS_CLIENT_NAME: 'whisper-webui'
  83 }
  84 
  85 WHISPER_STREAM_ENV = {
  86   ASR_IN_PREFIX: ASR_IN_PREFIX,
  87   ASR_OUT_PREFIX: ASR_OUT_PREFIX,
  88   LINTO_WS_URL: 'ws://192.168.28.134/streaming',
  89   MIN_SPEECH_DURATION_MS: '250',
  90   MIN_SILENCE_DURATION_MS: '500',
  91   MAX_SPEECH_DURATION_S: '30.0',
  92   SPEECH_PAD_MS: '30',
  93   VAD_THRESHOLD: '0.8',
  94   BUFFER_CHECK_INTERVAL_S: '1.0',
  95   STREAM_BRIDGE_ENABLED: '1'
  96 }
  97 
  98 WHISPER_DIARIZATION_ENV = {
  99   DIARIZATION_ENABLED: '1',
 100   DIARIZATION_HTTP_URL: 'http://diarization-models_pyannote_diarization_shared:8080/diarize',
 101   DIARIZATION_HTTP_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_pyannote_shared:7000',
 102   DIARIZATION_HTTP_HOST: '',
 103   DIARIZATION_HEALTH_URL: 'http://web-rtc-komaroff_nats_proxy_pyannote_shared:7000/healthz',
 104   DIARIZATION_HEALTH_HOST: '',
 105   DIARIZATION_HEALTH_TIMEOUT_S: '5',
 106   DIARIZATION_TIMEOUT_S: '1800',
 107   DIARIZATION_RETRY_LIMIT: '1',
 108   DIARIZATION_AUDIO_FORMAT: 'auto',
 109   DIARIZATION_WAV_MAX_UPLOAD_MB: '64',
 110   DIARIZATION_AUDIO_MAX_UPLOAD_MB: '64',
 111   DIARIZATION_NUM_SPEAKERS: '',
 112   DIARIZATION_MIN_SPEAKERS: '2',
 113   DIARIZATION_MAX_SPEAKERS: '8',
 114   DIARIZATION_EXTRA_FIELDS_JSON: '{"runtime":"shared","segmentation_batch_size":"2","embedding_batch_size":"2"}',
 115   DIARIZATION_TURN_BREAK_S: '1.0',
 116   DIARIZATION_SYNTAX_POSTPROCESS_DEFAULT: '0',
 117   DIARIZATION_SYNTAX_SENTENCE_GAP_S: '1.2',
 118   DIARIZATION_SYNTAX_DOMINANCE_THRESHOLD: '0.65'
 119 }
 120 
 121 WHISPER_DIARIZATION_EVENT_ENV = {
 122   ASR_FILE_DIAR_EVENT_PREFIX: 'inference.whisper.file.diar.event.',
 123   ASR_FILE_DIAR_TEXT_EVENT_PREFIX: 'inference.whisper.file.diar_text.event.',
 124   ASR_FILE_DIAR_TEXT_SESSION_EVENT_PREFIX: 'inference.whisper.file.diar_text.session.'
 125 }
 126 
 127 WHISPER_BENCH_CONFIGS_JSON = <<~JSON
 128 [
 129   {
 130     "id": "pyannote_selected_shared_b2_auto",
 131     "case_id": "pyannote_diarization",
 132     "label": "pyannote selected shared batch 2",
 133     "engine": "pyannote",
 134     "audio_format": "auto",
 135     "min_speakers": 2,
 136     "max_speakers": 8,
 137     "diarization_http_url": "http://diarization-models_pyannote_diarization_shared:8080/diarize",
 138     "diarization_http_proxy_url": "http://web-rtc-komaroff_nats_proxy_pyannote_shared:7000",
 139     "diarization_http_host": "",
 140     "diarization_health_url": "http://web-rtc-komaroff_nats_proxy_pyannote_shared:7000/healthz",
 141     "diarization_health_host": "",
 142     "runtime_label": "H100 MIG-750 shared with LinTO",
 143     "segmentation_batch_size": 2,
 144     "embedding_batch_size": 2,
 145     "notes": "Default selected path: shared pyannote service, request-level batch 2, auto compact audio, Russian long-file speaker hints 2-8. Isolated uses its own configured NATS route."
 146   },
 147   {
 148     "id": "pyannote_selected_isolated_b2_auto",
 149     "case_id": "pyannote_diarization",
 150     "runtime": "isolated",
 151     "label": "pyannote isolated batch 2",
 152     "engine": "pyannote",
 153     "audio_format": "auto",
 154     "min_speakers": 2,
 155     "max_speakers": 8,
 156     "diarization_http_url": "http://diarization-models_pyannote_diarization_isolated:8080/diarize",
 157     "diarization_http_proxy_url": "http://web-rtc-komaroff_nats_proxy_pyannote_isolated:7000",
 158     "diarization_http_host": "",
 159     "diarization_health_url": "http://web-rtc-komaroff_nats_proxy_pyannote_isolated:7000/healthz",
 160     "diarization_health_host": "",
 161     "runtime_label": "H100 isolated pyannote",
 162     "segmentation_batch_size": 2,
 163     "embedding_batch_size": 2
 164   },
 165   {
 166     "id": "sherpa_onnx_default_ru_check",
 167     "case_id": "sherpa_onnx_diarization",
 168     "label": "sherpa-onnx default Russian check",
 169     "engine": "sherpa_onnx",
 170     "audio_format": "auto",
 171     "diarization_http_url": "http://diarization-models_sherpa_onnx_diarization:8080/diarize",
 172     "diarization_http_proxy_url": "http://web-rtc-komaroff_nats_proxy_sherpa_onnx:7000",
 173     "diarization_http_host": "",
 174     "diarization_health_url": "http://web-rtc-komaroff_nats_proxy_sherpa_onnx:7000/healthz",
 175     "diarization_health_host": "",
 176     "runtime_label": "ONNX sidecar",
 177     "cluster_threshold": "0.5",
 178     "notes": "Real sherpa-onnx sidecar; used to validate CPU/ONNX throughput and speaker stability on Russian samples."
 179   },
 180   {
 181     "id": "sortformer_very_high_latency_b1_ru_check",
 182     "case_id": "sortformer_diarization",
 183     "label": "Sortformer very-high-latency batch 1 Russian check",
 184     "engine": "nemo_sortformer",
 185     "audio_format": "auto",
 186     "diarization_http_url": "http://diarization-models_sortformer_diarization:8080/diarize",
 187     "diarization_http_proxy_url": "http://web-rtc-komaroff_nats_proxy_sortformer:7000",
 188     "diarization_http_host": "",
 189     "diarization_health_url": "http://web-rtc-komaroff_nats_proxy_sortformer:7000/healthz",
 190     "diarization_health_host": "",
 191     "runtime_label": "H100 3g.47GB MIG shared with vLLM",
 192     "sortformer_profile": "very_high_latency",
 193     "batch_size": 1,
 194     "num_workers": 0,
 195     "chunk_len": 340,
 196     "chunk_right_context": 40,
 197     "fifo_len": 40,
 198     "spkcache_update_period": 300,
 199     "spkcache_len": 188,
 200     "sortformer_window_s": 900,
 201     "sortformer_overlap_s": 15,
 202     "max_speakers": 4,
 203     "notes": "Real NeMo Sortformer candidate for speed; very-high-latency profile, max four speakers, long files are chunked with overlap and Russian quality must be checked locally."
 204   }
 205 ]
 206 JSON
 207 
 208 LINTO_PROXY_SERVICE_ID = 'srv-gismaster-webrtc-linto-01'
 209 PYANNOTE_SHARED_PROXY_SERVICE_ID = 'srv-gismaster-webrtc-pyannote-shared-01'
 210 PYANNOTE_ISOLATED_PROXY_SERVICE_ID = 'srv-gismaster-webrtc-pyannote-isolated-01'
 211 SHERPA_ONNX_PROXY_SERVICE_ID = 'srv-gismaster-webrtc-sherpa-onnx-01'
 212 SORTFORMER_PROXY_SERVICE_ID = 'srv-gismaster-webrtc-sortformer-01'
 213 
 214 # Internal HTTP requesters for H100 model backends. They have no ingress or
 215 # published host ports; requests cross hosts through the existing inference
 216 # NATS/leafnode path and terminate at matching H100-side receivers.
 217 Service :nats_proxy_linto, image: 'dtorry/nats-proxy:latest', ports: 7000 do
 218   env NATS_URL: NATS_URL
 219   env NATS_STREAM: 'inference'
 220   env NATS_RESPONSE_TIMEOUT: '1800'
 221   env STREAM_RESPONSE_TIMEOUT: '1800'
 222   env SERVICE_ID: LINTO_PROXY_SERVICE_ID
 223   env SERVICE_ROLE: 'requester'
 224   env NATS_REQUEST_SUBJECT_ROOT: 'to.inference.linto'
 225   env NATS_RESPONSE_SUBJECT_ROOT: 'from.inference.linto'
 226   env NATS_CONSUMER_NAME: "linto-gateway-#{LINTO_PROXY_SERVICE_ID}"
 227   env NATS_JS_API_PREFIX: 'JS.INF.API'
 228   # Cross-account stream imports do not return the JetStream publish ACK.
 229   # The H100 receiver still consumes the request through JetStream.
 230   env NATS_MODE: 'core'
 231   env PROXY_AUTH_ENABLED: 'false'
 232 end
 233 
 234 Service :nats_proxy_pyannote_shared, image: 'dtorry/nats-proxy:latest', ports: 7000 do
 235   env NATS_URL: NATS_URL
 236   env NATS_STREAM: 'inference'
 237   env NATS_RESPONSE_TIMEOUT: '1800'
 238   env STREAM_RESPONSE_TIMEOUT: '1800'
 239   env SERVICE_ID: PYANNOTE_SHARED_PROXY_SERVICE_ID
 240   env SERVICE_ROLE: 'requester'
 241   env NATS_REQUEST_SUBJECT_ROOT: 'to.inference.pyannote.shared'
 242   env NATS_RESPONSE_SUBJECT_ROOT: 'from.inference.pyannote.shared'
 243   env NATS_CONSUMER_NAME: "pyannote-shared-gateway-#{PYANNOTE_SHARED_PROXY_SERVICE_ID}"
 244   env NATS_JS_API_PREFIX: 'JS.INF.API'
 245   # The request is transient and the H100 receiver remains the durable
 246   # JetStream consumer. Core mode avoids waiting for a cross-account publish
 247   # ACK before the already-running receiver can answer.
 248   env NATS_MODE: 'core'
 249   env PROXY_AUTH_ENABLED: 'false'
 250 end
 251 
 252 Service :nats_proxy_pyannote_isolated, image: 'dtorry/nats-proxy:latest' do
 253   env NATS_URL: NATS_URL
 254   env NATS_STREAM: 'inference'
 255   env NATS_RESPONSE_TIMEOUT: '1800'
 256   env STREAM_RESPONSE_TIMEOUT: '1800'
 257   env SERVICE_ID: PYANNOTE_ISOLATED_PROXY_SERVICE_ID
 258   env SERVICE_ROLE: 'requester'
 259   env NATS_REQUEST_SUBJECT_ROOT: 'to.inference.pyannote.isolated'
 260   env NATS_RESPONSE_SUBJECT_ROOT: 'from.inference.pyannote.isolated'
 261   env NATS_CONSUMER_NAME: "pyannote-isolated-gateway-#{PYANNOTE_ISOLATED_PROXY_SERVICE_ID}"
 262   env NATS_JS_API_PREFIX: 'JS.INF.API'
 263   env NATS_MODE: 'core'
 264   env PROXY_AUTH_ENABLED: 'false'
 265 end
 266 
 267 Service :nats_proxy_sherpa_onnx, image: 'dtorry/nats-proxy:latest' do
 268   env NATS_URL: NATS_URL
 269   env NATS_STREAM: 'inference'
 270   env NATS_RESPONSE_TIMEOUT: '1800'
 271   env STREAM_RESPONSE_TIMEOUT: '1800'
 272   env SERVICE_ID: SHERPA_ONNX_PROXY_SERVICE_ID
 273   env SERVICE_ROLE: 'requester'
 274   env NATS_REQUEST_SUBJECT_ROOT: 'to.inference.sherpa.onnx'
 275   env NATS_RESPONSE_SUBJECT_ROOT: 'from.inference.sherpa.onnx'
 276   env NATS_CONSUMER_NAME: "sherpa-onnx-gateway-#{SHERPA_ONNX_PROXY_SERVICE_ID}"
 277   env NATS_JS_API_PREFIX: 'JS.INF.API'
 278   env NATS_MODE: 'core'
 279   env PROXY_AUTH_ENABLED: 'false'
 280 end
 281 
 282 Service :nats_proxy_sortformer, image: 'dtorry/nats-proxy:latest' do
 283   env NATS_URL: NATS_URL
 284   env NATS_STREAM: 'inference'
 285   env NATS_RESPONSE_TIMEOUT: '1800'
 286   env STREAM_RESPONSE_TIMEOUT: '1800'
 287   env SERVICE_ID: SORTFORMER_PROXY_SERVICE_ID
 288   env SERVICE_ROLE: 'requester'
 289   env NATS_REQUEST_SUBJECT_ROOT: 'to.inference.sortformer'
 290   env NATS_RESPONSE_SUBJECT_ROOT: 'from.inference.sortformer'
 291   env NATS_CONSUMER_NAME: "sortformer-gateway-#{SORTFORMER_PROXY_SERVICE_ID}"
 292   env NATS_JS_API_PREFIX: 'JS.INF.API'
 293   env NATS_MODE: 'core'
 294   env PROXY_AUTH_ENABLED: 'false'
 295 end
 296 
 297 PublishPorts core: [
 298   { target: 19303, published: 19303, protocol: 'udp', mode: 'host' }
 299 ]
 300 
 301 # Core backend for browser sessions: handles /core, WebRTC-facing orchestration,
 302 # and coordinates agent / ASR requests through NATS and the API gateway.
 303 Service :src_core, image: webrtc_registry['core']do
 304   ingress host: HOST, path: '/core', port: 8000
 305 
 306   env NATS_URL: NATS_URL,
 307       NATS_EVENTS_SUBJECT: NATS_EVENTS_SUBJECT,
 308       NATS_AGENT_SUBJECT: NATS_AGENT_SUBJECT,
 309       ASR_IN_PREFIX: ASR_IN_PREFIX,
 310       ASR_OUT_PREFIX: ASR_OUT_PREFIX,
 311       API_URL: 'http://web-rtc-komaroff_src_api_gateway:8100/api',
 312       NATS_REQUEST_TIMEOUT: NATS_REQUEST_TIMEOUT,
 313       STACK_SERVICE_NAME: 'src_core',
 314       USER_ID: USER_ID,
 315       CORE_HOST: '0.0.0.0',
 316       CORE_PORT: 8000
 317 end
 318 
 319 # Internal REST facade for domain data used by core/agent/runtime services.
 320 Service :src_api_gateway, image: webrtc_registry['api-gateway']do
 321   ingress host: HOST, path: '/api', port: 8100
 322 
 323   env API_HOST: '0.0.0.0',
 324       API_PORT: 8100,
 325       NATS_URL: NATS_URL,
 326       NATS_TOOLS_PREFIX: NATS_TOOLS_PREFIX
 327 end
 328 
 329 # Main agent runtime: consumes agent requests from NATS and calls tools/workflows/LLM.
 330 Service :src_agent, image: webrtc_registry['agent'] do
 331   env STACK_SERVICE_NAME: 'src_agent',
 332       USER_ID: USER_ID,
 333       NATS_URL: NATS_URL,
 334       NATS_EVENTS_SUBJECT: NATS_EVENTS_SUBJECT,
 335       NATS_AGENT_SUBJECT: NATS_AGENT_SUBJECT,
 336       NATS_LLM_SUBJECT: NATS_LLM_SUBJECT,
 337       NATS_WORKFLOW_RUN_SUBJECT: NATS_WORKFLOW_RUN_SUBJECT,
 338       WORKFLOW_TIMEOUT_SECONDS: '270', # Two sequential LLM calls (120s each) plus tool/transport headroom.
 339       AGENT_MAX_STEPS: AGENT_MAX_STEPS,
 340       MAX_STEPS: MAX_STEPS,
 341       TIMEOUT_SECONDS: TIMEOUT_SECONDS,
 342       DATABASE_URL: DATABASE_URL,
 343       API_URL: 'http://web-rtc-komaroff_src_api_gateway:8100/api',
 344       AIRPORTS_API_URL: 'http://web-rtc-komaroff_src_api_gateway:8100/api/airports/search',
 345       RUNWAYS_API_URL: 'http://web-rtc-komaroff_src_api_gateway:8100/api/airports',
 346       PILOT_API_URL: 'http://web-rtc-komaroff_src_api_gateway:8100/api/pilot/location',
 347       ROUTES_API_URL: 'http://web-rtc-komaroff_src_api_gateway:8100/api/routes/nearest',
 348       WEATHER_API_URL: 'http://web-rtc-komaroff_src_api_gateway:8100/api/weather',
 349       FLIGHTS_API_URL: 'http://web-rtc-komaroff_src_api_gateway:8100/api/flights/status'
 350 end
 351 
 352 # LLM adapter service for the agent layer. In current config it points at remote Ollama.
 353 Service :src_llm, image: webrtc_registry['llm'] do
 354   env STACK_SERVICE_NAME: 'src_llm',
 355       USER_ID: USER_ID,
 356       NATS_URL: NATS_URL,
 357       NATS_EVENTS_SUBJECT: NATS_EVENTS_SUBJECT,
 358       NATS_LLM_SUBJECT: NATS_LLM_SUBJECT,
 359       OLLAMA_URL: 'https://nats-proxy-ollama.gis-master.ru',
 360       LLM_MODE: 'remote',
 361       LLM_LOCAL_MODEL: 'Qwen/Qwen3-1.7B-GGUF',
 362       LLM_REMOTE_MODEL: 'qwen3:30b',
 363       LLM_MODELS: '/app/models/llm',
 364       OLLAMA_MODEL_FILE: '',
 365       LLM_CONTEXT_SIZE: '8192'
 366   volume name: 'LLM_MODELS', target: '/app/models/llm'
 367 end
 368 
 369 # Python workflow runner. Used only when WORKFLOW_RUNTIME selects the python subjects.
 370 Service :src_langgraph, image: webrtc_registry['langgraph'] do
 371   env STACK_SERVICE_NAME: 'src_langgraph',
 372       USER_ID: USER_ID,
 373       NATS_URL: NATS_URL,
 374       NATS_LLM_SUBJECT: NATS_LLM_SUBJECT,
 375       NATS_TOOLS_PREFIX: NATS_TOOLS_PREFIX,
 376       NATS_WORKFLOW_RUN_SUBJECT: 'nats.workflow.run.python',
 377       NATS_WORKFLOW_HEALTH_SUBJECT: 'nats.workflow.health.python',
 378       NATS_REQUEST_TIMEOUT_SECONDS: '120',
 379       MAX_CONCURRENCY: '8'
 380 end
 381 
 382 # Ruby workflow runner. Used only when WORKFLOW_RUNTIME selects the ruby subjects.
 383 Service :src_langgraph_rb, image: webrtc_registry['langgraph-rb'] do
 384   env STACK_SERVICE_NAME: 'src_langgraph_rb',
 385       USER_ID: USER_ID,
 386       NATS_URL: NATS_URL,
 387       NATS_LLM_SUBJECT: NATS_LLM_SUBJECT,
 388       NATS_TOOLS_PREFIX: NATS_TOOLS_PREFIX,
 389       NATS_WORKFLOW_RUN_SUBJECT: 'nats.workflow.run.ruby',
 390       NATS_WORKFLOW_HEALTH_SUBJECT: 'nats.workflow.health.ruby',
 391       NATS_REQUEST_TIMEOUT_SECONDS: '120'
 392 
 393   command ['bundle', 'exec', 'ruby', './lib/src_langgraph_rb/runtime/service.rb']
 394 end
 395 
 396 # Ruby+Node workflow runner. This is the active path when WORKFLOW_RUNTIME=ruby_node.
 397 Service :src_langgraph_rb_node, image: webrtc_registry['langgraph-rb-node'] do
 398   env STACK_SERVICE_NAME: 'src_langgraph_rb_node',
 399       USER_ID: USER_ID,
 400       NATS_URL: NATS_URL,
 401       NATS_LLM_SUBJECT: NATS_LLM_SUBJECT,
 402       NATS_TOOLS_PREFIX: NATS_TOOLS_PREFIX,
 403       NATS_WORKFLOW_RUN_SUBJECT: 'nats.workflow.run.ruby.node',
 404       NATS_WORKFLOW_HEALTH_SUBJECT: 'nats.workflow.health.ruby.node',
 405       NATS_REQUEST_TIMEOUT_SECONDS: '120'
 406 
 407   command ['bundle', 'exec', 'ruby', './lib/src_langgraph_rb_node/runtime/service.rb']
 408 end
 409 
 410 # Browser frontend for the voice chat app. Serves UI and talks to core/NATS.
 411 Service :src_front, image: webrtc_registry['front'] do
 412   ingress host: HOST, port: 8000
 413 
 414   env FRONT_NATS_EVENTS_SUBJECT: "#{NATS_EVENTS_SUBJECT}#{USER_ID}",
 415       FRONT_NATS_AGENT_SUBJECT: "#{NATS_AGENT_SUBJECT}#{USER_ID}",
 416       FRONT_NATS_WS_UPSTREAM: 'web-rtc-komaroff_audio_nats',
 417       FRONT_NATS_USER: NATS_USER,
 418       FRONT_NATS_PASS: NATS_PASS,
 419       FRONT_NATS_CLIENT_NAME: 'src_front',
 420       FRONT_CORE_ORIGIN: 'http://web-rtc-komaroff_src_core:8000',
 421       COM_GITLAB_CI_PIPELINE_IID: ENV['CI_PIPELINE_ID'].to_s,
 422       FRONT_MESSAGE_REQUEST_TIMEOUT_MS: '300000' # Longer than the Agent workflow deadline.
 423 end
 424 
 425 # Persistent PostgreSQL for MCP / workflow / application state.
 426 Service :src_postgres, image: webrtc_registry['postgres'] do
 427   env POSTGRES_DB: POSTGRES_DB,
 428       POSTGRES_USER: POSTGRES_USER,
 429       POSTGRES_PASSWORD: POSTGRES_PASSWORD
 430 
 431   volume target: '/var/lib/postgresql/data'
 432 
 433   # оставляем твой параметр
 434   command ['postgres', '--wal_level=logical']
 435 
 436   ingress host_sni: "db.#{HOST}", port: 5432, entrypoints: 'postgres'
 437 end
 438 
 439 
 440 
 441 # GisMaster client
 442 # to.inference.ollama.requests.?? <- json (Ollama Rest API) reply: inference.ollama.responses.INBOX.12312312321
 443 # from.inference.ollama.responses.<id> ?? -> json (Ollama Rest API)
 444 
 445 # to.inference.whisper.stream.<sid> <- chunks
 446 # from.inference.whisper.text.<sid> -> text
 447 
 448 # Browser-facing NATS gateway: exposes /ws and bridges local websocket clients
 449 # into the inference account through a leafnode.
 450 Service :audio_nats, image: 'nats:2.11.6-scratch' do
 451   ingress host: HOST, path: '/ws', path_sub: ['^/ws(.*)', '$1'], port: 9222
 452 
 453   command ['-c', '/nats.conf']
 454   config target: '/nats.conf', file_content: <<~CONF
 455     jetstream: false
 456     websocket: { listen: 0.0.0.0:9222, no_tls: true, compression: true }
 457 
 458     debug: true
 459     trace: true
 460 
 461     accounts: {
 462       local: {
 463         users: [
 464           { user: #{NATS_USER}, password: #{NATS_PASS}, allowed_connection_types: [ STANDARD, WEBSOCKET ] }
 465         ]
 466         exports: [
 467           { stream: "to.inference.>" }
 468         ]
 469         imports: [
 470           { stream: { account: inference, subject: "inference.>" }, to: "from.inference.>" }
 471           { service: { account: inference, subject: "$JS.dinf.API.>" }, to: "JS.INF.API.>" }
 472         ]
 473       }
 474 
 475       inference: {
 476         exports: [
 477           { stream: "inference.>" }
 478           { service: "$JS.dinf.API.>" }
 479         ]
 480         imports: [
 481           { stream: { account: local, subject: "to.inference.>" }, to: "inference.>" }
 482         ]
 483         jetstream: enabled
 484       }
 485     }
 486 
 487     leafnodes: {
 488         remotes: [
 489             # Inference
 490             {
 491                 # url: nats://h100.local:37422,
 492                 url: nats://192.168.28.134:37422,
 493                 account: inference,
 494                 nkey: #{ENV.fetch('WEBRTC_NATS_LEAFNODE_NKEY')}
 495             }
 496         ]
 497     }
 498   CONF
 499 end
 500 
 501 
 502 # Shared guard used by all Whisper upload/transcription/diarization pages. The UI
 503 # polls it, and each backend runner also enforces it before model work starts.
 504 Service :whisper_file_job_lock, image: webrtc_registry['voice-chat/stt_whisper_to_nats'] do
 505   env HOST_SERVING_HOST: '0.0.0.0',
 506       HOST_SERVING_PORT: '8091',
 507       FILE_JOB_LOCK_TTL_S: '7200',
 508       STACK_SERVICE_NAME: 'whisper_file_job_lock'
 509 
 510   entrypoint 'python -m src.webui.job_lock_server'
 511 end
 512 
 513 # Plain transcription service. Same binary as py_faster_whisper_diarize, but this
 514 # deployment serves /whisper-trasncription, keeps the live stream bridge on, and disables diarization.
 515 Service :py_faster_whisper, image: webrtc_registry['voice-chat/stt_whisper_to_nats'] do
 516   ingress host: HOST, path: '/whisper-trasncription', port: 8090
 517 
 518   env WHISPER_FILE_JOB_ENV.merge(WHISPER_WEBUI_ENV).merge(WHISPER_STREAM_ENV).merge(
 519         DIARIZATION_ENABLED: '0',
 520         WEBUI_BASE_PATH: '/whisper-trasncription',
 521         WEBUI_PAGE_MODE: 'transcribe_only',
 522         STACK_SERVICE_NAME: 'stt_whisper_to_nats'
 523       )
 524 
 525   entrypoint 'python -m src.stt_whisper_to_nats'
 526 end
 527 
 528 # Combined file ASR + speaker separation service. Same binary as py_faster_whisper,
 529 # but this deployment serves /whisper-diarization, runs file-mode only, and enables diarization.
 530 Service :py_faster_whisper_diarize, image: webrtc_registry['voice-chat/stt_whisper_to_nats'] do
 531   ingress host: HOST, path: '/whisper-diarization', port: 8090
 532 
 533   env WHISPER_FILE_JOB_ENV.merge(WHISPER_WEBUI_ENV).merge(WHISPER_DIARIZATION_EVENT_ENV).merge(WHISPER_DIARIZATION_ENV).merge(
 534         STREAM_BRIDGE_ENABLED: '0',
 535         WEBUI_BASE_PATH: '/whisper-diarization',
 536         WEBUI_PAGE_MODE: 'diarize',
 537         WEBUI_HOME_LINK_PATH: '/whisper-trasncription',
 538         STACK_SERVICE_NAME: 'stt_whisper_to_nats_diarize'
 539       )
 540 
 541   entrypoint 'python -m src.stt_whisper_to_nats'
 542 end
 543 
 544 # Staged file ASR + speaker separation analysis service. Same binary as the other
 545 # py_faster_whisper deployments, but this deployment serves /whisper-staged and
 546 # exposes isolated Transcribe / Diarize / Combine steps on cached artifacts.
 547 Service :py_faster_whisper_staged, image: webrtc_registry['voice-chat/stt_whisper_to_nats'] do
 548   ingress host: HOST, path: '/whisper-staged', port: 8090
 549 
 550   env WHISPER_FILE_JOB_ENV.merge(WHISPER_WEBUI_ENV).merge(WHISPER_DIARIZATION_EVENT_ENV).merge(WHISPER_DIARIZATION_ENV).merge(
 551         STREAM_BRIDGE_ENABLED: '0',
 552         WEBUI_BASE_PATH: '/whisper-staged',
 553         WEBUI_PAGE_MODE: 'staged',
 554         WEBUI_HOME_LINK_PATH: '/whisper-diarization',
 555         STACK_SERVICE_NAME: 'stt_whisper_to_nats_staged'
 556       )
 557 
 558   entrypoint 'python -m src.stt_whisper_to_nats'
 559 end
 560 
 561 # Benchmark UI for non-streaming transcription + diarization experiments. It
 562 # keeps results separate from /whisper-staged while using the same deployed image.
 563 Service :py_faster_whisper_bench, image: webrtc_registry['voice-chat/stt_whisper_to_nats'] do
 564   ingress host: HOST, path: '/whisper-bench', port: 8090
 565 
 566   env WHISPER_FILE_JOB_ENV.merge(WHISPER_WEBUI_ENV).merge(WHISPER_DIARIZATION_EVENT_ENV).merge(WHISPER_DIARIZATION_ENV).merge(
 567         STREAM_BRIDGE_ENABLED: '0',
 568         WEBUI_BASE_PATH: '/whisper-bench',
 569         WEBUI_PAGE_MODE: 'bench',
 570         WEBUI_HOME_LINK_PATH: '/whisper-staged',
 571         FILE_JOB_STORAGE_DIR: 'локальный источник;,
 572         BENCH_CONFIGS_JSON: WHISPER_BENCH_CONFIGS_JSON,
 573         STACK_SERVICE_NAME: 'stt_whisper_to_nats_bench'
 574       )
 575 
 576   volume name: 'whisper_bench_jobs', target: 'локальный источник;
 577   entrypoint 'python -m src.stt_whisper_to_nats'
 578 end
 579 
 580 # Main page for all tried non-streaming transcription + diarization approaches.
 581 # The case pages under /whisper-summary/cases/<case-id> reuse the same
 582 # runner and result store as /whisper-bench, so measured stats stay comparable.
 583 Service :py_faster_whisper_summary, image: webrtc_registry['voice-chat/stt_whisper_to_nats'] do
 584   ingress host: HOST, path: '/whisper-summary', port: 8090
 585 
 586   env WHISPER_FILE_JOB_ENV.merge(WHISPER_WEBUI_ENV).merge(WHISPER_DIARIZATION_EVENT_ENV).merge(WHISPER_DIARIZATION_ENV).merge(
 587         STREAM_BRIDGE_ENABLED: '0',
 588         WEBUI_BASE_PATH: '/whisper-summary',
 589         WEBUI_PAGE_MODE: 'summary',
 590         WEBUI_HOME_LINK_PATH: '/whisper-summary',
 591         FILE_JOB_STORAGE_DIR: 'локальный источник;,
 592         BENCH_CONFIGS_JSON: WHISPER_BENCH_CONFIGS_JSON,
 593         STACK_SERVICE_NAME: 'stt_whisper_to_nats_summary'
 594       )
 595 
 596   volume name: 'whisper_bench_jobs', target: 'локальный источник;
 597   entrypoint 'python -m src.stt_whisper_to_nats'
 598 end
 599 
 600 # Fast browser page for checking that the voice-chat runtime and its rag-stack
 601 # dependencies are alive enough for the real transcription/diarization flows.
 602 Service :py_faster_whisper_status, image: webrtc_registry['voice-chat/stt_whisper_to_nats'] do
 603   ingress host: HOST, path: '/status', port: 8090
 604   ingress host: HOST, path: '/health', path_sub: ['^/health(.*)', '/status$1'], port: 8090
 605   deploy_label 'traefik.http.routers.web-rtc-komaroff_py_faster_whisper_status-0.priority=1000'
 606   deploy_label 'traefik.http.routers.web-rtc-komaroff_py_faster_whisper_status-1.priority=1000'
 607 
 608   env WHISPER_FILE_JOB_ENV.merge(WHISPER_WEBUI_ENV).merge(WHISPER_DIARIZATION_ENV).merge(
 609         STREAM_BRIDGE_ENABLED: '0',
 610         WEBUI_FILE_TRANSCRIBE_ENABLED: '0',
 611         WEBUI_BASE_PATH: '/status',
 612         WEBUI_PAGE_MODE: 'diagnostics',
 613         WEBUI_HOME_LINK_PATH: '/status',
 614         WEBUI_NATS_CLIENT_NAME: 'voice-chat-diagnostics',
 615         STACK_SERVICE_NAME: 'voice_chat_status',
 616         DIAGNOSTICS_NATS_CLIENT_NAME: 'voice-chat-diagnostics',
 617         NATS_AGENT_HISTORY_SUBJECT: NATS_AGENT_HISTORY_SUBJECT,
 618         NATS_WORKFLOW_HEALTH_SUBJECT: NATS_WORKFLOW_HEALTH_SUBJECT,
 619         NATS_LLM_SUBJECT: NATS_LLM_SUBJECT,
 620         DIAGNOSTICS_TIMEOUT_S: '5',
 621         DIAGNOSTICS_SMOKE_TIMEOUT_S: '20',
 622         DIAGNOSTICS_FRONT_URL: 'http://web-rtc-komaroff_src_front:8000/',
 623         DIAGNOSTICS_CORE_URL: 'http://web-rtc-komaroff_src_core:8000/core',
 624         DIAGNOSTICS_API_GATEWAY_URL: 'http://web-rtc-komaroff_src_api_gateway:8100/api/pilot/location',
 625         DIAGNOSTICS_FILE_JOB_LOCK_URL: 'http://web-rtc-komaroff_whisper_file_job_lock:8091/health',
 626         DIAGNOSTICS_WHISPER_TRANSCRIBE_URL: 'http://web-rtc-komaroff_py_faster_whisper:8090/whisper-trasncription/health',
 627         DIAGNOSTICS_WHISPER_DIARIZE_URL: 'http://web-rtc-komaroff_py_faster_whisper_diarize:8090/whisper-diarization/health',
 628         DIAGNOSTICS_WHISPER_STAGED_URL: 'http://web-rtc-komaroff_py_faster_whisper_staged:8090/whisper-staged/health',
 629         DIAGNOSTICS_WHISPER_BENCH_URL: 'http://web-rtc-komaroff_py_faster_whisper_bench:8090/whisper-bench/health',
 630         DIAGNOSTICS_WHISPER_SUMMARY_URL: 'http://web-rtc-komaroff_py_faster_whisper_summary:8090/whisper-summary/health',
 631         # Absolute-form HTTP proxy requests reach LinTO on H100; a direct
 632         # /healthcheck on nats-proxy only checks the proxy's own Rack process.
 633         DIAGNOSTICS_LINTO_HTTP_HEALTH_URL: 'http://llm-models_linto_stt_whisper_http:80/healthcheck',
 634         DIAGNOSTICS_LINTO_HTTP_HEALTH_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_linto:7000',
 635         DIAGNOSTICS_PYANNOTE_SHARED_HEALTH_URL: 'http://web-rtc-komaroff_nats_proxy_pyannote_shared:7000/healthz',
 636         DIAGNOSTICS_PYANNOTE_SHARED_HEALTH_HOST: '',
 637         DIAGNOSTICS_PYANNOTE_SHARED_DIARIZE_URL: 'http://diarization-models_pyannote_diarization_shared:8080/diarize',
 638         DIAGNOSTICS_PYANNOTE_SHARED_DIARIZE_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_pyannote_shared:7000',
 639         DIAGNOSTICS_PYANNOTE_SHARED_DIARIZE_HOST: '',
 640         DIAGNOSTICS_PYANNOTE_ISOLATED_HEALTH_URL: 'http://diarization-models_pyannote_diarization_isolated:8080/healthz',
 641         DIAGNOSTICS_PYANNOTE_ISOLATED_HEALTH_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_pyannote_isolated:7000',
 642         DIAGNOSTICS_PYANNOTE_ISOLATED_DIARIZE_URL: 'http://diarization-models_pyannote_diarization_isolated:8080/diarize',
 643         DIAGNOSTICS_PYANNOTE_ISOLATED_DIARIZE_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_pyannote_isolated:7000',
 644         DIAGNOSTICS_SHERPA_ONNX_HEALTH_URL: 'http://diarization-models_sherpa_onnx_diarization:8080/healthz',
 645         DIAGNOSTICS_SHERPA_ONNX_HEALTH_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_sherpa_onnx:7000',
 646         DIAGNOSTICS_SHERPA_ONNX_DIARIZE_URL: 'http://diarization-models_sherpa_onnx_diarization:8080/diarize',
 647         DIAGNOSTICS_SHERPA_ONNX_DIARIZE_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_sherpa_onnx:7000',
 648         DIAGNOSTICS_SORTFORMER_HEALTH_URL: 'http://diarization-models_sortformer_diarization:8080/healthz',
 649         DIAGNOSTICS_SORTFORMER_HEALTH_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_sortformer:7000',
 650         DIAGNOSTICS_SORTFORMER_DIARIZE_URL: 'http://diarization-models_sortformer_diarization:8080/diarize',
 651         DIAGNOSTICS_SORTFORMER_DIARIZE_PROXY_URL: 'http://web-rtc-komaroff_nats_proxy_sortformer:7000',
 652         DIAGNOSTICS_H100_INGRESS_INFORMATIONAL: '1'
 653       )
 654 
 655   entrypoint 'python -m src.stt_whisper_to_nats'
 656 end
 657 
 658 
 659 
 660 # Debug shell for manual NATS inspection inside the stack.
 661 Service :nats_box, image: 'natsio/nats-box' do
 662   env NATS_URL: NATS_URL
 663   entrypoint 'sh -c "sleep infinity"'
 664 end
 665 
 666 # Admin UI for inspecting NATS subjects/connections/messages.
 667 Service :nats_ui, image: registry['nats-ui/main:latest'], ports: 7000 do
 668   ingress host: "ns.#{HOST}", basic_auth: ENV.fetch('WEBRTC_NATS_UI_BASIC_AUTH')
 669   env NATS_URL: NATS_URL
 670 end
 671 
 672 # Admin UI for browsing the PostgreSQL database.
 673 Service :pgweb_mcp, image: 'sosedoff/pgweb:0.17.0', env: { PGWEB_DATABASE_URL: DATABASE_URL },
 674         ingress: { host: "pgweb.#{HOST}", port: 8081, basic_auth: ENV.fetch('WEBRTC_PGWEB_BASIC_AUTH') }