@pipedog/core 0.0.0 โ 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/NOTICE +9 -0
- package/dist/adapters/base_llm_adapter.d.ts +266 -0
- package/dist/adapters/base_llm_adapter.js +489 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/schemas/direct_function.d.ts +202 -0
- package/dist/adapters/schemas/direct_function.js +302 -0
- package/dist/adapters/schemas/function_schema.d.ts +67 -0
- package/dist/adapters/schemas/function_schema.js +72 -0
- package/dist/adapters/schemas/index.d.ts +2 -0
- package/dist/adapters/schemas/index.js +8 -0
- package/dist/adapters/schemas/tools_schema.d.ts +57 -0
- package/dist/adapters/schemas/tools_schema.js +79 -0
- package/dist/adapters/services/index.d.ts +2 -0
- package/dist/adapters/services/index.js +8 -0
- package/dist/adapters/services/open_ai_adapter.d.ts +147 -0
- package/dist/adapters/services/open_ai_adapter.js +250 -0
- package/dist/audio/dtmf/dtmf_data.d.ts +3 -0
- package/dist/audio/dtmf/dtmf_data.js +22 -0
- package/dist/audio/dtmf/index.d.ts +2 -0
- package/dist/audio/dtmf/index.js +8 -0
- package/dist/audio/dtmf/types.d.ts +31 -0
- package/dist/audio/dtmf/types.js +35 -0
- package/dist/audio/dtmf/utils.d.ts +13 -0
- package/dist/audio/dtmf/utils.js +62 -0
- package/dist/audio/filters/base_audio_filter.d.ts +51 -0
- package/dist/audio/filters/base_audio_filter.js +17 -0
- package/dist/audio/filters/index.d.ts +2 -0
- package/dist/audio/filters/index.js +8 -0
- package/dist/audio/index.d.ts +2 -0
- package/dist/audio/index.js +8 -0
- package/dist/audio/krisp_instance.d.ts +95 -0
- package/dist/audio/krisp_instance.js +229 -0
- package/dist/audio/mixers/base_audio_mixer.d.ts +56 -0
- package/dist/audio/mixers/base_audio_mixer.js +16 -0
- package/dist/audio/mixers/index.d.ts +2 -0
- package/dist/audio/mixers/index.js +8 -0
- package/dist/audio/resamplers/base_audio_resampler.d.ts +46 -0
- package/dist/audio/resamplers/base_audio_resampler.js +35 -0
- package/dist/audio/resamplers/index.d.ts +2 -0
- package/dist/audio/resamplers/index.js +8 -0
- package/dist/audio/resamplers/resampy_resampler.d.ts +10 -0
- package/dist/audio/resamplers/resampy_resampler.js +15 -0
- package/dist/audio/resamplers/soxr_resampler.d.ts +54 -0
- package/dist/audio/resamplers/soxr_resampler.js +69 -0
- package/dist/audio/resamplers/soxr_stream_resampler.d.ts +75 -0
- package/dist/audio/resamplers/soxr_stream_resampler.js +146 -0
- package/dist/audio/turn/base_turn_analyzer.d.ts +84 -0
- package/dist/audio/turn/base_turn_analyzer.js +61 -0
- package/dist/audio/turn/index.d.ts +2 -0
- package/dist/audio/turn/index.js +8 -0
- package/dist/audio/turn/smart_turn/_whisper_features.d.ts +30 -0
- package/dist/audio/turn/smart_turn/_whisper_features.js +225 -0
- package/dist/audio/turn/smart_turn/base_smart_turn.d.ts +94 -0
- package/dist/audio/turn/smart_turn/base_smart_turn.js +239 -0
- package/dist/audio/turn/smart_turn/local_smart_turn_v3.d.ts +76 -0
- package/dist/audio/turn/smart_turn/local_smart_turn_v3.js +143 -0
- package/dist/audio/utils.d.ts +224 -0
- package/dist/audio/utils.js +634 -0
- package/dist/audio/vad/data/index.d.ts +2 -0
- package/dist/audio/vad/data/index.js +8 -0
- package/dist/audio/vad/index.d.ts +2 -0
- package/dist/audio/vad/index.js +8 -0
- package/dist/audio/vad/vad_analyzer.d.ts +126 -0
- package/dist/audio/vad/vad_analyzer.js +211 -0
- package/dist/audio/vad/vad_controller.d.ts +143 -0
- package/dist/audio/vad/vad_controller.js +228 -0
- package/dist/audio/volume.d.ts +38 -0
- package/dist/audio/volume.js +75 -0
- package/dist/bus/adapters/base.d.ts +35 -0
- package/dist/bus/adapters/base.js +19 -0
- package/dist/bus/adapters/index.d.ts +10 -0
- package/dist/bus/adapters/index.js +15 -0
- package/dist/bus/adapters/llm_context_adapter.d.ts +36 -0
- package/dist/bus/adapters/llm_context_adapter.js +82 -0
- package/dist/bus/adapters/tools_schema_adapter.d.ts +25 -0
- package/dist/bus/adapters/tools_schema_adapter.js +70 -0
- package/dist/bus/bridge_processor.d.ts +111 -0
- package/dist/bus/bridge_processor.js +166 -0
- package/dist/bus/bus.d.ts +110 -0
- package/dist/bus/bus.js +200 -0
- package/dist/bus/index.d.ts +22 -0
- package/dist/bus/index.js +27 -0
- package/dist/bus/local/async_queue.d.ts +12 -0
- package/dist/bus/local/async_queue.js +21 -0
- package/dist/bus/local/index.d.ts +3 -0
- package/dist/bus/local/index.js +8 -0
- package/dist/bus/messages.d.ts +378 -0
- package/dist/bus/messages.js +357 -0
- package/dist/bus/network/index.d.ts +16 -0
- package/dist/bus/network/index.js +21 -0
- package/dist/bus/queue.d.ts +27 -0
- package/dist/bus/queue.js +51 -0
- package/dist/bus/serializers/base.d.ts +25 -0
- package/dist/bus/serializers/base.js +14 -0
- package/dist/bus/serializers/index.d.ts +9 -0
- package/dist/bus/serializers/index.js +14 -0
- package/dist/bus/serializers/json.d.ts +77 -0
- package/dist/bus/serializers/json.js +367 -0
- package/dist/bus/subscriber.d.ts +46 -0
- package/dist/bus/subscriber.js +27 -0
- package/dist/bus/ui/index.d.ts +3 -0
- package/dist/bus/ui/index.js +8 -0
- package/dist/bus/ui/messages.d.ts +186 -0
- package/dist/bus/ui/messages.js +171 -0
- package/dist/classifiers/base_classifier.d.ts +263 -0
- package/dist/classifiers/base_classifier.js +346 -0
- package/dist/classifiers/cloudflare/clef/classifier.d.ts +70 -0
- package/dist/classifiers/cloudflare/clef/classifier.js +204 -0
- package/dist/classifiers/cloudflare/clef/client.d.ts +104 -0
- package/dist/classifiers/cloudflare/clef/client.js +234 -0
- package/dist/classifiers/cloudflare/clef/index.d.ts +2 -0
- package/dist/classifiers/cloudflare/clef/index.js +8 -0
- package/dist/classifiers/cloudflare/index.d.ts +2 -0
- package/dist/classifiers/cloudflare/index.js +8 -0
- package/dist/classifiers/http_client.d.ts +70 -0
- package/dist/classifiers/http_client.js +161 -0
- package/dist/classifiers/index.d.ts +2 -0
- package/dist/classifiers/index.js +8 -0
- package/dist/classifiers/jev/classifier.d.ts +8 -0
- package/dist/classifiers/jev/classifier.js +13 -0
- package/dist/classifiers/jev/client.d.ts +8 -0
- package/dist/classifiers/jev/client.js +13 -0
- package/dist/classifiers/jev/index.d.ts +2 -0
- package/dist/classifiers/jev/index.js +8 -0
- package/dist/classifiers/llm/classifier.d.ts +72 -0
- package/dist/classifiers/llm/classifier.js +304 -0
- package/dist/classifiers/llm/index.d.ts +2 -0
- package/dist/classifiers/llm/index.js +8 -0
- package/dist/classifiers/openai/decisions/classifier.d.ts +77 -0
- package/dist/classifiers/openai/decisions/classifier.js +241 -0
- package/dist/classifiers/openai/decisions/client.d.ts +108 -0
- package/dist/classifiers/openai/decisions/client.js +234 -0
- package/dist/classifiers/openai/decisions/index.d.ts +2 -0
- package/dist/classifiers/openai/decisions/index.js +8 -0
- package/dist/classifiers/openai/index.d.ts +2 -0
- package/dist/classifiers/openai/index.js +8 -0
- package/dist/classifiers/typesafe/index.d.ts +2 -0
- package/dist/classifiers/typesafe/index.js +8 -0
- package/dist/classifiers/typesafe/jev/classifier.d.ts +70 -0
- package/dist/classifiers/typesafe/jev/classifier.js +220 -0
- package/dist/classifiers/typesafe/jev/client.d.ts +105 -0
- package/dist/classifiers/typesafe/jev/client.js +233 -0
- package/dist/classifiers/typesafe/jev/index.d.ts +2 -0
- package/dist/classifiers/typesafe/jev/index.js +8 -0
- package/dist/clocks/base_clock.d.ts +13 -0
- package/dist/clocks/base_clock.js +14 -0
- package/dist/clocks/index.d.ts +2 -0
- package/dist/clocks/index.js +8 -0
- package/dist/clocks/system_clock.d.ts +15 -0
- package/dist/clocks/system_clock.js +25 -0
- package/dist/frames/frames.d.ts +2533 -0
- package/dist/frames/frames.js +2485 -0
- package/dist/frames/index.d.ts +2 -0
- package/dist/frames/index.js +8 -0
- package/dist/frames/protobufs/frames_pb2.d.ts +229 -0
- package/dist/frames/protobufs/frames_pb2.js +466 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +56 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.js +8 -0
- package/dist/metrics/metrics.d.ts +276 -0
- package/dist/metrics/metrics.js +254 -0
- package/dist/observers/base_observer.d.ts +173 -0
- package/dist/observers/base_observer.js +151 -0
- package/dist/observers/error_observer.d.ts +75 -0
- package/dist/observers/error_observer.js +87 -0
- package/dist/observers/function_call_observer.d.ts +157 -0
- package/dist/observers/function_call_observer.js +222 -0
- package/dist/observers/index.d.ts +2 -0
- package/dist/observers/index.js +8 -0
- package/dist/observers/loggers/debug_log_observer.d.ts +91 -0
- package/dist/observers/loggers/debug_log_observer.js +228 -0
- package/dist/observers/loggers/index.d.ts +2 -0
- package/dist/observers/loggers/index.js +8 -0
- package/dist/observers/loggers/llm_log_observer.d.ts +24 -0
- package/dist/observers/loggers/llm_log_observer.js +110 -0
- package/dist/observers/loggers/metrics_log_observer.d.ts +85 -0
- package/dist/observers/loggers/metrics_log_observer.js +186 -0
- package/dist/observers/loggers/transcription_log_observer.d.ts +23 -0
- package/dist/observers/loggers/transcription_log_observer.js +74 -0
- package/dist/observers/service_metrics_observer.d.ts +168 -0
- package/dist/observers/service_metrics_observer.js +236 -0
- package/dist/observers/speaking_observer.d.ts +104 -0
- package/dist/observers/speaking_observer.js +150 -0
- package/dist/observers/startup_timing_observer.d.ts +221 -0
- package/dist/observers/startup_timing_observer.js +365 -0
- package/dist/observers/turn_tracking_observer.d.ts +78 -0
- package/dist/observers/turn_tracking_observer.js +189 -0
- package/dist/observers/user_bot_latency_observer.d.ts +411 -0
- package/dist/observers/user_bot_latency_observer.js +974 -0
- package/dist/pipeline/base_pipeline.d.ts +49 -0
- package/dist/pipeline/base_pipeline.js +101 -0
- package/dist/pipeline/capabilities.d.ts +76 -0
- package/dist/pipeline/capabilities.js +97 -0
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +8 -0
- package/dist/pipeline/job_context.d.ts +317 -0
- package/dist/pipeline/job_context.js +450 -0
- package/dist/pipeline/job_decorator.d.ts +54 -0
- package/dist/pipeline/job_decorator.js +92 -0
- package/dist/pipeline/llm_switcher.d.ts +84 -0
- package/dist/pipeline/llm_switcher.js +107 -0
- package/dist/pipeline/llm_with_backend.d.ts +435 -0
- package/dist/pipeline/llm_with_backend.js +806 -0
- package/dist/pipeline/parallel_pipeline.d.ts +83 -0
- package/dist/pipeline/parallel_pipeline.js +200 -0
- package/dist/pipeline/pipeline.d.ts +126 -0
- package/dist/pipeline/pipeline.js +196 -0
- package/dist/pipeline/service_switcher.d.ts +287 -0
- package/dist/pipeline/service_switcher.js +509 -0
- package/dist/pipeline/sync_parallel_pipeline.d.ts +192 -0
- package/dist/pipeline/sync_parallel_pipeline.js +387 -0
- package/dist/pipeline/worker.d.ts +814 -0
- package/dist/pipeline/worker.js +1639 -0
- package/dist/pipeline/worker_observer.d.ts +131 -0
- package/dist/pipeline/worker_observer.js +274 -0
- package/dist/pipeline/worker_ready_decorator.d.ts +45 -0
- package/dist/pipeline/worker_ready_decorator.js +76 -0
- package/dist/processors/aggregators/async_tool_messages.d.ts +146 -0
- package/dist/processors/aggregators/async_tool_messages.js +291 -0
- package/dist/processors/aggregators/dtmf_aggregator.d.ts +61 -0
- package/dist/processors/aggregators/dtmf_aggregator.js +144 -0
- package/dist/processors/aggregators/gated.d.ts +43 -0
- package/dist/processors/aggregators/gated.js +86 -0
- package/dist/processors/aggregators/gated_llm_context.d.ts +42 -0
- package/dist/processors/aggregators/gated_llm_context.js +86 -0
- package/dist/processors/aggregators/index.d.ts +2 -0
- package/dist/processors/aggregators/index.js +8 -0
- package/dist/processors/aggregators/llm_context.d.ts +370 -0
- package/dist/processors/aggregators/llm_context.js +580 -0
- package/dist/processors/aggregators/llm_context_summarizer.d.ts +219 -0
- package/dist/processors/aggregators/llm_context_summarizer.js +447 -0
- package/dist/processors/aggregators/llm_response.d.ts +37 -0
- package/dist/processors/aggregators/llm_response.js +71 -0
- package/dist/processors/aggregators/llm_response_universal.d.ts +966 -0
- package/dist/processors/aggregators/llm_response_universal.js +2394 -0
- package/dist/processors/aggregators/llm_text_processor.d.ts +53 -0
- package/dist/processors/aggregators/llm_text_processor.js +102 -0
- package/dist/processors/aggregators/sentence.d.ts +39 -0
- package/dist/processors/aggregators/sentence.js +69 -0
- package/dist/processors/async_generator.d.ts +42 -0
- package/dist/processors/async_generator.js +76 -0
- package/dist/processors/audio/audio_buffer_processor.d.ts +241 -0
- package/dist/processors/audio/audio_buffer_processor.js +565 -0
- package/dist/processors/audio/index.d.ts +2 -0
- package/dist/processors/audio/index.js +8 -0
- package/dist/processors/audio/vad_processor.d.ts +67 -0
- package/dist/processors/audio/vad_processor.js +103 -0
- package/dist/processors/consumer_processor.d.ts +49 -0
- package/dist/processors/consumer_processor.js +85 -0
- package/dist/processors/filters/frame_filter.d.ts +39 -0
- package/dist/processors/filters/frame_filter.js +52 -0
- package/dist/processors/filters/function_filter.d.ts +59 -0
- package/dist/processors/filters/function_filter.js +78 -0
- package/dist/processors/filters/identity_filter.d.ts +33 -0
- package/dist/processors/filters/identity_filter.js +37 -0
- package/dist/processors/filters/index.d.ts +2 -0
- package/dist/processors/filters/index.js +8 -0
- package/dist/processors/filters/null_filter.d.ts +34 -0
- package/dist/processors/filters/null_filter.js +47 -0
- package/dist/processors/filters/wake_notifier_filter.d.ts +48 -0
- package/dist/processors/filters/wake_notifier_filter.js +49 -0
- package/dist/processors/frame_processor.d.ts +360 -0
- package/dist/processors/frame_processor.js +932 -0
- package/dist/processors/frameworks/index.d.ts +2 -0
- package/dist/processors/frameworks/index.js +8 -0
- package/dist/processors/frameworks/rtvi/frames.d.ts +241 -0
- package/dist/processors/frameworks/rtvi/frames.js +259 -0
- package/dist/processors/frameworks/rtvi/index.d.ts +6 -0
- package/dist/processors/frameworks/rtvi/index.js +11 -0
- package/dist/processors/frameworks/rtvi/models.d.ts +1741 -0
- package/dist/processors/frameworks/rtvi/models.js +1807 -0
- package/dist/processors/frameworks/rtvi/observer.d.ts +340 -0
- package/dist/processors/frameworks/rtvi/observer.js +1113 -0
- package/dist/processors/frameworks/rtvi/processor.d.ts +182 -0
- package/dist/processors/frameworks/rtvi/processor.js +676 -0
- package/dist/processors/idle_frame_processor.d.ts +44 -0
- package/dist/processors/idle_frame_processor.js +86 -0
- package/dist/processors/index.d.ts +2 -0
- package/dist/processors/index.js +8 -0
- package/dist/processors/logger.d.ts +42 -0
- package/dist/processors/logger.js +87 -0
- package/dist/processors/metrics/frame_processor_metrics.d.ts +172 -0
- package/dist/processors/metrics/frame_processor_metrics.js +372 -0
- package/dist/processors/metrics/index.d.ts +2 -0
- package/dist/processors/metrics/index.js +8 -0
- package/dist/processors/producer_processor.d.ts +68 -0
- package/dist/processors/producer_processor.js +78 -0
- package/dist/processors/text_transformer.d.ts +30 -0
- package/dist/processors/text_transformer.js +48 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +8 -0
- package/dist/registry/registry.d.ts +69 -0
- package/dist/registry/registry.js +149 -0
- package/dist/registry/types.d.ts +47 -0
- package/dist/registry/types.js +39 -0
- package/dist/serializers/base_serializer.d.ts +94 -0
- package/dist/serializers/base_serializer.js +82 -0
- package/dist/serializers/index.d.ts +2 -0
- package/dist/serializers/index.js +8 -0
- package/dist/services/ai_service.d.ts +145 -0
- package/dist/services/ai_service.js +262 -0
- package/dist/services/anthropic/index.d.ts +2 -0
- package/dist/services/anthropic/index.js +7 -0
- package/dist/services/assemblyai/index.d.ts +2 -0
- package/dist/services/assemblyai/index.js +7 -0
- package/dist/services/asyncai/index.d.ts +2 -0
- package/dist/services/asyncai/index.js +7 -0
- package/dist/services/aws/index.d.ts +2 -0
- package/dist/services/aws/index.js +7 -0
- package/dist/services/azure/index.d.ts +2 -0
- package/dist/services/azure/index.js +7 -0
- package/dist/services/baseten/index.d.ts +2 -0
- package/dist/services/baseten/index.js +7 -0
- package/dist/services/bland/index.d.ts +2 -0
- package/dist/services/bland/index.js +7 -0
- package/dist/services/camb/index.d.ts +2 -0
- package/dist/services/camb/index.js +7 -0
- package/dist/services/cartesia/index.d.ts +2 -0
- package/dist/services/cartesia/index.js +7 -0
- package/dist/services/cerebras/index.d.ts +2 -0
- package/dist/services/cerebras/index.js +7 -0
- package/dist/services/crusoe/index.d.ts +2 -0
- package/dist/services/crusoe/index.js +7 -0
- package/dist/services/deepgram/index.d.ts +2 -0
- package/dist/services/deepgram/index.js +7 -0
- package/dist/services/deepseek/index.d.ts +2 -0
- package/dist/services/deepseek/index.js +7 -0
- package/dist/services/elevenlabs/index.d.ts +2 -0
- package/dist/services/elevenlabs/index.js +7 -0
- package/dist/services/fal/index.d.ts +2 -0
- package/dist/services/fal/index.js +7 -0
- package/dist/services/fireworks/index.d.ts +2 -0
- package/dist/services/fireworks/index.js +7 -0
- package/dist/services/fish/index.d.ts +2 -0
- package/dist/services/fish/index.js +7 -0
- package/dist/services/funasr/index.d.ts +3 -0
- package/dist/services/funasr/index.js +8 -0
- package/dist/services/gladia/index.d.ts +2 -0
- package/dist/services/gladia/index.js +7 -0
- package/dist/services/google/index.d.ts +2 -0
- package/dist/services/google/index.js +7 -0
- package/dist/services/gradium/index.d.ts +2 -0
- package/dist/services/gradium/index.js +7 -0
- package/dist/services/grok/index.d.ts +2 -0
- package/dist/services/grok/index.js +7 -0
- package/dist/services/groq/index.d.ts +2 -0
- package/dist/services/groq/index.js +7 -0
- package/dist/services/heygen/index.d.ts +2 -0
- package/dist/services/heygen/index.js +7 -0
- package/dist/services/hume/index.d.ts +2 -0
- package/dist/services/hume/index.js +7 -0
- package/dist/services/image_service.d.ts +52 -0
- package/dist/services/image_service.js +60 -0
- package/dist/services/inception/index.d.ts +2 -0
- package/dist/services/inception/index.js +7 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +8 -0
- package/dist/services/inworld/index.d.ts +2 -0
- package/dist/services/inworld/index.js +7 -0
- package/dist/services/keenable/index.d.ts +2 -0
- package/dist/services/keenable/index.js +7 -0
- package/dist/services/kokoro/index.d.ts +2 -0
- package/dist/services/kokoro/index.js +7 -0
- package/dist/services/llm_service.d.ts +1106 -0
- package/dist/services/llm_service.js +2231 -0
- package/dist/services/mem0/index.d.ts +2 -0
- package/dist/services/mem0/index.js +7 -0
- package/dist/services/meta/index.d.ts +2 -0
- package/dist/services/meta/index.js +7 -0
- package/dist/services/minimax/index.d.ts +2 -0
- package/dist/services/minimax/index.js +7 -0
- package/dist/services/mistral/index.d.ts +2 -0
- package/dist/services/mistral/index.js +7 -0
- package/dist/services/moondream/index.d.ts +2 -0
- package/dist/services/moondream/index.js +7 -0
- package/dist/services/moonshine/index.d.ts +2 -0
- package/dist/services/moonshine/index.js +7 -0
- package/dist/services/nebius/index.d.ts +2 -0
- package/dist/services/nebius/index.js +7 -0
- package/dist/services/neuphonic/index.d.ts +2 -0
- package/dist/services/neuphonic/index.js +7 -0
- package/dist/services/novita/index.d.ts +2 -0
- package/dist/services/novita/index.js +7 -0
- package/dist/services/nvidia/index.d.ts +2 -0
- package/dist/services/nvidia/index.js +11 -0
- package/dist/services/ollama/index.d.ts +2 -0
- package/dist/services/ollama/index.js +7 -0
- package/dist/services/openai/index.d.ts +2 -0
- package/dist/services/openai/index.js +7 -0
- package/dist/services/openclaw/index.d.ts +14 -0
- package/dist/services/openclaw/index.js +19 -0
- package/dist/services/openrouter/index.d.ts +2 -0
- package/dist/services/openrouter/index.js +7 -0
- package/dist/services/perplexity/index.d.ts +2 -0
- package/dist/services/perplexity/index.js +7 -0
- package/dist/services/piper/index.d.ts +2 -0
- package/dist/services/piper/index.js +7 -0
- package/dist/services/pocket_tts/index.d.ts +2 -0
- package/dist/services/pocket_tts/index.js +7 -0
- package/dist/services/qwen/index.d.ts +2 -0
- package/dist/services/qwen/index.js +7 -0
- package/dist/services/resembleai/index.d.ts +2 -0
- package/dist/services/resembleai/index.js +7 -0
- package/dist/services/rime/index.d.ts +2 -0
- package/dist/services/rime/index.js +7 -0
- package/dist/services/sambanova/index.d.ts +2 -0
- package/dist/services/sambanova/index.js +7 -0
- package/dist/services/sarvam/index.d.ts +2 -0
- package/dist/services/sarvam/index.js +7 -0
- package/dist/services/settings.d.ts +356 -0
- package/dist/services/settings.js +437 -0
- package/dist/services/simli/index.d.ts +2 -0
- package/dist/services/simli/index.js +7 -0
- package/dist/services/smallest/index.d.ts +2 -0
- package/dist/services/smallest/index.js +7 -0
- package/dist/services/soniox/index.d.ts +2 -0
- package/dist/services/soniox/index.js +7 -0
- package/dist/services/speechify/index.d.ts +2 -0
- package/dist/services/speechify/index.js +7 -0
- package/dist/services/speechmatics/index.d.ts +2 -0
- package/dist/services/speechmatics/index.js +7 -0
- package/dist/services/stt_latency.d.ts +62 -0
- package/dist/services/stt_latency.js +71 -0
- package/dist/services/stt_service.d.ts +601 -0
- package/dist/services/stt_service.js +1044 -0
- package/dist/services/tavus/index.d.ts +2 -0
- package/dist/services/tavus/index.js +7 -0
- package/dist/services/together/index.d.ts +2 -0
- package/dist/services/together/index.js +7 -0
- package/dist/services/tts_service.d.ts +909 -0
- package/dist/services/tts_service.js +2018 -0
- package/dist/services/ultravox/index.d.ts +2 -0
- package/dist/services/ultravox/index.js +9 -0
- package/dist/services/vision_service.d.ts +56 -0
- package/dist/services/vision_service.js +62 -0
- package/dist/services/websocket_service.d.ts +213 -0
- package/dist/services/websocket_service.js +654 -0
- package/dist/services/whisper/index.d.ts +2 -0
- package/dist/services/whisper/index.js +9 -0
- package/dist/services/xai/index.d.ts +2 -0
- package/dist/services/xai/index.js +9 -0
- package/dist/services/xtts/index.d.ts +2 -0
- package/dist/services/xtts/index.js +9 -0
- package/dist/tests/index.d.ts +2 -0
- package/dist/tests/index.js +8 -0
- package/dist/tests/utils.d.ts +123 -0
- package/dist/tests/utils.js +212 -0
- package/dist/transcriptions/index.d.ts +2 -0
- package/dist/transcriptions/index.js +8 -0
- package/dist/transcriptions/language.d.ts +376 -0
- package/dist/transcriptions/language.js +543 -0
- package/dist/transports/base_input.d.ts +149 -0
- package/dist/transports/base_input.js +338 -0
- package/dist/transports/base_output.d.ts +432 -0
- package/dist/transports/base_output.js +1231 -0
- package/dist/transports/base_transport.d.ts +223 -0
- package/dist/transports/base_transport.js +167 -0
- package/dist/transports/daily/index.d.ts +2 -0
- package/dist/transports/daily/index.js +9 -0
- package/dist/transports/heygen/index.d.ts +2 -0
- package/dist/transports/heygen/index.js +9 -0
- package/dist/transports/index.d.ts +2 -0
- package/dist/transports/index.js +8 -0
- package/dist/transports/lemonslice/index.d.ts +2 -0
- package/dist/transports/lemonslice/index.js +9 -0
- package/dist/transports/livekit/index.d.ts +2 -0
- package/dist/transports/livekit/index.js +9 -0
- package/dist/transports/local/index.d.ts +2 -0
- package/dist/transports/local/index.js +9 -0
- package/dist/transports/moq/index.d.ts +2 -0
- package/dist/transports/moq/index.js +9 -0
- package/dist/transports/smallwebrtc/index.d.ts +2 -0
- package/dist/transports/smallwebrtc/index.js +9 -0
- package/dist/transports/tavus/index.d.ts +2 -0
- package/dist/transports/tavus/index.js +9 -0
- package/dist/transports/video_in_sampler.d.ts +157 -0
- package/dist/transports/video_in_sampler.js +258 -0
- package/dist/transports/vonage/index.d.ts +2 -0
- package/dist/transports/vonage/index.js +9 -0
- package/dist/transports/websocket/index.d.ts +2 -0
- package/dist/transports/websocket/index.js +9 -0
- package/dist/transports/whatsapp/index.d.ts +2 -0
- package/dist/transports/whatsapp/index.js +9 -0
- package/dist/turns/eager_end_of_turn_mixin.d.ts +70 -0
- package/dist/turns/eager_end_of_turn_mixin.js +122 -0
- package/dist/turns/empty_user_turn.d.ts +55 -0
- package/dist/turns/empty_user_turn.js +53 -0
- package/dist/turns/index.d.ts +2 -0
- package/dist/turns/index.js +8 -0
- package/dist/turns/speculation_gate.d.ts +169 -0
- package/dist/turns/speculation_gate.js +294 -0
- package/dist/turns/types.d.ts +33 -0
- package/dist/turns/types.js +35 -0
- package/dist/turns/user_idle_controller.d.ts +94 -0
- package/dist/turns/user_idle_controller.js +187 -0
- package/dist/turns/user_mute/always_user_mute_strategy.d.ts +15 -0
- package/dist/turns/user_mute/always_user_mute_strategy.js +29 -0
- package/dist/turns/user_mute/base_user_mute_strategy.d.ts +39 -0
- package/dist/turns/user_mute/base_user_mute_strategy.js +45 -0
- package/dist/turns/user_mute/first_speech_user_mute_strategy.d.ts +28 -0
- package/dist/turns/user_mute/first_speech_user_mute_strategy.js +52 -0
- package/dist/turns/user_mute/function_call_user_mute_strategy.d.ts +25 -0
- package/dist/turns/user_mute/function_call_user_mute_strategy.js +50 -0
- package/dist/turns/user_mute/index.d.ts +6 -0
- package/dist/turns/user_mute/index.js +11 -0
- package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.d.ts +43 -0
- package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.js +76 -0
- package/dist/turns/user_start/base_user_turn_start_strategy.d.ts +135 -0
- package/dist/turns/user_start/base_user_turn_start_strategy.js +134 -0
- package/dist/turns/user_start/external_user_turn_start_strategy.d.ts +45 -0
- package/dist/turns/user_start/external_user_turn_start_strategy.js +64 -0
- package/dist/turns/user_start/index.d.ts +8 -0
- package/dist/turns/user_start/index.js +16 -0
- package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.d.ts +133 -0
- package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.js +243 -0
- package/dist/turns/user_start/min_words_user_turn_start_strategy.d.ts +60 -0
- package/dist/turns/user_start/min_words_user_turn_start_strategy.js +102 -0
- package/dist/turns/user_start/transcription_user_turn_start_strategy.d.ts +29 -0
- package/dist/turns/user_start/transcription_user_turn_start_strategy.js +43 -0
- package/dist/turns/user_start/vad_user_turn_start_strategy.d.ts +20 -0
- package/dist/turns/user_start/vad_user_turn_start_strategy.js +31 -0
- package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.d.ts +141 -0
- package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.js +238 -0
- package/dist/turns/user_stop/base_user_turn_stop_strategy.d.ts +174 -0
- package/dist/turns/user_stop/base_user_turn_stop_strategy.js +170 -0
- package/dist/turns/user_stop/deferred_user_turn_stop_strategy.d.ts +81 -0
- package/dist/turns/user_stop/deferred_user_turn_stop_strategy.js +107 -0
- package/dist/turns/user_stop/eager_match_policy.d.ts +36 -0
- package/dist/turns/user_stop/eager_match_policy.js +50 -0
- package/dist/turns/user_stop/eager_user_turn_stop_strategy.d.ts +84 -0
- package/dist/turns/user_stop/eager_user_turn_stop_strategy.js +154 -0
- package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.d.ts +45 -0
- package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.js +55 -0
- package/dist/turns/user_stop/external_user_turn_stop_strategy.d.ts +115 -0
- package/dist/turns/user_stop/external_user_turn_stop_strategy.js +205 -0
- package/dist/turns/user_stop/index.d.ts +10 -0
- package/dist/turns/user_stop/index.js +15 -0
- package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.d.ts +44 -0
- package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.js +59 -0
- package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.d.ts +140 -0
- package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.js +303 -0
- package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.d.ts +135 -0
- package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.js +332 -0
- package/dist/turns/user_turn_completion_mixin.d.ts +147 -0
- package/dist/turns/user_turn_completion_mixin.js +591 -0
- package/dist/turns/user_turn_controller.d.ts +134 -0
- package/dist/turns/user_turn_controller.js +335 -0
- package/dist/turns/user_turn_processor.d.ts +87 -0
- package/dist/turns/user_turn_processor.js +180 -0
- package/dist/turns/user_turn_strategies.d.ts +187 -0
- package/dist/turns/user_turn_strategies.js +222 -0
- package/dist/utils/async_tool_cancellation.d.ts +35 -0
- package/dist/utils/async_tool_cancellation.js +78 -0
- package/dist/utils/asyncio/index.d.ts +2 -0
- package/dist/utils/asyncio/index.js +7 -0
- package/dist/utils/asyncio/task_manager.d.ts +49 -0
- package/dist/utils/asyncio/task_manager.js +105 -0
- package/dist/utils/base_object.d.ts +76 -0
- package/dist/utils/base_object.js +154 -0
- package/dist/utils/context/aggregated_frame_sequencer.d.ts +491 -0
- package/dist/utils/context/aggregated_frame_sequencer.js +803 -0
- package/dist/utils/context/audio_context_manager.d.ts +131 -0
- package/dist/utils/context/audio_context_manager.js +233 -0
- package/dist/utils/context/index.d.ts +2 -0
- package/dist/utils/context/index.js +7 -0
- package/dist/utils/context/llm_context_summarization.d.ts +258 -0
- package/dist/utils/context/llm_context_summarization.js +511 -0
- package/dist/utils/context/text_segment_map.d.ts +611 -0
- package/dist/utils/context/text_segment_map.js +1132 -0
- package/dist/utils/context/word_completion_tracker.d.ts +273 -0
- package/dist/utils/context/word_completion_tracker.js +441 -0
- package/dist/utils/context/word_timestamp_tracker.d.ts +72 -0
- package/dist/utils/context/word_timestamp_tracker.js +98 -0
- package/dist/utils/deprecation.d.ts +28 -0
- package/dist/utils/deprecation.js +75 -0
- package/dist/utils/env.d.ts +38 -0
- package/dist/utils/env.js +62 -0
- package/dist/utils/errors.d.ts +75 -0
- package/dist/utils/errors.js +202 -0
- package/dist/utils/file_resolver.d.ts +126 -0
- package/dist/utils/file_resolver.js +314 -0
- package/dist/utils/file_storage.d.ts +99 -0
- package/dist/utils/file_storage.js +100 -0
- package/dist/utils/frame_queue.d.ts +36 -0
- package/dist/utils/frame_queue.js +66 -0
- package/dist/utils/http.d.ts +39 -0
- package/dist/utils/http.js +53 -0
- package/dist/utils/image.d.ts +15 -0
- package/dist/utils/image.js +30 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/log_config.d.ts +34 -0
- package/dist/utils/log_config.js +76 -0
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.js +91 -0
- package/dist/utils/mixin.d.ts +14 -0
- package/dist/utils/mixin.js +63 -0
- package/dist/utils/network.d.ts +73 -0
- package/dist/utils/network.js +90 -0
- package/dist/utils/security/allowed_origins.d.ts +16 -0
- package/dist/utils/security/allowed_origins.js +34 -0
- package/dist/utils/security/index.d.ts +2 -0
- package/dist/utils/security/index.js +7 -0
- package/dist/utils/security/redaction.d.ts +27 -0
- package/dist/utils/security/redaction.js +123 -0
- package/dist/utils/security/ssrf.d.ts +79 -0
- package/dist/utils/security/ssrf.js +327 -0
- package/dist/utils/shared.d.ts +35 -0
- package/dist/utils/shared.js +136 -0
- package/dist/utils/startup.d.ts +17 -0
- package/dist/utils/startup.js +130 -0
- package/dist/utils/string.d.ts +116 -0
- package/dist/utils/string.js +460 -0
- package/dist/utils/sync/base_notifier.d.ts +25 -0
- package/dist/utils/sync/base_notifier.js +16 -0
- package/dist/utils/sync/event_notifier.d.ts +38 -0
- package/dist/utils/sync/event_notifier.js +51 -0
- package/dist/utils/sync/index.d.ts +2 -0
- package/dist/utils/sync/index.js +7 -0
- package/dist/utils/text/alnum_utils.d.ts +109 -0
- package/dist/utils/text/alnum_utils.js +262 -0
- package/dist/utils/text/base_text_aggregator.d.ts +146 -0
- package/dist/utils/text/base_text_aggregator.js +102 -0
- package/dist/utils/text/base_text_filter.d.ts +54 -0
- package/dist/utils/text/base_text_filter.js +50 -0
- package/dist/utils/text/index.d.ts +2 -0
- package/dist/utils/text/index.js +7 -0
- package/dist/utils/text/markdown_text_filter.d.ts +147 -0
- package/dist/utils/text/markdown_text_filter.js +522 -0
- package/dist/utils/text/markup_utils.d.ts +54 -0
- package/dist/utils/text/markup_utils.js +156 -0
- package/dist/utils/text/pattern_pair_aggregator.d.ts +251 -0
- package/dist/utils/text/pattern_pair_aggregator.js +478 -0
- package/dist/utils/text/phonemes.d.ts +60 -0
- package/dist/utils/text/phonemes.js +172 -0
- package/dist/utils/text/simple_text_aggregator.d.ts +135 -0
- package/dist/utils/text/simple_text_aggregator.js +254 -0
- package/dist/utils/text/skip_tags_aggregator.d.ts +85 -0
- package/dist/utils/text/skip_tags_aggregator.js +147 -0
- package/dist/utils/text/transforms/pronunciations.d.ts +65 -0
- package/dist/utils/text/transforms/pronunciations.js +111 -0
- package/dist/utils/text/word_timestamp_utils.d.ts +26 -0
- package/dist/utils/text/word_timestamp_utils.js +52 -0
- package/dist/utils/time.d.ts +13 -0
- package/dist/utils/time.js +31 -0
- package/dist/utils/tracing/index.d.ts +3 -0
- package/dist/utils/tracing/index.js +8 -0
- package/dist/utils/tracing/service_attributes.d.ts +198 -0
- package/dist/utils/tracing/service_attributes.js +434 -0
- package/dist/utils/tracing/service_decorators.d.ts +214 -0
- package/dist/utils/tracing/service_decorators.js +1662 -0
- package/dist/utils/tracing/setup.d.ts +95 -0
- package/dist/utils/tracing/setup.js +110 -0
- package/dist/utils/tracing/tracing_context.d.ts +55 -0
- package/dist/utils/tracing/tracing_context.js +97 -0
- package/dist/utils/tracing/turn_trace_observer.d.ts +90 -0
- package/dist/utils/tracing/turn_trace_observer.js +219 -0
- package/dist/utils/types.d.ts +80 -0
- package/dist/utils/types.js +108 -0
- package/dist/utils/utils.d.ts +5 -0
- package/dist/utils/utils.js +22 -0
- package/dist/utils/yaml.d.ts +64 -0
- package/dist/utils/yaml.js +41 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +7 -0
- package/dist/workers/base_worker.d.ts +772 -0
- package/dist/workers/base_worker.js +1298 -0
- package/dist/workers/index.d.ts +2 -0
- package/dist/workers/index.js +8 -0
- package/dist/workers/llm/backend_llm_worker.d.ts +552 -0
- package/dist/workers/llm/backend_llm_worker.js +1055 -0
- package/dist/workers/llm/index.d.ts +6 -0
- package/dist/workers/llm/index.js +11 -0
- package/dist/workers/llm/llm_context_worker.d.ts +66 -0
- package/dist/workers/llm/llm_context_worker.js +72 -0
- package/dist/workers/llm/llm_worker.d.ts +180 -0
- package/dist/workers/llm/llm_worker.js +323 -0
- package/dist/workers/llm/tool_decorator.d.ts +68 -0
- package/dist/workers/llm/tool_decorator.js +60 -0
- package/dist/workers/runner.d.ts +268 -0
- package/dist/workers/runner.js +489 -0
- package/dist/workers/ui/index.d.ts +22 -0
- package/dist/workers/ui/index.js +27 -0
- package/dist/workers/ui/ui_event_decorator.d.ts +43 -0
- package/dist/workers/ui/ui_event_decorator.js +75 -0
- package/dist/workers/ui/ui_job_context.d.ts +12 -0
- package/dist/workers/ui/ui_job_context.js +17 -0
- package/dist/workers/ui/ui_prompts.d.ts +21 -0
- package/dist/workers/ui/ui_prompts.js +41 -0
- package/dist/workers/ui/ui_tools.d.ts +32 -0
- package/dist/workers/ui/ui_tools.js +112 -0
- package/dist/workers/ui/ui_worker.d.ts +571 -0
- package/dist/workers/ui/ui_worker.js +1156 -0
- package/package.json +46 -3
- package/README.md +0 -5
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/utils/context/text_segment_map.py
|
|
6
|
+
/**
|
|
7
|
+
* Keeps a position in three versions of the same text as a TTS speaks it.
|
|
8
|
+
*
|
|
9
|
+
* Positions are UTF-16 code unit offsets (upstream counts code points); see
|
|
10
|
+
* `alnum_utils.ts`.
|
|
11
|
+
*/
|
|
12
|
+
import { advanceByAlnums, advanceByChars, alnumOnly, charAt, foldForMatching, hasAlnum, isAlnum, isPunctuation, isSpace, stripTrailingPunctuation, } from "../text/alnum_utils.js";
|
|
13
|
+
import { rawOffsetAfterCleanChars, splitMarkupRuns, stripCompleteMarkup, stripMarkup, } from "../text/markup_utils.js";
|
|
14
|
+
/** Python's `len(s.split())`. */
|
|
15
|
+
function wordCount(text) {
|
|
16
|
+
return text.split(/\s+/).filter((w) => w).length;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A piece of the utterance, paired with what the TTS was given in its place.
|
|
20
|
+
*
|
|
21
|
+
* The map is a list of these, laid end to end over the whole utterance. In
|
|
22
|
+
* most of them the two sides are identical; the interesting ones are where a
|
|
23
|
+
* transform, a filter or a tag made them differ (see `isTransformed`).
|
|
24
|
+
*
|
|
25
|
+
* - `original`: the piece as a client displays it.
|
|
26
|
+
* - `tts`: the same piece as it was sent to the TTS. Identical to `original`
|
|
27
|
+
* unless something rewrote it, and empty if it was dropped entirely.
|
|
28
|
+
* - `originalStart`: where the piece starts in the full original text.
|
|
29
|
+
* - `originalEnd`: where it ends. Cursors jump straight here once a rewritten
|
|
30
|
+
* piece is finished, since no position inside one means anything.
|
|
31
|
+
*/
|
|
32
|
+
export class TextSegment {
|
|
33
|
+
original;
|
|
34
|
+
tts;
|
|
35
|
+
originalStart;
|
|
36
|
+
originalEnd;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.original = options.original;
|
|
39
|
+
this.tts = options.tts;
|
|
40
|
+
this.originalStart = options.originalStart;
|
|
41
|
+
this.originalEnd = options.originalEnd;
|
|
42
|
+
Object.freeze(this);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* True when the two sides cannot be followed together, character by character.
|
|
46
|
+
*
|
|
47
|
+
* A segment like this is all or nothing. The cursors into the other texts
|
|
48
|
+
* wait at its start until every spoken word of it has arrived, then jump
|
|
49
|
+
* straight to its end, because no position inside it means anything.
|
|
50
|
+
*
|
|
51
|
+
* Any one of these makes it true:
|
|
52
|
+
*
|
|
53
|
+
* - the letters and digits differ, as in `"$42.50"` against
|
|
54
|
+
* `"forty two dollars"`;
|
|
55
|
+
* - the two sides have different numbers of words;
|
|
56
|
+
* - the TTS side has tags in it, even when the words match. The cursor
|
|
57
|
+
* through the spoken text has tag characters to cross that the other
|
|
58
|
+
* texts do not have.
|
|
59
|
+
*
|
|
60
|
+
* Only the shape of a tag matters, never its name: `<phoneme
|
|
61
|
+
* ...>Siobhan</phoneme>` counts because of the tags around the word.
|
|
62
|
+
*/
|
|
63
|
+
get isTransformed() {
|
|
64
|
+
if (this.tts !== stripCompleteMarkup(this.tts)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (alnumOnly(this.original) !== alnumOnly(this.tts)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return wordCount(this.original) !== wordCount(this.tts);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* How many letters and digits the spoken side of this segment has.
|
|
74
|
+
*
|
|
75
|
+
* This is what the provider's words spend as they arrive. On a rewritten
|
|
76
|
+
* segment it has nothing to do with `originalAlnumCount`: "forty two
|
|
77
|
+
* dollars" against "$42.50".
|
|
78
|
+
*/
|
|
79
|
+
get ttsAlnumCount() {
|
|
80
|
+
return alnumOnly(this.tts).length;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* How many letters and digits the original side of this segment has.
|
|
84
|
+
*
|
|
85
|
+
* This is what the cursors into `originalText` and `llmText` spend, since
|
|
86
|
+
* those texts hold the original characters, not the spoken ones.
|
|
87
|
+
*/
|
|
88
|
+
get originalAlnumCount() {
|
|
89
|
+
return alnumOnly(this.original).length;
|
|
90
|
+
}
|
|
91
|
+
toString() {
|
|
92
|
+
return `TextSegment(original=${JSON.stringify(this.original)}, tts=${JSON.stringify(this.tts)}, original_start=${this.originalStart}, original_end=${this.originalEnd})`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What happened when a word was offered to one segment.
|
|
97
|
+
*
|
|
98
|
+
* Trying one word against one segment is called a hop. Two of these answers
|
|
99
|
+
* end the attempt; the other two send the word on to the next segment.
|
|
100
|
+
*
|
|
101
|
+
* - `PLACED`: the word fits here. Move to the end of the match and stop.
|
|
102
|
+
* - `CROSSES`: this segment holds only the beginning of the word. Finish the
|
|
103
|
+
* segment and take the rest of the word to the next one.
|
|
104
|
+
* - `EXHAUSTED`: there is nothing here that can be spoken, so no word will
|
|
105
|
+
* ever match -- an empty side of the diff, or a lone `<break/>`. Finish the
|
|
106
|
+
* segment and try the whole word again on the next one.
|
|
107
|
+
* - `NO_MATCH`: nothing here matches the word, usually a symbol the provider
|
|
108
|
+
* reports differently from the text. Step every cursor past the next run of
|
|
109
|
+
* punctuation, which the word stands for, and stop.
|
|
110
|
+
*/
|
|
111
|
+
export const _HopKind = {
|
|
112
|
+
PLACED: 1,
|
|
113
|
+
CROSSES: 2,
|
|
114
|
+
EXHAUSTED: 3,
|
|
115
|
+
NO_MATCH: 4,
|
|
116
|
+
};
|
|
117
|
+
function hop(kind, options = {}) {
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
kind,
|
|
120
|
+
segmentAdvance: options.segmentAdvance ?? 0,
|
|
121
|
+
wordConsumed: options.wordConsumed ?? 0,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function getOpcodes(a, b) {
|
|
125
|
+
const b2j = new Map();
|
|
126
|
+
b.forEach((elt, j) => {
|
|
127
|
+
let indices = b2j.get(elt);
|
|
128
|
+
if (!indices) {
|
|
129
|
+
indices = [];
|
|
130
|
+
b2j.set(elt, indices);
|
|
131
|
+
}
|
|
132
|
+
indices.push(j);
|
|
133
|
+
});
|
|
134
|
+
const findLongestMatch = (alo, ahi, blo, bhi) => {
|
|
135
|
+
let besti = alo;
|
|
136
|
+
let bestj = blo;
|
|
137
|
+
let bestsize = 0;
|
|
138
|
+
let j2len = new Map();
|
|
139
|
+
for (let i = alo; i < ahi; i++) {
|
|
140
|
+
const newj2len = new Map();
|
|
141
|
+
for (const j of b2j.get(a[i]) ?? []) {
|
|
142
|
+
if (j < blo)
|
|
143
|
+
continue;
|
|
144
|
+
if (j >= bhi)
|
|
145
|
+
break;
|
|
146
|
+
const k = (j2len.get(j - 1) ?? 0) + 1;
|
|
147
|
+
newj2len.set(j, k);
|
|
148
|
+
if (k > bestsize) {
|
|
149
|
+
besti = i - k + 1;
|
|
150
|
+
bestj = j - k + 1;
|
|
151
|
+
bestsize = k;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
j2len = newj2len;
|
|
155
|
+
}
|
|
156
|
+
while (besti > alo && bestj > blo && a[besti - 1] === b[bestj - 1]) {
|
|
157
|
+
besti -= 1;
|
|
158
|
+
bestj -= 1;
|
|
159
|
+
bestsize += 1;
|
|
160
|
+
}
|
|
161
|
+
while (besti + bestsize < ahi &&
|
|
162
|
+
bestj + bestsize < bhi &&
|
|
163
|
+
a[besti + bestsize] === b[bestj + bestsize]) {
|
|
164
|
+
bestsize += 1;
|
|
165
|
+
}
|
|
166
|
+
return [besti, bestj, bestsize];
|
|
167
|
+
};
|
|
168
|
+
const la = a.length;
|
|
169
|
+
const lb = b.length;
|
|
170
|
+
const queue = [[0, la, 0, lb]];
|
|
171
|
+
const matchingBlocks = [];
|
|
172
|
+
while (queue.length > 0) {
|
|
173
|
+
const [alo, ahi, blo, bhi] = queue.pop();
|
|
174
|
+
const x = findLongestMatch(alo, ahi, blo, bhi);
|
|
175
|
+
const [i, j, k] = x;
|
|
176
|
+
if (k) {
|
|
177
|
+
matchingBlocks.push(x);
|
|
178
|
+
if (alo < i && blo < j)
|
|
179
|
+
queue.push([alo, i, blo, j]);
|
|
180
|
+
if (i + k < ahi && j + k < bhi)
|
|
181
|
+
queue.push([i + k, ahi, j + k, bhi]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
matchingBlocks.sort((p, q) => p[0] - q[0] || p[1] - q[1] || p[2] - q[2]);
|
|
185
|
+
let i1 = 0;
|
|
186
|
+
let j1 = 0;
|
|
187
|
+
let k1 = 0;
|
|
188
|
+
const nonAdjacent = [];
|
|
189
|
+
for (const [i2, j2, k2] of matchingBlocks) {
|
|
190
|
+
if (i1 + k1 === i2 && j1 + k1 === j2) {
|
|
191
|
+
k1 += k2;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
if (k1)
|
|
195
|
+
nonAdjacent.push([i1, j1, k1]);
|
|
196
|
+
i1 = i2;
|
|
197
|
+
j1 = j2;
|
|
198
|
+
k1 = k2;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (k1)
|
|
202
|
+
nonAdjacent.push([i1, j1, k1]);
|
|
203
|
+
nonAdjacent.push([la, lb, 0]);
|
|
204
|
+
let i = 0;
|
|
205
|
+
let j = 0;
|
|
206
|
+
const answer = [];
|
|
207
|
+
for (const [ai, bj, size] of nonAdjacent) {
|
|
208
|
+
let tag = "";
|
|
209
|
+
if (i < ai && j < bj)
|
|
210
|
+
tag = "replace";
|
|
211
|
+
else if (i < ai)
|
|
212
|
+
tag = "delete";
|
|
213
|
+
else if (j < bj)
|
|
214
|
+
tag = "insert";
|
|
215
|
+
if (tag)
|
|
216
|
+
answer.push([tag, i, ai, j, bj]);
|
|
217
|
+
i = ai + size;
|
|
218
|
+
j = bj + size;
|
|
219
|
+
if (size)
|
|
220
|
+
answer.push(["equal", ai, i, bj, j]);
|
|
221
|
+
}
|
|
222
|
+
return answer;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Answers "where are we?" in three versions of one utterance, word by word.
|
|
226
|
+
*
|
|
227
|
+
* A TTS provider reports the words it speaks. Each report has to be turned
|
|
228
|
+
* into a position -- but into a position in *three* different strings,
|
|
229
|
+
* because the same utterance exists in three forms at once:
|
|
230
|
+
*
|
|
231
|
+
* - `ttsText` -- what was actually spoken, tags and all:
|
|
232
|
+
* `"Your balance is forty two dollars"`
|
|
233
|
+
* - `originalText` -- what a client displays: `"Your balance is $42.50"`
|
|
234
|
+
* - `llmText` -- what the LLM wrote, so what the transcript should keep:
|
|
235
|
+
* `"Your balance is <b>$42.50</b>"`. Defaults to `originalText`.
|
|
236
|
+
*
|
|
237
|
+
* For a frame nothing rewrote, all three are the same string and every
|
|
238
|
+
* position is the same.
|
|
239
|
+
*
|
|
240
|
+
* **The hard part is that a spoken word need not appear in the other two.**
|
|
241
|
+
* The provider says `"dollars"`; nothing in `"$42.50"` matches it. So the map
|
|
242
|
+
* is built once, by diffing `ttsText` against `originalText` into aligned
|
|
243
|
+
* `TextSegment` pieces -- each either survived unchanged or was rewritten
|
|
244
|
+
* whole.
|
|
245
|
+
*
|
|
246
|
+
* `llmText` is never compared against the others, and does not need to be.
|
|
247
|
+
* It holds the same letters and digits as `originalText`, in the same order,
|
|
248
|
+
* and differs only in what is wrapped around them -- tags, delimiters,
|
|
249
|
+
* punctuation. So counting letters and digits is enough to keep it in step,
|
|
250
|
+
* and its cursor moves by that count.
|
|
251
|
+
*
|
|
252
|
+
* From then on one real cursor moves: `rawPos`, how far into `ttsText` the
|
|
253
|
+
* provider has got. `userFacingPos` and the two LLM cursors follow it:
|
|
254
|
+
*
|
|
255
|
+
* - Through an **unchanged** segment they keep pace, word for word.
|
|
256
|
+
* - Through a **rewritten** one they wait. There is no honest position
|
|
257
|
+
* halfway through `"$42.50"` while `"forty two dollars"` is being spoken, so
|
|
258
|
+
* they hold and then jump to the end of the span in one step when the last
|
|
259
|
+
* of its words lands.
|
|
260
|
+
*
|
|
261
|
+
* The LLM side carries two of them because one position cannot answer both
|
|
262
|
+
* questions asked of it. `llmSpokenPos` is what the provider has reported;
|
|
263
|
+
* `llmPos` is what has been attributed to a word, and runs ahead of it over a
|
|
264
|
+
* mark stuck to a word the provider has already named. See
|
|
265
|
+
* `_advanceLlmCursors`.
|
|
266
|
+
*
|
|
267
|
+
* Callers ask two things. `wordBelongsCurrentSegment` -- does this token
|
|
268
|
+
* plausibly continue what is left to speak? -- and `advanceWord`, which
|
|
269
|
+
* consumes it. Both tolerate the ways providers mangle tokens (added
|
|
270
|
+
* punctuation, changed case or diacritics, a fragment of a half-open SSML
|
|
271
|
+
* tag) without the caller knowing anything about it; `_classifyHop` holds
|
|
272
|
+
* that logic.
|
|
273
|
+
*
|
|
274
|
+
* A word need not sit at the cursor to be placed. When the provider garbles
|
|
275
|
+
* or drops an event, the next good word still matches a few words on, and the
|
|
276
|
+
* text stepped over -- which no event will ever name -- is consumed with it.
|
|
277
|
+
* So the two questions above stay simple: a token either has a home somewhere
|
|
278
|
+
* in what is left to speak, or it belongs to another utterance entirely.
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* // "$42.50" was sent to the TTS as "forty two dollars and fifty cents"
|
|
282
|
+
* const smap = new TextSegmentMap(
|
|
283
|
+
* "Your balance is forty two dollars and fifty cents",
|
|
284
|
+
* "Your balance is $42.50",
|
|
285
|
+
* );
|
|
286
|
+
* for (const word of ["Your", "balance", "is"]) {
|
|
287
|
+
* smap.advanceWord(word); // unchanged: every cursor keeps pace
|
|
288
|
+
* }
|
|
289
|
+
* for (const word of ["forty", "two", "dollars", "and", "fifty"]) {
|
|
290
|
+
* smap.advanceWord(word); // rewritten: the other two cursors wait
|
|
291
|
+
* }
|
|
292
|
+
* smap.advanceWord("cents"); // the span is done, so they jump to its end
|
|
293
|
+
* // smap.lastCompletedSegment.original === "$42.50"
|
|
294
|
+
* // smap.inTransformedSegment === false
|
|
295
|
+
*/
|
|
296
|
+
export class TextSegmentMap {
|
|
297
|
+
/** How many words a word may be placed past -- see `_lookaheadHop`. */
|
|
298
|
+
static LOOKAHEAD_WORDS = 3;
|
|
299
|
+
_ttsText;
|
|
300
|
+
_originalText;
|
|
301
|
+
_llmText;
|
|
302
|
+
_segments;
|
|
303
|
+
_segIdx = 0;
|
|
304
|
+
_segRawPos = 0;
|
|
305
|
+
_userFacingPos = 0;
|
|
306
|
+
_llmPos = 0;
|
|
307
|
+
_llmSpokenPos = 0;
|
|
308
|
+
_lastCompleted = null;
|
|
309
|
+
_lastOverflow = null;
|
|
310
|
+
_lastLeadingDuplicate = 0;
|
|
311
|
+
/**
|
|
312
|
+
* Line the three texts up against each other.
|
|
313
|
+
*
|
|
314
|
+
* The comparison happens once, here. Everything after this only moves
|
|
315
|
+
* cursors.
|
|
316
|
+
*
|
|
317
|
+
* @param ttsText What was sent to the TTS, and so what incoming words are
|
|
318
|
+
* matched against. May carry synthesis tags and rewritten values.
|
|
319
|
+
* @param originalText The same content as a client displays it, before any
|
|
320
|
+
* rewriting. Diffed against `ttsText` to build the segments.
|
|
321
|
+
* @param llmText The same content as the LLM wrote it, which may add
|
|
322
|
+
* delimiters the other two never see. Rides its own cursor rather than
|
|
323
|
+
* being diffed. Defaults to `originalText`.
|
|
324
|
+
*/
|
|
325
|
+
constructor(ttsText, originalText, llmText = null) {
|
|
326
|
+
this._ttsText = ttsText;
|
|
327
|
+
this._originalText = originalText;
|
|
328
|
+
this._llmText = llmText !== null && llmText !== undefined ? llmText : originalText;
|
|
329
|
+
this._segments = TextSegmentMap._buildSegments(ttsText, originalText);
|
|
330
|
+
this._resetState();
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Compare the two texts and cut them into the segments the map walks.
|
|
334
|
+
*
|
|
335
|
+
* `difflib` lines them up a word at a time and reports each piece as equal,
|
|
336
|
+
* replaced, inserted or deleted. Every piece becomes one `TextSegment`.
|
|
337
|
+
* Spaces are kept as words of their own so that the positions stay exact.
|
|
338
|
+
*
|
|
339
|
+
* One extra step: a piece that came out equal is cut around any tag inside
|
|
340
|
+
* it, so a single tag does not turn the whole sentence into an
|
|
341
|
+
* all-or-nothing segment.
|
|
342
|
+
*
|
|
343
|
+
* Called once, from the constructor.
|
|
344
|
+
*/
|
|
345
|
+
static _buildSegments(ttsText, originalText) {
|
|
346
|
+
const tokenize = (text) => text.split(/(\s+)/);
|
|
347
|
+
const origTokens = tokenize(originalText);
|
|
348
|
+
const ttsTokens = tokenize(ttsText);
|
|
349
|
+
// SequenceMatcher lines the two texts up a word at a time. Each piece it
|
|
350
|
+
// reports becomes a TextSegment, positioned in the original text.
|
|
351
|
+
//
|
|
352
|
+
// Example:
|
|
353
|
+
//
|
|
354
|
+
// originalText = "Your balance is $42.50"
|
|
355
|
+
// ttsText = "Your balance is forty two dollars and fifty cents"
|
|
356
|
+
//
|
|
357
|
+
// Tokenization preserves whitespace, so SequenceMatcher sees:
|
|
358
|
+
//
|
|
359
|
+
// equal:
|
|
360
|
+
// "Your balance is "
|
|
361
|
+
//
|
|
362
|
+
// replace:
|
|
363
|
+
// "$42.50"
|
|
364
|
+
// ->
|
|
365
|
+
// "forty two dollars and fifty cents"
|
|
366
|
+
//
|
|
367
|
+
// This produces two segments:
|
|
368
|
+
//
|
|
369
|
+
// TextSegment(
|
|
370
|
+
// original="Your balance is ",
|
|
371
|
+
// tts="Your balance is ",
|
|
372
|
+
// original_start=0,
|
|
373
|
+
// original_end=16,
|
|
374
|
+
// )
|
|
375
|
+
//
|
|
376
|
+
// TextSegment(
|
|
377
|
+
// original="$42.50",
|
|
378
|
+
// tts="forty two dollars and fifty cents",
|
|
379
|
+
// original_start=16,
|
|
380
|
+
// original_end=22,
|
|
381
|
+
// )
|
|
382
|
+
//
|
|
383
|
+
// During playback, unchanged segments advance cursors
|
|
384
|
+
// proportionally. Transformed segments are treated as atomic:
|
|
385
|
+
// the cursors are held while the expanded TTS text is being
|
|
386
|
+
// consumed and jump to original_end only when the entire
|
|
387
|
+
// transformed segment completes.
|
|
388
|
+
const segments = [];
|
|
389
|
+
let origPos = 0;
|
|
390
|
+
for (const [tag, i1, i2, j1, j2] of getOpcodes(origTokens, ttsTokens)) {
|
|
391
|
+
const origChunk = origTokens.slice(i1, i2).join("");
|
|
392
|
+
const ttsChunk = ttsTokens.slice(j1, j2).join("");
|
|
393
|
+
// A segment is all-or-nothing as soon as it holds a tag, so one tag in
|
|
394
|
+
// the middle of otherwise identical text would hold the cursors still
|
|
395
|
+
// for the whole piece. Giving the tag a segment of its own limits that
|
|
396
|
+
// to the words the tag wraps:
|
|
397
|
+
//
|
|
398
|
+
// original = tts = "I love to count <spell>1234</spell>."
|
|
399
|
+
//
|
|
400
|
+
// "I love to count " plain, cursors move word by word
|
|
401
|
+
// "<spell>1234</spell>." all-or-nothing, lands as one
|
|
402
|
+
//
|
|
403
|
+
// Only equal pieces can be split this way. Both sides hold the same
|
|
404
|
+
// text there, so a single position cuts both. Where the sides differ
|
|
405
|
+
// there is no position that means the same thing on each.
|
|
406
|
+
const parts = tag === "equal"
|
|
407
|
+
? splitMarkupRuns(origChunk).map((part) => [part, part])
|
|
408
|
+
: [[origChunk, ttsChunk]];
|
|
409
|
+
for (const [origPart, ttsPart] of parts) {
|
|
410
|
+
const origEnd = origPos + origPart.length;
|
|
411
|
+
segments.push(new TextSegment({
|
|
412
|
+
original: origPart,
|
|
413
|
+
tts: ttsPart,
|
|
414
|
+
originalStart: origPos,
|
|
415
|
+
originalEnd: origEnd,
|
|
416
|
+
}));
|
|
417
|
+
origPos = origEnd;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return segments;
|
|
421
|
+
}
|
|
422
|
+
_resetState() {
|
|
423
|
+
this._segIdx = 0;
|
|
424
|
+
this._segRawPos = 0;
|
|
425
|
+
this._userFacingPos = 0;
|
|
426
|
+
this._llmPos = 0;
|
|
427
|
+
this._llmSpokenPos = 0;
|
|
428
|
+
this._lastCompleted = null;
|
|
429
|
+
this._lastOverflow = null;
|
|
430
|
+
this._lastLeadingDuplicate = 0;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Return `word`, then with its last mark removed, then with every trailing
|
|
434
|
+
* mark removed.
|
|
435
|
+
*
|
|
436
|
+
* A TTS can add punctuation the text it was given never had -- reading a
|
|
437
|
+
* list item `"my account"` as a sentence and reporting `"account."`.
|
|
438
|
+
* Matching tries the word as it arrived first, then the trimmed forms.
|
|
439
|
+
*
|
|
440
|
+
* Dropping only the added mark comes before dropping them all, so the marks
|
|
441
|
+
* the text does have are still matched: `"images:."` lands past the colon
|
|
442
|
+
* in `"images:"`, and `"---."` keeps something to match at all. The
|
|
443
|
+
* variants are tried in order and empty ones are skipped.
|
|
444
|
+
*/
|
|
445
|
+
static _wordVariants(word) {
|
|
446
|
+
const allMarksDropped = stripTrailingPunctuation(word);
|
|
447
|
+
// No trailing mark to drop: "hello" -> ["hello"]
|
|
448
|
+
if (allMarksDropped === word) {
|
|
449
|
+
return [word];
|
|
450
|
+
}
|
|
451
|
+
const lastMarkDropped = word.slice(0, -1);
|
|
452
|
+
// A single trailing mark, so both trims agree: "account." -> ["account.", "account"]
|
|
453
|
+
if (lastMarkDropped === allMarksDropped) {
|
|
454
|
+
return [word, allMarksDropped];
|
|
455
|
+
}
|
|
456
|
+
// Several trailing marks: "images:." -> ["images:.", "images:", "images"]
|
|
457
|
+
return [word, lastMarkDropped, allMarksDropped];
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Compare `remainingWord` to each candidate, character for character.
|
|
461
|
+
*
|
|
462
|
+
* Two things can match. If a candidate starts with the word, the word fits
|
|
463
|
+
* here (`PLACED`). If the word starts with a candidate, the candidate ran
|
|
464
|
+
* out first, so the rest of the word belongs to the next segment
|
|
465
|
+
* (`CROSSES`).
|
|
466
|
+
*
|
|
467
|
+
* `_foldedHop` calls this too, on folded copies of the same strings.
|
|
468
|
+
* Folding never changes a string's length, so a position found in a folded
|
|
469
|
+
* copy is the same position in the original.
|
|
470
|
+
*
|
|
471
|
+
* @param candidates `[text, offset]` pairs to match `remainingWord` against,
|
|
472
|
+
* tried in order.
|
|
473
|
+
* @param remainingWord The word (or its trailing-punctuation-stripped
|
|
474
|
+
* variant) to match.
|
|
475
|
+
* @param requireWordBoundary When true, the word must end where a word ends
|
|
476
|
+
* in the candidate -- either it used the candidate up, or the next
|
|
477
|
+
* character is not a letter or digit. This stops `"account"` from matching
|
|
478
|
+
* the start of `"Accountant"`. Only the folded pass asks for it, because
|
|
479
|
+
* folding away case makes that kind of accidental match much easier to hit.
|
|
480
|
+
* @returns A `PLACED` or `CROSSES` hop, or `null` if nothing matched.
|
|
481
|
+
*/
|
|
482
|
+
static _literalHop(candidates, remainingWord, requireWordBoundary = false) {
|
|
483
|
+
for (const word of TextSegmentMap._wordVariants(remainingWord)) {
|
|
484
|
+
if (!word) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
for (const [candidate, offset] of candidates) {
|
|
488
|
+
if (candidate.startsWith(word)) {
|
|
489
|
+
const landsMidWord = requireWordBoundary &&
|
|
490
|
+
word.length < candidate.length &&
|
|
491
|
+
isAlnum(charAt(candidate, word.length));
|
|
492
|
+
if (!landsMidWord) {
|
|
493
|
+
return hop(_HopKind.PLACED, { segmentAdvance: offset + word.length });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (candidate && word.startsWith(candidate)) {
|
|
497
|
+
return hop(_HopKind.CROSSES, { wordConsumed: candidate.length });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Count the characters at the start of `text` that are not letters or digits.
|
|
505
|
+
*
|
|
506
|
+
* For `", I can"` this returns 2 -- the comma and the space.
|
|
507
|
+
*
|
|
508
|
+
* With `stopAtMarkup`, counting also stops at a `'<'`, so the count never
|
|
509
|
+
* reaches inside a tag. Without it, `"<break/>hello"` would count past the
|
|
510
|
+
* `'<'`, and a provider that reports the tag's name `"break"` as a word
|
|
511
|
+
* would look like it had spoken it.
|
|
512
|
+
*/
|
|
513
|
+
static _leadingNonalnumLen(text, stopAtMarkup = false) {
|
|
514
|
+
let i = 0;
|
|
515
|
+
while (i < text.length) {
|
|
516
|
+
const ch = charAt(text, i);
|
|
517
|
+
if (isAlnum(ch)) {
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
if (stopAtMarkup && ch === "<") {
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
i += ch.length;
|
|
524
|
+
}
|
|
525
|
+
return i;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Return the three starting points a word may be matched against here.
|
|
529
|
+
*
|
|
530
|
+
* Providers disagree about what they include in a word, so the same text is
|
|
531
|
+
* offered from three places, each paired with how far in it starts:
|
|
532
|
+
*
|
|
533
|
+
* 1. The text as it is, for a provider that reports `" world"` with its own
|
|
534
|
+
* leading space.
|
|
535
|
+
* 2. Past any spaces.
|
|
536
|
+
* 3. Past everything that is not a letter or digit, for a provider that
|
|
537
|
+
* does not repeat punctuation it already spoke -- when `"I"` arrives for
|
|
538
|
+
* `"Yeah, I can"`, the `", "` is still waiting here.
|
|
539
|
+
*
|
|
540
|
+
* Each start is further in than the last, so the closest match is tried
|
|
541
|
+
* first. Identical starting points are dropped.
|
|
542
|
+
*/
|
|
543
|
+
static _matchCandidates(segmentRemaining) {
|
|
544
|
+
const leadWs = segmentRemaining.length - segmentRemaining.trimStart().length;
|
|
545
|
+
const leadNonalnum = TextSegmentMap._leadingNonalnumLen(segmentRemaining, true);
|
|
546
|
+
return [...new Set([0, leadWs, leadNonalnum])].map((offset) => [
|
|
547
|
+
segmentRemaining.slice(offset),
|
|
548
|
+
offset,
|
|
549
|
+
]);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Match `remainingWord` again, ignoring differences in how it is written.
|
|
553
|
+
*
|
|
554
|
+
* A provider may report a word in lower case, without accents, or with
|
|
555
|
+
* plain quotes: `"SQL"` as `"sql"`, `"cafรฉ"` as `"cafe"`, `"donโt"` as
|
|
556
|
+
* `"don't"`. Folding both sides makes those the same.
|
|
557
|
+
*
|
|
558
|
+
* Folding swaps characters one for one and never adds or removes any, so
|
|
559
|
+
* the strings keep their length and a position found here means the same
|
|
560
|
+
* position in the original text.
|
|
561
|
+
*
|
|
562
|
+
* Because folding hides case, a short word could now match inside a longer
|
|
563
|
+
* one -- `"account"` inside `"Accountant"`. So a match here is only accepted
|
|
564
|
+
* if it ends where a word ends.
|
|
565
|
+
*/
|
|
566
|
+
static _foldedHop(candidates, remainingWord) {
|
|
567
|
+
const foldedCandidates = candidates.map(([c, offset]) => [
|
|
568
|
+
foldForMatching(c),
|
|
569
|
+
offset,
|
|
570
|
+
]);
|
|
571
|
+
return TextSegmentMap._literalHop(foldedCandidates, foldForMatching(remainingWord), true);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Match `remainingWord` again, with tags removed from both sides.
|
|
575
|
+
*
|
|
576
|
+
* A provider may report a word wrapped in tags the text it was given did
|
|
577
|
+
* not have, or the other way round. Removing tags from both sides lets the
|
|
578
|
+
* words themselves be compared.
|
|
579
|
+
*
|
|
580
|
+
* The match is found in text that has no tags, so its position has to be
|
|
581
|
+
* translated back to a position in the real text, tags included, by
|
|
582
|
+
* `rawOffsetAfterCleanChars`.
|
|
583
|
+
*
|
|
584
|
+
* Only `PLACED` comes out of this. A word that runs past the end of the
|
|
585
|
+
* segment is left to the two earlier passes.
|
|
586
|
+
*/
|
|
587
|
+
static _markupHop(segmentRemaining, remainingWord) {
|
|
588
|
+
const stripped = segmentRemaining.trimStart();
|
|
589
|
+
const leadWs = segmentRemaining.length - stripped.length;
|
|
590
|
+
const haystack = stripMarkup(stripped);
|
|
591
|
+
for (const candidate of TextSegmentMap._wordVariants(stripMarkup(remainingWord))) {
|
|
592
|
+
if (candidate && haystack.startsWith(candidate)) {
|
|
593
|
+
const rawLen = rawOffsetAfterCleanChars(stripped, candidate.length);
|
|
594
|
+
return hop(_HopKind.PLACED, { segmentAdvance: leadWs + rawLen });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Look for `remainingWord` a few words further into this segment.
|
|
601
|
+
*
|
|
602
|
+
* Reached only once the three strategies above have all declined, which
|
|
603
|
+
* means the provider garbled or dropped an event and the text at the cursor
|
|
604
|
+
* will never be reported on its own. Matching a word a little further on
|
|
605
|
+
* puts the segment back in step, and the text stepped over is consumed with
|
|
606
|
+
* the word that found it rather than being lost.
|
|
607
|
+
*
|
|
608
|
+
* Two limits keep a coincidence from being read as a recovery. Only whole
|
|
609
|
+
* words anchor the match, so a partial prefix is not enough, and only the
|
|
610
|
+
* next `LOOKAHEAD_WORDS` of them are tried, so a word repeated later in a
|
|
611
|
+
* long segment cannot swallow everything before it.
|
|
612
|
+
*
|
|
613
|
+
* Word separators are what those anchors are found by, so a script written
|
|
614
|
+
* without them -- Japanese, Chinese -- offers a single word start for the
|
|
615
|
+
* whole run and never matches here; frames in one recover through
|
|
616
|
+
* force-complete instead. Spaced scripts, Korean among them, recover
|
|
617
|
+
* normally.
|
|
618
|
+
*
|
|
619
|
+
* `_foldedHop` alone does the matching, since it already demands the whole
|
|
620
|
+
* word this needs and folding only ever widens what matches -- the fold is
|
|
621
|
+
* one character for one, so anything matching literally matches folded too.
|
|
622
|
+
*
|
|
623
|
+
* A segment holding markup is left alone: tag names are made of letters, so
|
|
624
|
+
* a word start inside one would read as something spoken. That also keeps
|
|
625
|
+
* the match from anchoring inside a rewritten span whose words carry tags.
|
|
626
|
+
*/
|
|
627
|
+
static _lookaheadHop(segmentRemaining, remainingWord) {
|
|
628
|
+
if (segmentRemaining.includes("<")) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
const wordStarts = [];
|
|
632
|
+
let prevAlnum = false;
|
|
633
|
+
let i = 0;
|
|
634
|
+
for (const ch of segmentRemaining) {
|
|
635
|
+
const alnum = isAlnum(ch);
|
|
636
|
+
if (alnum && !prevAlnum) {
|
|
637
|
+
wordStarts.push(i);
|
|
638
|
+
}
|
|
639
|
+
prevAlnum = alnum;
|
|
640
|
+
i += ch.length;
|
|
641
|
+
}
|
|
642
|
+
// The first word start is where the strategies above already looked.
|
|
643
|
+
for (const offset of wordStarts.slice(1, 1 + TextSegmentMap.LOOKAHEAD_WORDS)) {
|
|
644
|
+
const candidates = [[segmentRemaining.slice(offset), offset]];
|
|
645
|
+
const h = TextSegmentMap._foldedHop(candidates, remainingWord);
|
|
646
|
+
if (h !== null && h.kind === _HopKind.PLACED) {
|
|
647
|
+
return h;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Decide what `remainingWord` does to the text left in this segment.
|
|
654
|
+
*
|
|
655
|
+
* Everything here is plain string comparison. No tag names are understood,
|
|
656
|
+
* and nothing is remembered between calls.
|
|
657
|
+
*
|
|
658
|
+
* Four ways of matching are tried, each more forgiving than the one before:
|
|
659
|
+
* `_literalHop`, then `_foldedHop`, then `_markupHop`. Any of them can
|
|
660
|
+
* report that the word fits here (`PLACED`) or that it runs past the end of
|
|
661
|
+
* the segment (`CROSSES`). Last comes `_lookaheadHop`, which places the word
|
|
662
|
+
* a few words further in, stepping over text a garbled or dropped event left
|
|
663
|
+
* unaccounted for.
|
|
664
|
+
*
|
|
665
|
+
* If none of them match, the answer depends on what is left in the segment:
|
|
666
|
+
*
|
|
667
|
+
* - `EXHAUSTED` when nothing here can be spoken -- only a tag such as
|
|
668
|
+
* `<break/>`, or trailing spaces and punctuation. The segment is finished
|
|
669
|
+
* so the word can try the next one. This is checked last, so a word that
|
|
670
|
+
* really does match something like a trailing emoji is found by the
|
|
671
|
+
* passes above first.
|
|
672
|
+
* - `NO_MATCH` otherwise. The word is taken to stand for the next run of
|
|
673
|
+
* punctuation (see `_unmatchedSymbolLen`), so the cursors step past that
|
|
674
|
+
* run alone, never past a letter or digit.
|
|
675
|
+
*/
|
|
676
|
+
static _classifyHop(segmentRemaining, remainingWord) {
|
|
677
|
+
const candidates = TextSegmentMap._matchCandidates(segmentRemaining);
|
|
678
|
+
let h = TextSegmentMap._literalHop(candidates, remainingWord);
|
|
679
|
+
if (h === null) {
|
|
680
|
+
h = TextSegmentMap._foldedHop(candidates, remainingWord);
|
|
681
|
+
}
|
|
682
|
+
if (h === null) {
|
|
683
|
+
h = TextSegmentMap._markupHop(segmentRemaining, remainingWord);
|
|
684
|
+
}
|
|
685
|
+
if (h === null) {
|
|
686
|
+
h = TextSegmentMap._lookaheadHop(segmentRemaining, remainingWord);
|
|
687
|
+
}
|
|
688
|
+
if (h !== null) {
|
|
689
|
+
return h;
|
|
690
|
+
}
|
|
691
|
+
// Nothing left here that can be spoken: finish the segment so the word
|
|
692
|
+
// can try the next one.
|
|
693
|
+
if (!hasAlnum(segmentRemaining)) {
|
|
694
|
+
return hop(_HopKind.EXHAUSTED);
|
|
695
|
+
}
|
|
696
|
+
return hop(_HopKind.NO_MATCH, {
|
|
697
|
+
segmentAdvance: TextSegmentMap._unmatchedSymbolLen(segmentRemaining),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Count how far a word that matched nothing moves the cursor.
|
|
702
|
+
*
|
|
703
|
+
* Such a word is usually a symbol the provider reports differently from the
|
|
704
|
+
* text -- `"-"` for `"โ"` -- so it stands for the next run of punctuation,
|
|
705
|
+
* and the cursor steps past that run alone:
|
|
706
|
+
*
|
|
707
|
+
* " โ step two" -> 2, stops before " step"
|
|
708
|
+
* " โ ### **Title**" -> 2, stops before " ###"
|
|
709
|
+
*
|
|
710
|
+
* Stopping at whitespace is what matters: the symbols after it (`###`,
|
|
711
|
+
* `**`) arrive as events of their own, and stepping past them would leave
|
|
712
|
+
* those events nothing to match.
|
|
713
|
+
*/
|
|
714
|
+
static _unmatchedSymbolLen(segmentRemaining) {
|
|
715
|
+
let i = segmentRemaining.length - segmentRemaining.trimStart().length;
|
|
716
|
+
while (i < segmentRemaining.length) {
|
|
717
|
+
const ch = charAt(segmentRemaining, i);
|
|
718
|
+
if (isAlnum(ch) || isSpace(ch)) {
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
i += ch.length;
|
|
722
|
+
}
|
|
723
|
+
return i;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Move every cursor to `newPos` within `seg`, and finish `seg` if reached.
|
|
727
|
+
*
|
|
728
|
+
* This is where the "keep pace or wait" rule from the class docstring is
|
|
729
|
+
* applied, and the only place the cursors into the other two texts move.
|
|
730
|
+
*
|
|
731
|
+
* **Unchanged segment** -- both sides hold the same text, so the other two
|
|
732
|
+
* cursors keep pace. They move by a count, not a position: however many
|
|
733
|
+
* letters and digits this step used up in the spoken text is how many they
|
|
734
|
+
* move past. `advanceByAlnums` does that, crossing tags for free (which is
|
|
735
|
+
* how a tag travels with the word beside it) and taking any punctuation
|
|
736
|
+
* stuck to the end of the word, so `"you?"` moves as one.
|
|
737
|
+
*
|
|
738
|
+
* **Rewritten segment** -- the other two cursors do not move at all until
|
|
739
|
+
* the segment is finished, then jump straight to its end.
|
|
740
|
+
*
|
|
741
|
+
* Either way a cursor stops before trailing spaces, which belong to
|
|
742
|
+
* whatever comes next.
|
|
743
|
+
*/
|
|
744
|
+
_advanceCursorsTo(seg, newPos) {
|
|
745
|
+
if (seg.isTransformed) {
|
|
746
|
+
// Whatever is left is only a closing tag or the like, which no word
|
|
747
|
+
// event will ever name. Take it now so the segment can finish.
|
|
748
|
+
// Unchanged segments are not given this: a trailing emoji there is
|
|
749
|
+
// real output, and its own event is still coming.
|
|
750
|
+
if (!hasAlnum(seg.tts.slice(newPos))) {
|
|
751
|
+
newPos = seg.tts.length;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
this._keepDerivedCursorsInPace(seg, newPos);
|
|
756
|
+
}
|
|
757
|
+
this._segRawPos = newPos;
|
|
758
|
+
if (newPos >= seg.tts.length) {
|
|
759
|
+
if (seg.isTransformed) {
|
|
760
|
+
this._commitTransformedSpan(seg);
|
|
761
|
+
}
|
|
762
|
+
this._finishSegment(seg);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Move the cursors into the other two texts by what this step just spoke.
|
|
767
|
+
*
|
|
768
|
+
* The user-facing cursor moves by the count of letters and digits consumed
|
|
769
|
+
* here. The two LLM cursors have their own rule -- see `_advanceLlmCursors`.
|
|
770
|
+
*/
|
|
771
|
+
_keepDerivedCursorsInPace(seg, newPos) {
|
|
772
|
+
const crossed = seg.tts.slice(this._segRawPos, newPos);
|
|
773
|
+
const nAlnum = alnumOnly(crossed).length;
|
|
774
|
+
if (nAlnum) {
|
|
775
|
+
this._userFacingPos = advanceByAlnums(this._originalText, this._userFacingPos, nAlnum);
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
// A token with no letters or digits to spend -- punctuation set
|
|
779
|
+
// off by a space, as French writes it ("va ?", "Attention :").
|
|
780
|
+
// There is no budget to advance by, so step straight to where the
|
|
781
|
+
// raw cursor got to, and the mark leaves the remaining text now
|
|
782
|
+
// rather than a word later. Both sides are identical here, so that
|
|
783
|
+
// offset is exact.
|
|
784
|
+
this._userFacingPos = seg.originalStart + seg.tts.slice(0, newPos).trimEnd().length;
|
|
785
|
+
}
|
|
786
|
+
this._advanceLlmCursors(crossed, nAlnum);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Move both LLM cursors for a step that just spoke `crossed`.
|
|
790
|
+
*
|
|
791
|
+
* The two answer different questions and so stop in different places,
|
|
792
|
+
* which is the whole reason there are two of them:
|
|
793
|
+
*
|
|
794
|
+
* - `llmSpokenPos` -- what has been **reported spoken**. It crosses exactly
|
|
795
|
+
* the characters the raw cursor did, so it stops in front of a mark no
|
|
796
|
+
* event has arrived for.
|
|
797
|
+
* - `llmPos` -- what has been **attributed**. It also takes a mark stuck to
|
|
798
|
+
* the end of the word, since the conversation context is rebuilt by
|
|
799
|
+
* joining the attributed spans and every character has to belong to one
|
|
800
|
+
* of them: `"Yeah,"` then `"I"` reads back correctly where `"Yeah"` then
|
|
801
|
+
* `", I"` would put a space before the comma.
|
|
802
|
+
*
|
|
803
|
+
* Attribution never moves backwards and never trails what was spoken, so a
|
|
804
|
+
* step that spends no budget on the attributed side -- an emoji, a symbol
|
|
805
|
+
* the source spells differently -- is still credited to the word that
|
|
806
|
+
* crossed it.
|
|
807
|
+
*/
|
|
808
|
+
_advanceLlmCursors(crossed, nAlnum) {
|
|
809
|
+
this._llmSpokenPos = advanceByChars(this._llmText, this._llmSpokenPos, crossed.length);
|
|
810
|
+
this._llmPos = Math.max(advanceByAlnums(this._llmText, this._llmPos, nAlnum), this._llmSpokenPos);
|
|
811
|
+
}
|
|
812
|
+
/** Jump the other two cursors to the end of `seg`, now that it is done. */
|
|
813
|
+
_commitTransformedSpan(seg) {
|
|
814
|
+
this._userFacingPos = seg.originalEnd;
|
|
815
|
+
// The original's count, not the TTS side's: llmText holds "$42.50"
|
|
816
|
+
// (4 alnums), never the spoken "forty two dollars".
|
|
817
|
+
this._llmPos = advanceByAlnums(this._llmText, this._llmPos, seg.originalAlnumCount);
|
|
818
|
+
// The span is spoken in full or not at all, so both cursors land together.
|
|
819
|
+
this._llmSpokenPos = this._llmPos;
|
|
820
|
+
}
|
|
821
|
+
/** Record `seg` as finished and move on to the next segment. */
|
|
822
|
+
_finishSegment(seg) {
|
|
823
|
+
this._lastCompleted = seg;
|
|
824
|
+
this._segIdx += 1;
|
|
825
|
+
this._segRawPos = 0;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Work out what `word` would do, without moving anything.
|
|
829
|
+
*
|
|
830
|
+
* This decides; `_consumeWord` acts. Keeping them apart is what stops them
|
|
831
|
+
* disagreeing, since `_canConsumeWord` asks the same question and must get
|
|
832
|
+
* the same answer.
|
|
833
|
+
*
|
|
834
|
+
* Most words are placed by the first segment tried, and the walk stops
|
|
835
|
+
* there. It goes on when a segment cannot finish the job -- the word runs
|
|
836
|
+
* past it, or it has nothing speakable left -- and whatever remains of the
|
|
837
|
+
* word is offered to the next segment.
|
|
838
|
+
*
|
|
839
|
+
* @returns What each segment answered, in order, and whatever is left of
|
|
840
|
+
* the word once the segments run out. Anything left over is the word
|
|
841
|
+
* running past the end of this text.
|
|
842
|
+
*/
|
|
843
|
+
_planHops(word) {
|
|
844
|
+
let segIdx = this._segIdx;
|
|
845
|
+
let rawPos = this._segRawPos;
|
|
846
|
+
let remainingWord = word;
|
|
847
|
+
const hops = [];
|
|
848
|
+
while (remainingWord && segIdx < this._segments.length) {
|
|
849
|
+
const h = TextSegmentMap._classifyHop(this._segments[segIdx].tts.slice(rawPos), remainingWord);
|
|
850
|
+
hops.push(h);
|
|
851
|
+
// PLACED and NO_MATCH both end the walk; the other two carry on.
|
|
852
|
+
if (h.kind === _HopKind.PLACED || h.kind === _HopKind.NO_MATCH) {
|
|
853
|
+
return [hops, ""];
|
|
854
|
+
}
|
|
855
|
+
remainingWord = remainingWord.slice(h.wordConsumed);
|
|
856
|
+
segIdx += 1;
|
|
857
|
+
rawPos = 0;
|
|
858
|
+
}
|
|
859
|
+
return [hops, remainingWord];
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Move the cursors according to what `_planHops` decided.
|
|
863
|
+
*
|
|
864
|
+
* Anything left unplaced ran past the end of this text and is kept in
|
|
865
|
+
* `lastOverflow`, for the caller to hand to the next frame.
|
|
866
|
+
*/
|
|
867
|
+
_consumeWord(word) {
|
|
868
|
+
const [hops, overflow] = this._planHops(word);
|
|
869
|
+
for (const h of hops) {
|
|
870
|
+
const seg = this._segments[this._segIdx];
|
|
871
|
+
if (h.kind === _HopKind.PLACED || h.kind === _HopKind.NO_MATCH) {
|
|
872
|
+
// NO_MATCH is a symbol the provider reports differently from the
|
|
873
|
+
// text ("-" for "โ"). It was still spoken, so every cursor moves
|
|
874
|
+
// past the symbol it stands for, the same as a placed word.
|
|
875
|
+
this._advanceCursorsTo(seg, this._segRawPos + h.segmentAdvance);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
// CROSSES or EXHAUSTED: this segment is done either way, and the
|
|
879
|
+
// next hop was classified against the one after it.
|
|
880
|
+
this._advanceCursorsTo(seg, seg.tts.length);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (overflow) {
|
|
884
|
+
this._lastOverflow = overflow;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Take one spoken word and move every cursor to where it ends.
|
|
889
|
+
*
|
|
890
|
+
* Afterwards `lastCompletedSegment`, `lastOverflow` and
|
|
891
|
+
* `lastLeadingDuplicate` describe what this particular word did; each is
|
|
892
|
+
* cleared at the start of the next call.
|
|
893
|
+
*
|
|
894
|
+
* @param word One token from the provider's word-timestamp stream. It may
|
|
895
|
+
* be a plain word, a word carrying its own spacing or punctuation, or a
|
|
896
|
+
* fragment of a half-open tag -- matching is textual, so the caller does
|
|
897
|
+
* not have to know which.
|
|
898
|
+
*/
|
|
899
|
+
advanceWord(word) {
|
|
900
|
+
this._lastCompleted = null;
|
|
901
|
+
this._lastOverflow = null;
|
|
902
|
+
this._lastLeadingDuplicate = 0;
|
|
903
|
+
if (word) {
|
|
904
|
+
this._lastLeadingDuplicate = this._leadingDuplicateLen(word);
|
|
905
|
+
this._consumeWord(word);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Count characters at the start of `word` that were already spoken.
|
|
910
|
+
*
|
|
911
|
+
* In `"Yeah, I can"` the comma travels with `"Yeah"`. If the provider then
|
|
912
|
+
* reports the next word as `", I"` instead of `"I"`, the comma would be
|
|
913
|
+
* recorded twice, so this returns 2 -- the comma and its space.
|
|
914
|
+
*
|
|
915
|
+
* Returns 0 when that punctuation is new text instead (`'"hello'`), and
|
|
916
|
+
* when the word is nothing but punctuation, which is a mark arriving on its
|
|
917
|
+
* own. Must be called before the cursors move, while `llmPos` still sits at
|
|
918
|
+
* the end of the previous word.
|
|
919
|
+
*/
|
|
920
|
+
_leadingDuplicateLen(word) {
|
|
921
|
+
let i = 0;
|
|
922
|
+
while (i < word.length && (isSpace(word[i]) || isPunctuation(word[i]))) {
|
|
923
|
+
i += 1;
|
|
924
|
+
}
|
|
925
|
+
if (i >= word.length) {
|
|
926
|
+
return 0;
|
|
927
|
+
}
|
|
928
|
+
const mark = word.slice(0, i).trim();
|
|
929
|
+
if (!mark) {
|
|
930
|
+
return 0;
|
|
931
|
+
}
|
|
932
|
+
const start = this._llmPos - mark.length;
|
|
933
|
+
if (start < 0 || this._llmText.slice(start, this._llmPos) !== mark) {
|
|
934
|
+
return 0;
|
|
935
|
+
}
|
|
936
|
+
return i;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Return true if `word` could be the next thing spoken here.
|
|
940
|
+
*
|
|
941
|
+
* `advanceWord` without the moving, so a caller can check first. A false
|
|
942
|
+
* answer means the provider skipped ahead, and the word should go to the
|
|
943
|
+
* next frame instead.
|
|
944
|
+
*
|
|
945
|
+
* A word with no letters or digits gets a second chance from
|
|
946
|
+
* `_symbolBelongsHere`, since there is nothing in it to match on.
|
|
947
|
+
*/
|
|
948
|
+
wordBelongsCurrentSegment(word) {
|
|
949
|
+
if (!word) {
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
if (this._canConsumeWord(word)) {
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
if (!hasAlnum(word)) {
|
|
956
|
+
return this._symbolBelongsHere(word);
|
|
957
|
+
}
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Ask whether this word would be placed, without placing it.
|
|
962
|
+
*
|
|
963
|
+
* True if some segment would take the word, or if the word simply runs off
|
|
964
|
+
* the end. False if there are no segments left, or a segment turns it down.
|
|
965
|
+
*/
|
|
966
|
+
_canConsumeWord(word) {
|
|
967
|
+
if (this._segIdx >= this._segments.length) {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
const [hops] = this._planHops(word);
|
|
971
|
+
return hops.length === 0 || hops[hops.length - 1].kind !== _HopKind.NO_MATCH;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Decide whether a word made only of punctuation or symbols belongs here.
|
|
975
|
+
*
|
|
976
|
+
* There is nothing in such a word to match on, so it gets two chances:
|
|
977
|
+
*
|
|
978
|
+
* 1. Look for the word itself in the text still to be spoken. The search
|
|
979
|
+
* starts a little before the cursor, because punctuation is often taken
|
|
980
|
+
* along with the word before it.
|
|
981
|
+
*
|
|
982
|
+
* 2. Accept it as a stand-in. Some providers report a different symbol than
|
|
983
|
+
* the one they were given -- ElevenLabs reports `"->"` as `"-"` -- so the
|
|
984
|
+
* first check can never succeed. If words remain to be spoken and the
|
|
985
|
+
* next thing in the text is itself a symbol, treat the word as that
|
|
986
|
+
* symbol.
|
|
987
|
+
*/
|
|
988
|
+
_symbolBelongsHere(word) {
|
|
989
|
+
let pos = this.rawPos;
|
|
990
|
+
let searchStart = pos;
|
|
991
|
+
while (searchStart > 0) {
|
|
992
|
+
const ch = this._ttsText[searchStart - 1];
|
|
993
|
+
if (isAlnum(ch) || isSpace(ch) || ch === ">") {
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
searchStart -= 1;
|
|
997
|
+
}
|
|
998
|
+
if (this._ttsText.slice(searchStart).includes(word)) {
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
if (this._segIdx >= this._segments.length) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
while (pos < this._ttsText.length && isSpace(this._ttsText[pos])) {
|
|
1005
|
+
pos += 1;
|
|
1006
|
+
}
|
|
1007
|
+
return pos < this._ttsText.length && !isAlnum(charAt(this._ttsText, pos));
|
|
1008
|
+
}
|
|
1009
|
+
/** How far into the user-facing text the spoken words have reached. */
|
|
1010
|
+
get userFacingPos() {
|
|
1011
|
+
return this._userFacingPos;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* How far into the LLM's text has been attributed to a word so far.
|
|
1015
|
+
*
|
|
1016
|
+
* Ahead of `llmSpokenPos` whenever a word swept up a mark that no event has
|
|
1017
|
+
* reported yet; the text between the two is attributed but not yet spoken.
|
|
1018
|
+
*/
|
|
1019
|
+
get llmPos() {
|
|
1020
|
+
return this._llmPos;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* How far into the LLM's text the provider has actually reported speaking.
|
|
1024
|
+
*
|
|
1025
|
+
* What a caller showing progress, or ending a frame early, should read:
|
|
1026
|
+
* text past this point has had no word event, so it still belongs to what
|
|
1027
|
+
* is left to say even when `llmPos` has already credited it to a word.
|
|
1028
|
+
*/
|
|
1029
|
+
get llmSpokenPos() {
|
|
1030
|
+
return this._llmSpokenPos;
|
|
1031
|
+
}
|
|
1032
|
+
/** How far into `ttsText` the provider has spoken, counted from its start. */
|
|
1033
|
+
get rawPos() {
|
|
1034
|
+
let pos = 0;
|
|
1035
|
+
for (const s of this._segments.slice(0, this._segIdx)) {
|
|
1036
|
+
pos += s.tts.length;
|
|
1037
|
+
}
|
|
1038
|
+
if (this._segIdx < this._segments.length) {
|
|
1039
|
+
pos += this._segRawPos;
|
|
1040
|
+
}
|
|
1041
|
+
return pos;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* The end of the last word passed to `advanceWord`, if it did not fit.
|
|
1045
|
+
*
|
|
1046
|
+
* `null` most of the time. It is set only when that word ran past the end
|
|
1047
|
+
* of `ttsText`, with no segment left to take the rest, which means the
|
|
1048
|
+
* leftover belongs to the next frame. It is always the tail of the word
|
|
1049
|
+
* that was passed in, so the part that did fit is
|
|
1050
|
+
* `word.slice(0, word.length - lastOverflow.length)`.
|
|
1051
|
+
*/
|
|
1052
|
+
get lastOverflow() {
|
|
1053
|
+
return this._lastOverflow;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* How much of the last word's start was punctuation already spoken.
|
|
1057
|
+
*
|
|
1058
|
+
* The opposite end of the word from `lastOverflow`: that one is about a
|
|
1059
|
+
* tail running past this text, this one about a head repeating punctuation
|
|
1060
|
+
* the previous word already took. Cut both off to get the part of the word
|
|
1061
|
+
* that belongs to this frame:
|
|
1062
|
+
*
|
|
1063
|
+
* word.slice(lastLeadingDuplicate, word.length - (lastOverflow ?? "").length)
|
|
1064
|
+
*/
|
|
1065
|
+
get lastLeadingDuplicate() {
|
|
1066
|
+
return this._lastLeadingDuplicate;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* True once every letter and digit in the text has been spoken.
|
|
1070
|
+
*
|
|
1071
|
+
* This is not the same as the cursor reaching the end. If all that is left
|
|
1072
|
+
* is punctuation or tags, the text counts as finished even though those
|
|
1073
|
+
* characters have not been walked over, because no word event is coming
|
|
1074
|
+
* for them.
|
|
1075
|
+
*
|
|
1076
|
+
* There is one exception. Punctuation separated from its word by a space,
|
|
1077
|
+
* as French writes `"Comment รงa va ?"`, does arrive as its own word event,
|
|
1078
|
+
* so the text stays unfinished until it does (see
|
|
1079
|
+
* `_pendingSeparatedPunctuation`). Punctuation stuck to the word itself, as
|
|
1080
|
+
* in `"you?"`, was already taken with the word.
|
|
1081
|
+
*/
|
|
1082
|
+
get isComplete() {
|
|
1083
|
+
if (this._segIdx >= this._segments.length) {
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
const seg = this._segments[this._segIdx];
|
|
1087
|
+
if (hasAlnum(seg.tts.slice(this._segRawPos))) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
if (TextSegmentMap._pendingSeparatedPunctuation(seg.tts.slice(this._segRawPos))) {
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
return this._segments.slice(this._segIdx + 1).every((s) => !hasAlnum(s.tts));
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* True when all that is left is punctuation set off from its word by a space.
|
|
1097
|
+
*
|
|
1098
|
+
* Some languages write a space before a mark, as in `"va ?"` or
|
|
1099
|
+
* `"Bonjour !"`. A TTS reports that mark as a word of its own, so the
|
|
1100
|
+
* segment has to stay open until it arrives.
|
|
1101
|
+
*
|
|
1102
|
+
* Only real punctuation counts. A trailing emoji or arrow (`"day! ๐"`,
|
|
1103
|
+
* `"โ"`) never arrives as its own word, so it must not keep the segment
|
|
1104
|
+
* open, and tags are removed first for the same reason. Only called once no
|
|
1105
|
+
* letters or digits are left.
|
|
1106
|
+
*/
|
|
1107
|
+
static _pendingSeparatedPunctuation(remaining) {
|
|
1108
|
+
const strippedMarkup = stripCompleteMarkup(remaining);
|
|
1109
|
+
if (!isSpace(strippedMarkup[0])) {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
const content = strippedMarkup.trim();
|
|
1113
|
+
return content.length > 0 && isPunctuation(charAt(content, 0));
|
|
1114
|
+
}
|
|
1115
|
+
/** True when the cursor is partway through a rewritten segment. */
|
|
1116
|
+
get inTransformedSegment() {
|
|
1117
|
+
if (this._segIdx >= this._segments.length) {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
const seg = this._segments[this._segIdx];
|
|
1121
|
+
return seg.isTransformed && this._segRawPos > 0;
|
|
1122
|
+
}
|
|
1123
|
+
/** The segment finished by the last `advanceWord` call, if any. */
|
|
1124
|
+
get lastCompletedSegment() {
|
|
1125
|
+
return this._lastCompleted;
|
|
1126
|
+
}
|
|
1127
|
+
/** Put every cursor back to the start of the text. */
|
|
1128
|
+
reset() {
|
|
1129
|
+
this._resetState();
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
//# sourceMappingURL=text_segment_map.js.map
|