برنامه نویسی

به عنوان مثال ساده بروید: داستانهای صوتی را با Google Gemini ، TTS و CloudFlare R2 ایجاد کنید

من در حال حاضر در حال کار روی یک پروژه جانبی در مورد یادگیری زبان هستم. ویژگی های اصلی شامل تولید محتوا با هوش مصنوعی و تبدیل متن به پرونده های صوتی است. برای ذخیره پرونده های صوتی ، من به ذخیره سازی ابری نیز نیاز دارم.

هزینه اولویت اصلی من بود زیرا فهمیدم که جابجایی بین سیستم عامل های ابر خیلی دشوار نخواهد بود.

در پایان ، من Google Gemini ، Google TTS و CloudFlare R2 را انتخاب کردم. آنها مستندات و نمونه های API را ارائه می دهند ، اما من برخی از قسمت ها را پیدا کردم ، بنابراین تصمیم گرفتم یک پست در مورد آن بنویسم. من از GO استفاده کردم ، و این فقط استفاده اساسی را پوشش می دهد.

برای Google Gemini و TTS ، من از API RESTful استفاده می کنم. اگرچه آنها یک کتابخانه ارائه می دهند ، اما من با استفاده از API آرامش بخش راحت تر از تنظیم کتابخانه.

  1. Google Gemini – ارسال سریع و دریافت پاسخ.
  2. Google TTS – ارسال متن و دریافت یک فایل صوتی.
  3. CloudFlare R2 – یک فایل صوتی را با فرمت باینری در CloudFlare ذخیره کنید.
  4. رمز نهایی

package api

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"

    "github.com/spf13/viper"
)

type Part struct {
    Text string `json:"text"`
}

type Content struct {
    Parts []Part `json:"parts"`
}

type Candidates struct {
    Content Content `json:"content"`
}

type PromptResult struct {
    Candidates []Candidates `json:"candidates"`
}

func Prompt(prompt string) (*PromptResult, error) {
    // I use viper to manage environment variables, you can replace this with your api key.
    url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=%s", viper.Get("GOOGLE_CLOUD_API_KEY"))

    // In this example, it sends only one prompt but you can send more information
    data, err := json.Marshal(map[string]interface{}{
        "contents": []map[string]interface{}{{
            "parts": []map[string]interface{}{{
                "text": prompt,
            }},
        }},
    })

    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))

    if err != nil {
        return nil, err
    }

    req.Header.Add("Content-Type", "application/json")

    // Request the API
    res, err := http.DefaultClient.Do(req)

    if err != nil {
        return nil, err
    }

    defer res.Body.Close()

    resBody, err := io.ReadAll(res.Body)

    if err != nil {
        return nil, err
    }

    var promptResult PromptResult

    // Parse the result
    err = json.Unmarshal(resBody, &promptResult)

    if err != nil {
        return nil, err
    }

    return &promptResult, nil
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

package main

import (
    "fmt"

    "github.com/hsk-kr/tutorial/lib/api"
    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile(".env")
    viper.ReadInConfig()

    promptResult, _ := api.Prompt("Generate a short story for kids")

    fmt.Println(promptResult.Candidates[0].Content.Parts[0].Text)
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

% go run main.go
Barnaby Bumble, a fuzzy, striped bee with a wobbly stinger, was known throughout Honeycomb Hollow for one thing: he was terribly afraid of heights.

“Buzz off, Barnaby!” the other young bees would tease, zooming past him as they practiced their loop-de-loops around the tallest sunflower stalks. Barnaby would cling tightly to the petals of a daisy, his tiny heart thumping like a hummingbird's wings.

He longed to fly high. He dreamed of seeing the whole meadow spread out below him, a carpet of shimmering colors. But every time he tried to climb a little higher, a dizzy feeling would overwhelm him, and he’d tumble back down, buzzing with fear.

One sunny morning, Mrs. Higgins, the wise old queen bee, announced a very important task. “The Queen Clover is blooming!” she declared. “Her nectar is extra sweet and good for making the best honey. But she’s blooming on the very highest hill, atop the tallest thistle! Someone brave and strong must bring her nectar back to the hive.”

All the young bees buzzed excitedly, eager to volunteer. Barnaby, however, shrunk back, his stripes seeming to fade to gray. He knew he couldn't possibly fly that high.

But then, he saw little Penelope Petal, a tiny bee with a torn wing. She looked longingly at the queen, but her wing flapped weakly. Penelope was too small and hurt to make the journey.

Barnaby felt a surge of courage. He knew he couldn’t let Penelope down, and he knew how important the Queen Clover nectar was. Taking a deep breath, he buzzed forward.

"Mrs. Higgins," he stammered, "I... I want to try."

Mrs. Higgins smiled kindly. "Are you sure, Barnaby? It's a long way up."

"I'll do my best," he promised, his voice trembling only a little.

He took off, his wings beating harder than ever. The air rushed past him, and his head began to spin. He looked down and saw the hive shrinking below. Fear prickled his antennae.

But then, he thought of Penelope and the delicious honey they could make. He focused on the top of the thistle, a tiny purple dot in the distance.

He flew on, one wing beat at a time. He rested on fluffy clouds of milkweed seeds, took tiny sips of dew, and told himself, "Just a little further, Barnaby. Just a little further."

Finally, after what seemed like forever, he reached the top of the thistle. There, bathed in sunshine, was the Queen Clover, her petals glistening with sweet nectar. Barnaby carefully collected the precious liquid into his pollen baskets.

The journey back was easier. He was filled with a sense of accomplishment, and the fear had almost completely vanished. He even managed to do a little wiggle in the air, just for fun!

When Barnaby landed at the hive, he was greeted with cheers. Penelope Petal buzzed around him, her eyes shining with gratitude. Mrs. Higgins beamed.

"Barnaby Bumble," she declared, "you are braver and stronger than you know! You not only brought us the nectar of the Queen Clover, but you also showed us that even the smallest bee can overcome their biggest fears."

From that day on, Barnaby Bumble was no longer known for being afraid of heights. He was known for his courage, his kindness, and the most delicious Queen Clover honey in all of Honeycomb Hollow. And every now and then, you might even see him doing a little loop-de-loop around the tallest sunflower.
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

می توانید جزئیات بیشتری درباره API در اینجا بیابید: https://ai.google.dev/gemini-api/docs.

در این مثال ، یک سریع ارسال می شود و پاسخی دریافت می شود. پاسخ حاوی چیزی بیش از متن است ، اما برای سادگی ، من ساختار را تعریف کردم تا فقط متن را اداره کند و آن را به کنسول چاپ کنم.

من از Viper برای مدیریت متغیرهای محیط استفاده می کنم ، اما می توانید کد را با جایگزینی کلید API با خود آزمایش کنید.


package api

import (
    "bytes"
    "encoding/base64"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "strings"

    "github.com/spf13/viper"
)

type Voice struct {
    LanguageCodes          []string `json:"languageCodes"`
    Name                   string   `json:"name"`
    SsmlGender             Gender   `json:"ssmlGender"` // "MALE" or "FEMALE"
    NaturalSampleRateHertz int      `json:"naturalSampleRateHertz"`
}

type VoiceSelectionParam struct {
    LanguageCode string `json:"languageCode"`
    Name         string `json:"name"`
    SsmlGender   Gender `json:"ssmlGender"`
}

type VoicesResponse struct {
    Voices []Voice `json:"voices"`
}

type AudioEncoding string

type Gender string

const (
    MALE   Gender = "MALE"
    FEMALE Gender = "FEMALE"
)

const (
    LINEAR16 AudioEncoding = "LINEAR16"
    MP3      AudioEncoding = "MP3"
    OGG_OPUS AudioEncoding = "OGG_OPUS"
    MULAW    AudioEncoding = "MULAW"
    ALAW     AudioEncoding = "ALAW "
)

func convertVoiceToVoiceSelectionParam(voice Voice) (*VoiceSelectionParam, error) {
    voiceSelectionParam := new(VoiceSelectionParam)

    if voice.LanguageCodes == nil || len(voice.LanguageCodes) <= 0 {
        return nil, errors.New("Empty LanguageCodes")
    }

    voiceSelectionParam.LanguageCode = voice.LanguageCodes[0]
    voiceSelectionParam.Name = voice.Name
    voiceSelectionParam.SsmlGender = voice.SsmlGender

    return voiceSelectionParam, nil
}

func GetVoiceList(languageCode string) ([]Voice, error) {
    url := fmt.Sprintf("https://texttospeech.googleapis.com/v1/voices?languageCode=%s&key=%s", languageCode, viper.GetString("GOOGLE_CLOUD_API_KEY"))

    req, err := http.NewRequest("GET", url, nil)

    if err != nil {
        return nil, err
    }

    req.Header.Add("Content-Type", "application/json")

    res, err := http.DefaultClient.Do(req)

    if err != nil {
        return nil, err
    }

    defer res.Body.Close()

    resBody, err := io.ReadAll(res.Body)

    if err != nil {
        return nil, err
    }

    var voicesRes VoicesResponse
    err = json.Unmarshal(resBody, &voicesRes)

    if err != nil {
        return nil, err
    }

    return voicesRes.Voices, nil
}

func ConvertTextToAudio(input string, voice Voice) ([]byte, error) {
    url := fmt.Sprintf("https://texttospeech.googleapis.com/v1/text:synthesize?key=%s", viper.GetString("GOOGLE_CLOUD_API_KEY"))

    voiceSelectionParam, err := convertVoiceToVoiceSelectionParam(voice)
    if err != nil {
        return nil, err
    }

    data, err := json.Marshal(map[string]interface{}{
        "input":       map[string]string{"text": input},
        "voice":       voiceSelectionParam,
        "audioConfig": map[string]string{"audioEncoding": string(OGG_OPUS)},
    })

    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))

    if err != nil {
        return nil, err
    }

    req.Header.Add("Content-Type", "application/json")

    res, err := http.DefaultClient.Do(req)

    if err != nil {
        return nil, err
    }

    defer res.Body.Close()

    body, err := io.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }

    var result map[string]interface{}
    if err := json.Unmarshal(body, &result); err != nil {
        return nil, err
    }

    audioContent, ok := result["audioContent"].(string)
    if !ok {
        return nil, fmt.Errorf("No audio content found in response")
    }

    audioData, err := base64.StdEncoding.DecodeString(audioContent)
    if err != nil {
        return nil, err
    }

    return audioData, nil
}

func getFirstXVoice(voices []Voice, strToFind string, gender Gender) *Voice {
    for i, v := range voices {
        if strings.Contains(strings.ToLower(v.Name), strToFind) && v.SsmlGender == gender {
            return &voices[i]
        }
    }

    return nil
}

func GetFirstStandardVoice(voices []Voice, gender Gender) *Voice {
    return getFirstXVoice(voices, "standard", gender)
}

func GetFirstWavenetVoice(voices []Voice, gender Gender) *Voice {
    return getFirstXVoice(voices, "wavenet", gender)
}

func GetFirstNeuralVoice(voices []Voice, gender Gender) *Voice {
    return getFirstXVoice(voices, "neural", gender)
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

package main

import (
    "fmt"
    "os"

    "github.com/hsk-kr/tutorial/lib/api"
    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile(".env")
    viper.ReadInConfig()

    voices, _ := api.GetVoiceList("en-US")
    voice := api.GetFirstWavenetVoice(voices, "MALE")
    bAudio, _ := api.ConvertTextToAudio("By the way, I am using neovim.", *voice)

    os.WriteFile("./audio.opus", bAudio, 0644)
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

پس از اجرای برنامه ، فایل صوتی را در همان دایرکتوری با نام Audio.opus پیدا خواهید کرد.

سه عملکرد وجود دارد:

  • Getvoicelist – لیست صداهای پشتیبانی شده توسط API را بازیابی می کند.
  • GetFirstwavenetVoice – از تاریخ 27 فوریه 2025 ، سه نوع صدا وجود دارد: موج ، استاندارد و عصبی. هر نوع شامل چندین صدای است ، اما از آنجا که این برای من اولویت نیست ، من یک تابع ایجاد کردم تا به سادگی اولین صدای یک نوع خاص را بدست آورم.
  • ConvertTexttoAudio – متن و صدا را به عنوان پارامترها می گیرد و نتیجه را به صورت بازگرداند []بایت این عملکرد از آنجا که من قصد دارم از آن در یک محیط وب استفاده کنم ، پرونده صوتی را با فرمت OGG_OPUS درخواست می کند. با این حال ، می توانید از هر قالب پشتیبانی شده استفاده کنید.

اگر مستندات را دنبال می کنید ، ممکن است متوجه شوید که فاقد جزئیات مهم است. به عنوان مثال ، من به پارامتر Audio_Encoding رسیدم و می خواستم بررسی کنم که از کدام قالب ها پشتیبانی می شوند ، اما هیچ پیوند مستقیمی با آن اطلاعات وجود ندارد. من فکر می کنم این مستندات می تواند بهبود یابد – وقتی من به دنبال پیوندها به اسناد رمزگذاری صوتی بودم ، هیچ کدام ، فقط متن سیاه ساده را پیدا نکردم. سرانجام ، من موفق شدم با جستجوی دستی در بالای اسناد ، این سند را پیدا کنم.


package api

import (
    "bytes"
    "context"
    "errors"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/credentials"
    "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
    "github.com/aws/aws-sdk-go-v2/service/s3"
    "github.com/google/uuid"
    "github.com/spf13/viper"
)

type Storage struct {
    client     *s3.Client
    uploader   *manager.Uploader
    bucketName string
}

func (s *Storage) Init() error {
    cfg, err := config.LoadDefaultConfig(context.TODO(),
        config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(viper.GetString("CLOUDFLARE_R2_ACCESS_KEY_ID"), viper.GetString("CLOUDFLARE_R2_SECRET_ACCESS_KEY"), "")),
        config.WithRegion("auto"),
    )
    if err != nil {
        return err
    }

    s.bucketName = viper.GetString("CLOUDFLARE_R2_BUCKET_NAME")
    s.client = s3.NewFromConfig(cfg, func(o *s3.Options) {
        o.BaseEndpoint = aws.String(fmt.Sprintf("https://%s.r2.cloudflarestorage.com", viper.GetString("CLOUDFLARE_R2_ACCOUNT_ID")))
    })

    s.uploader = manager.NewUploader(s.client)
    return nil
}

func (s *Storage) Put(data []byte) (string, error) {
    if s.client == nil {
        return "", errors.New("client is nil")
    }

    objectKey, err := uuid.NewUUID()
    if err != nil {
        return "", err
    }

    bucket := aws.String(s.bucketName)
    key := aws.String(objectKey.String())
    ctx := context.Background()

    input := &s3.PutObjectInput{
        Bucket: bucket,
        Key:    key,
        Body:   bytes.NewReader(data),
    }
    output, err := s.uploader.Upload(ctx, input)
    if err != nil {
        return "", err
    }

    err = s3.NewObjectExistsWaiter(s.client).Wait(ctx, &s3.HeadObjectInput{
        Bucket: bucket,
        Key:    key,
    }, time.Minute)

    if err != nil {
        return "", err
    }

    return *output.Key, nil
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

package main

import (
    "github.com/tutorial/justsayit/lib/api"
    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile(".env")
    viper.ReadInConfig()

    voices, _ := api.GetVoiceList("en-US")
    voice := api.GetFirstWavenetVoice(voices, "MALE")
    bAudio, _ := api.ConvertTextToAudio("By the way, I am using neovim.", *voice)

    storage := new(api.Storage)

    storage.Init()
    storage.Put(bAudio)
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

CloudFlare R2 با آمازون S3 قابل مقایسه است ، بنابراین می توانید از API آن با کتابخانه AWS S3 استفاده کنید.

می توانید نمونه های بیشتری را در مستندات AWS ، https://docs.aws.amazon.com/code-library/latest/ug/go_2_s3_code_examples.html پیدا کنید.

از آنجا که ایجاد برخی از موارد قبل از بارگذاری یک شی روی ابر مورد نیاز است ، من روشهای لازم را در یک ساختار تعریف کردم.

پس از اجرای برنامه ، باید شیء را با موفقیت در CloudFlare بارگذاری کنید.

شیء بارگذاری شده

برای آزمایش اینکه آیا پرونده صوتی به درستی در یک محیط وب بارگیری می شود ، من URL شی را در یک برچسب قرار دادم – اما این کار نکرد. به نظر می رسد که حتی اگر من پرونده را در دسترس عموم قرار داده ام ، اما برای دسترسی به آن نیاز به استفاده از پروکسی داشتم. این امر از دیدگاه امنیتی معنی دارد ، زیرا دسترسی باید به صراحت در تنظیمات مجاز باشد.

اگر می خواهید برای اهداف آزمایش به طور موقت به پرونده ها دسترسی پیدا کنید ، می توانید Dev Mode را فعال کرده و از Dev Link استفاده کنید.


نتیجه نهایی

package main

import (
    "github.com/tutorial/justsayit/lib/api"
    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile(".env")
    viper.ReadInConfig()

    promptResult, _ := api.Prompt("Say something short in German")
    generatedText := promptResult.Candidates[0].Content.Parts[0].Text

    voices, _ := api.GetVoiceList("de-DE")
    voice := api.GetFirstWavenetVoice(voices, "MALE")
    bAudio, _ := api.ConvertTextToAudio(generatedText, *voice)

    storage := new(api.Storage)
    storage.Init()
    storage.Put(bAudio)
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در اینجا کد نهایی وجود دارد: تولید متن با استفاده از Google Gemini ، تبدیل متن به یک فایل صوتی و ذخیره فایل صوتی در یک بستر ابری.


امیدوارم این مفید را پیدا کنید.

برنامه نویسی مبارک!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا