diff --git a/.gitignore b/.gitignore index 50da8f0..601b34f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ /tailwind/* *.log /temp_audio/* -app/temp_audio/* \ No newline at end of file +app/temp_audio/* +app/temp_pdfs/* \ No newline at end of file diff --git a/app/api_routes.py b/app/api_routes.py index d7aba91..e41a074 100644 --- a/app/api_routes.py +++ b/app/api_routes.py @@ -1,6 +1,8 @@ from flask import Flask, Blueprint, request, jsonify, session, Response, send_file +from fpdf import FPDF import re import os +import uuid from sqlalchemy import func from .models import User, Chatbot, Chat, Image, Comment, ChatbotVersion from sqlalchemy.exc import IntegrityError @@ -792,7 +794,6 @@ def api_tts(): return jsonify({"success": False, "message": "Text not found"}), 400 filepath = text_to_mp3(text) - print(filepath) response = send_file(filepath, as_attachment=True) @@ -850,3 +851,74 @@ def api_ocr(): except Exception as e: return jsonify({"success": False, "message": str(e)}), 500 + + +class HandwrittenPDF(FPDF): + def header(self): + pass + + def footer(self): + pass + + def add_custom_font(self, font_name, font_path): + self.add_font(font_name, "", font_path, uni=True) + + +@api_bp.route("/api/tth", methods=["POST"]) +@jwt_required() +def api_tth(): + try: + data = request.get_json() + text = data.get("text", "") + font_size = data.get("font_size", 12) + pdf = HandwrittenPDF() + + custom_font_name = "Handwritten" # Font identifier + font_path = os.path.join(os.path.dirname(__file__), "fonts", "handwriting.ttf") + pdf.add_custom_font(custom_font_name, font_path) + + pdf.add_page() + pdf.set_font(custom_font_name, size=font_size) # Use the custom font + pdf.set_text_color(0, 0, 255) # Blue ink color + + # Text formatting + line_height = font_size * 0.9 + margin = 10 + page_width = pdf.w - 2 * margin + pdf.set_left_margin(margin) + pdf.set_right_margin(margin) + + # Wrap text to fit within the page width + lines = text.split("\n") + for line in lines: + words = line.split() + current_line = "" + for word in words: + test_line = f"{current_line} {word}".strip() + if pdf.get_string_width(test_line) <= page_width: + current_line = test_line + else: + pdf.cell(0, line_height, current_line, ln=True) + current_line = word + if current_line: + pdf.cell(0, line_height, current_line, ln=True) + pdf.ln(line_height * 0.2) # Add extra line space after each paragraph + + base_path = os.path.dirname( + os.path.abspath(__file__) + ) # Get the absolute path of the script + temp_dir = os.path.join(base_path, "temp_pdfs") + os.makedirs(temp_dir, exist_ok=True) + # Save the PDF + output_path = f"{temp_dir}/{uuid.uuid4()}.pdf" + pdf.output(output_path) + + response = send_file(output_path, as_attachment=True) + + @response.call_on_close + def remove_file(): + os.remove(output_path) + + return response + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 500 diff --git a/app/fonts/handwriting.pkl b/app/fonts/handwriting.pkl new file mode 100644 index 0000000..4d8a19b Binary files /dev/null and b/app/fonts/handwriting.pkl differ diff --git a/app/fonts/handwriting.ttf b/app/fonts/handwriting.ttf new file mode 100644 index 0000000..aea1516 Binary files /dev/null and b/app/fonts/handwriting.ttf differ diff --git a/client/bun.lockb b/client/bun.lockb index 2bd5376..f0da48e 100755 Binary files a/client/bun.lockb and b/client/bun.lockb differ diff --git a/client/src/components/modals/command-modal.tsx b/client/src/components/modals/command-modal.tsx index 3793722..a9c0bc6 100644 --- a/client/src/components/modals/command-modal.tsx +++ b/client/src/components/modals/command-modal.tsx @@ -19,6 +19,7 @@ import { Image, Languages, PanelTopInactive, + PenLineIcon, Plus, TextCursorInput, } from "lucide-react"; @@ -28,6 +29,7 @@ import { useOcrMagic, useSettingsModal, useTranslateMagicModal, + usettHMagic, useTtsMagicModal, } from "@/stores/modal-store"; import { useNavigate } from "react-router-dom"; @@ -41,6 +43,7 @@ export function CommandModal() { const imagineModal = useImagineModal(); const ttsModal = useTtsMagicModal(); const ocrModal = useOcrMagic(); + const ttHModal = usettHMagic(); const translateModal = useTranslateMagicModal(); const navigate = useNavigate(); @@ -79,6 +82,10 @@ export function CommandModal() { Text Extractor (OCR) + ttHModal.onOpen({ text: "" })}> + + Text To Handwriting + imagineModal.onOpen()}> {t("commandbox.image_generation")} diff --git a/client/src/components/modals/ttH-magic-modal.tsx b/client/src/components/modals/ttH-magic-modal.tsx new file mode 100644 index 0000000..3358836 --- /dev/null +++ b/client/src/components/modals/ttH-magic-modal.tsx @@ -0,0 +1,136 @@ +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { usettHMagic } from "@/stores/modal-store"; +import { useEffect, useState } from "react"; +import { Button } from "../ui/button"; +import toast from "react-hot-toast"; +import { Textarea } from "../ui/textarea"; +import { Download, X } from "lucide-react"; +import { Label } from "../ui/label"; +import { Input } from "../ui/input"; +import axios from "axios"; +import { SERVER_URL } from "@/lib/utils"; + +const markdownToPlainText = (markdown: string) => { + return markdown + .replace(/(\*\*|__)(.*?)\1/g, "$2") // Bold + .replace(/(\*|_)(.*?)\1/g, "$2") // Italics + .replace(/~~(.*?)~~/g, "$1") // Strikethrough + .replace(/`{1,2}(.*?)`{1,2}/g, "$1") // Inline code + .replace(/### (.*?)\n/g, "$1\n") // H3 + .replace(/## (.*?)\n/g, "$1\n") // H2 + .replace(/# (.*?)\n/g, "$1\n") // H1 + .replace(/>\s?(.*?)(?=\n|$)/g, "$1") // Blockquote + .replace(/^\s*\n/g, "") // Remove empty lines + .replace(/\n+/g, "\n") // Consolidate newlines + .trim(); // Trim whitespace +}; + +export default function TtHMagicModal() { + const modal = usettHMagic(); + const initialText = markdownToPlainText(modal.extras.text || ""); + const [text, setText] = useState(""); + const [loading, setLoading] = useState(false); + const [fontSize, setFontSize] = useState(12); + + // Set initial text when the modal opens + useEffect(() => { + if (modal.isOpen) { + setText(initialText); // Set the initial text from modal extras + } + }, [modal.isOpen, initialText]); // Depend on modal open state and initial text + + // Function to download as PDF + const downloadAsPDF = async () => { + setLoading(true); + try { + const token = localStorage.getItem("token"); + + const authHeaders = { + Authorization: `Bearer ${token || ""}`, + }; + + const response = await axios.post( + `${SERVER_URL}/api/tth`, + { text, font_size: 12 }, + { responseType: "blob", headers: authHeaders } // Important to handle binary data + ); + + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", "handwritten_notes.pdf"); + document.body.appendChild(link); + link.click(); + link.remove(); + toast.success("PDF created successfully!"); + } catch (error) { + console.log("Error creating PDF:", error); + toast.error("Failed to create PDF."); + } finally { + setLoading(false); + } + }; + + return ( + modal.onClose()}> + + + + + Text-to-Handwriting Magic + modal.onClose()} + > + + + + + + Convert Text to Hand written Notes + + + setText(e.target.value)} + placeholder="Type your text here" + rows={5} + className="w-full p-2 border rounded" + /> + + + Font Size: + setFontSize(parseInt(e.target.value))} + min="16" + max="72" + step="1" + /> + + + + + {loading ? "Baking" : "Bake"} + + + + + ); +} diff --git a/client/src/contexts/modals.tsx b/client/src/contexts/modals.tsx index ef37f89..fa565fe 100644 --- a/client/src/contexts/modals.tsx +++ b/client/src/contexts/modals.tsx @@ -5,6 +5,7 @@ import OcrMagicModal from "@/components/modals/ocr-magic-modal"; import SettingsModal from "@/components/modals/settings-modal"; import ShareModal from "@/components/modals/share-modal"; import TranslateMagicModal from "@/components/modals/translate-magic-modal"; +import TtHMagicModal from "@/components/modals/ttH-magic-modal"; import TtsMagicModal from "@/components/modals/Tts-magic-modal"; import UpdateChatbotModal from "@/components/modals/update-chatbot-modal"; import UpdateProfileModal from "@/components/modals/update-profile-modal"; @@ -18,6 +19,7 @@ export default function Modals() { + diff --git a/client/src/index.css b/client/src/index.css index 379ff53..87a082d 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,5 +1,5 @@ @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); - +@import url("https://fonts.googleapis.com/css2?family=Reenie+Beanie&display=swap"); @tailwind base; @tailwind components; @tailwind utilities; diff --git a/client/src/pages/Chatbot.tsx b/client/src/pages/Chatbot.tsx index 63d0eb8..d0c50cb 100644 --- a/client/src/pages/Chatbot.tsx +++ b/client/src/pages/Chatbot.tsx @@ -34,6 +34,7 @@ import { useSettings } from "@/contexts/settings-context"; import { useSettingsModal, useTranslateMagicModal, + usettHMagic, useTtsMagicModal, } from "@/stores/modal-store"; import { @@ -63,6 +64,7 @@ export default function ChatbotPage() { const messageEl = useRef(null); const settingsModal = useSettingsModal(); const ttsMagicModal = useTtsMagicModal(); + const ttHMagicModal = usettHMagic(); const translateMagicModal = useTranslateMagicModal(); const { currentConfig } = useSettings(); const [loading, setLoading] = useState(false); @@ -325,6 +327,15 @@ export default function ChatbotPage() { > {t("chatbot_page.listen")} + + ttHMagicModal.onOpen({ + text: chat.response, + }) + } + > + Handwriting + diff --git a/client/src/stores/modal-store.ts b/client/src/stores/modal-store.ts index f2ab298..61223c6 100644 --- a/client/src/stores/modal-store.ts +++ b/client/src/stores/modal-store.ts @@ -11,3 +11,4 @@ export const useTtsMagicModal = create(defaultModalValues); export const useTranslateMagicModal = create(defaultModalValues); export const useImagineModal = create(defaultModalValues); export const useOcrMagic = create(defaultModalValues); +export const usettHMagic = create(defaultModalValues);
Text-to-Handwriting Magic