Compare commits
10 Commits
5469707c2d
...
f23733f78b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f23733f78b | ||
|
|
c40cc5d2a2 | ||
|
|
a2759b8169 | ||
|
|
0fd8170c5b | ||
|
|
f047a3c1c2 | ||
|
|
8e30a6ffbb | ||
|
|
64dc8d2517 | ||
|
|
fa567efd3a | ||
|
|
f59350cd73 | ||
|
|
96a0390418 |
3
.env
3
.env
@@ -1,3 +0,0 @@
|
||||
OPENAI_API_KEY=sk-e2f00b9fed01443b87407513ab14c494
|
||||
OPENAI_MODEL=deepseek-chat
|
||||
OPENAI_API_BASE=https://api.deepseek.com/v1 # (optional override if needed)
|
||||
100
.gitignore
vendored
100
.gitignore
vendored
@@ -0,0 +1,100 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Virtual environment
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv/
|
||||
.ENV/
|
||||
.env/
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Unit test / coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# MyPy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Local .env or config
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# SQLite
|
||||
*.sqlite3
|
||||
|
||||
# FastAPI docs build (if using Sphinx)
|
||||
_build/
|
||||
docs/_build/
|
||||
".env"
|
||||
|
||||
# Ignore all files in the pycache folder
|
||||
__pycache__
|
||||
app/__pycache__/main.cpython-313.pyc
|
||||
app/core/__pycache__/config.cpython-313.pyc
|
||||
app/core/__pycache__/openai_voice_client.cpython-313.pyc
|
||||
app/core/__pycache__/deepseek_client.cpython-313.pyc
|
||||
app/core/__pycache__/openai_client.cpython-313.pyc
|
||||
app/core/__pycache__/deepseek_voice_client.cpython-313.pyc
|
||||
app/services/__pycache__/voice.cpython-313.pyc
|
||||
app/services/__pycache__/translator.cpython-313.pyc
|
||||
app/api/__pycache__/voice.cpython-313.pyc
|
||||
app/api/__pycache__/translate.cpython-313.pyc
|
||||
|
||||
Binary file not shown.
Binary file not shown.
15
app/api/v1/lyric_romanji_translator.py
Normal file
15
app/api/v1/lyric_romanji_translator.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from fastapi import APIRouter
|
||||
from app.models.lyric_romanji_translator import LyricRomanjiTranslatorRequest, LyricRomanjiTranslatorResponse
|
||||
from app.services.lyric_romanji_translator import translate_lyric_romanji
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=LyricRomanjiTranslatorResponse)
|
||||
async def lyric_romanji_translator(request: LyricRomanjiTranslatorRequest):
|
||||
lyric_romanji = await translate_lyric_romanji(request.folder_path)
|
||||
return LyricRomanjiTranslatorResponse(
|
||||
results=lyric_romanji["results"],
|
||||
status=lyric_romanji["status"]
|
||||
)
|
||||
|
||||
10
app/api/v1/text_generator.py
Normal file
10
app/api/v1/text_generator.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
from app.models.text_generation import BPOMMobileResponseTextGenerationRequest, BPOMMobileResponseTextGenerationResponse
|
||||
from app.services.text_generation import generate_text
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=BPOMMobileResponseTextGenerationResponse)
|
||||
async def text_generator(request: BPOMMobileResponseTextGenerationRequest):
|
||||
text = await generate_text(request)
|
||||
return text
|
||||
16
app/api/v1/voice.py
Normal file
16
app/api/v1/voice.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter
|
||||
from app.models.voice import VoiceRequest, VoiceResponse, TranscriptionRequest, TranscriptionResponse
|
||||
from app.services.voice import generate_voice, generate_transcription
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=VoiceResponse)
|
||||
async def voice(request: VoiceRequest):
|
||||
voice = await generate_voice(request.text)
|
||||
return VoiceResponse(voice=voice)
|
||||
|
||||
@router.post("/transcription", response_model=TranscriptionResponse)
|
||||
async def transcription(request: TranscriptionRequest):
|
||||
transcription = await generate_transcription(request.audio_file_path)
|
||||
return TranscriptionResponse(transcription=transcription)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -8,3 +8,8 @@ load_dotenv()
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
||||
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
|
||||
OPENAI_API_BASE = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
|
||||
DEEPSEEK_API_BASE = os.getenv("DEEPSEEK_API_BASE", "https://api.deepseek.com/v1")
|
||||
DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-chat")
|
||||
OPENAI_AUDIO_MODEL = os.getenv("OPENAI_AUDIO_MODEL", "")
|
||||
|
||||
|
||||
@@ -1,27 +1,44 @@
|
||||
# app/services/openai_service.py
|
||||
import openai
|
||||
from openai import OpenAI
|
||||
from app.core.config import OPENAI_API_KEY, OPENAI_MODEL
|
||||
from app.core.config import OPENAI_API_BASE
|
||||
import os
|
||||
import anyio
|
||||
from openai import OpenAI, OpenAIError
|
||||
from app.core.config import DEEPSEEK_API_BASE, DEEPSEEK_MODEL, DEEPSEEK_API_KEY
|
||||
|
||||
# Set OpenAI API key from the environment
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
# Ensure the API key is properly set
|
||||
if not DEEPSEEK_API_KEY:
|
||||
raise ValueError("DEEPSEEK_API_KEY is not set in environment variables")
|
||||
|
||||
print(openai.api_key)
|
||||
print(OPENAI_MODEL)
|
||||
print(OPENAI_API_BASE)
|
||||
# Initialize the client
|
||||
client = OpenAI(
|
||||
api_key=DEEPSEEK_API_KEY,
|
||||
base_url=DEEPSEEK_API_BASE
|
||||
)
|
||||
|
||||
async def chat_with_openai(messages: list):
|
||||
# Use the model from environment variable or fallback to default
|
||||
model = OPENAI_MODEL
|
||||
client = OpenAI(api_key=openai.api_key, base_url=openai.api_base)
|
||||
async def chat_with_openai(messages: list[dict[str, str]]) -> str:
|
||||
if not messages:
|
||||
raise ValueError("Messages list cannot be empty")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="deepseek-chat", # Or the model you want
|
||||
messages=messages, # Update this according to the new API syntax
|
||||
max_tokens=100, # Example parameter
|
||||
stream=False
|
||||
)
|
||||
|
||||
return response.choices[0].message.content
|
||||
try:
|
||||
# Run sync client in a thread (non-blocking for FastAPI)
|
||||
response = await anyio.to_thread.run_sync(
|
||||
lambda: client.chat.completions.create(
|
||||
model=DEEPSEEK_MODEL,
|
||||
messages=messages,
|
||||
max_tokens=1000,
|
||||
temperature=0.7,
|
||||
stream=False
|
||||
)
|
||||
)
|
||||
|
||||
if not response.choices or not response.choices[0].message.content:
|
||||
return "No response content from the model"
|
||||
|
||||
return response.choices[0].message.content.strip()
|
||||
|
||||
except OpenAIError as e:
|
||||
error_msg = f"DeepSeek API Error: {str(e)}"
|
||||
print(error_msg)
|
||||
raise Exception(error_msg) from e
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
print(error_msg)
|
||||
raise Exception(error_msg) from e
|
||||
|
||||
67
app/core/openai_voice_transcription_client.py
Normal file
67
app/core/openai_voice_transcription_client.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import openai
|
||||
from openai import OpenAI
|
||||
from openai import OpenAIError
|
||||
from app.core.config import OPENAI_API_KEY, OPENAI_AUDIO_MODEL, OPENAI_API_BASE
|
||||
|
||||
# Ensure the API key is properly set
|
||||
if not OPENAI_API_KEY:
|
||||
raise ValueError("OPENAI_API_KEY is not set in environment variables")
|
||||
|
||||
# Initialize the client with proper configuration
|
||||
client = OpenAI(
|
||||
api_key=OPENAI_API_KEY,
|
||||
base_url=OPENAI_API_BASE
|
||||
)
|
||||
|
||||
async def generate_voice(messages: list):
|
||||
if not messages:
|
||||
raise ValueError("Messages list cannot be empty")
|
||||
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model=OPENAI_AUDIO_MODEL,
|
||||
messages=messages,
|
||||
max_tokens=1000,
|
||||
temperature=0.7,
|
||||
stream=False
|
||||
)
|
||||
|
||||
if not response.choices or not response.choices[0].message.content:
|
||||
return "No response content from the model"
|
||||
|
||||
return response.choices[0].message.content
|
||||
|
||||
except OpenAIError as e:
|
||||
error_msg = f"OpenAI API Error: {str(e)}"
|
||||
print(error_msg)
|
||||
raise Exception(error_msg) from e
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
print(error_msg)
|
||||
raise Exception(error_msg) from e
|
||||
|
||||
async def generate_transcription(audio_file_path: str) -> str:
|
||||
if not audio_file_path:
|
||||
raise ValueError("Audio file path cannot be empty")
|
||||
|
||||
try:
|
||||
response = client.audio.transcriptions.create(
|
||||
model=OPENAI_AUDIO_MODEL,
|
||||
file=audio_file_path,
|
||||
response_format="text",
|
||||
language="id"
|
||||
)
|
||||
|
||||
if not response.choices or not response.choices[0].message.content:
|
||||
return "No response content from the model"
|
||||
|
||||
return response.choices[0].message.content
|
||||
|
||||
except OpenAIError as e:
|
||||
error_msg = f"OpenAI API Error: {str(e)}"
|
||||
print(error_msg)
|
||||
raise Exception(error_msg) from e
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
print(error_msg)
|
||||
raise Exception(error_msg) from e
|
||||
@@ -1,7 +1,12 @@
|
||||
from fastapi import FastAPI
|
||||
from app.api.v1 import translate
|
||||
from app.api.v1 import voice
|
||||
from app.api.v1 import lyric_romanji_translator
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Include your routes
|
||||
app.include_router(translate.router, prefix="/api/v1/translate", tags=["translate"])
|
||||
app.include_router(voice.router, prefix="/api/v1/voice", tags=["voice"])
|
||||
app.include_router(lyric_romanji_translator.router, prefix="/api/v1/lyric_romanji_translator", tags=["lyric_romanji_translator"])
|
||||
app.include_router(text_generator.router, prefix="/api/v1/text_generator", tags=["text_generator"])
|
||||
Binary file not shown.
14
app/models/lyric_romanji_translator.py
Normal file
14
app/models/lyric_romanji_translator.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
class LyricRomanjiTranslatorRequest(BaseModel):
|
||||
folder_path: str
|
||||
|
||||
class FileResult(BaseModel):
|
||||
file: str
|
||||
processed: bool
|
||||
added_lines: int
|
||||
|
||||
class LyricRomanjiTranslatorResponse(BaseModel):
|
||||
results: List[FileResult]
|
||||
status: str
|
||||
5
app/models/text_generation.py
Normal file
5
app/models/text_generation.py
Normal file
@@ -0,0 +1,5 @@
|
||||
class BPOMMobileResponseTextGenerationRequest(BaseModel):
|
||||
text: str
|
||||
|
||||
class BPOMMobileResponseTextGenerationRequest(BaseModel):
|
||||
generated_text: str
|
||||
17
app/models/voice.py
Normal file
17
app/models/voice.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
# Text-to-Speech Models
|
||||
class VoiceRequest(BaseModel):
|
||||
text: str
|
||||
|
||||
class VoiceResponse(BaseModel):
|
||||
voice_output: str
|
||||
|
||||
# Speech-to-Text Models
|
||||
class TranscriptionRequest(BaseModel):
|
||||
audio_file_path: str
|
||||
target_language: Optional[str] = "id" # Default to English
|
||||
|
||||
class TranscriptionResponse(BaseModel):
|
||||
text: str
|
||||
Binary file not shown.
68
app/services/lyric_romanji_translator.py
Normal file
68
app/services/lyric_romanji_translator.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import os
|
||||
import re
|
||||
import asyncio
|
||||
from app.core.deepseek_client import chat_with_openai
|
||||
from app.models.lyric_romanji_translator import FileResult
|
||||
|
||||
semaphore = asyncio.Semaphore(5)
|
||||
timestamp_pattern = re.compile(r"^\[\d{2}:\d{2}\.\d{2}\]")
|
||||
|
||||
def needs_romaji(lines, idx):
|
||||
if idx + 1 < len(lines) and not timestamp_pattern.match(lines[idx + 1]):
|
||||
return False
|
||||
return True
|
||||
|
||||
async def get_romaji(text: str) -> str:
|
||||
messages = [
|
||||
{"role": "system", "content": "Convert Japanese text into romaji only. Output romaji without explanation."},
|
||||
{"role": "user", "content": text}
|
||||
]
|
||||
return await chat_with_openai(messages)
|
||||
|
||||
async def process_lrc_file(filepath: str) -> FileResult:
|
||||
added_lines = 0
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
new_lines = []
|
||||
for idx, line in enumerate(lines):
|
||||
new_lines.append(line)
|
||||
|
||||
if timestamp_pattern.match(line) and needs_romaji(lines, idx):
|
||||
japanese = line.strip().split("]", 1)[-1].strip()
|
||||
if japanese:
|
||||
romaji = await get_romaji(japanese)
|
||||
new_lines.append(f"{romaji}\n")
|
||||
added_lines += 1
|
||||
|
||||
if added_lines > 0:
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
return FileResult(file=filepath, processed=added_lines > 0, added_lines=added_lines)
|
||||
|
||||
async def safe_process(filepath):
|
||||
async with semaphore:
|
||||
print(f"Processing: {filepath}")
|
||||
return await process_lrc_file(filepath)
|
||||
|
||||
async def translate_lyric_romanji(folder_path: str):
|
||||
results = []
|
||||
|
||||
if not os.path.exists(folder_path):
|
||||
return {"results": [], "status": f"error: folder not found {folder_path}"}
|
||||
|
||||
tasks = []
|
||||
for root, _, files in os.walk(folder_path):
|
||||
for file in files:
|
||||
if file.endswith(".lrc"):
|
||||
filepath = os.path.join(root, file)
|
||||
tasks.append(asyncio.create_task(safe_process(filepath)))
|
||||
|
||||
if not tasks:
|
||||
return {"results": [], "status": "no .lrc files found"}
|
||||
|
||||
# Run them all concurrently
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
return {"results": results, "status": "completed"}
|
||||
88
app/services/text_generation.py
Normal file
88
app/services/text_generation.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import os
|
||||
import asyncio
|
||||
from app.core.deepseek_client import chat_with_openai
|
||||
from app.models.text_generation import BPOMMobileResponseTextGenerationRequest, BPOMMobileResponseTextGenerationResponse
|
||||
|
||||
async def generate_text(request: BPOMMobileResponseTextGenerationRequest) -> BPOMMobileResponseTextGenerationResponse:
|
||||
messages = [
|
||||
{"role": "system", "content": """
|
||||
Anda adalah asisten virtual resmi BPOM (Badan Pengawas Obat dan Makanan) yang bertugas menanggapi keluhan dan review pengguna aplikasi BPOM Mobile.
|
||||
|
||||
Tugas Utama:
|
||||
|
||||
Merespons semua jenis review (negatif, positif, netral) dengan profesional dan empati
|
||||
|
||||
Fokus pada solusi dan bantuan teknis
|
||||
|
||||
Menjaga citra positif institusi BPOM
|
||||
|
||||
Panduan Respons:
|
||||
|
||||
Untuk keluhan teknis (scan error, akses lambat, dll):
|
||||
|
||||
Awali dengan permintaan maaf yang tulus
|
||||
|
||||
Sarankan update aplikasi ke versi terbaru
|
||||
|
||||
Informasikan perbaikan berkelanjutan
|
||||
|
||||
Sediakan kontak support: barcodebpom@pom.go.id
|
||||
|
||||
Untuk review positif:
|
||||
|
||||
Ucapkan terima kasih
|
||||
|
||||
Tegaskan komitmen untuk terus meningkatkan kualitas
|
||||
|
||||
Dorong untuk terus menggunakan aplikasi
|
||||
|
||||
Untuk review negatif dengan emosi tinggi:
|
||||
|
||||
Tunjukkan empati lebih dalam
|
||||
|
||||
Hindari jargon teknis
|
||||
|
||||
Berikan solusi alternatif (input manual nomor registrasi)
|
||||
|
||||
Tawarkan jalur eskalsi via email
|
||||
|
||||
Format Respons:
|
||||
|
||||
Gunakan sapaan "Sobat Cerdas BPOM"
|
||||
|
||||
Bahasa informal namun profesional
|
||||
|
||||
Maksimal 3-4 kalimat
|
||||
|
||||
Selalu sertakan opsi kontak support
|
||||
|
||||
Contoh Respons yang Diinginkan:
|
||||
|
||||
Untuk review negatif:
|
||||
"Sobat Cerdas BPOM, mohon maaf atas kendala yang dialami. Tim kami terus melakukan perbaikan sistem. Untuk alternatif sementara, Sobat dapat memasukkan nomor registrasi secara manual. Jika kendala berlanjut, silakan hubungi barcodebpom@pom.go.id untuk bantuan lebih lanjut."
|
||||
|
||||
Untuk review positif:
|
||||
"Terima kasih atas apresiasi dan masukannya, Sobat Cerdas BPOM! Semangat ini akan kami teruskan untuk memberikan pelayanan terbaik. Jangan ragu untuk memberikan saran pengembangan lainnya ya!"
|
||||
|
||||
Penyesuaian Dinamis:
|
||||
|
||||
Sesuaikan tingkat empati berdasarkan tingkat emosi review
|
||||
|
||||
Untuk review dengan emosi sangat tinggi, tambahkan kalimat penenang
|
||||
|
||||
Untuk masalah spesifik, berikan solusi yang lebih terarah
|
||||
|
||||
Batasan:
|
||||
|
||||
Tidak membuat janji perbaikan yang tidak dapat ditepati
|
||||
|
||||
Tidak menyalahkan pengguna
|
||||
|
||||
Tidak memberikan respons template yang sama persis
|
||||
|
||||
Menghindari istilah teknis yang rumit
|
||||
"""},
|
||||
{"role": "user", "content": request.text}
|
||||
]
|
||||
generated_text = await chat_with_openai(messages)
|
||||
return BPOMMobileResponseTextGenerationResponse(generated_text=generated_text)
|
||||
10
app/services/voice.py
Normal file
10
app/services/voice.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from app.core.openai_voice_transcription_client import generate_voice, generate_transcription
|
||||
|
||||
async def generate_voice(text: str) -> str:
|
||||
voice = await generate_voice(text)
|
||||
return voice
|
||||
|
||||
async def generate_transcription(audio_file_path: str) -> str:
|
||||
transcription = await generate_transcription(audio_file_path)
|
||||
return transcription
|
||||
|
||||
31
readme.md
Normal file
31
readme.md
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
# Project Title
|
||||
|
||||
A brief description of what this project does and who it's for
|
||||
|
||||
|
||||
## Run Locally
|
||||
|
||||
Clone the project
|
||||
|
||||
```bash
|
||||
git clone https://link-to-project
|
||||
```
|
||||
|
||||
Go to the project directory
|
||||
|
||||
```bash
|
||||
cd my-project
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Start the server
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
Reference in New Issue
Block a user