Skip to content

Commit d3d2172

Browse files
authored
Merge pull request #1 from codergeek121/openai_text_to_speech_with_rails
Add project files
2 parents c5f851f + a5ad920 commit d3d2172

29 files changed

+516
-23
lines changed

openai_text_to_speech_with_rails/Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ ruby "3.2.2"
44

55
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
66
gem "rails", "~> 7.1.2"
7+
gem "webmock"
8+
gem "ruby-openai"
79

810
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
911
gem "sprockets-rails"

openai_text_to_speech_with_rails/Gemfile.lock

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ GEM
9494
xpath (~> 3.2)
9595
concurrent-ruby (1.2.2)
9696
connection_pool (2.4.1)
97+
crack (0.4.5)
98+
rexml
9799
crass (1.0.6)
98100
date (3.3.4)
99101
debug (1.8.0)
@@ -102,8 +104,17 @@ GEM
102104
drb (2.2.0)
103105
ruby2_keywords
104106
erubi (1.12.0)
107+
event_stream_parser (1.0.0)
108+
faraday (2.7.12)
109+
base64
110+
faraday-net_http (>= 2.0, < 3.1)
111+
ruby2_keywords (>= 0.0.4)
112+
faraday-multipart (1.0.4)
113+
multipart-post (~> 2)
114+
faraday-net_http (3.0.2)
105115
globalid (1.2.1)
106116
activesupport (>= 6.1)
117+
hashdiff (1.0.1)
107118
i18n (1.14.1)
108119
concurrent-ruby (~> 1.0)
109120
importmap-rails (1.2.3)
@@ -130,6 +141,7 @@ GEM
130141
mini_mime (1.1.5)
131142
minitest (5.20.0)
132143
msgpack (1.7.2)
144+
multipart-post (2.3.0)
133145
mutex_m (0.2.0)
134146
net-imap (0.4.7)
135147
date
@@ -201,6 +213,10 @@ GEM
201213
reline (0.4.1)
202214
io-console (~> 0.5)
203215
rexml (3.2.6)
216+
ruby-openai (6.3.0)
217+
event_stream_parser (>= 0.3.0, < 2.0.0)
218+
faraday (>= 1)
219+
faraday-multipart (>= 1)
204220
ruby2_keywords (0.0.5)
205221
rubyzip (2.3.2)
206222
selenium-webdriver (4.15.0)
@@ -233,6 +249,10 @@ GEM
233249
activemodel (>= 6.0.0)
234250
bindex (>= 0.4.0)
235251
railties (>= 6.0.0)
252+
webmock (3.19.1)
253+
addressable (>= 2.8.0)
254+
crack (>= 0.3.2)
255+
hashdiff (>= 0.4.0, < 2.0.0)
236256
webrick (1.8.1)
237257
websocket (1.2.10)
238258
websocket-driver (0.7.6)
@@ -256,13 +276,15 @@ DEPENDENCIES
256276
puma (>= 5.0)
257277
rails (~> 7.1.2)
258278
redis (>= 4.0.1)
279+
ruby-openai
259280
selenium-webdriver
260281
sprockets-rails
261282
sqlite3 (~> 1.4)
262283
stimulus-rails
263284
turbo-rails
264285
tzinfo-data
265286
web-console
287+
webmock
266288

267289
RUBY VERSION
268290
ruby 3.2.2p53
Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
1-
# README
1+
# OpenAI TTS with Rails
22

3-
This README would normally document whatever steps are necessary to get the
4-
application up and running.
3+
Read more about the project in the actual blogpost: http://localhost:1313/posts/using-the-openai-text-to-speech-api-with-rails/
54

6-
Things you may want to cover:
5+
## Setup
76

8-
* Ruby version
7+
1. Run `bin/setup`.
8+
2. Run `rails credentials:edit --environment development` and provide your own OpenAI API token `open_ai_access_token: <yourtokengoeshere>`
9+
3. Run `rails server`
10+
4. Visit [/articles](localhost:3000/articles)
911

10-
* System dependencies
12+
Have fun!
1113

12-
* Configuration
14+
## Tests
1315

14-
* Database creation
15-
16-
* Database initialization
17-
18-
* How to run the test suite
19-
20-
* Services (job queues, cache servers, search engines, etc.)
21-
22-
* Deployment instructions
23-
24-
* ...
16+
1. Run `bin/setup`.
17+
2. Run `rails credentials:edit --environment test` and add `open_ai_access_token: test`
18+
3. Run `rails test`
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
class ArticlesController < ApplicationController
2+
before_action :set_article, only: %i[ show edit update destroy ]
3+
4+
# GET /articles or /articles.json
5+
def index
6+
@articles = Article.all
7+
end
8+
9+
# GET /articles/1 or /articles/1.json
10+
def show
11+
end
12+
13+
# GET /articles/new
14+
def new
15+
@article = Article.new
16+
end
17+
18+
# GET /articles/1/edit
19+
def edit
20+
end
21+
22+
# POST /articles or /articles.json
23+
def create
24+
@article = Article.new(article_params)
25+
26+
respond_to do |format|
27+
if @article.save
28+
format.html { redirect_to article_url(@article), notice: "Article was successfully created." }
29+
format.json { render :show, status: :created, location: @article }
30+
else
31+
format.html { render :new, status: :unprocessable_entity }
32+
format.json { render json: @article.errors, status: :unprocessable_entity }
33+
end
34+
end
35+
end
36+
37+
# PATCH/PUT /articles/1 or /articles/1.json
38+
def update
39+
respond_to do |format|
40+
if @article.update(article_params)
41+
format.html { redirect_to article_url(@article), notice: "Article was successfully updated." }
42+
format.json { render :show, status: :ok, location: @article }
43+
else
44+
format.html { render :edit, status: :unprocessable_entity }
45+
format.json { render json: @article.errors, status: :unprocessable_entity }
46+
end
47+
end
48+
end
49+
50+
# DELETE /articles/1 or /articles/1.json
51+
def destroy
52+
@article.destroy!
53+
54+
respond_to do |format|
55+
format.html { redirect_to articles_url, notice: "Article was successfully destroyed." }
56+
format.json { head :no_content }
57+
end
58+
end
59+
60+
private
61+
# Use callbacks to share common setup or constraints between actions.
62+
def set_article
63+
@article = Article.find(params[:id])
64+
end
65+
66+
# Only allow a list of trusted parameters through.
67+
def article_params
68+
params.require(:article).permit(:content)
69+
end
70+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
module ArticlesHelper
2+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class TextToSpeechJob < ApplicationJob
2+
queue_as :default
3+
retry_on Faraday::Error, wait: :polynomially_longer, attempts: 10
4+
5+
def perform(article)
6+
response = TextToSpeech.new.speech(article.content)
7+
article.audio.attach(
8+
io: StringIO.new(response, 'rb'),
9+
filename: "article--#{article.id}.mp3"
10+
)
11+
end
12+
end
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class Article < ApplicationRecord
2+
broadcasts
3+
4+
has_one_attached :audio
5+
6+
after_commit :generate_audio_content_mp3, if: :content_previously_changed?
7+
8+
private
9+
10+
def generate_audio_content_mp3
11+
TextToSpeechJob.perform_later(self)
12+
end
13+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class TextToSpeech
2+
def initialize
3+
@client = OpenAI::Client.new(access_token: Rails.application.credentials.open_ai_access_token)
4+
end
5+
6+
def speech(text)
7+
@client.audio.speech(
8+
parameters: {
9+
model: "tts-1-hd",
10+
input: text,
11+
voice: "echo",
12+
speed: 0.95
13+
}
14+
)
15+
end
16+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div id="<%= dom_id article %>">
2+
<%= simple_format article.content %>
3+
4+
<% if article.audio.attached? %>
5+
<%= audio_tag article.audio, controls: true %>
6+
<% else %>
7+
<p>Audio is generating...</p>
8+
<% end %>
9+
</div>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
json.extract! article, :id, :content, :created_at, :updated_at
2+
json.url article_url(article, format: :json)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<%= form_with(model: article) do |form| %>
2+
<% if article.errors.any? %>
3+
<div style="color: red">
4+
<h2><%= pluralize(article.errors.count, "error") %> prohibited this article from being saved:</h2>
5+
6+
<ul>
7+
<% article.errors.each do |error| %>
8+
<li><%= error.full_message %></li>
9+
<% end %>
10+
</ul>
11+
</div>
12+
<% end %>
13+
14+
<div>
15+
<%= form.label :content, style: "display: block" %>
16+
<%= form.text_area :content %>
17+
</div>
18+
19+
<div>
20+
<%= form.submit %>
21+
</div>
22+
<% end %>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<h1>Editing article</h1>
2+
3+
<%= render "form", article: @article %>
4+
5+
<br>
6+
7+
<div>
8+
<%= link_to "Show this article", @article %> |
9+
<%= link_to "Back to articles", articles_path %>
10+
</div>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<p style="color: green"><%= notice %></p>
2+
3+
<h1>Articles</h1>
4+
5+
<div id="articles">
6+
<% @articles.each do |article| %>
7+
<%= render article %>
8+
<p>
9+
<%= link_to "Show this article", article %>
10+
</p>
11+
<% end %>
12+
</div>
13+
14+
<%= link_to "New article", new_article_path %>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
json.array! @articles, partial: "articles/article", as: :article
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<h1>New article</h1>
2+
3+
<%= render "form", article: @article %>
4+
5+
<br>
6+
7+
<div>
8+
<%= link_to "Back to articles", articles_path %>
9+
</div>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<%= turbo_stream_from(@article) %>
2+
<p style="color: green"><%= notice %></p>
3+
4+
<%= render @article %>
5+
6+
<div>
7+
<%= link_to "Edit this article", edit_article_path(@article) %> |
8+
<%= link_to "Back to articles", articles_path %>
9+
10+
<%= button_to "Destroy this article", @article, method: :delete %>
11+
</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
json.partial! "articles/article", article: @article

openai_text_to_speech_with_rails/bin/setup

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ FileUtils.chdir APP_ROOT do
1717
system! "gem install bundler --conservative"
1818
system("bundle check") || system!("bundle install")
1919

20-
# puts "\n== Copying sample files =="
21-
# unless File.exist?("config/database.yml")
22-
# FileUtils.cp "config/database.yml.sample", "config/database.yml"
23-
# end
24-
2520
puts "\n== Preparing database =="
2621
system! "bin/rails db:prepare"
2722

openai_text_to_speech_with_rails/config/environments/development.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,9 @@
7373

7474
# Raise error when a before_action's only/except options reference missing actions
7575
config.action_controller.raise_on_missing_callback_actions = true
76+
77+
config.action_controller.default_url_options = {
78+
host: 'localhost',
79+
port: '3000',
80+
}
7681
end

openai_text_to_speech_with_rails/config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Rails.application.routes.draw do
2+
resources :articles
23
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
34

45
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class CreateArticles < ActiveRecord::Migration[7.1]
2+
def change
3+
create_table :articles do |t|
4+
t.text :content
5+
6+
t.timestamps
7+
end
8+
end
9+
end

0 commit comments

Comments
 (0)