diff --git a/dashscope/api_entities/api_request_factory.py b/dashscope/api_entities/api_request_factory.py index c231287a..41f05aa7 100644 --- a/dashscope/api_entities/api_request_factory.py +++ b/dashscope/api_entities/api_request_factory.py @@ -1,13 +1,17 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +from typing import Any, Dict, Optional, Union from urllib.parse import urlencode +import aiohttp +import requests + import dashscope from dashscope.api_entities.api_request_data import ApiRequestData +from dashscope.api_entities.encryption import Encryption from dashscope.api_entities.http_request import HttpRequest from dashscope.api_entities.websocket_request import WebSocketRequest from dashscope.common.constants import ( - REQUEST_TIMEOUT_KEYWORD, SERVICE_API_PATH, ApiProtocol, HTTPMethod, @@ -15,31 +19,105 @@ from dashscope.common.error import InputDataRequired, UnsupportedApiProtocol from dashscope.common.logging import logger from dashscope.protocol.websocket import WebsocketStreamingMode -from dashscope.api_entities.encryption import Encryption -def _get_protocol_params(kwargs): - api_protocol = kwargs.pop("api_protocol", ApiProtocol.HTTPS) - ws_stream_mode = kwargs.pop("ws_stream_mode", WebsocketStreamingMode.OUT) - is_binary_input = kwargs.pop("is_binary_input", False) - http_method = kwargs.pop("http_method", HTTPMethod.POST) - stream = kwargs.get("stream", False) +def _build_api_request( # pylint: disable=too-many-branches + # pylint: disable=too-many-arguments,too-many-locals + model: str, + input: object, # pylint: disable=redefined-builtin + task_group: str, + task: str, + function: str, + api_key: str, + is_service: bool = True, + # Protocol and connection configuration + api_protocol: ApiProtocol = ApiProtocol.HTTPS, + http_method: HTTPMethod = HTTPMethod.POST, + stream: bool = False, + async_request: bool = False, + request_timeout: Optional[int] = None, + # WebSocket specific + ws_stream_mode: WebsocketStreamingMode = WebsocketStreamingMode.OUT, + is_binary_input: bool = False, + # HTTP specific + query: bool = False, + headers: Optional[Dict[str, str]] = None, + form: Optional[Dict] = None, + resources: Optional[Dict] = None, + base_address: Optional[str] = None, + flattened_output: bool = False, + extra_url_parameters: Optional[Dict[str, Any]] = None, + user_agent: str = "", + session: Optional[Union[requests.Session, aiohttp.ClientSession]] = None, + task_id: Optional[str] = None, + enable_encryption: bool = False, + pre_task_id: Optional[str] = None, + # Additional parameters for API request data + **kwargs, +): + # pylint: disable=too-many-statements + """Build API request object. + + Args: + model (str): The model name. + input (object): The input data for the request. + task_group (str): The task group for the API path. + task (str): The task name for the API path. + function (str): The function name for the API path. + api_key (str): The API key for authentication. + is_service (bool, optional): Whether this is a service call. + Defaults to True. + api_protocol (ApiProtocol, optional): The protocol to use + (HTTP, HTTPS, WEBSOCKET). Defaults to ApiProtocol.HTTPS. + http_method (HTTPMethod, optional): The HTTP method (GET, POST). + Defaults to HTTPMethod.POST. + stream (bool, optional): Enable streaming output. + Defaults to False. + async_request (bool, optional): Enable async request. + Defaults to False. + request_timeout (int, optional): Request timeout in seconds. + Defaults to None. + ws_stream_mode (WebsocketStreamingMode, optional): WebSocket + streaming mode. Defaults to WebsocketStreamingMode.OUT. + is_binary_input (bool, optional): Whether input is binary data. + Defaults to False. + query (bool, optional): Whether this is a query request. + Defaults to False. + headers (Dict[str, str], optional): Additional HTTP headers. + Defaults to None. + form (Dict, optional): Form data for multipart requests. + Defaults to None. + resources (Dict, optional): Resource data. Defaults to None. + base_address (str, optional): Custom base URL for the API. + Defaults to None. + flattened_output (bool, optional): Whether to flatten output. + Defaults to False. + extra_url_parameters (Dict[str, Any], optional): Extra URL query + parameters. Defaults to None. + user_agent (str, optional): Custom user agent string. + Defaults to "". + session (Union[requests.Session, aiohttp.ClientSession], optional): + Custom session for connection reuse. Defaults to None. + task_id (str, optional): Task ID for the request. + Defaults to None. + enable_encryption (bool, optional): Enable request encryption. + Defaults to False. + pre_task_id (str, optional): Previous task ID for WebSocket. + Defaults to None. + **kwargs: Additional parameters passed to the API request data. + + Returns: + HttpRequest or WebSocketRequest: The constructed request object. + + Raises: + InputDataRequired: If input data is missing or invalid. + UnsupportedApiProtocol: If the API protocol is not supported. + """ + # Handle stream mode for WebSocket if not stream and ws_stream_mode == WebsocketStreamingMode.OUT: ws_stream_mode = WebsocketStreamingMode.NONE - async_request = kwargs.pop("async_request", False) - query = kwargs.pop("query", False) - headers = kwargs.pop("headers", None) - request_timeout = kwargs.pop(REQUEST_TIMEOUT_KEYWORD, None) - form = kwargs.pop("form", None) - resources = kwargs.pop("resources", None) - base_address = kwargs.pop("base_address", None) - flattened_output = kwargs.pop("flattened_output", False) - extra_url_parameters = kwargs.pop("extra_url_parameters", None) - session = kwargs.pop("session", None) - - # Extract user_agent from kwargs (preferred) or from headers["user-agent"] - user_agent = kwargs.pop("user_agent", "") + # Handle user_agent from headers if headers and "user-agent" in headers: header_ua = headers.pop("user-agent") if user_agent: @@ -49,56 +127,6 @@ def _get_protocol_params(kwargs): else: user_agent = header_ua - return ( - api_protocol, - ws_stream_mode, - is_binary_input, - http_method, - stream, - async_request, - query, - headers, - request_timeout, - form, - resources, - base_address, - flattened_output, - extra_url_parameters, - user_agent, - session, - ) - - -def _build_api_request( # pylint: disable=too-many-branches - model: str, - input: object, # pylint: disable=redefined-builtin - task_group: str, - task: str, - function: str, - api_key: str, - is_service=True, - **kwargs, -): - ( - api_protocol, - ws_stream_mode, - is_binary_input, - http_method, - stream, - async_request, - query, - headers, - request_timeout, - form, - resources, - base_address, - flattened_output, - extra_url_parameters, - user_agent, - session, - ) = _get_protocol_params(kwargs) - task_id = kwargs.pop("task_id", None) - enable_encryption = kwargs.pop("enable_encryption", False) encryption = None if api_protocol in [ApiProtocol.HTTP, ApiProtocol.HTTPS]: @@ -146,7 +174,6 @@ def _build_api_request( # pylint: disable=too-many-branches websocket_url = base_address else: websocket_url = dashscope.base_websocket_api_url - pre_task_id = kwargs.pop("pre_task_id", None) request = WebSocketRequest( url=websocket_url, api_key=api_key, diff --git a/dashscope/app/application.py b/dashscope/app/application.py index 0926cbd2..9ee99b1a 100644 --- a/dashscope/app/application.py +++ b/dashscope/app/application.py @@ -6,7 +6,7 @@ @Desc : Application calls for both http and http sse """ import copy -from typing import Generator, List, Union +from typing import Any, Dict, Generator, List, Optional, Union from dashscope.api_entities.api_request_factory import _build_api_request from dashscope.api_entities.dashscope_response import Message, Role @@ -54,6 +54,8 @@ def _validate_params( # pylint: disable=arguments-renamed @classmethod def call( # type: ignore[override] + # pylint: disable=too-many-locals,arguments-renamed + # pylint: disable=too-many-branches,too-many-statements cls, app_id: str, prompt: str = None, @@ -61,6 +63,21 @@ def call( # type: ignore[override] workspace: str = None, api_key: str = None, messages: List[Message] = None, + stream: Optional[bool] = None, + temperature: Optional[float] = None, + top_p: Optional[float] = None, + top_k: Optional[int] = None, + seed: Optional[int] = None, + session_id: Optional[str] = None, + biz_params: Optional[Dict[str, Any]] = None, + has_thoughts: Optional[bool] = None, + doc_tag_codes: Optional[List[str]] = None, + doc_reference_type: Optional[str] = None, + memory_id: Optional[str] = None, + image_list: Optional[List[str]] = None, + file_list: Optional[List[str]] = None, + rag_options: Optional[Dict[str, Any]] = None, + incremental_output: Optional[bool] = None, **kwargs, ) -> Union[ ApplicationResponse, @@ -84,51 +101,51 @@ def call( # type: ignore[override] api_key (str, optional): The api api_key, can be None, if None, will get by default rule(TODO: api key doc). messages(list): The generation messages. + stream (bool, optional): Enable server-sent events + (ref: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) # noqa E501 # pylint: disable=line-too-long + the result will back partially[qwen-turbo,bailian-v1]. + temperature (float, optional): Used to control the degree + of randomness and diversity. Specifically, the temperature + value controls the degree to which the probability distribution + of each candidate word is smoothed when generating text. + A higher temperature value will reduce the peak value of + the probability, allowing more low-probability words to be + selected, and the generated results will be more diverse; + while a lower temperature value will enhance the peak value + of the probability, making it easier for high-probability + words to be selected, the generated results are more + deterministic, range(0, 2) .[qwen-turbo,qwen-plus]. + top_p (float, optional): A sampling strategy, called nucleus + sampling, where the model considers the results of the + tokens with top_p probability mass. So 0.1 means only + the tokens comprising the top 10% probability mass are + considered[qwen-turbo,bailian-v1]. + top_k (int, optional): The size of the sample candidate set when generated. # noqa E501 # pylint: disable=line-too-long + For example, when the value is 50, only the 50 highest-scoring tokens # noqa E501 # pylint: disable=line-too-long + in a single generation form a randomly sampled candidate set. # noqa E501 + The larger the value, the higher the randomness generated; # noqa E501 + the smaller the value, the higher the certainty generated. # noqa E501 + The default value is 0, which means the top_k policy is # noqa E501 + not enabled. At this time, only the top_p policy takes effect. # noqa E501 + seed (int, optional): When generating, the seed of the random number is used to control the # pylint: disable=line-too-long + randomness of the model generation. If you use the same seed, each run will generate the same results; # pylint: disable=line-too-long + you can use the same seed when you need to reproduce the model's generated results. # pylint: disable=line-too-long + The seed parameter supports unsigned 64-bit integer types. Default value 1234 + session_id (str, optional): Session if for multiple rounds call. + biz_params (dict, optional): The extra parameters for flow or plugin. + has_thoughts (bool, optional): Flag to return rag or plugin process details. Default value false. # pylint: disable=line-too-long + doc_tag_codes (list[str], optional): Tag code list for doc retrival. + doc_reference_type (str, optional): The type of doc reference. + simple: simple format of doc retrival which not include index in response text but in doc reference list. # pylint: disable=line-too-long + indexed: include both index in response text and doc reference list + memory_id (str, optional): Used to store long term context summary between end users and assistant. # pylint: disable=line-too-long + image_list (list, optional): Used to pass image url list. + file_list (list, optional): Used to pass file url list. + rag_options (dict, optional): Rag options for retrieval augmented generation options. # pylint: disable=line-too-long + incremental_output (bool, optional): In streaming mode, output only + new tokens (True) vs. cumulative output (False). + **kwargs: Additional parameters passed to the API. - **kwargs: - stream(bool, `optional`): Enable server-sent events - (ref: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) # noqa E501 # pylint: disable=line-too-long - the result will back partially[qwen-turbo,bailian-v1]. - temperature(float, `optional`): Used to control the degree - of randomness and diversity. Specifically, the temperature - value controls the degree to which the probability distribution - of each candidate word is smoothed when generating text. - A higher temperature value will reduce the peak value of - the probability, allowing more low-probability words to be - selected, and the generated results will be more diverse; - while a lower temperature value will enhance the peak value - of the probability, making it easier for high-probability - words to be selected, the generated results are more - deterministic, range(0, 2) .[qwen-turbo,qwen-plus]. - top_p(float, `optional`): A sampling strategy, called nucleus - sampling, where the model considers the results of the - tokens with top_p probability mass. So 0.1 means only - the tokens comprising the top 10% probability mass are - considered[qwen-turbo,bailian-v1]. - top_k(int, `optional`): The size of the sample candidate set when generated. # noqa E501 # pylint: disable=line-too-long - For example, when the value is 50, only the 50 highest-scoring tokens # noqa E501 # pylint: disable=line-too-long - in a single generation form a randomly sampled candidate set. # noqa E501 - The larger the value, the higher the randomness generated; # noqa E501 - the smaller the value, the higher the certainty generated. # noqa E501 - The default value is 0, which means the top_k policy is # noqa E501 - not enabled. At this time, only the top_p policy takes effect. # noqa E501 - seed( - int, - `optional` - ): When generating, the seed of the random number is used to control the - randomness of the model generation. If you use the same seed, each run will generate the same results; # pylint: disable=line-too-long - you can use the same seed when you need to reproduce the model's generated results. # pylint: disable=line-too-long - The seed parameter supports unsigned 64-bit integer types. Default value 1234 - session_id(str, `optional`): Session if for multiple rounds call. - biz_params(dict, `optional`): The extra parameters for flow or plugin. - has_thoughts(bool, `optional`): Flag to return rag or plugin process details. Default value false. # pylint: disable=line-too-long - doc_tag_codes(list[str], `optional`): Tag code list for doc retrival. - doc_reference_type(str, `optional`): The type of doc reference. - simple: simple format of doc retrival which not include index in response text but in doc reference list. # pylint: disable=line-too-long - indexed: include both index in response text and doc reference list - memory_id(str, `optional`): Used to store long term context summary between end users and assistant. # pylint: disable=line-too-long - image_list(list, `optional`): Used to pass image url list. - rag_options(dict, `optional`): Rag options for retrieval augmented generation options. # pylint: disable=line-too-long Raises: InvalidInput: The history and auto_history are mutually exclusive. @@ -145,27 +162,63 @@ def call( # type: ignore[override] ): raise InputRequired("prompt or messages is required!") + # Build kwargs from explicit parameters + explicit_kwargs = {} if workspace is not None and workspace: headers = kwargs.pop("headers", {}) headers["X-DashScope-WorkSpace"] = workspace - kwargs["headers"] = headers + explicit_kwargs["headers"] = headers + + if stream is not None: + explicit_kwargs["stream"] = stream + if temperature is not None: + explicit_kwargs["temperature"] = temperature + if top_p is not None: + explicit_kwargs["top_p"] = top_p + if top_k is not None: + explicit_kwargs["top_k"] = top_k + if seed is not None: + explicit_kwargs["seed"] = seed + if session_id is not None: + explicit_kwargs["session_id"] = session_id + if biz_params is not None: + explicit_kwargs["biz_params"] = biz_params + if has_thoughts is not None: + explicit_kwargs["has_thoughts"] = has_thoughts + if doc_tag_codes is not None: + explicit_kwargs["doc_tag_codes"] = doc_tag_codes + if doc_reference_type is not None: + explicit_kwargs["doc_reference_type"] = doc_reference_type + if memory_id is not None: + explicit_kwargs["memory_id"] = memory_id + if image_list is not None: + explicit_kwargs["image_list"] = image_list + if file_list is not None: + explicit_kwargs["file_list"] = file_list + if rag_options is not None: + explicit_kwargs["rag_options"] = rag_options + if incremental_output is not None: + explicit_kwargs["incremental_output"] = incremental_output + + # Merge with remaining kwargs (user-provided extras) + merged_kwargs = {**kwargs, **explicit_kwargs} # Check if we need to merge incremental output (compute once) - is_stream = kwargs.get("stream", False) - is_incremental_output = kwargs.get("incremental_output", None) + is_stream = merged_kwargs.get("stream", False) + is_incremental_output = merged_kwargs.get("incremental_output", None) to_merge_incremental_output = ( is_stream and is_incremental_output is False ) if to_merge_incremental_output: - kwargs["incremental_output"] = True + merged_kwargs["incremental_output"] = True # Pass incremental_to_full flag via user_agent parameter to avoid # overwriting the default SDK user-agent flag = "1" if to_merge_incremental_output else "0" - existing_ua = kwargs.get("user_agent", "") + existing_ua = merged_kwargs.get("user_agent", "") new_ua = f"incremental_to_full/{flag}" - kwargs["user_agent"] = ( + merged_kwargs["user_agent"] = ( f"{existing_ua}; {new_ua}".strip() if existing_ua else new_ua ) @@ -176,7 +229,7 @@ def call( # type: ignore[override] prompt, history, messages, - **kwargs, + **merged_kwargs, ) request = _build_api_request( model="", diff --git a/dashscope/audio/asr/recognition.py b/dashscope/audio/asr/recognition.py index 7dc7bc42..32f05caf 100644 --- a/dashscope/audio/asr/recognition.py +++ b/dashscope/audio/asr/recognition.py @@ -10,7 +10,7 @@ from http import HTTPStatus from queue import Queue from threading import Timer -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Optional, Union from dashscope.api_entities.dashscope_response import RecognitionResponse from dashscope.client.base_api import BaseApi @@ -27,6 +27,36 @@ from dashscope.protocol.websocket import WebsocketStreamingMode +def _merge_recognition_params( + kwargs: Dict[str, Any], + disfluency_removal_enabled: Optional[bool], + diarization_enabled: Optional[bool], + speaker_count: Optional[int], + timestamp_alignment_enabled: Optional[bool], + special_word_filter: Optional[str], + audio_event_detection_enabled: Optional[bool], +) -> Dict[str, Any]: + """Merge explicit recognition parameters into ``kwargs`` in place. + + Only parameters that are not None are written, so callers can pass + through values that the user did not set. + + Returns: + The merged ``kwargs`` dict (the same object). + """ + for key, value in ( + ("disfluency_removal_enabled", disfluency_removal_enabled), + ("diarization_enabled", diarization_enabled), + ("speaker_count", speaker_count), + ("timestamp_alignment_enabled", timestamp_alignment_enabled), + ("special_word_filter", special_word_filter), + ("audio_event_detection_enabled", audio_event_detection_enabled), + ): + if value is not None: + kwargs[key] = value + return kwargs + + class RecognitionResult(RecognitionResponse): """The result set of speech recognition, including the single-sentence recognition result returned by the callback mode, and all recognition @@ -165,6 +195,13 @@ def __init__( format: str, # pylint: disable=redefined-builtin sample_rate: int, workspace: str = None, + # Recognition parameters + disfluency_removal_enabled: Optional[bool] = None, + diarization_enabled: Optional[bool] = None, + speaker_count: Optional[int] = None, + timestamp_alignment_enabled: Optional[bool] = None, + special_word_filter: Optional[str] = None, + audio_event_detection_enabled: Optional[bool] = None, **kwargs, ): if model is None: @@ -185,6 +222,16 @@ def __init__( self._worker = None self._silence_timer = None self._kwargs = kwargs + # Store recognition parameters + _merge_recognition_params( + self._kwargs, + disfluency_removal_enabled, + diarization_enabled, + speaker_count, + timestamp_alignment_enabled, + special_word_filter, + audio_event_detection_enabled, + ) self._workspace = workspace self._start_stream_timestamp = -1 self._first_package_timestamp = -1 @@ -318,7 +365,18 @@ def __launch_request(self): ) return responses - def start(self, phrase_id: str = None, **kwargs): + def start( + self, + phrase_id: str = None, + # Recognition parameters + disfluency_removal_enabled: Optional[bool] = None, + diarization_enabled: Optional[bool] = None, + speaker_count: Optional[int] = None, + timestamp_alignment_enabled: Optional[bool] = None, + special_word_filter: Optional[str] = None, + audio_event_detection_enabled: Optional[bool] = None, + **kwargs, + ): """Real-time speech recognition in asynchronous mode. Please call 'stop()' after you have completed recognition. @@ -354,6 +412,16 @@ def start(self, phrase_id: str = None, **kwargs): self._stop_stream_timestamp = -1 self._on_complete_timestamp = -1 self._phrase = phrase_id + # Update recognition parameters + _merge_recognition_params( + self._kwargs, + disfluency_removal_enabled, + diarization_enabled, + speaker_count, + timestamp_alignment_enabled, + special_word_filter, + audio_event_detection_enabled, + ) self._kwargs.update(**kwargs) self._recognition_once = False self._worker = threading.Thread(target=self.__receive_worker) @@ -372,11 +440,18 @@ def start(self, phrase_id: str = None, **kwargs): self._running = False raise InvalidTask("Invalid task, task create failed.") - # pylint: disable=R1702,too-many-branches,too-many-statements + # pylint: disable=W0237,R1702,too-many-branches,too-many-statements def call( # type: ignore[override] # noqa: E501 self, file: str, phrase_id: str = None, + # Recognition parameters + disfluency_removal_enabled: Optional[bool] = None, + diarization_enabled: Optional[bool] = None, + speaker_count: Optional[int] = None, + timestamp_alignment_enabled: Optional[bool] = None, + special_word_filter: Optional[str] = None, + audio_event_detection_enabled: Optional[bool] = None, **kwargs, ) -> RecognitionResult: """Real-time speech recognition in synchronous mode. @@ -418,6 +493,16 @@ def call( # type: ignore[override] # noqa: E501 self._recognition_once = True self._stream_data = Queue() self._phrase = phrase_id + # Update recognition parameters + _merge_recognition_params( + self._kwargs, + disfluency_removal_enabled, + diarization_enabled, + speaker_count, + timestamp_alignment_enabled, + special_word_filter, + audio_event_detection_enabled, + ) self._kwargs.update(**kwargs) error_flag: bool = False sentences: List[Any] = [] diff --git a/dashscope/audio/asr/translation_recognizer.py b/dashscope/audio/asr/translation_recognizer.py index 2f3405c8..d24d277f 100644 --- a/dashscope/audio/asr/translation_recognizer.py +++ b/dashscope/audio/asr/translation_recognizer.py @@ -9,8 +9,9 @@ from http import HTTPStatus from queue import Queue from threading import Timer -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional +from dashscope.audio.asr.recognition import _merge_recognition_params from dashscope.client.base_api import BaseApi from dashscope.common.constants import ApiProtocol from dashscope.common.error import ( @@ -324,6 +325,13 @@ def __init__( source_language: str = None, translation_enabled: bool = False, workspace: str = None, + # Recognition parameters + disfluency_removal_enabled: Optional[bool] = None, + diarization_enabled: Optional[bool] = None, + speaker_count: Optional[int] = None, + timestamp_alignment_enabled: Optional[bool] = None, + special_word_filter: Optional[str] = None, + audio_event_detection_enabled: Optional[bool] = None, **kwargs, ): if model is None: @@ -347,6 +355,16 @@ def __init__( self._worker = None self._silence_timer = None self._kwargs = kwargs + # Store recognition parameters + _merge_recognition_params( + self._kwargs, + disfluency_removal_enabled, + diarization_enabled, + speaker_count, + timestamp_alignment_enabled, + special_word_filter, + audio_event_detection_enabled, + ) self._workspace = workspace self._start_stream_timestamp = -1 self._first_package_timestamp = -1 @@ -456,7 +474,17 @@ def __launch_request(self): ) return responses - def start(self, **kwargs): + def start( + self, + # Recognition parameters + disfluency_removal_enabled: Optional[bool] = None, + diarization_enabled: Optional[bool] = None, + speaker_count: Optional[int] = None, + timestamp_alignment_enabled: Optional[bool] = None, + special_word_filter: Optional[str] = None, + audio_event_detection_enabled: Optional[bool] = None, + **kwargs, + ): """Real-time translation recognizer in asynchronous mode. Please call 'stop()' after you have completed translation & recognition. # noqa: E501 @@ -493,6 +521,16 @@ def start(self, **kwargs): self._first_package_timestamp = -1 self._stop_stream_timestamp = -1 self._on_complete_timestamp = -1 + # Update recognition parameters + _merge_recognition_params( + self._kwargs, + disfluency_removal_enabled, + diarization_enabled, + speaker_count, + timestamp_alignment_enabled, + special_word_filter, + audio_event_detection_enabled, + ) self._kwargs.update(**kwargs) self._recognition_once = False self._worker = threading.Thread(target=self.__receive_worker) @@ -511,11 +549,18 @@ def start(self, **kwargs): self._running = False raise InvalidTask("Invalid task, task create failed.") - # pylint: disable=too-many-branches,too-many-statements + # pylint: disable=W0237,too-many-branches,too-many-statements def call( # type: ignore[override] self, file: str, phrase_id: str = None, + # Recognition parameters + disfluency_removal_enabled: Optional[bool] = None, + diarization_enabled: Optional[bool] = None, + speaker_count: Optional[int] = None, + timestamp_alignment_enabled: Optional[bool] = None, + special_word_filter: Optional[str] = None, + audio_event_detection_enabled: Optional[bool] = None, **kwargs, ) -> TranslationRecognizerResultPack: """TranslationRecognizerRealtime in synchronous mode. @@ -559,6 +604,16 @@ def call( # type: ignore[override] self._recognition_once = True self._stream_data = Queue() self._phrase = phrase_id + # Update recognition parameters + _merge_recognition_params( + self._kwargs, + disfluency_removal_enabled, + diarization_enabled, + speaker_count, + timestamp_alignment_enabled, + special_word_filter, + audio_event_detection_enabled, + ) self._kwargs.update(**kwargs) results = TranslationRecognizerResultPack() error_message = None @@ -789,6 +844,13 @@ def __init__( source_language: str = None, translation_enabled: bool = False, workspace: str = None, + # Recognition parameters + disfluency_removal_enabled: Optional[bool] = None, + diarization_enabled: Optional[bool] = None, + speaker_count: Optional[int] = None, + timestamp_alignment_enabled: Optional[bool] = None, + special_word_filter: Optional[str] = None, + audio_event_detection_enabled: Optional[bool] = None, **kwargs, ): if model is None: @@ -812,6 +874,16 @@ def __init__( self._worker = None self._silence_timer = None self._kwargs = kwargs + # Store recognition parameters + _merge_recognition_params( + self._kwargs, + disfluency_removal_enabled, + diarization_enabled, + speaker_count, + timestamp_alignment_enabled, + special_word_filter, + audio_event_detection_enabled, + ) self._workspace = workspace self._start_stream_timestamp = -1 self._first_package_timestamp = -1 @@ -938,7 +1010,17 @@ def __launch_request(self): ) return responses - def start(self, **kwargs): + def start( + self, + # Recognition parameters + disfluency_removal_enabled: Optional[bool] = None, + diarization_enabled: Optional[bool] = None, + speaker_count: Optional[int] = None, + timestamp_alignment_enabled: Optional[bool] = None, + special_word_filter: Optional[str] = None, + audio_event_detection_enabled: Optional[bool] = None, + **kwargs, + ): """Real-time translation recognizer in asynchronous mode. Please call 'stop()' after you have completed translation & recognition. # noqa: E501 @@ -973,6 +1055,16 @@ def start(self, **kwargs): self._first_package_timestamp = -1 self._stop_stream_timestamp = -1 self._on_complete_timestamp = -1 + # Update recognition parameters + _merge_recognition_params( + self._kwargs, + disfluency_removal_enabled, + diarization_enabled, + speaker_count, + timestamp_alignment_enabled, + special_word_filter, + audio_event_detection_enabled, + ) self._kwargs.update(**kwargs) self._recognition_once = False self._worker = threading.Thread(target=self.__receive_worker) diff --git a/dashscope/tokenizers/tokenization.py b/dashscope/tokenizers/tokenization.py index ffb27bae..9a29de53 100644 --- a/dashscope/tokenizers/tokenization.py +++ b/dashscope/tokenizers/tokenization.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. import copy -from typing import Any, List +from typing import Any, List, Optional from dashscope.api_entities.dashscope_response import ( DashScopeAPIResponse, @@ -91,11 +91,17 @@ def call( # pylint: disable=arguments-renamed # noqa: E501 if model is None or not model: raise ModelRequired("Model is required!") if input is None: + # Extract model-specific parameters + enable_search = kwargs.pop("enable_search", False) + customized_model_id = kwargs.pop("customized_model_id", None) + input, parameters = cls._build_llm_parameters( model, prompt, history, messages, + enable_search=enable_search, + customized_model_id=customized_model_id, **kwargs, ) else: @@ -116,7 +122,30 @@ def call( # pylint: disable=arguments-renamed # noqa: E501 ) @classmethod - def _build_llm_parameters(cls, model, prompt, history, messages, **kwargs): + def _build_llm_parameters( + cls, + model, + prompt, + history, + messages, + enable_search: bool = False, + customized_model_id: Optional[str] = None, + **kwargs, + ): + """Build LLM parameters for tokenization. + + Args: + model: Model name + prompt: User prompt + history: Conversation history (deprecated) + messages: Conversation messages + enable_search: Enable search for qwen models + customized_model_id: Customized model ID for bailian models + **kwargs: Additional parameters + + Returns: + Tuple of (input, parameters) + """ parameters = {} input = {} # pylint: disable=redefined-builtin if history is not None: @@ -133,11 +162,9 @@ def _build_llm_parameters(cls, model, prompt, history, messages, **kwargs): input[PROMPT] = prompt if model.startswith("qwen"): - enable_search = kwargs.pop("enable_search", False) if enable_search: parameters["enable_search"] = enable_search elif model.startswith("bailian"): - customized_model_id = kwargs.pop("customized_model_id", None) if customized_model_id is None: raise InputRequired( f"customized_model_id is required for {model}", diff --git a/tests/unit/test_recognition_explicit_params.py b/tests/unit/test_recognition_explicit_params.py new file mode 100644 index 00000000..5edcd96f --- /dev/null +++ b/tests/unit/test_recognition_explicit_params.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Test explicit parameters for Recognition class.""" + +# pylint: disable=protected-access +from dashscope.audio.asr.recognition import Recognition, RecognitionCallback + + +class MockRecognitionCallback(RecognitionCallback): + """Mock callback for testing.""" + + def on_event(self, result): + pass + + def on_complete(self): + pass + + def on_error(self, result): + pass + + def on_close(self): + pass + + +class TestRecognitionExplicitParams: + """Test explicit parameters in Recognition class.""" + + def test_init_with_all_explicit_params(self): + """Test __init__ with all explicit parameters.""" + recognition = Recognition( + model="test-model", + callback=MockRecognitionCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=True, + diarization_enabled=True, + speaker_count=2, + timestamp_alignment_enabled=True, + special_word_filter="test_filter", + audio_event_detection_enabled=True, + ) + + # Verify parameters are stored in _kwargs + assert recognition._kwargs["disfluency_removal_enabled"] is True + assert recognition._kwargs["diarization_enabled"] is True + assert recognition._kwargs["speaker_count"] == 2 + assert recognition._kwargs["timestamp_alignment_enabled"] is True + assert recognition._kwargs["special_word_filter"] == "test_filter" + assert recognition._kwargs["audio_event_detection_enabled"] is True + + def test_init_with_none_params(self): + """Test __init__ with None parameters should not add to _kwargs.""" + recognition = Recognition( + model="test-model", + callback=MockRecognitionCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=None, + diarization_enabled=None, + speaker_count=None, + ) + + # Verify None parameters are not in _kwargs + assert "disfluency_removal_enabled" not in recognition._kwargs + assert "diarization_enabled" not in recognition._kwargs + assert "speaker_count" not in recognition._kwargs + + def test_init_with_partial_params(self): + """Test __init__ with some explicit parameters.""" + recognition = Recognition( + model="test-model", + callback=MockRecognitionCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=True, + speaker_count=3, + ) + + # Verify only specified parameters are in _kwargs + assert recognition._kwargs["disfluency_removal_enabled"] is True + assert recognition._kwargs["speaker_count"] == 3 + assert "diarization_enabled" not in recognition._kwargs + assert "timestamp_alignment_enabled" not in recognition._kwargs + + def test_init_with_extra_kwargs(self): + """Test __init__ with extra kwargs should still work.""" + recognition = Recognition( + model="test-model", + callback=MockRecognitionCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=True, + custom_param="custom_value", + ) + + # Verify both explicit and extra kwargs are stored + assert recognition._kwargs["disfluency_removal_enabled"] is True + assert recognition._kwargs["custom_param"] == "custom_value" + + def test_start_with_explicit_params(self): + """Test start() method with explicit parameters.""" + recognition = Recognition( + model="test-model", + callback=MockRecognitionCallback(), + format="pcm", + sample_rate=16000, + ) + + # Mock the thread and timer to avoid actual execution + recognition._running = False + recognition._callback = MockRecognitionCallback() + + # Call start with explicit parameters + # Note: This will fail at thread creation, but we can verify params + try: + recognition.start( + phrase_id="test_phrase", + disfluency_removal_enabled=True, + diarization_enabled=True, + speaker_count=2, + ) + except Exception: + # Expected to fail at thread creation + pass + finally: + # Clean up to stop any background threads + recognition._running = False + if ( + recognition._worker is not None + and recognition._worker.is_alive() + ): + recognition._worker.join(timeout=1) + + # Verify parameters are updated in _kwargs + assert recognition._kwargs.get("disfluency_removal_enabled") is True + assert recognition._kwargs.get("diarization_enabled") is True + assert recognition._kwargs.get("speaker_count") == 2 + + def test_param_override_in_start(self): + """Test that start() can override __init__ parameters.""" + recognition = Recognition( + model="test-model", + callback=MockRecognitionCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=False, + speaker_count=1, + ) + + # Verify initial values + assert recognition._kwargs["disfluency_removal_enabled"] is False + assert recognition._kwargs["speaker_count"] == 1 + + # Mock the thread and timer to avoid actual execution + recognition._running = False + + # Override parameters in start + try: + recognition.start( + disfluency_removal_enabled=True, + speaker_count=3, + ) + except Exception: + # Expected to fail at thread creation + pass + finally: + # Clean up to stop any background threads + recognition._running = False + if ( + recognition._worker is not None + and recognition._worker.is_alive() + ): + recognition._worker.join(timeout=1) + + # Verify parameters are overridden + assert recognition._kwargs.get("disfluency_removal_enabled") is True + assert recognition._kwargs.get("speaker_count") == 3 diff --git a/tests/unit/test_tokenization_explicit_params.py b/tests/unit/test_tokenization_explicit_params.py new file mode 100644 index 00000000..3d3eab8e --- /dev/null +++ b/tests/unit/test_tokenization_explicit_params.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Test explicit parameters for Tokenization class.""" + +# pylint: disable=protected-access,unused-variable +import pytest +from dashscope.tokenizers.tokenization import Tokenization + + +class TestTokenizationExplicitParams: + """Test explicit parameters in Tokenization class.""" + + def test_build_llm_parameters_with_enable_search(self): + """Test _build_llm_parameters with enable_search parameter.""" + # Test with enable_search=True for qwen model + input_data, parameters = Tokenization._build_llm_parameters( + model="qwen-turbo", + prompt="test prompt", + history=None, + messages=None, + enable_search=True, + ) + + # Verify enable_search is in parameters + assert parameters["enable_search"] is True + # Verify input is correctly built + assert input_data["prompt"] == "test prompt" + + def test_build_llm_parameters_with_enable_search_false(self): + """Test _build_llm_parameters with enable_search=False.""" + input_data, parameters = Tokenization._build_llm_parameters( + model="qwen-turbo", + prompt="test prompt", + history=None, + messages=None, + enable_search=False, + ) + + # Verify enable_search is not in parameters when False + assert "enable_search" not in parameters + + def test_build_llm_parameters_with_customized_model_id(self): + """Test _build_llm_parameters with customized_model_id parameter.""" + # Test with customized_model_id for bailian model + input_data, parameters = Tokenization._build_llm_parameters( + model="bailian-test", + prompt="test prompt", + history=None, + messages=None, + customized_model_id="custom-model-123", + ) + + # Verify customized_model_id is in input + assert input_data["customized_model_id"] == "custom-model-123" + + def test_build_llm_parameters_without_customized_model_id(self): + """Test _build_llm_parameters without customized_model_id.""" + # Should raise InputRequired error for bailian model + with pytest.raises(Exception) as exc_info: + Tokenization._build_llm_parameters( + model="bailian-test", + prompt="test prompt", + history=None, + messages=None, + customized_model_id=None, + ) + + assert "customized_model_id is required" in str(exc_info.value) + + def test_build_llm_parameters_with_messages(self): + """Test _build_llm_parameters with messages parameter.""" + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there"}, + ] + + input_data, parameters = Tokenization._build_llm_parameters( + model="qwen-turbo", + prompt=None, + history=None, + messages=messages, + enable_search=True, + ) + + # Verify messages are correctly built + assert "messages" in input_data + assert len(input_data["messages"]) == 2 + # Verify enable_search is in parameters + assert parameters["enable_search"] is True + + def test_build_llm_parameters_with_extra_kwargs(self): + """Test _build_llm_parameters with extra kwargs.""" + input_data, parameters = Tokenization._build_llm_parameters( + model="qwen-turbo", + prompt="test prompt", + history=None, + messages=None, + enable_search=True, + custom_param="custom_value", + another_param=123, + ) + + # Verify explicit parameter + assert parameters["enable_search"] is True + # Verify extra kwargs are passed through + assert parameters["custom_param"] == "custom_value" + assert parameters["another_param"] == 123 + + def test_build_llm_parameters_non_qwen_non_bailian(self): + """Test _build_llm_parameters with non-qwen, non-bailian model.""" + input_data, parameters = Tokenization._build_llm_parameters( + model="other-model", + prompt="test prompt", + history=None, + messages=None, + enable_search=True, # Should be ignored for non-qwen models + ) + + # Verify enable_search is not in parameters for non-qwen models + assert "enable_search" not in parameters + # Verify input is correctly built + assert input_data["prompt"] == "test prompt" + + def test_build_llm_parameters_with_history_deprecated(self): + """Test _build_llm_parameters with deprecated history parameter.""" + history = [ + {"user": "Hello"}, + {"bot": "Hi"}, + ] + + input_data, parameters = Tokenization._build_llm_parameters( + model="qwen-turbo", + prompt="test prompt", + history=history, + messages=None, + enable_search=True, + ) + + # Verify history is used (deprecated but still supported) + assert "history" in input_data + assert input_data["prompt"] == "test prompt" + # Verify enable_search is in parameters + assert parameters["enable_search"] is True diff --git a/tests/unit/test_translation_recognizer_explicit_params.py b/tests/unit/test_translation_recognizer_explicit_params.py new file mode 100644 index 00000000..8be53bbe --- /dev/null +++ b/tests/unit/test_translation_recognizer_explicit_params.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Test explicit parameters for TranslationRecognizer classes.""" + +# pylint: disable=protected-access +from dashscope.audio.asr.translation_recognizer import ( + TranslationRecognizerRealtime, + TranslationRecognizerChat, + TranslationRecognizerCallback, +) + + +class MockTranslationCallback(TranslationRecognizerCallback): + """Mock callback for testing.""" + + def on_open(self): + pass + + def on_event( + self, + request_id, + transcription_result, + translation_result, + usage, + ): + pass + + def on_error(self, message): + pass + + def on_close(self): + pass + + +class TestTranslationRecognizerRealtimeExplicitParams: + """Test explicit parameters in TranslationRecognizerRealtime class.""" + + def test_init_with_all_explicit_params(self): + """Test __init__ with all explicit parameters.""" + recognizer = TranslationRecognizerRealtime( + model="test-model", + callback=MockTranslationCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=True, + diarization_enabled=True, + speaker_count=2, + timestamp_alignment_enabled=True, + special_word_filter="test_filter", + audio_event_detection_enabled=True, + ) + + # Verify parameters are stored in _kwargs + assert recognizer._kwargs["disfluency_removal_enabled"] is True + assert recognizer._kwargs["diarization_enabled"] is True + assert recognizer._kwargs["speaker_count"] == 2 + assert recognizer._kwargs["timestamp_alignment_enabled"] is True + assert recognizer._kwargs["special_word_filter"] == "test_filter" + assert recognizer._kwargs["audio_event_detection_enabled"] is True + + # Clean up + recognizer._running = False + + def test_init_with_none_params(self): + """Test __init__ with None parameters should not add to _kwargs.""" + recognizer = TranslationRecognizerRealtime( + model="test-model", + callback=MockTranslationCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=None, + diarization_enabled=None, + speaker_count=None, + ) + + # Verify None parameters are not in _kwargs + assert "disfluency_removal_enabled" not in recognizer._kwargs + assert "diarization_enabled" not in recognizer._kwargs + assert "speaker_count" not in recognizer._kwargs + + # Clean up + recognizer._running = False + + def test_init_with_partial_params(self): + """Test __init__ with some explicit parameters.""" + recognizer = TranslationRecognizerRealtime( + model="test-model", + callback=MockTranslationCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=True, + speaker_count=3, + ) + + # Verify only specified parameters are in _kwargs + assert recognizer._kwargs["disfluency_removal_enabled"] is True + assert recognizer._kwargs["speaker_count"] == 3 + assert "diarization_enabled" not in recognizer._kwargs + + # Clean up + recognizer._running = False + + def test_init_with_translation_params(self): + """Test __init__ with translation-specific parameters.""" + recognizer = TranslationRecognizerRealtime( + model="test-model", + callback=MockTranslationCallback(), + format="pcm", + sample_rate=16000, + transcription_enabled=True, + translation_enabled=True, + source_language="zh", + disfluency_removal_enabled=True, + ) + + # Verify translation parameters + assert recognizer.transcription_enabled is True + assert recognizer.translation_enabled is True + assert recognizer.source_language == "zh" + # Verify recognition parameters + assert recognizer._kwargs["disfluency_removal_enabled"] is True + + # Clean up + recognizer._running = False + + +class TestTranslationRecognizerChatExplicitParams: + """Test explicit parameters in TranslationRecognizerChat class.""" + + def test_init_with_all_explicit_params(self): + """Test __init__ with all explicit parameters.""" + recognizer = TranslationRecognizerChat( + model="test-model", + callback=MockTranslationCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=True, + diarization_enabled=True, + speaker_count=2, + timestamp_alignment_enabled=True, + special_word_filter="test_filter", + audio_event_detection_enabled=True, + ) + + # Verify parameters are stored in _kwargs + assert recognizer._kwargs["disfluency_removal_enabled"] is True + assert recognizer._kwargs["diarization_enabled"] is True + assert recognizer._kwargs["speaker_count"] == 2 + assert recognizer._kwargs["timestamp_alignment_enabled"] is True + assert recognizer._kwargs["special_word_filter"] == "test_filter" + assert recognizer._kwargs["audio_event_detection_enabled"] is True + + # Clean up + recognizer._running = False + + def test_init_with_none_params(self): + """Test __init__ with None parameters should not add to _kwargs.""" + recognizer = TranslationRecognizerChat( + model="test-model", + callback=MockTranslationCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=None, + diarization_enabled=None, + ) + + # Verify None parameters are not in _kwargs + assert "disfluency_removal_enabled" not in recognizer._kwargs + assert "diarization_enabled" not in recognizer._kwargs + + # Clean up + recognizer._running = False + + def test_init_with_partial_params(self): + """Test __init__ with some explicit parameters.""" + recognizer = TranslationRecognizerChat( + model="test-model", + callback=MockTranslationCallback(), + format="pcm", + sample_rate=16000, + diarization_enabled=True, + audio_event_detection_enabled=True, + ) + + # Verify only specified parameters are in _kwargs + assert recognizer._kwargs["diarization_enabled"] is True + assert recognizer._kwargs["audio_event_detection_enabled"] is True + assert "disfluency_removal_enabled" not in recognizer._kwargs + assert "speaker_count" not in recognizer._kwargs + + # Clean up + recognizer._running = False + + def test_init_with_extra_kwargs(self): + """Test __init__ with extra kwargs should still work.""" + recognizer = TranslationRecognizerChat( + model="test-model", + callback=MockTranslationCallback(), + format="pcm", + sample_rate=16000, + disfluency_removal_enabled=True, + custom_param="custom_value", + ) + + # Verify both explicit and extra kwargs are stored + assert recognizer._kwargs["disfluency_removal_enabled"] is True + assert recognizer._kwargs["custom_param"] == "custom_value" + + # Clean up + recognizer._running = False