Unity 工具 之 Azure 微软SSML语音合成TTS流式获取音频数据的简单整理

Unity 工具 之 Azure 微软SSML语音合成TTS流式获取音频数据的简单整理

目录

Unity 工具 之 Azure 微软SSML语音合成TTS流式获取音频数据的简单整理

一、简单介绍

二、实现原理

三、实现步骤

四、关键代码


一、简单介绍

Unity 工具类,自己整理的一些游戏开发可能用到的模块,单独独立使用,方便游戏开发。

本节介绍,这里在使用微软的Azure 进行语音合成的两个方法的做简单整理,这里简单说明,如果你有更好的方法,欢迎留言交流。

语音合成标记语言 (SSML) 是一种基于 XML 的标记语言,可用于微调文本转语音输出属性,例如音调、发音、语速、音量等。 与纯文本输入相比,你拥有更大的控制权和灵活性。

可以使用 SSML 来执行以下操作:

  • 定义输入文本结构,用于确定文本转语音输出的结构、内容和其他特征。 例如,可以使用 SSML 来定义段落、句子、中断/暂停或静音。 可以使用事件标记(例如书签或视素)来包装文本,这些标记可以稍后由应用程序处理。
  • 选择语音、语言、名称、样式和角色。 可以在单个 SSML 文档中使用多个语音。 调整重音、语速、音调和音量。 还可以使用 SSML 插入预先录制的音频,例如音效或音符。
  • 控制输出音频的发音。 例如,可以将 SSML 与音素和自定义词典配合使用来改进发音。 还可以使用 SSML 定义单词或数学表达式的具体发音。
下面是 SSML 文档的基本结构和语法的子集:
 
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="string">
    <mstts:backgroundaudio src="string" volume="string" fadein="string" fadeout="string"/>
    <voice name="string" effect="string">
        <audio src="string"></audio>
        <bookmark mark="string"/>
        <break strength="string" time="string" />
        <emphasis level="value"></emphasis>
        <lang xml:lang="string"></lang>
        <lexicon uri="string"/>
        <math xmlns="http://www.w3.org/1998/Math/MathML"></math>
        <mstts:audioduration value="string"/>
        <mstts:express-as style="string" styledegree="value" role="string"></mstts:express-as>
        <mstts:silence type="string" value="string"/>
        <mstts:viseme type="string"/>
        <p></p>
        <phoneme alphabet="string" ph="string"></phoneme>
        <prosody pitch="value" contour="value" range="value" rate="value" volume="value"></prosody>
        <s></s>
        <say-as interpret-as="string" format="string" detail="string"></say-as>
        <sub alias="string"></sub>
    </voice>
</speak>

 SSML 语音和声音
语音合成标记语言 (SSML) 的语音和声音 - 语音服务 - Azure AI services | Microsoft Learn

官网注册:

面向学生的 Azure - 免费帐户额度 | Microsoft Azure

官网技术文档网址:

技术文档 | Microsoft Learn

官网的TTS:

文本转语音快速入门 - 语音服务 - Azure Cognitive Services | Microsoft Learn

Azure Unity SDK  包官网:

安装语音 SDK - Azure Cognitive Services | Microsoft Learn

SDK具体链接:

https://aka.ms/csspeech/unitypackage
 

二、实现原理

1、官网申请得到语音合成对应的 SPEECH_KEY 和 SPEECH_REGION

2、然后对应设置 语言 和需要的声音 配置

3、使用 SSML 带有流式获取得到音频数据,在声源中播放或者保存即可,样例如下

public static async Task SynthesizeAudioAsync()
{
    var speechConfig = SpeechConfig.FromSubscription("YourSpeechKey", "YourSpeechRegion");
    using var speechSynthesizer = new SpeechSynthesizer(speechConfig, null);

    var ssml = File.ReadAllText("./ssml.xml");
    var result = await speechSynthesizer.SpeakSsmlAsync(ssml);

    using var stream = AudioDataStream.FromResult(result);
    await stream.SaveToWaveFileAsync("path/to/write/file.wav");
}

三、实现步骤

基础的环境搭建参照:Unity 工具 之 Azure 微软语音合成普通方式和流式获取音频数据的简单整理_unity 语音合成

1、脚本实现,挂载对应脚本到场景中

2、运行场景,会使用 SSML方式合成TTS,并播放

 

四、关键代码

1、AzureTTSDataWithSSMLHandler

using Microsoft.CognitiveServices.Speech;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using UnityEngine;

/// <summary>
/// 使用 SSML 方式语音合成
/// </summary>
public class AzureTTSDataWithSSMLHandler
{
    /// <summary>
    /// Azure TTS 合成 必要数据
    /// </summary>
    private const string SPEECH_KEY = "YOUR_SPEECH_KEY";
    private const string SPEECH_REGION = "YOUR_SPEECH_REGION";
    private const string SPEECH_RECOGNITION_LANGUAGE = "zh-CN";
    private string SPEECH_VOICE_NAME = "zh-CN-XiaoxiaoNeural";

    /// <summary>
    /// 创建 TTS 中的参数
    /// </summary>
    private CancellationTokenSource m_CancellationTokenSource;
    private AudioDataStream m_AudioDataStream;
    private Connection m_Connection;
    private SpeechConfig m_Config;
    private SpeechSynthesizer m_Synthesizer;

    /// <summary>
    /// 音频获取事件
    /// </summary>
    private Action<AudioDataStream> m_AudioStream;

    /// <summary>
    /// 开始播放TTS事件
    /// </summary>
    private Action m_StartTTSPlayAction;
    /// <summary>
    /// 停止播放TTS事件
    /// </summary>
    private Action m_StartTTSStopAction;

    /// <summary>
    /// 初始化
    /// </summary>
    public void Initialized()
    {
        m_Config = SpeechConfig.FromSubscription(SPEECH_KEY, SPEECH_REGION);
        m_Synthesizer = new SpeechSynthesizer(m_Config, null);
        m_Connection = Connection.FromSpeechSynthesizer(m_Synthesizer);
        m_Connection.Open(true);

    }

    /// <summary>
    /// 开始进行语音合成
    /// </summary>
    /// <param name="msg">合成的内容</param>
    /// <param name="stream">获取到的音频流数据</param>
    /// <param name="style"></param>
    public async void Start(string msg, Action<AudioDataStream> stream, string style = "chat")
    {
        this.m_AudioStream = stream;
        await SynthesizeAudioAsync(CreateSSML(msg, SPEECH_RECOGNITION_LANGUAGE, SPEECH_VOICE_NAME, style));
    }

    /// <summary>
    /// 停止语音合成
    /// </summary>
    public void Stop()
    {
        m_StartTTSStopAction?.Invoke();

        if (m_AudioDataStream != null)
        {
            m_AudioDataStream.Dispose();
            m_AudioDataStream = null;
        }

        if (m_CancellationTokenSource != null)
        {
            m_CancellationTokenSource.Cancel();
        }

        if (m_Synthesizer != null)
        {
            m_Synthesizer.Dispose();
            m_Synthesizer = null;
        }

        if (m_Connection != null)
        {
            m_Connection.Dispose();
            m_Connection = null;
        }
    }

    /// <summary>
    /// 设置语音合成开始播放事件
    /// </summary>
    /// <param name="onStartAction"></param>
    public void SetStartTTSPlayAction(Action onStartAction)
    {
        if (onStartAction != null)
        {
            m_StartTTSPlayAction = onStartAction;
        }
    }

    /// <summary>
    /// 设置停止语音合成事件
    /// </summary>
    /// <param name="onAudioStopAction"></param>
    public void SetStartTTSStopAction(Action onAudioStopAction)
    {
        if (onAudioStopAction != null)
        {
            m_StartTTSStopAction = onAudioStopAction;
        }
    }


    /// <summary>
    /// 开始异步请求合成 TTS 数据
    /// </summary>
    /// <param name="speakMsg"></param>
    /// <returns></returns>
    private async Task SynthesizeAudioAsync(string speakMsg)
    {

        Cancel();

        m_CancellationTokenSource = new CancellationTokenSource();

        var result = m_Synthesizer.StartSpeakingSsmlAsync(speakMsg);
        await result;

        m_StartTTSPlayAction?.Invoke();

        m_AudioDataStream = AudioDataStream.FromResult(result.Result);
        m_AudioStream?.Invoke(m_AudioDataStream);
    }

    private void Cancel()
    {
        if (m_AudioDataStream != null)
        {
            m_AudioDataStream.Dispose();
            m_AudioDataStream = null;
        }

        if (m_CancellationTokenSource != null)
        {
            m_CancellationTokenSource.Cancel();
        }
    }

    /// <summary>
    /// 生成 需要的 SSML XML 数据
    /// (格式不唯一,可以根据需要自行在增加删减)
    /// </summary>
    /// <param name="msg">合成的音频内容</param>
    /// <param name="language">合成语音</param>
    /// <param name="voiceName">采用谁的声音合成音频</param>
    /// <param name="style">合成时的语气类型</param>
    /// <returns>ssml XML</returns>
    private string CreateSSML(string msg, string language, string voiceName, string style = "chat")
    {
        // XmlDocument
        XmlDocument xmlDoc = new XmlDocument();

        // 设置 speak 基础元素
        XmlElement speakElem = xmlDoc.CreateElement("speak");
        speakElem.SetAttribute("version", "1.0");
        speakElem.SetAttribute("xmlns", "http://www.w3.org/2001/10/synthesis");
        speakElem.SetAttribute("xmlns:mstts", "http://www.w3.org/2001/mstts");
        speakElem.SetAttribute("xml:lang", language);

        // 设置 voice 元素
        XmlElement voiceElem = xmlDoc.CreateElement("voice");
        voiceElem.SetAttribute("name", voiceName);

        // 设置 mstts:viseme 元素
        XmlElement visemeElem = xmlDoc.CreateElement("mstts", "viseme", "http://www.w3.org/2001/mstts");
        visemeElem.SetAttribute("type", "FacialExpression");

        // 设置 语气 元素
        XmlElement styleElem = xmlDoc.CreateElement("mstts", "express-as", "http://www.w3.org/2001/mstts");
        styleElem.SetAttribute("style", style.ToString().Replace("_", "-"));

        // 创建文本节点,包含文本信息
        XmlNode textNode = xmlDoc.CreateTextNode(msg);

        // 设置好的元素添加到 xml 中
        voiceElem.AppendChild(visemeElem);
        styleElem.AppendChild(textNode);
        voiceElem.AppendChild(styleElem);
        speakElem.AppendChild(voiceElem);
        xmlDoc.AppendChild(speakElem);
        
        Debug.Log("[SSML  XML] Result : " + xmlDoc.OuterXml);
        return xmlDoc.OuterXml;
    }

}

2、AzureTTSMono

using Microsoft.CognitiveServices.Speech;
using System;
using System.Collections.Concurrent;
using System.IO;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class AzureTTSMono : MonoBehaviour
{
    private AzureTTSDataWithSSMLHandler m_AzureTTSDataWithSSMLHandler;

    /// <summary>
    /// 音源和音频参数
    /// </summary>
    private AudioSource m_AudioSource;
    private AudioClip m_AudioClip;

    /// <summary>
    /// 音频流数据
    /// </summary>
    private ConcurrentQueue<float[]> m_AudioDataQueue = new ConcurrentQueue<float[]>();
    private AudioDataStream m_AudioDataStream;

    /// <summary>
    /// 音频播放完的事件
    /// </summary>
    private Action m_AudioEndAction;

    /// <summary>
    /// 音频播放结束的布尔变量
    /// </summary>
    private bool m_NeedPlay = false;
    private bool m_StreamReadEnd = false;

    private const int m_SampleRate = 16000;
    //最大支持60s音频 
    private const int m_BufferSize = m_SampleRate * 60;
    //采样容量
    private const int m_UpdateSize = m_SampleRate;

    //audioclip 设置过的数据个数
    private int m_TotalCount = 0;
    private int m_DataIndex = 0;

    #region Lifecycle function

    private void Awake()
    {
        m_AudioSource = GetComponent<AudioSource>();

        m_AzureTTSDataWithSSMLHandler = new AzureTTSDataWithSSMLHandler();
        m_AzureTTSDataWithSSMLHandler.SetStartTTSPlayAction(() => { Debug.Log(" Play TTS "); });
        m_AzureTTSDataWithSSMLHandler.SetStartTTSStopAction(() => { Debug.Log(" Stop TTS "); AudioPlayEndEvent(); });
        m_AudioEndAction = () => { Debug.Log(" End TTS "); };
        m_AzureTTSDataWithSSMLHandler.Initialized();
    }

    // Start is called before the first frame update
    void Start()
    {
        m_AzureTTSDataWithSSMLHandler.Start("今朝有酒,今朝醉,人生几年百花春", OnGetAudioStream);
    }

    // Update is called once per frame
    private void Update()
    {
        UpdateAudio();
    }

    #endregion

    #region Audio handler

    /// <summary>
    /// 设置播放TTS的结束的结束事件
    /// </summary>
    /// <param name="act"></param>
    public void SetAudioEndAction(Action act)
    {
        this.m_AudioEndAction = act;
    }

    /// <summary>
    /// 处理获取到的TTS流式数据
    /// </summary>
    /// <param name="stream">流数据</param>
    public async void OnGetAudioStream(AudioDataStream stream)
    {
        m_StreamReadEnd = false;
        m_NeedPlay = true;
        m_AudioDataStream = stream;
        Debug.Log("[AzureTTSMono] OnGetAudioStream");

        MemoryStream memStream = new MemoryStream();
        byte[] buffer = new byte[m_UpdateSize * 2];
        uint bytesRead;
        m_DataIndex = 0;
        m_TotalCount = 0;
        m_AudioDataQueue.Clear();

        // 回到主线程进行数据处理
        Loom.QueueOnMainThread(() =>
        {
            m_AudioSource.Stop();
            m_AudioSource.clip = null;
            m_AudioClip = AudioClip.Create("SynthesizedAudio", m_BufferSize, 1, m_SampleRate, false);
            m_AudioSource.clip = m_AudioClip;
        });

        do
        {
            bytesRead = await System.Threading.Tasks.Task.Run(() => m_AudioDataStream.ReadData(buffer));
            if (bytesRead <= 0)
            {
                break;
            }

            // 读取写入数据
            memStream.Write(buffer, 0, (int)bytesRead);
            {
                var tempData = memStream.ToArray();
                var audioData = new float[memStream.Length / 2];

                for (int i = 0; i < audioData.Length; ++i)
                {
                    audioData[i] = (short)(tempData[i * 2 + 1] << 8 | tempData[i * 2]) / 32768.0F;
                }
                try
                {
                    m_TotalCount += audioData.Length;
                    // 把数据添加到队列中
                    m_AudioDataQueue.Enqueue(audioData);

                    // new 获取新的地址,为后面写入数据
                    memStream = new MemoryStream();
                }
                catch (Exception e)
                {
                    Debug.LogError(e.ToString());
                }
            }

        } while (bytesRead > 0);

        m_StreamReadEnd = true;
    }

    /// <summary>
    /// Update 播放音频
    /// </summary>
    private void UpdateAudio() {
        if (!m_NeedPlay) return;

        //数据操作
        if (m_AudioDataQueue.TryDequeue(out float[] audioData))
        {

            m_AudioClip.SetData(audioData, m_DataIndex);
            m_DataIndex = (m_DataIndex + audioData.Length) % m_BufferSize;
        }

        //检测是否停止
        if (m_StreamReadEnd && m_AudioSource.timeSamples >= m_TotalCount)
        {
            AudioPlayEndEvent();
        }

        if (!m_NeedPlay) return;

        //由于网络,可能额有些数据还没有过来,所以根据需要判断是否暂停播放
        if (m_AudioSource.timeSamples >= m_DataIndex && m_AudioSource.isPlaying)
        {
            m_AudioSource.timeSamples = m_DataIndex;

            //暂停
            Debug.Log("[AzureTTSMono] Pause");
            m_AudioSource.Pause();


        }

        //由于网络,可能有些数据过来比较晚,所以这里根据需要判断是否继续播放
        if (m_AudioSource.timeSamples < m_DataIndex && !m_AudioSource.isPlaying)
        {
            //播放
            Debug.Log("[AzureTTSMono] Play");
            m_AudioSource.Play();
        }
    }

    /// <summary>
    /// TTS 播放结束的事件
    /// </summary>
    private void AudioPlayEndEvent()
    {
        Debug.Log("[AzureTTSMono] End");
        m_NeedPlay = false;
        m_AudioSource.timeSamples = 0;
        m_AudioSource.Stop();
        m_AudioEndAction?.Invoke();
    }

    #endregion
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/75788.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

WSL2 Ubuntu子系统安装cuda+cudnn+torch

文章目录 前言一、安装cudncudnn安装pytorch 前言 确保Windows系统版本高于windows10 21H2或Windows11&#xff0c;然后在Windows中将显卡驱动升级到最新即可&#xff0c;WSL2已支持对显卡的直接调用。 一、安装cudncudnn 配置cuda环境&#xff0c;WSL下的Ubuntu子系统的cu…

Flink 流式读写文件、文件夹

文章目录 一、flink 流式读取文件夹、文件二、flink 写入文件系统——StreamFileSink三、查看完整代码 一、flink 流式读取文件夹、文件 Apache Flink针对文件系统实现了一个可重置的source连接器&#xff0c;将文件看作流来读取数据。如下面的例子所示&#xff1a; StreamExe…

emqx-5.1.4开源版使用记录

emqx-5.1.4开源版使用记录 windows系统安装eqmx 去官网下载 emqx-5.1.4-windows-amd64.zip&#xff0c;然后找个目录解压 进入bin目录,执行命令启动emqx 执行命令 emqx.cmd start使用emqx 访问内置的web管理页面 浏览器访问地址 http://localhost:18083/#/dashboard/overv…

H3C交换机MIB库

非常齐全的官方MIB库 为Zabbix监控华三交换机提供诸多方便。 如下信息提供下载链接和下载账号: MIB清单下载:交换机-新华三集团-H3C MIB库:MIB-新华三集团-H3C

Aspera替代方案:探索这些安全且可靠的文件传输工具

科技的发展日新月异&#xff0c;文件的传输方式也在不断地更新换代。传统的邮件附件、FTP等方式已经难以满足人们对于传输速度和安全性的需求了。近年来&#xff0c;一些新兴的文件传输工具受到了人们的关注&#xff0c;其中除了知名的Aspera之外&#xff0c;还有许多可靠安全的…

梅赛德斯-奔驰将成为首家集成ChatGPT的汽车制造商

ChatGPT的受欢迎程度毋庸置疑。OpenAI这个基于人工智能的工具&#xff0c;每天能够吸引无数用户使用&#xff0c;已成为当下很受欢迎的技术热点。因此&#xff0c;有许多公司都在想方设法利用ChatGPT来提高产品吸引力&#xff0c;卖点以及性能。在汽车领域&#xff0c;梅赛德斯…

springBoot 集中配置管理

springBoot 集中配置管理 项目配置如果上线项目&#xff0c;运维或者开发者可以直接和jar包同目录下创建文件&#xff0c;然后更改属性 项目配置 创建文件&#xff0c;调整配置如果上线项目&#xff0c;运维或者开发者可以直接和jar包同目录下创建文件&#xff0c;然后更改 属…

Redis实现共享Session

Redis实现共享Session 分布式系统中&#xff0c;sessiong共享有很多的解决方案&#xff0c;其中托管到缓存中应该是最常用的方案之一。 1、引入依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM…

undefined reference to `dlopen‘ ‘SSL_library_init‘ `X509_certificate_type‘

使用Crow的时候需要注意crow依赖asio依赖OpenSSL&#xff0c;asio要求1.22以上版本&#xff0c;我使用的是1.26.0&#xff1b; 这个版本的asio要求OpenSSL是1.0.2&#xff0c;其他版本我得机器上编不过&#xff0c;ubuntu上默认带的OpenSSL是1.1.1; 所以我下载了OPENSSL1.2.0重…

QT使用QML实现地图绘制虚线

QML提供了MapPolyline用于在地图上绘制线段&#xff0c;该线段是实线&#xff0c;因此我使用Canvas自定义绘制的方式在地图上绘制线段&#xff0c;如图&#xff1a; 鼠标在地图上点击后&#xff0c;在点击位置添加图标 &#xff0c;当有多个图标被添加到地图上后&#xff0c;计…

mysql-事务特性以及隔离机制

一.ACID 事务&#xff08;Transaction&#xff09;是访问和更新数据库的程序执行单元&#xff1b;事务中可能包含一个或多个sql语句&#xff0c;这些语句要么都执行&#xff0c;要么都不执行。 1.逻辑架构和存储引擎 如上图所示&#xff0c;MySQL服务器逻辑架构从上往下可以分…

对应分析介绍及SPSS案例分析

在开展统计分析的过程中&#xff0c;分类变量&#xff08;定序和定类变量&#xff09;是我们研究的一个重点。通常我们分析分类变量间关系时&#xff0c;最常用的分析方法是卡方检验&#xff0c;其次是逻辑回归和对数线性模型等。 如果类别变量的分类较少&#xff0c;我们可以…

构建 LVS-DR 群集、配置nginx负载均衡。

目录 一、基于 CentOS 7 构建 LVS-DR 群集 1、准备四台虚拟机 2、配置负载调度器&#xff08;192.168.2.130&#xff09; 3、部署共享存储&#xff08;192.168.2.133&#xff09; 4、配置两个Web服务器&#xff08;192.168.2.131、192.168.2.132&#xff09; 测试集群 二…

PHP最简单自定义自己的框架数据库封装调用(五)

1、实现效果调用实现数据增删改查封装 2、创建数据表 CREATE TABLE test (id int(11) NOT NULL AUTO_INCREMENT,name varchar(30) DEFAULT NULL,age int(11) DEFAULT NULL,PRIMARY KEY (id) ) ENGINEMyISAM AUTO_INCREMENT4 DEFAULT CHARSETutf8;3、index.php 入口定义数据库…

云服务 Ubuntu 20.04 版本 使用 Nginx 部署静态网页

所需操作&#xff1a; 1.安装Nginx 2.修改配置文件 3.测试、重启 Nginx 4.内部修改防火墙 5.配置解析 6.测试是否部署成功 1.安装Nginx // 未使用 root 账号 apt-get update // 更新apt-get install nginx // 安装 nginx 1.1.测试是否安装没问题 在网页上输入云服务的公网…

山西电力市场日前价格预测【2023-08-16】

日前价格预测 预测明日&#xff08;2023-08-16&#xff09;山西电力市场全天平均日前电价为363.90元/MWh。其中&#xff0c;最高日前电价为430.17元/MWh&#xff0c;预计出现在19: 30。最低日前电价为318.33元/MWh&#xff0c;预计出现在13: 00。 价差方向预测 1&#xff1a; 实…

css3 瀑布流布局遇见截断下一列展示后半截现象

css3 瀑布流布局遇见截断下一列展示后半截现象 注&#xff1a;css3实现瀑布流布局简直不要太香&#xff5e;&#xff5e;&#xff5e;&#xff5e;&#xff5e; 场景-在uniapp项目中 当瀑布流布局column-grap:10px 相邻两列之间的间隙为10px&#xff0c;column-count:2,2列展…

如何使用CSS实现一个纯CSS的滚动条样式?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 使用CSS实现自定义滚动条样式⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对Web开发感兴趣…

Leetcode 21. 合并两个有序链表

题目描述 题目链接&#xff1a;https://leetcode.cn/problems/merge-two-sorted-lists/description/ 思路 两个链表都是升序链表&#xff0c;新建一个链表&#xff0c;引入伪头节点作为辅助节点&#xff0c;将各节点添加到伪节点之后&#xff0c;再用一个cur节点指向新链表的…

创新方案|超越炒作 – 从产品驱动增长PLG到产品驱动销售PLS的务实策略指南

这篇文章探讨从产品驱动增长到产品驱动销售的策略&#xff0c;超越了产品驱动增长的炒作。尽管产品驱动增长模式被认为是科技公司的灵丹妙药&#xff0c;但要取得成功&#xff0c;通常需要结合更传统的企业模式的要素。研究表明&#xff0c;只有少数采用产品驱动增长模式的公司…