Skip to content

Commit 81c31da

Browse files
Copilotks6088ts
andcommitted
Add Speeches service with Azure AI Speech integration
Co-authored-by: ks6088ts <[email protected]>
1 parent 8984c78 commit 81c31da

File tree

8 files changed

+720
-1
lines changed

8 files changed

+720
-1
lines changed

.env.template

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ AZURE_COSMOSDB_CONTAINER_NAME="items"
1313
# Azure Blob Storage
1414
AZURE_BLOB_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=<YOUR_STORAGE_ACCOUNT>;AccountKey=<YOUR_ACCOUNT_KEY>;EndpointSuffix=core.windows.net"
1515
AZURE_BLOB_STORAGE_CONTAINER_NAME="files"
16+
17+
# Azure AI Speech
18+
AZURE_SPEECH_KEY="<YOUR_SPEECH_KEY>"
19+
AZURE_SPEECH_REGION="<YOUR_SPEECH_REGION>"
20+
AZURE_SPEECH_ENDPOINT="https://<YOUR_SPEECH_REGION>.api.cognitive.microsoft.com/"

docs/index.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,37 @@ uv run python scripts/files.py delete-multiple-files "file1.txt" "file2.jpg" "fi
7373
uv run python scripts/files.py delete-multiple-files "file1.txt" "file2.jpg" "file3.pdf" --force
7474
```
7575

76+
### Speeches Service
77+
78+
```shell
79+
# Help
80+
uv run python scripts/speeches.py --help
81+
82+
# Create a new transcription job
83+
uv run python scripts/speeches.py create-transcription "https://example.com/audio.wav" --locale "ja-JP" --name "My Transcription"
84+
85+
# Get transcription job status
86+
uv run python scripts/speeches.py get-transcription JOB_ID
87+
88+
# Get transcription files
89+
uv run python scripts/speeches.py get-transcription-files JOB_ID
90+
91+
# Get transcription result
92+
uv run python scripts/speeches.py get-transcription-result "https://example.com/result.json" --save "result.json"
93+
94+
# List all transcription jobs
95+
uv run python scripts/speeches.py list-transcriptions
96+
97+
# Wait for transcription completion
98+
uv run python scripts/speeches.py wait-for-completion JOB_ID --timeout 300 --interval 10
99+
100+
# Delete transcription job
101+
uv run python scripts/speeches.py delete-transcription JOB_ID
102+
103+
# Delete transcription job (without confirmation)
104+
uv run python scripts/speeches.py delete-transcription JOB_ID --force
105+
```
106+
76107
## MCP
77108

78109
- [FastAPI-MCP](https://github.com/tadata-org/fastapi_mcp)

scripts/speeches.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#!/usr/bin/env python
2+
# filepath: /home/runner/work/template-fastapi/template-fastapi/scripts/speeches.py
3+
4+
import json
5+
import time
6+
from typing import List
7+
8+
import typer
9+
from rich.console import Console
10+
from rich.table import Table
11+
from rich.progress import Progress, SpinnerColumn, TextColumn
12+
13+
from template_fastapi.models.speech import BatchTranscriptionRequest, TranscriptionStatus
14+
from template_fastapi.repositories.speeches import SpeechRepository
15+
16+
app = typer.Typer()
17+
console = Console()
18+
speech_repo = SpeechRepository()
19+
20+
21+
@app.command()
22+
def create_transcription(
23+
content_urls: List[str] = typer.Argument(..., help="転写するファイルのURL(複数指定可能)"),
24+
locale: str = typer.Option("ja-JP", "--locale", "-l", help="言語設定"),
25+
display_name: str = typer.Option(None, "--name", "-n", help="転写ジョブの表示名"),
26+
model: str = typer.Option(None, "--model", "-m", help="使用するモデル"),
27+
):
28+
"""新しい転写ジョブを作成する"""
29+
console.print(f"[bold green]転写ジョブを作成します[/bold green]")
30+
console.print(f"ファイルURL: {', '.join(content_urls)}")
31+
console.print(f"言語設定: {locale}")
32+
33+
try:
34+
request = BatchTranscriptionRequest(
35+
content_urls=content_urls,
36+
locale=locale,
37+
display_name=display_name or "CLI Batch Transcription",
38+
model=model
39+
)
40+
41+
response = speech_repo.create_transcription_job(request)
42+
43+
console.print(f"✅ [bold green]転写ジョブが正常に作成されました[/bold green]")
44+
console.print(f"ジョブID: {response.job_id}")
45+
console.print(f"ステータス: {response.status.value}")
46+
47+
if response.message:
48+
console.print(f"メッセージ: {response.message}")
49+
50+
except Exception as e:
51+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
52+
53+
54+
@app.command()
55+
def get_transcription(
56+
job_id: str = typer.Argument(..., help="転写ジョブID"),
57+
):
58+
"""転写ジョブの状態を取得する"""
59+
console.print(f"[bold green]転写ジョブの状態を取得します[/bold green]")
60+
console.print(f"ジョブID: {job_id}")
61+
62+
try:
63+
job = speech_repo.get_transcription_job(job_id)
64+
65+
console.print(f"\n[bold blue]転写ジョブ情報[/bold blue]:")
66+
console.print(f"ID: {job.id}")
67+
console.print(f"名前: {job.name}")
68+
console.print(f"ステータス: {job.status.value}")
69+
console.print(f"作成日時: {job.created_date_time}")
70+
console.print(f"最終更新日時: {job.last_action_date_time}")
71+
72+
if job.links:
73+
console.print(f"リンク: {json.dumps(job.links, indent=2, ensure_ascii=False)}")
74+
75+
except Exception as e:
76+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
77+
78+
79+
@app.command()
80+
def get_transcription_files(
81+
job_id: str = typer.Argument(..., help="転写ジョブID"),
82+
):
83+
"""転写ジョブのファイル一覧を取得する"""
84+
console.print(f"[bold green]転写ファイル一覧を取得します[/bold green]")
85+
console.print(f"ジョブID: {job_id}")
86+
87+
try:
88+
files = speech_repo.get_transcription_files(job_id)
89+
90+
if not files:
91+
console.print("[yellow]転写ファイルが見つかりませんでした[/yellow]")
92+
return
93+
94+
# テーブルで表示
95+
table = Table(title="転写ファイル一覧")
96+
table.add_column("名前", style="cyan")
97+
table.add_column("種類", style="green")
98+
table.add_column("リンク", style="yellow")
99+
100+
for file in files:
101+
table.add_row(
102+
file.get("name", "N/A"),
103+
file.get("kind", "N/A"),
104+
file.get("links", {}).get("contentUrl", "N/A")
105+
)
106+
107+
console.print(table)
108+
console.print(f"[bold blue]合計: {len(files)}件[/bold blue]")
109+
110+
except Exception as e:
111+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
112+
113+
114+
@app.command()
115+
def get_transcription_result(
116+
file_url: str = typer.Argument(..., help="転写結果ファイルのURL"),
117+
save_file: str = typer.Option(None, "--save", "-s", help="結果を保存するファイル名"),
118+
):
119+
"""転写結果を取得する"""
120+
console.print(f"[bold green]転写結果を取得します[/bold green]")
121+
console.print(f"ファイルURL: {file_url}")
122+
123+
try:
124+
result = speech_repo.get_transcription_result(file_url)
125+
126+
console.print(f"\n[bold blue]転写結果[/bold blue]:")
127+
console.print(f"ソース: {result.source}")
128+
console.print(f"タイムスタンプ: {result.timestamp}")
129+
console.print(f"継続時間: {result.duration_in_ticks}")
130+
131+
if result.combined_recognized_phrases:
132+
console.print(f"\n[bold yellow]統合認識フレーズ[/bold yellow]:")
133+
for phrase in result.combined_recognized_phrases:
134+
console.print(f"- {phrase.get('display', 'N/A')}")
135+
136+
if result.recognized_phrases:
137+
console.print(f"\n[bold yellow]認識フレーズ({len(result.recognized_phrases)}件)[/bold yellow]:")
138+
for i, phrase in enumerate(result.recognized_phrases[:5]): # 最初の5件のみ表示
139+
console.print(f"{i+1}. {phrase.get('display', 'N/A')}")
140+
141+
if len(result.recognized_phrases) > 5:
142+
console.print(f"... および {len(result.recognized_phrases) - 5} 件の追加フレーズ")
143+
144+
# ファイルに保存
145+
if save_file:
146+
with open(save_file, 'w', encoding='utf-8') as f:
147+
json.dump(result.dict(), f, ensure_ascii=False, indent=2, default=str)
148+
console.print(f"✅ 結果を {save_file} に保存しました")
149+
150+
except Exception as e:
151+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
152+
153+
154+
@app.command()
155+
def delete_transcription(
156+
job_id: str = typer.Argument(..., help="転写ジョブID"),
157+
force: bool = typer.Option(False, "--force", "-f", help="確認なしで削除"),
158+
):
159+
"""転写ジョブを削除する"""
160+
console.print(f"[bold yellow]転写ジョブを削除します[/bold yellow]")
161+
console.print(f"ジョブID: {job_id}")
162+
163+
if not force:
164+
confirm = typer.confirm("本当に削除しますか?")
165+
if not confirm:
166+
console.print("削除をキャンセルしました")
167+
return
168+
169+
try:
170+
success = speech_repo.delete_transcription_job(job_id)
171+
172+
if success:
173+
console.print(f"✅ [bold green]転写ジョブ '{job_id}' を正常に削除しました[/bold green]")
174+
else:
175+
console.print(f"❌ [bold red]転写ジョブの削除に失敗しました[/bold red]")
176+
177+
except Exception as e:
178+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
179+
180+
181+
@app.command()
182+
def list_transcriptions():
183+
"""転写ジョブの一覧を取得する"""
184+
console.print("[bold green]転写ジョブ一覧を取得します[/bold green]")
185+
186+
try:
187+
jobs = speech_repo.list_transcription_jobs()
188+
189+
if not jobs:
190+
console.print("[yellow]転写ジョブが見つかりませんでした[/yellow]")
191+
return
192+
193+
# テーブルで表示
194+
table = Table(title="転写ジョブ一覧")
195+
table.add_column("ID", style="cyan")
196+
table.add_column("名前", style="green")
197+
table.add_column("ステータス", style="yellow")
198+
table.add_column("作成日時", style="magenta")
199+
table.add_column("最終更新日時", style="blue")
200+
201+
for job in jobs:
202+
table.add_row(
203+
job.id,
204+
job.name or "N/A",
205+
job.status.value,
206+
str(job.created_date_time) if job.created_date_time else "N/A",
207+
str(job.last_action_date_time) if job.last_action_date_time else "N/A"
208+
)
209+
210+
console.print(table)
211+
console.print(f"[bold blue]合計: {len(jobs)}件[/bold blue]")
212+
213+
except Exception as e:
214+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
215+
216+
217+
@app.command()
218+
def wait_for_completion(
219+
job_id: str = typer.Argument(..., help="転写ジョブID"),
220+
timeout: int = typer.Option(300, "--timeout", "-t", help="タイムアウト時間(秒)"),
221+
interval: int = typer.Option(10, "--interval", "-i", help="チェック間隔(秒)"),
222+
):
223+
"""転写ジョブの完了を待つ"""
224+
console.print(f"[bold green]転写ジョブの完了を待ちます[/bold green]")
225+
console.print(f"ジョブID: {job_id}")
226+
console.print(f"タイムアウト: {timeout}秒")
227+
console.print(f"チェック間隔: {interval}秒")
228+
229+
start_time = time.time()
230+
231+
with Progress(
232+
SpinnerColumn(),
233+
TextColumn("[progress.description]{task.description}"),
234+
transient=True,
235+
) as progress:
236+
task = progress.add_task(description="転写処理中...", total=None)
237+
238+
while time.time() - start_time < timeout:
239+
try:
240+
job = speech_repo.get_transcription_job(job_id)
241+
242+
if job.status == TranscriptionStatus.SUCCEEDED:
243+
progress.update(task, description="✅ 転写が完了しました")
244+
console.print(f"✅ [bold green]転写ジョブが正常に完了しました[/bold green]")
245+
console.print(f"ジョブID: {job.id}")
246+
console.print(f"最終更新日時: {job.last_action_date_time}")
247+
return
248+
elif job.status == TranscriptionStatus.FAILED:
249+
progress.update(task, description="❌ 転写が失敗しました")
250+
console.print(f"❌ [bold red]転写ジョブが失敗しました[/bold red]")
251+
console.print(f"ジョブID: {job.id}")
252+
return
253+
elif job.status == TranscriptionStatus.RUNNING:
254+
progress.update(task, description="🔄 転写処理中...")
255+
else:
256+
progress.update(task, description=f"⏳ 待機中 ({job.status.value})")
257+
258+
time.sleep(interval)
259+
260+
except Exception as e:
261+
progress.update(task, description=f"❌ エラー: {str(e)}")
262+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
263+
return
264+
265+
# タイムアウト
266+
console.print(f"⏰ [bold yellow]タイムアウトしました({timeout}秒)[/bold yellow]")
267+
console.print("転写ジョブはまだ処理中の可能性があります")
268+
269+
270+
if __name__ == "__main__":
271+
app()

template_fastapi/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
1212
from opentelemetry.trace import Span
1313

14-
from template_fastapi.routers import demos, files, foodies, games, items
14+
from template_fastapi.routers import demos, files, foodies, games, items, speeches
1515

1616
app = FastAPI()
1717

@@ -41,3 +41,4 @@ def server_request_hook(span: Span, scope: dict):
4141
app.include_router(games.router)
4242
app.include_router(foodies.router)
4343
app.include_router(files.router)
44+
app.include_router(speeches.router)

0 commit comments

Comments
 (0)