Repository navigation
Feature: share the Word heading font, metadata paragraph, and file response #554
Closed
branch1337
started this conversation in
Ideas
Replies: 1 comment 1 reply
|
Thank you for the detailed request. The heading font, the metadata paragraph and the Word file response are now shared functions in |
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Description
Summary, notes, and the chat on one recording each download a Word file. Each route builds that file by pasting the same two blocks.
download_summary_word(src/api/recordings.py:367-471),download_chat_word(src/api/recordings.py:477-609), anddownload_notes_word(src/api/recordings.py:615-718) each define a nestedadd_unicode_paragraph(src/api/recordings.py:400-410,src/api/recordings.py:515-525,src/api/recordings.py:647-657) and each set Arial plus the east-Asia font on the heading when the title is not ASCII (src/api/recordings.py:388-397,src/api/recordings.py:505-512,src/api/recordings.py:636-644). Each then saves aBytesIO, callssend_filewithas_attachment=False, and setsContent-Disposition, including the RFC 2231 branch (src/api/recordings.py:428-466,src/api/recordings.py:565-604,src/api/recordings.py:675-713).The markdown body is already one function,
process_markdown_to_docx(src/services/document.py:11-281). Its font helper,ensure_unicode_font, is nested inside that function (src/services/document.py:33-47) and is not callable from the three routes, so the routes carry a second copy of the same Arial and east-Asia assignment. A later Word download has no function to call for the heading, the metadata paragraph, or the file response. The workaround is to paste those blocks again.Each route also builds
ascii_filenameand never reads it (src/api/recordings.py:438-441,src/api/recordings.py:575-578,src/api/recordings.py:685-688). The header usesfilenamewhen that string is ASCII, andencode_rfc2231when it is not.Affected Version
v0.10.19-alpha
Requested Feature
Summary, notes, and chat downloads keep the files they return today. The heading font, the metadata paragraph font, and the Word file response exist once, in
src/services/document.py, and the three routes call them.add_unicode_heading(doc, text, level)adds a heading and, whentextis not ASCII, sets every run to Arial with east-Asia Arial.add_unicode_paragraph(doc, text)does the same for one paragraph. Both use oneensure_unicode_font(run, text).process_markdown_to_docxcalls that same function for its runs. Inline code keeps its own Consolas east-Asia font (src/services/document.py:66-79). A link run still goes throughensure_unicode_font(src/services/document.py:81-87).docx_attachment_response(doc, filename, fallback_filename)saves the document and returns the response.send_fileis called withas_attachment=Falseand the Word media type. An ASCIIfilenameproducesattachment; filename="<filename>". A non-ASCIIfilenameproducesattachment; filename*=<encode_rfc2231(filename, charset='utf-8')>and the same info log the routes write today. If that encoding raises, the header isattachment; filename="<fallback_filename>"and the error is logged. The unusedascii_filenamevalue is not computed.What stays on each route is the access check, the empty-body refusal, the heading string, the metadata lines, the filename built from the title, and the markdown source. The chat route still loops the posted messages itself, including the reasoning paragraph. There is no new environment variable and no change to the transcript download or the on-disk markdown export.
Proposed Implementation
process_markdown_to_docx(src/services/document.py:11) is the function that already sees every Word body. The font and the file response belong next to it.Before, each Word route carries its own heading font, its own metadata paragraph, and its own file response. The markdown body is the only shared call, and
ensure_unicode_fontis nested inside it, so the routes cannot call that font helper.flowchart TD summaryBefore[download_summary_word] notesBefore[download_notes_word] chatBefore["download_chat_word, message loop inside"] summaryFont[heading font and add_unicode_paragraph] notesFont[heading font and add_unicode_paragraph] chatFont[heading font and add_unicode_paragraph] summaryFile["BytesIO, send_file, Content-Disposition"] notesFile["BytesIO, send_file, Content-Disposition"] chatFile["BytesIO, send_file, Content-Disposition"] markdownBefore[process_markdown_to_docx] nestedFont["ensure_unicode_font, nested and not callable"] summaryBefore --> summaryFont summaryBefore --> summaryFile summaryBefore --> markdownBefore notesBefore --> notesFont notesBefore --> notesFile notesBefore --> markdownBefore chatBefore --> chatFont chatBefore --> chatFile chatBefore --> markdownBefore markdownBefore --> nestedFontAfter, the three routes call the shared helpers. The chat message loop stays inside
download_chat_word. Transcript text, the on-disk markdown export, and HTML rendering do not call them.flowchart TD summary[download_summary_word] notes[download_notes_word] chat["download_chat_word, message loop stays here"] heading[add_unicode_heading] paragraph[add_unicode_paragraph] font[ensure_unicode_font] markdown[process_markdown_to_docx] codeRun["add_code_run, Consolas"] response[docx_attachment_response] transcript[download_transcript_with_template] exporter[export_recording] html[md_to_html] summary --> heading summary --> paragraph summary --> markdown summary --> response notes --> heading notes --> paragraph notes --> markdown notes --> response chat --> heading chat --> paragraph chat --> markdown chat --> response heading --> font paragraph --> font markdown --> font markdown --> codeRuntranscript,exporter, andhtmlhave no arrow. They keep their own output. Extend the import atsrc/api/recordings.py:53so the three routes takeadd_unicode_heading,add_unicode_paragraph, anddocx_attachment_responsefromsrc.services.document. Do not add those names tosrc/services/__init__.py:30, and do not add them to the unused import atsrc/app.py:560. After the three call sites are gone, deletefrom email.utils import encode_rfc2231atsrc/api/recordings.py:23.quoteatsrc/api/recordings.py:24stays, because the transcript download still uses it (src/api/recordings.py:353).Delete the nested
ensure_unicode_fontatsrc/services/document.py:33-47. The calls insideprocess_markdown_to_docx(src/services/document.py:57-61,src/services/document.py:87,src/services/document.py:109,src/services/document.py:123) use the module function. Leaveadd_code_run(src/services/document.py:66-79) on Consolas.In each route, delete the nested helper, the heading font block, the
BytesIOsave, and the header block. Keep the title string, the metadata, the blank line before the body, and the filename regex.download_summary_word(src/api/recordings.py:382-467) becomes:download_notes_word(src/api/recordings.py:630-714) is the same shape withNotes:,recording.notes, thenotes-filename, and the same fallbackdownload-<id>.docx.download_chat_word(src/api/recordings.py:498-605) usesChat Conversation:, the two metadata lines atsrc/api/recordings.py:528-531, the message loop atsrc/api/recordings.py:533-563unchanged, thechat-filename, and the same fallback. Thefrom docx.shared import Inchesandfrom io import BytesIOlines inside the three functions go away.from docx import Documentstays inside thetry, so a failure while building the file is stillword_document_failed.Leave
download_transcript_with_template(src/api/recordings.py:206-360) on its text or markdown body and itsfilename*header (src/api/recordings.py:346-354). Leaveexport_recording(src/file_exporter.py:705-720) writing markdown to disk. Leavemd_to_html(src/utils/markdown.py:94-100) converting markdown to HTML. Do not merge summary and notes into one route. Do not move the chat message loop.Tests already pin the summary and chat responses:
test_download_summary_empty_400,test_download_summary_present_returns_docx,test_download_chat_empty_messages_400,test_download_chat_missing_key_400, andtest_download_chat_with_messages_returns_docxintests/test_cov_recordings_read.py.test_download_summary_present_returns_docx(tests/test_cov_recordings_read.py:1704-1720) andtest_download_chat_with_messages_returns_docx(tests/test_cov_recordings_read.py:1755-1776) return 500 ifas_attachmentbecomesTrue. Notes has no such test.test_download_endpoints_reject_non_owner(tests/test_cov_recordings_download_authz.py:44-79) only checks that the owner is not answered with 403. The recording it builds has a summary and no notes, so the owner request to/download/notesis the empty-notes 400 and never builds a file.Add
notes=Nonetomake_recording(tests/test_cov_recordings_read.py:158-176) and pass it toRecording. The default keeps every current caller on a row with no notes. Then addtest_download_notes_empty_400andtest_download_notes_present_returns_docxnext to the summary tests. The empty case uses the default,GET /recording/<id>/download/notes, expects 400, and expectsnotesinerror. The present case passesnotes="## Heading\n\nSome **notes** content.", expects 200, a content type containingwordprocessingml, and a non-empty body.no_notesis the English stringNo notes available for this recording(static/locales/en.json:3363).Add
tests/test_word_download_helpers.py. It importsappfromsrc.appthe waytests/test_cov_recordings_read.pydoes, and calls the helpers insideapp.test_request_context()becausesend_fileneeds a request. Build documents withdocx.Document.test_unicode_paragraph_sets_east_asia_font:add_unicode_paragraphwith预算sets the run font name toArialandw:eastAsiatoArial.test_ascii_paragraph_leaves_east_asia_unset:add_unicode_paragraphwithTags: Planningdoes not setw:eastAsia.test_unicode_heading_sets_east_asia_font:add_unicode_headingwithSummary: 预算at level 0 setsArialandw:eastAsiaArial.test_markdown_paragraph_uses_shared_font:process_markdown_to_docxwith预算setsw:eastAsiatoArialon that run.test_inline_code_keeps_consolas:process_markdown_to_docxwith`预算`setsw:eastAsiatoConsolason the code run.test_ascii_filename_disposition:docx_attachment_response(doc, 'summary-weekly.docx', 'download-4.docx')setsContent-Dispositiontoattachment; filename="summary-weekly.docx".test_non_ascii_filename_uses_rfc2231: filenamesummary-预算.docxsets the header toattachment; filename*=plusencode_rfc2231(filename, charset='utf-8').test_rfc2231_failure_uses_fallback: patchingencode_rfc2231to raise setsattachment; filename="download-4.docx".Run from
speakr.git:python -m pytest tests/test_word_download_helpers.py tests/test_cov_recordings_read.py::test_download_summary_empty_400 tests/test_cov_recordings_read.py::test_download_summary_present_returns_docx tests/test_cov_recordings_read.py::test_download_notes_empty_400 tests/test_cov_recordings_read.py::test_download_notes_present_returns_docx tests/test_cov_recordings_read.py::test_download_chat_empty_messages_400 tests/test_cov_recordings_read.py::test_download_chat_missing_key_400 tests/test_cov_recordings_read.py::test_download_chat_with_messages_returns_docx tests/test_cov_recordings_download_authz.py::test_download_endpoints_reject_non_ownerExpected Behavior
GET /recording/<id>/download/summarywith a summary still returns 200 and a Word body. With no summary it still returns 400 andno_summary. The heading is stillSummary: <title>, and the metadata lines are still uploaded time, recording date, participants, and visible tags.GET /recording/<id>/download/noteswith notes returns 200 and a Word body. With no notes it returns 400 andno_notes. The heading is stillNotes: <title>.POST /recording/<id>/download/chatwith messages still returns 200. An empty list and a missingmessageskey still return 400. The file still hasChat Conversation: <title>,Recording Date,Chat Export Date,YouandAssistant, the reasoning paragraph whenthinkingis set on an assistant turn, and a blank paragraph between turns.summary-<title>.docx,notes-<title>.docx, orchat-<title>.docx. A non-ASCII title still uses the RFC 2231 header and the same info log. A failure ofencode_rfc2231still falls back todownload-<id>.docx.process_markdown_to_docx. Inline code that is not ASCII still uses Consolas for east-Asia.GET /recording/<id>/download/transcriptstill returns text or markdown, with the header atsrc/api/recordings.py:346-354. A non-owner is still refused on all four download routes bytest_download_endpoints_reject_non_owner.Root Cause Analysis
The three Word routes were written as three copies of one download.
download_summary_word(src/api/recordings.py:367-471) inlines the font and the response.download_notes_word(src/api/recordings.py:615-718) repeats them.download_chat_word(src/api/recordings.py:477-609) repeats them and then adds the message loop.process_markdown_to_docx(src/services/document.py:11-281) was the part that got shared.ensure_unicode_fontstayed inside it (src/services/document.py:33-47), so the routes could not call it and grew their own copy.download_transcript_with_template(src/api/recordings.py:206-360) is the nearby download and is not this path. It returns text or markdown, and its non-ASCII header puts an ASCIIfilenameand afilename*value on the same line (src/api/recordings.py:350-354). The Word routes put onlyfilename*on that branch (src/api/recordings.py:459-462). Sharing one header function across both shapes would change one of them.Additional Context
as_attachmentstaysFalse. The summary and chat tests record thatTrueon aBytesIOwith no download name returns 500 (tests/test_cov_recordings_read.py:1704-1720,tests/test_cov_recordings_read.py:1755-1776). The helper is now the line those tests pin.No user-guide page changes. The downloaded files look the same. No new setting belongs in
config/env.asr.example.This request does not add the Inquire conversation download. That is #555, and it is implemented after this one because it calls these helpers. This request also does not share the chat turn loop. That loop stays in
download_chat_word(src/api/recordings.py:533-563) until the Inquire download is the second caller.All reactions